Download: Developer’s Guide Delphi™ 4

Developer’s Guide Borland® Delphi™ 4 for Windows 95 and Windows NT Inprise Corporation, 100 Enterprise Way Scotts Valley, CA 95066-3249 Refer to the file DEPLOY.TXT located in the root directory of your Delphi 4 product for a complete list of files that you can distribute in accordance with the No-Nonsense License Statement. Inprise may have patents and/or pending patent applications covering subject matter in this document. The furnishing of this document does not give you any license to these patents. COPYRIGHT © 1983–1998 Inprise Corporation. All rights reserved. All Inprise and Borland pro...
Author: Naheeda Kanaan Shared: 7/30/19
Downloads: 1312 Views: 3390

Content

Developer’s Guide

Borland®

Delphi™ 4

for Windows 95 and Windows NT Inprise Corporation, 100 Enterprise Way Scotts Valley, CA 95066-3249, Refer to the file DEPLOY.TXT located in the root directory of your Delphi 4 product for a complete list of files that you can distribute in accordance with the No-Nonsense License Statement. Inprise may have patents and/or pending patent applications covering subject matter in this document. The furnishing of this document does not give you any license to these patents. COPYRIGHT © 1983–1998 Inprise Corporation. All rights reserved. All Inprise and Borland products are trademarks or registered trademarks of Inprise Corporation. Other brand and product names are trademarks or registered trademarks of their respective holders. Printed in the U.S.A. HDA1340WW21001 2E1R698 9899000102-987654321D4,

Contents Chapter 1 Tab-jumping to property names Introduction 1-1 in the Object Inspector.2-36

What’s in this manual? .1-1 Displaying and setting shared Manual conventions .1-2 properties .2-37 Inprise developer support services .1-3 Using property editors .2-37 Ordering printed documentation .1-3 Setting properties at runtime .2-38 Calling methods .2-38

Part I Working with event handlers .2-39 Programming with Delphi Generating the default event handler .2-39Generating a new event handler .2-39

Locating event handlers.2-40

Chapter 2 Associating an event with an existing Using Object Pascal with the VCL 2-1 event handler .2-40

Understanding the distinction between Associating menu events with code .2-43 Object Pascal and the VCL .2-1 Using helper objects .2-44 Using the object model .2-2 Working with string lists .2-44 What is an object? .2-2 Manipulating strings in a list .2-45 Examining a Delphi object .2-3 Loading and saving string lists.2-48 Inheriting data and code from an object .2-6 Creating a new string list .2-48 Objects, components, and controls .2-8 Adding objects to a string list .2-50 Object scope.2-8 Operating on objects in a string list .2-51 Accessing components on another The registry and Windows INI files .2-52 form .2-9 Using streams .2-52 Scope and descendants of an object.2-10 Using data modules and remote data Accessing object fields and methods .2-11 modules .2-52 Assigning values to object variables .2-12 Creating a new data module .2-53 Creating nonvisual objects .2-14 Naming a data module and its Creating an instance of an object .2-15 unit file .2-53 Destroying your object .2-15 Placing and naming components .2-54 Using components .2-16 Using component properties and Understanding the VCL .2-16 methods in a data module .2-54 Delphi’s standard components .2-17 Creating business rules in a Choosing the right component .2-18 data module .2-55 Properties common to all components .2-18 Adding a remote data module to an Text controls .2-20 application server project .2-55 Specialized input controls .2-21 Accessing a data module from a form .2-56 Buttons and similar controls .2-24 Using the Object Repository .2-56 Handling lists .2-26 Sharing forms, dialogs, data modules, Grouping components .2-28 and projects .2-57 Visual feedback .2-31 Adding items to the Object Repository .2-57 Tabular display .2-32 Sharing objects in a team environment .2-57 Graphic display .2-34 Using an Object Repository item in a Windows common dialog boxes .2-35 project .2-58 Setting component properties.2-36 Copying an item .2-58 How the Object Inspector Inheriting an item .2-58 displays properties .2-36 Using an item.2-58 i, Using project templates .2-59 Handling RTL exceptions.4-5 Modifying a shared form .2-59 What are the RTL exceptions? .4-6 Specifying a default project, new form, Creating an exception handler .4-6 and main form .2-60 Exception handling statements.4-7 Adding custom components to the IDE .2-60 Using the exception instance .4-8 Understanding the difference between Scope of exception handlers .4-8 Install Component and Install Package .2-60 Providing default exception handlers .4-9 Install Component .2-60 Handling classes of exceptions .4-9 Install Package .2-60 Reraising the exception .4-10 Installing components .2-61 Handling component exceptions .4-10 Using TApplication.HandleException .4-11

Chapter 3 Silent exceptions .4-12 Building applications, components, Defining your own exceptions .4-12

and libraries 3-1 Declaring an exception object type.4-13 Creating applications .3-1 Raising an exception.4-13 Windows 95/NT EXEs.3-2 Using interfaces .4-13 User interface models .3-2 Interfaces as a language feature .4-14 Setting IDE, project, and compilation Sharing interfaces between classes.4-14 options .3-3 Using interfaces with procedures .4-16 Console applications .3-3 Implementing IUnknown .4-16 Service applications .3-3 TInterfacedObject .4-17 Service threads .3-5 Using the as operator .4-17 Service name properties .3-7 Reusing code and delegation.4-18 Creating packages and DLLs .3-8 Using implements for delegation .4-18 When to use packages and DLLs .3-8 Aggregation .4-19 Programming templates .3-8 Memory management of interface Building distributed applications .3-9 objects .4-20 Distributing applications using TCP/IP .3-9 Using reference counting .4-20 Using sockets in applications .3-9 Not using reference counting.4-21 Creating Web server applications .3-10 Using interfaces in distributed Distributing applications using COM applications .4-22 and DCOM .3-10 Working with strings .4-22 COM and ActiveX .3-10 Character types .4-23 MTS .3-11 String types .4-23 Distributing applications using CORBA.3-11 Short strings .4-23 Distributing database applications .3-11 Long strings .4-24 WideString .4-25

Chapter 4 PChar types.4-25 Common programming tasks 4-1 OpenString .4-25Runtime library string handling routines.4-25

Handling exceptions.4-1 Wide character routines .4-26 Protecting blocks of code .4-1 Commonly used long string routines .4-26 Responding to exceptions .4-2 Declaring and initializing strings .4-28 Exceptions and the flow of control .4-2 Mixing and converting string types .4-29 Nesting exception responses .4-3 String to PChar conversions .4-29 Protecting resource allocations .4-3 String dependencies .4-30 What kind of resources need Returning a PChar local variable.4-30 protection? .4-4 Passing a local variable as a PChar .4-30 Creating a resource protection block .4-4 Compiler directives for strings.4-31 ii, Related topics .4-32 Creating and managing menus .5-11 International character sets .4-32 Opening the Menu Designer .5-12 Character arrays .4-32 Building menus .5-13 Character strings .4-32 Naming menus.5-14 Character pointers .4-32 Naming the menu items .5-14 String operators .4-32 Adding, inserting, and deleting Working with files .4-33 menu items .5-15 Manipulating files .4-33 Creating submenus .5-16 Deleting a file .4-33 Viewing the menu .5-18 Finding a file .4-33 Editing menu items in the Object Changing file attributes .4-35 Inspector .5-18 Renaming a file .4-35 Using the Menu Designer context menu .5-19 File date-time routines .4-36 Commands on the context menu.5-19 Copying a file .4-36 Switching between menus at design File types with file I/O .4-36 time .5-19 Using file streams .4-37 Using menu templates .5-20 Creating and opening files .4-37 Saving a menu as a template .5-21 Using the file Handle .4-38 Naming conventions for template Reading and writing to files .4-38 menu items and event handlers .5-22 Reading and writing strings .4-39 Manipulating menu items at runtime .5-23 Seeking a file .4-39 Merging menus .5-23 File position and size .4-40 Specifying the active menu: Copying .4-40 Menu property .5-23 Determining the order of merged

Chapter 5 menu items: GroupIndex property.5-23 Developing the application user Importing resource files .5-24

interface 5-1 Designing toolbars and cool bars .5-25 Understanding TApplication, TScreen, Adding a toolbar using a panel and TForm .5-1 component .5-26 Using the main form .5-1 Adding a speed button to a panel .5-26 Adding additional forms .5-2 Assigning a speed button’s glyph .5-26 Linking forms .5-2 Setting the initial condition of a Working at the application level .5-3 speed button .5-27 Handling the screen .5-3 Creating a group of speed buttons .5-27 Managing layout .5-3 Allowing toggle buttons .5-27 Working with messages .5-4 Adding a toolbar using the toolbar More details on forms .5-4 component .5-28 Controlling when forms reside in Adding a tool button .5-28 memory .5-5 Assigning images to tool buttons .5-28 Displaying an auto-created form .5-5 Setting tool button appearance and Creating forms dynamically .5-5 initial conditions .5-29 Creating modeless forms such as Creating groups of tool buttons .5-29 windows .5-6 Allowing toggled tool buttons .5-29 Using a local variable to create a form Adding a cool bar component .5-30 instance.5-6 Setting the appearance of the cool bar .5-30 Passing additional arguments to forms .5-7 Responding to clicks .5-30 Retrieving data from forms .5-8 Writing an event handler for a Retrieving data from modeless forms .5-8 button click .5-31 Retrieving data from modal forms .5-9 Assigning a menu to a tool button .5-31 iii, Adding hidden toolbars .5-31 Handling the OnPopup event .6-12 Hiding and showing toolbars .5-31 Adding graphics to controls .6-13 Using action lists .5-32 Setting the owner-draw style.6-13 Action objects .5-32 Adding graphical objects to a string list .6-14 Using actions .5-33 Adding images to an application .6-14 Centralizing code .5-34 Adding images to a string list .6-14 Linking properties .5-34 Drawing owner-drawn items.6-15 Executing actions .5-34 Sizing owner-draw items .6-15 Updating actions .5-36 Drawing each owner-draw item .6-16 Pre-defined action classes .5-36 Standard edit actions .5-36 Chapter 7 Standard Window actions .5-37 Working with graphics 7-1 DataSet actions .5-37 Overview of graphics programming .7-1 Writing action components .5-38 Common properties and methods of How actions find their targets .5-38 Canvas .7-2 Registering actions .5-39 Refreshing the screen .7-3 Writing action list editors .5-40 When graphic images appear in the Demo programs .5-40 application .7-4 Types of graphic objects .7-4

Chapter 6 Using the properties of the Canvas object .7-5 Working with controls 6-1 Using pens .7-5

Implementing drag-and-drop in controls .6-1 Changing the pen color .7-5 Starting a drag operation .6-1 Changing the pen width .7-5 Accepting dragged items .6-2 Changing the pen style .7-6 Dropping items .6-3 Changing the pen mode.7-6 Ending a drag operation.6-3 Getting the pen position .7-7 Customizing drag and drop with Using brushes .7-7 TDragObject.6-4 Changing the brush color .7-7 Changing the drag mouse pointer .6-4 Changing the brush style .7-7 Implementing drag-and-dock in controls .6-5 Setting the Brush Bitmap property.7-8 Making a windowed control a docking Reading and setting pixels .7-8 site .6-5 Using Canvas methods to draw graphic Making a control a dockable child control.6-5 objects .7-9 Controlling how child controls are Drawing lines and polylines .7-9 docked in a docking site .6-6 Drawing lines .7-9 Controlling how child controls are Drawing polylines .7-9 undocked in a docking site .6-6 Drawing shapes .7-10 Controlling how child controls respond Drawing rectangles and ellipses .7-10 to drag-and-dock operations .6-7 Drawing rounded rectangles .7-10 Working with text in controls.6-7 Drawing polygons .7-11 Setting text alignment .6-8 Handling multiple drawing objects in your Adding scroll bars at runtime.6-8 application .7-11 Adding the Clipboard object .6-9 Keeping track of which drawing tool Selecting text .6-9 to use .7-11 Selecting all text .6-10 Changing the tool with speed buttons .7-12 Cutting, copying, and pasting text .6-10 Using drawing tools.7-13 Deleting selected text .6-11 Drawing shapes .7-13 Disabling menu items .6-11 Sharing code among event handlers.7-14 Providing a pop-up menu .6-12 Drawing on a graphic .7-16 iv, Making scrollable graphics .7-16 Using critical sections .9-7 Adding an image control .7-16 Using the multi-read exclusive-write Placing the control .7-16 synchronizer .9-7 Setting the initial bitmap size .7-17 Other techniques for sharing memory.9-8 Drawing on the bitmap .7-17 Waiting for other threads .9-8 Loading and saving graphics files .7-18 Waiting for a thread to finish Loading a picture from a file .7-18 executing .9-8 Saving a picture to a file .7-19 Waiting for a task to be completed.9-9 Replacing the picture .7-19 Executing thread objects .9-10 Using the Clipboard with graphics .7-20 Overriding the default priority .9-10 Copying graphics to the Clipboard.7-21 Starting and stopping threads .9-11 Cutting graphics to the Clipboard .7-21 Caching threads .9-11 Pasting graphics from the Clipboard.7-21 Using threads in distributed applications .9-12 Rubber banding example .7-22 Using threads in message-based servers .9-12 Responding to the mouse .7-22 Using threads with distributed objects .9-13 What’s in a mouse event? .7-23 Writing applications (.EXEs) .9-13 Responding to a mouse-down action.7-23 Writing Libraries .9-13 Responding to a mouse-up action .7-24 Debugging multi-threaded applications .9-14 Responding to a mouse move .7-24 Adding a field to a form object to track Chapter 10 mouse actions .7-25 Working with packages and Refining line drawing .7-26 components 10-1 Tracking the origin point.7-26 Why use packages? .10-2 Tracking movement .7-27 Packages and standard DLLs .10-2

Chapter 8 Runtime packages .10-2 Working with multimedia 8-1 Using packages in an application .10-3Dynamically loading packages .10-4

Adding silent video clips to an application .8-1 Deciding which runtime packages to use.10-4 Example of adding silent video clips.8-2 Custom packages .10-5 Adding audio and/or video clips to an Design-time packages .10-5 application .8-3 Installing component packages .10-6 Example of adding audio and/or Creating and editing packages .10-7 video clips .8-5 Creating a package .10-8

Chapter 9 Editing an existing package .10-8Editing package source files manually .10-9 Writing multi-threaded applications 9-1 Understanding the structure of a

Defining thread objects .9-1 package .10-9 Initializing the thread .9-2 Naming packages .10-9 Assigning a default priority .9-2 The Requires clause .10-9 Indicating when threads are freed .9-3 The Contains clause .10-10 Writing the thread function .9-3 Compiling packages .10-10 Using the main VCL thread .9-4 Package-specific compiler directives . .10-11 Using thread-local variables .9-5 Using the command-line compiler Checking for termination by other and linker .10-12 threads .9-5 Package files created by a successful Writing clean-up code .9-6 compilation .10-13 Coordinating threads .9-6 Deploying packages .10-13 Avoiding simultaneous access .9-6 Deploying applications that use Locking objects .9-6 packages .10-13 v, Distributing packages to other Deploying Web applications .12-7 developers .10-13 Programming for varying host Package collection files .10-13 environments .12-7 Screen resolutions and color depths.12-7

Chapter 11 Considerations when not Creating international applications 11-1 dynamically resizing.12-8

Internationalization and localization .11-1 Considerations when dynamically Internationalization .11-1 resizing forms and controls .12-8 Localization .11-1 Accommodating varying color depths . 12-9 Internationalizing applications .11-2 Fonts .12-9 Enabling application code .11-2 Windows versions.12-10 Character sets .11-2 Software license requirements .12-10 OEM and ANSI character sets.11-2 DEPLOY.TXT ..12-11 Double byte character sets .11-2 README.TXT ..12-11 Wide characters .11-3 No-nonsense license agreement .12-12 Including bi-directional functionality Third-party product documentation .12-12 in applications .11-4 BiDiMode property .11-5 Part II Locale-specific features.11-7 Developing database applications Designing the user interface.11-8 Text .11-8 Chapter 13 Graphic images .11-8 Designing database applications 13-1 Formats and sort order .11-9 Using databases .13-1 Keyboard mappings .11-9 Types of databases.13-2 Isolating resources .11-9 Local databases .13-2 Creating resource DLLs .11-9 Remote database servers .13-2 Using resource DLLs .11-11 Database security .13-3 Dynamic switching of resource DLLs .11-12 Transactions .13-3 Localizing applications .11-12 The Data Dictionary.13-4 Localizing resources .11-12 Referential integrity, stored procedures,

Chapter 12 and triggers.13-5

Deploying applications 12-1 Database architecture.13-5Planning for scalability .13-6 Deploying general applications .12-1 Single-tiered database applications .13-8 Using installation programs.12-2 Two-tiered database applications .13-8 Application files, listed by file name Multi-tiered database applications .13-9 extension .12-2 Designing the user interface ..13-11 Package files .12-2 Displaying a single record ..13-11 ActiveX controls .12-3 Displaying multiple records .13-12 Helper applications.12-3 Analyzing data .13-12 DLL locations .12-3 Selecting what data to show .13-13 Deploying database applications.12-4 Writing reports.13-14 Providing the database engine .12-4 Borland Database Engine .12-4 Chapter 14 Third-party database engines .12-5 Building one- and two-tiered SQL Links .12-5 Multi-tiered Distributed Application applications 14-1 Services (MIDAS) .12-5 BDE-based applications .14-1 BDE-based architecture .14-2 vi, Understanding databases and Creating a data provider for the datasets.14-2 application server .15-15 Using sessions.14-3 Controlling what information is Connecting to databases .14-4 included in data packets .15-15 Using transactions .14-4 Responding to client data requests .15-17 Explicitly controlling transactions .14-5 Responding to client update Using a database component for requests .15-18 transactions .14-6 Editing delta packets before Using the TransIsolation property .14-6 updating the database .15-19 Using passthrough SQL .14-8 Influencing how updates are applied . 15-19 Using local transactions .14-8 Screening individual updates .15-21 Caching updates .14-9 Resolving update errors on the Creating and restructuring database provider .15-21 tables .14-10 Responding to client-generated Flat-file database applications .14-10 events .15-21 Creating the datasets .14-11 Handling server constraints .15-22 Creating a new dataset using Creating the client application.15-23 persistent fields .14-11 Connecting to the application server .15-24 Creating a dataset using field and Specifying a connection using DCOM. 15-24 index definitions .14-11 Specifying a connection using sockets. 15-25 Creating a dataset based on an Specifying a connection using existing table.14-13 OLEnterprise .15-26 Loading and saving data .14-13 Specifying a connection using CORBA 15-26 Using the briefcase model .14-14 Brokering connections.15-26 Scaling up to a three-tiered application .14-15 Managing server connections .15-27 Connecting to the server .15-27

Chapter 15 Dropping or changing a server Creating multi-tiered applications 15-1 connection .15-28

Advantages of the multi-tiered database Calling server interfaces .15-28 model .15-2 Handling constraints .15-29 Understanding MIDAS technology .15-2 Updating records .15-30 Overview of a MIDAS-based Applying updates .15-31 multi-tiered application .15-3 Reconciling update errors.15-31 The structure of the client application .15-4 Refreshing records.15-32 The structure of the application server.15-5 Getting parameters from the Using MTS.15-5 application server .15-33 Using the IDataBroker interface.15-6 Customizing application servers .15-33 Using the IProvider interface .15-7 Extending the application server’s Choosing a connection protocol .15-7 interface.15-33 Using DCOM connections .15-8 Providing from and resolving to a Using Socket connections .15-8 dataset .15-35 Using OLEnterprise .15-8 Managing transactions in multi-tiered Using CORBA connections .15-9 applications .15-35 Building a multi-tiered application .15-9 Supporting master/detail relationships .15-36 Creating the application server.15-10 Supporting stateless remote data modules.15-37 Setting up the remote data module.15-11 Distributing a client application as an Configuring TRemoteDataModule .15-12 ActiveX control .15-38 Configuring TMTSDataModule.15-13 Creating an Active Form for the client Configuring TCorbaDataModule .15-14 application .15-39 vii,

Chapter 16 Creating database components at design Managing database sessions 16-1 time .17-2

Working with a session component .16-1 Creating database components at Using the default session .16-2 runtime .17-2 Creating additional sessions .16-3 Controlling connections .17-4 Naming a session.16-4 Associating a database component with Activating a session .16-5 a session .17-4 Customizing session start-up .16-6 Specifying a BDE alias .17-4 Specifying default database connection Setting BDE alias parameters .17-5 behavior .16-6 Controlling server login .17-6 Creating, opening, and closing database Connecting to a database server .17-6 connections .16-6 Special considerations when connecting Closing a single database connection.16-7 to a remote server .17-7 Closing all database connections .16-7 Working with network protocols.17-7 Dropping temporary database Using ODBC .17-8 connections .16-8 Disconnecting from a database server .17-8 Searching for a database connection .16-8 Closing datasets without disconnecting Retrieving information about a session .16-8 from a server .17-8 Working with BDE aliases .16-9 Iterating through a database component’s Specifying alias visibility.16-10 datasets .17-8 Making session aliases visible to Understanding database and session other sessions and applications .16-10 component interactions.17-9 Determining known aliases, drivers, Using database components in data and parameters .16-10 modules .17-9 Creating, modifying, and deleting Chapter 18 aliases .16-11 Iterating through a session’s database Understanding datasets 18-1 components .16-12 What is TDataSet?.18-2 Specifying Paradox directory locations .16-13 Types of datasets .18-2 Specifying the control file location .16-13 Opening and closing datasets .18-3 Specifying a temporary files location .16-13 Determining and setting dataset states .18-3 Working with password-protected Inactivating a dataset .18-5 Paradox and dBase tables .16-13 Browsing a dataset .18-6 Using the AddPassword method .16-14 Enabling dataset editing .18-7 Using the RemovePassword and Enabling insertion of new records .18-7 RemoveAllPasswords methods .16-14 Enabling index-based searches and Using the GetPassword method and ranges on tables .18-8 OnPassword event .16-15 Calculating fields .18-8 Managing multiple sessions .16-16 Filtering records .18-9 Using a session component in data Updating records .18-9 modules .16-17 Navigating datasets.18-9 Using the First and Last methods .18-10

Chapter 17 Using the Next and Prior methods .18-10 Connecting to databases 17-1 Using the MoveBy method ..18-11

Understanding persistent and Using the Eof and Bof properties ..18-11 temporary database components.17-1 Eof ..18-11 Using temporary database components .17-2 Bof .18-12 viii, Marking and returning to records .18-13 Programming a calculated field .19-9 Searching datasets .18-14 Defining a lookup field .19-10 Using Locate .18-14 Defining an aggregate field ..19-11 Using Lookup.18-15 Deleting persistent field components .19-12 Displaying and editing a subset of data Setting persistent field properties and using filters .18-16 events .19-12 Enabling and disabling filtering .18-16 Setting display and edit properties at Creating filters .18-17 design time .19-12 Setting the Filter property .18-17 Setting field component properties at Writing an OnFilterRecord event runtime .19-14 handler.18-18 Creating attribute sets for field Switching filter event handlers at components .19-14 runtime.18-19 Associating attribute sets with field Setting filter options .18-19 components .19-15 Navigating records in a filtered dataset .18-19 Removing attribute associations .19-15 Modifying data.18-20 Controlling and masking user input .19-15 Editing records .18-20 Using default formatting for numeric, Adding new records .18-21 date, and time fields .19-16 Inserting records .18-22 Handling events .19-17 Appending records .18-22 Working with field component methods at Deleting records .18-22 runtime .19-17 Posting data to the database .18-23 Displaying, converting, and accessing Canceling changes .18-23 field values.19-18 Modifying entire records .18-23 Displaying field component values in Using dataset events.18-25 standard controls .19-18 Aborting a method .18-25 Converting field values .19-19 Using OnCalcFields .18-25 Accessing field values with the default Using BDE-enabled datasets .18-26 dataset property .19-20 Overview of BDE-enablement .18-27 Accessing field values with a dataset’s Handling database and session Fields property.19-20 connections .18-27 Accessing field values with a dataset’s Using the DatabaseName and FieldByName method.19-21 SessionName properties .18-28 Checking a field’s current value.19-21 Working with BDE handle Setting a default value for a field .19-21 properties .18-28 Working with constraints .19-22 Working with a provider component .18-29 Creating a custom constraint.19-22 Working with cached updates .18-29 Using server constraints .19-22 Caching BLOBs .18-30 Using object fields .19-23 Displaying ADT and array fields .19-24

Chapter 19 Working with ADT fields.19-24 Working with field components 19-1 Accessing ADT field values.19-24

Understanding field components .19-2 Working with array fields .19-25 Dynamic field components .19-3 Accessing array field values .19-26 Persistent field components .19-4 Working with dataset fields .19-26 Creating persistent fields .19-5 Displaying dataset fields .19-27 Arranging persistent fields .19-6 Accessing data in a nested dataset .19-27 Defining new persistent fields .19-7 Working with reference fields .19-27 Defining a data field .19-8 Displaying reference fields .19-27 Defining a calculated field.19-8 Accessing data in a reference field .19-27 ix,

Chapter 20 Renaming a table .20-16 Working with tables 20-1 Creating a table .20-17

Using table components.20-1 Importing data from another table .20-18 Setting up a table component.20-2 Using TBatchMove .20-19 Specifying a database location .20-2 Creating a batch move component .20-20 Specifying a table name .20-3 Specifying a batch move mode .20-21 Specifying the table type for local tables.20-3 Appending .20-21 Opening and closing a table.20-4 Updating .20-21 Controlling read/write access to a table.20-4 Appending and updating.20-21 Searching for records .20-5 Copying.20-21 Searching for records based on indexed Deleting .20-22 fields .20-6 Mapping data types .20-22 Executing a search with Goto Executing a batch move.20-23 methods .20-6 Handling batch move errors .20-23 Executing a search with Find Synchronizing tables linked to the same methods .20-7 database table .20-24 Specifying the current record after a Creating master/detail forms .20-24 successful search .20-7 Building an example master/detail Searching on partial keys .20-8 form .20-25 Searching on alternate indexes .20-8 Working with nested tables .20-26 Repeating or extending a search .20-8 Setting up a nested table component .20-26 Sorting records .20-9 Chapter 21 Retrieving a list of available indexes with GetIndexNames.20-9 Working with queries 21-1 Specifying an alternative index with Using queries effectively .21-1 IndexName .20-9 Queries for desktop developers .21-2 Specifying a dBASE index file .20-9 Queries for server developers .21-3 Specifying sort order for SQL tables .20-10 What databases can you access with a Specifying fields with query component? .21-4 IndexFieldNames .20-10 Using a query component: an overview .21-4 Examining the field list for an index .20-10 Specifying the SQL statement to execute .21-5 Working with a subset of data .20-11 Specifying the SQL property at design Understanding the differences time .21-6 between ranges and filters.20-11 Specifying an SQL statement at runtime .21-7 Creating and applying a new range .20-12 Setting the SQL property directly .21-7 Setting the beginning of a range .20-12 Loading the SQL property from a file .21-8 Setting the end of a range .20-13 Loading the SQL property from Setting start- and end-range values .20-13 string list object.21-8 Specifying a range based on partial Setting parameters .21-8 keys .20-14 Supplying parameters at design time .21-9 Including or excluding records that Supplying parameters at runtime .21-10 match boundary values .20-14 Using a data source to bind parameters . 21-10 Applying a range .20-14 Executing a query .21-12 Canceling a range .20-15 Executing a query at design time .21-12 Modifying a range .20-15 Executing a query at runtime .21-12 Editing the start of a range.20-15 Executing a query that returns a Editing the end of a range .20-16 result set.21-13 Deleting all records in a table .20-16 Executing a query without a Deleting a table.20-16 result set.21-13 x, Preparing a query .21-13 Setting parameter information at design Unpreparing a query to release resources .21-14 time .22-13 Creating heterogeneous queries .21-14 Creating parameters at runtime .22-14 Improving query performance .21-15 Binding parameters .22-15 Disabling bi-directional cursors.21-15 Viewing parameter information at design Working with result sets .21-16 time .22-15 Enabling editing of a result set .21-16 Working with Oracle overloaded stored Local SQL requirements for a live procedures .22-16 result set .21-16 Restrictions on live queries .21-17 Chapter 23 Remote server SQL requirements for Creating and using a client dataset 23-1 a live result set .21-17 Working with data using a client dataset.23-2 Restrictions on updating a live result Navigating data in client datasets .23-2 set.21-17 Limiting what records appear .23-2 Updating a read-only result set .21-17 Representing master/detail relationships .23-3

Chapter 22 Constraining data values .23-3 Working with stored procedures 22-1 Making data read-only .23-4

When should you use stored procedures? .22-2 Editing data .23-4 Using a stored procedure .22-2 Undoing changes .23-5 Creating a stored procedure component.22-3 Saving changes .23-5 Creating a stored procedure.22-4 Sorting and indexing .23-6 Preparing and executing a stored Adding a new index .23-6 procedure .22-5 Deleting and switching indexes .23-7 Using stored procedures that return Using indexes to group data .23-7 result sets .22-5 Indexing on the fly.23-8 Retrieving a result set with a TQuery .22-5 Representing calculated values .23-8 Retrieving a result set with a Using internally calculated fields in TStoredProc .22-6 client datasets.23-8 Using stored procedures that return Using maintained aggregates .23-9 data using parameters .22-7 Specifying aggregates .23-9 Retrieving individual values with Aggregating over groups of records .23-10 a TQuery .22-7 Obtaining aggregate values..23-11 Retrieving individual values with a Adding application-specific TStoredProc .22-7 information to the data .23-12 Using stored procedures that perform Copying data from another dataset .23-12 actions on data .22-8 Assigning data directly .23-12 Executing an action stored procedure Cloning a client dataset cursor.23-13 with a TQuery .22-8 Using a client dataset with a data provider . 23-13 Executing an action stored procedure Specifying a data provider .23-13 with a TStoredProc .22-9 Passing parameters to the application Understanding stored procedure server .23-14 parameters .22-10 Sending query or stored procedure Using input parameters .22-10 parameters .23-15 Using output parameters .22-11 Limiting records with parameters .23-15 Using input/output parameters .22-11 Requesting data from an application Using the result parameter .22-12 server .23-15 Accessing parameters at design time.22-12 Applying updates to the database.23-16 xi, Using a client dataset with flat-file data .23-17 Using dataset components to update Creating a new dataset.23-17 a dataset .24-20 Loading data from a file or stream .23-18 Updating a read-only result set .24-21 Merging changes into Data .23-18 Controlling the update process .24-21 Saving data to a file or stream.23-18 Determining if you need to control the updating process.24-22

Chapter 24 Creating an OnUpdateRecord event Working with cached updates 24-1 handler .24-22

Deciding when to use cached updates.24-1 Handling cached update errors .24-23 Using cached updates .24-2 Referencing the dataset to which to Enabling and disabling cached updates .24-3 apply updates .24-24 Fetching records .24-3 Indicating the type of update that Applying cached updates .24-4 generated an error .24-24 Applying cached updates with a Specifying the action to take .24-25 database component method .24-5 Working with error message text .24-25 Applying cached updates with Accessing a field’s OldValue, NewValue, dataset component methods .24-6 and CurValue properties .24-26 Applying updates for master/detail tables .24-6 Chapter 25 Canceling pending cached updates .24-7 Using data controls 25-1 Cancelling pending updates and Using common data control features .25-1 disabling further cached updates .24-8 Associating a data control with a dataset .25-2 Canceling pending cached updates.24-8 Editing and updating data .25-3 Canceling updates to the current Enabling editing in controls on user record.24-8 entry .25-3 Undeleting cached records .24-8 Editing data in a control.25-3 Specifying visible records in the cache .24-9 Disabling and enabling data display .25-4 Checking update status .24-10 Refreshing data display.25-5 Using update objects to update a dataset .24-11 Enabling mouse, keyboard, and timer Specifying the UpdateObject property events .25-5 for a dataset .24-11 Using data sources .25-5 Using a single update object .24-12 Using TDataSource properties .25-6 Using multiple update objects.24-12 Setting the DataSet property .25-6 Creating SQL statements for update Setting the Name property .25-6 components .24-13 Setting the Enabled property .25-7 Creating SQL statements at Setting the AutoEdit property .25-7 design time .24-13 Using TDataSource events .25-7 Understanding parameter Using the OnDataChange event .25-7 substitution in update SQL Using the OnUpdateData event .25-7 statements .24-14 Using the OnStateChange event .25-7 Composing update SQL statements .24-15 Controls that represent a single field .25-8 Using an update component’s Displaying data as labels .25-8 Query property .24-16 Displaying and editing fields in an Using the DeleteSQL, InsertSQL, and edit box .25-9 ModifySQL properties .24-17 Displaying and editing text in a memo Executing update statements .24-18 control.25-9 Calling the Apply method .24-18 Displaying and editing text in a rich edit Calling the SetParams method .24-18 memo control .25-10 Calling the ExecSQL method .24-19 xii, Displaying and editing graphics fields Navigating and manipulating records .25-29 in an image control .25-10 Choosing navigator buttons to display .25-29 Displaying and editing data in list and Hiding and showing navigator combo boxes.25-11 buttons at design time .25-30 Displaying and editing data in a Hiding and showing navigator list box .25-11 buttons at runtime .25-30 Displaying and editing data in a Displaying fly-over help .25-31 combo box .25-12 Using a single navigator for multiple Displaying and editing data in lookup datasets .25-31 list and combo boxes .25-12 Specifying a list based on a lookup Chapter 26 field .25-13 Using decision support Specifying a list based on a components 26-1 secondary data source .25-13 Overview .26-1 Setting lookup list and combo box About crosstabs .26-2 properties .25-14 One-dimensional crosstabs .26-2 Searching incrementally for list item Multidimensional crosstabs .26-3 values.25-14 Guidelines for using decision support Handling Boolean field values with components .26-3 check boxes .25-14 Using datasets with decision support Restricting field values with radio components .26-4 controls .25-15 Creating decision datasets with TQuery Viewing and editing data with TDBGrid .25-16 or TTable .26-5 Using a grid control in its default state .25-17 Creating decision datasets with the Creating a customized grid .25-17 Decision Query editor.26-6 Understanding persistent columns .25-18 Using the Decision Query editor .26-6 Determining the source of a column Decision query properties .26-7 property at runtime .25-18 Using decision cubes .26-7 Creating persistent columns .25-19 Decision cube properties and events .26-7 Deleting persistent columns .25-20 Using the Decision Cube editor .26-7 Arranging the order of persistent Viewing and changing dimension columns .25-20 settings .26-8 Defining a lookup list column.25-20 Setting the maximum available Defining a pick list column .25-20 dimensions and summaries .26-8 Putting a button in a column .25-21 Viewing and changing design options . 26-8 Setting column properties at design Using decision sources .26-9 time .25-21 Properties and events .26-9 Restoring default values to a Using decision pivots.26-9 column .25-22 Decision pivot properties .26-10 Displaying ADT and array fields .25-22 Creating and using decision grids .26-10 Setting grid options .25-24 Creating decision grids .26-10 Editing in the grid .25-25 Using decision grids ..26-11 Rearranging column order at design Opening and closing decision grid time.25-25 fields ..26-11 Rearranging column order at runtime .25-26 Reorganizing rows and columns in Controlling grid drawing .25-26 decision grids..26-11 Responding to user actions at runtime.25-26 Drilling down for detail in decision Creating a grid that contains other grids ..26-11 data-aware controls .25-27 xiii, Limiting dimension selection in Customizing CORBA applications .27-14 decision grids .26-12 Displaying objects in the user interface .27-14 Decision grid properties .26-12 Exposing and hiding CORBA objects .27-15 Creating and using decision graphs .26-13 Passing client information to server Creating decision graphs .26-13 objects .27-15 Using decision graphs .26-13 Deploying CORBA applications.27-16 The decision graph display .26-15 Configuring Smart Agents .27-17 Customizing decision graphs .26-15 Starting the Smart Agent .27-17 Setting decision graph template Configuring ORB domains .27-17 defaults.26-16 Connecting Smart Agents on Customizing decision graph series .26-17 different local networks .27-18 Decision support components at runtime .26-18 Chapter 28 Decision pivots at runtime .26-18 Creating Internet server Decision grids at runtime .26-18 applications 28-1 Decision graphs at runtime .26-19 Terminology and standards .28-1 Decision support components and Parts of a Uniform Resource Locator .28-2 memory control .26-19 URI vs. URL .28-2 Setting maximum dimensions, HTTP request header information.28-2 summaries, and cells .26-19 HTTP server activity .28-3 Setting dimension state .26-19 Composing client requests .28-3 Using paged dimensions .26-20 Serving client requests .28-3

Part III Responding to client requests .28-4

Writing distributed applications Web server applications .28-4Types of Web server applications .28-4 ISAPI and NSAPI .28-5

Chapter 27 CGI stand-alone .28-5 Writing CORBA applications 27-1 Win-CGI stand-alone .28-5

Overview of a CORBA application .27-2 Creating Web server applications .28-5 Understanding stubs and skeletons .27-2 The Web module.28-6 Using Smart Agents .27-3 The Web Application object .28-7 Activating server applications .27-3 The structure of a Web server application .28-7 Binding interface calls dynamically .27-4 The Web dispatcher .28-8 Writing CORBA servers .27-4 Adding actions to the dispatcher .28-8 Using the CORBA wizards .27-4 Dispatching request messages .28-8 Defining object interfaces .27-5 Action items .28-9 Automatically generated code .27-7 Specifying action item properties .28-9 Registering server interfaces .27-7 The target URL .28-9 Registering interfaces with the The request method type .28-9 Interface Repository .27-8 Enabling and disabling action items.28-10 Registering interfaces with the Choosing a default action item .28-10 Object Activation Daemon .27-9 Responding to request messages with Writing CORBA clients .27-11 action items..28-11 Using stubs .27-11 Sending the response ..28-11 Using the dynamic invocation Using multiple action items ..28-11 interface .27-12 Accessing client request information ..28-11 Obtaining the interface .27-13 Properties that contain request header Calling interfaces with DII .27-13 information.28-12 xiv, Properties that identify the target .28-12 Debugging CGI and Win-CGI Properties that describe the Web applications .28-25 client .28-12 Simulating the server .28-25 Properties that identify the purpose Debugging as a DLL.28-25 of the request .28-12 Properties that describe the expected Chapter 29 response .28-13 Working with sockets 29-1 Properties that describe the content .28-13 Implementing services .29-1 The content of HTTP request messages .28-13 Understanding service protocols .29-2 Creating HTTP response messages .28-14 Communicating with applications.29-2 Filling in the response header.28-14 Services and ports .29-2 Indicating the response status .28-14 Types of socket connections .29-2 Indicating the need for client action .28-15 Client connections .29-3 Describing the server application .28-15 Listening connections .29-3 Describing the content .28-15 Server connections .29-3 Setting the response content .28-15 Describing sockets .29-3 Sending the response .28-16 Describing the host .29-4 Generating the content of response Choosing between a host name and messages .28-16 an IP address .29-4 Using page producer components .28-16 Using ports .29-5 HTML templates .28-16 Using socket components .29-5 Specifying the HTML template .28-17 Using client sockets .29-5 Converting HTML-transparent tags .28-18 Specifying the desired server .29-6 Using page producers from an Forming the connection .29-6 action item .28-18 Getting information about the Chaining page producers together .28-19 connection .29-6 Using database information in responses .28-19 Closing the connection .29-6 Adding a session to the Web module .28-20 Using server sockets .29-6 Representing database information Specifying the port.29-7 in HTML.28-20 Listening for client requests .29-7 Using dataset page producers .28-20 Connecting to clients .29-7 Using table producers .28-21 Getting information about connections .29-7 Specifying the table attributes .28-21 Closing server connections .29-8 Specifying the row attributes .28-21 Responding to socket events.29-8 Specifying the columns.28-22 Error events .29-8 Embedding tables in HTML Client events .29-9 documents .28-22 Server events.29-9 Setting up a dataset table producer .28-22 Events when listening .29-9 Setting up a query table producer .28-22 Events with client connections .29-9 Debugging server applications .28-23 Reading and writing over socket Debugging ISAPI and NSAPI connections .29-10 applications .28-23 Non-blocking connections .29-10 Debugging under Windows NT.28-23 Reading and writing events .29-10 Debugging with a Microsoft IIS Blocking connections ..29-11 server .28-23 Using threads with blocking Debugging with a Windows 95 connections ..29-11 Personal Web Server .28-24 Using TWinSocketStream .29-12 Debugging with Netscape Server Writing client threads .29-12 Version 2.0 .28-24 Writing server threads.29-13 xv,

Part IV Virtual methods .31-8 Creating custom components Overriding methods .31-8

Dynamic methods .31-8

Chapter 30 Abstract class members .31-9Classes and pointers .31-9 Overview of component creation 30-1

Visual Component Library .30-1 Chapter 32 Components and classes .30-2 Creating properties 32-1 How do you create components? .30-2 Why create properties? .32-1 Modifying existing controls .30-3 Types of properties .32-2 Creating windowed controls .30-3 Publishing inherited properties .32-2 Creating graphic controls .30-4 Defining properties .32-3 Subclassing Windows controls .30-4 The property declaration .32-3 Creating nonvisual components .30-5 Internal data storage .32-4 What goes into a component? .30-5 Direct access .32-4 Removing dependencies.30-5 Access methods .32-5 Properties, methods, and events .30-6 The read method .32-6 Properties .30-6 The write method .32-6 Events .30-6 Default property values .32-7 Methods .30-6 Specifying no default value.32-7 Graphics encapsulation .30-7 Creating array properties .32-8 Registration .30-7 Storing and loading properties .32-9 Creating a new component .30-8 Using the store-and-load mechanism .32-9 Using the Component wizard.30-8 Specifying default values .32-9 Creating a component manually .30-10 Determining what to store .32-10 Creating a unit file .30-10 Initializing after loading ..32-11 Deriving the component .30-11 Registering the component .30-11 Chapter 33 Testing uninstalled components .30-12 Creating events 33-1

Chapter 31 What are events? .33-1

Object-oriented programming for Events are method pointers .33-2Events are properties .33-2 component writers 31-1 Event types are method-pointer types .33-3 Defining new classes .31-1 Event-handler types are procedures .33-3 Deriving new classes.31-2 Event handlers are optional .33-4 To Change class defaults to avoid Implementing the standard events .33-4 repetition.31-2 Identifying standard events .33-4 To Add new capabilities to a class .31-2 Standard events for all controls .33-5 Declaring a new component class .31-3 Standard events for standard controls.33-5 Ancestors, descendants, and class Making events visible .33-5 hierarchies .31-3 Changing the standard event handling .33-6 Controlling access .31-4 Defining your own events .33-6 Hiding implementation details .31-4 Triggering the event.33-6 Defining the component writer’s Two kinds of events .33-7 interface .31-5 Defining the handler type .33-7 Defining the runtime interface .31-6 Simple notifications .33-7 Defining the design-time interface .31-6 Event-specific handlers .33-7 Dispatching methods .31-7 Returning information from the Static methods .31-7 handler .33-8 xvi, Declaring the event.33-8 Defining your own messages .36-5 Event names start with “On” .33-8 Declaring a message identifier .36-5 Calling the event .33-8 Declaring a message-record type.36-6 Empty handlers must be valid .33-8 Declaring a new message-handling Users can override default method .36-6 handling .33-9

Chapter 37 Chapter 34 Making components available at Creating methods 34-1 design time 37-1

Avoiding dependencies .34-1 Registering components .37-1 Naming methods .34-2 Declaring the Register procedure .37-2 Protecting methods .34-2 Writing the Register procedure .37-2 Methods that should be public .34-3 Specifying the components .37-2 Methods that should be protected .34-3 Specifying the palette page .37-3 Abstract methods.34-3 Using the RegisterComponents Making methods virtual .34-3 function .37-3 Declaring methods.34-4 Adding palette bitmaps .37-3

Chapter 35 Providing Help for your component .37-4Creating the Help file .37-4 Using graphics in components 35-1 Creating the entries .37-4

Overview of graphics .35-1 Making component help Using the canvas .35-2 context-sensitive .37-6 Working with pictures.35-3 Adding help files to Delphi help .37-6 Using a picture, graphic, or canvas.35-3 Adding property editors .37-6 Loading and storing graphics .35-4 Deriving a property-editor class .37-7 Handling palettes .35-4 Editing the property as text .37-8 Specifying a palette for a control .35-5 Displaying the property value .37-8 Responding to palette changes .35-5 Setting the property value .37-8 Off-screen bitmaps .35-5 Editing the property as a whole .37-9 Creating and managing off-screen Specifying editor attributes.37-10 bitmaps .35-6 Registering the property editor ..37-11 Copying bitmapped images.35-6 Adding component editors .37-12 Responding to changes .35-7 Adding items to the context menu .37-12 Specifying menu items .37-13

Chapter 36 Implementing commands.37-13 Handling messages 36-1 Changing the double-click behavior .37-14

Understanding the message-handling Adding clipboard formats .37-14 system .36-1 Registering the component editor .37-15 What’s in a Windows message?.36-2 Compiling components into packages .37-15 Dispatching messages .36-2 Tracing the flow of messages .36-3 Chapter 38 Changing message handling .36-3 Modifying an existing component 38-1 Overriding the handler method .36-3 Creating and registering the component .38-1 Using message parameters .36-4 Modifying the component class .38-2 Trapping messages .36-4 Overriding the constructor .38-2 Creating new message handlers .36-5 Specifying the new default property value .38-3 xvii,

Chapter 39 An example of declaring access Creating a graphic component 39-1 properties .41-5

Creating and registering the component .39-1 Initializing the data link.41-6 Publishing inherited properties .39-2 Responding to data changes .41-6 Adding graphic capabilities .39-3 Creating a data-editing control .41-7 Determining what to draw .39-3 Changing the default value of Declaring the property type .39-3 FReadOnly .41-8 Declaring the property .39-4 Handling mouse-down and Writing the implementation method .39-4 key-down messages .41-8 Overriding the constructor and Responding to mouse-down destructor .39-4 messages .41-8 Changing default property values .39-4 Responding to key-down messages .41-9 Publishing the pen and brush.39-5 Updating the field data-link class .41-9 Declaring the class fields .39-5 Modifying the Change method .41-10 Declaring the access properties .39-6 Updating the dataset ..41-11 Initializing owned classes .39-6 Chapter 42 Setting owned classes’ properties .39-7 Drawing the component image .39-8 Making a dialog box a component 42-1 Refining the shape drawing .39-9 Defining the component interface.42-1 Creating and registering the component .42-2

Chapter 40 Creating the component interface.42-3 Customizing a grid 40-1 Including the form unit .42-3

Creating and registering the component .40-1 Adding interface properties .42-3 Publishing inherited properties .40-2 Adding the Execute method .42-4 Changing initial values .40-3 Testing the component .42-6 Resizing the cells .40-4 Part V Filling in the cells .40-4 Tracking the date .40-5 Developing COM-based applications Storing the internal date .40-5 Accessing the day, month, and year .40-6 Chapter 43 Generating the day numbers .40-7 Overview of COM Technologies 43-1 Selecting the current day.40-9 COM as a specification and Navigating months and years .40-10 implementation .43-1 Navigating days .40-10 COM extensions .43-2 Moving the selection .40-11 Parts of a COM application .43-2 Providing an OnChange event .40-11 COM Interfaces .43-3 Excluding blank cells .40-12 The fundamental COM interface, IUnknown .43-4

Chapter 41 COM interface pointers .43-4 Making a control data aware 41-1 COM servers .43-5

Creating a data-browsing control .41-1 CoClasses and class factories .43-5 Creating and registering the component.41-2 In-process, out-of-process, and Making the control read-only .41-2 remote servers .43-6 Adding the ReadOnly property .41-3 The marshaling mechanism .43-7 Allowing needed updates .41-3 COM clients .43-8 Adding the data link .41-4 COM extensions.43-8 Declaring the class field .41-4 Automation servers and controllers .43-10 Declaring the access properties .41-5 ActiveX controls ..43-11 xviii, Type libraries .43-11 Automation and the registry.45-6 The content of type libraries .43-12 Running an Automation server in the Creating type libraries .43-12 background .45-7 When to use type libraries .43-12 Automation optional parameters: Accessing type libraries .43-13 named and positional .45-7 Benefits of using type libraries .43-13 Using variant arrays .45-8 Using type library tools .43-14 Creating variant arrays .45-9 Active Documents .43-14 Resizing variant arrays .45-9 Visual cross-process objects .43-15 Creating a one-dimensional variant Implementing COM objects with wizards .43-15 array.45-10 Getting variant array bounds and

Chapter 44 dimensions .45-10 Creating a simple COM object 44-1 Locking variant arrays ..45-11

Overview of creating a COM object .44-1 Designing a COM object .44-2 Chapter 46 Creating a COM object with the COM Creating an Automation server 46-1 object wizard .44-2 Creating an Automation object for an COM object instancing types .44-3 application .46-1 Choosing a threading model .44-3 Managing events in your Automation Writing an object that supports the free object .46-2 threading model .44-5 Exposing an application’s properties, Writing an object that supports the methods, and events .46-3 apartment threading model .44-5 Exposing a property for Automation .46-3 Registering a COM object .44-6 Exposing a method for Automation.46-4 Testing a COM object .44-6 Exposing an event for Automation .46-4 Getting more information .46-5

Chapter 45 Registering an application as an Creating an Automation controller 45-1 Automation server .46-5

Creating an Automation controller by Registering an in-process server .46-5 importing a type library .45-1 Registering an out-of-process server .46-6 Controlling an Automation server with Testing and debugging the application.46-6 a dual interface .45-2 Automation interfaces .46-6 Controlling an Automation server with Dual interfaces .46-6 a dispatch interface .45-2 Dispatch interfaces .46-8 Example: Printing a document with Custom interfaces .46-8 Microsoft Word .45-2 Marshaling data .46-9 Step 1: Import the Word type library .45-3 Automation compatible types .46-9 Step 2: Use a dual or dispatch interface Type restrictions for automatic object to control Microsoft Word .45-3 marshaling .46-10 Getting more information .45-4 Custom marshaling .46-10 Creating an Automation controller using Variants .45-4 Chapter 47 Example: Printing a document with Creating an ActiveX control 47-1 Microsoft Word .45-4 Overview of ActiveX control creation .47-1 Step 1: Creating a Variant object for Elements of an ActiveX control .47-2 Word Basic .45-5 VCL control.47-2 Step 2: Using the Variant method to Type library.47-2 print the document .45-5 Properties, methods, and events .47-3 Determining the variant type .45-5 Property page .47-3 xix, Designing an ActiveX control .47-3 Timestamp server .47-21 Generating an ActiveX control from a Cryptographic digest algorithm .47-22 VCL control .47-4 Generating an ActiveX control based on a Chapter 48 VCL form .47-5 Working with type libraries 48-1 Working with properties, methods, and Type Library editor .48-2 events in an ActiveX control .47-8 Toolbar .48-3 Adding additional properties, methods, Object list pane .48-4 and events .47-8 Status bar .48-4 How Delphi adds properties .47-9 Pages of type information .48-5 How Delphi adds methods .47-9 Attributes page.48-5 How Delphi adds events.47-10 Text page .48-6 Enabling simple data binding with the Flags page .48-6 type library .47-10 Type library information .48-6 Creating a property page for an Attributes page for a type library .48-7 ActiveX control .47-11 Uses page for a type library .48-7 Creating a new property page .47-11 Flags page for a type library .48-7 Adding controls to a property page .47-12 Interface pages .48-8 Associating property page controls with Attributes page for an interface .48-8 ActiveX control properties.47-12 Interface flags .48-8 Updating the property page .47-12 Interface members.48-9 Updating the object .47-12 Interface methods .48-9 Connecting a property page to an Interface properties .48-10 ActiveX control .47-13 Property and method parameters Publishing properties of an ActiveX page ..48-11 control .47-13 Dispatch type information .48-13 Registering an ActiveX control .47-14 Attributes page for dispatch .48-14 Testing an ActiveX control .47-14 Dispatch flags page .48-14 Deploying an ActiveX control on Dispatch members.48-14 the Web .47-15 CoClass pages .48-15 Setting options .47-16 Attributes page for a CoClass .48-15 Web Deploy Options Default CoClass Implements page .48-15 checkbox .47-16 CoClass flags.48-16 INF file .47-17 Enumeration type information .48-16 Option combinations .47-17 Attributes page for an Enum.48-17 Project tab .47-17 Enumeration members .48-17 Packages tab .47-18 Alias type information .48-17 Packages used by this project .47-19 Attributes page for an alias.48-18 CAB options .47-19 Record type information .48-18 Output options .47-19 Attributes page for a record .48-18 Directory and URL options .47-19 Record members.48-19 Additional Files tab .47-19 Union type information .48-19 Files associated with project .47-19 Attributes page for a union.48-19 CAB options .47-20 Union members .48-20 Output options .47-20 Module type information .48-20 Directory and URL options .47-20 Attributes page for a module .48-21 Code Signing tab .47-20 Module members .48-21 Required information .47-21 Module methods .48-21 Optional information .47-21 Module constants .48-22 xx, Creating new type libraries .48-22 MTS transaction slupport .49-7 Valid types .48-22 Transaction attributes .49-7 SafeArrays .48-24 Object context holds transaction attribute . 49-8 Using Object Pascal or IDL syntax .48-24 Stateful and stateless objects .49-8 Attribute specifications.48-24 Enabling multiple objects to support Interface syntax .48-26 transactions.49-9 Dispatch interface syntax .48-26 MTS or client-controlled transactions .49-9 CoClass syntax .48-27 Advantage of transactions .49-10 Enum syntax .48-28 Transaction timeout .49-10 Alias syntax .48-28 Role-based security ..49-11 Record syntax .48-28 Resource dispensers ..49-11 Union syntax .48-29 BDE resource dispenser..49-11 Module syntax .48-29 Shared property manager .49-12 Creating a new type library .48-30 Example: Sharing properties among Opening an existing type library .48-30 MTS object instances.49-12 Adding an interface to the type Tips for using the Shared Property library .48-31 Manager.49-13 Adding properties and methods to the Base clients and MTS components .49-13 type library .48-31 MTS underlying technologies, COM and Adding a CoClass to the type library .48-31 DCOM .49-14 Adding an enumeration to the type Overview of creating MTS objects .49-14 library .48-32 Using the MTS Object wizard .49-15 Saving and registering type library Choosing a threading model for an information .48-32 MTS object .49-16 Saving a type library .48-33 MTS activities .49-17 Refreshing the type library .48-33 Setting the transaction attribute .49-17 Registering the type library .48-33 Passing object references .49-18 Exporting an IDL file .48-33 Using the SafeRef method .49-18 Deploying type libraries .48-34 Callbacks .49-19 Setting up a transaction object on the

Chapter 49 client side .49-19 Creating MTS objects 49-1 Setting up a transaction object on the

Microsoft Transaction Server components .49-2 server side .49-20 Requirements for an MTS component .49-4 Debugging and testing MTS objects .49-20 Managing resources with just-in-time Installing MTS objects into an MTS activation and resource pooling .49-4 package.49-21 Just-in-time activation .49-4 Administering MTS objects with the Resource pooling .49-5 MTS Explorer .49-22 Releasing resources .49-5 Using MTS documentation .49-22 Object pooling .49-6 Accessing the object context.49-6 Index I-1 xxi,

Tables

1.1 Typefaces and symbols .1-2 15.3 IProvider interface members .15-7 2.1 The Delphi Component palette pages .2-17 15.4 Provider options .15-16 2.2 Corresponding string and object 15.5 UpdateStatus values .15-19 methods in string lists .2-51 15.6 UpdateMode values .15-20 2.3 Context menu options for data 15.7 ProviderFlags values .15-20 modules.2-53 16.1 Database-related informational 4.1 Object Pascal character types .4-23 methods for session components .16-9 4.2 String comparison routines .4-27 16.2 TSessionList properties and 4.3 Case conversion routines .4-27 methods .16-16 4.4 String modification routines .4-28 18.1 Values for the dataset State property .18-3 4.5 Sub-string routines .4-28 18.2 Navigational methods of datasets .18-9 4.6 Attribute constants and values .4-34 18.3 Navigational properties of datasets.18-10 5.1 Sample captions and their derived 18.4 Comparison and logical operators names .5-14 that can appear in a filter .18-18 5.2 Menu Designer context menu 18.5 FilterOptions values .18-19 commands .5-19 18.6 Filtered dataset navigational 5.3 Setting speed buttons’ appearance.5-27 methods .18-19 5.4 Setting tool buttons’ appearance .5-29 18.7 Dataset methods for inserting, 5.5 Setting a cool button’s appearance.5-30 updating, and deleting data .18-20 6.1 Properties of selected text.6-10 18.8 Methods that work with entire 6.2 Fixed vs. variable owner-draw styles .6-14 records .18-24 7.1 Common properties of the Canvas 18.9 Dataset events .18-25 object .7-2 18.10 TDBDataSet database and session 7.2 Common methods of the Canvas object.7-2 properties and function .18-27 7.3 Graphic object types .7-4 18.11 Properties, events, and methods for 7.4 Mouse-event parameters .7-23 cached updates .18-29 8.1 Multimedia device types and their 19.1 Field components .19-1 functions .8-4 19.2 TFloatField properties that affect 9.1 Thread priorities .9-3 data display .19-2 9.2 WaitFor return values .9-9 19.3 Special persistent field kinds .19-7 10.1 Compiled package files .10-2 19.4 Field component properties .19-13 10.2 Delphi runtime packages .10-4 19.5 Field component formatting routines . 19-16 10.3 Delphi design-time packages.10-5 19.6 Field component events .19-17 10.4 Package-specific compiler directives .10-11 19.7 Selected field component methods .19-18 10.5 Package-specific command-line 19.8 Field component conversion functions. 19-19 compiler switches .10-12 19.9 Special conversion results .19-19 10.6 Compiled package files .10-13 19.10 Types of object field components.19-23 11.1 VCL objects that support BiDi .11-4 19.11 Common object field descendent 11.2 Estimating string lengths .11-8 properties .19-23 12.1 Application files .12-2 20.1 Table types recognized by the BDE 12.2 SQL Database Client Software Files .12-5 based on file extension .20-4 13.1 Data Dictionary interface .13-4 20.2 TableType values .20-4 14.1 Possible values for the TransIsolation 20.3 Legacy TTable search methods .20-6 property.14-7 20.4 BatchMove import modes .20-19 14.2 Transaction isolation levels .14-7 20.5 Batch move modes .20-21 15.1 MIDAS components .15-3 23.1 Summary operators for maintained 15.2 Connection components .15-4 aggregates .23-10 xxii, 23.2 Client datasets properties and 35.2 Image-copying methods .35-6 method for handling data requests .23-15 37.1 Predefined property-editor types .37-7 24.1 TUpdateRecordType values .24-9 37.2 Methods for reading and writing 24.2 Return values for UpdateStatus .24-10 property values .37-8 24.3 UpdateKind values .24-24 37.3 Property-editor attribute flags.37-10 24.4 UpdateAction values .24-25 43.1 COM object requirements .43-10 25.1 Data controls .25-2 43.2 Delphi wizards for implementing 25.2 Properties affecting editing in data COM, Automation, and ActiveX controls .25-4 objects .43-16 25.3 Data-aware list box and combo 44.1 Threading models for COM objects .44-4 box controls.25-11 48.1 Type library pages .48-5 25.4 TDBLookupListBox and 48.2 Attributes common to all types .48-6 TDBLookupComboBox properties.25-14 48.3 Type library attributes .48-7 25.5 Column properties.25-21 48.4 Type library flags .48-7 25.6 Expanded TColumn Title properties .25-22 48.5 Interface attributes .48-8 25.7 Properties that affect the way ADT 48.6 Interface flags .48-8 and array fields appear in a 48.7 Method attributes.48-9 TDBGrid .25-23 48.8 Method flags .48-10 25.8 Expanded TDBGrid Options 48.9 Property attributes .48-10 properties .25-24 48.10 Property flags.48-10 25.9 Grid control events .25-27 48.11 Parameter modifiers 25.10 Selected database control grid (Object Pascal Syntax) .48-12 properties .25-28 48.12 Parameter flags (IDL syntax) .48-13 25.11 TDBNavigator buttons .25-29 48.13 Dispinterface attributes .48-14 27.1 Types allowed in a CORBA interface .27-6 48.14 CoClass attributes .48-15 27.2 irep arguments .27-8 48.15 CoClass Implements page options .48-15 27.3 idl2ir arguments .27-9 48.16 CoClass flags .48-16 27.4 OAD arguments .27-9 48.17 Enum attributes .48-17 27.5 oadutil reg arguments.27-10 48.18 Alias attributes .48-18 27.6 oadutil unreg arguments .27-10 48.19 Record attributes .48-18 27.7 ORB methods for creating 48.20 Union attributes .48-19 structured TAny values .27-14 48.21 Module attributes .48-21 27.8 CORBA environment variables .27-16 48.22 Module method attributes .48-21 27.9 osagent arguments.27-17 48.23 Valid types .48-23 28.1 Web server application 48.24 Attribute syntax .48-25 components .28-5 49.1 IObjectContext methods for 28.2 MethodType values .28-10 transaction support .49-9 30.1 Component creation starting points .30-3 49.2 MTS server objects versus base 31.1 Levels of visibility within an object .31-4 clients .49-14 32.1 How properties appear in the Object 49.3 Threading models for COM objects .49-16 Inspector .32-2 49.4 Microsoft MTS documentation 35.1 Canvas capability summary .35-3 roadmap .49-22 xxiii,

Figures

2.1 A simple form .2-4 25.1 TDBGrid control .25-16 2.2 Inheriting from TForm .2-7 25.2 TDBGrid control with ObjectView 2.3 A simplified hierarchy diagram .2-8 set to False .25-23 2.4 Three views of the track bar 25.3 TDBGrid control with Expanded component .2-22 set to False .25-23 2.5 A progress bar .2-31 25.4 TDBGrid control with Expanded 2.6 The Picture Editor is a property-editor set to True .25-24 dialog box .2-38 25.5 TDBCtrlGrid at design time .25-28 5.1 Delphi menu terminology .5-12 25.6 Buttons on the TDBNavigator 5.2 MainMenu and PopupMenu control .25-29 components .5-12 26.1 Decision support components at 5.3 Menu Designer for a main menu .5-13 design time .26-2 5.4 Menu Designer for a pop-up menu .5-13 26.2 One-dimensional crosstab .26-3 5.5 Nested menu structures.5-16 26.3 Three-dimensional crosstab .26-3 5.6 Select Menu dialog box .5-20 26.4 Decision graphs bound to different 5.7 Sample Insert Template dialog box decision sources.26-14 for menus .5-21 27.1 The structure of a CORBA 5.8 Save Template dialog box for menus .5-22 application.27-2 5.9 Action list mechanism.5-33 27.2 Separate ORB domains.27-18 5.10 Execution cycle for an action .5-35 27.3 Two Smart Agents on separate 5.11 Action targets .5-38 local networks.27-19 7.1 Bitmap-dimension dialog box from the 28.1 Parts of a Uniform Resource Locator .28-2 BMPDlg unit..7-20 28.2 Structure of a Server Application .28-7 11.1 TListBox set to bdLeftToRight .11-6 30.1 Visual Component Library class 11.2 TListBox set to bdRightToLeft .11-6 hierarchy.30-2 11.3 TListBox set to bdRightToLeftNoAlign .11-6 30.2 Component wizard .30-9 11.4 TListBox set to 43.1 A COM interface .43-3 bdRightToLeftReadingOnly .11-6 43.2 Interface vtable .43-4 13.1 User-interface to dataset connections 43.3 In-process server .43-6 in all database applications .13-7 43.4 Out-of-process and remote servers .43-7 13.2 Single-tiered database application 43.5 COM-based technologies .43-9 architectures .13-8 43.6 Simple COM object interface .43-15 13.3 Two-tiered database application 43.7 Automation object interface .43-15 architecture .13-9 43.8 ActiveX object interface .43-16 13.4 Multi-tiered database architectures .13-10 46.1 Dual interface vtable .46-7 14.1 Components in a BDE-based 47.1 Mask Edit property page in design application .14-2 mode .47-12 18.1 Delphi dataset hierarchy .18-1 48.1 Type Library editor .48-2 18.2 Relationship of Inactive and 48.2 Object list pane .48-4 Browse states .18-5 49.1 MTS object interface .49-2 18.3 Relationship of Browse to other 49.2 MTS In-Process Component .49-3 dataset states .18-6 49.3 An MTS component in an 18.4 Dataset component hierarchy .18-26 out-of-process server .49-3 21.1 Sample master/detail query form 49.4 An MTS component in a remote and data module at design time .21-11 server process .49-3 xxiv,

Chapter

Chapter 1Introduction The Developer’s Guide describes intermediate and advanced development topics, such as building client/server database applications, writing custom components, and creating Internet Web server applications. It allows you to build applications that meet many industry-standard specifications such as CORBA,TCP/IP, MTS, COM, and ActiveX. The Developer’s Guide assumes you are familiar with using Delphi and understand fundamental Delphi programming techniques. For an introduction to Delphi programming and the integrated development environment (IDE), see the online Help.

What’s in this manual?

This manual contains five parts, as follows: • Part I, “Programming with Delphi,” describes how to build general-purpose Delphi applications. This part provides details on programming techniques you can use in any Delphi application. For example, it describes how to use common Visual Component Library (VCL) objects that make user interface programming easy such as handling strings, manipulating text, implementing the Windows common dialog, toolbars, and cool bars. It also includes chapters on working with graphics, error and exception handling, using DLLs, OLE automation, and writing international applications. The chapter on deployment details the tasks involved in deploying your application to your application users. For example, it includes information on effective compiler options, using InstallShield Express, licensing issues, and how to determine which packages, DLLs, and other libraries to use when building the production-quality version of your application. • Part II, “Developing database applications,” describes how to build database applications using database tools and components. Delphi lets you access many types of databases. With the forms and reports you create, you can access local databases such as Paradox and dBASE, network SQL server databases likeIntroduction1-1, ManualconventionsInterBase and Sybase, and any data source accessible through open database connectivity (ODBC). To implement the more advanced Client/Server database applications, you need the Delphi features available in the Client/Server and Enterprise editions. • Part III, “Writing distributed applications,” describes how to create applications that are distributed over a local area network. These include CORBA applications, and Web server applications such as CGI applications or NSAPI and ISAPI dynamic-link libraries (DLLs). For lower-level support of distributed applications, this section also describes how to work with socket components, that handle the details of communication using TCP/IP and related protocols. The components that support CORBA and Web server applications are available in the Client/ Server and Enterprise editions of Delphi. The socket components are available in the Professional version as well. • Part IV, “Creating custom components,” describes how to design and implement your own components, and how to make them available on the Component palette of the IDE. A component can be almost any program element that you want to manipulate at design time. Implementing custom components entails deriving a new class from an existing class type in the VCL class library. • Part V, “Developing COM-based applications,” describes how to build applications that can interoperate with other COM-based API objects on the system such as Win95 Shell extensions or multimedia applications. Delphi contains components that support the ActiveX, COM-based library for COM controls that can be used for general-purpose and Web-based applications. This part also describes how to write servers that can reside in the MTS runtime environment. MTS provides extensive runtime support for security, transactions, and resource pooling. Support for COM controls is available in all editions of Delphi. To create ActiveX controls, you need the Professional, Client/Server, or Enterprise edition. To create MTS servers, you need the Client/Server or Enterprise edition.

Manual conventions

This manual uses the typefaces and symbols described in Table 1.1 to indicate special text. Table 1.1 Typefaces and symbols Typeface or symbol Meaning Monospace type Monospaced text represents text as it appears on screen or in Object Pascal code. It also represents anything you must type. [ ] Square brackets in text or syntax listings enclose optional items. Text of this sort should not be typed verbatim. Boldface Boldfaced words in text or code listings represent Object Pascal keywords or compiler options. 1-2Developer’ sGuide, InprisedevelopersupportservicesTable 1.1 Typefaces and symbols (continued) Typeface or symbol Meaning Italics Italicized words in text represent Object Pascal identifiers, such as variable or type names. Italics are also used to emphasize certain words, such as new terms. Keycaps This typeface indicates a key on your keyboard. For example, “Press Esc to exit a menu.”

Inprise developer support services

Inprise offers developers high-quality support options. These include free services on the Internet, where you can search our extensive information base and connect with other users of Inprise products. In addition to this basic level of support, you can choose from several categories of telephone support, ranging from support on installation of the Inprise product to fee-based consultant-level support and detailed assistance. To obtain pricing information for Inprise’s developer support services, see our Web site, at http://www.inprise.com/devsupport, or call Inprise Assist at (800) 523-7070.

Ordering printed documentation

For information about ordering additional documentation, refer to the Inprise Web site at http://www.inprise.com. Introduction1-3, 1-4Developer’ sGuide,

Part I

Part IProgramming with Delphi The chapters in “Programming with Delphi” present concepts and skills necessary for creating Delphi applications using any edition of the product. ProgrammingwithDelphi,

Chapter

Chapter 2Using Object Pascal with the VCL This chapter discusses how to use Object Pascal and the object and component library in Delphi applications.

Understanding the distinction between Object Pascal and the VCL

A simple way of describing Delphi is a sophisticated Pascal compiler. Delphi’s roots lie in Borland’s Turbo Pascal, introduced in the mid-1980s. This view of Delphi, however, doesn’t capture the real power of Delphi. Object Pascal, the object-oriented extensions to Pascal, is the underlying language of Delphi. The Visual Component Library, or VCL, is a hierarchy of Object Pascal objects that allow you to design programs. A better way of describing Delphi is an Object Pascal-based visual development environment. The VCL is intimately tied to the Delphi IDE, and is what gives you the ability to quickly develop applications. The Component palette and Object Inspector allow you to drop VCL components on forms and then manipulate the properties and events of those controls without having to write a single line of code. Despite its name, the VCL is not entirely made up of visual components. In fact, of the over 600 objects in the VCL, most are not visual. The Delphi IDE allows you to visually add some nonvisual components to your programs. For example, if you wanted to write a database application that connected to a table, you would drop a TDataSource component on your form. TDataSource is a nonvisual component, but is represented on the form by an icon (which doesn’t show up at runtime), and you can manipulate the properties and events of TDataSource in the Object Inspector just as you would a visual control. All VCL objects, and in fact all objects in Object Pascal, are derived from TObject. TObject is unique in that it is an abstract object that has no properties or events, only methods that allow you to derive objects from this base class. Use TObject as the immediate base class when writing simple objects that are not components. Components are objects that you can manipulate at design time. All components inUsingObjectPascalwiththeVCL2-1, Usingtheobjectmodelthe VCL are derived from the abstract component type TComponent. The VCL components you will likely use the most are the VCL’s controls, such as TForm or TSpeedButton. Controls are visual components derived from the abstract component type TControl. You can use Delphi to create Object Pascal objects without using the VCL, although by creating any objects in Object Pascal, both your objects and VCL objects will share a common ancestor in TObject. However, by deriving new objects from VCL object, much of the work in designing applications is done for you by Delphi. For example, if you wanted to use a progress bar in your application but didn’t like TProgressBar, Delphi’s control that creates a progress bar, you could create a new object based on TProgressBar and override its properties, events, or methods.

Using the object model

Object-oriented programming (OOP) is a natural extension of structured programming. OOP requires that you use good programming practices and makes it very easy for you to do so. The result is clean code that is easy to extend and simple to maintain. Once you create an object for an application, you and other programmers can then use that same object in other applications. Reusing objects can greatly cut your development time and increase productivity for yourself and others. If you want to create new components and put them on the Delphi Component palette, see Chapter 30, “Overview of component creation.”

What is an object?

An object is a data type that wraps up data and code all into one bundle. Before OOP, code and data were treated as separate elements. Think of the work involved to assemble a bicycle if you have all the bicycle parts and a list of instructions for the assembly process. This is analogous to writing a Windows program from scratch without using objects. Delphi gives you a head start on “building your bicycle” because it already gives you many of the “preassembled bicycle parts”—the Delphi forms and components. You can begin to understand what an Object Pascal object is if you understand what an Object Pascal record is. Records are made of up fields that contain data, and each of these fields has its own data type. Records make it easy to refer to a related collection of varied data elements as one entity. Objects are also collections of data elements. Like records, they also have fields, each of which has its own data type. Unlike records, objects also contain code—procedures and functions—that act on the data contained in the object’s fields. These procedures and functions are called methods. 2-2Developer’ sGuide, UsingtheobjectmodelAlso unlike records, objects can contain properties. The properties of Delphi objects have default values. You can change these values at design time to modify an object to suit the needs of your project without writing code. If you want a property value to change at runtime, you need to write only a very small amount of code. Examining a Delphi object When you create a new project, Delphi displays a new form for you to customize. In the Code editor, Delphi declares a new object type for the form and produces the code that creates the new form object. A later section discusses why a new object type is declared for each new form. For now, examine the following example to see what the code in the Code editor looks like: unit Unit1; interface uses Windows, Classes, Graphics, Forms, Controls, Apps; type TForm1 = class(TForm){ The type declaration of the form begins here } private { Private declarations } public { Public declarations } end;{ The type declaration of the form ends here } var Form1: TForm1; implementation{ Beginning of implementation part } {$R *.DFM} end.{ End of of implemntation part and unit} The new object type is TForm1, and it is derived from type TForm, which is also an object. A later section presents more about TForm and objects derived from other objects. An object is like a record in that they both contain data fields, but an object also contains methods—code that acts on the object’s data. So far, type TForm1 appears to contain no fields or methods, because you haven’t added to the form any components (the fields of the new object), and you haven’t created any event handlers (the methods of the new object). As discussed later, TForm1 does contain fields and methods, even though you don’t see them in the type declaration. This variable declaration declares a variable named Form1 of the new object type TForm1. var Form1: TForm1; Form1 is called an instance of the type TForm1. The Form1 variable refers to the form itself to which you add components to design your user interface. You can declare more than one instance of an object type. You might want to do this to manage multiple child windows in a Multiple Document Interface (MDI) application, for example. Each instance carries its own data in its own package, and all the instances of an object use the same code. UsingObjectPascalwiththeVCL2-3, UsingtheobjectmodelAlthough you haven’t added any components to the form or written any code, you already have a complete Delphi application that you can compile and run. All it does is display a blank form because the form object doesn’t yet contain the data fields or methods to do more. Suppose, though, that you add a button component to this form and an OnClick event handler for the button, that changes the color of the form when the user clicks the button. You then have an application that’s about as simple as it can be and still actually do something. Here is the form for the application: Figure 2.1 A simple form When the user clicks the button, the form changes color to green. This is the event- handler code for the button’s OnClick event: procedure TForm1.Button1Click(Sender: TObject); begin Form1.Color := clGreen; end; If you create this application and then look at the code in the Code editor, this is what you see: unit Unit1; interface uses Windows, Classes, Graphics, Forms, Controls, Apps; type TForm1 = class(TForm) Button1: TButton;{ New data field } procedure Button1Click(Sender: TObject);{ New method declaration } private { Private declarations } public { Public declarations } end; var Form1: TForm1; 2-4Developer’ sGuide, Usingtheobjectmodelimplementation {$R *.DFM} procedure TForm1.Button1Click(Sender: TObject);{ The code of the new method } begin Form1.Color := clGreen; end; end. The new TForm1 object now has a Button1 field—that’s the button you added to the form. TButton is an object type, so Button1 is also an object. Object types, such as TForm1, can contain other objects, such as Button1, as data fields. Each time you put a new component on a form, a new field with the component’s name appears in the form’s type declaration. All the event handlers you write in Delphi are methods of the form object. Each time you create an event handler, a method is declared in the form object type. The TForm1 type now contains a new method, the Button1Click procedure, declared within the TForm1 type declaration. The actual code for the Button1Click method appears in the implementation part of the unit. The method is the same as the empty event handler you created with Delphi and then filled in to respond when the user clicks the button as the application runs. Changing the name of a component You should always use the Object Inspector to change the name of a component. For example, when you change the default name of a form from Form1 to something else by changing the value of the Name property using the Object Inspector, the name change is reflected throughout the code Delphi produces. If you wrote the previous application, but named the form ColorBox, this is how your code would appear: unit Unit1; interface uses Windows, Classes, Graphics, Forms, Controls, Apps; type TColorBox = class(TForm){ Changed from TForm1 to TColorBox } Button1: TButton; procedure Button1Click(Sender: TObject); private { Private declarations } public { Public declarations } end; var ColorBox: TColorBox;{ Changed from Form1 to ColorBox } implementation {$R *.DFM} UsingObjectPascalwiththeVCL2-5, Usingtheobjectmodelprocedure TColorBox.Button1Click(Sender: TObject); begin Form1.Color := clGreen;{ The reference to Form1 didn't change! } end; end. Notice that the name of the form object type changed from TForm1 to TColorBox. Also note that the form variable is now ColorBox, the name you gave to the form. References to the ColorBox variable and the TColorBox type also appear in the final begin and end block of code, the code that creates the form object and starts the application running. If Delphi originally generated the code, it updates it automatically when you use the Object Inspector to change the name of the form or any other component. Note that the code you wrote in the OnClick event handler for the button hasn’t changed. Because you wrote the code, you have to update it yourself and correct any references to the form if you change the form’s name. If you don’t, your code won’t be compiled. In this case, change the code to this: procedure TColorBox.Button1Click(Sender: TObject); begin ColorBox.Color := clGreen; end; You should change the name of a component only with the Object Inspector. While nothing prevents you from altering the code Delphi produced and changing the name of component variables, you won’t be able to compile your program.

Inheriting data and code from an object

The TForm1 object described on page 2-3 might seem quite simple. If you create the application, TForm1 appears to contain one field, Button1, one method, Button1Click, and no properties. Yet you can change the size of the form, add or delete minimize and maximize buttons, or set up the form to become part of a Multiple Document Interface (MDI) application. How is it possible to do all these things with a form that contains only one field and one method? The answer lies in the notion of inheritance. Consider the bicycle analogy: once you have put together all the objects that make up a complete “bicycle object,” you can ride it because it has all the essentials to make the bicycle useful—it has pedals you push to make the wheels go around, it has a seat for you to sit on, it has handlebars so you can steer, and so on. Similarly, when you add a new form to your project, it has all the capabilities of any form. For example, all forms provide a space to put other components on them, all forms have the methods to open, show, and hide themselves, and so on. Suppose, though, that you want to customize that bicycle, just as you would customize a form object in Delphi. You might adjust a few gear settings, add a headlight, and provide a horn with a selection of sounds—just as you might customize a form by adding or rearranging buttons, changing a few property values, and adding a new method that allows the form to appear with a plaid background. 2-6Developer’ sGuide, UsingtheobjectmodelTo change the bicycle to make it exactly as you want it, you start with the basic model and then customize it. You do the same thing with Delphi forms. When you add a new form to your project, you’ve added the “basic model” form. By adding components to the form, changing property values, and writing event handlers, you are customizing the new form. To customize any object, whether it be a blank form, a form with multiple controls that is used as a dialog box, or a new version of the Delphi bitmap button, you start by deriving a new object from an existing object. When you add a new form to your project, Delphi automatically derives a new form object for you from the TForm type. At the moment a new form is added to a project, the new form object is identical to the TForm type. Once you add components to it, change properties, and write event handlers, the new form object and the TForm type are no longer the same. No matter how you customize your bicycle, it can still do all the things you expect a bicycle to do. Likewise, a customized form object still exhibits all the built-in capabilities of a form, and it can still do all the things you expect a form to do, such as change color, resize, close, and so on. That’s because the new form object inherits all the data fields, properties, methods, and events from the TForm type. When you add a new form to an Delphi project, Delphi creates a new object type, TForm1, deriving it from the more generic object, TForm. The first line of the TForm1 type declaration specifies that TForm1 is derived from TForm: TForm1 = class(TForm) Because TForm1 is derived from TForm, all the elements that belong to a TForm type automatically become part of the TForm1 type. If you look up TForm in online Help (you can click the form itself and press F1), you see lists of all the properties, methods, and events in the TForm object type. You can use all the elements inherited from TForm in your application. Only the elements you specifically add to your TForm1 type, such as components you place on the form or event handlers (methods) you write to respond to events, appear in the TForm1 type declaration. These are the things that make TForm1 different from TForm. The more broad-based or generic object from which another more customized object inherits data and code is called the ancestor of the customized object. The customized object itself is a descendant of its ancestor. An object can have only one immediate ancestor, but it can have many descendants. For example, TForm is the ancestor type of TForm1, and TForm1 is a descendant of TForm. All form objects are descendants of TForm, and you can derive many form objects from TForm. Figure 2.2 Inheriting from TForm TForm TForm1 TForm2 TForm3 MyDialogBoxUsingObjectPascalwiththeVCL2-7, UsingtheobjectmodelObjects, components, and controls Figure 2.3 A simplified hierarchy diagram TObject TComponent TControl TForm TButton TCheckBox TListBox The diagram above, a greatly simplified view of the inheritance hierarchy of the Visual Component Library, illustrates how everything in the VCL is an object, though many objects, such as TForm, are often referred to as components. Components, which inherit data and code from a TObject type, are objects with additional properties, methods, and events that make them suitable for specialized purposes, such as the ability to save their state to a file. Controls, which inherit data and code from a TComponent type (which in turn inherits elements from TObject) have additional specialized capabilities, such as the ability to display something. So controls are components and objects, components are objects but not necessarily controls, and objects are simply objects. This chapter refers to all components and controls as objects. Even though TCheckBox isn’t an immediate descendant of TObject, it still has all the attributes of any object because TCheckBox is ultimately derived from TObject in the VCL hierarchy. TCheckBox is a very specialized kind of object that inherits all the functionality of TObject, TComponent, and TControl, and defines some unique capabilities of its own.

Object scope

An object’s scope determines the availability and accessibility of the data fields, properties, and methods within that object. Using the bicycle analogy, if you were to add a headlight only to your customized “bicycle object,” the headlight would belong to that bicycle and to no other. If, however, the “basic model bicycle object” included a headlight, then all bicycle objects would inherit the presence of a headlight. The headlight could lie either within the scope of the ancestor bicycle object—in which case, a headlight would be a part of all descendant bicycle objects— or within the scope only of the customized bicycle object, and available only to that bicycle. Likewise, all data fields, properties, and methods declared within an object declaration are within the scope of the object, and are available to that object and its 2-8Developer’ sGuide, Usingtheobjectmodeldescendants. Even though the code that makes up the methods appears outside of the object declaration in the implementation part of the unit, those methods are still within the scope of the object because they were declared within the object’s declaration. When you write code in an event handler of an object that refers to properties, methods, or fields of the object itself, you don’t need to preface these identifiers with the name of the object variable. For example, if you put a button and an edit box on a new form, you could write this event handler for the OnClick event of the button: procedure TForm1.Button1Click(Sender: TObject); begin Color := clFuchsia; Edit1.Color := clLime; end; The first statement colors the form. You could have written the statement like this: Form1.Color := clFuchsia It’s not necessary, however, to put the Form1 qualifier on the Color property because the Button1Click method is within the scope of the TForm1 object. Any time you are within an object’s scope, you can omit the qualifier on all properties, methods, and fields that are part of the object. The second statement refers to the Color property of a TEdit object. Because you want to access the Color property of the TEdit1 type, not of the TForm1 type, you need to specify the scope of the Color property by including the name of the edit box, so the compiler can determine which Color property you are referring to. If you omit it, the second statement is like the first; the form ends up lime green, and the edit box control remains unchanged when the handler runs. Because it’s necessary to specify the name of the edit box whose Color property you are changing, you might wonder why it’s not necessary to specify the name of the form as well. This is unnecessary because the control Edit1 is within the scope of the TForm1 object; it’s declared to be a data field of TForm1. Accessing components on another form If Edit1 were on some other form, you would need to preface the name of the edit box with the name of the form object variable. For example, if Edit1 were on Form2, it would be a data field in the TForm2 object declaration, and would lie with the scope of Form2. You would write the statement to change the color of the edit box in Form2 from the TForm1.ButtonClick method like this: Form2.Edit1.Color := clLime; In the same way, you can also access methods of a component on another form. For example, Form2.Edit1.Clear; To give the code of Form1 access to properties, methods, and events of Form2, you need to add Unit2 to the uses clause of Unit1 (assuming the units associated with Form1 and Form2 are named Unit1 and Unit2, respectively). For more information about the uses clause, see the Object Pascal Language Guide. UsingObjectPascalwiththeVCL2-9, UsingtheobjectmodelScope and descendants of an object The scope of an object extends to all the object’s descendants. That means all the data fields, properties, methods, and events that are part of TForm are within the scope of TForm1 also, because TForm1 is a descendant of TForm. Your application won’t be able to declare a data field using the same name as a data field in the object’s ancestor. If Delphi displays a duplicate-identifier message, it’s possible a data field with the same name already exists in the ancestor object, such as in TForm. If you see the duplicate identifier message and you don’t understand why, try altering the name of the identifier slightly. It might be that you happened to choose the name of a data field in an ancestor object. Overriding a method You can, however, use the name of a method within an ancestor object to declare a method within a descendant object. This is how you override a method. You would most likely want to override an existing method if you want the method in the descendant object to do the same thing as the method in the ancestor object, but the task is accomplished in another way. In other words, the code that implements the two methods differs. It’s not often that you would want to override a method unless you are creating new components. You should be aware that you can do so, though, and that you won’t receive any warning or error message from the compiler. Public and private declarations When you build an application using the Delphi environment, you are adding data fields and methods to a descendant of TForm. You can also add fields and methods to an object without putting components on a form or filling in event handlers, but by modifying the object type declaration directly. You can add new data fields and methods to either the public or private part of an object. Public and private are standard directives in the Object Pascal language. Treat them as if they were reserved words. When you add a new form to the project, Delphi begins constructing the new form object. Each new object contains the public and private directives that mark locations for data fields and methods you want to add to the code directly. For example, note the private and public parts in this new form object declaration that so far contains no fields or methods: type TForm1 = class(TForm) private { Private declarations } public { Public declarations } end; Use the public part to • Declare data fields you want methods in objects in other units to access • Declare methods you want objects in other units to access 2-10Developer’ sGuide, UsingtheobjectmodelDeclarations in the private part are restricted in their access. If you declare fields or methods to be private, they are unknown and inaccessible outside the unit the object is defined in. Use the private part to • Declare data fields you want only methods in the current unit to access • Declare methods you want only objects defined in the current unit to access To add data fields or methods to a public or private section, put the fields or method declarations after the appropriate comment, or erase the comments before you add the code. Here is an example: type TForm1 = class(TForm) Edit1: TEdit; Button1: TButton; procedure Button1Click(Sender: TObject); private { Private declarations } Number: Integer; function Calculate(X, Y: Integer): Integer; public { Public declarations } procedure ChangeColor; end; Place the code that implements the Calculate and ChangeColor methods in the implementation part of the unit. The Number data field and Calculate function are declared to be private. Only objects within the unit can use Number and Calculate. Usually, this restriction means that only the form object can use them, because each Delphi unit contains just one object type declaration of type TForm. Because the ChangeColor method is declared to be public, code in other units can use it. Such a method call from another unit must include the object name in the call: Form1.ChangeColor; The unit making this method call must have Form1 in its uses clause. Note When adding fields or methods to an object, always put the fields before the method declarations within each public or private part. Accessing object fields and methods When you want to change the value of a property of an object that is a field in the form object, or you want to call a method of an object that is a field in the form object, you must include the name of that object in the property name or method call. For example, if your form has an edit box control on it and you want to change the value of its Text property, you should write the assignment statement something like this, making sure to specify the name of the edit box (in this example, Edit1): Edit1.Text := 'Frank Borland was here'; Likewise, if you want to clear the text selected in the edit box control, you would write the call to the ClearSelection method like this: Edit1.ClearSelection; UsingObjectPascalwiththeVCL2-11, UsingtheobjectmodelIf you want to change several property values or call several methods for an object that is a field in a form object, you can use the with statement to simplify your code. The with statement is as convenient to use with objects as it is with records. For example, this is an event handler that makes several changes to a list box when a user clicks the button on the form: procedure TForm1.Button1Click(Sender: TObject); begin ListBox1.Clear; ListBox1.MultiSelect := True; ListBox1.Items.Add('One'); ListBox1.Items.Add('Two'); ListBox1.Items.Add('Three'); ListBox1.Sorted := True; ListBox1.Font.Style := [fsBold]; ListBox1.Font.Color := clPurple; ListBox1.Font.Name := 'Times New Roman'; ListBox1.ScaleBy(125, 100); end; If the code uses a with statement, it looks like this: procedure TForm1.Button1Click(Sender: TObject); begin with ListBox1 do begin Clear; MultiSelect := True; Items.Add('One'); Items.Add('Two'); Items.Add('Three'); Sorted := True; Font.Style := [fsBold]; Font.Color := clPurple; Font.Name := 'Times New Roman'; ScaleBy(125, 100); end; end; By using the with statement, you don’t have to qualify each reference to a ListBox1 property or method with the ListBox1 identifier. Within the with statement, all the properties and methods called are local in scope to the ListBox1 object variable.

Assigning values to object variables

You can assign one object variable to another object variable if the variables are of the same type or assignment compatible, just as you can assign variables of any type other than objects to variables of the same type or assignment-compatible types. For example, if objects TForm1 and TForm2 are derived from type TForm, and the variables Form1 and Form2 have been declared, you could assign Form1 to Form2: Form2 := Form1; 2-12Developer’ sGuide, UsingtheobjectmodelYou can also assign an object variable to another object variable if the type of the variable you are assigning a value to is an ancestor of the type of the variable being assigned. For example, here is a TDataForm type declaration and a variable declaration section declaring two variables, AForm and DataForm: type TDataForm = class(TForm) Button1: TButton; Edit1: TEdit; DataGrid1: TDataGrid; Database1: TDatabase; private { Private declarations } public { Public declarations } end; var AForm: TForm; DataForm: TDataForm; AForm is of type TForm, and DataForm is of type TDataForm. Because TDataForm is a descendant of TForm, this assignment statement is legal: AForm := DataForm; You might wonder why this is important. Taking a look at what happens behind the scenes when your application calls an event handler shows you why. Suppose you fill in an event handler for the OnClick event of a button. When the button is clicked, the event handler for the OnClick event is called. Each event handler has a Sender parameter of type TObject. For example, note the Sender parameter in this empty Button1Click handler: procedure TForm1.Button1Click(Sender: TObject); begin end; If you look back at Figure 2.3, “A simplified hierarchy diagram,” on page 2-8, you’ll recall that TObject is at the top of the Delphi Visual Component Library. That means that all Delphi objects are descendants of TObject. Because Sender is of type TObject, any object can be assigned to Sender. Although you don’t see the code that makes the assignment, the component or control to which the event happened is assigned to Sender. This means the value of Sender is always the control or component that responds to the event that occurred. How is this information useful? You can test Sender to find the type of component or control that called the event handler using the reserved word is. For example, if Sender is TEdit then DoSomething else DoSomethingElse; The drag-and-drop demonstration project shipped as part of the Delphi package has a project file named DRAGDROP.DPR. If you load the project and look at theUsingObjectPascalwiththeVCL2-13, UsingtheobjectmodelDROPFONT.PAS unit code, you’ll find that the Memo1DragOver method checks the type of an object variable. In this case, the parameter is Source rather than Sender: procedure TForm1.Memo1DragOver(Sender, Source: TObject; X, Y: Integer; State: TDragState; var Accept: Boolean); begin Accept := Source is TLabel; end; The Source parameter is also of type TObject. Source is assigned the object that is being dragged. The purpose of the Memo1DragOver method is to ensure that only labels can be dragged. Accept is a Boolean parameter, so it can have only one of two values, True or False. If Accept is True, the control the user has selected to drag can be dragged. If Accept is False, the user can’t drag the selected control. This expression is assigned to the Accept variable: Source is TLabel The is reserved word checks the Source variable to determine if Source is of type TLabel. If it is, the expression evaluates to True. Because the expression becomes True only when the user tries to drag a label, Accept becomes True only under the same circumstances. The next event handler in the drag-and-drop demonstration, Memo1DragDrop, also uses the Source parameter. The purpose of this method is to change the font of the text in the memo control to the font of the label dropped on the memo control. Here is the method’s implementation: procedure TForm1.Memo1DragDrop(Sender, Source: TObject; X, Y: Integer); begin Memo1.Font := (Source as TLabel).Font; end; When writing the assignment statement in this handler, the developer didn’t know which label the user would drag. By referring to the name of the label as (Source as TLabel), the font of the label the user dropped is assigned to Memo1.Font because Source contains the name of the control the user dragged and dropped. Only if Source is a label does the event handler allow the assignment to occur.

Creating nonvisual objects

Most of the objects you use in Delphi are components you can see at both design time and runtime, such as edit boxes and string grids. A few, such as common dialog boxes, are components you don’t see at design time, but they appear at runtime. Still others, such as timers and data source components, never have any visual representation in your application at runtime, but they are there for your application to use. Occasionally you might want to create your own nonvisual objects for your application. For example, you might want to create a TEmployee object that contains Name, Title, and HourlyPayRate fields. You could then add a CalculatePay method that uses the data in the HourlyPayRate field to compute a paycheck amount for a given period. 2-14Developer’ sGuide, UsingtheobjectmodelThe TEmployee type declaration could look like this: type TEmployee = class(TObject) Name: string[25]; Title: string[25]; HourlyRate: Double; function CalculatePayAmount: Double; end; In this case, TEmployee is derived from TObject. TEmployee contains three fields and one method. Because it is derived from TObject, TEmployee also contains all methods of TObject (TObject has no fields). Place object type declarations you create without the help of Delphi in the type declaration part of your unit along with the form type declaration. In the variable declaration part of the unit, you need to declare a variable of the new type: var Employee: TEmployee; Creating an instance of an object TEmployee is just an object type. An actual object doesn’t exist in memory until the object is instantiated, or created, by a constructor call. A constructor is a method that allocates memory for the new object and points to the new object, which is called an instance of the object type. Calling the Create method, the constructor, assigns this instance to a variable. If you want to declare an instance of the TEmployee type, your code must call Create before you can access any of the fields of the object: Employee := TEmployee.Create; In the TEmployee type declaration, there isn’t a Create method, but because TEmployee is derived from TObject, and TObject has a Create method, TEmployee can call Create to create a TEmployee instance, which is assigned to the Employee variable. Now you are ready to access the fields of an Employee object, just as you would any other Delphi object. Destroying your object When you are through using your object, you should destroy it, which releases the memory the object used. You destroy an object by calling a destructor, a method that releases the memory allocated to the object. Delphi has two destructors you can use, Destroy and Free. You should almost always use Free. In its implementation, the Free method calls Destroy, but only when the instance pointer isn’t nil; in other words, the pointer still points to the instance. For this reason, it is safer to call Free than Destroy. Also, calling Free is slightly more efficient in the resulting code size. UsingObjectPascalwiththeVCL2-15, UsingcomponentsTo destroy the Employee object when you have finished using it, you would make this call: Employee.Free; Just as it inherits the Create method, TEmployee inherits Free from TObject. To follow good programming practices, you should place your destructor call in the finally part of a try..finally block, while placing the code that uses the object in the try part. This assures that the memory allocated for your object is released even if an exception occurs while your code is attempting to use the object. You can read about exceptions and what a try..finally block is in the Object Pascal Language Guide.

Using components

Delphi uses components extensively to aid the development of applications. Components are objects that are used to create the UI and provide additional non- visual functionality to programs. In Delphi, the Component palette has most of the components you will use when creating applications, and all components are derived from the TComponent class of the VCL. Common Delphi components include TLabel and TTrackBar. You can also create your own components that provide functionality not provided by the VCL. For more information on creating custom components, see Part IV, “Creating custom components.”

Understanding the VCL

Each component you put on a form becomes a user-interface object, a database object, or a system function, such as a system timer. By placing components on forms, you build the interface of your application. Components are objects in the true object-oriented programming (OOP) sense. Because they are objects, Delphi components • Encapsulate some set of data and data-access functions • Inherit data and behavior from the objects they are derived from (called their ancestors) • Operate interchangeably with other objects derived from a common ancestor, through a concept called polymorphism Although each component has its own unique aspects, all components share certain qualities they inherit from a common ancestor object called TComponent. TComponent defines the minimum set of attributes necessary for a component to operate in the Delphi environment. The important thing to understand about components at this point is that they contain three kinds of information: • State information. Information about the present condition of a component is called a property of the component. Properties are named attributes of a component that a user or application can read or set. Examples of component properties are Height and Color. 2-16Developer’ sGuide, Usingcomponents• Action information. Components generally have certain actions they can perform on demand. These actions are defined by methods, which are procedures and functions you call in code to tell the component what to do. Examples of component methods are Hide and Refresh. • Feedback information. Components provide opportunities for application developers to attach code to certain occurrences, or events, making components fully interactive. By responding to events, you bring your application to life. Examples of component events are OnClick and OnKeyDown.

Delphi’s standard components

The Delphi Component palette contains a large selection of components you can use in your Delphi applications. You can add, remove and rearrange components on the palette, as you choose. You can also create component templates that are made up of a number of components and add the templates to the Component palette. The components on the palette are grouped into pages according to similar functions. Table 2.1 lists the default pages and the kinds of components they contain. Table 2.1 The Delphi Component palette pages Page name Contents Standard Standard Windows controls, menus Additional Customized controls Win32 Windows 95/NT 4.0 common controls System Components and controls for system-level access, including timers, file system, multimedia, and DDE Internet Components that help manage client/server apps: components for various internet communications protocols; sockets for managing low-level TCP/IPs; objects for building Web server apps Data Access Non-visual components that access databases, tables, queries, and reports Data Controls Visual, data-aware controls Decision Cube Controls that let you summarize information from databases and view it from a variety of perspectives, changing the focus of the analysis on-the-fly (not available in all versions) QReport Quick Report components for easily creating embedded reports Dialogs Windows common dialog boxes Win 3.1 Components for compatibility with Delphi 1.0 projects. Also, the box components on this page are useful for building custom dialog boxes that will look compatible with 32-bit dialog boxes. Samples Sample custom components, including gauge, color grid, spin buttons, directory outline, calendar grid, IBEvent Alerter ActiveX Sample ActiveX controls MIDAS Midas components used for creating multi-tiered database applications (not available in all versions) For information on each component you see on the default palette, see online Help: on the Contents page of Help, choose Using Delphi|Basic Skills|Working with components|About the Component palette. UsingObjectPascalwiththeVCL2-17, Usingcomponents

Choosing the right component

Delphi provides a large number of different components, many of which perform similar functions, but in specialized ways. This section is designed to help you choose among those groups of similar components. First it describes those properties common to all visual components. Then it discusses different functional groups of components, how to choose among them, and briefly describes the properties that make each component in a group unique. Properties common to all components All visual components have a number of properties in common. Once you have an understanding of what is in every visual component, you can better evaluate the unique properties that define the various specialized components you’ll use to create your applications. The common properties fall into these general categories: • Position and size properties • Display properties • Parent properties • Navigation properties • Drag-and-drop properties Position and size properties Four properties define the position and size of a control on a form: • Height sets the vertical size • Width sets the horizontal size • Top positions the top edge • Left positions the left edge These properties aren’t accessible in nonvisual components, but Delphi does keep track of where you place the component icons on your forms. Most of the time you’ll set and alter these properties by manipulating the control’s image on the form or using the Alignment palette. You can, however, alter them at runtime. Display properties Four properties govern the general appearance of a control: • BorderStyle specifies whether a control has a border. • Color changes the background color of a control. • Ctrl3D specifies whether a control will have a 3-D look or a flat border. • Font changes the color, name, style, or size of text. You can set aspects of the font, such as its size and color, individually, or you can use a Font dialog to change several aspects at a time. 2-18Developer’ sGuide, UsingcomponentsParent properties To maintain a consistent look across your application you can make any control look to its container for display properties. Setting the parent property to True for ParentColor, ParentFont, or ParentCtrl3D will make display edits automatically update across all components on a form. If you change Font, Color or Ctrl3D after you set the corresponding parent property to True, your color or font choice will take effect and the associated parent property will be set to False. Navigation properties Several properties determine how users navigate among the controls in a form: • Caption contains the text string that labels a component. To underline a character in a string, include an ampersand (&) before the character. This type of character is called an accelerator character. The user can then select the control or menu item by pressing Alt while typing the underlined character. • TabOrder indicates the position of the control in its parent’s tab order, the order in which controls receive focus when the user presses the Tab key. Initially, tab order is the order in which the components were added to the form, but you can change this by changing TabOrder. TabOrder is meaningful only if TabStop is True. • TabStop determines whether the user can tab to a control. If TabStop is True, the control is in the tab order. Drag-and-drop properties Two component properties affect drag-and-drop behavior: • DragMode determines how dragging starts. By default, DragMode is dmManual, and you must call the BeginDrag method to start dragging. When DragMode is dmAutomatic, dragging starts as soon as the mouse button goes down. • DragCursor determines the shape of the mouse pointer when the pointer is over a component that will accept an object being dragged. Drag-and-dock properties The following component properties control drag-and-dock behavior: • DockSite • DragKind • DragMode • FloatingDockSiteClass • AutoSize For more information, see “Implementing drag-and-dock in controls” on page 6-5. UsingObjectPascalwiththeVCL2-19, UsingcomponentsText controls Many applications require the user to enter text or need to present text to the user. The type of control used to hold the information depends on the size and format of the information. Use this component: When you want users to do this: Edit Edit a single line of text Memo Edit multiple lines of text MaskEdit Adhere to a particular format, such as a nine-digit postal code or a phone number RichEdit Edit unlimited lines of text or use rich text format Properties common to all text controls All of the text controls have these properties in common: • Text determines the text that appears within an edit box or memo control. • CharCase forces the case of the text being entered to lowercase, mixed case or uppercase. • ReadOnly specifies whether the user is restricted from making changes. By contrast, all edit controls can be edited. • MaxLength limits the number of characters in an edit box. • PasswordChar hides the text the user types by assigning a single character. PasswordChar is most often used to show asterisks instead of a user’s password. • HideSelection specifies whether selected text will remain highlighted when the control does not have focus. Properties shared by memo and rich text controls In addition to the properties common to all text controls, memos and rich text controls have several other properties in common with each other: • Alignment specifies how text is aligned (left, right, or center) within the component. • Lines contains the text of a memo or rich edit component as a list of strings. • OEMConvert determines whether the text in the control is converted to OEM characters. If True, the text is converted. Otherwise, the characters remain as ANSI characters. • WordWrap determines whether the text will wrap at the right margin. • WantReturns determines whether return characters the user enters in the memo or rich edit components by pressing Enter affect the text in the control or go to the form. If WantReturns is True and the user presses Enter, a return character is entered in the memo or rich edit component. Otherwise, the return goes to the form. • Set WantTabs property to True to enable users to type tabs in a memo control. 2-20Developer’ sGuide, UsingcomponentsMemo controls A memo control handles text much like an edit-box component, but the memo component can handle multiple text lines. • The text in the memo is the value of the Text property. Your application can tell if the text changes by checking the Modified property. • To have the text in a memo automatically selected whenever it becomes the active control, set the AutoSelect property to True. • At runtime, you can select all the text in the memo with the SelectAll method. • To find out which text in the memo the user has selected, or to replace selected text, use the SelText property. • To select only part of the text or to find out what part of the text is selected, use the SelStart and SelLength properties. Rich text controls The rich text component is a memo control that supports rich text formatting. That is, you can change the formatting of individual characters, words, or paragraphs. It includes a Paragraphs property that contains information on paragraph formatting. Rich text controls also have printing and text-searching capabilities. By default, the rich text editor supports • Font properties, such as typeface, size, color, bold, and italic format • Format properties, such as alignment, tabs, indents, and numbering • Automatic drag-and-drop of selected text • Display in both rich text and plain text formats. Set PlainText to True to remove formatting. Specialized input controls The following components provide additional ways of capturing input. Use this component: When you want users to do this: ScrollBar Select values on a continuous range TrackBar Select values on a continuous range (more visually effective than scroll bar) UpDown Select a value from a spinner attached to an edit component HotKey Enter Ctrl/Shift/Alt keyboard sequences Scroll bars The scroll bar component is a Windows scroll bar, used to scroll the contents of a window, form, or control. In the OnScroll event handler, you write code that determines how the window, form, or control behaves in response to the user scrolling the scroll bar. • LargeChange determines how far a thumb tab moves when the user clicks the scroll bar on either side of the thumb tab. UsingObjectPascalwiththeVCL2-21, Usingcomponents• SmallChange determines how far the thumb tab moves when the user clicks the arrows at the end of the scroll bar to scroll or uses the arrow keys on the keyboard. The default value is 1. • Min and Max property values together determine how many positions are available on the scroll bar for the thumb tab to move when the user scrolls the scroll bar. • Your application can set the position of the thumb tab with the Position property, or use the property to determine how far the scroll bar has scrolled. Track bar controls The track bar can set or adjust integer values on a continuous range, such as volume or brightness. A track bar consists of a bar that defines the extent or range of the adjustment, and an indicator that both shows the current value for the control and provides the means for changing the value. The user moves the slide indicator by dragging to a particular location or clicking in the hot zone area of the bar, which moves the slide indicator directly to that location. • Use the Max and Min properties to set the upper and lower range of the track bar. • Use SelEnd and SelStart to highlight a selection range. See Figure 2.4. • By default, a track bar has one row of ticks along the bottom. Set the TickMarks property to tmNone to remove them, or tmBoth to put ticks on both the top and bottom. See Figure 2.4. • You can build either a horizontal or vertical track bar with the Orientation property. • TickStyle controls automatic or manual tickstyle. • Position sets a default position for the track bar, it also tracks the value the user has selected at runtime. Figure 2.4 Three views of the track bar component • By default, users can move one tickmark up or down by pressing the up and down arrow keys. Set LineSize to change that increment. • Set PageSize to determine the number of ticks moved when the user presses Page Up and Page Down. Up-down controls Up-down controls are text boxes that accept a limited set of discrete ordered input values that make up a circular loop. An up-down control is a combination of a text box and a special control that incorporates a pair of buttons, it is similar to the spin button. 2-22Developer’ sGuide, UsingcomponentsWhen the user clicks the text box or the buttons, the input focus is set to the text box of the control. The user can type a value directly into the control or use the buttons to increment or decrement the value. • Use Associate to attach the up-down control to a text control. • Use the AlignButton property to place the up-down component to the left or right of its associated text control. • To switch the orientation from vertical to horizontal, set Orientation to horizontal. • Use Max and Min to set limits on the values the up-down control can have. • By default, you can use the arrow keys to increment and decrement the up-down control. To disable keyboard interaction, set ArrowKeys to False. • The Position property holds the value as the control moves up or down. Use Position to set the default value at design time. • When the value of an up-down control is over 1000, it automatically places a separator between every three digits. To remove the separator, set Thousands to False. • By default, an up-down control stops at its maximum value. If you want the control to move back to the minimum value when it reaches the maximum, set Wrap to True. Hot key controls Use the hot key component to assign a hot-key sequence to transfer focus to any component. The HotKey property contains the current key combination of the hot-key control. Alt+A is the default. To modify the value of the HotKey property either edit it directly or set the value of the Modifiers property. • The Modifiers property allows selection of one or more modifier keys such as Ctrl, Alt, Extra, or Shift for the hot key. Modifier keys are used in combination with another non-modifier key such as characters, function keys, or arrow keys. • The InvalidKeys property allows the user to specify that one or more modifier keys should be considered invalid. If this property is set a default modifier should also be specified in the Modifiers property. Splitter control Add a splitter to a form between two aligned controls to allow users to resize the controls. The splitter sits between a control aligned to one of the edges of the form and the controls that fill up the rest of the client area. Give the splitter the same alignment as the control that is anchored to the edge of the form. Use each control on the form as a separate pane. After each pane is placed, place a splitter with the same alignment to allow that pane to be resized. The last pane you place on the form should be client-aligned, so that it resizes automatically to fill up the remaining space after all other panes are resized. • Set MinSize to specify a minimum size the splitter must leave when resizing its neighboring control. • Set Beveled to True to give the splitter’s edge a 3D beveled look. UsingObjectPascalwiththeVCL2-23, Usingcomponents

Buttons and similar controls

Aside from menus, buttons provide the most common way to invoke a command in an application. There are several variations of the standard text-based button: Use this component: To do this: Button Present command choices BitBtn Present command choices on buttons with glyphs SpeedButton Create toolbar buttons CheckBox Present Boolean options RadioButton Present a set of mutually exclusive choices ToolBar Arrange tool buttons and other controls in rows and automatically adjust their sizes and positions. CoolBar Display a collection of windowed controls within movable, resizable bands Button controls A button component is a push button control. Users choose button controls to initiate actions. Double-clicking a button at design time takes you to the button’s OnClick event handler in the Code editor. • Set Cancel to true if you want the button to trigger its OnClick event when the user presses Esc. • Set Default to true if you want the Enter key to trigger the button’s OnClick event. Enter triggers OnClick even if another control has focus. Bitmap buttons A bitmap button component is a push button control that can include a bitmap on its face. You can choose from predefined bitmap button styles or specify your own bitmap. • To choose a custom bitmap for your button, set the Glyph property. • Use Kind to automatically configure a button with a glyph and default behavior. • By default, the glyph is to the left of any text. To move it, use the Layout property. • The glyph and text are automatically centered in the button. To move their position, use the Margin property. Margin determines the number of pixels between the edge of the image and the edge of the button. • By default, the image and the text are separated by 4 pixels. Use Spacing to increase or decrease the distance. • Bitmap buttons can have 3 states: up, down, and held down. Set the NumGlyphs property to 3 to show a different bitmap for each state. Speed buttons Speed button components are buttons that usually have graphical images on their faces that execute commands or set modes. They have some unique capabilities that allow them to work as a set. Speed buttons are commonly used with panels to create toolbars. 2-24Developer’ sGuide, Usingcomponents• By default, speed buttons appear in an up (unselected) state. To initially display a speed button as selected, set the Down property to True. • If AllowAllUp is True, all of the speed buttons in a group can be unselected, or “up” state. Set AllowAllUp to False if you want a group of buttons to act like a radio group. • To make speedbuttons act as a group, give the GroupIndex property of all the buttons the same non-zero value. Adding speed buttons to toolbars is discussed in Chapter 5. Check boxes A check box lets the user make a binary decision like yes/no, male/female. • Set Checked to True and a check mark appears in the check box, indicating the option is selected. • The value of the AllowGrayed property determines if a check box can have two or three possible states. If AllowGrayed is False, the default value, clicking a check box alternately checks and unchecks it. If AllowGrayed is True, clicking a check box either checks, grays, or unchecks it. • The State property determines the various states a check box control can have. cbUnchecked: the check box has no check mark indicating the user hasn’t selected the option. cbChecked: the check box has a check mark in it indicating the user has selected the option, cbGrayed: the check box is gray indicating a third state that is neither checked nor unchecked. Your application determines the meaning of a grayed check box. Radio buttons Radio buttons present a set of mutually exclusive choices. You can use the radio button component to place individual buttons, or use the radio group component to set a rectangular region with a Caption property, and list your radio-button items using the Items property. See “Grouping components” on page 2-28 for more information. • Use the Caption property to set a value for the radio button. • When Checked is True, a black circle appears in the radio button, indicating that the option is selected. Toolbars Toolbars provide an easy way to arrange and manage visual controls. You can create a toolbar out of a panel component and speed buttons, or you can use the ToolBar component, then choose New Button from its SpeedMenu for each button you want. Using the last way has these advantages: • All tool buttons on a toolbar maintain a uniform width and height. • Other controls you place on the toolbar are held in place by invisible tool buttons and maintain a uniform height. UsingObjectPascalwiththeVCL2-25, Usingcomponents• Controls can automatically wrap around and start a new row when they do not fit horizontally on the toolbar. • Spaces and dividers (which are specially configured tool buttons) can group controls on the toolbar. • You have added display options, such as pop-up borders and transparency. Adding a toolbar to a form is discussed in Chapter 5. Cool bars A cool bar contains child controls that can be moved and resized independently. Each control resides on an individual band. The user positions the controls by dragging the sizing grip to the left of each band. The cool bar requires version 4.70 or later of COMCTL32.DLL (usually located in the WINDOWS\SYSTEM or WINDOWS\SYSTEM32 directory) at both design time and runtime. • The Bands property holds a collection of TCoolBand objects. At design time, you can add, remove, or modify bands with the Bands editor. To open the Bands editor, select the Bands property in the Object Inspector, then double-click in the Value column to the right, or click the ellipsis (...) button. You can also create bands simply by adding new windowed controls from the palette. • You can specify if a user can reorder the bands by setting the FixedOrder property to True or False. • Specify if the bands automatically maintain a uniform height or width with the FixedSize property.

Handling lists

Lists present the user with a collection of items to select from. Several components display lists: Use this component: To do this: ListBox Display a list of text strings; the user may select one or more. ComboBox Display collapsible list of text strings; this component conserves form space. TreeView Display items hierarchically. ListView Display items graphically. ImageList Manage a list of images. CheckListBox Display a list with check boxes in front of each item. DateTimePicker Display a list box for entering dates or times. Properties common to list controls The following properties are common to the display of list controls: • Columns specifies the number of columns in a list control. 2-26Developer’ sGuide, Usingcomponents• IntegralHeight specifies whether the list box shows only entries that fit completely in the vertical space. If IntegralHeight is False, the bottom of the list box is at the location determined by its ItemHeight property, and the bottom item visible in the list might not be complete. • ItemHeight specifies the height of a list item in pixels. The Style property can cause ItemHeight to be ignored. The following properties are common to the control of lists: • Items uses a string list to fill the list control with values. See “Working with string lists” on page 2-44 for information on how to use string lists. • ItemIndex indicates which item in the list is selected. • MultiSelect specifies whether a user can select more than one item at a time. The user can then use the Ctrl key to select multiple noncontiguous items, or the Shift key to select a range of items. • Sorted determines whether the list will be sorted in alphabetical order. List boxes A list box displays a list from which users can select one or more items. The following properties are key to using a list box. • You can add, delete, and insert items in the list box using the Add, Delete, and Insert methods of the Items property, which is a string list. • When MultiSelect is set to True, users can select more than one item. By default, the multiselect process for list boxes is to click on individual items with the Ctrl key pressed. Holding down the Shift key has no effect. To activate Shift to select a range of values, set both MultiSelect and ExtendedSelect to True. • The Style property determines how a list box displays its items. By default, Style is lbStandard, meaning that the list box displays each item as a string. By changing the value of Style, you can create owner-draw list boxes, meaning that items can be graphical and of either fixed or varying height. For a discussion of implementing owner-draw controls in your application, see “Adding graphics to controls” on page 6-13. Combo boxes A combo box component is a control that combines an edit box with a list much like that of a list box. Users can either type text in the edit box or select an item from the list. • When users enter data into the combo box, either by typing text or selecting an item from the list, the value of the Text property changes. • By default, the drop-down list will display eight items. To increase or decrease displayed items, assign DropDownCount a new value. UsingObjectPascalwiththeVCL2-27, UsingcomponentsCombo boxes have five different styles. Use the Style property to select the type of combo box you need: • Use csDropdown if you want a list of values like a list box but don’t want to see the list of values until the user pushes the button. By default the text area will be editable. • Use csDropDownList style to make the combo box edit region read-only. The user can’t edit an item or type in a new item, but can select items from the list. • csSimple to create a combo box with a fixed-size list that does not close up. Tree views A tree view control is a special list box control that displays a set of objects as an indented outline based on their logical hierarchical relationship. The control includes buttons that allow the outline to be expanded and collapsed. You can use a tree view to display the relationship between a set of containers or other hierarchical elements. You can optionally include icons with the text label of each item in the tree. Different icons can be displayed when the user expands or collapses the item in the tree. In addition, you can also include a graphic, such as a check box, that can be used to reflect state information about the item. The control also supports drawing lines that define the hierarchical relationship of the items in the list and buttons for expanding and collapsing the outline. • Indent sets the number of pixels horizontally separating items from their parents. • Set ShowButtons to False to remove the ‘+’ button that indicates the item can be expanded. • Set ShowLines to False to remove the connecting lines of the tree view. • Set ShowRoot to False to remove the lines connecting the root (top level) items. DateTimePicker DateTimePicker displays a list box for entering dates or times. To use DateTimePicker, you must have the latest version of COMCTL32.DLL (usually located in the WINDOWS\SYSTEM or WINDOWS\SYSTEM32 directory). Grouping components It is much easier for users to navigate through an application when the information is presented in groups. Delphi provides several components that can be used to group information: Use this component: When you want this: GroupBox A standard group box with a title RadioGroup A simple group of radio buttons Panel A more visually flexible group of controls ScrollBox A scrollable region containing controls TabControl A set of mutually-exclusive notebook-style tabs 2-28Developer’ sGuide, UsingcomponentsUse this component: When you want this: PageControl A set of mutually-exclusive notebook-style tabs with corresponding pages, each of which may contain other controls HeaderControl To provide titles for columns of data Group boxes The group box component is a standard Windows group box. Use a group box component to group related controls on a form. The most commonly grouped controls in a group box are radio buttons. Place the group box on the form, then select the components you want to appear in the group box from the Component palette, and place them in the group box. The text that identifies the grouping appears as the value of the Caption property. Radio groups The radio group component simplifies the task of grouping radio buttons and getting them to work together. The radio group gives you a rectangular region with a Caption property. • Use the Items property of the radio group component to specify the choices in the box: click the ellipsis (...) button in the Value column on the Properties page of the Object Inspector to open the String List editor. Type the strings you want to appear next to the radio buttons, and press Enter. • You cannot select individual radio buttons in a radio group component. To respace the buttons, resize the radio group component. To edit their names, use the String List editor. Panels The panel component is used to place panels on a form on which other controls can be placed. For example, you can create a panel which is a toolbar. Panels are containers. Use a bevels component instead for purely visual effects. Panels can be aligned with the form so that they maintain the same relative position to the form even when the form is resized. • The BorderWidth property determines the width in pixels of the border around a panel. The default value is 0, which means no border. • The Locked property determines whether a panel is replaced by an in-place active OLE object. If Locked is False, the OLE server can replace the panel. If Locked is True and the panel is aligned to one of the edges of the form, then the panel remains when an OLE object in an OLE container component is activated in place. Use Locked to prevent status bars and the like from being replaced. Page controls The page control component is a page set that is used to make a multiple-page dialog box. To create a new page in a page control at design time, right-click the page control and choose New Page. UsingObjectPascalwiththeVCL2-29, Usingcomponents• The ActivePage property specifies the active tab sheet object. • Set Multiline to True to allow multiple lines. • PageCount specifies the number of pages. • Pages gives you access to the pages of the tab sheet object. • TabHeight property sets the height in pixels of the tabs. TabWidth is the horizontal size in pixels of the individual tabs. If zero, TabWidth tabs will automatically size themselves to fit their text. Tab controls The tab control component has the appearance of notebook dividers. To create a multiple page dialog box, use a page control. The tab control replaces the tab set included in Delphi 1.0. Scroll boxes Scroll box components make it possible to create scrolling areas on a form that are smaller than the entire form. There are many times when an application needs to display more information than will fit in a particular area. Some controls, such as list boxes and memos can automatically scroll their contents. But other controls, and sometimes even forms full of controls, need to be able to scroll. Delphi handles these scrolling regions with a control called a scroll box. A scroll box is much like a panel or a group box, in that it can contain other controls, but a scroll box is normally invisible. However, if the controls contained in the scroll box cannot all fit in the visible area of the scroll box, it automatically displays one or two scroll bars, enabling users to move controls outside the visible region into position where they can be seen and used. • Set the boundaries of the scroll-box to the region you want to scroll. • Use the Kind property to determine whether a scroll box has horizontal or vertical scroll bars. • You can use the Align property of a scroll box to allow the scroll box to adjust its area to a form or a part of a form Header controls The header component is a sectioned visual control that displays text and allows each section to be resized with the mouse. At design time, resize a section by clicking the mouse button on a section border and dragging to the new size. At runtime, the user can resize the header by clicking and dragging with the left mouse button. The widths of the other sections that are not resized remain unchanged. • Use the Sections property to specify the separate sections in a header. The control also supports the user dragging on the divisions that separate header parts to set the width of each column. As an option, you can support double-clicking on a division as a shortcut to a command that applies to formatting the column, such as automatically sizing the column to the largest value in that column. 2-30Developer’ sGuide, Usingcomponents

Visual feedback

Well-designed applications provide users with information pertaining to the current state of the application. This information is conveyed using the following components: Use this component or property: To do this: Label Display non-editable text strings ProgressBar Display the percentage of work remaining for a particular task StatusBar Display a non-editable status region at the bottom of a window StaticText Display non-editable text in a component that has a window handle Hint and ShowHint Activate fly-by of “tool-tip” help HelpContext Link in online help systems Labels The label component displays text on a form. Usually labels are placed adjacent to edit boxes or other controls. The label is a nonwindowed control, so it cannot receive focus. • Caption contains the text string for the label. • The FocusControl links the label control with another control on the form. If the caption of a label includes an accelerator key, the control specified in the FocusControl property becomes the focused control when the user uses the accelerator key. • If you want a label to appear on top of a graphic, but you want to be able to see through the label so that part of the graphic/bitmap isn’t hidden, set the Transparent property to True. • The ShowAccelChar property determines how an ampersand (&) in the caption of a label appears. If ShowAccelChar is True, an ampersand appears as an underline under the character to its right in the caption indicating the underlined character is an accelerator character. Otherwise, the ampersand character appears as an ampersand. Progress bars A progress bar is a control you can use to show the percentage of completion of a lengthy operation. It consists of a rectangular bar that “fills” from left to right. Use the control as feedback for long operations or background processes. Figure 2.5 A progress bar • Use Position to set a default position for the progress bar. At runtime, Position tracks the exact location as values increment. • Use Max and Min to set the range of Position. UsingObjectPascalwiththeVCL2-31, Usingcomponents• By default, the progress meter will advance by a value of one. Assign StepBy a higher number to increase the increments. See the StepIt method in online Help for more information. Status bars A status bar usually occupies the bottom of a window and usually displays text. Although you can use Delphi’s panel component to make a status bar, it is simpler to use the Windows status-bar component. You will generally subdivide the status bar into multiple text areas. • The status-bar component automatically has its Align property set to alBottom, which takes care of both positioning and size. • To create subpanels in the status bar component, use the Panels property: in the Values column of the Object Inspector, click the ellipsis (..) button to open the Panels editor. • Set each panel’s Width property while the Panels editor is open and a panel is selected in the Panels editor. Set Alignment and other properties at this time, as well. • Add code to the individual status panels to reflect the status of the application. Note An example application in the DEMOS\DOC\GRAPHEX directory shows a two- panel status bar that reflects a drawing’s origin in the first panel when the user presses the mouse button, and it tracks the current position in the other panel. You can study the code for that status bar. Help or hint properties for a component The following properties are common to all controls that show help and hints: • Hint is the text string that can appear when the user moves the mouse pointer over a control or menu item. To make the hint appear when the user moves the mouse over the component, set ShowHint to True. • ParentShowHint quickly shows all defined hints for controls in a group. Like the parent properties for font and color, ParentShowHint determines where a control looks to find out if hints should be shown. • HelpContext establishes a Help context number for the control. Tabular display Grids consist of a set of rows and columns that define a collection of cells. What appears in each of the cells depends on the application and the type of grid control used: Use this component: To do this: DrawGrid Display an existing data structure in a grid StringGrid Display a grid of text strings DBGrid Display the contents of a dataset 2-32Developer’ sGuide, UsingcomponentsDraw grids A draw grid component is a grid control that permits the display of an existing data structure in column and row format. • The grid uses the OnDrawCell event to fill in the cells of the grid. If the DefaultDrawing property is False, the code you write in the OnDrawCell event handler draws in the cells. If DefaultDrawing is True, the contents of the cells are automatically drawn using some default values. • You can obtain the drawing area of a cell with the CellRect method. The MouseToCell method returns the column and row coordinates of the cell the mouse cursor is in. • You can determine which cell is selected in the grid by checking the value of the Selection property. • You can change the appearance and behavior of a data grid by changing the value of the Options property. For example, you can choose to allow the user to use the Tab key to move to a new column, you can decide to display grid lines between columns but not between rows, or let the user edit the data displayed in the grid. • Several properties affect the appearance of the grid. The DefaultColWidth and DefaultRowHeight properties determine the default widths and heights of the columns and rows. • You can change the width or height of a specific column or row with the ColWidths and RowHeights properties. You can choose to have fixed or nonscrolling columns and rows with the FixedCols and FixedRows properties, and you can assign the color of the fixed columns and rows with the FixedColor property. Set the width of the grid lines with the GridLineWidth property. Add scroll bars to the grid with the ScrollBars property. • You can determine which row is currently the top row in the grid, or set a specified row to be the top row with the TopRow property. To determine which column is the first visible column in the grid, use the LeftCol property. The values of the VisibleColCount and VisibleRowCount properties are the number of columns and rows visible in the grid. In addition to these properties, methods, and events, this component also has the properties, methods, and events that apply to all windowed controls. String grids The string grid component is a grid control designed to simplify the handling of strings and associated objects while maintaining all the features of the draw grid component. • All the strings in a string grid are contained in the Cells property, which you can use to access a particular string within the grid. All the objects associated with the strings in a string grid are contained in the Objects property. • All the strings and their associated objects for a particular column can be accessed using the Cols property. The Rows property gives you access to all the strings and their associated objects for a particular row. UsingObjectPascalwiththeVCL2-33, UsingcomponentsIn addition to these properties, methods, and events, this component also has the properties, methods, and events that apply to all windowed controls. Graphic display Graphics, when used appropriately, can add that extra bit of flash and style which makes an application stand out. To incorporate graphics into your Delphi applications use the following components: Use this component To do this: Image Display the contents of a graphics file Shape Display a geometric shape Bevel Display 3D lines and frames PaintBox Programmatically display graphics Animate Silently display an AVI file Images The image component displays a graphical image, like a bitmap, icon, or metafile on a form. Delphi includes a library of images to choose from in the IMAGES subdirectories. Use the following properties to set up the image component. • Select an image to display by clicking on the Picture property. • By default, any image is centered in the Image component, to align the image to the upper left corner, change Center to False. • To make the image shrink or grow with the border, change Stretch to True. Shapes The shape component displays a geometric shape on the form. It is a nonwindowed component and cannot receive user input. • You determine which geometric shape the shape control assumes by setting the Shape property. • To change the color of the border, use BorderColor. • To change the color of the shape or to add a pattern, use the Brush property to fill it. How the shape is painted depends on the two nested properties of the Brush object, Color and Style. Bevels The bevel component lets you put beveled lines, boxes, or frames on the forms in your application. • A panel component has two bevels, an outer bevel drawn next to the border of the control, and an inner bevel drawn inside the outer bevel. Use BevelInner and BevelOuter to determine the style of the bevel: none, raised, or lowered. • The BevelWidth property determines the width in pixels between the inner and the outer bevels of a panel. 2-34Developer’ sGuide, UsingcomponentsPaint boxes The paint box component provides a way for your application to draw on the form using code in a specified rectangular area. It prevents drawing outside the boundaries of the paint box. Once a paint box is added to your form, your application can use the OnPaint event handler to draw on the paint box’s Canvas. For information on how to draw on a canvas, see Chapter 7, “Working with graphics.” Animation control The animation component is a window that silently displays an Audio Video Interleaved (AVI) clip. An AVI clip is a series of bitmap frames, like a movie. Although AVI clips can have sound, animation controls work only with silent AVI clips. The files you use must be either uncompressed AVI files or AVI clips compressed using run-length encoding (RLE). These are some of the properties of an animation component: • ResHandle is the Windows handle for the module that contains the AVI clip as a resource. Set ResHandle at runtime to the instance handle or module handle of the module that includes the animation resource. After setting ResHandle, set the ResID or ResName property to specify which resource in the indicated module is the AVI clip that should be displayed by the animation control. • Set AutoSize to True to have the animation control adjust its size to the size of the frames in the AVI clip. • StartFrame and StopFrame specify in which frames to start and stop the clip. • You can set CommonAVI to display one of the common Windows AVI clips provided in Shell32.DLL. • Specify when to start and interrupt the animation by setting the Active property to True and False, respectively, and how many repetitions to play by setting the Repetitions property. • The Timers property lets you display the frames using a timer. This is useful for synchronizing the animation sequence with other actions, such as playing a sound track. Windows common dialog boxes The dialog box components on the Dialogs page of the Component palette make the Windows “common” dialog boxes available to your Delphi applications. These dialog boxes provide all Windows-based applications with a familiar, consistent interface that enables the user to perform common file operations such as opening, saving, and printing files. Each dialog box opens when its Execute method is called. Execute returns a Boolean value: if the user chooses OK to accept any changes made in the dialog box, execute returns True; if the user chooses Cancel to escape from the dialog box without making or saving changes, Execute returns False. UsingObjectPascalwiththeVCL2-35, Usingcomponents

Setting component properties

Delphi provides many different ways to set component properties at design time. You can also change property values while an application is running. This is discussed in the section, “Setting properties at runtime” on page 2-38. For more information about the properties for each component, use the help index and select the component whose properties you want to view. You can also press F1 with the component selected in the form. Note The components on the ActiveX and Samples pages of the Component palette are provided as examples only. How the Object Inspector displays properties The Object Inspector dynamically changes the set of properties it displays, based on the component selected. The Object Inspector has several other behaviors that make it easier to set component properties at design time. • When you use the Object Inspector to select a property, the property remains selected in the Object Inspector while you add or switch focus to other components in the form, provided that those components also share the same property. This enables you to type a new value for the property without always having to reselect the property. If a component does not share the selected property, Delphi selects its Caption property. If the component does not have a Caption property, Delphi selects its Name property. • When more than one component is selected in the form, the Object Inspector displays all properties that are shared among the selected components. This is true even when the value for the shared property differs among the selected components. In this case, the property value displayed is either the default or the value of the first component selected. When you change any of the shared properties in the Object Inspector, the property value changes to the new value in all the selected components. There is one notable exception to this: when you select multiple components in a form, the Name property no longer appears in the Object Inspector, even though all components have a Name property. This is because you cannot assign the same value for the Name property to more than one component in a form. Tab-jumping to property names in the Object Inspector Pressing Tab acts as a toggle between the Value column and the Property column of the Object Inspector. Whenever you are in the Value column (as you are by default when you select a property with the mouse), pressing Tab moves you to the Property column. 2-36Developer’ sGuide, UsingcomponentsTo navigate quickly to a property in the Object Inspector, 1 Activate the Property column (just press Tab if your cursor is in the value column). 2 Type the first letter of the property you want. 3 The cursor jumps to the first property beginning with that letter. 4 Type the second letter of the property, and so on, until the cursor is at the property you want. Now, pressing Tab places the cursor in the Value column, where you can begin entering your edits. Displaying and setting shared properties You can set shared properties to the same value without having to individually set them for each component. To display and edit shared properties, 1 In the form, select the components whose shared property you want to set. The Properties page of the Object Inspector displays only those properties that the selected components have in common. (Notice, however, that the Name property is no longer visible.) 2 With the components still selected, use the Object Inspector to set the property. For information about more ways to set properties at design time, search the online Help index for “setting properties.” Using property editors The Object Inspector provides several ways to edit properties at design time. So far, this chapter has described two such ways: directly typing in values, or choosing from a drop-down list of pre-existing values. In addition, with certain properties, double-clicking in the Value column opens a dialog box that you use to set the property. Likewise, with some components, double-clicking the component in the form opens such a dialog box. Properties you can edit through a dialog box are indicated in the Object Inspector by an ellipsis (...) in the Value column beside that property. Example of a property editor The Image component provides an example of a dialog box property editor. When you double-click an Image component in a form, the Picture Editor dialog box appears, as shown in Figure 2.6. Use the dialog box to specify the image you want displayed in the component. UsingObjectPascalwiththeVCL2-37, UsingcomponentsFigure 2.6 The Picture Editor is a property-editor dialog box For detailed information about other types of property editors, search for the online Help topic property editors. Setting properties at runtime Any property you can set at design time can also be set at runtime by using code. There are also properties that can be accessed only at runtime. These are known as runtime-only properties. When you use the Object Inspector to set a component property at design time, you follow these steps: 1 Select the component. 2 Specify the property (by selecting it from the Properties page). 3 Enter a new value for that property. Setting properties at runtime involves the same steps: in your source code, you specify the component, the property, and the new value, in that order. Run-time property settings override any settings made at design time. Using properties to customize forms at runtime When you include a form, such as a dialog box, in several projects, you should avoid making design-time changes to the form that might conflict with the use of the form in another project. Instead, write code that changes the form’s properties at runtime. For example, the caption of an About box generally includes the name of the program that uses it. If you use the same About box in several projects simultaneously, you don’t want to accidentally use the name from another application.

Calling methods

Methods use the same calling conventions as ordinary procedures and functions, except that every method has an additional implicit parameter, Self, which is a reference to the instance or class for which the method is invoked. The Self parameter is always passed as a 32-bit pointer. 2-38Developer’ sGuide, Usingcomponents• With the register convention, the Self parameter behaves as if it was declared before all other parameters. It is therefore always passed in the EAX register. • With the pascal convention, the Self parameter behaves as if it was declared after all other parameters, including the additional var parameter that might be passed for a function result. It is therefore always pushed last, ending up at a lower address than all other parameters. • For the cdecl, stdcall and safecall conventions, the Self parameter behaves as if it was declared before all other parameters, but after the additional var parameter that might be passed for a function result. It is therefore always pushed last, but before the additional var parameter for the function result (if any).

Working with event handlers

In Delphi, almost all the code you write is executed, directly or indirectly, in response to events. Such code is called an event handler. Event handlers are actually specialized procedures. This section shows how to • Generate the default event handler • Generate a new event handler • Locate event handlers • Associate an event with an existing event handler • Associate menu events with code Generating the default event handler The default event is the one the component most commonly needs to handle at runtime. For example, a button’s default event is the OnClick event. To generate a default event handler, if one exists, double-click the component in the form. The Code editor opens, with the cursor placed exactly where you need to be to modify the event handler. Note Not all components have a default event handler. Some components, such as the Bevel, don’t respond to any events. For these components, double-clicking them in the form doesn’t generate an event handler. Double-clicking certain other components, such as the Image component or either of the menu components, opens a dialog box where you perform design-time property edits. Another way to generate a default event handler is to use the Object Inspector: On the Events page, double-click an event’s value column. Generating a new event handler You can use the Object Inspector to generate an event handler for the form, or any component you have on the form. When you use the Object Inspector, Delphi generates and maintains parts of the code for you. The first line of the event handler names the procedure and specifies the parameters it uses. The code you write between the begin..end block will execute whenever the event occurs. UsingObjectPascalwiththeVCL2-39, UsingcomponentsTo use the Object Inspector to generate an event handler, 1 Select a component (or the form). 2 Click the Events tab on the Object Inspector. The Events page of the Object Inspector displays all events that can be associated with the component, with the default event highlighted. 3 Select the event you want, then double-click the Value column, or press Ctrl+Enter. Delphi generates the event handler in the Code editor and places the cursor inside the begin..end block. 4 Inside the begin..end block, type the code that you want to execute when the event occurs. In Delphi, you usually use the Object Inspector to generate event handlers. You can write source code in the Code editor without using the Object Inspector. For example, you can write a general-purpose routine that’s not associated directly with a component event, but is called by an event handler. You can also write event handlers manually, but any time you can use the Object Inspector, you should. The Object Inspector not only generates the event handler name for you, but if you change that name later using the Object Inspector, Delphi changes it everywhere it occurs in your source code. This is not the case for event handlers you write without using the Object Inspector. Locating event handlers If you generated a default event handler for a component by double-clicking it in the form, you can locate that event handler in the same way: double-click the component in the form. The Code editor opens, with your cursor placed at the start of the first line of code for the component’s default event handler. Locating an event handler that’s not the default To locate an event handler that’s not the default, 1 In the form, select the component whose event handler(s) you want to locate. 2 In the Object Inspector, click the Events tab. 3 Double-click the event whose code you want to view. The Code editor opens, with the cursor placed inside the begin..end block of the event handler. You can now make modifications. Associating an event with an existing event handler You can reuse code by writing event handlers that handle more than one component event. You can do this in the case of generic event handlers that, for example, are called by both a menu command and an equivalent button on a SpeedBar. Once you write one such event handler, you can easily associate other events with the original handler. 2-40Developer’ sGuide, UsingcomponentsTo associate another component’s event with an existing event handler, 1 In the form, select the component whose event you want to code. 2 On the Events page of the Object Inspector, select the event to which you want to attach the handler code. 3 Click the down arrow next to the event to open a list of previously written event handlers. The list shows only the event handlers in this form that can be assigned to the selected event. 4 Select from the list by clicking an event-handler name. The code written for this event handler is now associated with the selected component event. Note Delphi does not duplicate the event handler code for every component event associated with a shared handler. You see the code for shared handlers in the Code editor only once, even though the same code is called whenever the associated component events occurs. Using the Sender parameter You can also have several different components share a handler that does different things depending on which component called it. To do so, use the Sender parameter in an if..then..else statement. The Sender parameter in an event handler informs Delphi which component received the event, and therefore called the handler. The following code either displays the title of the application in the caption of a modal dialog box, or else displays nothing, depending on whether the OnClick event was received by Button1 or by another button using the same event handler: procedure TMain1.Button1Click(Sender: TObject); begin if Sender = Button1 then AboutBox.Caption := 'About ' + Application.Title else AboutBox.Caption := ''; AboutBox.ShowModal; end; If you run the program and click Button1, the application title appears in the About box caption. If you click any other component associated with this event handler, the About box opens but does not display the application title. Displaying and coding shared events There is another way to share event handlers. When components have events in common, you can select the components to display their shared events, select a shared event, and then create a new event handler for it, or select an existing one. All the selected components will now use this event handler. UsingObjectPascalwiththeVCL2-41, UsingcomponentsTo display and code shared events, 1 In the form, select (Shift-click) all the components whose common events you want to view. 2 On the Events page of the Object Inspector, select the event. The Object Inspector displays only those events that the selected components have in common. (Only events in the current form are displayed.) To associate an event that several components have in common with an existing event handler, 1 Select the components. 2 On the Events page of the Object Inspector, select the event. The Object Inspector displays only those events that the selected components have in common. 3 From the drop-down list next to the event, select an existing event handler, and press Enter. Whenever any of the components you selected receives the specified event, the event handler you selected is called. To create an event handler for a shared event, 1 Select the components for which you want to create a shared event handler. 2 On the Events page of the Object Inspector, select an event. 3 Type a name for the new event handler and press Enter, or double-click the Handler column if you want Delphi to generate a name. Delphi creates an event handler in the Code editor, positioning the cursor in the begin..end block. If you choose not to name the event handler, Delphi names it for you based on the order in which you selected the components. For example, if you create a shared event handler for several buttons, Delphi names the event handler ButtonClick, where Button is the first button you selected for sharing an event handler. 4 Type the code you want executed when the selected event occurs for any of the components. Modifying a shared event handler Modifying an event handler that is shared by more than one component is virtually the same as modifying any existing event handler. Just remember that whenever you modify a shared event handler, you are modifying it for all the component events that call it. To modify the shared event handler, 1 Select any of the components whose event calls the handler you want to modify. 2 On the Events page of the Object Inspector, double-click the event handler name. 2-42Developer’ sGuide, Usingcomponents3In the Code editor, modify the handler. Now when any of the components that share this handler receive the shared event, the modified code gets called. Deleting event handlers When you delete a component, Delphi removes its reference in the form’s type declaration. However, deleting a component does not delete any associated methods from the unit file, because as noted earlier, those methods might be called by other components in the form. You can still run your application so long as the method declaration and the method itself both remain in the unit file. If you delete the method without deleting its declaration, Delphi generates an “Undefined forward” error message, indicating that you need to either replace the method itself (if you intend to use it), or delete its declaration as well as the method code (if you do not intend to use it). You can still explicitly delete an event handler, if you choose, or you can have Delphi do it for you. To manually delete an event handler, • Remove all event handler code and the handler’s declaration. To have Delphi delete an event handler, • Remove all the code (including comments) inside the event handler. The handler is removed when you compile or save the project. Associating menu events with code While you use the Delphi Menu Designer to visually design your application menus, the underlying code is what makes the menus useful. Each menu command needs to be able to respond to an OnClick event, and there are many times when you want to change menus dynamically in response to program conditions. Menu component events The MainMenu component is different from most other components in that it doesn’t have any associated events. You access the events for items in a main menu by means of the Menu Designer. By contrast, the PopupMenu component has one event: the OnPopup event. This is necessary because pop-up menus have no menu bar, therefore no OnClick event is available prior to users opening the menu. Handling menu item events There is only one event for menu items (as opposed to menu components): The OnClick event. Code that you associate with a menu item’s OnClick event is executed whenever the user chooses the menu item in the running application, either by clicking the menu command or by using its accelerator or shortcut keys. UsingObjectPascalwiththeVCL2-43, UsinghelperobjectsTo generate an event handler for any menu item, 1 From the Menu Designer window, double-click the menu item. 2 Inside the begin..end block, type the code you want to execute when the user clicks this menu command. You can also easily generate event handlers for menu items displayed in a form. This procedure does not apply to pop-up menu items, as they are not displayed in the form at design time. To generate an event handler for a menu item displayed in the form, simply click the menu item (not the menu component). For example, if your form contains a File menu with an Open menu item below it, you can click the Open menu item to generate or open the associated event handler. This procedure applies only to menu items in a list, not those on a menu bar. Clicking a menu item on a menu bar opens that menu, displaying the subordinate menu items. Associating a menu item with an existing event handler You can associate a menu item with an existing event handler, so that you don’t have to rewrite the same code in order to reuse it. To associate a menu item with an existing OnClick event handler, 1 In the Menu Designer window, select the menu item. 2 On the Properties page of the Object Inspector, ensure that a value is assigned to the menu item’s Name property. 3 On the Events page of the Object Inspector, click the down arrow next to OnClick to open a list of previously written event handlers. Only those event handlers written for OnClick events in this form appear in the list. 4 Select from the list by clicking an event handler name. The code written for this event handler is now associated with the selected menu item.

Using helper objects

Delphi provides several objects that aid you in developing your applications. A typical windows program performs functions like writing output to a file or reading a registry key for application-specific information. By using helper objects, adding this functionality to your projects is easy.

Working with string lists

Delphi applications often need to deal with lists of character strings. For example, you may want to list items in a list box or combo box, display lines of text in a memo 2-44Developer’ sGuide, Usinghelperobjectsfield, list fonts installed on the system, display names of tabs on a notepad, display items in an outline, or label a row or column of entries in a string grid. Delphi provides a common interface to any type of list through an object called a string list. In addition, these list objects are interchangeable—that is, you can edit a string list in a memo field and then use that same list as a list of items in a list box. A string-list property appears in the Object Inspector with TStrings in the Value column. When you double-click TStrings, the String List editor appears, where you can edit, add, or delete lines. You can also work with string lists at runtime to perform such tasks as: • Manipulating strings in a list • Loading and saving string lists • Creating a new string list • Adding objects to a string list • Operating objects on a string list Manipulating strings in a list In Delphi applications, you are likely to want to get strings from or modify a component’s string-list property. These are the common operations you might need to perform: • Counting the strings in a list • Accessing a particular string • Finding the position of a string in the list in the list • Iterating through strings in a list • Adding a string to a list • Moving a string within a list • Deleting a string from a list • Copying a complete string list • Copying complete string lists using local variables Counting the strings in a list The Count property is a read-only property that indicates the number of strings in the list. Since the indexes used in string lists are zero-based, Count is one more than the index of the last string in the list. For example, an application can find out how many different fonts the system supports by reading the Count property on the screen object’s font list, which is a string list containing the names of all the supported fonts: FontCount := Screen.Fonts.Count; Accessing a particular string The string list has an indexed property called Strings, which you can treat like an array of strings for most purposes. For example, the first string in the list is Strings[0]. However, since the Strings property is the most common part of a string list to access, Strings is the default property of the list, so you can omit the Strings identifier and treat the string list itself as an indexed array of strings. UsingObjectPascalwiththeVCL2-45, UsinghelperobjectsTo access a particular string in a string list, refer to it by its ordinal position, or index, in the list. The string numbers are zero-based, so if a list has three strings in it, the indexes cover the range 0..2. To determine the maximum index, check the Count property. If you try to access a string outside the range of valid indexes, the string list raises an exception. Finding the position of a string in the list To locate a string in a string list, use the string list’s IndexOf method. IndexOf takes a string as its parameter, and returns the index of the matching string in the list, or –1 if the string is not in the list. Note that IndexOf works with complete strings only. That is, it must find an exact match for the whole string passed to it, and it must match a complete string in the list. If you want to match partial strings (for instance, to see if any of the strings in the list contains a given series of characters), you must iterate through the list yourself and compare the strings. Here’s an example of using IndexOf to determine whether a given file name is in the list of files in a file list box: if FileListBox1.Items.IndexOf('WIN.INI') > -1 then { you're in the Windows directory }; In the above case, if there are multiple instances of the string, IndexOf returns the index of the first instance in the list. Iterating through strings in a list To iterate through each string in a list, use a for loop with an Integer-type index. The loop should run from zero up to one less than the number of strings in the list (Count – 1). Inside the loop you can access each string and perform the operation you want. This example is a loop that iterates the strings in a list box and converts each string to all uppercase characters in response to a button click: procedure TForm1.Button1Click(Sender: TObject); var Index: Integer; begin for Index := 0 to ListBox1.Items.Count - 1 do ListBox1.Items[Index] := UpperCase(ListBox1.Items[Index]); end; Adding a string to a list To add a string to the end of the list, call the Add method, passing the new string as the parameter. The added string becomes the last string in the list. To insert a string into the list, call the Insert method, passing two parameters: the index where you want the inserted string to appear and the string. 2-46Developer’ sGuide, UsinghelperobjectsFor example, to make the string “Three” become the third string in a list, you use the following code: Insert(2, 'Three'); If the list doesn’t already have at least two strings, Delphi raises an index-out-of- range exception. Note Do not use Insert on a sorted string list, use Add. Moving a string within a list To move a string in the list, call the Move method, passing two parameters: the current index of the item and the index to where you want to move the item. For example, to move the third string in a list to the fifth position, you call: Move(2, 4) Deleting a string from a list To delete a string from a string list, call the string list’s Delete method, passing the index of the string you want to delete. If you don’t know the index of the string you want to delete, use the IndexOf method to locate it. To delete all the strings in a string list, use the Clear method. This example uses IndexOf to determine the location of a string in a list, and deletes that string if present. In this case, if there are multiple instances of the string, IndexOf returns the index of the first instance in the list. with ListBox1.Items do begin if IndexOf('bureaucracy') > -1 then Delete(IndexOf('bureaucracy')); end; Copying a complete string list To copy a list of strings from one string list to another, assign the source list to the destination list. Even if the lists are associated with different kinds of components (or no components at all), Delphi handles the copying of the list for you. Copying the strings from one list to another overwrites the strings that were originally in the destination list. If you want to add a list of strings to the end of another list, you call the AddStrings method, passing as a parameter the list of strings you want to add. Memo11.Lines := ComboBox1.Items; { overwrites original strings } Memo1.Lines.AddStrings(ComboBox1.Items); { appends strings to end } Copying complete string lists using local variables When you are copying strings between local variables, use the Tstrings.Assign method to avoid unexpected results. If you simply assign one string to another using local variables, the variable will not copy the strings, and it will lose the originalUsingObjectPascalwiththeVCL2-47, Usinghelperobjectsstring list object. The following example shows the wrong and right ways to copy strings using local variables: var f: TFont; begin f := TFont.Create; f := Font1.Font; { Incorrect } f.Assign(Form1.Font); { Correct } Loading and saving string lists You can easily store any Delphi string list in a text file and load it back again (or load it into a different list). String list objects have methods to handle both operations. Using these methods, you can quickly create a simple text editor by loading a file into a memo component. You can also use the same mechanism to save lists of items for list boxes or complete outlines. To load a string list from a file, call the LoadFromFile method and pass the name of the text file from which to load. LoadFromFile reads each line from the text file into a string in the list. To store a string list in a text file, call the SaveToFile method and pass it the name of the text file to save to. If the file doesn’t already exist, SaveToFile creates it. Otherwise, it overwrites the current contents of the file with the strings from the string list. This example loads a copy of the WIN.INI file from the Windows directory of the C drive into a memo field and makes a backup copy called WIN.BAK: procedure TForm1.FormCreate(Sender: TObject); var FileName: string;{ storage for file name } begin FileName := 'C:\WINDOWS\WIN.INI';{ set the file name } with Memo1.Lines do begin LoadFromFile(FileName);{ load from file } SaveToFile(ChangeFileExt(FileName, '.BAK'));{ save into backup file } end; end; Creating a new string list Typically, you manipulate string lists as part of a component, so you don’t have to construct the list yourself. However, you can also create standalone string lists that have no associated component. For instance, your application might need to keep a list of strings for a lookup table. For all string lists, your application must create, use, and then free the list when you finish with it. However, the way you create and manage a string list depends on whether the list is a short-term list or a long-term list. 2-48Developer’ sGuide, Usinghelperobjects• A short-term list is one that the application creates, uses, and destroys in a single routine. • A long-term list is one that the application creates, uses throughout runtime, and destroys before it shuts down. Short-term string lists If you need to use a string list only for the duration of a single routine, you can create it, use it, and destroy it all in one place. This is the safest way to use string list objects. Because the string list object allocates memory for itself and its strings, it is important that you protect the allocation by using a try..finally block to ensure that the object frees its memory even if an exception occurs. Basically, to implement a short-term string list, you 1 Construct the string-list object. 2 In the try part of a try..finally block, use the string list. 3 In the finally part, free the string-list object. The following event handler responds to a button click by constructing a string list, using it, and then destroying it. procedure TForm1.Button1Click(Sender: TObject); var TempList: TStrings;{ declare the list } begin TempList := TStringList.Create;{ construct the list object } try { use the string list } finally TempList.Free;{ destroy the list object } end; end; Long-term string lists If the string list must be available at any time while your application runs, you construct the list when the application first executes, then destroy it before the application terminates. To construct long-term a string list, you do the following: 1 In the unit’s header file, add a field of type TStrings to the application’s main form object, giving it the name you want to use. 2 Create a handler for the main form’s OnCreate event. The create-event handler executes before the form appears onscreen at runtime. 3 In the create-event handler, construct the string-list object. 4 Create a handler for the main form’s OnDestroy event. The destroy-event handler executes just after the main form disappears from the screen before the application stops running. UsingObjectPascalwiththeVCL2-49, UsinghelperobjectsAny event handlers can then access the string list by the name you declared in the first step. This example shows an added string list named ClickList. The click-event handler for the main form adds a string to the list every time the user clicks a mouse button, and the application writes the list out to a file before destroying the list. unit Unit1; interface uses SysUtils, WinTypes, WinProcs, Messages, Classes, Graphics, Controls, Forms, Dialogs; type TForm1 = class(TForm) procedure FormCreate(Sender: TObject); procedure FormDestroy(Sender: TObject); procedure FormMouseDown(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer); private { Private declarations } public { Public declarations } ClickList: TStrings;{ declare the field } end; var Form1: TForm1; implementation {$R *.DFM} procedure TForm1.FormCreate(Sender: TObject); begin ClickList := TStringList.Create;{ construct the list } end; procedure TForm1.FormDestroy(Sender: TObject); begin ClickList.SaveToFile(ChangeFileExt(Application.ExeName, '.LOG'));{ save the list } ClickList.Free;{ destroy the list object } end; procedure TForm1.FormMouseDown(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer); begin ClickList.Add(Format('Click at (%d, %d)', [X, Y]));{ add a string to the list } end; end.

Adding objects to a string list

In addition to its list of strings, stored in the Strings property, a string list can also have a list of objects, which it stores in its Objects property. Like Strings, Objects is an 2-50Developer’ sGuide, Usinghelperobjectsindexed property, but instead of an indexed list of strings, it is an indexed list of objects. If you’re just using the strings in the list, the list does nothing with any associated objects unless you specifically access them. Note Some string lists ignore added objects because it doesn’t make sense to have them. For example, the string lists representing the lines in a memo do not store objects added to them. Other string lists, such as the strings in a string grid, support associated objects that can be used by the application. You can assign any type of objects you want to Objects, however, the most common use is to associate bitmaps with strings for owner-draw controls. Note that the strings and objects in a string list work in pairs. For every string, there is an associated object, although by default, the pointer to that object is NULL. Operating on objects in a string list For each operation that you can perform on a string list with a string, you can perform the corresponding operation with a string and its associated object. For example, you can access a particular object by indexing into the Objects property, just as you would the Strings property. Table 2.2 summarizes the properties and methods you use to perform corresponding operations on strings and objects in a string list. Table 2.2 Corresponding string and object methods in string lists Operation For strings For objects Access an item Strings property Objects property Add an item Add method AddObject method Insert an item Insert method InsertObject method Locate an item IndexOf method IndexOfObject method LoadFromFile and SaveToFile methods operate on only the strings, since they work with text files. Methods such as Delete, Clear, and Move operate on items as a whole. That is, deleting an item removes both the string and its corresponding object from the list. Accessing associated objects You access objects associated with a string list just as you access the strings. For example, to get the first string in a string list, you access the string list’s Strings property at index 0: Strings[0]. Objects[0] is a pointer to the object corresponding to that string. Adding associated objects To associate an object with an existing string, you assign the object to the Objects property at the same index. For example, if a string list named Fruits contains theUsingObjectPascalwiththeVCL2-51, Usingdatamodulesandremotedatamodulesstring “apple” and you want to associate a bitmap called AppleBitmap with it, you would use the following assignment: with Fruits do Objects[IndexOf('apple')] := AppleBitmap; To add objects at the same time you add strings, call the string list’s AddObject method instead of Add, which just adds a string. AddObject takes two parameters, the string and the object. For example, Fruits.AddObject('apple', AppleBitmap); You cannot add an object without adding a corresponding string.

The registry and Windows INI files

Use TRegistry to encapsulate access to the Windows 95/NT system registry in an application. The registry is a database that an applications can use to store and retrieve configuration information. Configuration information is stored in a hierarchical tree. Each node in the tree is called a key. Every key can contain subkeys and data values that represent part of the configuration information for an application. Use TRegistry’s methods to open, close, save, move, copy, and delete registry keys, as well are read in the information stored in the key itself. Until Windows 95, most applications stored their configuration information in initialization files, usually named with the extension .INI. TIniFile and TRegIniFile are provided to help the migration of legacy programs to the Windows 95/NT 4.0 convention of using the registry to store all application settings. TIniFile is provided when working with legacy Windows 3.1 programs, and TRegIniFile is for existing Delphi applications that want to migrate to using the system registry with the minimum of code changes.

Using streams

Use specialized stream objects to read from, write to, or copy information stored in a particular medium. Each descendant of TStream implements methods for transferring information to and from a particular storage medium, such as a disk file, dynamic memory, and so on. In addition to methods for reading, writing, and copying bytes to and from the stream, stream objects permit applications to seek to an arbitrary position in the stream. Properties of TStream provide information about the stream, such as its size and the current position in the stream.

Using data modules and remote data modules

There are two types of data modules: standard and remote. If you create single- or two-tiered applications, you use a standard data module. If you have the Delphi Client/Server Suite and you are creating a multi-tiered database application, then you add a remote data module to your application server. To learn how to add a 2-52Developer’ sGuide, Usingdatamodulesandremotedatamodulesremote data module to your project, see “Adding a remote data module to an application server project” on page 2-55.

Creating a new data module

To create a new, empty data module for a project, choose File|New Data Module. Delphi opens a data module container on the desktop and adds the data module to the project file. At design time a data module looks like a standard Delphi form with a white background and no alignment grid. As with forms, you can place nonvisual components on a module from the Component palette, and you can resize a data module to accommodate the components you add to it. You can also right-click a module to display a context menu for it. The following table summarizes the context menu options for a data module: Table 2.3 Context menu options for data modules Menu item Purpose Align To Grid Aligns data access components to the module’s invisible grid. Align Aligns data access components according to criteria you supply in the Alignment dialog box. Revert to Inherited Discards changes made to a module inherited from another module in the Object Repository, and reverts to the originally inherited module. Creation Order Enables you to change the order in which data access components are created at start-up. Add to Repository Stores a link to the data module in the Object Repository. View as Text Displays the text representation of the data module’s properties. Behind the data module container, there is a corresponding unit file containing source code for the data module.

Naming a data module and its unit file

The title bar of a data module displays the module’s name. The default name for a data module is “DataModuleN” where N is a number representing the lowest unused unit number in a project. For example, if you start a new project, and add a module to it before doing any other application building, the name of the module defaults to “DataModule2”. The corresponding unit file for DataModule2 defaults to “Unit2”. You should rename your data modules and their corresponding unit files at design time to make them more descriptive. You should especially rename data modules you add to the Object Repository to avoid name conflicts with other data modules in the Repository or in applications that use your modules. To rename a data module, 1 Select the module. 2 Edit the Name property for the module in the Object Inspector. UsingObjectPascalwiththeVCL2-53, UsingdatamodulesandremotedatamodulesThe new name for the module appears in the title bar when the Name property in the Object Inspector no longer has focus. Changing the name of a data module at design time changes its variable name in the interface section of code. It also changes any use of the type name in procedure declarations. You must manually change any references to the data module in code you write. To rename a unit file for a data module, 1 Select the unit file. 2 Choose File|SaveAs. 3 In the Save As dialog box, enter a file name that clearly identifies the unit with the renamed data module. Placing and naming components You place nonvisual components, such as TTable and TQuery, in a data module just as you place visual components on a form. Click the desired component on the appropriate page of the Component palette, then click in the data module to place the component. You cannot place visible controls, such as grids, on a data module. If you attempt it, you receive an error message. For ease of use, components are displayed with their names in a data module. When you first place a component, Delphi assigns it a generic name that identifies what kind of component it is, such as DataSource1 and Table1. This makes it easy to select specific components whose properties and methods you want to work with. To make it even easier, you should give your components more descriptive names (for example, CustSource and CustTable). To change the name of a component in a data module, 1 Select the component. 2 Edit the component’s Name property in the Object Inspector. The new name for the component appears under its icon in the data module as soon as the Name property in the Object Inspector no longer has focus. When you name a component, the name you give it should reflect the type of component and what it is used for. For example, for database components, the name should reflect the type of component, and the database it accesses. For example, suppose your application uses the CUSTOMER table. To access CUSTOMER you need a minimum of two data access components: a data source component and a table component. When you place these components in your data module, Delphi assigns them the names DataSource1 and Table1. To reflect that these components use CUSTOMER, and to relate the components to one another, you could change these names to CustSource and CustTable. Using component properties and methods in a data module Placing components in a data module centralizes their behavior for your entire application. For example, you can use the properties of dataset components, such as TTable and TQuery, to control the data available to the data source components that 2-54Developer’ sGuide, Usingdatamodulesandremotedatamodulesuse those datasets. Setting the ReadOnly property to True for a dataset prevents users from editing the data they see in a data-aware visual control on a form. You can also invoke the Fields editor for a dataset to restrict the fields within a table or query that are available to a data source and therefore to the data-aware controls on forms. The properties you set for components in a data module apply consistently to all forms in your application that use the module. In addition to properties, you can write event handlers for components. For example, a TDataSource component has three possible events: OnDataChange, OnStateChange, and OnUpdateData. A TTable component has over 20 potential events. You can use these events to create a consistent set of business rules that govern data manipulation throughout your application. Creating business rules in a data module Besides writing event handlers for the components in a data module, you can code methods directly in the unit file for a data module. These methods can be applied to the forms that use the data module as business rules. For example, you might write a procedure to perform month-, quarter-, or year-end bookkeeping. You might call the procedure from an event handler for a component in the data module. The prototypes for the procedures and functions you write for a data module should appear in the module’s type declaration: type TCustomerData = class(TDataModule) Customers: TTable; Orders: TTable; ƒ private { Private declarations } public { Public declarations } procedure LineItemsCalcFields(DataSet: TDataSet); { A procedure you add } end; var CustomerData: TCustomerData; The procedures and functions you write should follow in the implementation section of the code for the module.

Adding a remote data module to an application server project

If you have the Delphi Client/Server Suite and you are building a multi-tiered database application, you can add a remote data module to your application server project. A remote data module is a data module with an interface that client applications can access remotely. For more information about creating multi-tiered database applications, see Chapter 15, “Creating multi-tiered applications.” UsingObjectPascalwiththeVCL2-55, UsingtheObjectRepositoryTo add a remote data module to a project, 1 Choose File|New. 2 Select the Multitier page in the New Items dialog box. 3 Choose the desired type of remote data module (Remote Data Module, MTS Data Module, or CORBA Data Module). Once you place a remote data module in a project, you use it just like a standard data module.

Accessing a data module from a form

To associate visual controls on a form with a data module, you must first add the data module to the form’s uses clause. To add a module to a form’s uses clause, you can: • Choose File|Use Unit and enter the name of the module or pick it from the list box in the Use Unit dialog box, or • Drag selected field from the Fields editor of a TTable or TQuery component in the data module to a form. In this case Delphi prompts you to confirm that you want to add the module to the form’s uses clause, then creates controls based on data dictionary information for each field you dragged into the form. Before you drag fields to a form, set the data source for the fields you intend to drag. Delphi uses an existing data source in the data module if it is already connected to the dataset. If a data source is not defined, Delphi adds a new data source component to the form and sets the DataSource properties of the controls it creates to point to this data source. The data source itself is associated with a table or query component in the data module. Should this happen, you can keep this arrangement, or, to keep your data access model cleaner, you can change it. Delete the data source component on the form, and set the DataSource properties of the control on the form to point directly to the appropriate data source in the data module.

Using the Object Repository

Delphi’s Object Repository (Tools|Repository) is a versatile tool that makes it possible to easily share (or copy) forms, dialog boxes, and data modules across projects and within a single project. It also provides project templates as starting points for new projects. By adding forms, dialog boxes, and data modules to the Object Repository, you make them available to other projects. In fact, you can add an entire project to the Object Repository as a template for future projects. You’ll also see wizards in the Object Repository. Wizards are small applications that lead the user through a series of dialog boxes to create a form or project. Delphi provides a number of wizards, and you can also add your own. 2-56Developer’ sGuide, UsingtheObjectRepositoryThe repository is actually a text file named DELPHI32.DRO (Delphi repository objects) in the BIN directory that contains references to the items that appear in the Object Repository dialog box and the New Items dialog box. You can open this file in any text editor.

Sharing forms, dialogs, data modules, and projects

It’s also easy for you to share items within a project without having to add them to the Object Repository: when you open the New Items dialog box (File|New), you’ll see a page tab with the name of your project. If you click that page tab, you’ll see all the forms, dialog boxes, and data modules in your project. You can then derive a new item from an existing item, and customize it as needed.

Adding items to the Object Repository

You can add your own projects, forms, and data modules to those already available in the Object Repository. To add an item to the Object Repository, 1 If the item is a project or is in a project, open the project. 2 For a project, choose Project|Add To Repository. For a form or data module, right- click the item and choose Add To Repository from the SpeedMenu. 3 Type a description, title, and author. The title will appear in the Object Repository window and in the New Items dialog box (File|New). 4 Decide where you want this item to appear in the New Items dialog box, and select that page from the Page combo box. Or, type the name of the page. If you type the name of a page that doesn’t exist, Delphi creates a new page for you, and your new page name will appear on a tab of the New Items dialog box. 5 Choose Browse to select an icon to represent the object in the Object Repository. 6 Choose OK.

Sharing objects in a team environment

To share objects in a team environment, you need to specify a directory that’s available to team members. After you do this, another DELPHI 32.DRO file is created in the specified directory as soon as you add an item to the Object Repository. The new DELPHI32.DRO text file contains pointers to the objects you want to share. To specify a shared repository directory, 1 Choose Tools|Environment Options. 2 On the Preferences page, locate the Shared Repository panel. 3 In the Directory edit box, enter the name of the directory where you want to locate the shared repository. UsingObjectPascalwiththeVCL2-57, UsingtheObjectRepositoryThe location of your shared directory is stored in the Windows registry. Changing the location in the Environment Options dialog box changes it in the registry as well. To share Object Repository items among team members, every member’s Directory setting in the Environment Options dialog box must point to the same location.

Using an Object Repository item in a project

To gain access to items in the Object Repository, choose File|New. The New Items dialog box appears, showing you all the items in the Object Repository. You have three options for adding an item to your project: • Copy • Inherit • Use Copying an item Select Copy to make an exact copy of the selected item and add the copy to your project. Future changes made to the item in the Object Repository will not be reflected in your copy, and alterations made to your copy will not affect the original Object Repository item. Copy is the only option available for using project templates. Inheriting an item Select Inherit to derive a new class from the selected item in the Object Repository and add the new class to your project. The Inherit option creates a link to the ancestor item in the repository. When you recompile your project, any changes that have been made to the item in the Object Repository are reflected in your derived class. These changes apply in addition to any changes or additions you make to the item in your project. Changes made to your derived class do not affect the shared item in the Object Repository. Inherit is available as an option for forms, dialog boxes, and data modules, but not for project templates. It is the only option available for reusing items from within the same project. Using an item Select Use when you want the selected item itself to become part of your project. In this case, you are not making a copy of the item; you are using the item itself, “live.” Using an item is like reverse inheritance: instead of inheriting changes others make to an item, they inherit your changes when they use the item in the repository. Changes to the item appear in all projects that have added the item with the Inherit or Use options selected. Caution The Use option is available for forms, dialog boxes, and data modules, but you should use it carefully. Make sure the changes you make to an item are thoroughly tested before letting others copy it into their applications from the repository. 2-58Developer’ sGuide, UsingtheObjectRepositoryNote The Use option is the only option available for experts, whether form experts or project experts. Using an expert doesn’t actually add shared code, but rather runs a process that generates its own code.

Using project templates

Project templates are predesigned projects you can use as starting points for your own projects. To start a new project from a project template, 1 Choose File|New to display the New Items dialog box. 2 Choose the Projects tab. 3 Select the project template you want and choose OK. 4 In the Select Directory dialog box, specify a directory for the new project’s files. If you specify a directory that doesn’t exist, Delphi creates it for you. Delphi copies the template files to the project directory. You can then modify the project, adding new forms and units, or use it unmodified, adding only your event- handler code. In any case, your changes affect only the open project. The original project template is unaffected and can be used again.

Modifying a shared form

If several projects share a form in the Object Repository, then modifications you make to the form can affect all projects, depending on how the form is imported into each project. If a project copies a form from the Object Repository, then later modifications to the form in the Object Repository have no effect on the project. If a project inherits a form from the Object Repository, then each time the project is compiled, it inherits the latest version of the form from the Object Repository, including any changes made since the project was last compiled. If a project “uses” a form from the Object Repository, then any time you make changes to the form in the project, the changes are stored in the Object Repository directly where other applications can copy or inherit them. If you know that other projects inherit a form in the Object Repository, but you do not want to replicate your changes to those projects, there are several ways to prevent inheritance. You can • Save the form under a different name and use the renamed form in your project instead of the original. • Make the changes to the form at runtime instead of at design time. • Make the shared form a component that can be installed onto the Component palette. This has the added advantage of enabling users to customize the form at design time. For more information, see “Adding items to the Object Repository” on page 2-57. UsingObjectPascalwiththeVCL2-59, AddingcustomcomponentstotheIDEIf you expect to be the only user of the form, and you don’t plan extensive or frequent changes, runtime customization is probably acceptable. If you plan on using the form in many different applications, runtime customization involves more coding for you and other developers. In this case, it’s usually more convenient, whenever possible, to make the form a component that other users or developers can install onto their Component palette. Note You can also reuse a form by making it into a DLL.

Specifying a default project, new form, and main form

To specify a default project, a new form, and the project’s main form, select Project| Options to bring up the Project Options dialog.

Adding custom components to the IDE

The ability to add custom components to the Delphi IDE for use at design-time allows you to extend the built-in features of Delphi. This is done by either writing custom designed components, or by purchasing Delphi components from third-party vendors. How you install custom components is dependent on whether you are adding components you have written or you are installing compiled third-party components. If you are writing a component from scratch, see Chapter 37, “Making components available at design time” for more information on how to create installable components. Once you have created the component, however, the procedure to add the component to the IDE is the same.

Understanding the difference between Install Component and Install Package

The Component|Install Component and Component|Install Package menu items perform very different functions in Delphi, despite their similar names. Install Component Use Component|Install Component when you want to install a component into an existing or new package. Whether the component is installed to a new package or an existing one, you write the component code, which is identified by the unit name. Install Package Use Component|Install Package to install compiled components for use in the Delphi IDE, and to manage the installed components for your projects. 2-60Developer’ sGuide, AddingcustomcomponentstotheIDE

Installing components

To install or your own compiled components, or components from a third-party vendor: 1 Copy or move the necessary files to a local directory. • If the package is shipped with separate .BPL, .DCP, and .DCU files, be sure to copy all of these files to your local directory. • If the package is shipped as a .DPC (package collection) file, only the one file need be copied; the .DPC file contains the other files. Note The directory where you store the .DCP file, and the .DCU files, if they are included with the distribution, must be in the Delphi Library Path. It is customary to put executable files in the Delphi\BIN directory. 2 Choose Component|Install Packages. Or, choose Project|Options and click the Packages tab. 3 A list of available packages appears under Design Packages. • To add a package to the list, click Add and browse in the Open Package dialog box for the directory where the .BPL or .DPC file resides (see step 1). Select the .BPL or .DPC file and click Open. If you select a .DPC file, a new dialog box appears to handle the extraction of the .BPL and other files from the package collection. • To install a package, select the check box next to it. • To uninstall a package, uncheck its check box. • To see a list of components included in an installed package, select the package and click Components. • To remove a package from the list, select the package and click Remove. 4 Click OK. The components in the package are installed on the Component palette pages specified in the components’ RegisterComponents procedure, with the names they were assigned in the same procedure. New projects are created with all available packages installed, unless you change the default settings. To make the current installation choices into the automatic default for new projects, check the Default check box at the bottom of the dialog box. UsingObjectPascalwiththeVCL2-61, 2-62Developer’ sGuide,

Chapter

Chapter 3Building applications, components, and libraries This chapter provides an overview of how to use Delphi to create applications, libraries, and components.

Creating applications

The main use of Delphi is designing, building, and compiling of Windows applications. There are three basic kinds of Windows applications: • Windows GUI applications • Console applications • Service applications Windows GUI applications comprise the bulk of software available for the Windows platform. The User Interface (UI) consists of windows and dialog boxes which work together to perform a group of functions. Word processors, spreadsheets, and Delphi are examples of GUI applications. Console applications are 32-bit Windows applications that run without a GUI interface, usually in a console window. These applications typically don’t require much user input, and perform a limited set of functions. The included Grep utility is an example of a console application, as is the command-line compiler and linker included with Delphi. Service applications are applications that take requests from client applications, process those requests, and return information to the client applications. They typically run in the background, without much user input. A web, FTP, or e-mail server is an example of a service application. Buildingapplications, components, andlibraries3-1, Creatingapplications

Windows 95/NT EXEs

When you compile a project in Delphi, an executable (.EXE) file is created that can then be run. The executable usually provides the basic functionality of your program, and simple programs often consist of only an EXE. You can extend the application by calling DLLs, packages, and other support files from the executable. In designing a Windows EXE, there are two UI models for the implementation of the application: • Single document interface (SDI) • Multiple document interface (MDI) In addition to the implementation model of your applications, the design-time behavior of your project and the run-time behavior of your application can be manipulated by setting project options in the Delphi IDE. User interface models Any form you design can be implemented in your application as a multiple document interface (MDI) or single document interface (SDI) form. In an MDI application, more than one document or child window can be opened within a single parent window. This is common in applications such as spreadsheets or word processors. An SDI application, by contrast, normally contains a single document view. To make your form an SDI application, set the FormStyle property of your Form object to fsNormal. For more information on developing the UI for an application, see Chapter 5, “Developing the application user interface.” SDI Applications To create a new SDI application, 1 Select File|New to bring up the New Items dialog. 2 Click on the Projects page and select SDI Application. 3 Click OK. By default, the FormStyle property of your Form object is set to fsNormal, so Delphi assumes that all new applications are SDI applications. MDI applications To create a new MDI application, 1 Select File|New to bring up the New Items dialog. 2 Click on the Projects page and select MDI Application. 3 Click OK. MDI applications require more planning and are somewhat more complex to design than SDI applications. However, the same principles used to create a well designed UI in an SDI application apply to MDI applications. MDI applications spawn child windows that reside within the client window. In a Delphi MDI application, the application’s main form contains child forms. Set the 3-2Developer’ sGuide, CreatingapplicationsFormStyle property of the TForm object to specify whether a form is a child (fsMDIForm) or main form (fsMDIChild). It is a good idea to define a base class for your child forms, and derive each individual child form from this base class, to avoid having to redefine the child form’s settings. Setting IDE, project, and compilation options Use Project|Project Options to specify the various options for your project. The tabbed pages in the Project Options dialog affect both the visible and the back-end behavior of your programs as well as the design-time behavior of your project. Setting default project options To change the default options that apply to all future projects, set the options in the Project Options dialog box and check the Default box at the bottom right of the window. All new projects will now have the current options selected by default.

Console applications

Use the Console wizard to create new console applications. Select File|New to bring up the New Items dialog, select the Console wizard, and click OK. Console applications are programs written without a Graphical User Interface, or GUI. When a console application is run, it runs in a Windows console window. The visual controls of the VCL normally used in Windows programming are not used in console applications. When you create a new console application, Delphi does not create a new form. Only the code editor is displayed. You can, however, use the VCL in console applications.

Service applications

To create an application that implements a Win32 service, Choose File|New, and select Service Application from the new items page. This adds a global variable named Application to your project, which is of type TServiceApplication. Once you have created a service application, You will see a window in the designer that corresponds to a service (TService). Implement the service by setting its properties and event handlers in the Object Inspector. You can add additional services to your service application by choosing Service from the new items dialog. Do not add services to an application that is not a service application. While a TService object can be added, the application will not generate the requisite events or make the appropriate Windows calls on behalf of the service. Example This service has a TServerSocket whose port is set to 80. This is the default port for Web Browsers to make requests to Web Servers and for Web Servers to make responses to Web Browsers. This particular example will produce a text document in the C:\Temp directory called WebLogxxx.log (where xxx is the ThreadID). There should be only one Server listening on any given port, so if you have a web server, you should make sure that it is not listening (the service is stopped). Buildingapplications, components, andlibraries3-3, CreatingapplicationsTo see the results: open up a web browser on the local machine and for the address, type ‘localhost’ (with no quotes). The Browser will time out eventually, but you should now have a file called weblogxxx.log in the C:\temp directory. interface uses Windows, Messages, SysUtils, Classes, Graphics, Controls, SvcMgr, Dialogs, ScktComp; type TService1 = class(TService) ServerSocket1: TServerSocket; procedure ServerSocket1ClientRead(Sender: TObject; Socket: TCustomWinSocket); procedure Service1Execute(Sender: TService); private { Private declarations } Stream: TMemoryStream; public function GetServiceController: PServiceController; override; { Public declarations } end; var Service1: TService1; implementation {$R *.DFM} procedure ServiceController(CtrlCode: DWord); stdcall; begin Service1.Controller(CtrlCode); end; function TService1.GetServiceController: PServiceController; begin Result := @ServiceController; end; procedure TService1.ServerSocket1ClientRead(Sender: TObject; Socket: TCustomWinSocket); var Buffer: PChar; begin Buffer := nil; while Socket.ReceiveLength > 0 do begin try Buffer := AllocMem(Socket.ReceiveLength); Socket.ReceiveBuf(Buffer^, Socket.ReceiveLength); Stream.Write(Buffer^, StrLen(Buffer)); finally FreeMem(Buffer); end; 3-4Developer’ sGuide, CreatingapplicationsStream.Seek(0, soFromBeginning); Stream.SaveToFile('c:\Temp\Weblog' + IntToStr(ServiceThread.ThreadID) + '.log'); end; end; procedure TService1.Service1Execute(Sender: TService); begin Stream := TMemoryStream.Create; try ServerSocket1.Port := 80; // WWW port ServerSocket1.Active := True; while not Terminated do begin ServiceThread.ProcessRequests(False); end; ServerSocket1.Active := False; finally Stream.Free; end; end; end. When working with services you should be aware of: • Service threads • Service name properties

Service threads

Each service has its own thread (TServiceThread), so if your service application implements more than one service you must ensure that the implementation of your services is thread-safe. TServiceThread is designed so that you can implement the service in the TService OnExecute event handler. The service thread has its own Execute method which contains a loop that calls the service’s OnStart and OnExecute handlers before processing new requests. Because services can take a long time to process and can receive simultaneous requests from more than one client, it is more efficient to spawn a new thread (derived from TThread, not TServiceThread) for each request and move the implement of that service to the new thread’s Execute method. This allows the service thread’s Execute loop to process new requests continually without having to wait for the service’s OnExecute handler to finish. The following example demonstrates: { This Service Beeps every 500 milliseconds from within a standard TThread. It handles pausing, continuing, and stopping of the Thread when the Service is told to Pause, Continue, or stop. } interface uses Windows, Messages, SysUtils, Classes, Graphics, Controls, SvcMgr, Dialogs; Buildingapplications, components, andlibraries3-5, Creatingapplicationstype TService2 = class(TService) procedure Service1Start(Sender: TService; var Started: Boolean); procedure Service1Continue(Sender: TService; var Continued: Boolean); procedure Service1Pause(Sender: TService; var Paused: Boolean); procedure Service1Stop(Sender: TService; var Stopped: Boolean); private { Private declarations } public function GetServiceController: PServiceController; override; { Public declarations } end; TSparkyThread = class(TThread) public procedure Execute; override; end; var Service2: TService2; implementation {$R *.DFM} var SparkyThread: TSparkyThread; procedure ServiceController(CtrlCode: DWord); stdcall; begin Service2.Controller(CtrlCode); end; function TService2.GetServiceController: PServiceController; begin Result := @ServiceController; end; procedure TSparkyThread.Execute; begin while not Terminated do begin Beep; Sleep(500); end; end; procedure TService2.Service1Start(Sender: TService; var Started: Boolean); begin SparkyThread := TSparkyThread.Create(False); Started := True; end; procedure TService2.Service1Continue(Sender: TService; var Continued: Boolean); begin SparkyThread.Resume; Continued := True; end; 3-6Developer’ sGuide, Creatingapplicationsprocedure TService2.Service1Pause(Sender: TService; var Paused: Boolean); begin SparkyThread.Suspend; Paused := True; end; procedure TService2.Service1Stop(Sender: TService; var Stopped: Boolean); begin SparkyThread.Terminate; Stopped := True; end; end. When developing server applications, choosing to spawn a new thread depends on the nature of the service being provided, the anticipated number of connections, and the expected number of processors on the computer running the service. Service name properties The VCL provides classes for creating service applications. These include TService and TDependency. When using these classes, the various name properties can be confusing. This section describes the differences. Services have user names (called Service Start names) that are associated with passwords, display names for display in manager and editor windows, and actual names (the name of the service). Dependencies can be services or they can be load ordering groups. They also have names and display names. And because service objects are derived from TComponent, they inherit the Name property. The following two sections summarize these various name properties. TDependency properties The TDependency DisplayName is both a display name and the actual name of the service. It is nearly always the same as the TDependency Name property. TService name properties The TService Name property is inherited from TComponent. It is the name of the component, and is also the name of the service. For dependencies that are services, this property is the same as the TDependency Name and DisplayName properties. TService’s DisplayName is the name displayed in the Service Manager window. This often differs from the actual service name (TService.Name, TDependency.DisplayName, TDependency.Name). Note that the DisplayName for the Dependency and the DisplayName for the Service usually differ. Service start names are distinct from both the service display names and the actual service names. A ServiceStartName is the user name input on the Start dialog selected from the Service Control Manager. Buildingapplications, components, andlibraries3-7, CreatingpackagesandDLLs

Creating packages and DLLs

Dynamic link libraries (DLLs) are modules of compiled code that work in conjunction with an executable to provide functionality to an application. Packages are special DLLs used by Delphi applications, the Delphi IDE, or both. There are two kinds of packages: runtime packages and design-time packages. Runtime packages provide functionality to a program while that program is running. Design-time packages extend the functionality of the Delphi IDE. For more information on packages, see Chapter 10, “Working with packages and components.”

When to use packages and DLLs

For most applications written in Delphi, packages provide greater flexibility and are easier to create than DLLs. However, there are several situations where DLLs would be better suited to your projects than packages: • Your code module will be called from non-Delphi applications. • You are extending the functionality of a web server. • You are creating a code module to be used by third-party developers. • Your project is an OLE container.

Programming templates

Programming templates are part of the Code Insight features of Delphi, and are common, general code structures that allow you to add code specific to your programming task without having to reenter the general code structure. For instance, if you want to insert a for loop into your project you could select one of the for loop code templates. The following code is inserted: for := to do begin end; To insert a code template in the Code editor, press Ctrl-j and select the template you want to use. You can also add your own templates to this collection. To add a template: 1 Select Tools|Environment Options. 2 Click the Code Insight tab. 3 In the templates section click Add. 4 Choose a shortcut name and enter a brief description of the new template. 5 Add the template code to the Code text box. 6 Click OK. Now your new template is available when pressing Ctrl-j. 3-8Developer’ sGuide, Buildingdistributedapplications

Building distributed applications

Distributed applications are applications that are deployed to various machines and platforms and work together, typically over a network, to perform a set of related functions. For instance, an application for purchasing items and tracking those purchases for a nationwide company would require individual client applications for all the outlets, a main server that would process the requests of those clients, and an interface to a database that stores all the information regarding those transactions. By building a distributed client application (for instance, a web-based application), maintaining and updating the individual clients is vastly simplified. Delphi provides several options for the implementation model of distributed applications: • TCP/IP applications • COM and DCOM applications • CORBA applications • Database applications

Distributing applications using TCP/IP

TCP/IP is a communication protocol that allows you to write applications that communicate over networks. You can implement virtually any design in your applications. TCP/IP provides a transport layer, but does not impose any particular architecture for creating your distributed application. The growth of the Internet has created an environment where most computers already have some form of TCP/IP access, which simplifies distributing and setting up the application. Applications that use TCP/IP can be message-based distributed applications (such as Web server applications that service HTTP request messages) or distributed object applications (such as distributed database applications that communicate using Windows sockets). The most basic method of adding TCP/IP functionality to your applications is to use client or server sockets. Delphi also provides support for applications that extend Web servers by creating CGI scripts or DLLs. In addition, Delphi provides support for TCP/IP-based database applications. Using sockets in applications Two VCL classes, TClientSocket and TServerSocket, allow you to create TCP/IP socket connections to communicate with other remote applications. For more information on sockets, see Chapter 29, “Working with sockets.” Using client sockets Adding a TClientSocket object to your application makes that application a TCP/IP client application. TClientSocket manages client socket connections to a specified server and terminates those connections when the application is closed. Buildingapplications, components, andlibraries3-9, BuildingdistributedapplicationsUsing server sockets By adding a TServerSocket object to your application, it becomes a TCP/IP server application. TServerSocket listens for requests for TCP/IP connections from other machines and establishes connections when those requests are processed. Creating Web server applications To create a new Web server application, select File|New and select Web Server Application in the New Items dialog box. Then select the Web server application type: • ISAPI and NSAPI • CGI stand-alone • Win-CGI stand-alone CGI and Win-CGI applications use more system resources on the server, so complex applications are better created as ISAPI or NSAPI applications. For more information on building Web server applications, see Chapter 28, “Creating Internet server applications.” ISAPI and NSAPI Web server applications Selecting this type of application sets up your project as a DLL. ISAPI or NSAPI Web server applications are DLLs loaded by the Web server. Information is passed to the DLL, processed, and returned to the client by the Web server. CGI stand-alone Web server applications CGI Web server applications are console applications that receive requests from clients on standard input, processes those requests, and sends back the results to the server on standard output to be sent to the client. Win-CGI stand-alone Web server applications Win-CGI Web server applications are Windows applications that receive requests from clients from an INI file written by the server and writes the results to a file that the server sends to the client.

Distributing applications using COM and DCOM

COM provides a Windows-based distributed object architecture. COM applications use objects that are implemented by a different process or, if you use DCOM, on a separate machine. COM and ActiveX You must always register all COM objects and ActiveX controls when distributing applications that use COM and ActiveX. This is often done during the installation of the application, and the COM objects or ActiveX controls are subsequently unregistered during the uninstall. By registering the COM object or ActiveX control, other applications can locate the object and use it. 3-10Developer’ sGuide, BuildingdistributedapplicationsFor more information on COM and Active X controls, see Chapter 43, “Overview of COM Technologies,” Chapter 47, “Creating an ActiveX control,” and “Distributing a client application as an ActiveX control” on page 15-38. For more information on DCOM, see “Using DCOM connections” on page 15-8.

MTS

The Microsoft Transaction Server (MTS) is a robust environment that provides transaction services, security, and resource pooling for distributed COM applications. For more information on MTS, see Chapter 49, “Creating MTS objects” and “Using MTS” on page 15-5.

Distributing applications using CORBA

Common Object Request Broker Architecture (CORBA) is a method of using distributed objects in applications. The CORBA standard is used on many platforms, so writing CORBA applications allows you to make use of programs that are not running on a Windows machine. Like COM, CORBA is a distributed object architecture, meaning that client applications can make use of objects that are implemented on a remote server. For more information on CORBA, see Chapter 27, “Writing CORBA applications.” For instructions on distributing applications using CORBA, see “Deploying CORBA applications” on page 27-16.

Distributing database applications

Delphi provides support for creating distributed database applications using the MIDAS technology. This powerful technology includes a coordinated set of components that allow you to build a wide variety of multi-tiered database applications. Distributed database applications can be built on a variety of communications protocols, including DCOM, CORBA, TCP/IP, and OLEnterprise. For more information about building distributed database applications, see Chapter 15, “Creating multi-tiered applications.” Distributing database applications often requires you to distribute the Borland Database Engine (BDE) in addition to the application files. For information on deploying the BDE, see “Deploying database applications” on page 12-4. Buildingapplications, components, andlibraries3-11, 3-12Developer’ sGuide,

Chapter

Chapter 4Common programming tasks This chapter discusses the fundamentals for some of the common programming tasks in Delphi: • Handling exceptions • Using interfaces • Working with strings • Working with files

Handling exceptions

Delphi provides a mechanism to ensure that applications are robust, meaning that they handle errors in a consistent manner. Exception handling allows the application to recover from errors if possible and to shut down if need be, without losing data or resources. Error conditions in Delphi are indicated by exceptions. This section describes the following tasks for using exceptions to create safe applications: • Protecting blocks of code • Protecting resource allocations • Handling RTL exceptions • Handling component exceptions • Using TApplication.HandleException • Silent exceptions • Defining your own exceptions

Protecting blocks of code

To make your applications robust, your code needs to recognize exceptions when they occur and respond to them. If you don’t specify a response, the application will present a message box describing the error. Your job, then, is to recognize placesCommonprogrammingtasks4-1, Handlingexceptionswhere errors might happen, and define responses, particularly in areas where errors could cause the loss of data or system resources. When you create a response to an exception, you do so on blocks of code. When you have a series of statements that all require the same kind of response to errors, you can group them into a block and define error responses that apply to the whole block. Blocks with specific responses to exceptions are called protected blocks because they can guard against errors that might otherwise either terminate the application or damage data. To protect blocks of code you need to understand • Responding to exceptions • Exceptions and the flow of control • Nesting exception responses Responding to exceptions When an error condition occurs, the application raises an exception, meaning it creates an exception object. Once an exception is raised, your application can execute cleanup code, handle the exception, or both. • Executing cleanup code: The simplest way to respond to an exception is to guarantee that some cleanup code is executed. This kind of response doesn’t correct the condition that caused the error but lets you ensure that your application doesn’t leave its environment in an unstable state. You typically use this kind of response to ensure that the application frees allocated resources, regardless of whether errors occur. • Handling an exception: This is a specific response to a specific kind of exception. Handling an exception clears the error condition and destroys the exception object, which allows the application to continue execution. You typically define exception handlers to allow your applications to recover from errors and continue running. Types of exceptions you might handle include attempts to open files that don’t exist, writing to full disks, or calculations that exceed legal bounds. Some of these, such as “File not found,” are easy to correct and retry, while others, such as running out of memory, might be more difficult for the application or the user to correct. Exceptions and the flow of control Object Pascal makes it easy to incorporate error handling into your applications because exceptions don’t get in the way of the normal flow of your code. In fact, by moving error checking and error handling out of the main flow of your algorithms, exceptions can simplify the code you write. When you declare a protected block, you define specific responses to exceptions that might occur within that block. When an exception occurs in that block, execution immediately jumps to the response you defined, then leaves the block. Example The following code that includes a protected block. If any exception occurs in the protected block, execution jumps to the exception-handling part, which beeps. Execution resumes outside the block. 4-2Developer’ sGuide, Handlingexceptions... try{ begin the protected block } Font.Name := 'Courier';{ if any exception occurs... } Font.Size := 24;{ ...in any of these statements... } Color := clBlue; except{ ...execution jumps to here } on Exception do MessageBeep(0);{ this handles any exception by beeping } end; ...{ execution resumes here, outside the protected block}

Nesting exception responses

Your code defines responses to exceptions that occur within blocks. Because Pascal allows you to nest blocks inside other blocks, you can customize responses even within blocks that already customize responses. In the simplest case, for example, you can protect a resource allocation, and within that protected block, define blocks that allocate and protect other resources. Conceptually, that might look something like this: You can also use nested blocks to define local handling for specific exceptions that overrides the handling in the surrounding block. Conceptually, that looks something like this: You can also mix different kinds of exception-response blocks, nesting resource protections within exception handling blocks and vice versa.

Protecting resource allocations

One key to having a robust application is ensuring that if it allocates resources, it also releases them, even if an exception occurs. For example, if your application allocatesCommonprogrammingtasks4-3, Handlingexceptionsmemory, you need to make sure it eventually releases the memory, too. If it opens a file, you need to make sure it closes the file later. Keep in mind that exceptions don’t come just from your code. A call to an RTL routine, for example, or another component in your application might raise an exception. Your code needs to ensure that if these conditions occur, you release allocated resources. To protect resources effectively, you need to understand the following: • What kind of resources need protection? • Creating a resource protection block What kind of resources need protection? Under normal circumstances, you can ensure that an application frees allocated resources by including code for both allocating and freeing. When exceptions occur, however, you need to ensure that the application still executes the resource-freeing code. Some common resources that you should always be sure to release are: • Files • Memory • Windows resources • Objects Example The following event handler allocates memory, then generates an error, so it never executes the code to free the memory: procedure TForm1.Button1Click(Sender: TComponent); var APointer: Pointer; AnInteger, ADividend: Integer; begin ADividend := 0; GetMem(APointer, 1024);{ allocate 1K of memory } AnInteger := 10 div ADividend;{ this generates an error } FreeMem(APointer, 1024);{ it never gets here } end; Although most errors are not that obvious, the example illustrates an important point: When the division-by-zero error occurs, execution jumps out of the block, so the FreeMem statement never gets to free the memory. In order to guarantee that the FreeMem gets to free the memory allocated by GetMem, you need to put the code in a resource-protection block. Creating a resource protection block To ensure that you free allocated resources, even in case of an exception, you embed the resource-using code in a protected block, with the resource-freeing code in a special part of the block. Here’s an outline of a typical protected resource allocation: 4-4Developer’ sGuide, Handlingexceptions{ allocate the resource } try { statements that use the resource } finally { free the resource } end; The key to the try..finally block is that the application always executes any statements in the finally part of the block, even if an exception occurs in the protected block. When any code in the try part of the block (or any routine called by code in the try part) raises an exception, execution halts at that point. Once an exception handler is found, execution jumps to the finally part, which is called the cleanup code. After the finally part is executed, the exception handler is called. If no exception occurs, the cleanup code is executed in the normal order, after all the statements in the try part. Example The following code illustrates an event handler that allocates memory and generates an error, but still frees the allocated memory: procedure TForm1.Button1Click(Sender: TComponent); var APointer: Pointer; AnInteger, ADividend: Integer; begin ADividend := 0; GetMem(APointer, 1024);{ allocate 1K of memory } try AnInteger := 10 div ADividend;{ this generates an error } finally FreeMem(APointer, 1024);{ execution resumes here, despite the error } end; end; The statements in the termination code do not depend on an exception occurring. If no statement in the try part raises an exception, execution continues through the termination code.

Handling RTL exceptions

When you write code that calls routines in the runtime library (RTL), such as mathematical functions or file-handling procedures, the RTL reports errors back to your application in the form of exceptions. By default, RTL exceptions generate a message that the application displays to the user. You can define your own exception handlers to handle RTL exceptions in other ways. There are also silent exceptions that do not, by default, display a message. To handle RTL exceptions effectively, you need to understand the following: • What are the RTL exceptions? • Creating an exception handler • Exception handling statements • Using the exception instanceCommonprogrammingtasks4-5, Handlingexceptions• Scope of exception handlers • Providing default exception handlers • Handling classes of exceptions • Reraising the exception

What are the RTL exceptions?

The runtime library’s exceptions are defined in the SysUtils unit, and they all descend from a generic exception-object type called Exception. Exception provides the string for the message that RTL exceptions display by default. There are several kinds of exceptions raised by the RTL, as described in the following table. Error type Cause Meaning Input/output Error accessing a file Most I/O exceptions are related to error codes or I/O device returned by Windows when accessing a file. Heap Error using dynamic Heap errors can occur when there is insufficient memory memory available, or when an application disposes of a pointer that points outside the heap. Integer math Illegal operation on Errors include division by zero, numbers or integer-type expressions out of range, and overflows. expressions Floating-point math Illegal operation on Floating-point errors can come from either a real-type expressions hardware coprocessor or the software emulator. Errors include invalid instructions, division by zero, and overflow or underflow. Typecast Invalid typecasting Objects can only be typecast to compatible types. with the as operator Conversion Invalid type Type-conversion functions such as IntToStr, conversion StrToInt, and StrToFloat raise conversion exceptions when the parameter cannot be converted to the desired type. Hardware System condition Hardware exceptions indicate that either the processor or the user generated some kind of error condition or interruption, such as an access violation, stack overflow, or keyboard interrupt. Variant Illegal type coercion Errors can occur when referring to variants in expressions where the variant cannot be coerced into a compatible type. For a list of the RTL exception types, see the SysUtils unit.

Creating an exception handler

An exception handler is code that handles a specific exception or exceptions that occur within a protected block of code. To define an exception handler, embed the code you want to protect in an exception- handling block and specify the exception handling statements in the except part of the block. Here is an outline of a typical exception-handling block: 4-6Developer’ sGuide, Handlingexceptionstry { statements you want to protect } except { exception-handling statements } end; The application executes the statements in the except part only if an exception occurs during execution of the statements in the try part. Execution of the try part statements includes routines called by code in the try part. That is, if code in the try part calls a routine that doesn’t define its own exception handler, execution returns to the exception-handling block, which handles the exception. When a statement in the try part raises an exception, execution immediately jumps to the except part, where it steps through the specified exception-handling statements, or exception handlers, until it finds a handler that applies to the current exception. Once the application locates an exception handler that handles the exception, it executes the statement, then automatically destroys the exception object. Execution continues at the end of the current block.

Exception handling statements

Each on statement in the except part of a try..except block defines code for handling a particular kind of exception. The form of these exception-handling statements is as follows: on do ; Example You can define an exception handler for division by zero to provide a default result: function GetAverage(Sum, NumberOfItems: Integer): Integer; begin try Result := Sum div NumberOfItems;{ handle the normal case } except on EDivByZero do Result := 0;{ handle the exception only if needed } end; end; Note that this is clearer than having to test for zero every time you call the function. Here’s an equivalent function that doesn’t take advantage of exceptions: function GetAverage(Sum, NumberOfItems: Integer): Integer; begin if NumberOfItems <> 0 then{ always test } Result := Sum div NumberOfItems{ use normal calculation } else Result := 0;{ handle exceptional case } end; The difference between these two functions really defines the difference between programming with exceptions and programming without them. This example is quite simple, but you can imagine a more complex calculation involving hundreds of steps, any one of which could fail if one of dozens of inputs were invalid. By using exceptions, you can spell out the “normal” expression of your algorithm, then provide for those exceptional cases when it doesn’t apply. Without exceptions, Commonprogrammingtasks4-7, Handlingexceptionsyou have to test every single time to make sure you’re allowed to proceed with each step in the calculation. Using the exception instance Most of the time, an exception handler doesn’t need any information about an exception other than its type, so the statements following on.do are specific only to the type of exception. In some cases, however, you might need some of the information contained in the exception instance. To read specific information about an exception instance in an exception handler, you use a special variation of on..do that gives you access to the exception instance. The special form requires that you provide a temporary variable to hold the instance. Example If you create a new project that contains a single form, you can add a scroll bar and a command button to the form. Double-click the button and add the following line to its click-event handler: ScrollBar1.Max := ScrollBar1.Min - 1; That line raises an exception because the maximum value of a scroll bar must always exceed the minimum value. The default exception handler for the application opens a dialog box containing the message in the exception object. You can override the exception handling in this handler and create your own message box containing the exception’s message string: try ScrollBar1.Max := ScrollBar1.Min - 1; except on E: EInvalidOperation do MessageDlg('Ignoring exception: ' + E.Message, mtInformation, [mbOK], 0); end; The temporary variable (E in this example) is of the type specified after the colon (EInvalidOperation in this example). You can use the as operator to typecast the exception into a more specific type if needed. Note Never destroy the temporary exception object. Handling an exception automatically destroys the exception object. If you destroy the object yourself, the application attempts to destroy the object again, generating an access violation. Scope of exception handlers You do not need to provide handlers for every kind of exception in every block. In fact, you need to provide handlers only for those exceptions that you want to handle specially within a particular block. If a block does not handle a particular exception, execution leaves that block and returns to the block that contains the block (or to the code that called the block), with the exception still raised. This process repeats with increasingly broad scope until either execution reaches the outermost scope of the application or a block at some level handles the exception. 4-8Developer’ sGuide, HandlingexceptionsProviding default exception handlers You can provide a single default exception handler to handle any exceptions you haven’t provided specific handlers for. To do that, you add an else part to the except part of the exception-handling block: try { statements } except on ESomething do { specific exception-handling code }; else { default exception-handling code }; end; Adding default exception handling to a block guarantees that the block handles every exception in some way, thereby overriding all handling from the containing block. Caution It is not advisable to use this all-encompassing default exception handler. The else clause handles all exceptions, including those you know nothing about. In general, your code should handle only exceptions you actually know how to handle. If you want to handle cleanup and leave the exception handling to code that has more information about the exception and how to handle it, then you can do so use an enclosing try..finally block: try try { statements } except on ESomething do { specific exception-handling code }; end; finally {cleanup code }; end; For another approach to augmenting exception handling, see Reraising the exception. Handling classes of exceptions Because exception objects are part of a hierarchy, you can specify handlers for entire parts of the hierarchy by providing a handler for the exception class from which that part of the hierarchy descends. Example The following block outlines an example that handles all integer math exceptions specially: try { statements that perform integer math operations } except on EIntError do { special handling for integer math errors }; end; You can still specify specific handlers for more specific exceptions, but you need to place those handlers above the generic handler, because the application searches the handlers in the order they appear in, and executes the first applicable handler itCommonprogrammingtasks4-9, Handlingexceptionsfinds. For example, this block provides special handling for range errors, and other handling for all other integer math errors: try { statements performing integer math } except on ERangeError do { out-of-range handling }; on EIntError do { handling for other integer math errors }; end; Note that if the handler for EIntError came before the handler for ERangeError, execution would never reach the specific handler for ERangeError. Reraising the exception Sometimes when you handle an exception locally, you actually want to augment the handling in the enclosing block, rather than replacing it. Of course, when your local handler finishes its handling, it destroys the exception instance, so the enclosing block’s handler never gets to act. You can, however, prevent the handler from destroying the exception, giving the enclosing handler a chance to respond. Example When an exception occurs, you might want to display some sort of message to the user, then proceed with the standard handling. To do that, you declare a local exception handler that displays the message then calls the reserved word raise. This is called reraising the exception, as shown in this example: try { statements } try { special statements } except on ESomething do begin { handling for only the special statements } raise;{ reraise the exception } end; end; except on ESomething do ...;{ handling you want in all cases } end; If code in the { statements } part raises an ESomething exception, only the handler in the outer except part executes. However, if code in the { special statements } part raises ESomething, the handling in the inner except part is executed, followed by the more general handling in the outer except part. By reraising exceptions, you can easily provide special handling for exceptions in special cases without losing (or duplicating) the existing handlers.

Handling component exceptions

Delphi’s components raise exceptions to indicate error conditions. Most component exceptions indicate programming errors that would otherwise generate a runtime 4-10Developer’ sGuide, Handlingexceptionserror. The mechanics of handling component exceptions are no different than handling RTL exceptions. Example A common source of errors in components is range errors in indexed properties. For example, if a list box has three items in its list (0..2) and your application attempts to access item number 3, the list box raises an “Index out of range” exception. The following event handler contains an exception handler to notify the user of invalid index access in a list box: procedure TForm1.Button1Click(Sender: TObject); begin ListBox1.Items.Add('a string');{ add a string to list box } ListBox1.Items.Add('another string');{ add another string... } ListBox1.Items.Add('still another string');{ ...and a third string } try Caption := ListBox1.Items[3];{ set form caption to fourth string in list box } except on EStringListError do MessageDlg('List box contains fewer than four strings', mtWarning, [mbOK], 0); end; end; If you click the button once, the list box has only three strings, so accessing the fourth string (Items[3]) raises an exception. Clicking a second time adds more strings to the list, so it no longer causes the exception.

Using TApplication.HandleException

HandleException provides default handling of exceptions for the application. If an exception passes through all the try blocks in the application code, the application automatically calls the HandleException method, which displays a dialog box indicating that an error has occurred. You can use HandleException in this fashion: try { statements } except Application.HandleException(Self); end; For all exceptions but EAbort, HandleException calls the OnException event handler, if one exists. Therefore, if you want to both handle the exception, and provide this default behavior as the VCL does, you can add a call to HandleException to your code: try { special statements } except on ESomething do begin { handling for only the special statements } Application.HandleException(Self);{ call HandleException } end; end; Commonprogrammingtasks4-11, HandlingexceptionsFor more information, see exception-handling routines in the categorical list of runtime library routines.

Silent exceptions

Delphi applications handle most exceptions that your code doesn’t specifically handle by displaying a message box that shows the message string from the exception object. You can also define “silent” exceptions that do not, by default, cause the application to show the error message. Silent exceptions are useful when you don’t intend to handle an exception, but you want to abort an operation. Aborting an operation is similar to using the Break or Exit procedures to break out of a block, but can break out of several nested levels of blocks. Silent exceptions all descend from the standard exception type EAbort. The default exception handler for Delphi VCL applications displays the error-message dialog box for all exceptions that reach it except those descended from EAbort. Note For console applications, an error-message dialog is displayed on an EAbort exception. There is a shortcut for raising silent exceptions. Instead of manually constructing the object, you can call the Abort procedure. Abort automatically raises an EAbort exception, which will break out of the current operation without displaying an error message. Example The following code shows a simple example of aborting an operation. On a form containing an empty list box and a button, attach the following code to the button’s OnClick event: procedure TForm1.Button1Click(Sender: TObject); var I: Integer; begin for I := 1 to 10 do{ loop ten times } begin ListBox1.Items.Add(IntToStr(I));{ add a numeral to the list } if I = 7 then Abort;{ abort after the seventh one } end; end;

Defining your own exceptions

In addition to protecting your code from exceptions generated by the runtime library and various components, you can use the same mechanism to manage exceptional conditions in your own code. To use exceptions in your code, you need to understand these steps: • Declaring an exception object type • Raising an exception 4-12Developer’ sGuide, UsinginterfacesDeclaring an exception object type Because exceptions are objects, defining a new kind of exception is as simple as declaring a new object type. Although you can raise any object instance as an exception, the standard exception handlers handle only exceptions descended from Exception. It’s therefore a good idea to derive any new exception types from Exception or one of the other standard exceptions. That way, if you raise your new exception in a block of code that isn’t protected by a specific exception handler for that exception, one of the standard handlers will handle it instead. Example For example, consider the following declaration: type EMyException = class(Exception); If you raise EMyException but don’t provide a specific handler for EMyException, a handler for Exception (or a default exception handler) will still handle it. Because the standard handling for Exception displays the name of the exception raised, you could at least see that it was your new exception raised. Raising an exception To indicate an error condition in an application, you can raise an exception which involves constructing an instance of that type and calling the reserved word raise. To raise an exception, call the reserved word raise, followed by an instance of an exception object. When an exception handler actually handles the exception, it finishes by destroying the exception instance, so you never need to do that yourself. Setting the exception address is done through the ErrorAddr variable in the System unit. Raising an exception sets this variable to the address where the application raised the exception. You can refer to ErrorAddr in your exception handlers, for example, to notify the user of where the error occurred. You can also specify a value for ErrorAddr when you raise an exception. To specify an error address for an exception, add the reserved word at after the exception instance, followed by an address expression such as an identifier. For example, given the following declaration, type EPasswordInvalid = class(Exception); you can raise a “password invalid” exception at any time by calling raise with an instance of EPasswordInvalid, like this: if Password <> CorrectPassword then raise EPasswordInvalid.Create('Incorrect password entered');

Using interfaces

Delphi’s interface keyword allows you to create and use interfaces in your application. Interfaces are a way extending the single-inheritance model of the VCLCommonprogrammingtasks4-13, Usinginterfacesby allowing a single class to implement more than one interface, and by allowing several classes descended from different bases to share the same interface. Interfaces are useful when sets of operations, such as streaming, are used across a broad range of objects. Interfaces are also a fundamental aspect of the COM (the Component Object Model) and Corba (Common Object Request Broker Architecture) distributed object models.

Interfaces as a language feature

An interface is like a class that contains only abstract methods and a clear definition of their functionality. Strictly speaking, interface method definitions include the number and types of their parameters, their return type, and their expected behavior. Interface methods are semantically or logically related to indicate the purpose of the interface. It is convention for interfaces to be named according to their behavior and to be prefaced with a capital I. For example, an IMalloc interface would allocate, free, and manage memory. Similarly, an IPersist interface could be used as a general base interface for descendants, each of which defines specific method prototypes for loading and saving the state of an object to a storage, stream, or file. A simple example of declaring an interface is: type IEdit = interface procedure Copy; stdcall; procedure Cut; stdcall; procedure Paste; stdcall; function Undo: Boolean; stdcall; end; Like abstract classes, interfaces themselves can never be instantiated. To use an interface, you need to obtain it from an implementing class. To implement an interface, you must define a class that declares the interface in its ancestor list, indicating that it will implement all of the methods of that interface: TEditor = class(TInterfacedObject, IEdit) procedure Copy; stdcall; procedure Cut; stdcall; procedure Paste; stdcall; function Undo: Boolean; stdcall; end; While interfaces define the behavior and signature of their methods, they do not define the implementations. As long as the class’s implementation conforms to the interface definition, the interface is fully polymorphic, meaning that accessing and using the interface is the same for any implementation of it. Sharing interfaces between classes Using interfaces offers a design approach to separating the way a class is used from the way it is implemented. Two classes can share the same interface without requiring that they descend from the same base class. This polymorphic invocation of the same methods on unrelated objects is possible as long as the objects implement the same interface. For example, consider the interface, 4-14Developer’ sGuide, UsinginterfacesIPaint = interface procedure Paint; end; and the two classes, TSquare = class(TPolygonObject, IPaint) procedure Paint; end; TCircle = class(TCustomShape, IPaint) procedure Paint; end; Whether or not the two classes share a common ancestor, they are still assignment compatible with a variable of IPaint as in var Painter: IPaint; begin Painter := TSquare.Create; Painter.Paint; Painter := TCircle.Create; Painter.Paint; end; This could have been accomplished by having TCircle and TSquare descend from say, TFigure which implemented a virtual method Paint. Both TCircle and TSquare would then have overridden the Paint method. The above IPaint would be replaced by TFigure. However, consider the following the interface: IRotate = interface procedure Rotate(Degrees: Integer); end; which makes sense for the rectangle to support but not the circle. The classes would look like TSquare = class(TRectangularObject, IPaint, IRotate) procedure Paint; procedure Rotate(Degrees: Integer); end; TCircle = class(TCustomShape, IPaint) procedure Paint; end; Later, you could create a class TFilledCircle that implements the IRotate interface to allow rotation of a pattern used to fill the circle without having to add rotation to the simple circle. Note For these examples, the immediate base class or an ancestor class is assumed to have implemented the methods of IUnknown that manage reference counting. For more information, see “Implementing IUnknown” on page 4-16 and “Memory management of interface objects” on page 4-20. Commonprogrammingtasks4-15, UsinginterfacesUsing interfaces with procedures Interfaces also allow you to write generic procedures that can handle objects without requiring the objects to descend from a particular base class. Using the above IPaint and IRotate interfaces you can write the following procedures, procedure PaintObjects(Painters: array of IPaint); var I: Integer; begin for I := Low(Painters) to High(Painters) do Painters[I].Paint; end; procedure RotateObjects(Degrees: Integer; Rotaters: array of IRotate); var I: Integer; begin for I := Low(Rotaters) to High(Rotaters) do Rotaters[I].Rotate(Degrees); end; RotateObjects does not require that the objects know how to paint themselves and PaintObjects does not require the objects know how to rotate. This allows the above objects to be used more often than if they where written to only work against a TFigure class. Form details about the syntax, language definitions and rules for interfaces, see the Object Pascal Language Guide online Help section on “Object interfaces.”

Implementing IUnknown

All interfaces derive either directly or indirectly from the IUnknown interface. This interface provides the essential functionality of an interface, that is, dynamic querying and lifetime management. This functionality is established in the three IUnknown methods: • QueryInterface provides a method for dynamically querying a given object and obtaining interface references for the interfaces the object supports. • AddRef is a reference counting method that increments the count each time the call to QueryInterface succeeds. While the reference count is nonzero the object must remain in memory. • Release is used in conjunction with AddRef to enable an object to track its own lifetime and to determine when it is safe to delete itself. Once the reference count reaches zero the interface implementation releases the underlying object(s). Every class that implements interfaces must implement the three IUnknown methods, as well as all of the methods declared by any other ancestor interfaces, and all of the methods declared by the interface itself. You can, however, inherit the implementations of methods of interfaces declared in your class. 4-16Developer’ sGuide, Usinginterfaces

TInterfacedObject

The VCL defines a simple class, TInterfacedObject, that serves as a convenient base because it implements the methods of IUnknown. TInterfacedObject class is declared in the System unit as follows: type TInterfacedObject = class(TObject, IUnknown) private FRefCount: Integer; protected function QueryInterface(const IID: TGUID; out Obj): Integer; stdcall; function _AddRef: Integer; stdcall; function _Release: Integer; stdcall; public property RefCount: Integer read FRefCount; end; Deriving directly from TInterfacedObject is straightforward. In the following example declaration, TDerived is a direct descendent of TInterfacedObject and implements a hypothetical IPaint interface. type TDerived = class(TInterfacedObject, IPaint) ... end; Because it implements the methods of IUnknown, TInterfacedObject automatically handles reference counting and memory management of interfaced objects. For more information, see “Memory management of interface objects” on page 4-20, which also discusses writing your own classes that implement interfaces but that do not follow the reference-counting mechanism inherent in TInterfacedObject.

Using the as operator

Classes that implement interfaces can use the as operator for dynamic binding on the interface. In the following example: procedure PaintObjects(P: TInterfacedObject) var X: IPaint; begin X := P as IPaint; { statements } end; the variable P of type TInterfacedObject, can be assigned to the variable X, which is an IPaint interface reference. Dynamic binding makes this assignment possible. For this assignment, the compiler generates code to call the QueryInterface method of P’s IUnknown interface since the complier cannot tell from P’s declared type whether P’s instance actually supports IPaint. At runtime, P either resolves to an IPaint reference or an exception is raised. In either case, assigning P to X will not generate a compile- time error, as it would if P was of a class type that did not implement IUnknown. Commonprogrammingtasks4-17, UsinginterfacesWhen you use the as operator for dynamic binding on an interface, you should be aware of the following requirements: • Explicitly declaring IUnknown: Although all interfaces derive from IUnknown, it is not sufficient, if you want to use the as operator, for a class to simply implement the methods of IUnknown. This is true even if it also implements the interfaces it explicitly declares. The class must explicitly declare IUnknown in its ancestor list. • Using an IID: Interfaces can use an identifier that is based on a GUID (globally unique identifier). GUIDs that are used to identify interfaces are referred to as interface identifiers (IIDs). If you are using the as operator with an interface, it must have an associated IID. To create a new GUID in your source code you can use the Ctrl+Shift+G editor hot-key.

Reusing code and delegation

One approach to reusing code with interfaces is to have an object contain, or be contained by another. The VCL uses properties that are object types as an approach to containment and code reuse. To support this design for interfaces Delphi has a keyword implements, that makes if easy to write code to delegate all or part of the implementation of an interface to a sub-object. Aggregation is another way of reusing code through containment and delegation. In aggregation, an outer object contains an inner object that implements interfaces which are exposed only by the outer object. The VCL has classes that support aggregation. Using implements for delegation Many classes in the VCL have properties that are sub-objects. You can also use interfaces as property types. When a property is of an interface type (or a class type that implements the methods of an interface) you can use the keyword implements to specify that the methods of that interface are delegated to the object or interface reference which is the property instance. The delegate only needs to provide implementation for the methods, it does not have to declare the interface support. The class containing the property must include the interface in its ancestor list. By default using the keyword implements delegates all interface methods. However, you can use methods resolution clauses or declare methods in your class that implement some of the interface methods as a way of overriding this default behavior. The following example uses the implements keyword in the design of a color adapter object that converts an 8-bit RGB color value to a Color reference: type IRGB8bit = interface ['{1d76360a-f4f5-11d1-87d4-00c04fb17199}'] function Red: Byte; function Green: Byte; function Blue: Byte; end; 4-18Developer’ sGuide, UsinginterfacesIColorRef = interface ['{1d76360b-f4f5-11d1-87d4-00c04fb17199}'] function Color: Integer; end; { TRGB8ColorRefAdapter map an IRGB8bit to an IColorRef } TRGB8ColorRefAdapter = class(TInterfacedObject, IRGB8bit, IColorRef) private FRGB8bit: IRGB8bit; FPalRelative: Boolean; public constructor Create(rgb: IRGB8bit); property RGB8Intf: IRGB8bit read FRGB8bit implements IRGB8bit; property PalRelative: Boolean read FPalRelative write FPalRelative; function Color: Integer; end; implementation constructor TRGB8ColorRefAdapter.Create(rgb: IRGB8bit); begin FRGB8bit := rgb; end; function TRGB8ColorRefAdapter.Color: Integer; begin if FPalRelative then Result := PaletteRGB(RGB8Intf.Red, RGB8Intf.Green, RGB8Intf.Blue) else Result := RGB(RGB8Intf.Red, RGB8Intf.Green, RGB8Intf.Blue); end; end. For more information about the syntax, implementation details, and language rules of the implements keyword, see the Object Pascal Language Guide online Help section on “Object interfaces.”

Aggregation

Aggregation offers a modular approach to code reuse through sub-objects that define the functionality of a containing object, but that hide the implementation details from that object. In aggregation, an outer object implements one or more interfaces. The only requirement is that it implement IUnknown. The inner object, or objects, can implement one or more interfaces, however only the outer object exposes the interfaces. These include both the interfaces it implements and the ones implemented by its contained objects. Clients know nothing about inner objects. While the outer object provides access to the inner object interfaces, their implementation is completely transparent. Therefore, the outer object class can exchange the inner object class type for any class that implements the same interface. Correspondingly, the code for the inner object classes can be shared by other classes that want to use it. The implementation model for aggregation defines explicit rules for implementing IUnknown using delegation. The inner object must implement an IUnknown on itself, that controls the inner object’s reference count. This implementation of IUnknown tracks the relationship between the outer and the inner object. For example, when anCommonprogrammingtasks4-19, Usinginterfacesobject of its type (the inner object) is created, the creation succeeds only for a requested interface of type IUnknown. The inner object also implements a second IUnknown for all the interfaces it implements. These are the interfaces exposed by the outer object. This second IUnknown delegates calls to QueryInterface, AddRef, and Release to the outer object. The outer IUnknown is referred to as the “controlling Unknown.” Refer to the MS online help for the rules about creating an aggregation. When writing your own aggregation classes, you can also refer to the implementation details of IUnknown in TComObject. TComObject is a COM class that supports aggregation. If you are writing COM applications, you can also use TComObject directly as a base class.

Memory management of interface objects

One of the concepts behind the design of interfaces is ensuring the lifetime management of the objects that implement them. The AddRef and Release methods of IUnknown provide a way of implementing this functionality. Their defined behavior states that they will track the lifetime of an object by incrementing the reference count on the object when an interface reference is passed to a client, and will destroy the object when that reference count is zero. If you are creating COM objects for distributed applications, then you should strictly adhere to the reference counting rules. However, if you are using interfaces only internally in your application, then you have a choice that depends upon the nature of your object and how you decide to use it. Using reference counting Delphi provides most of the IUnknown memory management for you by its implementation of interface querying and reference counting. Therefore, if you have an object that lives and dies by its interfaces, you can easily use reference counting by deriving from these classes. TInterfacedObject is the non-CoClass that provides this behavior. If you decide to use reference counting, then you must be careful to only hold the object as an interface reference, and to be consistent in your reference counting. For example: procedure beep(x: ITest); function test_func() var y: ITest; begin y := TTest.Create; // because y is of type ITest, the reference count is one beep(y); // the act of calling the beep function increments the reference count // and then decrements it when it returns y.something; // object is still here with a reference count of one end; This is the cleanest and safest approach to memory management; and if you use TInterfacedObject it is handled automatically. If you do not follow this rule, your object can unexpectedly disappear, as demonstrated in the following code: 4-20Developer’ sGuide, Usinginterfacesfunction test_func() var x: TTest; begin x := TTest.Create; // no count on the object yet beep(x as ITest); // count is incremented by the act of calling beep // and decremented when it returns x.something; // surprise, the object is gone end; Note In the examples above, the beep procedure, as it is declared, will increment the reference count (call AddRef) on the parameter, whereas either of the following declarations: procedure beep(const x: ITest); or procedure beep(var x: ITest); will not. These declarations generate smaller, faster code. One case where you cannot use reference counting, because it cannot be consistently applied, is if your object is a component or a control owned by another component. In that case, you can still use interfaces, but you should not use reference counting because the lifetime of the object is not dictated by its interfaces. Not using reference counting If your object is a VCL component or a control that is owned by another component, then your object is part of a different memory management system that is based in TComponent. You should not mix the object lifetime management approaches of VCL components and COM reference counting. If you want to create a component that supports interfaces, you can implement the IUnknown AddRef and Release methods as empty functions to bypass the COM reference counting mechanism: function TMyObject.AddRef: Integer; begin Result := -1; end; function TMyObject.Release: Integer; begin Result := -1; end; You would still implement QueryInterface as usual to provide dynamic querying on your object. Note that, because you do implement QueryInterface, you can still use the as operator for interfaces on components, as long as you create an interface identifier (IID). You can also use aggregation. If the outer object is a component, the inner object implements reference counting as usual, by delegating to the “controlling Unknown.” It is at the level of the outer, component object that the decision is made to circumvent the AddRef and Release methods, and to handle memory management via the VCL approach. In fact, you can use TInterfacedObject as a base class for an inner object of an aggregation that has a component as its containing outer object. Commonprogrammingtasks4-21, WorkingwithstringsNote The “controlling Unknown” is the IUnknown implemented by the outer object and the one for which the reference count of the entire object is maintained. For more information distinguishing the various implementations of the IUnknown interface by the inner and outer objects, see “Aggregation” on page 4-19 and the Microsoft online Help topics on the “controlling Unknown.”

Using interfaces in distributed applications

Interfaces are a fundamental element in the COM and CORBA distributed object models. Delphi provides base classes for these technologies that extend the basic interface functionality in TInterfacedObject, which simply implements the IUnknown interface methods. COM classes add functionality for using class factories and class identifiers (CLSIDs). Class factories are responsible for creating class instances via CLSIDs. The CLSIDs are used to register and manipulate COM classes. COM classes that have class factories and class identifiers are called CoClasses. CoClasses take advantage of the versioning capabilities of QueryInterface, so that when a software module is updated QueryInterface can be invoked at runtime to query the current capabilities of an object. New versions of old interfaces, as well as any new interfaces or features of an object, can become immediately available to new clients. At the same time, objects retain complete compatibility with existing client code; no recompilation is necessary because interface implementations are hidden (while the methods and parameters remain constant). In COM applications, developers can change the implementation to improve performance, or for any internal reason, without breaking any client code that relies on that interface. For more information about COM interfaces, see Chapter 43, “Overview of COM Technologies.” The other distributed application technology is CORBA. The use of interfaces in CORBA applications is mediated by stub classes on the client and skeleton classes on the server. These stub and skeleton classes handle the details of marshaling interface calls so that parameter values and return values can be transmitted correctly. Applications must use either a stub or skeleton class, or employ the Dynamic Invocation Interface (DII) which converts all parameters to special variants (so that they carry their own type information.) Although it is not a necessary feature of CORBA technology, Delphi implements CORBA using class factories, similar to the way in which COM uses class factories and CoClasses. By unifying the two distributed model architectures in this way, Delphi supports a combined COM/ CORBA server that can service both COM and CORBA clients simultaneously. For more information about using interfaces with Corba, see Chapter 27, “Writing CORBA applications.”

Working with strings

Delphi has a number of different character and string types that have been introduced throughout the development of the Object Pascal language. This section is an overview of these types, their purpose, and usage. For language details, see the Object Pascal Language online Help on “String types.” 4-22Developer’ sGuide, Workingwithstrings

Character types

Delphi has three character types: Char, AnsiChar, and WideChar. The Char character type came from Standard Pascal, and was used in Turbo Pascal and then in Object Pascal. Later Object Pascal added AnsiChar and WideChar as specific character types that were used to support standards for character representation on the Windows operating system. AnsiChar was introduced to support an 8-bit character ANSI standard, and WideChar was introduced to support a 16-bit Unicode standard. The name WideChar is used because Unicode characters are also known as wide characters. Wide characters are two bytes instead of one, so that the character set can represent many more different characters. When AnsiChar and WideChar were implemented, Char became the default character type representing the currently recommended implementation. If you use Char in your application, remember that its implementation is subject to change in future versions of Delphi. The following table summarizes these character types: Table 4.1 Object Pascal character types Type Bytes Contents Purpose Char 1 Holds a single ANSI Default character type. character. AnsiChar 1 Holds a single ANSI 8-bit Ansi character standard on Windows. character. WideChar 2 Holds a single 16-bit Unicode standard on Windows. Unicode character. For more information about using these character types, see the Object Pascal Language Guide online Help on Character types” For more information about Unicode characters, see the Object Pascal Language Guide online Help on “About extended character sets.”

String types

Delphi has three categories of types that you can use when working with strings. These are character pointers, string types, and VCL string classes. This section summarizes string types, and discusses using them with character pointers. For information about using VCL string classes, see the online Help on TStrings. There are currently three string implementations in Delphi: short strings, long strings, and wide strings. There are several different string types that represent these implementations. In addition, there is a reserved word string that defaults to the currently recommended string implementation. Short strings String was the first string type used in Turbo Pascal. String was originally implemented as a short string. Short strings are an allocation of between 1 andCommonprogrammingtasks4-23, Workingwithstrings256 bytes, of which the first byte contains the length of the string and the remaining bytes contain the characters in the string: S: string[0..n]// the original string type When long strings were implemented, string was changed to a long string implementation by default and ShortString was introduced as a backward compatibility type. ShortString is a predefined type for a maximum length string: S: string[255]// the ShortString type The size of the memory allocated for a ShortString is static, meaning that it is determined at compile time. However, the location of the memory for the ShortString can be dynamically allocated, for example if you use a PShortString, which is a pointer to a ShortString. The number of bytes of storage occupied by a short string type variable is the maximum length of the short string type plus one. For the ShortString predefined type the size is 256 bytes. Both short strings, declared using the syntax string[0..n], and the ShortString predefined type exist primarily for backward compatibility with earlier versions of Delphi and Borland Pascal. A compiler directive, $H, controls whether the reserved word string represents a short string or a long string. In the default state, {$H+}, string represents a long string. You can change it to a ShortString by using the {$H-} directive. The {$H-} state is mostly useful for using code from versions of Object Pascal that used short strings by default. However, short strings can be useful in data structures where you need a fixed-size component or in DLLs when you don’t want to use the ShareMem unit (see also the online Help on “Managing memory”). You can locally override the meaning of string-type definitions to ensure generation of short strings. You can also change declarations of short string types to string[255] or ShortString, which are unambiguous and independent of the $H setting. For details about short strings and the ShortString type, see the Object Pascal Language Guide online Help on “Short strings.” Long strings Long strings are dynamically-allocated strings with a maximum length limited only by available memory. Like short strings, long strings use 8-bit Ansi characters and have a length indicator. Unlike short strings, long strings have no zeroth element that contains the dynamic string length. To find the length of a long string you must use the Length standard function, and to set the length of a long string you must use the SetLength standard procedure. Long strings are also reference-counted and, like PChars, long strings are null-terminated. For details about the implementation of longs strings, see the Object Pascal Language Guide online Help on “Long strings.” Long strings are denoted by the reserved word string and by the predefined identifier AnsiString. For new applications, it is recommended that you use the long string type. All components in the Visual Component Library are compiled in this state,typically using string. If you write components, they should also use long strings, as should any code that receives data from VCL string-type properties. If you want to write specific code that always uses a long string, then you should use 4-24Developer’ sGuide, WorkingwithstringsAnsiString. If you want to write flexible code that allows you to easily change the type as new string implementations become standard, then you should use string. WideString The WideChar type allows wide character strings to be represented as arrays of WideChars.Wide strings are strings composed of 16-bit Unicode characters. As with long strings, WideStrings are dynamically allocated with a maximum length limited only by available memory. However, wide strings are not reference counted. The dynamically allocated memory that contains the string is deallocated when the wide string goes out of scope. In all other respects wide strings possess the same attributes as long strings. The WideString type is denoted by the predefined identifier WideString. Since the 32-bit version of OLE uses Unicode for all strings, strings must be of wide string type in any OLE automated properties and method parameters. Also, most OLE API functions use null-terminated wide strings. For more information about WideStrings, see the Object Pascal Language Guide online Help on “WideString.” PChar types A PChar is a pointer to a null-terminated string of characters of the type Char. Each of the three character types also has a built-in pointer type: • A PChar is a pointer to a null-terminated string of 8-bit characters. • A PAnsiChar is a pointer to a null-terminated string of 8-bit characters. • A PWideChar is a pointer to a null-terminated string of 16-bit characters. PChars are, with short strings, one of the original Object Pascal string types. They were created primarily asaClanguage and Windows API compatibility type. OpenString An OpenString is essentially obsolete, but you may see it in older code. It is for 16-bit compatibility and is only allowed on parameters. OpenStrings were used, before long strings were implemented, to allow a short string of an unspecified length string to be passed as a parameter. The OpenString type was thereby useful when the short string type was the only alternative. For example, this declaration: procedure a(v : openstring); will allow any length string to be passed as a parameter, where normally the string length of the formal and actual parameters must match exactly. You should not have to use OpenString in any new applications you write. Refer also to the {$P+/-} switch under “Compiler directives for strings” on page 4-31.

Runtime library string handling routines

The runtime library provides many specialized string handling routines specific to a string type. These are routines for wide strings, longs strings, and null-terminated strings (meaning PChars). Routines that deal with PChar types use the null- termination to determine the length of the string. For more details about null- Commonprogrammingtasks4-25, Workingwithstringsterminated strings, see “Working with null-terminated strings” in the Object Pascal Language Guide online Help. The runtime library also includes a category of string formatting routines. There are no categories of routines listed for ShortString types. However, some built-in compiler routines deal with the ShortString type. These include, for example, the Low and High standard functions. Because wide strings and long strings are the commonly used types, the remaining sections discuss these routines. Wide character routines When working with strings you should make sure that the code in your application can handle the strings it will encounter in the various target locales. Sometimes you will need to use wide characters and wide strings. In fact, one approach to working with ideographic character sets is to convert all characters to a wide character encoding scheme such as Unicode. The runtime library includes the following wide character string functions for converting between standard single-byte character strings (or MBCS strings) and Unicode strings: • StringToWideChar • WideCharLenToString • WideCharLenToStrVar • WideCharToString • WideCharToStrVar Using a wide character encoding scheme has the advantage that you can make many of the usual assumptions about strings that do not work for MBCS systems. There is a direct relationship between the number of bytes in the string and the number of characters in the string. You do not need to worry about cutting characters in half or mistaking the second half of a character for the start of a different character. A disadvantage of working with wide characters is that Windows 95 does not support wide character API function calls. Because of this, the VCL components represent all string values as single byte or MBCS strings. Translating between the wide character system and the MBCS system every time you set a string property or read its value would require tremendous amounts of extra code and slow your application down. However, you may want to translate into wide characters for some special string processing algorithms that need to take advantage of the 1:1 mapping between characters and WideChars. Commonly used long string routines The long string handling routines cover several functional areas. Within these areas, some are used for the same purpose, the differences being whether or not they use a particular criteria in their calculations. The following tables list these routines by these functional areas: • Comparison • Case conversion • Modification • Sub-string 4-26Developer’ sGuide, WorkingwithstringsWhere appropriate, the tables also provide columns indicating whether or not a routine supports one or more of the following criteria: • Uses case sensitivity – If the Windows locale is used, it determines the definition of case. If the routine does not use the Windows locale, analysis are based upon the ordinal values of the characters. If the routine is case-insensitive, there is a logical merging of upper and lower case characters that is determined by a predefined pattern. • Uses the Windows locale – Windows locale enablement allows you to add extra features to your application for specific locales. In particular, for Asian language environments. Most Windows locales consider lowercase characters to be less than the corresponding uppercase characters. This is in contrast to ASCII order, in which lowercase characters are greater than uppercase characters. Routines that use the Windows locale are typically prefaced with Ansi (that is, AnsiXXX). Supports the multi-byte character set (MBCS) – MBCSs are used when writing code for far eastern locales. Multi-byte characters are represented as a mix of one and two byte character codes, so the length in bytes does not necessarily correspond to the length of the string. The routines that support MBCS are written parse one- and two- byte characters. The ByteType and StrByteType determine whether a particular byte is the lead byte of a two-byte character. Be careful when using multi-byte characters not to truncate a string by cutting a two-byte character in half. Do not pass characters as a parameter to a function or procedure, since the size of a character cannot be predetermined. Pass, instead, a pointer to a to a character or string. For more information about MBCS, see “Enabling application code” on page 11-2 of Chapter 11, “Creating international applications.” Table 4.2 String comparison routines Routine Case-sensitive Uses Windows locale Supports MBCS AnsiCompareStr yes yes yes AnsiCompareText no yes yes AnsiCompareFileName no yes yes CompareStr yes no no CompareText no no no Table 4.3 Case conversion routines Routine Uses Windows locale Supports MBCS AnsiLowerCase yes yes AnsiLowerCaseFileName yes yes AnsiUpperCaseFileName yes yes AnsiUpperCase yes yes LowerCase no no UpperCase no noCommonprogrammingtasks4-27, WorkingwithstringsTable 4.4 String modification routines Routine Case-sensitive Supports MBCS AdjustLineBreaks NA yes AnsiQuotedStr NA yes StringReplace optional by flag yes Trim NA yes TrimLeft NA yes TrimRight NA yes WrapText NA yes Table 4.5 Sub-string routines Routine Case-sensitive Supports MBCS AnsiExtractQuotedStr NA yes AnsiPos yes yes IsDelimiter yes yes IsPathDelimiter yes yes LastDelimiter yes yes QuotedStr no no The routines used for string filenames: AnsiCompareFileName, AnsiLowerCaseFileName, and AnsiUpperCaseFileName all use the Windows locale. You should always use filenames that are perfectly portable because the locale (character set) used for filenames can and might differ from the default user interface.

Declaring and initializing strings

When you declare a long string: S: string; you do not need to initialize it. Long strings are automatically initialized to empty. To test a string for empty you can either use the EmptyStr variable: S = EmptyStr; or test against an empty string: S = ‘’; An empty string has no valid data. Therefore, trying to index an empty string is like trying to access nil and will result in an access violation: var S: string; begin S[i];// this will cause an access violation // statements end; 4-28Developer’ sGuide, WorkingwithstringsSimilarly, if you cast an empty string to a PChar, the result is a nil pointer. So, if you are passing such a PChar to a routine that needs to read or write to it, be sure that the routine can handle nil: var S: string;// empty string begin proc(PChar(S));// be sure that proc can handle nil // statements end; If it cannot, then you can either initialize the string: S := ‘No longer nil’; proc(PChar(S));// proc does not need to handle nil now or set the length, using the SetLength procedure: SetLength(S, 100);//sets the dynamic length of S to 100 proc(PChar(S));// proc does not need to handle nil now When you use SetLength, existing characters in the string are preserved, but the contents of any newly allocated space is undefined. Following a call to SetLength, S is guaranteed to reference a unique string, that is a string with a reference count of one. To obtain the length of a string, use the Length function. Remember when declaring a string that: S: string[n]; implicitly declares a short string, not a long string of n length. To declare a long string of specifically n length, declare a variable of type string and use the SetLength procedure. S: string; SetLength(S, n);

Mixing and converting string types

Short strings, long strings and wide strings can be mixed in assignments and expressions, and the compiler automatically generates code to perform the necessary string type conversions. However, when assigning a string value to a short string variable, be aware that the string value is truncated if it is longer than the declared maximum length of the short string variable. Long strings are already dynamically allocated. If you use one of the built-in pointer types, such as PAnsiString, PString, or PWideString, remember that you are introducing another level of indirection. Be sure this is what you intend.

String to PChar conversions

Long string to PChar conversions are not automatic. The following list describes some of the differences between strings and PChars that can make conversions problematic. Commonprogrammingtasks4-29, Workingwithstrings• Long strings are reference-counted, while PChars are not. • Assigning to a string copies the data, while a PChar is a pointer to memory. • Long strings are null-terminated and also contain the length of the string, while PChars are simply null-terminated. Situations in which these differences can cause subtle errors are discussed in this section. String dependencies Sometimes you will need convert a long string to a null-terminated string, for example, if you are using a function that takes a PChar. However, because long strings are reference counted, typecasting a string to a PChar increases the dependency on the string by one, without actually incrementing the reference count. When the reference count hits zero, the string will be destroyed, even though there is an extra dependency on it. The cast PChar will also disappear, while the routine you passed it to may still be using it. If you must cast a string to a PChar, be aware that you are responsible for the lifetime of the resulting PChar. For example: procedure my_func(x: string); begin // do something with x some_proc(PChar(x)); // cast the string to a PChar // you now need to guarantee that the string remains // as long as the some_proc procedure needs to use it end; Returning a PChar local variable A common error when working with PChars is to store in a data structure, or return as a value, a local variable. When your routine ends, the PChar will disappear because it is simply a pointer to memory, and is not a reference counted copy of the string. For example: function title(n: Integer): PChar; var s: string; begin s := Format(‘title - %d’, [n]); Result := PChar(s); // DON’T DO THIS end; This example returns a pointer to string data that is freed when the title function returns. Passing a local variable as a PChar Consider that you have a local string variable that you need to initialize by calling a function that takes a PChar. One approach is to create a local array of char and pass it to the function, then assign that variable to the string: 4-30Developer’ sGuide, Workingwithstrings// assume MAXSIZE is a predefined constant var i: Integer; buf: array[0..MAX_SIZE] of char; S: string; begin i := GetModuleFilename(0, @buf, SizeOf(buf));// treats @buf as a PChar S := buf; //statements end; This approach is useful if the size of the buffer is relatively small, since it is allocated on the stack. It is also safe, since the conversion between an array of char and a string is automatic. When GetModuleFilename returns, the Length of the string correctly indicates the number of bytes written to buf. To eliminate the overhead of copying the buffer, you can cast the string to a PChar (if you are certain that the routine does not need the PChar to remain in memory). However, synchronizing the length of the string does not happen automatically, as it does when you assign an array of char to a string. You should reset the string Length so that it reflects the actual width of the string. If you are using a function that returns the number of bytes copied, you can do this safely with one line of code: var S: string; begin SetLength(S, 100);// when casting to a PChar, be sure the string is not empty SetLength(S, GetModuleFilename( 0, PChar(S), Length(S) ) ); // statements end;

Compiler directives for strings

The following is a list of compiler directives dealing with character and string types: • {$H+/-}: A compiler directive, $H, controls whether the reserved word string represents a short string or a long string. In the default state, {$H+}, string represents a long string. You can change it to a ShortString by using the {$H-} directive. • {$P+/-}: The $P directive is meaningful only for code compiled in the {$H-} state, and is provided for backwards compatibility with earlier versions of Delphi and Borland Pascal. $P controls the meaning of variable parameters declared using the string keyword in the {$H-} state. In the {$P-} state, variable parameters declared using the string keyword are normal variable parameters, but in the {$P+} state, they are open string parameters. Regardless of the setting of the $P directive, the OpenString identifier can always be used to declare open string parameters. Make a link to the compiler directives, since this is a direct quote. • {$V+/-}: The $V directive controls type checking on short strings passed as variable parameters. In the {$V+} state, strict type checking is performed, requiring the formal and actual parameters to be of identical string types. In the {$V-} (relaxed) state, any short string type variable is allowed as an actualCommonprogrammingtasks4-31, Workingwithstringsparameter, even if the declared maximum length is not the same as that of the formal parameter. Be aware that this could lead to memory corruption. For example: var S: string[3]; procedure Test(var T: string); begin T := ‘1234’; end; begin Test(S); end. • {$X+/-}: The {$X+} compiler directive enables Delphi’s support for null- terminated strings by activating the special rules that apply to the built-in PChar type and zero-based character arrays. (These rules allow zero-based arrays and character pointers to be used with Write, Writeln, Val, Assign, and Rename from the System unit.)

Related topics

The following is a list of string and character related topics: International character sets For information about extended character sets, see the Object Pascal Language Guide online Help topic “About extended character sets” and “Enabling application code” on page 11-2. Character arrays For information about working with character arrays, see the Object Pascal Language Guide online Help topic “Working with null-terminated strings.” Character strings For information about character strings, see the Object Pascal Language Guide online Help topic “Character strings.” Character pointers For information about character pointers, see the Object Pascal Language Guide online Help topic “Character pointers.” String operators For information about string operators, see the Object Pascal Language Guide online Help topic “String operators.” 4-32Developer’ sGuide, Workingwithfiles

Working with files

This section describes working with files and distinguishes between manipulating files on disk, and input/output operations such as reading and writing to files. The first section discusses the runtime library and Windows API routines you would use for common programming tasks that involve manipulating files on disk. The next section is an overview of file types used with file I/O. The last section focuses on the recommended approach to working with file I/O, which is to use file streams. Note Previous versions of the Object Pascal language performed operations on files themselves, rather than on the filename parameters commonly used now. With these older file types you had to locate a file and assign it to a file variable before you could, for example, rename the file.

Manipulating files

There are several common file operations built into Object Pascal’s runtime library. The procedures and functions for working with files operate at a high level. For most routines, you specify the name of the file and the routine makes the necessary calls to the operating system for you. In some cases, you use file handles instead. Object Pascal provides routines for most file manipulation. When it does not, alternative routines are discussed. Deleting a file Deleting a file erases the file from the disk and removes the entry from the disk’s directory. There is no corresponding operation to restore a deleted file, so applications should generally allow users to confirm deletions of files. To delete a file, pass the name of the file to the DeleteFile function: DeleteFile(FileName); DeleteFile returns True if it deleted the file and False if it did not (for example, if the file did not exist or if it was read-only). DeleteFile erases the file named by FileName from the disk. Finding a file There are three routines used for finding a file: FindFirst, FindNext, and FindClose. FindFirst searches for the first instance of a filename with a given set of attributes in a specified directory. FindNext returns the next entry matching the name and attributes specified in a previous call to FindFirst. FindClose releases memory allocated by FindFirst. In 32-bit Windows you should always use FindClose to terminates a FindFirst/FindNext sequence. If you want to know if a file exists, there is a FileExists function that returns True if the file exists, False otherwise. The three file find routines take a TSearchRec as one of the parameters. TSearchRec defines the file information searched for by FindFirst or FindNext. The declaration for TSearchRec is: Commonprogrammingtasks4-33, Workingwithfilestype TFileName = string; TSearchRec = record Time: Integer;//Time contains the time stamp of the file. Size: Integer;//Size contains the size of the file in bytes. Attr: Integer;//Attr represents the file attributes of the file. Name: TFileName;//Name contains the DOS filename and extension. ExcludeAttr: Integer; FindHandle: THandle; FindData: TWin32FindData;//FindData contains additional information such as //file creation time, last access time, long and short filenames. end; If a file is found, the fields of the TSearchRec type parameter are modified to specify the found file. You can test Attr against the following attribute constants or values to determine if a file has a specific attribute: Table 4.6 Attribute constants and values Constant Value Description faReadOnly $00000001 Read-only files faHidden $00000002 Hidden files faSysFile $00000004 System files faVolumeID $00000008 Volume ID files faDirectory $00000010 Directory files faArchive $00000020 Archive files faAnyFile $0000003F Any file To test for an attribute, combine the value of the Attr field with the attribute constant with the and operator. If the file has that attribute, the result will be greater than 0. For example, if the found file is a hidden file, the following expression will evaluate to True: (SearchRec.Attr and faHidden > 0). Attributes can be combined by adding their constants or values. For example, to search for read-only and hidden files in addition to normal files, pass (faReadOnly + faHidden) the Attr parameter. Example This example uses a label, a button named Search, and a button named Again on a form. When the user clicks the Search button, the first file in the specified path is found, and the name and the number of bytes in the file appear in the label’s caption. Each time the user clicks the Again button, the next matching filename and size is displayed in the label: var SearchRec: TSearchRec; procedure TForm1.SearchClick(Sender: TObject); begin FindFirst('c:\Program Files\delphi3\bin\*.*', faAnyFile, SearchRec); Label1.Caption := SearchRec.Name + ' is ' + IntToStr(SearchRec.Size) + ' bytes in size'; end; 4-34Developer’ sGuide, Workingwithfilesprocedure TForm1.AgainClick(Sender: TObject); begin if (FindNext(SearchRec) = 0) Label1.Caption := SearchRec.Name + ' is ' + IntToStr(SearchRec.Size) + ' bytes in size'; else FindClose(SearchRec); end; Changing file attributes Every file has various attributes stored by the operating system as bitmapped flags. File attributes include such items as whether a file is read-only or a hidden file. Changing a file’s attributes requires three steps: reading, changing, and setting. Reading file attributes: Operating systems store file attributes in various ways, generally as bitmapped flags. To read a file’s attributes, pass the filename to the FileGetAttr function, which returns the file attributes of a file. The return value is a group of bitmapped file attributes, of type Word. The attributes can be examined by AND-ing the attributes with the constants defined in TSearchRec. A return value of –1 indicates that an error occurred. Changing individual file attributes: Because Delphi represents file attributes in a set, you can use normal logical operators to manipulate the individual attributes. Each attribute has a mnemonic name defined in the SysUtils unit. For example, to set a file’s read-only attribute, you would do the following: Attributes := Attributes or faReadOnly; You can also set or clear several attributes at once. For example, the clear both the system-file and hidden attributes: Attributes := Attributes and not (faSysFile or faHidden); Setting file attributes: Delphi enables you to set the attributes for any file at any time. To set a file’s attributes, pass the name of the file and the attributes you want to the FileSetAttr function. FileSetAttr sets the file attributes of a specified file. You can use the reading and setting operations independently, if you only want to determine a file’s attributes, or if you want to set an attribute regardless of previous settings. To change attributes based on their previous settings, however, you need to read the existing attributes, modify them, and write the modified attributes. Renaming a file To change a filename, simply use the RenameFile function: function RenameFile(const OldFileName, NewFileName: string): Boolean; which changes a filename, identified by OldFileName, to the name specified by NewFileName. If the operation succeeds, RenameFile returns True. If it cannot rename the file, for example, if a file called NewFileName already exists, it returns False. For example: if not RenameFile('OLDNAME.TXT','NEWNAME.TXT') then ErrorMsg('Error renaming file!'); Commonprogrammingtasks4-35, WorkingwithfilesYou cannot rename (move) a file across drives using RenameFile. You would need to first copy the file and then delete the old one. Note RenameFile is a wrapper around the Windows API MoveFile function, so MoveFile will not work across drives either. File date-time routines The FileAge, FileGetDate, and FileSetDate routines operate on operating system date- time values. FileAge returns the date-and-time stamp of a file, or –1 if the file does not exist. FileSetDate sets the date-and-time stamp for a specified file, and returns zero on success or a Windows error code on failure. FileGetDate returns a date-and-time stamp for the specified file or –1 if the handle is invalid. As with most of the file manipulating routines, FileAge uses a string filename. FileGetDate and FileSetDate, however, take a Windows Handle type as a parameter. To get access to a Windows file Handle either: • Call the Windows API CreateFile function. CreateFile is a 32-bit only function that creates or opens a file and returns a Handle that can be used to access the file. • Instantiate TFileStream to create or open a file. Then use the Handle property as you would a Windows’ file Handle. See “Using file streams” on page 4-37 for more information. Copying a file The runtime library does not provide any routines for copying a file. However, you can directly call the Windows API CopyFile function to copy a file. Like most of the Delphi runtime library file routines, CopyFile takes a filename as a parameter, not a Window Handle. When copying a file, be aware that the file attributes for the existing file are copied to the new file, but the security attributes are not. CopyFile is also useful when moving files across drives because neither the Delphi RenameFile function nor the Windows API MoveFile function can rename/move files across drives. For more information, see the Microsoft Windows online Help.

File types with file I/O

There are three file types you can use when working with file I/O: Old style Pascal files, Windows file handles, and file stream objects. This section overviews these types. Old style Pascal files: These are the types used with the old file variables, usually of the format “F: Text: or “F: File”. There are three classes of these files: typed, text, and untyped and a number of Delphi file-handling routines, such as AssignPrn and writeln, use them. These file types are essentially obsolete and are incompatible with Windows file handles. If you need to work with the old style file types, see the Object Pascal Language Guide online Help, which completely covers using them with file I/O. Windows file handles: The Object Pascal file handles are wrappers for the Windows file handle type. The runtime library file-handling routines that use Windows file Handles are typically wrappers around Windows API functions. For example, the 4-36Developer’ sGuide, WorkingwithfilesFileRead calls the Windows ReadFile function. Because the Delphi functions use Object Pascal syntax, and occasionally provide default parameter values, they are a convenient interface to the Windows API. Using these routines is straightforward, and if you are familiar and comfortable with the Windows API file routines, you may want to use them when working with file I/O. File streams: File streams are object instances of the VCL TFileStream class used to access the information in disk files. File streams are a portable and high level approach to file I/O. TFileStream has a Handle property that gives you access to the Windows file handle. The next section discusses TFileStream.

Using file streams

TFileStream is a VCL class used for high level object representations of file streams. TFileStream offers multiple functionality: persistence, interaction with other streams, and file I/O. • TFileStream is a descendant of the stream classes. As such, one advantage of using file streams is that you automatically inherit support for persistence. The stream classes are enabled to work with the TFiler classes, TReader and TWriter, to stream objects out to disk. Therefore, when you have a file stream, you can use that same code for the VCL streaming mechanism. For more information about using the VCL streaming system, see the VCL Reference online Help on the TStream, TFiler, TReader, TWriter, and TComponent classes. • TFileStream can interact easily with other stream classes. For example, if you want to dump a dynamic memory block to disk, you can do so using a TFileStream and a TMemoryStream. • TFileStream provides the basic methods and properties for file I/O. The remaining sections focus on this aspect of file streams. Creating and opening files To create or open a file and get access to a handle for the file, you simply instantiate a TFileStream. This opens or creates a named file and provides methods to read from or write to it. If the file can not be opened, TFileStream raises an exception. constructor Create(const filename: string; Mode: Word); The Mode parameter specifies how the file should be opened when creating the file stream. The Mode parameter consists of an open mode and a share mode or’ed together. The open mode must be one of the following values: Value Meaning fmCreate TFileStream a file with the given name. If a file with the given name exists, open the file in write mode. fmOpenRead Open the file for reading only. fmOpenWrite Open the file for writing only. Writing to the file completely replaces the current contents. fmOpenReadWrite Open the file to modify the current contents rather than replace them. Commonprogrammingtasks4-37, WorkingwithfilesThe share mode must be one of the following values: Value Meaning fmShareCompat Sharing is compatible with the way FCBs are opened. fmShareExclusive Other applications can not open the file for any reason. fmShareDenyWrite Other applications can open the file for reading but not for writing. fmShareDenyRead Other applications can open the file for writing but not for reading. fmShareDenyNone No attempt is made to prevent other applications from reading from or writing to the file. The file open and share mode constants are in the SysUtils unit.

Using the file Handle

When you instantiate TFileStream you get access to the file handle. The file handle is contained in the Handle property. Handle is read-only and indicates the mode in which the file was opened. If you want to change the attributes of the file Handle, you must create a new file stream object. Some file manipulation routines take a Window’s file handle as a parameter. Once you have a file stream, you can use the Handle property in any situation in which you would use a Window’s file handle. Be aware that, unlike handle streams, file streams close file handles when the object is destroyed.

Reading and writing to files

TFileStream has several different methods for reading from and writing to files. These are distinguished by whether they: • return the number of bytes read or written. • require the number of bytes is known. • raise an exception on error. Read is a function that reads up to Count bytes from the file associated with the file stream, starting at the current Position, into Buffer. Read then advances the current position in the file by the number of bytes actually transferred. The prototype for Read is: function Read(var Buffer; Count: Longint): Longint; override; Read is useful when the number of bytes in the file is not known. Read returns the number of bytes actually transferred, which may be less than Count if the end of file marker is encountered. Write is a function that writes Count bytes from the Buffer to the file associated with the file stream, starting at the current Position. The prototype for Write is: function Write(const Buffer; Count: Longint): Longint; override; After writing to the file, Write advances the current position by the number bytes written, and returns the number of bytes actually written, which may be less than Count if the end of the buffer is encountered. 4-38Developer’ sGuide, WorkingwithfilesThe counterpart procedures are ReadBuffer and WriteBuffer which, unlike Read and Write, do not return the number of bytes read or written. These procedures are useful in cases where the number of bytes is known and required, for example when reading in structures. ReadBuffer and WriteBuffer raise an exception on error (EReadError and EWriteError) while the Read and Write methods do not. The prototypes for ReadBuffer and WriteBuffer are: procedure ReadBuffer(var Buffer; Count: Longint); procedure WriteBuffer(const Buffer; Count: Longint); These methods call the Read and Write methods, to perform the actual reading and writing.

Reading and writing strings

If you are passing a string to a read or write function, you need to be aware of the correct syntax to use. The Buffer parameters for the read and write routines are var and const types, respectively. These are untyped parameters, so the routine takes the address of variables passed in as these types. The most commonly used type when working with strings is long strings. However, for long strings, the result will not give you the right value. Long strings contain a size, a reference count, and a pointer to the characters in the string. Consequently, dereferencing a long string will not result in only the pointer element. What you need to do is first cast the string to a Pointer or PChar, and then dereference it. For example: procedure cast-string; var fs: TFileStream; s: string = 'Hello'; begin fs := TFileStream.Create('foo', fmCreate or fmOpenRead); fs.Write(s, Length(s));// this will give you garbage fs.Write(PChar(s)^, Length(s));// this is the correct way end;

Seeking a file

Most typical file I/O mechanisms have a process of seeking a file in order to read from or write to a particular location within it. For this purpose, TFileStream provides a Seek method. The prototype for Seek is: function Seek(Offset: Longint; Origin: Word): Longint; override; The Origin parameter indicates how to interpret the Offset parameter. Origin should be one of the following values: Value Meaning soFromBeginning Offset is from the beginning of the resource. Seek moves to the position Offset. Offset must be >= 0. soFromCurrent Offset is from the current position in the resource. Seek moves to Position + Offset. soFromEnd Offset is from the end of the resource. Offset must be <= 0 to indicate a number of bytes before the end of the file. Commonprogrammingtasks4-39, WorkingwithfilesSeek resets the current Position of the stream, moving it by the indicated offset. Seek returns the new value of the Position property, the new current position in the resource. File position and size TFileStream has properties that hold the current position and size of the file. These are used by the Seek, read, and write methods. The Position property of TFileStream is used to indicate the current offset, in bytes, into the stream (from the beginning of the streamed data). The declaration for Position is: property Position: Longint; The Size property indicates the size in bytes of the stream. It is used as an end of file marker to truncate the file. The declaration for Size is: property Size: Longint; Size is used internally by routines that read and write to and from the stream. Setting the Size property changes the size of the file. If the Size of the file can not be changed, an exception is raised. For example, trying to changes the Size for a file that was opened in fmOpenRead mode will raise an exception. Copying CopyFrom copies a specified number of bytes from one (file) stream to another. function CopyFrom(Source: TStream; Count: Longint): Longint; Using CopyFrom eliminates the need for the user to create, read into, write from, and free a buffer when copying data. CopyFrom copies Count bytes from Source into the stream. CopyFrom then moves the current position by Count bytes, and returns the number of bytes copied. If Count is 0, CopyFrom sets Source position to 0 before reading and then copies the entire contents of Source into the stream. If Count is greater than or less than 0, CopyFrom reads from the current position in Source. 4-40Developer’ sGuide,

Chapter

Chapter 5Developing the application user interface One of Delphi’s most powerful features is how easy it is to create the user interface (UI) using the VCL. By clicking and dropping components from the Component palette onto forms, the architecture of your application can be quickly designed.

Understanding TApplication, TScreen, and TForm

TApplication, TScreen, and TForm are VCL classes that form the backbone of all Delphi applications by controlling the behavior of your project. The TApplication class forms the foundation of a Windows application by providing properties and methods that encapsulate the behavior of a standard Windows program. TScreen is used at runtime to keep track of forms and data modules that have been loaded as well as system specific information such as screen resolution and what fonts are available for display. Instances of the TForm class are the building blocks of your application’s user interface. The windows and dialog boxes of your application are based on TForm.

Using the main form

TForm is the key class for creating Windows GUI applications. The first form you create and save in a project becomes, by default, the project’s main form, which is the first form created at runtime. As you add forms to your projects, you might decide to designate a different form as your application’s main form. Also, specifying a form as the main form is an easy way to test it at runtime, because unless you change the form creation order, the main form is the first form displayed in the running application. Developingtheapplicationuserinterface5-1, UnderstandingTApplication, TScreen, andTFormTo change the project main form, 1 Choose Project|Options and select the Forms page. 2 In the Main Form combo box, select the form you want as the project main form and choose OK. Now if you run the application, your new main form choice is displayed.

Adding additional forms

To add an additional form to your project, select File|New Form. You can see all your project’s forms and their associated units listed in the Project Manager (View|Project Manager). Linking forms Adding a form to a project adds a reference to it in the project file, but not to any other units in the project. Before you can write code that references the new form, you need to add a reference to it in the referencing forms’ unit files. This is called form linking. A common reason to link forms is to provide access to the components in that form. For example, you’ll often use form linking to enable a form that contains data-aware components to connect to the data-access components in a data module. To link a form to another form, 1 Select the form that needs to refer to another. 2 Choose File|Use Unit. 3 Select the name of the form unit for the form to be referenced. 4 Choose OK. Linking a form to another just means that the uses clauses of one form unit contains a reference to the other’s form unit, meaning that the linked form and its components are now in scope for the linking form. Avoiding circular unit references When two forms must reference each other, it’s possible to cause a “Circular reference” error when you compile your program. To avoid such an error, do one of the following: • Place both uses clauses, with the unit identifiers, in the implementation parts of the respective unit files. (This is what the File|Use Unit command does.) • Place one uses clause in an interface part and the other in an implementation part. (You rarely need to place another form’s unit identifier in this unit’s interface part.) Do not place both uses clauses in the interface parts of their respective unit files. This will generate the “Circular reference” error at compile time. 5-2Developer’ sGuide, UnderstandingTApplication, TScreen, andTForm

Working at the application level

The global variable Application, of type TApplication, is in every Delphi Windows application. Application encapsulates your application as well as providing many functions that occur in the background of the program. For instance, Application would handle how you would call a help file from the menu of your program. Understanding how TApplication works is more important to a component writer than to developers of stand-alone applications, but you should set the options that Application handles in the Project|Options Application page when you create a project.

Handling the screen

An global variable of type TScreen called Screen is created when you create a project. Screen encapsulates the state of the screen on which your application is running. Common tasks performed by Screen include specifying the look of the cursor, the size of the window in which your application is running, the list of fonts available to the screen device, and multiple screen behavior. If your application runs on multiple monitors, Screen maintains a list of monitors and their dimensions so that you can effectively manage the layout of your user interface.

Managing layout

At its simplest, you control the layout of your user interface by how you place controls in your forms. The placement choices you make are reflected in the control’s Top, Left, Width, and Height properties. You can change these values at runtime to change the position and size of the controls in your forms. Controls have a number of other properties, however, that allow them to automatically adjust to their contents or containers. This allows you to lay out your forms so that the pieces fit together into a unified whole. Two properties affect how a control is positioned and sized in relation to its parent. The Align property lets you force a control to fit perfectly within its parent along a specific edge or filling up the entire client area after any other controls have been aligned. When the parent is resized, the controls aligned to it are automatically resized and remain positioned so that they fit against a particular edge. If you want to keep a control positioned on a particular edge of its parent, but don’t want it to be resized so that it always runs along the entire edge, you can use the Anchors property. If you want to ensure that a control does not grow too big or too small, you can use the Constraints property. Constraints lets you specify the control’s maximum height, minimum height, maximum width, and minimum width. Set these to limit the size (in pixels) of the control’s height and width. For example, by setting the MinWidth and MinHeight of the constraints on a container object, you can ensure that child objects are always visible. Developingtheapplicationuserinterface5-3, WorkingwithmessagesThe value of Constraints propagates through the parent/child hierarchy so that an object’s size can be constrained because it contains aligned children that have size constraints. Constraints can also prevent a control from being scaled in a particular dimension when its ChangeScale method is called. TControl introduces a protected event, OnConstrainedResize, of type TConstrainedResizeEvent: TConstrainedResizeEvent = procedure(Sender: TObject; var MinWidth, MinHeight, MaxWidth, MaxHeight: Integer) of object; This event allows you to override the size constraints when an attempt is made to resize the control. The values of the constraints are passed as var parameters which can be changed inside the event handler. OnConstrainedResize is published for container objects (TForm, TScrollBox, TControlBar, and TPanel). In addition, component writers can use or publish this event for any descendant of TControl. Controls that have contents that can change in size have an AutoSize property that causes the control to adjust its size to its font or contained objects.

Working with messages

A message is a notification that some event has occurred that is sent by Windows to an application. The message itself is a record passed to a control by Windows. For instance, when you click a mouse button on a dialog box, Windows sends a message to the active control and the application containing that control reacts to this new event. If the click occurs over a button, the OnClick event could be activated upon receipt of the message. If the click occurs just in the form, the application can ignore the message. The record type passed to the application by Windows is called a TMsg. Windows predefines a constant for each message, and these values are stored in the message field of the TMsg record. Each of these constants begin with the letters wm. The VCL automatically handles messages unless you override the message handling system and create your own message handlers. For more information on messages and message handling, see “Understanding the message-handling system” on page 36-1, “Changing message handling” on page 36-3, and “Creating new message handlers” on page 36-5.

More details on forms

When you create a form in Delphi from the IDE, Delphi automatically creates the form in memory by including code in the WinMain() function. Usually, this is the desired behavior and you don’t have to do anything to change it. That is, the main window persists through the duration of your program, so you would likely not change the default Delphi behavior when creating the form for your main window. 5-4Developer’ sGuide, MoredetailsonformsHowever, you may not want all your application’s forms in memory for the duration of the program execution. That is, if you do not want all your application’s dialogs in memory at once, you can create the dialogs dynamically when you want them to appear. Forms can be modal or modeless. Modal forms are forms with which the user must interact before switching to another form (for example, a dialog box requiring user input). Modeless forms, though, are windows that are displayed until they are either obscured by another window or until they are closed or minimized by the user.

Controlling when forms reside in memory

By default, Delphi automatically creates the application’s main form in memory by including the following code in the application’s project source unit: Application.CreateForm(TForm1, Form1); This function creates a global variable with the same name as the form. So, every form in an application has an associated global variable. This variable is a pointer to an instance of the form’s class and is used to reference the form while the application is running. Any unit that includes the form’s unit in its uses clause can access the form via this variable. All forms created in this way in the project unit appear when the program is invoked and exist in memory for the duration of the application. Displaying an auto-created form If you choose to create a form at startup, and do not want it displayed until sometime later during program execution, the form’s event handler uses the ShowModal method to display the form that is already loaded in memory: procedure TMainForm.Button1Click(Sender: TObject); begin ResultsForm.ShowModal; end; In this case, since the form is already in memory, there is no need to create another instance or destroy that instance. Creating forms dynamically You may not always want all your application’s forms in memory at once. To reduce the amount of memory required at load time, you may want to create some forms only when you need to use them. For example, a dialog box needs to be in memory only during the time a user interacts with it. Developingtheapplicationuserinterface5-5, MoredetailsonformsTo create a form at a different stage during execution using the IDE, you: 1 Select the File|New Form from the Component bar to display the new form. 2 Remove the form from the Auto-create forms list of the Project Options|Forms page. This removes the form’s invocation at startup. As an alternative, you can manually remove the following line from the project source: Application.CreateForm(TResultsForm, ResultsForm); 3 Invoke the form when desired by using the form’s Show method, if the form is modeless, or ShowModal method, if the form is modal. An event handler for the main form must create an instance of the result form and destroy it. One way to invoke the result form is to use the global variable as follows. Note that ResultsForm is a modal form so the handler uses the ShowModal method. procedure TMainForm.Button1Click(Sender: TObject); begin ResultsForm:=TResultForm.Create(self) ResultsForm.ShowModal; ResultsForm.Free; end; The event handler in the example deletes the form after it is closed, so the form would need to be recreated if you needed to use ResultsForm elsewhere in the application. If the form were displayed using Show you could not delete the form within the event handler because Show returns while the form is still open. Note If you create a form using its constructor, be sure to check that the form is not in the Auto-create forms list on the Project Options|Forms page. Specifically, if you create the new form without deleting the form of the same name from the list, Delphi creates the form at startup and this event-handler creates a new instance of the form, overwriting the reference to the auto-created instance. The auto-created instance still exists, but the application can no longer access it. After the event-handler terminates, the global variable no longer points to a valid form. Any attempt to use the global variable will likely crash the application. Creating modeless forms such as windows You must guarantee that reference variables for modeless forms exist for as long as the form is in use. This means that these variables should have global scope. In most cases, you use the global reference variable that was created when you made the form (the variable name that matches the name property of the form). If your application requires additional instances of the form, declare separate global variables for each instance. Using a local variable to create a form instance A safer way to create a unique instance of a modal form is to use a local variable in the event handler as a reference to a new instance. If a local variable is used, it does not matter whether ResultsForm is auto-created or not. The code in the event handler makes no reference to the global form variable. For example: 5-6Developer’ sGuide, Moredetailsonformsprocedure TMainForm.Button1Click(Sender: TObject); var RF:TResultForm; begin RF:=TResultForm.Create(self) RF.ShowModal; RF.Free; end; Notice how the global instance of the form is never used in this version of the event handler. Typically, applications use the global instances of forms. However, if you need a new instance of a modal form, and you use that form in a limited, discrete section of the application, such as a single function, a local instance is usually the safest and most efficient way of working with the form. Of course, you cannot use local variables in event handlers for modeless forms because they must have global scope to ensure that the forms exist for as long as the form is in use. Show returns as soon as the form opens, so if you used a local variable, the local variable would go out of scope immediately.

Passing additional arguments to forms

Typically, you create forms for your application from within the IDE. When created this way, the forms have a constructor that takes one argument, Owner, which is the owner of the form being created. (The owner is the calling application object or form object.) Owner can be nil. To pass additional arguments to a form, create a separate constructor and instantiate the form using this new constructor. The example form class below shows an additional constructor, with the extra argument whichButton. This new constructor is added to the form class manually. TResultsForm = class(TForm) ResultsLabel: TLabel; OKButton: TButton; procedure OKButtonClick(Sender: TObject); private public constructor CreateWithButton(whichButton: Integer; Owner: TComponent); end; Here’s the manually coded constructor that passes the additional argument, whichButton. This constructor uses the whichButton parameter to set the Caption property of a Label control on the form. constructor CreateWithButton(whichButton: Integer; Owner: TComponent); begin case whichButton of 1: ResultsLabel.Caption := 'You picked the first button.'; 2: ResultsLabel.Caption := 'You picked the second button.'; 3: ResultsLabel.Caption := 'You picked the third button.'; end; end; Developingtheapplicationuserinterface5-7, MoredetailsonformsWhen creating an instance of a form with multiple constructors, you can select the constructor that best suits your purpose. For example, the following OnClick handler for a button on a form calls creates an instance of TResultsForm that uses the extra parameter: procedure TMainForm.SecondButtonClick(Sender: TObject); var rf: TResultsForm; begin rf := TResultsForm.CreateWithButton(2, self); rf.ShowModal; rf.Free; end;

Retrieving data from forms

Most real-world applications consist of several forms. Often, information needs to be passed between these forms. Information can be passed to a form by means of parameters to the receiving form’s constructor, or by assigning values to the form’s properties. The way you get information from a form depends on whether the form is modal or modeless. Retrieving data from modeless forms You can easily extract information from modeless forms by calling public member functions of the form or by querying properties of the form. For example, assume an application contains a modeless form called ColorForm that contains a listbox called ColorListBox with a list of colors (“Red”, “Green”, “Blue”, and so on). The selected color name string in ColorListBox is automatically stored in a property called CurrentColor each time a user selects a new color. The class declaration for the form is as follows: TColorForm = class(TForm) ColorListBox:TListBox; procedure ColorListBoxClick(Sender: TObject); private FColor:String; public property CurColor:String read FColor write FColor; end; The OnClick event handler for the listbox, ColorListBoxClick, sets the value of the CurrentColor property each time a new item in the listbox is selected. The event handler gets the string from the listbox containing the color name and assigns it to CurrentColor. The CurrentColor property uses the setter function, SetColor, to store the actual value for the property in the private data member FColor: 5-8Developer’ sGuide, Moredetailsonformsprocedure TColorForm.ColorListBoxClick(Sender: TObject); var Index: Integer; begin Index := ColorListBox.ItemIndex; if Index >= 0 then CurrentColor := ColorListBox.Items[Index] else CurrentColor := ''; end; Now suppose that another form within the application, called ResultsForm, needs to find out which color is currently selected on ColorForm whenever a button (called UpdateButton) on ResultsForm is clicked. The OnClick event handler for UpdateButton might look like this: procedure TResultForm.UpdateButtonClick(Sender: TObject); var MainColor: String; begin if Assigned(ColorForm) then begin MainColor := ColorForm.CurrentColor; {do something with the string MainColor} end; end; The event handler first verifies that ColorForm exists using the Assigned function. It then gets the value of ColorForm’s CurrentColor property. Alternatively, if ColorForm had a public function named GetColor, another form could get the current color without using the CurrentColor property (for example, MainColor := ColorForm.GetColor;). In fact, there’s nothing to prevent another form from getting the ColorForm’s currently selected color by checking the listbox selection directly: with ColorForm.ColorListBox do MainColor := Items[ItemIndex]; However, using a property makes the interface to ColorForm very straightforward and simple. All a form needs to know about ColorForm is to check the value of CurrentColor.

Retrieving data from modal forms

Just like modeless forms, modal forms often contain information needed by other forms. The most common example is form A launches modal form B. When form B is closed, form A needs to know what the user did with form B to decide how to proceed with the processing of form A. If form B is still in memory, it can be queried through properties or member functions just as in the modeless forms example above. But how do you handle situations where form B is deleted from memory upon closing? Since a form does not have an explicit return value, you must preserve important information from the form before it is destroyed. Developingtheapplicationuserinterface5-9, MoredetailsonformsTo illustrate, consider a modified version of the ColorForm form that is designed to be a modal form. The class declaration is as follows: TColorForm = class(TForm) ColorListBox:TListBox; SelectButton: TButton; CancelButton: TButton; procedure CancelButtonClick(Sender: TObject); procedure SelectButtonClick(Sender: TObject); private FColor: Pointer; public constructor CreateWithColor(Value: Pointer; Owner: TComponent); end; The form has a listbox called ColorListBox with a list of names of colors. When pressed, the button called SelectButton makes note of the currently selected color name in ColorListBox then closes the form. CancelButton is a button that simply closes the form. Note that a user-defined constructor was added to the class that takes a Pointer argument. Presumably, this Pointer points to a string that the form launching ColorForm knows about. The implementation of this constructor is as follows: constructor TColorForm(Value: Pointer; Owner: TComponent); begin FColor := Value; String(FColor^) := ''; end; The constructor saves the pointer to a private data member FColor and initializes the string to an empty string. Note To use the above user-defined constructor, the form must be explicitly created. It cannot be auto-created when the application is started. For details, see “Controlling when forms reside in memory” on page 5-5. In the application, the user selects a color from the listbox and presses SelectButton to save the choice and close the form. The OnClick event handler for SelectButton might look like this: procedure TColorForm.SelectButtonClick(Sender: TObject); begin with ColorListBox do if ItemIndex >= 0 then String(FColor^) := ColorListBox.Items[ItemIndex]; end; Close; end; Notice that the event handler stores the selected color name in the string referenced by the pointer that was passed to the constructor. 5-10Developer’ sGuide, CreatingandmanagingmenusTo use ColorForm effectively, the calling form must pass the constructor a pointer to an existing string. For example, assume ColorForm was instantiated by a form called ResultsForm in response to a button called UpdateButton on ResultsForm being clicked. The event handler would look as follows: procedure TResultsForm.UpdateButtonClick(Sender: TObject); var MainColor: String; begin GetColor(Addr(MainColor)); if MainColor <> '' then {do something with the MainColor string} else {do something else because no color was picked} end; procedure GetColor(PColor: Pointer); begin ColorForm := TColorForm.CreateWithColor(PColor, Self); ColorForm.ShowModal; ColorForm.Free; end; UpdateButtonClick creates a String called MainColor. The address of MainColor is passed to the GetColor function which creates ColorForm, passing the pointer to MainColor as an argument to the constructor. As soon as ColorForm is closed it is deleted, but the color name that was selected is still preserved in MainColor, assuming that a color was selected. Otherwise, MainColor contains an empty string which is a clear indication that the user exited ColorForm without selecting a color. This example uses one string variable to hold information from the modal form. Of course, more complex objects can be used depending on the need. Keep in mind that you should always provide a way to let the calling form know if the modal form was closed without making any changes or selections (such as having MainColor default to an empty string).

Creating and managing menus

Menus provide an easy way for your users to execute logically grouped commands. The Delphi Menu Designer enables you to easily add a menu—either predesigned or custom tailored—to your form. You simply add a menu component to the form, open the Menu Designer, and type menu items directly into the Menu Designer window. You can add or delete menu items, or drag and drop them to rearrange them during design time. You don’t even need to run your program to see the results—your design is immediately visible in the form, appearing just as it will during runtime. Your code can also change menus at runtime, to provide more information or options to the user. Developingtheapplicationuserinterface5-11, CreatingandmanagingmenusThis chapter explains how to use the Delphi Menu Designer to design menu bars and pop-up (local) menus. It discusses the following ways to work with menus at design time and runtime: • Opening the Menu Designer • Building menus • Editing menu items in the Object Inspector • Using the Menu Designer context menu • Using menu templates • Saving a menu as a template • Adding images to menu items Figure 5.1 Delphi menu terminology Menu items on the menu bar Accelerator key Menu items in a menu list Keyboard shortcut Separator bar

Opening the Menu Designer

To start using the Delphi Menu Designer, first add either a MainMenu or PopupMenu component to your form. Both menu components are located on the Standard page of the Component palette. Figure 5.2 MainMenu and PopupMenu components MainMenu component PopupMenu component A MainMenu component creates a menu that’s attached to the form’s title bar. A PopupMenu component creates a menu that appears when the user right-clicks in the form. Pop-up menus do not have a menu bar. To open the Menu Designer, select a menu component on the form, and then choose from one of the following methods: • Double-click the menu component. • From the Properties page of the Object Inspector, select the Items property, and then either double-click [Menu] in the Value column, or click the ellipsis (...) button. The Menu Designer appears, with the first (blank) menu item highlighted in the Designer, and the Caption property highlighted in the Object Inspector. 5-12Developer’ sGuide, CreatingandmanagingmenusFigure 5.3 Menu Designer for a main menu Title bar (shows Name property for Menu component) Menu bar Placeholder for menu item Menu Designer displays WYSIWYG menu items as you build the menu. A TMenuItem object is created and the Name property set to the menu item Caption you specify (minus any illegal characters and plus a numeric suffix). Figure 5.4 Menu Designer for a pop-up menu Placeholder for first menu item

Building menus You add a menu component to your form, or forms, for every menu you want to

include in your application. You can build each menu structure entirely from scratch, or you can start from one of the Delphi predesigned menu templates. Developingtheapplicationuserinterface5-13, CreatingandmanagingmenusThis section discusses the basics of creating a menu at design time. For more information about Delphi menu templates, see “Using menu templates” on page 5-20.

Naming menus

As with all components, when you add a menu component to the form, Delphi gives it a default name; for example, MainMenu1. You can give the menu a more meaningful name that follows Object Pascal naming conventions. Delphi adds the menu name to the form’s type declaration, and the menu name then appears in the Component list.

Naming the menu items

In contrast to the menu component itself, you need to explicitly name menu items as you add them to the form. You can do this in one of two ways: • Directly type in the value for the Name property. • Type in the value for the Caption property first, and let Delphi derive the Name property from the caption. For example, if you give a menu item a Caption property value of File, Delphi assigns the menu item a Name property of File1. If you fill in the Name property before filling in the Caption property, Delphi leaves the Caption property blank until you type in a value. Note If you enter characters in the Caption property that are not valid for Object Pascal identifiers, Delphi modifies the Name property accordingly. For example, if you want the caption to start with a number, Delphi precedes the number with a character to derive the Name property. The following table demonstrates some examples of this, assuming all menu items shown appear in the same menu bar. Table 5.1 Sample captions and their derived names Component caption Derived name Explanation &File File1 Removes ampersand &File (2nd occurrence) File2 Numerically orders duplicate items 1234 N12341 Adds a preceding letter and numerical order 1234 (2nd occurrence) N12342 Adds a number to disambiguate the derived name $@@@# N1 Removes all non-standard characters, adding preceding letter and numerical order - (hyphen) N2 Numerical ordering of second occurrence of caption with no standard characters As with the menu component, Delphi adds any menu item names to the form’s type declaration, and those names then appear in the Component list. 5-14Developer’ sGuide, CreatingandmanagingmenusAdding, inserting, and deleting menu items The following procedures describe how to perform the basic tasks involved in building your menu structure. Each procedure assumes you have the Menu Designer window open. To add menu items at design time, 1 Select the position where you want to create the menu item. If you’ve just opened the Menu Designer, the first position on the menu bar is already selected. 2 Begin typing to enter the caption. Or enter the Name property first by specifically placing your cursor in the Object Inspector and entering a value. In this case, you then need to reselect the Caption property and enter a value. 3 Press Enter. The next placeholder for a menu item is selected. If you entered the Caption property first, use the arrow keys to return to the menu item you just entered. You’ll see that Delphi has filled in the Name property based on the value you entered for the caption. (See “Naming the menu items” on page 5-14.) 4 Continue entering values for the Name and Caption properties for each new item you want to create, or press Esc to return to the menu bar. Use the arrow keys to move from the menu bar into the menu, and to then move between items in the list; press Enter to complete an action. To return to the menu bar, press Esc. To insert a new, blank menu item, 1 Place the cursor on a menu item. 2 Press Ins. Menu items are inserted to the left of the selected item on the menu bar, and above the selected item in the menu list. To delete a menu item or command, 1 Place the cursor on the menu item you want to delete. 2 Press Del. Note You cannot delete the default placeholder that appears below the item last entered in a menu list, or next to the last item on the menu bar. This placeholder does not appear in your menu at runtime. Adding separator bars Separator bars insert a line between menu items. You can use separator bars to indicate groupings within the menu list, or simply to provide a visual break in a list. To make the menu item a separator bar, • Type a hyphen (-) for the caption. Developingtheapplicationuserinterface5-15, CreatingandmanagingmenusSpecifying accelerator keys and keyboard shortcuts Accelerator keys enable the user to access a menu command from the keyboard by pressing Alt+ the appropriate letter, indicated in your code by the preceding ampersand. The letter after the ampersand appears underlined in the menu. Keyboard shortcuts enable the user to perform the action without accessing the menu directly, by typing in the shortcut key combination. To specify an accelerator, • Add an ampersand in front of the appropriate letter. For example, to add a Save menu command with the S as an accelerator key, type &Save. To specify a keyboard shortcut, • Use the Object Inspector to enter a value for the ShortCut property, or select a key combination from the drop-down list. This list is only a subset of the valid combinations you can type in. When you add a shortcut, it appears next to the menu item caption. Caution Delphi does not check for duplicate shortcut keys or accelerators, so you need to track values you have entered in your application menus.+ Creating submenus Many application menus contain drop-down lists that appear next to a menu item to provide additional, related commands. Such lists are indicated by an arrow to the right of the menu item. Delphi supports as many levels of such submenus as you want to build into your menu. Organizing your menu structure this way can save vertical screen space. However, for optimal design purposes you probably want to use no more than two or three menu levels in your interface design. (For pop-up menus, you might want to use only one submenu, if any.) Figure 5.5 Nested menu structures Menu item on the menu bar Menu item in a menu list Nested menu item 5-16Developer’ sGuide, CreatingandmanagingmenusTo create a submenu, 1 Select the menu item under which you want to create a submenu. 2 Press Ctrl + → to create the first placeholder, or right-click and choose Create Submenu. 3 Type a name for the submenu item, or drag an existing menu item into this placeholder. 4 Press Enter, or ↓, to create the next placeholder. 5 Repeat steps 3 and 4 for each item you want to create in the submenu. 6 Press Esc to return to the previous menu level. Creating submenus by demoting existing menus You can create a submenu by inserting a menu item from the menu bar (or a menu template) between menu items in a list. When you move a menu into an existing menu structure, all its associated items move with it, creating a fully intact submenu. This pertains to submenus as well—moving a menu item into an existing submenu just creates one more level of nesting. Moving menu items During design time, you can move menu items simply by dragging and dropping. You can move menu items along the menu bar, or to a different place in the menu list, or into a different menu entirely. The only exception to this is hierarchical: you cannot demote a menu item from the menu bar into its own menu; nor can you move a menu item into its own submenu. However, you can move any item into a different menu, no matter what its original position is. While you are dragging, the cursor changes shape to indicate whether you can release the menu item at the new location. When you move a menu item, any items beneath it move as well. To move a menu item along the menu bar, 1 Drag the menu item along the menu bar until the arrow tip of the drag cursor points to the new location. 2 Release the mouse button to drop the menu item at the new location. To move a menu item into a menu list, 1 Drag the menu item along the menu bar until the arrow tip of the drag cursor points to the new menu. This causes the menu to open, enabling you to drag the item to its new location. 2 Drag the menu item into the list, releasing the mouse button to drop the menu item at the new location. Developingtheapplicationuserinterface5-17, CreatingandmanagingmenusAdding images to menu items Images can help users navigate in menus by matching glyphs and images to menu item action, similar to toolbar images. To add an image to a menu item: 1 Drop a TMainMenu or TPopupMenu object on a form. 2 Drop a TImageList object on the form. 3 Open the ImageList editor by double clicking on the TImageList object. 4 Click Add to select the bitmap or bitmap group you want to use in the menu. Click OK. 5 Set the TMainMenu or TPopupMenu object’s Images property to the ImageList you just created. 6 Create your menu items and submenu items as described above. 7 Select the menu item you want to have an image in the Object Inspector and set the ImageIndex property to the corresponding number of the image in the ImageList (the default value for ImageIndex is –1, which doesn’t display an image). Note Use images that are 16 by 16 pixels for proper display in the menu. Although you can use other sizes for the menu images, alignment and consistency problems may result when using images greater than or smaller than 16 by 16 pixels. Viewing the menu You can view your menu in the form at design time without first running your program code. (Pop-up menu components are visible in the form at design time, but the pop-up menus themselves are not. Use the Menu Designer to view a pop-up menu at design time.) To view the menu, 1 If the form is visible, click the form, or from the View menu, choose the form whose menu you want to view. 2 If the form has more than one menu, select the menu you want to view from the form’s Menu property drop-down list. The menu appears in the form exactly as it will when you run the program.

Editing menu items in the Object Inspector

This section has discussed how to set several properties for menu items—for example, the Name and Caption properties—by using the Menu Designer. The section has also described how to set menu item properties, such as the ShortCut property, directly in the Object Inspector, just as you would for any component selected in the form. When you edit a menu item by using the Menu Designer, its properties are still displayed in the Object Inspector. You can switch focus to the Object Inspector and continue editing the menu item properties there. Or you can select the menu item 5-18Developer’ sGuide, Creatingandmanagingmenusfrom the Component list in the Object Inspector and edit its properties without ever opening the Menu Designer. To close the Menu Designer window and continue editing menu items, 1 Switch focus from the Menu Designer window to the Object Inspector by clicking the properties page of the Object Inspector. 2 Close the Menu Designer as you normally would. The focus remains in the Object Inspector, where you can continue editing properties for the selected menu item. To edit another menu item, select it from the Component list.

Using the Menu Designer context menu

The Menu Designer context menu provides quick access to the most common Menu Designer commands, and to the menu template options. (For more information about menu templates, refer to “Using menu templates” on page 5-20.) To display the context menu, right-click the Menu Designer window, or press Alt+F10 when the cursor is in the Menu Designer window.

Commands on the context menu

The following table summarizes the commands on the Menu Designer context menu. Table 5.2 Menu Designer context menu commands Menu command Action Insert Inserts a placeholder above or to the left of the cursor. Delete Deletes the selected menu item (and all its sub-items, if any). Create Submenu Creates a placeholder at a nested level and adds an arrow to the right of the selected menu item. Select Menu Opens a list of menus in the current form. Double-clicking a menu name opens the designer window for the menu. Save As Template Opens the Save Template dialog box, where you can save a menu for future reuse. Insert From Opens the Insert Template dialog box, where you can select a template to Template reuse. Delete Templates Opens the Delete Templates dialog box, where you can choose to delete any existing templates. Insert From Opens the Insert Menu from Resource file dialog box, where you can Resource choose an .MNU file to open in the current form.

Switching between menus at design time

If you’re designing several menus for your form, you can use the Menu Designer context menu or the Object Inspector to easily select and move among them. Developingtheapplicationuserinterface5-19, CreatingandmanagingmenusTo use the context menu to switch between menus in a form, 1 Right-click in the Menu Designer and choose Select Menu. The Select Menu dialog box appears. Figure 5.6 Select Menu dialog box This dialog box lists all the menus associated with the form whose menu is currently open in the Menu Designer. 2 From the list in the Select Menu dialog box, choose the menu you want to view or edit. To use the Object Inspector to switch between menus in a form, 1 Give focus to the form whose menus you want to choose from. 2 From the Component list, select the menu you want to edit. 3 On the Properties page of the Object Inspector, select the Items property for this menu, and then either click the ellipsis button, or double-click [Menu].

Using menu templates

Delphi provides several predesigned menus, or menu templates, that contain frequently used commands. You can use these menus in your applications without modifying them (except to write code), or you can use them as a starting point, customizing them as you would a menu you originally designed yourself. Menu templates do not contain any event handler code. The menu templates shipped with Delphi are stored in the BIN subdirectory in a default installation. These files have a .DMT (Delphi menu template) extension. You can also save as a template any menu that you design using the Menu Designer. After saving a menu as a template, you can use it as you would any predesigned menu. If you decide you no longer want a particular menu template, you can delete it from the list. 5-20Developer’ sGuide, CreatingandmanagingmenusTo add a menu template to your application, 1 Right-click the Menu Designer and choose Insert From Template. (If there are no templates, the Insert From Template option appears dimmed in the context menu.) The Insert Template dialog box opens, displaying a list of available menu templates. Figure 5.7 Sample Insert Template dialog box for menus 2 Select the menu template you want to insert, then press Enter or choose OK. This inserts the menu into your form at the cursor’s location. For example, if your cursor is on a menu item in a list, the menu template is inserted above the selected item. If your cursor is on the menu bar, the menu template is inserted to the left of the cursor. To delete a menu template, 1 Right-click the Menu Designer and choose Delete Templates. (If there are no templates, the Delete Templates option appears dimmed in the context menu.) The Delete Templates dialog box opens, displaying a list of available templates. 2 Select the menu template you want to delete, and press Del. Delphi deletes the template from the templates list and from your hard disk.

Saving a menu as a template

Any menu you design can be saved as a template so you can use it again. You can use menu templates to provide a consistent look to your applications, or use them as a starting point which you then further customize. The menu templates you save are stored in your BIN subdirectory as .DMT files. Developingtheapplicationuserinterface5-21, CreatingandmanagingmenusTo save a menu as a template, 1 Design the menu you want to be able to reuse. This menu can contain as many items, commands, and submenus as you like; everything in the active Menu Designer window will be saved as one reusable menu. 2 Right-click in the Menu Designer and choose Save As Template. The Save Template dialog box appears. Figure 5.8 Save Template dialog box for menus 3 In the Template Description edit box, type a brief description for this menu, and then choose OK. The Save Template dialog box closes, saving your menu design and returning you to the Menu Designer window. Note The description you enter is displayed only in the Save Template, Insert Template, and Delete Templates dialog boxes. It is not related to the Name or Caption property for the menu. Naming conventions for template menu items and event handlers When you save a menu as a template, Delphi does not save its Name property, since every menu must have a unique name within the scope of its owner (the form). However, when you insert the menu as a template into a new form by using the Menu Designer, Delphi then generates new names for it and all of its items. For example, suppose you save a File menu as a template. In the original menu, you name it MyFile. If you insert it as a template into a new menu, Delphi names it File1. If you insert it into a menu with an existing menu item named File1, Delphi names it File2. Delphi also does not save any OnClick event handlers associated with a menu saved as a template, since there is no way to test whether the code would be applicable in the new form. When you generate a new event handler for the menu template item, Delphi still generates the event handler name. 5-22Developer’ sGuide, CreatingandmanagingmenusYou can easily associate items in the menu template with existing OnClick event handlers in the form. For more information, see “Associating an event with an existing event handler” on page 2-40.

Manipulating menu items at runtime

Sometimes you want to add menu items to an existing menu structure while the application is running, to provide more information or options to the user. You can insert a menu item by using the menu item’s Add or Insert method, or you can alternately hide and show the items in a menu by changing their Visible property. The Visible property determines whether the menu item is displayed in the menu. To dim a menu item without hiding it, use the Enabled property. For specific code examples that use the menu item’s Visible and Enabled properties, see “Disabling menu items” on page 6-11. In multiple document interface (MDI) and Object Linking and Embedding (OLE) applications, you can also merge menu items into an existing menu bar. The following section discusses this in more detail.

Merging menus

For MDI applications, such as the text editor sample application, and for OLE client applications, your application’s main menu needs to be able to receive menu items either from another form or from the OLE server object. This is often called merging menus. You prepare menus for merging by specifying values for two properties: • Menu, a property of the form • GroupIndex, a property of menu items in the menu Specifying the active menu: Menu property The Menu property specifies the active menu for the form. Menu-merging operations apply only to the active menu. If the form contains more than one menu component, you can change the active menu at runtime by setting the Menu property in code. For example, Form1.Menu := SecondMenu; Determining the order of merged menu items: GroupIndex property The GroupIndex property determines the order in which the merging menu items appear in the shared menu bar. Merging menu items can replace those on the main menu bar, or can be inserted. Developingtheapplicationuserinterface5-23, CreatingandmanagingmenusThe default value for GroupIndex is 0. Several rules apply when specifying a value for GroupIndex: • Lower numbers appear first (farther left) in the menu. For instance, set the GroupIndex property to 0 (zero) for a menu that you always want to appear leftmost, such as a File menu. Similarly, specify a high number (it needn’t be in sequence) for a menu that you always want to appear rightmost, such as a Help menu. • To replace items in the main menu, give items on the child menu the same GroupIndex value. This can apply to groupings or to single items. For example, if your main form has an Edit menu item with a GroupIndex value of 1, you can replace it with one or more items from the child form’s menu by giving them a GroupIndex value of 1 as well. Giving multiple items in the child menu the same GroupIndex value keeps their order intact when they merge into the main menu. • To insert items without replacing items in the main menu, leave room in the numeric range of the main menu’s items and “plug in” numbers from the child form. For example, number the items in the main menu 0 and 5, and insert items from the child menu by numbering them 1, 2, 3 and 4.

Importing resource files

Delphi supports menus built with other applications, so long as they are in the standard Windows resource (.RC) file format. You can import such menus directly into your Delphi project, saving you the time and effort of rebuilding menus that you created elsewhere. To load existing .RC menu files, 1 In the Menu Designer, place your cursor where you want the menu to appear. The imported menu can be part of a menu you are designing, or an entire menu in itself. 2 Right-click and choose Insert From Resource. The Insert Menu From Resource dialog box appears. 3 In the dialog box, select the resource file you want to load, and choose OK. The menu appears in the Menu Designer window. Note If your resource file contains more than one menu, you first need to save each menu as a separate resource file before importing it. 5-24Developer’ sGuide, Designingtoolbarsandcoolbars

Designing toolbars and cool bars

A toolbar is a panel, usually across the top of a form (under the menu bar), that holds buttons and other controls. Toolbars provide a visible way to present options to the user, and Delphi makes it easy to add toolbars to your forms. A cool bar is a kind of toolbar that displays controls on movable, resizable bands. You can add as many toolbars to a form as you want. If you have multiple panels aligned to the top of the form, they stack vertically in the order added. You can put controls of any sort on a toolbar. In addition to buttons, you may want to put use color grids, scroll bars, labels, and so on. There are several ways to add a toolbar to a form: • Place a panel (TPanel) on the form and add controls (typically speed buttons) to it. • Use a toolbar component (TToolBar) instead of TPanel, and add controls to it. TToolBar manages buttons and other controls, arranging them in rows and automatically adjusting their sizes and positions. If you use tool button (TToolButton) controls on the toolbar, TToolBar makes it easy to group the buttons functionally and provides other display options. • Use a cool bar (TCoolBar) component and add controls to it. The cool bar displays controls on independently movable and resizable bands. How you implement your toolbar depends on your application. The advantage of using the Panel component is that you have total control over the look and feel of the toolbar. By using the toolbar and cool bar components, you are ensuring that your application has the look and feel of a Windows application because you are using the native Windows controls. If these operating system controls change in the future, your application could change as well. Also, since the toolbar and cool bar rely on common components in Windows, your application requires the COMCTL32.DLL. Toolbars and cool bars are Windows 95/NT 4 controls; they are not supported in WinNT 3.51 applications. The following sections describe how to: • Add a toolbar and corresponding speed button controls using the panel component • Add a toolbar and corresponding tool button controls using the Toolbar component • Add a cool bar using the cool bar component • Respond to clicks • Add hidden toolbars and cool bars • Hide and show toolbars and cool barsDevelopingtheapplicationuserinterface5-25, Designingtoolbarsandcoolbars

Adding a toolbar using a panel component

To add a toolbar to a form using the panel component, 1 Add a panel component to the form (from the Standard page of the Component palette). 2 Set the panel’s Align property to alTop. When aligned to the top of the form, the panel maintains its height, but matches its width to the full width of the form’s client area, even if the window changes size. 3 Add speed buttons or other controls to the panel. Speed buttons are designed to work on toolbar panels. A speed button usually has no caption, only a small graphic (called a glyph), which represents the button’s function. Speed buttons have three possible modes of operation. They can • Act like regular pushbuttons • Toggle on and off when clicked • Act like a set of radio buttons To implement speed buttons on toolbars, do the following: • Add a speed button to a toolbar panel • Assign a speed button’s glyph • Set the initial condition of a speed button • Create a group of speed buttons • Allow toggle buttons Adding a speed button to a panel To add a speed button to a toolbar panel, place the speed button component (from the Additional page of the Component palette) on the panel. The panel, rather than the form, “owns” the speed button, so moving or hiding the panel also moves or hides the speed button. The default height of the panel is 41, and the default height of speed buttons is 25. If you set the Top property of each button to 8, they’ll be vertically centered. The default grid setting snaps the speed button to that vertical position for you. Assigning a speed button’s glyph Each speed button needs a graphic image called a glyph to indicate to the user what the button does. If you supply the speed button only one image, the button manipulates that image to indicate whether the button is pressed, unpressed, selected, or disabled. You can also supply separate, specific images for each state if you prefer. You normally assign glyphs to speed buttons at design time, although you can assign different glyphs at runtime. 5-26Developer’ sGuide, DesigningtoolbarsandcoolbarsTo assign a glyph to a speed button at design time, 1 Select the speed button. 2 In the Object Inspector, select the Glyph property. 3 Double-click the Value column beside Glyph to open the Picture Editor and select the desired bitmap. Setting the initial condition of a speed button Speed buttons use their appearance to give the user clues as to their state and purpose. Because they have no caption, it’s important that you use the right visual cues to assist users. Table 5.3 lists some actions you can set to change a speed button’s appearance. Table 5.3 Setting speed buttons’ appearance To make a speed button: Set the toolbar’s: Appear pressed GroupIndex property to a value other than zero and its Down property to True. Appear disabled Enabled property to False. Have a left margin Indent property to a value greater than 0. If your application has a default drawing tool, ensure that its button on the toolbar is pressed when the application starts. To do so, set its GroupIndex property to a value other than zero and its Down property to True. Creating a group of speed buttons A series of speed buttons often represents a set of mutually exclusive choices. In that case, you need to associate the buttons into a group, so that clicking any button in the group causes the others in the group to pop up. To associate any number of speed buttons into a group, assign the same number to each speed button’s GroupIndex property. The easiest way to do this is to select all the buttons you want in the group, and, with the whole group selected, set GroupIndex to a unique value. Allowing toggle buttons Sometimes you want to be able to click a button in a group that’s already pressed and have it pop up, leaving no button in the group pressed. Such a button is called a toggle. Use AllowAllUp to create a grouped button that acts as a toggle: click it once, it’s down; click it again, it pops up. To make a grouped speed button a toggle, set its AllowAllUp property to True. Setting AllowAllUp to True for any speed button in a group automatically sets the same property value for all buttons in the group. This enables the group to act as a normal group, with only one button pressed at a time, but also allows every button to be up at the same time. Developingtheapplicationuserinterface5-27, Designingtoolbarsandcoolbars

Adding a toolbar using the toolbar component

The toolbar component (TToolBar) offers button management and display features that panel components do not. To add a toolbar to a form using the toolbar component, 1 Add a toolbar component to the form (from the Win32 page of the Component palette). The toolbar automatically aligns to the top of the form. 2 Add tool buttons or other controls to the bar. Tool buttons are designed to work on toolbar components. Like speed buttons, tool buttons can • Act like regular pushbuttons • Toggle on and off when clicked • Act like a set of radio buttons To implement tool buttons on a toolbar, do the following: • Add a tool button • Assign images to tool buttons • Set the tool buttons’ appearance • Create a group of tool buttons • Allow toggled tool buttons Adding a tool button To add a tool button to a toolbar, right-click on the toolbar and choose New Button. The toolbar “owns” the tool button, so moving or hiding the toolbar also moves or hides the button. In addition, all tool buttons on the toolbar automatically maintain the same height and width. You can drop other controls from the Component palette onto the toolbar, and they will automatically maintain a uniform height. Controls will also wrap around and start a new row when they do not fit horizontally on the toolbar. Assigning images to tool buttons Each tool button has an ImageIndex property that determines what image appears on it at runtime. If you supply the tool button only one image, the button manipulates that image to indicate whether the button is disabled. To assign images to tool buttons at design time, 1 Select the toolbar on which the buttons appear. 2 In the Object Inspector, assign a TImageList object to the toolbar’s Images property. An image list is a collection of same-sized icons or bitmaps. 3 Select a tool button. 4 In the Object Inspector, assign an integer to the tool button’s ImageIndex property that corresponds to the image in the image list that you want to assign to the button. 5-28Developer’ sGuide, DesigningtoolbarsandcoolbarsYou can also specify separate images to appear on the tool buttons when they are disabled and when they are under the mouse pointer. To do so, assign separate image lists to the toolbar’s DisabledImages and HotImages properties. Setting tool button appearance and initial conditions Table 5.4 lists some actions you can set to change a tool button’s appearance: Table 5.4 Setting tool buttons’ appearance To make a tool button: Set the toolbar’s: Appear pressed GroupIndex property to a nonzero value and its Down property to True. Appear disabled Enabled property to False. Have a left margin Indent property to a value greater than 0. Appear to have “pop-up” borders, thus Flat property to True. making the toolbar appear transparent Note Using the Flat property of TToolBar requires version 4.70 or later of COMCTL32.DLL. To force a new row of controls after a specific tool button, Select the tool button that you want to appear last in the row and set its Wrap property to True. To turn off the auto-wrap feature of the toolbar, set the toolbar’s Wrapable property to False. Creating groups of tool buttons To create a group of tool buttons, select the buttons you want to associate and set their Style property to tbsCheck; then set their Grouped property to True. Selecting a grouped tool button causes other buttons in the group to pop up, which is helpful to represent a set of mutually exclusive choices. Any unbroken sequence of adjacent tool buttons with Style set to tbsCheck and Grouped set to True forms a single group. To break up a group of tool buttons, separate the buttons with any of the following: • A tool button whose Grouped property is False. • A tool button whose Style property is not set to tbsCheck. To create spaces or dividers on the toolbar, add a tool button whose Style is tbsSeparator or tbsDivider. • Another control besides a tool button. Allowing toggled tool buttons Use AllowAllUp to create a grouped tool button that acts as a toggle: click it once, it is down; click it again, it pops up. To make a grouped tool button a toggle, set its AllowAllUp property to True. As with speed buttons, setting AllowAllUp to True for any tool button in a group automatically sets the same property value for all buttons in the group. Developingtheapplicationuserinterface5-29, Designingtoolbarsandcoolbars

Adding a cool bar component

The cool bar component—also called a rebar—displays windowed controls on independently movable, resizable bands. The user can position the bands by dragging the resizing grips on the left side of each band. To add a cool bar to a form, 1 Add a cool bar component to the form (from the Win32 page of the Component palette). The cool bar automatically aligns to the top of the form. 2 Add windowed controls from the Component palette to the bar. Only components that descend from TWinControl are windowed controls. You can add graphic controls—such as labels or speed buttons—to the cool bar, but they will not appear on separate bands. Note The cool bar component requires version 4.70 or later of COMCTL.DLL.

Setting the appearance of the cool bar

The cool bar component offers several useful configuration options. Table 5.5 lists some actions you can set to change a tool button’s appearance: Table 5.5 Setting a cool button’s appearance To make the cool bar: Set the toolbar’s: Resize automatically to accommodate the AutoSize property to true. bands it contains Bands maintain a uniform height FixedSize property to true. Reorient to vertical rather than horizontal Vertical property to true. This changes the effect of the FixedSize property. Prevent the Text properties of the bands from ShowText property to false. Each band in a cool displaying at runtime bar has its own Text property. Remove the border around the bar BandBorderStyle to bsNone. Keep users from changing the bands’ order at FixedOrder to true. runtime. (The user can still move and resize the bands.) Create a background image for the cool bar Bitmap property to TBitmap object. Choose a list of images to appear on the left of Images property to TImageList object. any band To assign images to individual bands, select the cool bar and double-click on the Bands property in the Object Inspector. Then select a band and assign a value to its ImageIndex property.

Responding to clicks

When the user clicks a control, such as a button on a toolbar, the application generates an OnClick event. You handle these OnClick events by writing OnClick event handlers for your forms. 5-30Developer’ sGuide, DesigningtoolbarsandcoolbarsWriting an event handler for a button click Button controls, including speed buttons and tool buttons, have OnClick events built into them. To respond to the user’s click on a button, define a handler for the button’s OnClick event. When you double-click on a button in the Form designer, C++Builder generates an OnClick event handler named after the control clicked. For example, the default OnClick event handler for a speed button named LineButton would be procedure TForm1.LineButtonClick(Sender: TObject); begin end; Then, supply the action you want to occur when the user clicks that button. Assigning a menu to a tool button If you are using a toolbar (TToolBar) with tool buttons (TToolButton), you can associate menu with a specific button: 1 Select the tool button. 2 In the Object Inspector, assign a pop-up menu (TPopupMenu) to the tool button’s DropDownMenu property. If the menu’s AutoPopup property is set to True, it will appear automatically when the button is pressed.

Adding hidden toolbars

Toolbars do not have to be visible all the time. In fact, it is often convenient to have a number of toolbars available, but show them only when the user wants to use them. Often you create a form that has several toolbars, but hide some or all of them. To create a hidden toolbar, 1 Add a toolbar, cool bar, or panel component to the form. 2 Set the component’s Visible property to False. Although the toolbar remains visible at design time so you can modify it, it remains hidden at runtime until the application specifically makes it visible.

Hiding and showing toolbars

Often, you want an application to have multiple toolbars, but you do not want to clutter the form with them all at once. Or you may want to let users decide whether to display toolbars. As with all components, toolbars can be shown or hidden at runtime as needed. To hide or show a toolbar at runtime, set its Visible property to False or True, respectively. Usually you do this in response to particular user events or changes inDevelopingtheapplicationuserinterface5-31, Usingactionliststhe operating mode of the application. To do this, you typically have a close button on each toolbar. When the user clicks that button, the application hides the corresponding toolbar. You can also provide a means of toggling the toolbar. In the following example, a toolbar of pens is toggled from a button on the main toolbar. Since each click presses or releases the button, an OnClick event handler can show or hide the Pen toolbar depending on whether the button is up or down. procedure TForm1.PenButtonClick(Sender: TObject); begin PenBar.Visible := PenButton.Down; end;

Using action lists

Action lists let you centralize the response to user commands (actions) for objects such as menus and buttons that respond to those commands. This section is an overview of actions and action lists, describing how to use them and how they interact with their clients and targets.

Action objects

Actions are user commands that operate on target objects. You create actions in the action list component editor. These actions are later connected to client controls via their action links. Following are descriptions of each type of component in the action/action list mechanism: • An action (TAction) is the implementation of an action, such as copying highlighted text, on a target, such as an edit control. An action is triggered by a client in response to a user command (i.e. a mouse click). Clients are typically menu items or buttons. The StdActns unit contains classes derived from TAction that implement the basic Edit and Window menu commands (actions) found in most Window applications. • An action list (TActionList) is a component that maintains a list of actions (TAction). Action lists are the design-time user interface for working with actions. • An action link (TActionLink) is an object that maintains the connection between actions and clients. Action links determine whether an action, or which action, is currently applicable for a given client. • A client of an action is typically a menu item or a button (TToolButton, TSpeedButton, TMenuItem, TButton, TCheckBox, TRadioButton, and so on). An action is initiated by a corresponding command in the client. Typically a client Click is associated with an action Execute. • An action target is usually a control, such as a rich edit, a memo, or a data control. The DBActns unit, for example, contains classes that implement actions specific to data set controls. Component writers can create their own actions specific to the 5-32Developer’ sGuide, Usingactionlistsneeds of the controls they design and use, and then package those units to create more modular applications. Figure 5.9 shows the relationship of these objects. In this diagram, Cut1 is the action, ActionList1 is the action list containing Cut1, SpeedButton1 is the client of Cut1, and Memo1 is the target. Unlike actions, action lists, action clients, and action targets, action links are non-visual objects. The action link in this diagram is therefore indicated by a white rectangle. The action link associates the SpeedButton1 client to the Cut1 action contained in ActionList1. Figure 5.9 Action list mechanism Action Linked to Cut1 ActionList1 contains: Cut1 The VCL includes TAction, TActionList, and TActionLink type classes for working with Action lists. By unit these are: • ActnList.pas: TAction, TActionLink, TActionList, TContainedAction, TCustomAction, and TCustomActionList • Classes.pas: TBasicAction and TBasicActionLink • Controls.pas: TControlActionLink and TWinControlActionLink • ComCtrls.pas: TToolButtonActionLink • Menus.pas: TMenuActionLink • StdCtrls.pas: TButtonActionLink There are also two units, StdActns and DBActns, that contain auxiliary classes that implement specific, commonly used standard Windows and data set actions. These are described in “Pre-defined action classes” on page 5-36. Many of the VCL controls include properties (such as, Action) and methods (such as, ExecuteAction) that enable them to be used as action clients and targets.

Using actions

You can add an action list to your forms or data modules from the standard page of the Component Palette. Double-click the action list to display the Action List editor, which lets you add, delete, and rearrange actions in much the same way you use the collection editor. Developingtheapplicationuserinterface5-33, UsingactionlistsIn the Object Inspector, set the properties for each action. The Name property identifies the action, and the other properties and events (Caption, Checked, Enabled, HelpContext, Hint, ImageIndex, ShortCut, and Visible) correspond to the properties of client controls. These are typically, but not necessarily, the same name as the client property. For example, an action’s Checked property corresponds to a TToolButton’s Down property. Centralizing code A number of controls such as TToolButton, TSpeedButton, TMenuItem, and TButton have a published property called Action. When you set the Action property to one of the actions in your action list, the values of the corresponding properties in the action are copied to those of the control. All properties and events in common with the action object (except Name and Tag) are dynamically linked to the control. Thus, for example, instead of duplicating the code that disables buttons and menu items, you can centralize this code in an action object, and when the action is disabled, all corresponding buttons and menu items are disabled. Linking properties The client’s action link is the mechanism through which its properties are associated with (linked to) the properties of an action. When an action changes, the action link is responsible for updating the client’s properties. For details about which properties a particular action link class handles, refer to the individual action link classes in the VCL reference online Help. You can selectively override the values of the properties controlled by an associated action object by setting the property’s value in the client component or control. This does not change the property in the action, so only the client is affected. Executing actions When a client component or control is clicked, the OnExecute event occurs for it’s associated action. For example, the following code illustrates the OnExecute event handler for an action that toggles the visibility of a toolbar when the action is executed: procedure TForm1.Action1Execute(Sender: TObject); begin { Toggle Toolbar1’s visibility } ToolBar1.Visible := not ToolBar1.Visible; end; Note If you are using a tool button or a menu item, you must manually set the Images property of the corresponding toolbar or menu component to the Images property of the action list. This is true even though the ImageIndex property is dynamically linked to the client. Figure 5.10 illustrates the dispatching sequence for the execution cycle of an action called Cut1. This diagram assumes the relationship of the components in Figure 5.9, meaning that the Speedbutton1 client is linked to the Cut1 action via its action link. Speedbutton1’s Action property is therefore Cut1. Consequently, Speedbutton1’s Click method invokes Cut1’s Execute method. 5-34Developer’ sGuide, UsingactionlistsFigure 5.10 Execution cycle for an action ActionList1.ExecuteAction SpeedButton1 Cut1.Execute Cut1 ActionList1

EXECU TE

CM_AC

TION

Application ActionList1.OnExecuteAction Cut1.OnExecute Application.OnExecuteAction LEGEND Application Events Calls Objects Returns Application.ExecuteAction Note In the description of this sequence, one method invoking another does not necessarily mean that the invocation is explicit in the code for that method. Clicking on Speedbutton1 initiates the following execution cycle: • Speedbutton1’s Click method invokes Cut1.Execute. • The Cut1 action defers to its action list (ActionList1) for the processing of its Execute. This is done by calling the Action list’s ExecuteAction method, passing itself as a parameter. • ActionList1 calls its event handler (OnExecuteAction) for ExecuteAction. (An action list’s ExecuteAction method applies to all actions contained by the action list.) This handler has a parameter Handled, that returns False by default. If the handler is assigned and handles the event, it should return True, and the processing sequence ends here. For example: procedure TForm1.ActionList1ExecuteAction(Action: TBasicAction; var Handled: Boolean); begin { Prevent execution of actions contained by ActionList1 } Handled := True; end; If execution is not handled, at this point, in the action list event handler, then processing continues: • The Cut1 action is routed to the Application object’s ExecuteAction method, which invokes the OnExecuteAction event handler. (The application’s ExecuteAction method applies to all of the actions in that application.) The sequence is the same as for the action list ExecuteAction: The handler has a parameter Handled thatDevelopingtheapplicationuserinterface5-35, Usingactionlistsreturns False by default. If the handler is assigned and handles the event, it should return True, and the processing sequence ends here. For example: procedure TForm1.ApplicationExecuteAction(Action: TBasicAction; var Handled: Boolean); begin { Prevent execution of all actions in Application } Handled := True; end; If execution is not handled in the application’s event handler, then Cut1 send the CM_ACTIONEXECUTE message to the application’s WndProc, passing itself as a parameter. The application then tries to find a target on which to execute the action (see Figure 5.11, “Action targets”). Updating actions When the application is idle, the OnUpdate event occurs for every action that is linked to a visible control or menu item that is showing. This provides an opportunity for applications to execute centralized code for enabling and disabling, checking and unchecking, and so on. For example, the following code illustrates the OnUpdate event handler for an action that is “checked” when the toolbar is visible: procedure TForm1.Action1Update(Sender: TObject); begin { Indicate whether ToolBar1 is currently visible } (Sender as TAction).Checked := ToolBar1.Visible; end; See also the RichEdit demo (Delphi\Demos\RichEdit). The dispatching cycle for updating actions follows the same sequence as the execution cycle in Figure 5.10. Note Do not add time-intensive code to the OnUpdate event handler. This executes whenever the application is idle. If the event handler takes too much time, it will adversely affect performance of the entire application.

Pre-defined action classes

Component writers can use the classes in the StdActns and DBActns units as examples for deriving their own action classes that implement behaviors specific to certain controls or components. The base classes for these specialized actions (TEditAction, TWindowAction) generally override HandlesTarget, UpdateTarget, and other methods to limit the target for the action to a specific class of objects. The descendant classes typically override ExecuteTarget to perform a specialized task. Standard edit actions The standard edit actions are designed to be used with an edit control target. TEditAction is the base class for descendants that each override the ExecuteTarget method to implement copy, cut, and paste tasks by using the Windows Clipboard. 5-36Developer’ sGuide, Usingactionlists• TEditAction ensures that the target control is a TCustomEdit class (or descendant). • TEditCopy copies highlighted text to the Clipboard. • TEditCut cuts highlighted text from the target to the Clipboard. • TEditPaste pastes text from the Clipboard to the target and ensures that the Clipboard is enabled for the text format. Standard Window actions The standard Window actions are designed to be used with forms as targets in an MDI application. TWindowAction is the base class for descendants that each override the ExecuteTarget method to implement arranging, cascading, closing, tiling, and minimizing MDI child forms. • TWindowAction ensures that the target control is a TForm class and checks whether the form has MDI child forms. • TWindowArrange arranges the icons of minimized MDI child forms. • TWindowCascade cascades the MDI child forms. • TWindowClose closes the active MDI child form. • TWindowMinimizeAll minimizes all of the MDI child forms. • TWindowTileHorizontal arranges MDI child forms so that they are all the same size, tiled horizontally. • TWindowTileVertical arranges MDI child forms so that they are all the same size, tiled vertically. DataSet actions The standard dataset actions are designed to be used with a dataset component target. TDataSetAction is the base class for descendants that each override the ExecuteTarget and UpdateTarget methods to implement navigation and editing of the target. The TDataSetAction introduces a DataSource property which ensures actions are performed on that dataset. If DataSource is nil, the currently focused data-aware control is used. For details, refer to Figure 5.11, “Action targets,” on page 5-38. • TDataSetAction ensures that the target is a TDataSource class and has an associated data set. • TDataSetCancel cancels the edits to the current record, restores the record display to its condition prior to editing, and turns off Insert and Edit states if they are active. • TDataSetDelete deletes the current record and makes the next record the current record. • TDataSetEdit puts the dataset into Edit state so that the current record can be modified. • TDataSetFirst sets the current record to the first record in the dataset. Developingtheapplicationuserinterface5-37, Usingactionlists• TDataSetInsert inserts a new record before the current record, and sets the dataset into Insert and Edit states. • TDataSetLast sets the current record to the last record in the dataset. • TDataSetNext sets the current record to the next record. • TDataSetPost writes changes in the current record to the dataset. • TDataSetPrior sets the current record to the previous record. • TDataSetRefresh refreshes the buffered data in the associated dataset.

Writing action components

The pre-defined actions are examples of extending the VCL action classes. The following topics are useful if you are writing your own action classes: • How actions find their targets • Registering actions • Writing action list editors How actions find their targets Figure 5.10 illustrates the execution cycle for the standard VCL action classes. If execution is not handled by the action list, the application, or the default action event handlers, then the CM_ACTIONEXECUTE message is sent to the application’s WndProc. Figure 5.11 continues the execution sequence at this point. The pre-defined action classes described above, as well as any action class that you create, use this path of execution: Figure 5.11 Action targets CM_ACTIONE Form1.CM_ACTIONEXECUTEXECUTE Application Form1 Memo1 Memo1.ExecuteAction(Cut1) yes Cut1.HandlesTarget(Memo1)

LEGEND

Cut1 Events Calls Objects Returns Cut1.ExecuteTarget(Memo1): 5-38Developer’ sGuide, Usingactionlists• Upon receiving the CM_ACTIONEXECUTE message the application first dispatches it to the Screen’s ActiveForm. If there is no active form, the application sends the message to it’s MainForm. • Form1 (in this example, the active form) first looks for the active control (Memo1) and calls that control’s ExecuteAction method passing Cut1 as a parameter. • Memo1 calls Cut1’s HandlesTarget method, passing itself to determine whether it is an appropriate target for the action. If Memo1 is not an appropriate target, HandlesTarget returns False and Memo1’s ExecuteAction handler returns False. • In this case, Memo1 is an appropriate target for Cut1, so HandlesTarget returns True. Memo1 then calls Cut1.ExecuteTarget passing itself as a parameter. Finally, since Cut1 is an instance of a TEditCut action, the action calls Memo1’s CutToClipoard method: procedure TEditCut.ExecuteTarget(Target: TObject); begin GetControl(Target).CutToClipboard; end; If the control were not an appropriate target, processing would continue as follows: • Form1 calls its ExecuteAction method. If Form1 is an appropriate target (for example, a form would be a target for the TWindowCascade action) then it calls Cut1’s ExecuteTarget method, passing itself as a parameter. • If Form1 is not an appropriate target, it invokes ExecuteAction on every visible control it owns until a target is found. Note If the action involved is a TCustomAction type, then the action is automatically disabled for you if, the action is not handled and, its DisableIfNoHandler property is True.

Registering actions

You can register and unregister your own actions with the IDE by using the global routines in the ActnList unit: procedure RegisterActions(const CategoryName: string; const AClasses: array of TBasicActionClass); procedure UnRegisterActions(const AClasses: array of TBasicActionClass); Use these routines the same way you would when registering components RegisterComponents). For example, the following code registers the standard actions with the IDE in the StdReg unit: { Standard action registration } RegisterActions('', [TAction]); RegisterActions('Edit', [TEditCut, TEditCopy, TEditPaste]); RegisterActions('Window', [TWindowClose, TWindowCascade, TWindowTileHorizontal, TWindowTileVertical, TWindowMinimizeAll, TWindowArrange]); Developingtheapplicationuserinterface5-39, Usingactionlists

Writing action list editors

You may want to write your own component editor for action lists. If you do, you can assign your own procedures to the four global procedure variables in the ActnList unit: CreateActionProc: function (AOwner: TComponent; ActionClass: TBasicActionClass): TBasicAction = nil; EnumRegisteredActionsProc: procedure (Proc: TEnumActionProc; Info: Pointer) = nil; RegisterActionsProc: procedure (const CategoryName: string; const AClasses: array of TBasicActionClass; Resource: TComponentClass) = nil; UnRegisterActionsProc: procedure (const AClasses: array of TBasicActionClass) = nil; You only need to reassign these if you want to manage the registration, unregistration, creation, and enumeration procedures of actions differently from the default behavior. If you do, write your own handlers and assign them to these variables within the initialization section of your design-time unit.

Demo programs

For examples of programs that use actions and action lists, refer to Delphi\Demos\ RichEdit. In addition, the Application wizard (File|New Project page) demos, MDI Application, SDI Application, and Win95 Logo Application can use the action and action list objects. 5-40Developer’ sGuide,

Chapter

Chapter 6Working with controls This chapter explains how to use controls in your applications to create a usable user interface.

Implementing drag-and-drop in controls

Dragging and dropping items can be a handy way to enable users to manipulate objects in a form. You can let users drag entire components, or let them drag items out of components such as list boxes into other components. The elements to drag-and-drop operations: • Starting a drag operation • Accepting dragged items • Dropping items • Ending a drag operation • Customizing drag and drop with TDragObject • Changing the drag mouse pointer

Starting a drag operation

Every control has a property called DragMode that controls how the component responds when a user begins dragging the component at runtime. If DragMode is dmAutomatic, dragging begins automatically when the user presses a mouse button with the cursor on the control. Because dmAutomatic can interfere with normal mouse activity, you may typically want to set DragMode to dmManual (which is the default) and start the dragging by handling mouse-down events. To start dragging a control manually, call the control’s BeginDrag method. BeginDrag takes a boolean parameter called Immediate. If you pass True, dragging begins immediately, much as if DragMode were dmAutomatic. If you pass False, Workingwithcontrols6-1, Implementingdrag- and- dropincontrolsdragging does not begin until the user actually moves the mouse a short distance. Calling BeginDrag(False) allows the control to accept mouse clicks without beginning a drag operation. You can also place conditions on whether to begin dragging, such as checking which button the user pressed, by testing the parameters of the mouse-down event before calling BeginDrag. The following code, for example, handles a mouse-down event on a file list box by beginning dragging only if the left mouse button was pressed: procedure TFMForm.FileListBox1MouseDown(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer); begin if Button = mbLeft then { drag only if left button pressed } with Sender as TFileListBox do { treat Sender as TFileListBox } begin if ItemAtPos(Point(X, Y), True) >= 0 then { is there an item here? } BeginDrag(False); { if so, drag it } end; end; This code does not let you drop the item anywhere. Before you can drop items, you must have controls that accept drops as described in the next section.

Accepting dragged items

When a user drags something over a control, that control receives an OnDragOver event, at which time it must indicate whether it can accept the item if the user drops it there. Delphi changes the drag cursor to indicate whether the control can accept the dragged item. To accept items dragged over a control, attach an event handler to the control’s OnDragOver event. The drag-over event has a variable parameter called Accept that the event handler can set to True if it will accept the item. Setting Accept to True specifies that if the user releases the mouse button at that point, thereby dropping the dragged item, the application can then send a drag-drop event to the same control. If Accept is False, the application does not drop the item on that control. This means that a control should never have to handle a drag-drop event for an item it does not know how to handle. The drag-over event includes several parameters, including the source of the dragging and the current location of the mouse cursor. The event handler can use those parameters to determine whether to accept the drop. Most often, a control accepts or rejects a dragged item based on the type of the sender, but it can also accept items only from specific instances. 6-2Developer’ sGuide, Implementingdrag- and- dropincontrolsIn the following example, a directory tree view accepts dragged items only if they come from a file list box: procedure TFMForm.DirectoryOutline1DragOver(Sender, Source: TObject; X, Y: Integer; State: TDragState; var Accept: Boolean); begin if Source is TFileListBox then Accept := True; else Accept := False; end;

Dropping items

Once a control indicates that it can accept a dragged item, it should then also define some way to handle the item should it be dropped. If a user sees the mouse cursor change to indicate that a control will accept the item being dragged, it is reasonable for the user to then expect that dropping the item there will accomplish some task. To handle dropped items, attach an event handler to the OnDragDrop event of the control accepting the dropped item. Like the drag-over event, the drag-drop event indicates the source of the dragged item and the coordinates of the mouse cursor over the accepting control. These parameters enable the drag-drop handler to get any needed information from the source of the drag and determine how to handle it. For example, a directory tree view accepting items dragged from a file list box can move the file from its current location to the directory dropped on: procedure TFMForm.DirectoryOutline1DragDrop(Sender, Source: TObject; X, Y: Integer); begin if Source is TFileListBox then with DirectoryOutline1 do ConfirmChange('Move', FileList.FileName, Items[GetItem(X, Y)].FullPath); end; The source is the component being dragged, while the event handler is associated with the component being passed over. The OnDragDrop event allows you to monitor the path a component takes while being dragged. For example, you might want to change the background color of the component as it is being passed over.

Ending a drag operation

When a dragging operation ends, either by dropping the dragged item or by the user releasing the mouse button over a control that does not accept the dragged item, Delphi sends an end-drag event back to the control the user dragged. To enable a control to respond when items have been dragged from it, attach an event handler to the OnEndDrag event of the control. Workingwithcontrols6-3, Implementingdrag- and- dropincontrolsThe most important parameter in an OnEndDrag event is called Target, which indicates which control, if any, accepts the drop. If Target is nil, it means no control accepts the dragged item. Otherwise, Target is the control that accepts the item. The OnEndDrag event also includes the x- and y-coordinates on the receiving control where the drop occurs. In this example, a file list box handles an end-drag event by refreshing its file list, assuming that dragging a file from the list changes the contents of the current directory: procedure TFMForm.FileList1EndDrag(Sender, Target: TObject; X, Y: Integer); begin if Target <> nil then FileList1.Update; end;

Customizing drag and drop with TDragObject

You can use TDragObject to customize your object’s drag and drop behavior. By default, the drag-over and drag-drop events indicate the source of the dragged item and the coordinates of the mouse cursor over the accepting control. You can get additional state information by deriving the drag object from TDragObject and overriding its virtual methods as needed. By using TDragObject, the source of the drag is the object itself; not the control object as with the TDragControl object. Create the custom drag object in the OnStartDrag event. Use the public IsDragObject function within a target’s OnDragOver event when accepting a drag drop. TDragObject allows more flexible drag and drop handling. Normally, the source parameter of the OnDragOver and OnDragDrop events is the control that starts the drag operation. If multiple controls of differing kinds need to start a drag of the same kind of data (e.g. a filename, text, a dollar amount, etc.), the source would need support for each kind of control. A drag object allows the target to need know only how to handle a drag object as a source since each of the source controls can create the same kind of drag object in their OnStartDrag events. The OnDragOver and OnDragDrop events can tell if the source is a drag object, as opposed to the control, by calling IsDragObject. Drag objects can be dragged between multiple .DLLs as well as inside the main .EXE. This is useful if you are not using packages but still want drag operations to function from forms implemented in .EXEs to forms implemented in .DLLs.

Changing the drag mouse pointer

You can change the look of the drag mouse pointer that Delphi uses during a drag operation. To do this, set the component’s DragCursor property at design time. You can also create your cursor resource. 6-4Developer’ sGuide, Implementingdrag- and- dockincontrols

Implementing drag-and-dock in controls

TControl and TWinControl allow you to implement docking support for your application controls. Descendants of TWinControl can act as docking sites. Descendants of TControl can act as child windows that are docked into docking sites. For example, to provide a docking site at the left edge of a form window, align a panel to the left edge of the form and make the panel a docking site. When dockable controls are dragged to the panel and released, they become child controls of the panel (client-aligned). The elements of drag-and-dock operation: • Making a windowed control a docking site • Making a control a dockable child control • Controlling how child controls are docked in a docking site • Controlling how child controls are undocked in a docking site • Controlling how child controls respond to drag-and-dock operations

Making a windowed control a docking site

To make a windowed control a docking site: 1 Set the DockSite property to True. 2 If the dock site object should not appear except when it contains a docked client, set its AutoSize property to True. When AutoSize is True, the dock site is sized to 0 until it accepts a child control for docking. Then it resizes to fit around the child control.

Making a control a dockable child control

To make a control a dockable child control: 1 Set its DragKind property to dkDock. When DragKind is dkDock, dragging the control moves the control to a new docking site or undocks the control so that it becomes a floating window. When DragKind is dkDrag (the default), dragging the control starts a drag-and-drop operation which must be implemented using the OnDragOver, OnEndDrag, and OnDragDrop events. 2 Set its DragMode to dmAutomatic. When DragMode is dmAutomatic, dragging (for drag-and-drop or docking, depending on DragKind) is initiated automatically when the user starts dragging the control with the mouse. When DragMode is dmManual, you can still begin a drag-and-dock (or drag-and-drop) operation by calling the BeginDrag method. 3 Set its FloatingDockSiteClass property to indicate the TWinControl descendant that should host the control when it is undocked and left as a floating window. When the control is released and not over a docking site, a windowed control of this class is created dynamically, and becomes the parent of the dockable child. If the dockable child control is a descendant of TWinControl, it is not necessary to createWorkingwithcontrols6-5, Implementingdrag- and- dockincontrolsaseparate floating dock site to host the control, although you may want to specify a form in order to get a border and title bar. To omit a dynamic container window, set FloatingDockSiteClass to the same class as the control, and it will become a floating window with no parent.

Controlling how child controls are docked in a docking site

A docking site automatically accepts child controls when they are released over the docking site. For most controls, the first child is docked to fill the client area, the second splits that into separate regions, and so on. Page controls dock children into new tab sheets (or merge in the tab sheets if the child is another page control). Three events allow docking sites to further constrain how child controls are docked: property OnGetSiteInfo: TGetSiteInfoEvent; TGetSiteInfoEvent = procedure(Sender: TObject; DockClient: TControl; var InfluenceRect: TRect; var CanDock: Boolean) of object; OnGetSiteInfo occurs on the docking site when the user drags a dockable child over the control. It allows the site to indicate whether it will accept the control specified by the DockClient parameter as a child, and if so, where the child must be to be considered for docking. When OnGetSiteInfo occurs, InfluenceRect is initialized to the screen coordinates of the docking site, and CanDock is intialized to True. A more limited docking region can be created by changing InfluenceRect and the child can be rejected by setting CanDock to False. property OnDockOver: TDockOverEvent; TDockOverEvent = procedure(Sender: TObject; Source: TDragDockObject; X, Y: Integer; State: TDragState; var Accept: Boolean) of object; OnDockOver occurs on the docking site when the user drags a dockable child over the control. It is analogous to the OnDragOver event in a normal drag-and-drop operation. Use it to signal that the child can be released for docking, by setting the Accept parameter. If the dockable control was rejected by the OnGetSiteInfo event handler (perhaps because it was the wrong type of control), OnDockOver will not occur. property OnDockDrop: TDockDropEvent; TDockDropEvent = procedure(Sender: TObject; Source: TDragDockObject; X, Y: Integer) of object; OnDockDrop occurs on the docking site when the user releases the dockable child over the control. It is analogous to the OnDragDrop event in a normal drag-and-drop operation. Use this event to perform any necessary accommodations to accepting the control as a child control. Access to the child control can be obtained using the Control property of the TDragDockObject specified by the Source parameter.

Controlling how child controls are undocked in a docking site

A docking site automatically allows child controls to be undocked when they are dragged and have a DragMode property of dmAutomatic. Docking sites can respond 6-6Developer’ sGuide, Workingwithtextincontrolswhen child controls are dragged off, and even prevent the undocking, in an OnUnDock event handler: property OnUnDock: TUnDockEvent; TUnDockEvent = procedure(Sender: TObject; Client: TControl; var Allow: Boolean) of object; The Client parameter indicates the child control that is trying to undock, and the Allow parameter lets the docking site (Sender) reject the undocking. When implementing an OnUnDock event handler, it can be useful to know what other children (if any) are currently docked. This information is available in the read-only DockClients property, which is an indexed array of TControl. The number of dock clients is given by the read-only DockClientCount property.

Controlling how child controls respond to drag-and-dock operations

Dockable child controls have two events that occur during drag-and-dock operations. property OnStartDock: TStartDockEvent; TStartDockEvent = procedure(Sender: TObject; var DragObject: TDragDockObject) of object; OnStartDock is analogous to the OnStartDrag event of a drag-and-drop operation. Similar to OnStartDrag, it allows the dockable child control to create a custom drag object. property OnEndDock: TEndDragEvent; TEndDragEvent = procedure(Sender, Target: TObject; X, Y: Integer) of object; OnEndDock is analogous to the OnEndDrag event of a drag-and-drop operation.

Working with text in controls

The VCL provides many methods and properties for you to easily manipulate text in a rich edit or memo component. The following sections explain how to add these features to a rich edit or memo component. Some features work with an edit component as well. The topics cover: • Setting text alignment • Adding scrollbars at runtime • Adding the Clipboard object • Selecting text • Selecting all text • Cutting, copying, and pasting text • Deleting selected text • Disabling menu items • Providing a pop-up menu • Handling the OnPopup eventWorkingwithcontrols6-7, Workingwithtextincontrols

Setting text alignment

In a rich edit or memo component, users can align text to either the right or left margin or center the text between the two. Applications such as the rich text editor application (in the \EXAMPLES\APPS\RICHEDIT directory), use a check mark on the menu to indicate which option is in effect. To set text alignment in an edit component, set the edit component’s Alignment property. Alignment takes effect only if the WordWrap property is True. If wordwrapping is turned off, there is no right margin to align to. For example, the following code attaches an OnClick event handler to the Character| Left menu item, then attaches the same event handler to both the Right and Center menu items on the Character menu. procedure TEditForm.AlignClick(Sender: TObject); begin Left1.Checked := False; { clear all three checks } Right1.Checked := False; Center1.Checked := False; with Sender as TMenuItem do Checked := True; { check the item clicked } with Editor do { then set Alignment to match } if Left1.Checked then Alignment := taLeftJustify else if Right1.Checked then Alignment := taRightJustify else if Center1.Checked then Alignment := taCenter; end; Note The simple TEdit object does not support alignment because Windows does not support it.

Adding scroll bars at runtime

Rich edit and memo components can contain horizontal or vertical scroll bars, or both, as needed. When word-wrapping is enabled, the component needs only a vertical scroll bar. If the user turns off word-wrapping, the component might also need a horizontal scroll bar, since text is not limited by the right side of the editor. To add scroll bars at runtime, do the following: 1 Determine whether the text might exceed the right margin. In most cases, you check that the rich edit or memo component has wordwrapping enabled. You might also check whether any text lines actually exceed the width of the rich edit or memo component. 2 Set the rich edit or memo component’s ScrollBars property to include or exclude scroll bars. The following example from the simple text editor example (EXAMPLES\DOC\ TEXTEDIT) attaches an OnClick event handler to a Character|WordWrap menu item. In addition to toggling the Editor component’s WordWrap property, the OnClick event 6-8Developer’ sGuide, Workingwithtextincontrolshandler sets the ScrollBars property to a value appropriate to the current wordwrapping: procedure TEditForm.WordWrap1Click(Sender: TObject); begin with Editor do begin WordWrap := not WordWrap; { toggle word-wrapping } if WordWrap then ScrollBars := ssVertical { wrapped requires only vertical } else ScrollBars := ssBoth; { unwrapped might need both } WordWrap1.Checked := WordWrap; { check menu item to match property } end; end; The rich edit and memo components handle their scroll bars in a slightly different way. The rich edit component can hide its scroll bars if the text fits inside the bounds of the component. The memo always shows scroll bars if they are enabled.

Adding the Clipboard object

Most text handling applications provide users with a way to move selected text between documents, including documents in different applications. The Clipboard object in Delphi encapsulates the Windows Clipboard and includes methods for cutting, copying, and pasting text (and other formats, including graphics). The Clipboard object is declared in the Clipbrd unit. To add the Clipboard object to an application, 1 Select the unit that will use the Clipboard. 2 Search for the implementation reserved word. 3 Add Clipbrd to the uses clause below implementation. • If there is already a uses clause in the implementation part, add Clipbrd to the end of it. • If there is not already a uses clause, add one that says uses Clipbrd; For example, in an application with a child window, the uses clause in the unit’s implementation part might look like this: uses MDIFrame, Clipbrd;

Selecting text

Before you can send any text to the Clipboard, that text must be selected. Highlighting of selected text is built into the edit components. When the user selects text, it appears highlighted. Workingwithcontrols6-9, WorkingwithtextincontrolsTable 6.1 lists properties commonly used to handle selected text. Table 6.1 Properties of selected text Property Description SelText Contains a string representing the selected text in the component. SelLength Contains the length of a selected string. SelStart Contains the starting position of a string. SelLength is used in the example in the section “Disabling menu items” on page 6-11.

Selecting all text

The SelectAll method selects the entire contents of the rich edit or memo component. This is especially useful when the component’s contents exceed the visible area of the component. In most other cases, users select text with either keystrokes or mouse dragging. To select the entire contents of a rich edit or memo control, call the RichEdit1 control’s SelectAll method. For example, procedure TMainForm.SelectAll(Sender: TObject); begin RichEdit1.SelectAll; { select all text in RichEdit } end;

Cutting, copying, and pasting text

Delphi applications that use the Clipbrd unit can cut, copy and paste text, graphics, and objects both internally and in other applications through the Windows Clipboard. The edit components that encapsulate the standard Windows text- handling controls all have methods built into them for interacting with the Clipboard. (See Chapter 4 for information on using the Clipboard with graphics.) To cut, copy, or paste text with the Clipboard, call the edit component’s CutToClipboard, CopyToClipboard, and PasteFromClipboard methods, respectively. For example, the following code attaches event handlers to the OnClick events of the Edit|Cut, Edit|Copy, and Edit|Paste commands, respectively: procedure TEditForm.CutToClipboard(Sender: TObject); begin Editor.CutToClipboard; end; procedure TEditForm.CopyToClipboard(Sender: TObject); begin Editor.CopyToClipboard; end; 6-10Developer’ sGuide, Workingwithtextincontrolsprocedure TEditForm.PasteFromClipboard(Sender: TObject); begin Editor.PasteFromClipboard; end;

Deleting selected text

Selected text is often used with the Clipboard, but you can also write code to delete the selected text in an edit component without cutting it to the Clipboard. To delete the selected text from an edit component, call the editor’s ClearSelection method. For example, if you have a Delete item on the Edit menu and a memo control named RichEdit1, your code would look something like this: procedure TEditForm.Delete(Sender: TObject); begin Editor.ClearSelection; end;

Disabling menu items

Often in an application you want to disable menu commands that are not immediately applicable, but you do not want to remove the command from the menu altogether. For example, in a text editor, if there is no text currently selected, the Cut, Copy, and Delete items on the Edit menu should appear dimmed. Disabling the item lets the user know there is such a command, but that it does not currently apply. An appropriate time to enable or disable items on a particular menu is when the user clicks the item at the top of the menu. That way, before displaying the items, your application can make the correct items available. (This event is also fired prior to evaluating menu shortcut keystrokes.) To disable a menu item, set its Enabled property to False. In the following code example, an event handler is attached to the OnClick event for the Edit item on a child form’s menu bar. It sets Enabled for the Cut, Copy, and Delete menu items on the Edit menu based on whether RichEdit1 has selected text. The Paste command is enabled or disabled based on whether any text exists on the Clipboard. procedure TEditForm.Edit1Click(Sender: TObject); var HasSelection: Boolean; { declare a temporary variable } begin Paste1.Enabled := Clipboard.HasFormat(CF_TEXT); {enable or disable the Paste menu item} HasSelection := Editor.SelLength > 0; { True if text is selected } Cut1.Enabled := HasSelection; { enable menu items if HasSelection is True } Copy1.Enabled := HasSelection; Delete1.Enabled := HasSelection; end; Workingwithcontrols6-11, WorkingwithtextincontrolsThe HasFormat method of the Clipboard returns a Boolean value based on whether the Clipboard contains objects, text, or images of a particular format. By calling HasFormat with the parameter CF_TEXT, you can determine whether the Clipboard contains any text, and enable or disable the Paste item as appropriate. Chapter 7, “Working with graphics” provides more information about using the Clipboard with graphics.

Providing a pop-up menu

Pop-up, or local, menus are a common ease-of-use feature for any application. They enable users to minimize mouse movement by clicking the right mouse button in the application workspace to access a list of frequently used commands. In a text editor application, for example, you can add a pop-up menu that repeats the Cut, Copy, and Paste editing commands. These pop-up menu items can use the same event handlers as the corresponding items on the Edit menu. You don’t need to create accelerator or shortcut keys for pop-up menus because the corresponding regular menu items generally already have shortcuts. A form’s PopupMenu property specifies what pop-up menu to display when a user right-clicks any item on the form. Individual controls also have PopupMenu properties that can override the form’s property, allowing customized menus for particular controls. To add a pop-up menu to a form, 1 Place a pop-up menu component on the form. 2 Use the Menu Designer to define the items for the pop-up menu. 3 Set the PopupMenu property of the form or control that displays the menu to the name of the pop-up menu component. 4 Attach handlers to the OnClick events of the pop-up menu items.

Handling the OnPopup event

You may want to adjust pop-up menu items before displaying the menu, just as you may want to enable or disable items on a regular menu. With a regular menu, you can handle the OnClick event for the item at the top of the menu, as described in “Disabling menu items” on page 6-11. With a pop-up menu, however, there is no top-level menu bar, so to prepare the pop- up menu commands, you handle the event in the menu component itself. The pop-up menu component provides an event just for this purpose, called OnPopup. To adjust menu items on a pop-up menu before displaying them, 1 Select the pop-up menu component. 2 Attach an event handler to its OnPopup event. 3 Write code in the event handler to enable, disable, hide, or show menu items. 6-12Developer’ sGuide, AddinggraphicstocontrolsIn the following code, the EditEditClick event handler described previously in “Disabling menu items” on page 6-11 is attached to the pop-up menu component’s OnPopup event. A line of code is added to EditEditClick for each item in the pop-up menu. procedure TEditForm.Edit1Click(Sender: TObject); var HasSelection: Boolean; begin Paste1.Enabled := Clipboard.HasFormat(CF_TEXT); Paste2.Enabled := Paste1.Enabled;{Add this line} HasSelection := Editor.SelLength <> 0; Cut1.Enabled := HasSelection; Cut2.Enabled := HasSelection;{Add this line} Copy1.Enabled := HasSelection; Copy2.Enabled := HasSelection;{Add this line} Delete1.Enabled := HasSelection; end;

Adding graphics to controls

Windows list-box, combo-box, and menu controls have a style available called “owner draw,” which means that instead of using Windows’ standard method of drawing text for each item in the control, the control’s owner (generally, the form) draws each item at runtime. The most common use for owner-draw controls is to provide graphics instead of, or in addition to, text for items. For information on using owner-draw to add images to menus, see “Adding images to menu items” on page 5-18. All owner-draw controls contain lists of items. By default, those lists are lists of strings, which Windows displays as text. You can associate an object with each item in a list to make it easy to use that object when drawing items. In general, creating an owner-draw control in Delphi involves these steps: 1 Setting the owner-draw style 2 Adding graphical objects to a string list 3 Drawing owner-drawn items

Setting the owner-draw style

Both list boxes and combo boxes have a property called Style. Style determines whether the control uses the default drawing (called the “standard” style) or owner drawing. Grids use a property called DefaultDrawing to enable or disable the default drawing. List boxes and combo boxes have additional owner-draw styles, called fixed and variable, as Table 6.2 describes. Owner-draw grids are always fixed: although the size of each row and column might vary, the size of each cell is determined before drawing the grid. Workingwithcontrols6-13, AddinggraphicstocontrolsTable 6.2 Fixed vs. variable owner-draw styles Owner-draw style Meaning Examples Fixed Each item is the same height, with that height lbOwnerDrawFixed, determined by the ItemHeight property. csOwnerDrawFixed Variable Each item might have a different height, lbOwnerDrawVariable, determined by the data at runtime. csOwnerDrawVariable

Adding graphical objects to a string list

Every Delphi string list has the ability to hold a list of objects in addition to its list of strings. For example, in a file manager application, you may want to add bitmaps indicating the type of drive along with the letter of the drive. To do that, you need to add the bitmap images to the application, then copy those images into the proper places in the string list as described in the following sections. Adding images to an application An image control is a nonvisual control that contains a graphical image, such as a bitmap. You use image controls to display graphical images on a form. You can also use them to hold hidden images that you’ll use in your application. For example, you can store bitmaps for owner-draw controls in hidden image controls, like this: 1 Add image controls to the main form. 2 Set their Name properties. 3 Set the Visible property for each image control to false. 4 Set the Picture property of each image to the desired bitmap using the Picture editor from the Object Inspector. The image controls are invisible when you run the application. Adding images to a string list Once you have graphical images in an application, you can associate them with the strings in a string list. You can either add the objects at the same time as the strings, or associate objects with existing strings. The preferred method is to add objects and strings at the same time, if all the needed data is available. The following example shows how you might want to add images to a string list. This is part of a file manager application where, along with a letter for each valid drive, it adds a bitmap indicating each drive’s type. The OnCreate event handler looks like this: 6-14Developer’ sGuide, Addinggraphicstocontrolsprocedure TFMForm.FormCreate(Sender: TObject); var Drive: Char; AddedIndex: Integer; begin for Drive := 'A' to 'Z' do { iterate through all possible drives } begin case GetDriveType(Drive + ':/') of { positive calues mean valid drives } DRIVE_REMOVABLE: { add a tab } AddedIndex := DriveTabSet.Tabs.AddObject(Drive, Floppy.Picture.Graphic); DRIVE_FIXED: { add a tab } AddedIndex := DriveTabSet.Tabs.AddObject(Drive, Fixed.Picture.Graphic); DRIVE_REMOTE: { add a tab } AddedIndex := DriveTabSet.Tabs.AddObject(Drive, Network.Picture.Graphic); end; if UpCase(Drive) = UpCase(DirectoryOutline.Drive) then { current drive? } DriveTabSet.TabIndex := AddedIndex; { then make that current tab } end; end;

Drawing owner-drawn items

When you set a control’s style to owner draw, Windows no longer draws the control on the screen. Instead, it generates events for each visible item in the control. Your application handles the events to draw the items. To draw the items in an owner-draw control, do the following for each visible item in the control. Use a single event handler for all items. 1 Size the item, if needed. Items of the same size (for example, with a list box style of lsOwnerDrawFixed), do not require sizing. 2 Draw the item.

Sizing owner-draw items

Before giving your application the chance to draw each item in a variable owner- draw control, Windows generates a measure-item event. The measure-item event tells the application where the item appears on the control. Windows determines the size the item (generally, it is just large enough to display the item’s text in the current font). Your application can handle the event and change the rectangle Windows chose. For example, if you plan to substitute a bitmap for the item’s text, change the rectangle to be the size of the bitmap. If you want a bitmap and text, adjust the rectangle to be big enough for both. Workingwithcontrols6-15, AddinggraphicstocontrolsTo change the size of an owner-draw item, attach an event handler to the measure- item event in the owner-draw control. Depending on the control, the name of the event can vary. List boxes and combo boxes use OnMeasureItem. Grids have no measure-item event. The sizing event has two important parameters: the index number of the item and the size of that item. The size is variable: the application can make it either smaller or larger. The positions of subsequent items depend on the size of preceding items. For example, in a variable owner-draw list box, if the application sets the height of the first item to five pixels, the second item starts at the sixth pixel down from the top, and so on. In list boxes and combo boxes, the only aspect of the item the application can alter is the height of the item. The width of the item is always the width of the control. Note Owner-draw grids cannot change the sizes of their cells as they draw. The size of each row and column is set before drawing by the ColWidths and RowHeights properties. The following code, attached to the OnMeasureItem event of an owner-draw list box, increases the height of each list item to accommodate its associated bitmap: procedure TFMForm.DriveTabSetMeasureTab(Sender: TObject; Index: Integer; var TabWidth: Integer); { note that TabWidth is a var parameter} var BitmapWidth: Integer; begin BitmapWidth := TBitmap(DriveTabSet.Tabs.Objects[Index]).Width; { increase tab width by the width of the associated bitmap plus two } Inc(TabWidth, 2 + BitmapWidth); end; Note You must typecast the items from the Objects property in the string list. Objects is a property of type TObject so that it can hold any kind of object. When you retrieve objects from the array, you need to typecast them back to the actual type of the items.

Drawing each owner-draw item

When an application needs to draw or redraw an owner-draw control, Windows generates draw-item events for each visible item in the control. To draw each item in an owner-draw control, attach an event handler to the draw- item event for that control. The names of events for owner drawing always start with OnDraw, such as OnDrawItem or OnDrawCell. The draw-item event contains parameters indicating the index of the item to draw, the rectangle in which to draw, and usually some information about the state of the item (such as whether the item has focus). The application handles each event by rendering the appropriate item in the given rectangle. 6-16Developer’ sGuide, AddinggraphicstocontrolsFor example, the following code shows how to draw items in a list box that has bitmaps associated with each string. It attaches this handler to the OnDrawItem event for the list box: procedure TFMForm.DriveTabSetDrawTab(Sender: TObject; TabCanvas: TCanvas; R: TRect; Index: Integer; Selected: Boolean); var Bitmap: TBitmap; begin Bitmap := TBitmap(DriveTabSet.Tabs.Objects[Index]); with TabCanvas do begin Draw(R.Left, R.Top + 4, Bitmap); { draw bitmap } TextOut(R.Left + 2 + Bitmap.Width, { position text } R.Top + 2, DriveTabSet.Tabs[Index]); { and draw it to the right of the bitmap } end; end; Workingwithcontrols6-17, 6-18Developer’ sGuide,

Chapter

Chapter 7Working with graphics You can use a variety of ways to handle graphic images in your Delphi applications: you can insert pre-drawn pictures at design time, create them using graphical controls at design time, or draw them dynamically at runtime. This chapter describes how to draw graphics at runtime, either on a window or on a custom control or owner-draw control. It also provides information on creating design-time graphics using image (owner-draw) controls. This chapter provides information on working with graphics in the VCL. It covers: • Overview of graphics • Using the properties of the Canvas object • Using Canvas methods to draw graphic objects • Handling multiple drawing objects in an application • Drawing on a bitmap • Loading and saving graphics files • Using the Clipboard with graphics • Rubber banding example

Overview of graphics programming

The VCL graphics components encapsulate the Windows Graphics Device Interface (GDI), making it very easy to add graphics to your Windows programming. To draw graphics in a Delphi application, you draw on an object’s canvas, rather than directly on the object. The canvas is a property of the object, and is itself an object. A main advantage of the canvas object is that it handles resources effectively and it takes care of device context, so your programs can use the same methods regardless of whether you are drawing on the screen, to a printer, or on bitmaps or metafiles. Canvases are available only at runtime, so you do all your work with canvases by writing code. The following sections describe how to use the VCL graphics components to simplify your coding. Workingwithgraphics7-1, OverviewofgraphicsprogrammingNote Since TCanvas is a wrapper resource manager around the Windows device context, you can also use all Windows GDI functions on the canvas. The Handle property of the canvas is the device context Handle.

Common properties and methods of Canvas

Table 7.1 lists the commonly used properties of the Canvas object. For a complete list of properties and methods, see the TCanvas component in online Help. Table 7.1 Common properties of the Canvas object Properties Descriptions Font Specifies the font to use when writing text on the image. Set the properties of the TFont object to specify the font face, color, size, and style of the font. Brush Determines the color and pattern the canvas uses for filling graphical shapes and backgrounds. Set the properties of the TBrush object to specify the color and pattern or bitmap to use when filling in spaces on the canvas. Pen Specifies the kind of pen the canvas uses for drawing lines and outlining shapes. Set the properties of the TPen object to specify the color, style, width, and mode of the pen. PenPos Specifies the current drawing position of the pen. Pixels Specifies the color of the area of pixels within the current ClipRect. Table 7.2 is a list of several methods you can use: Table 7.2 Common methods of the Canvas object Method Descriptions Arc Draws an arc on the image along the perimeter of the ellipse bounded by the specified rectangle. Chord Draws a closed figure represented by the intersection of a line and an ellipse. CopyRect Copies part of an image from another canvas into the canvas. Draw Renders the graphic object specified by the Graphic parameter on the canvas at the location given by the coordinates (X, Y). Ellipse Draws the ellipse defined by a bounding rectangle on the canvas. FillRect Fills the specified rectangle on the canvas using the current brush. FloodFill Fills an area of the canvas using the current brush. FrameRect Draws a rectangle using the Brush of the canvas to draw the border. LineTo Draws a line on the canvas from PenPos to the point specified by X and Y, and sets the pen position to (X, Y). MoveTo Changes the current drawing position to the point (X,Y). Pie Draws a pie-shaped the section of the ellipse bounded by the rectangle (X1, Y1) and (X2, Y2) on the canvas. Polygon Draws a series of lines on the canvas connecting the points passed in and closing the shape by drawing a line from the last point to the first point. PolyLine Draws a series of lines on the canvas with the current pen, connecting each of the points passed to it in Points. 7-2Developer’ sGuide, OverviewofgraphicsprogrammingTable 7.2 Common methods of the Canvas object (continued) Method Descriptions Rectangle Draws a rectangle on the canvas with its upper left corner at the point (X1, Y1) and its lower right corner at the point (X2, Y2). Use Rectangle to draw a box using Pen and fill it using Brush. RoundRect Draws a rectangle with rounded corners on the canvas. StretchDraw Draws a graphic on the canvas so that the image fits in the specified rectangle. The graphic image may need to change its magnitude or aspect ratio to fit. TextHeight, Returns the height and width, respectively, of a string in the current font. TextWidth Height includes leading between lines. TextOut Writes a string on the canvas, starting at the point (X,Y), and then updates the PenPos to the end of the string. TextRect Writes a string inside a region; any portions of the string that fall outside the region do not appear. When working with graphics, you often encounter the terms drawing and painting: • Drawing is the creation of a single, specific graphic element, such as a line or a shape, with code. In your code, you tell an object to draw a specific graphic in a specific place on its canvas by calling a drawing method of the canvas. • Painting is the creation of the entire appearance of an object. Painting usually involves drawing. That is, in response to OnPaint events, an object generally draws some graphics. An edit box, for example, paints itself by drawing a rectangle and then drawing some text inside. A shape control, on the other hand, paints itself by drawing a single graphic. The examples in the beginning of this chapter demonstrate how to draw various graphics, but they do so in response to OnPaint events. Later sections show how to do the same kind of drawing in response to other events.

Refreshing the screen

At certain times, Windows determines that objects onscreen need to refresh their appearance, so it generates WM_PAINT messages, which the VCL routes to OnPaint events. The VCL calls any OnPaint event handler that you have written for that object when you use the Refresh method. The default name generated for the OnPaint event handler in a form is FormPaint. You may want to use the Refresh method at times to refresh a component or form. For example, you might call Refresh in the form’s OnResize event handler to redisplay any graphics or if you want to paint a background on a form. While some operating systems automatically handle the redrawing of the client area of a window that has been invalidated, Windows does not. In the Windows operating system anything drawn on the screen is permanent. When a form or control is temporarily obscured, for example during window dragging, the form or control must repaint the obscured area when it is re-exposed. For more information about the WM_PAINT message, see the Windows online Help. Workingwithgraphics7-3, OverviewofgraphicsprogrammingIf you use the TImage control, the painting and refreshing of the graphic contained in the TImage is handled automatically by the VCL. Drawing on a TImage creates a persistent image. Consequently, you do not need to do anything to redraw the contained image. In contrast, TPaintBox’s canvas maps directly onto the screen device, so that anything drawn to the PaintBox’s canvas is transitory. This is true of nearly of controls, including the form itself. Therefore, if you draw or paint on a TPaintBox in its constructor, you will need to add that code to your OnPaint event handler in order for image to be repainted each time the client area is invalidated.

When graphic images appear in the application

How graphic images appear in your application depends on how they are drawn. If you are drawing directly onto the canvas property of a control, the picture object is displayed immediately. However, if you drawing on an offscreen image such as a TBitmap canvas, the image is not displayed until a control copies from a bitmap onto the control’s canvas. That is, when drawing bitmaps and assigning them to an image control, the image appears only when the control has an opportunity to process its OnPaint message. For details on drawing on bitmap objects, see “Drawing on a graphic” on page 7-16.

Types of graphic objects

The VCL provides the following graphic objects.as shown in Table 7.3. These objects have methods to draw on the canvas, which is described in “Using Canvas methods to draw graphic objects” on page 7-9 and load and save to graphics files, as described in “Loading and saving graphics files” on page 7-18. Table 7.3 Graphic object types Object Description Picture Used to hold any graphic image. To add additional graphic file formats, use the Picture Register method. Use this to handle arbitrary files such as displaying images in an image control. Bitmap A powerful graphics object used to create, manipulate (scale, scroll, rotate, and paint), and store images as files on a disk. Creating copies of a bitmap is fast since the handle is copied, not the image. Clipboard Represents the container for any text or graphics that are cut, copied, or pasted from or to an application. With the clipboard, you can get and retrieve data according to the appropriate format; handle reference counting, and opening and closing the Clipboard; manage and manipulate formats for objects in the Clipboard. Icon Represents the value loaded from a Windows icon file (::ICO file). Metafile Contains a metafile, which records the operations required to construct an image, rather than contain the actual bitmap pixels of the image. Metafiles are extremely scalable without the loss of image detail and often require much less memory than bitmaps, particularly for high-resolution devices, such as printers. However, metafiles do not draw as fast as bitmaps. Use a metafile when versatility or precision is more important than performance. 7-4Developer’ sGuide, UsingthepropertiesoftheCanvasobject

Using the properties of the Canvas object

With the Canvas object, you can set the properties of a pen for drawing lines, a brush for filling shapes, a font for writing text, and an array of pixels to represent the image. This section describes • Using pens • Using brushes • Reading and setting pixels

Using pens

The Pen property of a canvas controls the way lines appear, including lines drawn as the outlines of shapes. Drawing a straight line is really just changing a group of pixels that lie between two points. The pen itself has four properties you can change: Color, Width, Style, and Mode. • Color property: Changes the pen color • Width property: Changes the pen width • Style property: Changes the pen style • Mode property: Changes the pen mode The values of these properties determine how the pen changes the pixels in the line. By default, every pen starts out black, with a width of 1 pixel, a solid style, and a mode called copy that overwrites anything already on the canvas. Changing the pen color You can set the color of a pen as you would any other Color property at runtime. A pen’s color determines the color of the lines the pen draws, including lines drawn as the boundaries of shapes, as well as other lines and polylines. To change the pen color, assign a value to the Color property of the pen. To let the user choose a new color for the pen, put a color grid on the pen’s toolbar. A color grid can set both foreground and background colors. For a non-grid pen style, you must consider the background color, which is drawn in the gaps between line segments. Background color comes from the Brush color property. Since the user chooses a new color by clicking the grid, this code changes the pen’s color in response to the OnClick event: procedure TForm1.PenColorClick(Sender: TObject); begin Canvas.Pen.Color := PenColor.ForegroundColor; end; Changing the pen width A pen’s width determines the thickness, in pixels, of the lines it draws. Note When the thickness is greater than 1, Windows 95 always draw solid lines, no matter what the value of the pen’s Style property. Workingwithgraphics7-5, UsingthepropertiesoftheCanvasobjectTo change the pen width, assign a numeric value to the pen’s Width property. Suppose you have a scroll bar on the pen’s toolbar to set width values for the pen. And suppose you want to update the label next to the scroll bar to provide feedback to the user. Using the scroll bar’s position to determine the pen width, you update the pen width every time the position changes. This is how to handle the scroll bar’s OnChange event: procedure TForm1.PenWidthChange(Sender: TObject); begin Canvas.Pen.Width := PenWidth.Position;{ set the pen width directly } PenSize.Caption := IntToStr(PenWidth.Position);{ convert to string for caption } end;

Changing the pen style

A pen’s Style property allows you to set solid lines, dashed lines, dotted lines, and so on. Note Windows 95 does not support dashed or dotted line styles for pens wider than one pixel and makes all larger pens solid, no matter what style you specify. The task of setting the properties of pen is an ideal case for having different controls share same event handler to handle events. To determine which control actually got the event, you check the Sender parameter. To create one click-event handler for six pen-style buttons on a pen’s toolbar, do the following: 1 Select all six pen-style buttons and select the Object Inspector|Events|OnClick event and in the Handler column, type SetPenStyle. Delphi generates an empty click-event handler called SetPenStyle and attaches it to the OnClick events of all six buttons. 2 Fill in the click-event handler by setting the pen’s style depending on the value of Sender, which is the control that sent the click event: procedure TForm1.SetPenStyle(Sender: TObject); begin with Canvas.Pen do begin if Sender = SolidPen then Style := psSolid else if Sender = DashPen then Style := psDash else if Sender = DotPen then Style := psDot else if Sender = DashDotPen then Style := psDashDot else if Sender = DashDotDotPen then Style := psDashDotDot else if Sender = ClearPen then Style := psClear; end; end;

Changing the pen mode

A pen’s Mode property lets you specify various ways to combine the pen’s color with the color on the canvas. For example, the pen could always be black, be an inverse of 7-6Developer’ sGuide, UsingthepropertiesoftheCanvasobjectthe canvas background color, inverse of the pen color, and so on. See TPen in online Help for details. Getting the pen position The current drawing position—the position from which the pen begins drawing its next line—is called the pen position. The canvas stores its pen position in its PenPos property. Pen position affects the drawing of lines only; for shapes and text, you specify all the coordinates you need. To set the pen position, call the MoveTo method of the canvas. For example, the following code moves the pen position to the upper left corner of the canvas: Canvas.MoveTo(0, 0); Note Drawing a line with the LineTo method also moves the current position to the endpoint of the line.

Using brushes

The Brush property of a canvas controls the way you fill areas, including the interior of shapes. Filling an area with a brush is a way of changing a large number of adjacent pixels in a specified way. The brush has three properties you can manipulate: • Color property: Changes the fill color • Style property: Changes the brush style • Bitmap property: Uses a bitmap as a brush pattern The values of these properties determine the way the canvas fills shapes or other areas. By default, every brush starts out white, with a solid style and no pattern bitmap. Changing the brush color A brush’s color determines what color the canvas uses to fill shapes. To change the fill color, assign a value to the brush’s Color property. Brush is used for background color in text and line drawing so you typically set the background color property. You can set the brush color just as you do the pen color, in response to a click on a color grid on the brush’s toolbar (see “Changing the pen color” on page 7-5): procedure TForm1.BrushColorClick(Sender: TObject); begin Canvas.Brush.Color := BrushColor.ForegroundColor; end; Changing the brush style A brush style determines what pattern the canvas uses to fill shapes. It lets you specify various ways to combine the brush’s color with any colors already on the canvas. The predefined styles include solid color, no color, and various line and hatch patterns. Workingwithgraphics7-7, UsingthepropertiesoftheCanvasobjectTo change the style of a brush, set its Style property to one of the predefined values: bsSolid, bsClear, bsHorizontal, bsVertical, bsFDiagonal, bsBDiagonal, bsCross, or bsDiagCross. This example sets brush styles by sharing a click-event handler for a set of eight brush-style buttons. All eight buttons are selected, the Object Inspector|Events| OnClick is set, and the OnClick handler is named SetBrushStyle. Here is the handler code: procedure TForm1.SetBrushStyle(Sender: TObject); begin with Canvas.Brush do begin if Sender = SolidBrush then Style := bsSolid else if Sender = ClearBrush then Style := bsClear else if Sender = HorizontalBrush then Style := bsHorizontal else if Sender = VerticalBrush then Style := bsVertical else if Sender = FDiagonalBrush then Style := bsFDiagonal else if Sender = BDiagonalBrush then Style := bsBDiagonal else if Sender = CrossBrush then Style := bsCross else if Sender = DiagCrossBrush then Style := bsDiagCross; end; end;

Setting the Brush Bitmap property

A brush’s Bitmap property lets you specify a bitmap image for the brush to use as a pattern for filling shapes and other areas. The following example loads a bitmap from a file and assigns it to the Brush of the Canvas of Form1: var Bitmap: TBitmap; begin Bitmap := TBitmap.Create; try Bitmap.LoadFromFile('MyBitmap.bmp'); Form1.Canvas.Brush.Bitmap := Bitmap; Form1.Canvas.FillRect(Rect(0,0,100,100)); finally Form1.Canvas.Brush.Bitmap := nil; Bitmap.Free; end; end; Note The brush does not assume ownership of a bitmap object assigned to its Bitmap property. You must ensure that the Bitmap object remain valid for the lifetime of the Brush, and you must free the Bitmap object yourself afterwards.

Reading and setting pixels

You will notice that every canvas has an indexed Pixels property that represents the individual colored points that make up the image on the canvas. You rarely need to 7-8Developer’ sGuide, UsingCanvasmethodstodrawgraphicobjectsaccess Pixels directly, it is available only for convenience to perform small actions such as finding or setting a pixel’s color. Note Setting and getting individual pixels is thousands of times slower than performing graphics operations on regions. Do not use the Pixel array property to access the image pixels of a general array. For high-performance access to image pixels, see the TBitmap.ScanLine property.

Using Canvas methods to draw graphic objects

This section shows how to use some common methods to draw graphic objects. It covers: • Drawing lines and polylines • Drawing shapes • Drawing rounded rectangles • Drawing polygons

Drawing lines and polylines

A canvas can draw straight lines and polylines. A straight line is just a line of pixels connecting two points. A polyline is a series of straight lines, connected end-to-end. The canvas draws all lines using its pen. Drawing lines To draw a straight line on a canvas, use the LineTo method of the canvas. LineTo draws a line from the current pen position to the point you specify and makes the endpoint of the line the current position. The canvas draws the line using its pen. For example, the following method draws crossed diagonal lines across a form whenever the form is painted: procedure TForm1.FormPaint(Sender: TObject); begin with Canvas do begin MoveTo(0, 0); LineTo(ClientWidth, ClientHeight); MoveTo(0, ClientHeight); LineTo(ClientWidth, 0); end; end; Drawing polylines In addition to individual lines, the canvas can also draw polylines, which are groups of any number of connected line segments. To draw a polyline on a canvas, call the Polyline method of the canvas. Workingwithgraphics7-9, UsingCanvasmethodstodrawgraphicobjectsThe parameter passed to the PolyLine method is an array of points. You can think of a polyline as performing a MoveTo on the first point and LineTo on each successive point. For drawing multiple lines, Polyline is faster than using the MoveTo method and the LineTo method because it eliminates a lot of call overhead. The following method, for example, draws a rhombus in a form: procedure TForm1.FormPaint(Sender: TObject); begin with Canvas do PolyLine([Point(0, 0), Point(50, 0), Point(75, 50), Point(25, 50), Point(0, 0)]); end; This example takes advantage of Delphi’s ability to create an open-array parameter on-the-fly. You can pass any array of points, but an easy way to construct an array quickly is to put its elements in brackets and pass the whole thing as a parameter. For more information, see online Help.

Drawing shapes

Canvases have methods for drawing different kinds of shapes. The canvas draws the outline of a shape with its pen, then fills the interior with its brush. The line that forms the border for the shape is controlled by the current Pen object. This section covers: • Drawing rectangles and ellipses • Drawing rounded rectangles • Drawing polygons Drawing rectangles and ellipses To draw a rectangle or ellipse on a canvas, call the canvas’s Rectangle method or Ellipse method, passing the coordinates of a bounding rectangle. The Rectangle method draws the bounding rectangle; Ellipse draws an ellipse that touches all sides of the rectangle. The following method draws a rectangle filling a form’s upper left quadrant, then draws an ellipse in the same area: procedure TForm1.FormPaint(Sender: TObject); begin Canvas.Rectangle(0, 0, ClientWidth div 2, ClientHeight div 2); Canvas.Ellipse(0, 0, ClientWidth div 2, ClientHeight div 2); end; Drawing rounded rectangles To draw a rounded rectangle on a canvas, call the canvas’s RoundRect method. The first four parameters passed to RoundRect are a bounding rectangle, just as for the Rectangle method or the Ellipse method. RoundRect takes two more parameters that indicate how to draw the rounded corners. 7-10Developer’ sGuide, HandlingmultipledrawingobjectsinyourapplicationThe following method, for example, draws a rounded rectangle in a form’s upper left quadrant, rounding the corners as sections of a circle with a diameter of 10 pixels: procedure TForm1.FormPaint(Sender: TObject); begin Canvas.RoundRect(0, 0, ClientWidth div 2, ClientHeight div 2, 10, 10); end; Drawing polygons To draw a polygon with any number of sides on a canvas, call the Polygon method of the canvas. Polygon takes an array of points as its only parameter and connects the points with the pen, then connects the last point to the first to close the polygon. After drawing the lines, Polygon uses the brush to fill the area inside the polygon. For example, the following code draws a right triangle in the lower left half of a form: procedure TForm1.FormPaint(Sender: TObject); begin Canvas.Polygon([Point(0, 0), Point(0, ClientHeight), Point(ClientWidth, ClientHeight)]); end;

Handling multiple drawing objects in your application

Various drawing methods (rectangle, shape, line, and so on) are typically available on the toolbar and button panel. Applications can respond to clicks on speed buttons to set the desired drawing objects. This section describes how to: • Keep track of which drawing tool to use • Changing the tool with speed buttons • Using drawing tools

Keeping track of which drawing tool to use

A graphics program needs to keep track of what kind of drawing tool (such as a line, rectangle, ellipse, or rounded rectangle) a user might want to use at any given time. You could assign numbers to each kind of tool, but then you would have to remember what each number stands for. You can do that more easily by assigning mnemonic constant names to each number, but your code won’t be able to distinguish which numbers are in the proper range and of the right type. Fortunately, Object Pascal provides a means to handle both of these shortcomings. You can declare an enumerated type. An enumerated type is really just a shorthand way of assigning sequential values to constants. Since it’s also a type declaration, you can use Object Pascal’s type-checking to ensure that you assign only those specific values. Workingwithgraphics7-11, HandlingmultipledrawingobjectsinyourapplicationTo declare an enumerated type, use the reserved work type, followed by an identifier for the type, then an equal sign, and the identifiers for the values in the type in parentheses, separated by commas. For example, the following code declares an enumerated type for each drawing tool available in a graphics application: type TDrawingTool = (dtLine, dtRectangle, dtEllipse, dtRoundRect); By convention, type identifiers begin with the letter T, and groups of similar constants (such as those making up an enumerated type) begin with a 2-letter prefix (such as dt for “drawing tool”). The declaration of the TDrawingTool type is equivalent to declaring a group of constants: const dtLine = 0; dtRectangle = 1; dtEllipse = 2; dtRoundRect = 3; The main difference is that by declaring the enumerated type, you give the constants not just a value, but also a type, which enables you to use Object Pascal’s type- checking to prevent many errors. A variable of type TDrawingTool can be assigned only one of the constants dtLine..dtRoundRect. Attempting to assign some other number (even one in the range 0..3) generates a compile-time error. In the following code, a field added to a form keeps track of the form’s drawing tool: type TDrawingTool = (dtLine, dtRectangle, dtEllipse, dtRoundRect); TForm1 = class(TForm) ...{ method declarations } public Drawing: Boolean; Origin, MovePt: TPoint; DrawingTool: TDrawingTool;{ field to hold current tool } end;

Changing the tool with speed buttons

Each drawing tool needs an associated OnClick event handler. Suppose your application had a toolbar button for each of four drawing tools: line, rectangle, ellipse, and rounded rectangle. You would attach the following event handlers to the OnClick events of the four drawing-tool buttons, setting DrawingTool to the appropriate value for each: procedure TForm1.LineButtonClick(Sender: TObject);{ LineButton } begin DrawingTool := dtLine; end; 7-12Developer’ sGuide, Handlingmultipledrawingobjectsinyourapplicationprocedure TForm1.RectangleButtonClick(Sender: TObject);{ RectangleButton } begin DrawingTool := dtRectangle; end; procedure TForm1.EllipseButtonClick(Sender: TObject);{ EllipseButton } begin DrawingTool := dtEllipse; end; procedure TForm1.RoundedRectButtonClick(Sender: TObject);{ RoundRectButton } begin DrawingTool := dtRoundRect; end;

Using drawing tools

Now that you can tell what tool to use, you must indicate how to draw the different shapes. The only methods that perform any drawing are the mouse-move and mouse-up handlers, and the only drawing code draws lines, no matter what tool is selected. To use different drawing tools, your code needs to specify how to draw, based on the selected tool. You add this instruction to each tool’s event handler. This section describes • Drawing shapes • Sharing code among event handlers

Drawing shapes

Drawing shapes is just as easy as drawing lines: Each one takes a single statement; you just need the coordinates. Here’s a rewrite of the OnMouseUp event handler that draws shapes for all four tools: procedure TForm1.FormMouseUp(Sender: TObject); begin case DrawingTool of dtLine: begin Canvas.MoveTo(Origin.X, Origin.Y); Canvas.LineTo(X, Y) end; dtRectangle: Canvas.Rectangle(Origin.X, Origin.Y, X, Y); dtEllipse: Canvas.Ellipse(Origin.X, Origin.Y, X, Y); dtRoundRect: Canvas.RoundRect(Origin.X, Origin.Y, X, Y, (Origin.X - X) div 2, (Origin.Y - Y) div 2); end; Drawing := False; end; Workingwithgraphics7-13, HandlingmultipledrawingobjectsinyourapplicationOf course, you also need to update the OnMouseMove handler to draw shapes: procedure TForm1.FormMouseMove(Sender: TObject; Shift: TShiftState; X, Y: Integer); begin if Drawing then begin Canvas.Pen.Mode := pmNotXor; case DrawingTool of dtLine: begin Canvas.MoveTo(Origin.X, Origin.Y); Canvas.LineTo(MovePt.X, MovePt.Y); Canvas.MoveTo(Origin.X, Origin.Y); Canvas.LineTo(X, Y); end; dtRectangle: begin Canvas.Rectangle(Origin.X, Origin.Y, MovePt.X, MovePt.Y); Canvas.Rectangle(Origin.X, Origin.Y, X, Y); end; dtEllipse: begin Canvas.Ellipse(Origin.X, Origin.Y, X, Y); Canvas.Ellipse(Origin.X, Origin.Y, X, Y); end; dtRoundRect: begin Canvas.RoundRect(Origin.X, Origin.Y, X, Y, (Origin.X - X) div 2, (Origin.Y - Y) div 2); Canvas.RoundRect(Origin.X, Origin.Y, X, Y, (Origin.X - X) div 2, (Origin.Y - Y) div 2); end; end; MovePt := Point(X, Y); end; Canvas.Pen.Mode := pmCopy; end; Typically, all the repetitious code that is in the above example would be in a separate routine. The next section shows all the shape-drawing code in a single routine that all mouse-event handlers can call.

Sharing code among event handlers

Any time you find that many your event handlers use the same code, you can make your application more efficient by moving the repeated code into a routine that all event handlers can share. To add a method to a form, 1 Add the method declaration to the form object. You can add the declaration in either the public or private parts at the end of the form object’s declaration. If the code is just sharing the details of handling some events, it’s probably safest to make the shared method private. 2 Write the method implementation in the implementation part of the form unit. The header for the method implementation must match the declaration exactly, with the same parameters in the same order. 7-14Developer’ sGuide, HandlingmultipledrawingobjectsinyourapplicationThe following code adds a method to the form called DrawShape and calls it from each of the handlers. First, the declaration of DrawShape is added to the form object’s declaration: type TForm1 = class(TForm) ...{ fields and methods declared here} public { Public declarations } procedure DrawShape(TopLeft, BottomRight: TPoint; AMode: TPenMode); end; Then, the implementation of DrawShape is written in the implementation part of the unit: implementation {$R *.FRM} ...{ other method implementations omitted for brevity } procedure TForm1.DrawShape(TopLeft, BottomRight: TPoint; AMode: TPenMode); begin with Canvas do begin Pen.Mode := AMode; case DrawingTool of dtLine: begin MoveTo(TopLeft.X, TopLeft.Y); LineTo(BottomRight.X, BottomRight.Y); end; dtRectangle: Rectangle(TopLeft.X, TopLeft.Y, BottomRight.X, BottomRight.Y); dtEllipse: Ellipse(TopLeft.X, TopLeft.Y, BottomRight.X, BottomRight.Y); dtRoundRect: RoundRect(TopLeft.X, TopLeft.Y, BottomRight.X, BottomRight.Y, (TopLeft.X - BottomRight.X) div 2, (TopLeft.Y - BottomRight.Y) div 2); end; end; end; The other event handlers are modified to call DrawShape. procedure TForm1.FormMouseUp(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer); begin DrawShape(Origin, Point(X, Y), pmCopy);{ draw the final shape } Drawing := False; end; procedure TForm1.FormMouseMove(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer); begin if Drawing then begin DrawShape(Origin, MovePt, pmNotXor);{ erase the previous shape } MovePt := Point(X, Y);{ record the current point } DrawShape(Origin, MovePt, pmNotXor);{ draw the current shape } end; end; Workingwithgraphics7-15, Drawingonagraphic

Drawing on a graphic

You don’t need any components to manipulate your application’s graphic objects. You can construct, draw on, save, and destroy graphic objects without ever drawing anything on screen. In fact, your applications rarely draw directly on a form. More often, an application operates on graphics and then uses a VCL image control component to display the graphic on a form. Once you move the application’s drawing to the graphic in the image control, it is easy to add printing, Clipboard, and loading and saving operations for any graphic objects. graphic objects can be bitmap files, metafiles, icons or whatever other graphics classes that have been installed such as JPEG graphics. Note Because you are drawing on an offscreen image such as a TBitmap canvas, the image is not displayed until a control copies from a bitmap onto the control’s canvas. That is, when drawing bitmaps and assigning them to an image control, the image appears only when the control has an opportunity to process its paint message. Contrastly, if you are drawing directly onto the canvas property of a control, the picture object is displayed immediately.

Making scrollable graphics

The graphic need not be the same size as the form: it can be either smaller or larger. By adding a scroll box control to the form and placing the graphic image inside it, you can display graphics that are much larger than the form or even larger than the screen. To add a scrollable graphic first you add a TScrollbox component and then you add the image control.

Adding an image control

An image control is a container component that allows you to display your bitmap objects. You use an image control to hold a bitmap that is not necessarily displayed all the time, or which an application needs to use to generate other pictures. “Graphic display” on page 2-34 shows how to use graphics in controls. Placing the control You can place an image control anywhere on a form. If you take advantage of the image control’s ability to size itself to its picture, you need to set the top left corner only. If the image control is a nonvisible holder for a bitmap, you can place it anywhere, just as you would a nonvisual component. If you drop the image control on a scroll box already aligned to the form’s client area, this assures that the scroll box adds any scroll bars necessary to access offscreen portions of the image’s picture. Then set the image control’s properties. 7-16Developer’ sGuide, DrawingonagraphicSetting the initial bitmap size When you place an image control, it is simply a container. However, you can set the image control’s Picture property at design time to contain a static graphic. The control can also load its picture from a file at runtime, as described in “Loading and saving graphics files” on page 7-18. To create a blank bitmap when the application starts, 1 Attach a handler to the OnCreate event for the form that contains the image. 2 Create a bitmap object, and assign it to the image control’s Picture.Graphic property. In this example, the image is in the application’s main form, Form1, so the code attaches a handler to Form1’s OnCreate event: procedure TForm1.FormCreate(Sender: TObject); var Bitmap: TBitmap;{ temporary variable to hold the bitmap } begin Bitmap := TBitmap.Create;{ construct the bitmap object } Bitmap.Width := 200;{ assign the initial width... } Bitmap.Height := 200;{ ...and the initial height } Image.Picture.Graphic := Bitmap;{ assign the bitmap to the image control } end; Assigning the bitmap to the picture’s Graphic property gives ownership of the bitmap to the picture object. The picture object destroys the bitmap when it finishes with it, so you should not destroy the bitmap object. You can assign a different bitmap to the picture (see “Replacing the picture” on page 7-19), at which point the picture disposes of the old bitmap and assumes ownership of the new one. If you run the application now, you see that client area of the form has a white region, representing the bitmap. If you size the window so that the client area cannot display the entire image, you’ll see that the scroll box automatically shows scroll bars to allow display of the rest of the image. But if you try to draw on the image, you don’t get any graphics, because the application is still drawing on the form, which is now behind the image and the scroll box. Drawing on the bitmap To draw on a bitmap, use the image control’s canvas and attach the mouse-event handlers to the appropriate events in the image control. Typically you would use region operations (fills, rectangles, polylines, and so on). These are fast and efficient methods of drawing. An efficient way to draw images when you need to access individual pixels is to use the bitmap ScanLine property. For general-purpose usage, you can set up the bitmap pixel format to 24 bits and then treat the pointer returned from ScanLine as an array of RGB. Otherwise, you will need to know the native format of the ScanLine property. Workingwithgraphics7-17, LoadingandsavinggraphicsfilesThis example shows how to use ScanLine to get pixels one line at a time. procedure TForm1.Button1Click(Sender: TObject); // This example shows drawing directly to the BitMap var x,y : integer; BitMap : TBitMap; P : PByteArray; begin BitMap := TBitMap.create; try BitMap.LoadFromFile('C:\Program Files\Borland\Delphi 3\Images\Splash\256color\ factory.bmp'); for y := 0 to BitMap.height -1 do begin P := BitMap.ScanLine[y]; for x := 0 to BitMap.width -1 do P[x] := y; end; canvas.draw(0,0,BitMap); finally BitMap.free; end; end;

Loading and saving graphics files

Graphic images that exist only for the duration of one running of an application are of very limited value. Often, you either want to use the same picture every time, or you want to save a created picture for later use. The VCL’s image control makes it easy to load pictures from a file and save them again. The VCL components you use to load, save, and replace graphic images support many graphic formats including bitmap files, metafiles, glyphs, and so on. They also support installable graphic classes. The way to load and save graphics files is the similar to any other files and is described in the following sections: • Loading a picture from a file • Saving a picture to a file • Replacing the picture

Loading a picture from a file

Your application should provide the ability to load a picture from a file if your application needs to modify the picture or if you want to store the picture outside the application so a person or another application can modify the picture. To load a graphics file into an image control, call the LoadFromFile method of the image control’s Picture object. 7-18Developer’ sGuide, LoadingandsavinggraphicsfilesThe following code gets a file name from an open-file dialog box, and then loads that file into an image control named Image: procedure TForm1.Open1Click(Sender: TObject); begin if OpenDialog1.Execute then begin CurrentFile := OpenDialog1.FileName; Image.Picture.LoadFromFile(CurrentFile); end; end;

Saving a picture to a file

The VCL picture object can load and save graphics in several formats, and you can create and register your own graphic-file formats so that picture objects can load and store them as well. To save the contents of an image control in a file, call the SaveToFile method of the image control’s Picture object. The SaveToFile method requires the name of a file in which to save. If the picture is newly created, it might not have a file name, or a user might want to save an existing picture in a different file. In either case, the application needs to get a file name from the user before saving, as shown in the next section. The following pair of event handlers, attached to the File|Save and File|Save As menu items, respectively, handle the resaving of named files, saving of unnamed files, and saving existing files under new names. procedure TForm1.Save1Click(Sender: TObject); begin if CurrentFile <> '' then Image.Picture.SaveToFile(CurrentFile){ save if already named } else SaveAs1Click(Sender);{ otherwise get a name } end; procedure TForm1.Saveas1Click(Sender: TObject); begin if SaveDialog1.Execute then{ get a file name } begin CurrentFile := SaveDialog1.FileName;{ save the user-specified name } Save1Click(Sender);{ then save normally } end; end;

Replacing the picture

You can replace the picture in an image control at any time. If you assign a new graphic to a picture that already has a graphic, the new graphic replaces the existing one. To replace the picture in an image control, assign a new graphic to the image control’s Picture object. Workingwithgraphics7-19, UsingtheClipboardwithgraphicsCreating the new graphic is the same process you used to create the initial graphic (see “Setting the initial bitmap size” on page 7-17), but you should also provide a way for the user to choose a size other than the default size used for the initial graphic. An easy way to provide that option is to present a dialog box, such as the one in Figure 7.1. Figure 7.1 Bitmap-dimension dialog box from the BMPDlg unit. WidthEdit HeightEdit This particular dialog box is created in the BMPDlg unit included with the GraphEx project (in the EXAMPLES\DOC\GRAPHEX directory). With such a dialog box in your project, add it to the uses clause in the unit for your main form. You can then attach an event handler to the File|New menu item’s OnClick event. Here’s an example: procedure TForm1.New1Click(Sender: TObject); var Bitmap: TBitmap;{ temporary variable for the new bitmap } begin with NewBMPForm do begin ActiveControl := WidthEdit;{ make sure focus is on width field } WidthEdit.Text := IntToStr(Image.Picture.Graphic.Width);{ use current dimensions... } HeightEdit.Text := IntToStr(Image.Picture.Graphic.Height);{ ...as default } if ShowModal <> idCancel then{ continue if user doesn't cancel dialog box } begin Bitmap := TBitmap.Create;{ create fresh bitmap object } Bitmap.Width := StrToInt(WidthEdit.Text);{ use specified width } Bitmap.Height := StrToInt(HeightEdit.Text);{ use specified height } Image.Picture.Graphic := Bitmap;{ replace graphic with new bitmap } CurrentFile := '';{ indicate unnamed file } end; end; end; Note Assigning a new bitmap to the picture object’s Graphic property causes the picture object to destroy the existing bitmap and take ownership of the new one. The VCL handles the details of freeing the resources associated with the previous bitmap automatically.

Using the Clipboard with graphics

You can use the Windows Clipboard to copy and paste graphics within your applications or to exchange graphics with other applications. The VCL’s Clipboard object makes it easy to handle different kinds of information, including graphics. 7-20Developer’ sGuide, UsingtheClipboardwithgraphicsBefore you can use the Clipboard object in your application, you must add the Clipbrd unit to the uses clause of any unit that needs to access Clipboard data.

Copying graphics to the Clipboard

You can copy any picture, including the contents of image controls, to the Clipboard. Once on the Clipboard, the picture is available to all Windows applications. To copy a picture to the Clipboard, assign the picture to the Clipboard object using the Assign method. This code shows how to copy the picture from an image control named Image to the Clipboard in response to a click on an Edit|Copy menu item:

Cutting graphics to the Clipboard

Cutting a graphic to the Clipboard is exactly like copying it, but you also erase the graphic from the source. To cut a graphic from a picture to the Clipboard, first copy it to the Clipboard, then erase the original. In most cases, the only issue with cutting is how to show that the original image is erased. Setting the area to white is a common solution, as shown in the following code that attaches an event handler to the OnClick event of the Edit|Cut menu item: procedure TForm1.Cut1Click(Sender: TObject); var ARect: TRect; begin Copy1Click(Sender);{ copy picture to Clipboard } with Image.Canvas do begin CopyMode := cmWhiteness;{ copy everything as white } ARect := Rect(0, 0, Image.Width, Image.Height);{ get bitmap rectangle } CopyRect(ARect, Image.Canvas, ARect);{ copy bitmap over itself } CopyMode := cmSrcCopy;{ restore normal mode } end; end;

Pasting graphics from the Clipboard

If the Windows Clipboard contains a bitmapped graphic, you can paste it into any image object, including image controls and the surface of a form. To paste a graphic from the Clipboard, 1 Call the Clipboard’s HasFormat method to see whether the Clipboard contains a graphic. HasFormat is a Boolean function. It returns True if the Clipboard contains an item of the type specified in the parameter. To test for graphics, you pass CF_BITMAP. 2 Assign the Clipboard to the destination. Workingwithgraphics7-21, RubberbandingexampleThis code shows how to paste a picture from the Clipboard into an image control in response to a click on an Edit|Paste menu item: procedure TForm1.PasteButtonClick(Sender: TObject); var Bitmap: TBitmap; begin if Clipboard.HasFormat(CF_BITMAP) then { is there a bitmap on the Clipboard? ) begin Image.Picture.Bitmap.Assign(Clipboard); end; end; The graphic on the Clipboard could come from this application, or it could have been copied from another application, such as Windows Paintbrush. You do not need to check the clipboard format in this case because the paste menu should be disabled when the clipboard does not contain a supported format.

Rubber banding example

This section walks you through the details of implementing the “rubber banding” effect in an graphics application that tracks mouse movements as the user draws a graphic at runtime. The example code in this section is taken from a sample application located in the EXAMPLES\DOC\GRAPHEX directory. The application draws lines and shapes on a window’s canvas in response to clicks and drags: pressing a mouse button starts drawing, and releasing the button ends the drawing. To start with, the example code shows how to draw on the surface of the main form. Later examples demonstrate drawing on a bitmap. This section covers: • Responding to the mouse • Adding a field to a form object to track mouse actions • Refining line drawing

Responding to the mouse

Your application can respond to the mouse actions: mouse-button down, mouse moved, and mouse-button up. It can also respond to a click (a complete press-and- release, all in one place) that can be generated by some kinds of keystrokes (such as pressing Enter in a modal dialog box). This section covers: • What’s in a mouse event • Responding to a mouse-down action • Responding to a mouse-up action • Responding to a mouse move 7-22Developer’ sGuide, Rubberbandingexample

What’s in a mouse event?

The VCL has three mouse events: OnMouseDown event, OnMouseMove event, and OnMouseUp event. When a VCL application detects a mouse action, it calls whatever event handler you’ve defined for the corresponding event, passing five parameters. Use the information in those parameters to customize your responses to the events. The five parameters are as follows: Table 7.4 Mouse-event parameters Parameter Meaning Sender The object that detected the mouse action Button Indicates which mouse button was involved: mbLeft, mbMiddle, or mbRight Shift Indicates the state of the Alt, Ctrl, and Shift keys at the time of the mouse action X, Y The coordinates where the event occurred Most of the time, you need the coordinates returned in a mouse-event handler, but sometimes you also need to check Button to determine which mouse button caused the event. Note Delphi uses the same criteria as Microsoft Windows in determining which mouse button has been pressed. Thus, if you have switched the default “primary” and “secondary” mouse buttons (so that the right mouse button is now the primary button), clicking the primary (right) button will record mbLeft as the value of the Button parameter.

Responding to a mouse-down action

Whenever the user presses a button on the mouse, an OnMouseDown event goes to the object the pointer is over. The object can then respond to the event. To respond to a mouse-down action, attach an event handler to the OnMouseDown event. The VCL generates an empty handler for a mouse-down event on the form: procedure TForm1.FormMouseDown(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer); begin end; Here’s code that displays some text at the point where the mouse button is pressed. It uses the X and Y parameters sent to the method, and calls the TextOut method of the canvas to display text there: The following code displays the string ‘Here!’ at the location on a form clicked with the mouse: procedure TForm1.FormMouseDown(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer); begin Canvas.TextOut(X, Y, 'Here!');{ write text at (X, Y) } end; Workingwithgraphics7-23, RubberbandingexampleWhen the application runs, you can press the mouse button down with the mouse cursor on the form and have the string, “Here!” appear at the point clicked. This code sets the current drawing position to the coordinates where the user presses the button: procedure TForm1.FormMouseDown(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer); begin Canvas.MoveTo(X, Y);{ set pen position } end; Pressing the mouse button now sets the pen position, setting the line’s starting point. To draw a line to the point where the user releases the button, you need to respond to a mouse-up event. Responding to a mouse-up action An OnMouseUp event occurs whenever the user releases a mouse button. The event usually goes to the object the mouse cursor is over when the user presses the button, which is not necessarily the same object the cursor is over when the button is released. This enables you, for example, to draw a line as if it extended beyond the border of the form. To respond to mouse-up actions, define a handler for the OnMouseUp event. Here’s a simple OnMouseUp event handler that draws a line to the point of the mouse-button release: procedure TForm1.FormMouseUp(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer); begin Canvas.LineTo(X, Y);{ draw line from PenPos to (X, Y) } end; This code lets a user draw lines by clicking, dragging, and releasing. In this case, the user cannot see the line until the mouse button is released. Responding to a mouse move An OnMouseMove event occurs periodically when the user moves the mouse. The event goes to the object that was under the mouse pointer when the user pressed the button. This allows you to give the user some intermediate feedback by drawing temporary lines while the mouse moves. To respond to mouse movements, define an event handler for the OnMouseMove event. This example uses mouse-move events to draw intermediate shapes on a form while the user holds down the mouse button, thus providing some feedback to the user. The OnMouseMove event handler draws a line on a form to the location of the OnMouseMove event: procedure TForm1.FormMouseMove(Sender: TObject;Button: TMouseButton; Shift: TShiftState; X, Y: Integer); begin Canvas.LineTo(X, Y);{ draw line to current position } end; 7-24Developer’ sGuide, RubberbandingexampleWith this code, moving the mouse over the form causes drawing to follow the mouse, even before the mouse button is pressed. Mouse-move events occur even when you haven’t pressed the mouse button. If you want to track whether there is a mouse button pressed, you need to add an object field to the form object.

Adding a field to a form object to track mouse actions

To track whether a mouse button was pressed, you must add an object field to the form object. When you add a component to a form, Delphi adds a field that represents that component to the form object, so that you can refer to the component by the name of its field. You can also add your own fields to forms by editing the type declaration in the form unit’s header file. In the following example, the form needs to track whether the user has pressed a mouse button. To do that, it adds a Boolean field and sets its value when the user presses the mouse button. To add a field to an object, edit the object’s type definition, specifying the field identifier and type after the public directive at the bottom of the declaration. Delphi “owns” any declarations before the public directive: that’s where it puts the fields that represent controls and the methods that respond to events. The following code gives a form a field called Drawing of type Boolean, in the form object’s declaration. It also adds two fields to store points Origin and MovePt of typeTPoint. type TForm1 = class(TForm) procedure FormMouseDown(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer); procedure FormMouseUp(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer); procedure FormMouseMove(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer); public Drawing: Boolean;{ field to track whether button was pressed } Origin, MovePt: TPoint;{ fields to store points } end; When you have a Drawing field to track whether to draw, set it to True when the user presses the mouse button, and False when the user releases it: procedure TForm1.FormMouseDown(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer); begin Drawing := True;{ set the Drawing flag } Canvas.MoveTo(X, Y); end; procedure TForm1.FormMouseUp(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer); Workingwithgraphics7-25, Rubberbandingexamplebegin Canvas.LineTo(X, Y); Drawing := False;{ clear the Drawing flag } end; Then you can modify the OnMouseMove event handler to draw only when Drawing is True: procedure TForm1.FormMouseMove(Sender: TObject;Button: TMouseButton; Shift: TShiftState; X, Y: Integer); begin if Drawing then{ only draw if Drawing flag is set } Canvas.LineTo(X, Y); end; This results in drawing only between the mouse-down and mouse-up events, but you still get a scribbled line that tracks the mouse movements instead of a straight line. The problem is that each time you move the mouse, the mouse-move event handler calls LineTo, which moves the pen position, so by the time you release the button, you’ve lost the point where the straight line was supposed to start.

Refining line drawing

With fields in place to track various points, you can refine an application’s line drawing.

Tracking the origin point

When drawing lines, track the point where the line starts with the Origin field. Origin must be set to the point where the mouse-down event occurs, so the mouse-up event handler can use Origin to place the beginning of the line, as in this code: procedure TForm1.FormMouseDown(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer); begin Drawing := True; Canvas.MoveTo(X, Y); Origin := Point(X, Y);{ record where the line starts } end; procedure TForm1.FormMouseUp(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer); begin Canvas.MoveTo(Origin.X, Origin.Y);{ move pen to starting point } Canvas.LineTo(X, Y); Drawing := False; end; Those changes get the application to draw the final line again, but they do not draw any intermediate actions—the application does not yet support “rubber banding.” 7-26Developer’ sGuide, Rubberbandingexample

Tracking movement

The problem with this example as the OnMouseMove event handler is currently written is that it draws the line to the current mouse position from the last mouse position, not from the original position.You can correct this by moving the drawing position to the origin point, then drawing to the current point: procedure TForm1.FormMouseMove(Sender: TObject;Button: TMouseButton; Shift: TShiftState; X, Y: Integer); begin if Drawing then begin Canvas.MoveTo(Origin.X, Origin.Y);{ move pen to starting point } Canvas.LineTo(X, Y); end; end; The above tracks the current mouse position, but the intermediate lines do not go away, so you can hardly see the final line. The example needs to erase each line before drawing the next one, by keeping track of where the previous one was. The MovePt field allows you to do this. MovePt must be set to the endpoint of each intermediate line, so you can use MovePt and Origin to erase that line the next time a line is drawn: procedure TForm1.FormMouseDown(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer); begin Drawing := True; Canvas.MoveTo(X, Y); Origin := Point(X, Y); MovePt := Point(X, Y);{ keep track of where this move was } end; procedure TForm1.FormMouseMove(Sender: TObject;Button: TMouseButton; Shift: TShiftState; X, Y: Integer); begin if Drawing then begin Canvas.Pen.Mode := pmNotXor;{ use XOR mode to draw/erase } Canvas.MoveTo(Origin.X, Origin.Y);{ move pen back to origin } Canvas.LineTo(MovePt.X, MovePt.Y);{ erase the old line } Canvas.MoveTo(Origin.X, Origin.Y);{ start at origin again } Canvas.LineTo(X, Y);{ draw the new line } end; MovePt := Point(X, Y);{ record point for next move } Canvas.Pen.Mode := pmCopy; end; Now you get a “rubber band” effect when you draw the line. By changing the pen’s mode to pmNotXor, you have it combine your line with the background pixels. When you go to erase the line, you’re actually setting the pixels back to the way they were. By changing the pen mode back to pmCopy (its default value) after drawing the lines, you ensure that the pen is ready to do its final drawing when you release the mouse button. Workingwithgraphics7-27, 7-28Developer’ sGuide,

Chapter

Chapter 8Working with multimedia Delphi 4 allows you to add multimedia components to your applications. To do this, you can use either the TAnimate component on the Win32 page or the TMediaplayer component on the System page of the components palette. Use the animate component when you want to add silent video clips to your application. Use the media player component when you want to add audio and/or video clips to an application. For more information on TAnimate and TMediaplayer components, see the VCL online help. The following topics are discussed in this section: • Adding silent video clips to an application • Adding audio and/or video clips to an application

Adding silent video clips to an application

The animation control in Delphi4 allows you to add silent video clips to your application. To add a silent video clip to an application: 1 Double-click the animate icon on the Win32 page of the components palette. This automatically puts an animation control on the form window in which you want to display the video clip. 2 Using the Object Inspector, select the Name property and enter a new name for your animation control. You will use this name when you call the animation control. (Follow the standard rules for naming Delphi identifiers). Always work directly with the Object Inspector when setting design time properties and creating event handlers. Workingwithmultimedia8-1, Addingsilentvideoclipstoanapplication3Do one of the following: • Select the Common AVI property and choose one of the AVIs available from the drop down list; or • Select the FileName property and click the ellipsis button, choose an AVI file from any available local or network directories and click Open in the Open AVI dialog; or • Select the resource of an AVI using the ResName or ResID properties. Use ResHandle to indicate the module that contains the resource identified by ResName or ResID. This loads the AVI file into memory. If you want to display the first frame of the AVI clip on-screen until it is played using the Active property or the Play method, then set the Open property to True. 4 Set the Repetitions property to the number of times you want to the AVI clip to play. If this value is 0, then the sequence is repeated until the Stop method is called. 5 Make any other changes to the animation control settings. For example, if you want to change the first frame displayed when animation control opens, then set the StartFrame property to the desired frame value. 6 Set the Active property to True using the drop down list or write an event handler to run the AVI clip when a specific event takes place at runtime. For example, to activate the AVI clip when a button object is clicked, write the button’s OnClick event specifying that. You may also call the Play method to specify when to play the AVI. Note If you make any changes to the form or any of the components on the form after setting Active to True, the Active property becomes False and you have to reset it to True. Do this either just before runtime or at runtime.

Example of adding silent video clips

Suppose you want to display an animated logo as the first screen that appears when your application starts. After the logo finishes playing the screen disappears. To run this example, create a new project and save the Unit1.pas file as Frmlogo.pas and save the Project1.dpr file as Logo.dpr. Then: 1 Double-click the animate icon from the Win32 page of the components palette. 2 Using the Object Inspector, set its Name property to Logo1. 3 Select its FileName property, click the ellipsis (…) button, choose the cool.avi file from your Delphi4\Demos\Coolstuf directory. Then click Open in the Open AVI dialog. This loads the cool.avi file into memory. 4 Position the animation control box on the form by clicking and dragging it to the top right hand side of the form. 5 Set its Repetitions property to 5. 8-2Developer’ sGuide, Addingaudioand/ orvideoclipstoanapplication6Click the form to bring focus to it and set its Name property to LogoForm1 and its Caption property to Logo Window. Now decrease the height of the form to center the animation control on it. 7 Double-click the form’s OnActivate event and write the following code to run the AVI clip when the form is in focus at runtime: Logo1.Active := True; 8 Double-click the Label icon on the Standard page of the components palette. Select its Caption property and enter Welcome to Cool Images 4.0. Now select its Font property, click the ellipsis (…) button and choose Font Style: Bold, Size: 18, Color: Navy from the Font dialog and click OK. Click and drag the label control to center it on the form. 9 Click the animation control to bring focus back to it. Double-click its OnStop event and write the following code to close the form when the AVI file stops: LogoForm1.Close; 10 Select Run|Run to execute the animated logo window.

Adding audio and/or video clips to an application

The media player component in Delphi4 allows you to add audio and/or video clips to your application. It opens a media device and plays, stops, pauses, records, etc., the audio and/or video clips used by the media device. The media device may be hardware or software. To add an audio and/or video clip to an application: 1 Double-click the media player icon on the System page of the components palette. This automatically put a media player control on the form window in which you want the media feature. 2 Using the Object Inspector, select the Name property and enter a new name for your media player control. You will use this when you call the media player control. (Follow the standard rules for naming Delphi identifiers). Always work directly with the Object Inspector when setting design time properties and creating event handlers. 3 Select the DeviceType property and choose the appropriate device type to open using the AutoOpen property or the Open method. (If DeviceType is dtAutoSelect the device type is selected based on the file extension of the media file specified by the FileName property.) For more information on device types and their functions, see Table 8.1. 4 If the device stores its media in a file, specify the name of the media file using the FileName property. Select the FileName property, click the ellipsis button, and choose a media file from any available local or network directories and click Open in the Open dialog. Otherwise, insert the hardware the media is stored in (disk, cassette, and so on) for the selected media device, at runtime. Workingwithmultimedia8-3, Addingaudioand/ orvideoclipstoanapplication5Set the AutoOpen property to True. This way the media player automatically opens the specified device when the form containing the media player control is created at runtime. If AutoOpen is False, the device must be opened with a call to the Open method. 6 Set the AutoEnable property to True to automatically enable or disable the media player buttons as required at runtime; or, double-click the EnabledButtons property to set each button to True or False depending on which ones you want to enable or disable. The multimedia device is played, paused, stopped, and so on when the user clicks the corresponding button on the mediaplayer component. The device can also be controlled by the methods that correspond to the buttons (Play, Pause, Stop, Next, Previous, and so on). 7 Position the media player control bar on the form by either clicking and dragging it to the appropriate place on the form or by selecting the Align property and choosing the appropriate align position from the drop down list. If you want the media player to be invisible at runtime, set the Visible property to False and control the device by calling the appropriate methods (Play, Pause, Stop, Next, Previous, Step, Back, Start Recording, Eject). 8 Make any other changes to the media player control settings. For example, if the media requires a display window, set the Display property to the control that displays the media. If the device uses multiple tracks, set the Tracks property to the desired track. Table 8.1 Multimedia device types and their functions Uses a Uses Display Device Type Software/Hardware used Plays Tracks Window dtAVIVideo AVI Video Player for AVI Video files No Yes Windows dtCDAudio CD Audio Player for CD Audio Disks Yes No Windows or a CD Audio Player dtDAT Digital Audio Tape Player Digital Audio Tapes Yes No dtDigitalVideo Digital Video Player for AVI, MPG, MOV files No Yes Windows dtMMMovie MM Movie Player MM film No Yes dtOverlay Overlay device Analog Video No Yes dtScanner Image Scanner N/a for Play (scans No No images on Record) dtSequencer MIDI Sequencer for MIDI files Yes No Windows dtVCR Video Cassette Recorder Video Cassettes No Yes dtWaveAudio Wave Audio Player for WAV files No No Windows 8-4Developer’ sGuide, Addingaudioand/ orvideoclipstoanapplication

Example of adding audio and/or video clips

This example runs an AVI video clip of a multimedia advertisement for Delphi. To run this example, create a new project and save the Unit1.pas file to FrmAd.pas and save the Project1.dpr file to DelphiAd.dpr. Then: 1 Double-click the media player icon on the System page of the components palette. 2 Using the Object Inspector, set the Name property of the media player to VideoPlayer1. 3 Select its DeviceType property and choose dtAVIVideo from the drop down list. 4 Select its FileName property, click the ellipsis (…) button, choose the speedis.avi file from your Delphi4\Demos\Coolstuf directory. Click Open in the Open dialog. 5 Set its AutoOpen property to True and its Visible property to False. 6 Double-click the Animate icon from the Win32 page of the components palette. Set its AutoSize property to False, its Height property to 175 and Width property to 200. Click and drag the animation control to the top left corner of the form. 7 Click the media player to bring back focus to it. Select its Display property and choose Animate1 from the drop down list. 8 Click the form to bring focus to it and select its Name property and enter Delphi_Ad. Now resize the form to the size of the animation control. 9 Double-click the form’s OnActivate event and write the following code to run the AVI video when the form is in focus: Videoplayer1.Play; 10 Choose Run|Run to execute the AVI video. Workingwithmultimedia8-5, 8-6Developer’ sGuide,

Chapter

Chapter 9Writing multi-threaded applications The VCL provides several objects that make writing multi-threaded applications easier. Multi-threaded applications are applications that include several simultaneous paths of execution. While using multiple threads requires careful thought, it can enhance your programs by • Avoiding bottlenecks. With only one thread, a program must stop all execution when waiting for slow processes such as accessing files on disk, communicating with other machines, or displaying multimedia content. The CPU sits idle until the process completes. With multiple threads, your application can continue execution in separate threads while one thread waits for the results of a slow process. • Organizing program behavior. Often, a program’s behavior can be organized into several parallel processes that function independently. Use threads to launch a single section of code simultaneously for each of these parallel cases. Use threads to assign priorities to various program tasks so that you can give more CPU time to more critical tasks. • Multiprocessing. If the system running your program has multiple processors, you can improve performance by dividing the work into several threads and letting them run simultaneously on separate processors. Note The NT operating system implements true multi-processing when it is supported by the underlying hardware. Windows 95 only simulates multiprocessing, even if the underlying hardware supports multiprocessing.

Defining thread objects

For most applications, you can use a thread object to represent an execution thread in your application. Thread objects simplify writing multi-threaded applications by encapsulating the most commonly needed uses of threads. Note Thread objects do not allow you to control the security attributes or stack size of your threads. If you need to control these, you must use the BeginThread function. EvenWritingmulti- threadedapplications9-1, Definingthreadobjectswhen using BeginThread, you can still benefit from some of the thread synchronization objects and methods described in “Coordinating threads” on page 9-6. For more information on using BeginThread, see the online help. To use a thread object in your application, you must create a new descendant of TThread. To create a descendant of TThread, choose File|New from the main menu. In the new objects dialog box, select Thread Object. You are prompted to provide a class name for your new thread object. After you provide the name, Delphi creates a new unit file to implement the thread. Note Unlike most dialog boxes in the IDE that require a class name, the New Thread Object dialog does not automatically prepend a ‘T’ to the front of the class name you provide. The automatically generated file contains the skeleton code for your new thread object. If you named your thread TMyThread, it would look like the following: unit Unit2; interface uses Classes; type TMyThread = class(TThread) private { Private declarations } protected procedure Execute; override; end; implementation { TMyThread } procedure TMyThread.Execute; begin { Place thread code here } end; end. You must fill in the code for the Execute method. These steps are described in the following sections.

Initializing the thread

If you want to write initialization code for your new thread class, you must override the Create method. Add a new constructor to the declaration of your thread class and write the initialization code as its implementation. This is where you can assign a default priority for your thread and indicate whether it should be freed automatically when it finishes executing. Assigning a default priority Priority indicates how much preference the thread gets when the operating system schedules CPU time among all the threads in your application. Use a high priority thread to handle time critical tasks, and a low priority thread to perform other tasks. 9-2Developer’ sGuide, DefiningthreadobjectsTo indicate the priority of your thread object, set the Priority property. Priority values fall along a seven point scale, as described in Table 9.1: Table 9.1 Thread priorities Value Priority tpIdle The thread executes only when the system is idle. Windows won’t interrupt other threads to execute a thread with tpIdle priority. tpLowest The thread’s priority is two points below normal. tpLower The thread’s priority is one point below normal. tpNormal The thread has normal priority. tpHigher The thread’s priority is one point above normal. tpHighest The thread’s priority is two points above normal. tpTimeCritical The thread gets highest priority. Warning Boosting the thread priority of a CPU intensive operation may “starve” other threads in the application. Only apply priority boosts to threads that spend most of their time waiting for external events. The following code shows the constructor of a low-priority thread that performs background tasks which should not interfere with the rest of the application’s performance: constructor TMyThread.Create(CreateSuspended: Boolean); { inherited Create(CreateSuspended); Priority := tpIdle; }

Indicating when threads are freed

Often, applications execute a particular thread only once. When this is the case, it is easiest to let the thread object free itself. To do this, set the FreeOnTerminate property to true. There are times, however, when your thread object represents a task that the application performs several times, in response to an event such as a user action or external message. When your application needs multiple instances of the same thread object (to run the thread multiple times), you can improve performance by caching threads for reuse rather than destroying them after they run and creating new ones every time. To do this, set the FreeOnTerminate property to false. For more information, see “Caching threads” on page 9-11.

Writing the thread function

The Execute method is your thread function. You can think of it as a program that is launched by your application, except that it shares the same process space. Writing the thread function is a little trickier than writing a separate program because you must make sure that you don’t overwrite memory that is used by other threads in your application. On the other hand, because the thread shares the same processWritingmulti- threadedapplications9-3, Definingthreadobjectsspace with other threads, you can use the shared memory to communicate between threads. Using the main VCL thread When you use objects from the VCL object hierarchy, their properties and methods are not guaranteed to be thread-safe. That is, accessing properties or executing methods may perform some actions that use memory which is not protected from the actions of other threads. Because of this, a main VCL thread is set aside for access of VCL objects. This is the thread that handles all Windows messages received by components in your application. If all objects access their properties and execute their methods within this single thread, you need not worry about your objects interfering with each other. To use the main VCL thread, create a separate routine that performs the required actions. Call this separate routine from within your thread’s Synchronize method. For example: procedure TMyThread.PushTheButton; begin Button1.Click; end; ƒ procedure TMyThread.Execute; begin ƒ Synchronize(PushTheButton); ƒ end; Synchronize waits for the main VCL thread to enter the message loop and then executes the passed method. Note Because Synchronize uses the message loop, it does not work in console applications. You must use other mechanisms, such as critical sections, to protect access to VCL objects in console applications. You do not always need to use the main VCL thread. Some objects are thread-aware. Omitting the use of the Synchronize method when you know an object’s methods are thread-safe will improve performance because you don’t need to wait for the VCL thread to enter its message loop. You do not need to use the Synchronize method in the following situations: • Data access components are thread-safe as long as each thread has its own database session component. The one exception to this is when you are using Access drivers. Access drivers are built using the Microsoft ADO library, which is not thread-safe. When using data access components, you must still wrap all calls that involve data-aware controls in the Synchronize method. Thus, for example, you need to synchronize calls that link a data control to a dataset by setting the DataSet property of the data source object, but you don’t need to synchronize to access the data in a field of the dataset. For more information about using database sessions with threads, see “Managing multiple sessions” on page 16-16. 9-4Developer’ sGuide, Definingthreadobjects• Graphics objects are thread-safe. You do not need to use the main VCL thread to access TFont, TPen, TBrush, TBitmap, TMetafile, or TIcon. Canvas objects can be used outside the Synchronize method by locking them (see “Locking objects” on page 9-6). • While list objects are not thread-safe, you can use a thread-safe version, TThreadList, instead of TList. Using thread-local variables Your Execute method and any of the routines it calls have their own local variables, just like any other Object Pascal routines. These routines also can access any global variables. In fact, global variables provide a powerful mechanism for communicating between threads. Sometimes, however, you may want to use variables that are global to all the routines running in your thread, but not shared with other instances of the same thread class. You can do this by declaring thread-local variables. Make a variable thread-local by declaring it in a threadvar section. For example, threadvar x : integer; declares an integer type variable that is private to each thread in the application, but global within each thread. The threadvar section can only be used for global variables. Pointer and Function variables can’t be thread variables. Types that use copy-on-write semantics, such as long strings don’t work as thread variables either. Checking for termination by other threads Your thread begins running when the Execute method is called (see “Executing thread objects” on page 9-10) and continues until Execute finishes. This reflects the model that the thread performs a specific task, and then stops when it is finished. Sometimes, however, an application needs a thread to execute until some external criterion is satisfied. You can allow other threads to signal that it is time for your thread to finish executing by checking the Terminated property. When another thread tries to terminate your thread, it calls the Terminate method. Terminate sets your thread’s Terminated property to true. It is up to your Execute method to implement the Terminate method by checking and responding to the Terminated property. The following example shows one way to do this: procedure TMyThread.Execute; begin while not Terminated do PerformSomeTask; end; Writingmulti- threadedapplications9-5, Coordinatingthreads

Writing clean-up code

You can centralize the code that cleans up when your thread finishes executing. Just before a thread shuts down, an OnTerminate event occurs. Put any clean-up code in the OnTerminate event handler to ensure that it is always executed, no matter what execution path the Execute method follows. The OnTerminate event handler is not run as part of your thread. Instead, it is run in the context of the main VCL thread of your application. This has two implications: • You can’t use any thread-local variables in an OnTerminate event handler (unless you want the main VCL thread values). • You can safely access any components and VCL objects from the OnTerminate event handler without worrying about clashing with other threads. For more information about the main VCL thread, see “Using the main VCL thread” on page 9-4.

Coordinating threads

When writing the code that runs when your thread is executed, you must consider the behavior of other threads that may be executing simultaneously. In particular, care must be taken to avoid two threads trying to use the same global object or variable at the same time. In addition, the code in one thread can depend on the results of tasks performed by other threads.

Avoiding simultaneous access

To avoid clashing with other threads when accessing global objects or variables, you may need to block the execution of other threads until your thread code has finished an operation. Be careful not to block other execution threads unnecessarily. Doing so can cause performance to degrade seriously and negate most of the advantages of using multiple threads. Locking objects Some objects have built-in locking that prevents the execution of other threads from using that object instance. For example, canvas objects (TCanvas and descendants) have a Lock method that prevents other threads from accessing the canvas until the Unlock method is called. The VCL also includes a thread-safe list object, TThreadList. CallingTThreadList.LockList returns the list object while also blocking other execution threads from using the list until the UnlockList method is called. Calls to TCanvas.Lock or TThreadList.LockList can be safely nested. The lock is not released until the last locking call is matched with a corresponding unlock call in the same thread. 9-6Developer’ sGuide, CoordinatingthreadsUsing critical sections If objects do not provide built-in locking, you can use a critical section. Critical sections work like gates that allow only a single thread to enter at a time. To use a critical section, create a global instance of TCriticalSection. TCriticalSection has two methods, Acquire (which blocks other threads from executing the section) and Release (which removes the block). Each critical section is associated with the global memory you want to protect. Every thread that accesses that global memory should first use the Acquire method to ensure that no other thread is using it. When finished, threads call the Release method so that other threads can access the global memory by calling Acquire. Warning Critical sections only work if every thread uses them to access the associated global memory. Threads that ignore the critical section and access the global memory without calling Acquire can introduce problems of simultaneous access. For example, consider an application that has a global critical section variable, LockXY, that blocks access to global variables X and Y. Any thread that uses X or Y must surround that use with calls to the critical section such as the following: LockXY.Acquire; { lock out other threads } try Y := sin(X); finally LockXY.Release; end; Using the multi-read exclusive-write synchronizer When you use critical sections to protect global memory, only one thread can use the memory at a time. This can be more protection than you need, especially if you have an object or variable that must be read often but to which you very seldom write. There is no danger in multiple threads reading the same memory simultaneously, as long as no thread is writing to it. When you have some global memory that is read often, but to which threads occasionally write, you can protect it using TMultiReadExclusiveWriteSynchronizer. This object acts like a critical section, but one which allows multiple threads to read the memory it protects as long as no thread is writing to it. Threads must have exclusive access to write to memory protected by TMultiReadExclusiveWriteSynchronizer. To use a multi-read exclusive-write synchronizer, create a global instance of TMultiReadExclusiveWriteSynchronizer that is associated with the global memory you want to protect. Every thread that reads from this memory must first call the BeginRead method. BeginRead ensures that no other thread is currently writing to the memory. When a thread finishes reading the protected memory, it calls the EndRead method. Any thread that writes to the protected memory must call BeginWrite first. BeginWrite ensures that no other thread is currently reading or writing to the memory. When a thread finishes writing to the protected memory, it calls the EndWrite method, so that threads waiting to read the memory can begin. Writingmulti- threadedapplications9-7, CoordinatingthreadsWarning Like critical sections, the multi-read exclusive-write synchronizer only works if every thread uses it to access the associated global memory. Threads that ignore the synchronizer and access the global memory without calling BeginRead or BeginWrite introduce problems of simultaneous access. Other techniques for sharing memory When using objects in the VCL, use the main VCL thread to execute your code. Using the main VCL thread ensures that the object does not indirectly access any memory that is also used by VCL objects in other threads. See “Using the main VCL thread” on page 9-4 for more information on the main VCL thread. If the global memory does not need to be shared by multiple threads, consider using thread-local variables instead of global variables. By using thread-local variables, your thread does not need to wait for or lock out any other threads. See “Using thread-local variables” on page 9-5 for more information about thread-local variables.

Waiting for other threads

If your thread must wait for another thread to finish some task, you can tell your thread to temporarily suspend execution. You can either wait for another thread to completely finish executing, or you can wait for another thread to signal that it has completed a task. Waiting for a thread to finish executing To wait for another thread to finish executing, use the WaitFor method of that other thread. WaitFor doesn’t return until the other thread terminates, either by finishing its own Execute method or by terminating due to an exception. For example, the following code waits until another thread fills a thread list object before accessing the objects in the list: if ListFillingThread.WaitFor then begin with ThreadList1.LockList do begin for I := 0 to Count - 1 do ProcessItem(Items[I]); end; ThreadList1.UnlockList; end; Warning Don’t call the WaitFor method of a thread that uses Synchronize from the main VCL thread. If the main VCL thread has called WaitFor, the other thread won’t enter the message loop and Synchronize will never return. Thread objects detect this case and raise an EThread exception in the thread that called Synchronize. If Synchronize was already waiting on the main VCL thread when WaitFor is called, the thread with the Synchronize method can’t intervene, and the application will deadlock. In the previous example, the list items were only accessed when the WaitFor method indicated that the list was successfully filled. This return value must be assigned by 9-8Developer’ sGuide, Coordinatingthreadsthe Execute method of the thread that was waited for. However, because threads that call WaitFor want to know the result of thread execution, not code that calls Execute, the Execute method does not return any value. Instead, the Execute method sets the ReturnValue property. ReturnValue is then returned by the WaitFor method when it is called by other threads. Return values are integers. Your application determines their meaning.

Waiting for a task to be completed

Sometimes, you need to wait for a thread to finish some operation rather than waiting for a particular thread to complete execution. To do this, use an event object. Event objects (TEvent) should be created with global scope so that they can act like signals that are visible to all threads. When a thread completes an operation that other threads depend on, it calls TEvent.SetEvent. SetEvent turns on the signal, so any other thread that checks will know that the operation has completed. To turn off the signal, use the ResetEvent method. For example, consider a situation where you must wait for several threads to complete their execution rather than a single thread. Because you don’t know which thread will finish last, you can’t simply use the WaitFor method of one of the threads. Instead, you can have each thread increment a counter when it is finished, and have the last thread signal that they are all done by setting an event. The following code shows the end of the OnTerminate event handler for all of the threads that must complete. CounterGuard is a global critical section object that prevents multiple threads from using the counter at the same time. Counter is a global variable that counts the number of threads that have completed. procedure TDataModule.TaskThreadTerminate(Sender: TObject); begin ƒ CounterGuard.Acquire; { obtain a lock on the counter } Dec(Counter); { decrement the global counter variable } if Counter = 0 then Event1.SetEvent; { signal if this is the last thread } CounterGuard.Release; { release the lock on the counter } ƒ end; The main thread initializes the Counter variable, launches the task threads, and waits for the signal that they are all done by calling the WaitFor method. WaitFor waits for a specified time period for the signal to be set, and returns one of the values from Table 9.2. Table 9.2 WaitFor return values Value Meaning wrSignaled The signal of the event was set. wrTimeout The specified time elapsed without the signal being set. wrAbandoned The event object was destroyed before the timeout period elapsed. wrError An error occurred while waiting. Writingmulti- threadedapplications9-9, ExecutingthreadobjectsThe following shows how the main thread launches the task threads and then resumes when they have all completed: Event1.ResetEvent; { clear the event before launching the threads } for i := 1 to Counter do TaskThread.Create(False); { create and launch task threads } if Event1.WaitFor(20000) != wrSignaled then raise Exception; { now continue with the main thread. All task threads have finished } Note If you do not want to stop waiting for an event after a specified time period, pass the WaitFor method a parameter value of INFINITE. Be careful when using INFINITE, because your thread will hang if the anticipated signal is never received.

Executing thread objects

Once you have implemented a thread class by giving it an Execute method, you can use it in your application to launch the code in the Execute method. To use a thread, first create an instance of the thread class. You can create a thread instance that starts running immediately, or you can create your thread in a suspended state so that it only begins when you call the Resume method. To create a thread so that it starts up immediately, set the constructor’s CreateSuspended parameter to false. For example, the following line creates a thread and starts its execution: SecondProcess := TMyThread.Create(false); {create and run the thread } Warning Do not create too many threads in your application. The overhead in managing multiple threads can impact performance. The recommended limit is 16 threads per process on single processor systems. This limit assumes that most of those threads are waiting for external events. If all threads are active, you will want to use fewer. You can create multiple instances of the same thread type to execute parallel code. For example, you can launch a new instance of a thread in response to some user action, allowing each thread to perform the expected response.

Overriding the default priority

When the amount of CPU time the thread should receive is implicit in the thread’s task, its priority is set in the constructor. This is described in “Initializing the thread” on page 9-2. However, if the thread priority varies depending on when the thread is executed, create the thread in a suspended state, set the priority, and then start the thread running: SecondProcess := TMyThread.Create(True); { create but don’t run } SecondProcess.Priority := tpLower; { set the priority lower than normal } SecondProcess.Resume; { now run the thread } 9-10Developer’ sGuide, Executingthreadobjects

Starting and stopping threads

A thread can be started and stopped any number of times before it finishes executing. To stop a thread temporarily, call its Suspend method. When it is safe for the thread to resume, call its Resume method. Suspend increases an internal counter, so you can nest calls to Suspend and Resume. The thread does not resume execution until all suspensions have been matched by a call to Resume. You can request that a thread end execution prematurely by calling the Terminate method. Terminate sets the thread’s Terminated property to True. If you have implemented the Execute method properly, it checks the Terminated property periodically, and stops execution when Terminated is True.

Caching threads

When your application needs multiple instances of the same thread object (to run the thread multiple times), you can improve performance by caching threads for reuse rather than destroying them after they run and creating new ones every time. To cache threads, you must maintain a list of threads that have been created. This list can be maintained by an object that uses the threads, or you can use a global variable to store the thread cache. When a new thread is needed, a cached thread is used, or a new thread is created (as in the following GetThread function): var Cache: TThreadList; ƒ function GetThread:TThread; begin with Cache.LockList do begin if Count then { cache is not empty } begin Result := TThread(Items[0]); Delete(0); // remove from cache end else Result := TMyThread.Create(true); {create but don’t run } end; Cache.UnlockList; end; When a thread finishes executing, it is added to the cache: procedure TMyThread.Terminate(Sender: TObject); { this is the OnTerminate handler } begin Cache.Add(self); { add pointer to this thread to the cache } end; Writingmulti- threadedapplications9-11, UsingthreadsindistributedapplicationsFinally, when the application finishes executing (or the object that owns the thread cache is destroyed), the threads in the cache must be freed, as in the following application OnDeactivate handler: procedure TDataModule1.ApplicationDeactivate(Sender: TObject); var I: Integer; begin with Cache.LockList do begin for I := Count - 1 downto 0 do begin Items[I].Free; { free the thread } Delete[I]; { remove it from the cache } end; end; Cache.UnlockList; end;

Using threads in distributed applications

Distributed applications introduce additional challenges for writing multi-threaded applications. When considering how to coordinate threads, you must also keep in mind how other processes affect the threads in your application. Usually, handling distributed threading issues is the responsibility of the server application. When writing servers, you must consider how requests from clients are serviced. If each client request has its own thread, you must ensure that different client threads do not interfere with each other. In addition to the usual issues that arise when coordinating multiple threads, you may need to ensure that each client has a consistent view of your application. For example, you can’t use thread variables to store information that must persist over multiple client requests if each time the client calls your application it uses a different thread. When clients change the values of object properties or global variables, they are influencing not only their own view of that object or variable, but the view of any other clients.

Using threads in message-based servers

Message-based servers receive client request messages, perform some action in response to that message, and return messages to the client. Examples include internet server applications and simple services that you can write using sockets. Usually, when writing message-based servers, each client message gets its own thread. When client messages are received, the application spawns a thread to handle the message. This thread runs until it sends a response to the client, and then terminates. You must be careful when using global objects and variables, but it is fairly easy to control how threads are created and run because client messages are all received and dispatched by the main application thread. 9-12Developer’ sGuide, Usingthreadsindistributedapplications

Using threads with distributed objects

When writing servers for distributed objects, the threading issues are more complicated. Unlike message-based servers, where there is a point in the code where messages are received and dispatched, clients call into server objects by calling any of their methods or by accessing any of their properties. Because of this, there is no easy way for server applications to spawn separate threads for each client request. Writing applications (.EXEs) When writing an .EXE that implements an object or objects for remote clients, client requests come in as threads. How this works depends on whether clients access your object using COM or CORBA. • Under COM, client requests come in as part of the application’s message loop. This means that any code which executes after the application’s main message loop starts up must be prepared to protect access to objects and global memory from other threads. When running in an environment that supports DCOM, Delphi ensures that no client requests occur until all code in the initialization part of your units has executed. If you are not running in an environment that supports DCOM, you must ensure that any code in the initialization part of your units is thread-safe. • Under CORBA, you can choose a threading model in the wizard that starts a new CORBA server. You can choose either single-threading or multi-threading. Under both models, each client connection has its own thread. You can use thread variables for information that persists across client calls because all calls for a given client use the same thread. With single-threading, only one client thread has access to an object instance at a time. While you must protect access to global memory, you are assured of no conflicts when accessing the object’s instance data (such as property values). With multi-threading, multiple clients may access your application at the same time. If you are sharing object instances over clients, you must protect instance data as well as global data. Writing Libraries When an Active Library implements the distributed object, threading is usually controlled by the technology (COM, DCOM, or MTS) that supports distributed object calls. When you first create your server library with the appropriate wizard, you are prompted to specify a threading model that dictates how client requests are assigned threads. These models include the following: • Single-threaded model. Client requests are serialized by the calling mechanism. Your .DLL does not need to be concerned with threading issues because it receives one client request at a time. • Single-threaded apartment model. (Also called Apartment model.) Each object instantiated by a client is accessed by one thread at a time. You must protect against multiple threads accessing global memory, but instance data (such as object properties) is thread-safe. Further, each client always accesses the object instance using the same thread, so that you can use thread variables. Writingmulti- threadedapplications9-13, Debuggingmulti- threadedapplications• Activity model. (Called both Apartment-threaded and Free-threaded under MTS.) Each object instance is accessed by one thread at a time, but clients do not always use the same thread for every call. Instance data is safe, but you must guard global memory, and thread variables will not be consistent across client calls. • Multi-threaded apartment model. (Also called Free-threading.) Each object instance may be called by multiple threads simultaneously. You must protect instance data as well as global memory. Thread variables are not consistent across client calls. • Single/Multi-threaded apartment model. (Also called Both.) This is the same as the Multi-threaded apartment model, except that all callbacks supplied by clients are guaranteed to execute in the same thread. This means you do not need protect values supplied as parameters to callback functions. COM-based systems use the application’s message loop to synchronize threads in all but the Multi-threaded apartment model (which is only available under DCOM). Because of this, you must ensure that any lengthy call made through a COM interface calls the application object’s ProcessMessages method. Failure to do so prevents other clients from gaining access to your application, effectively making your library single-threaded.

Debugging multi-threaded applications

When debugging multi-threaded applications, it can be confusing trying to keep track of the status of all the threads that are executing simultaneously, or even to determine which thread is executing when you stop at a breakpoint. You can use the Threads status box to help you keep track of and manipulate all the threads in your application. To display the Threads status box, choose View|Threads from the main menu. When a debug event occurs (breakpoint, exception, paused), the thread status view indicates the status of each thread. Right-click the Threads status box to access commands that locate the corresponding source location or make a different thread current. When a thread is marked as current, the next step or run operation is relative to that thread. The Threads status box lists all your application’s threads by their thread ID. If you are using thread objects, the thread ID is the value of the ThreadID property. If you are not using thread objects, the thread ID for each thread is returned by the call to BeginThread. 9-14Developer’ sGuide, Debuggingmulti- threadedapplicationsFor each thread, the Threads status box provides the following information: ThreadId Identifies the thread State Indicates whether the thread is Running or Stopped Status Displays one of the following: • Breakpoint (The thread stopped due to a breakpoint.) • Faulted (The thread stopped due to a processor exception.) • Unknown (The thread is not the current thread so its status is unknown.) • Stepped (The last step command was successfully completed.) Location Indicates the source position. Location displays the address if no source location is available. Right-click a thread in the Threads status box to access the following commands: View Source Displays the Code editor at the corresponding source location of the selected thread ID, but does not make the Code editor the active window. Go to Source Displays the Code editor at the corresponding source location of the selected thread ID and makes the Code editor the active window. Make Current Makes the selected thread the active process if it is not so already. Writingmulti- threadedapplications9-15, 9-16Developer’ sGuide,

Chapter

Chapter 10Working with packages and components A package is a special dynamic-link library used by Delphi applications, the IDE, or both. Runtime packages provide functionality when a user runs an application. Design- time packages are used to install components in the IDE and to create special property editors for custom components. A single package can function at both design time and runtime, and design-time packages frequently work by calling runtime packages. To distinguish them from other DLLs, package libraries are stored in files that end with the .BPL (Borland package library) extension. Like other runtime libraries, packages contain code that can be shared among applications. For example, the most frequently used Delphi components reside in a package called VCL40. Each time you create an application, you can specify that it uses VCL40. When you compile an application created this way, the application’s executable image contains only the code and data unique to it; the common code is in VCL40.BPL. A computer with several package-enabled applications installed on it needs only a single copy of VCL40.BPL, which is shared by all the applications and the IDE itself. Delphi ships with several precompiled runtime packages, including VCL40, that encapsulate VCL components. Delphi also uses design-time packages to manipulate components in the IDE. You can build applications with or without packages. However, if you want to add custom components to the IDE, you must install them as design-time packages. You can create your own runtime packages to share among applications. If you write Delphi components, you can compile your components into design-time packages before installing them. Workingwithpackagesandcomponents10-1, Whyusepackages?

Why use packages?

Design-time packages simplify the tasks of distributing and installing custom components. Runtime packages, which are optional, offer several advantages over conventional programming. By compiling reused code into a runtime library, you can share it among applications. For example, all of your applications—including Delphi itself—can access standard components through packages. Since the applications don’t have separate copies of the component library bound into their executables, the executables are much smaller—saving both system resources and hard disk storage. Moreover, packages allow faster compilation because only code unique to the application is compiled with each build.

Packages and standard DLLs

Create a package when you want to make a custom component that’s available through the Delphi IDE. Create a standard DLL when you want to build a library that can be called from any Windows application, regardless of the development tool used to build the application. The following table lists the file types associated with packages. Table 10.1 Compiled package files File extension Contents DPK The source file listing the units contained in the package. DCP A binary image containing a package header and the concatenation of all DCU files in the package, including all symbol information required by the compiler. A single DCP file is created for each package. The base name for the DCP is the base name of the DPK source file. You must have a .DCP file to build an application with packages. DCU A binary image for a unit file contained in a package. One DCU is created, when necessary, for each unit file. BPL The runtime package. This file is a Windows DLL with special Delphi-specific features. The base name for the BPL is the base name of the DPK source file. Note Packages share their global data with other modules in an application. For more information about DLLs and packages, see Chapter 9, “Dynamic-link libraries and packages” in the Object Pascal Language Guide.

Runtime packages

Runtime packages are deployed with Delphi applications. They provide functionality when a user runs the application. To run an application that uses packages, a computer must have both the application’s .EXE file and all the packages (.BPL files) that the application uses. The .BPL files must be on the system path for an application to use them. When you 10-2Developer’ sGuide, Runtimepackagesdeploy an application, you must make sure that users have correct versions of any required BPLs.

Using packages in an application

To use packages in an application, 1 Load or create a project in the IDE. 2 Choose Project|Options. 3 Choose the Packages tab. 4 Select the “Build with Runtime Packages” check box, and enter one or more package names in the edit box underneath. (Runtime packages associated with installed design-time packages are already listed in the edit box.) To add a package to an existing list, click the Add button and enter the name of the new package in the Add Runtime Package dialog. To browse from a list of available packages, click the Add button, then click the Browse button next to the Package Name edit box in the Add Runtime Package dialog. If you edit the Search Path edit box in the Add Runtime Package dialog, you will be changing Delphi’s global Library Path. You do not need to include file extensions with package names. If you type directly into the Runtime Packages edit box, be sure to separate multiple names with semicolons. For example: VCL40;VCLDB40;VCLDBX40 Packages listed in the Runtime Packages edit box are automatically linked to your application when you compile. Duplicate package names are ignored, and if the edit box is empty the application is compiled without packages. Runtime packages are selected for the current project only. To make the current choices into automatic defaults for new projects, select the “Defaults” check box at the bottom of the dialog. Note When you create an application with packages, you still need to include the names of the original Delphi units in the uses clause of your source files. For example, the source file for your main form might begin like this: unit MainForm; interface uses Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs; Each of the units referenced in this example is contained in the VCL40 package. Nonetheless, you must keep these references in the uses clause, even if you use VCL40 in your application, or you will get compiler errors. In generated source files, Delphi adds these units to the uses clause automatically. Workingwithpackagesandcomponents10-3, Runtimepackages

Dynamically loading packages

A package can be loaded dynamically from an application by calling the LoadPackage function. For example, the following code is executed when a file is chosen to be loaded in an open dialog. with OpenDialog do if Execute then with PackageList.Items do AddObject(FileName, Pointer(LoadPackage(Filename))); Packages are also unloaded dynamically by calling the UnloadPackage procedure. You must be careful that when you unload a package you destroy any objects using those classes and unregister any classes that were registered.

Deciding which runtime packages to use

Delphi ships with several precompiled runtime packages, including VCL40, which supply basic language and component support. The VCL40 package contains the most commonly used components, system functions, and Windows interface elements. It does not include database or Windows 3.1 components, which are available in separate packages. The following table lists the runtime packages shipped with Delphi and the units they contain. Table 10.2 Delphi runtime packages Package Units VCL40.BPL Ax, Buttons, Classes, Clipbrd, Comctrls, Commctrl, Commdlg, Comobj, Comstrs, Consts, Controls, Ddeml, Dialogs, Dlgs, Dsgnintf, Dsgnwnds, Editintf, Exptintf, Extctrls, Extdlgs, Fileintf, Forms, Graphics, Grids, Imm, IniFiles, Isapi, Isapi2, Istreams, Libhelp, Libintf, Lzexpand, Mapi, Mask, Math, Menu, Messages, Mmsystem, Nsapi, Ole2I, Oleconst, Olectnrs, Olectrls, Oledlg, Penwin, Printers, Proxies, Registry, Regstr, Richedit, Shellapi, Shlobj, Stdctrls, Stdvcl, Sysutils, Tlhelp32, Toolintf, Toolwin, Typinfo, Vclcom, Virtintf, Windows, Wininet, Winsock, Winspool, Winsvc VCLX40.BPL Checklst, Colorgrd, Ddeman, Filectrl, Mplayer, Outline, Tabnotbk, Tabs VCLDB40.BPL Bde, Bdeconst, Bdeprov, Db, Dbcgrids, Dbclient, Dbcommon, Dbconsts, Dbctrls, Dbgrids, Dbinpreq, Dblogdlg, Dbpwdlg, Dbtables, Dsintf, Provider, SMintf VCLDBX40.BPL Dblookup, Report DSS40.BPL Mxarrays, Mxbutton, Mxcommon, Mxconsts, Mxdb, Mxdcube, Mxdssqry, Mxgraph, Mxgrid, Mxpivsrc, Mxqedcom, Mxqparse, Mxqryedt, Mxstore, Mxtables, Mxqvb QRPT40.BPL Qr2const, Qrabout, Qralias, Qrctrls, Qrdatasu, Qrexpbld, Qrextra, Qrprev, Qrprgres, Qrprntr, Qrqred32, Quickrpt TEE40.BPL Arrowcha, Bubblech, Chart, Ganttch, Series, Teeconst, Teefunci, Teengine, Teeprocs, Teeshape TEEDB40.BPL Dbchart, Qrtee 10-4Developer’ sGuide, Design- timepackagesTable 10.2 Delphi runtime packages (continued) Package Units TEEUI40.BPL Areaedit, Arrowedi, Axisincr, Axmaxmin, Baredit, Brushdlg, Bubbledi, Custedit, Dbeditch, Editchar, Flineedi, Ganttedi, Ieditcha, Pendlg, Pieedit, Shapeedi, Teeabout, Teegally, Teelisb, Teeprevi, Teexport VCLSMP40.BPL Sampreg, Smpconst Suppose you want to create a client/server database application that uses packages. In this case, you need at least two runtime packages: VCL40 and VCLDB40. If you want to use Outline components in your application, you also need VCLX40. To use these packages, choose Project|Options, select the Packages tab, and enter the following list in the Runtime Packages edit box. VCL40;VCLDB40;VCLX40 Actually, you don’t have to include VCL40, because VCL40 is referenced in the Requires clause of VCLDB40. (See “The Requires clause” on page 10-9.) Your application will compile just the same whether or not VCL40 is included in the Runtime Packages edit box.

Custom packages

A custom package is either a BPL you code and compile yourself, or a precompiled package from a third-party vendor. To use a custom runtime package with an application, choose Project|Options and add the name of the package to the Runtime Packages edit box on the Packages page. For example, suppose you have a statistical package called STATS.BPL. To use it in an application, the line you enter in the Runtime Packages edit box might look like this: VCL40;VCLDB40;STATS If you create your own packages, you can add them to the list as needed.

Design-time packages

Design-time packages are used to install components on the IDE’s Component palette and to create special property editors for custom components. Delphi ships with the following design-time component packages preinstalled in the IDE. Table 10.3 Delphi design-time packages Package Component palette pages DCLSTD40.BPL Standard, Additional, System, Win32, Dialogs DCLTEE40.BPL Additional (TChart component) DCLDB40.BPL Data Access, Data Controls DCLMID40.BPL Data Access (MIDAS) DCL31W40.BPL Win 3.1Workingwithpackagesandcomponents10-5, Design- timepackagesTable 10.3 Delphi design-time packages (continued) Package Component palette pages DCLNET40.BPL Internet NMFAST.BPL DCLSMP40.BPL Samples DCLOCX40.BPL ActiveX DCLQRT40.BPL QReport DCLDSS40.BPL Decision Cube IBSMP40.BPL Samples (IBEventAlerter component) DCLINT40.BPL (International Tools—Resource DLL wizard) These design-time packages work by calling runtime packages, which they reference in their Requires clauses. (See “The Requires clause” on page 10-9.) For example, DCLSTD40 references VCL40. DCLSTD40 itself contains additional functionality that makes most of the standard components available on the Component palette. In addition to preinstalled packages, you can install your own component packages, or component packages from third-party developers, in the IDE. The DCLUSR40 design-time package is provided as a default container for new components.

Installing component packages

In Delphi all components are installed in the IDE as packages. If you’ve written your own components, create and compile a package that contains them. (See “Creating and editing packages” on page 10-7.) Your component source code must follow the model described in Part IV, “Creating custom components.” To install or uninstall your own components, or components from a third-party vendor, follow these steps: 1 If you are installing a new package, copy or move the package files to a local directory. If the package is shipped with .BPL, .DCP, and .DCU files, be sure to copy all of them. (For information about these files, see “Package files created by a successful compilation” on page 10-13.) The directory where you store the .DCP file—and the .DCU files, if they are included with the distribution—must be in the Delphi Library Path. It is customary to put executable files in the Delphi BIN or LIB directory. If the package is shipped as a .DPC (package collection) file, only the one file need be copied; the .DPC file contains the other files. (For more information about package collection files, see “Package collection files” on page 10-13.) 2 Choose Component|Install Packages from the IDE menu, or choose Project| Options and click the Packages tab. 10-6Developer’ sGuide, Creatingandeditingpackages3Alist of available packages appears under “Design packages”. • To install a package in the IDE, select the check box next to it. • To uninstall a package, deselect its check box. • To see a list of components included in an installed package, select the package and click Components. • To add a package to the list, click Add and browse in the Open Package dialog box for the directory where the .BPL or .DPC file resides (see step 1). Select the .BPL or .DPC file and click Open. If you select a .DPC file, a new dialog box appears to handle the extraction of the .BPL and other files from the package collection. • To remove a package from the list, select the package and click Remove. 4 Click OK. The components in the package are installed on the Component palette pages specified in the components’ RegisterComponents procedure, with the names they were assigned in the same procedure. New projects are created with all available packages installed, unless you change the default settings. To make the current installation choices into the automatic default for new projects, check the Default check box at the bottom of the dialog box. To remove components from the Component palette without uninstalling a package, select Component|Configure Palette, or select Tools|Environment Options and click the Palette tab. The Palette options tab lists each installed component along with the name of the Component palette page where it appears. Selecting any component and clicking Hide removes the component from the palette.

Creating and editing packages

Creating a package involves specifying • A name for the package. • A list of other packages to be required by, or linked to, the new package. • A list of unit files to be contained by, or bound into, the package when it is compiled. The package is essentially a wrapper for these source-code units, which contain the functionality of the compiled .BPL. The Contains clause is where you put the source-code units for custom components that you want to compile into a package. Package source files, which end with the .DPK extension, are generated by the Package editor. Workingwithpackagesandcomponents10-7, Creatingandeditingpackages

Creating a package

To create a package, follow the procedure below. Refer to “Understanding the structure of a package” on page 10-9 for more information about the steps outlined here. 1 Choose File|New, select the Package icon, and click OK. 2 The generated package is displayed in the Package editor. 3 The Package editor shows a Requires node and a Contains node for the new package. 4 To add a unit to the contains clause, click the Add to package speed button. In the Add unit page, type a .PAS file name in the Unit file name edit box, or click Browse to browse for the file, and then click OK. The unit you’ve selected appears under the Contains node in the Package editor. You can add additional units by repeating this step. 5 To add a package to the requires clause, click the Add to package speed button. In the Requires page, type a .DCP file name in the Package name edit box, or click Browse to browse for the file, and then click OK. The package you’ve selected appears under the Requires node in the Package editor. You can add additional packages by repeating this step. 6 Click the Options speedbutton, and decide what kind of package you want to build. • To create a design-time–only package (a package that cannot be used at runtime), select the Designtime only radio button. (Or add the {$DESIGNONLY} compiler directive to the DPK file.) • To create a runtime-only package (a package that cannot be installed), select the Runtime only radio button. (Or add the {$RUNONLY} compiler directive to the DPK file.) • To create a package that is available at both design time and runtime, select the Designtime and runtime radio button. 7 In the Package editor, click the Compile package speed button to compile your package.

Editing an existing package

There are several ways to open an existing package for editing, • Choose File|Open (or File|Reopen) and select a DPK file. • Choose Component|Install Packages, select a package from the Design Packages list, and click the Edit button. • When the Package editor is open, select one of the packages in the Requires node, right-click, and choose Open. 10-8Developer’ sGuide, Creatingandeditingpackages

Editing package source files manually

Package source files, like project files, are generated by Delphi from information you supply. Like project files, they can also be edited manually. A package source file should be saved with the .DPK (Delphi package) extension to avoid confusion with other files containing Object Pascal source code. To open a package source file in the Code editor, 1 Open the package in the Package editor. 2 Right-click in the Package editor and select View Source. • The package heading specifies the name for the package. • The requires clause lists other, external packages used by the current package. If a package does not contain any units that use units in another package, then it doesn’t need a requires clause. • The contains clause identifies the unit files to be compiled and bound into the package. All units used by contained units which do not exist in required packages will also be bound into the package, although they won’t be listed in the contains clause (the compiler will give a warning). For example, the following code declares the VCLDB40 package. package VCLDB40; requires VCL40; contains Db, Dbcgrids, Dbctrls, Dbgrids, Dbinpreq, Dblogdlg, Dbpwdlg, Dbtables, mycomponent in ‘C:\components\mycomponent.pas’; end.

Understanding the structure of a package

Naming packages Package names must be unique within a project. If you name a package STATS, the Package editor generates a source file for it called STATS.DPK; the compiler generates an executable and a binary image called STATS.BPL and STATS.DCP, respectively. Use STATS to refer to the package in the requires clause of another package, or when using the package in an application. The Requires clause The requires clause specifies other, external packages that are used by the current package. An external package included in the requires clause is automatically linked at compile time into any application that uses both the current package and one of the units contained in the external package. If the unit files contained in your package make references to other packaged units, the other packages should appear in your package’s requires clause or you should add them. If the other packages are omitted from the requires clause, the compiler will import them into your package ‘implicitly contained units’. Workingwithpackagesandcomponents10-9, CreatingandeditingpackagesNote Most packages that you create will require VCL40. Any package that depends on VCL units (including SysUtils) must list VCL40, or another package that requires VCL40, in its requires clause. Avoiding circular package references Packages cannot contain circular references in their requires clause. This means that • A package cannot reference itself in its own requires clause. • A chain of references must terminate without rereferencing any package in the chain. If package A requires package B, then package B cannot require package A; if package A requires package B and package B requires package C, then package C cannot require package A. Handling duplicate package references Duplicate references in a package’s requires clause—or in the Runtime Packages edit box—are ignored by the compiler. For programming clarity and readability, however, you should catch and remove duplicate package references. The Contains clause The contains clause identifies the unit files to be bound into the package. If you are writing your own package, put your source code in PAS files and include them in the contains clause. Avoiding redundant source code uses A package cannot appear in the contains clause of another package. All units included directly in a package’s contains clause, or included indirectly in any of those units, are bound into the package at compile time. A unit cannot be contained (directly or indirectly) in more than one package used by the same application, including the Delphi IDE. This means that if you create a package that contains one of the units in VCL40, you won’t be able to install your package in the IDE. To use an already-packaged unit file in another package, put the first package in the second package’s requires clause.

Compiling packages

You can compile a package from the IDE or from the command line. To recompile a package by itself from the IDE, 1 Choose File|Open. 2 Select Delphi Package Source (*.DPK) from the Files Of Type drop-down list. 3 Select a .DPK file in the dialog. 4 When the Package editor opens, click the Compile speed button. You can insert compiler directives into your package source code. For more information, see “Package-specific compiler directives,” below. 10-10Developer’ sGuide, CreatingandeditingpackagesIf you compile from the command line, new package-specific switches are available. For more information, see “Using the command-line compiler and linker” on page 10-12.

Package-specific compiler directives

The following table lists package-specific compiler directives that you can insert into your source code. Table 10.4 Package-specific compiler directives Directive Purpose {$IMPLICITBUILD OFF} Prevents a package from being implicitly recompiled later. Use in .DPK files when compiling packages that provide low-level functionality, that change infrequently between builds, or whose source code will not be distributed. {$G-} or {IMPORTEDDATA OFF} Disables creation of imported data references. This directive increases memory-access efficiency, but prevents the unit where it occurs from referencing variables in other packages. {$WEAKPACKAGEUNIT ON} Packages unit “weakly.” See “Weak packaging” on page 10-11 below. {$DENYPACKAGEUNIT ON} Prevents unit from being placed in a package. {$DESIGNONLY ON} Compiles the package for installation in the IDE. (Put in .DPK file.) {$RUNONLY ON} Compiles the package as runtime only. (Put in .DPK file.) Note Including {$DENYPACKAGEUNIT ON} in your source code prevents the unit file from being packaged. Including {$G-} or {IMPORTEDDATA OFF} may prevent a package from being used in the same application with other packages. Packages compiled with the {$DESIGNONLY ON} directive should not ordinarily be used in applications, since they contain extra code required by the IDE. Other compiler directives may be included, if appropriate, in package source code. See Compiler directives in the online help for information on compiler directives not discussed here. Weak packaging The $WEAKPACKAGEUNIT directive affects the way a .DCU file is stored in a package’s .DCP and .BPL files. (For information about files generated by the compiler, see “Package files created by a successful compilation” on page 10-13.) If {$WEAKPACKAGEUNIT ON} appears in a unit file, the compiler omits the unit from BPLs when possible, and creates a non-packaged local copy of the unit when it is required by another application or package. A unit compiled with this directive is said to be “weakly packaged.” For example, suppose you’ve created a package called PACK that contains only one unit, UNIT1. Suppose UNIT1 does not use any further units, but it makes calls to RARE.DLL. If you put {$WEAKPACKAGEUNIT ON} in UNIT1.PAS when you compile your package, UNIT1 will not be included in PACK.BPL; you will not have to distribute copies of RARE.DLL with PACK. However, UNIT1 will still be includedWorkingwithpackagesandcomponents10-11, Creatingandeditingpackagesin PACK.DCP. If UNIT1 is referenced by another package or application that uses PACK, it will be copied from PACK.DCP and compiled directly into the project. Now suppose you add a second unit, UNIT2, to PACK. Suppose that UNIT2 uses UNIT1. This time, even if you compile PACK with {$WEAKPACKAGEUNIT ON} in UNIT1.PAS, the compiler will include UNIT1 in PACK.BPL. But other packages or applications that reference UNIT1 will use the (non-packaged) copy taken from PACK.DCP. Note Unit files containing the {$WEAKPACKAGEUNIT ON} directive must not have global variables, initialization sections, or finalization sections. The $WEAKPACKAGEUNIT directive is an advanced feature intended for developers who distribute their BPLs to other Delphi programmers. It can help you to avoid distribution of infrequently used DLLs, and to eliminate conflicts among packages that may depend on the same external library. For example, Delphi’s PenWin unit references PENWIN.DLL. Most projects don’t use PenWin, and most computers don’t have PENWIN.DLL installed on them. For this reason, the PenWin unit is weakly packaged in VCL40. When you compile a project that uses PenWin and the VCL40 package, PenWin is copied from VCL40.DCP and bound directly into your project; the resulting executable is statically linked to PENWIN.DLL. If PenWin were not weakly packaged, two problems would arise. First, VCL40 itself would be statically linked to PENWIN.DLL, and so you could not load it on any computer which didn’t have PENWIN.DLL installed. Second, if you tried to create a package that contained PenWin, a compiler error would result because the PenWin unit would be contained in both VCL40 and your package. Thus, without weak packaging, PenWin could not be included in standard distributions of VCL40.

Using the command-line compiler and linker

When you compile from the command line, you can use the package-specific switches listed in the following table. Table 10.5 Package-specific command-line compiler switches Switch Purpose -$G- Disables creation of imported data references. Using this switch increases memory-access efficiency, but prevents packages compiled with it from referencing variables in other packages. -LEpath Specifies the directory where the package BPL file will be placed. -LNpath Specifies the directory where the package DCP file will be placed. -LUpackage Use packages. -Z Prevents a package from being implicitly recompiled later. Use when compiling packages that provide low-level functionality, that change infrequently between builds, or whose source code will not be distributed. Note Using the -$G- switch may prevent a package from being used in the same application with other packages. Other command-line options may be used, if appropriate, when compiling packages. See “The Command-line compiler” in the online help for information on command-line options not discussed here. 10-12Developer’ sGuide, DeployingpackagesPackage files created by a successful compilation To create a package, you compile a source file that has a .DPK extension. The base name of the .DPK file becomes the base name of the files generated by the compiler. For example, if you compile a package source file called TRAYPAK.DPK, the compiler creates a package called TRAYPAK.BPL. The following table lists the files produced by the successful compilation of a package. Table 10.6 Compiled package files File extension Contents DCP A binary image containing a package header and the concatenation of all DCU files in the package. A single DCP file is created for each package. The base name for the DCP is the base name of the DPK source file. DCU A binary image for a unit file contained in a package. One DCU is created, when necessary, for each unit file. BPL The runtime package. This file is a Windows DLL with special Delphi-specific features. The base name for the BPL is the base name of the DPK source file.

Deploying packages Deploying applications that use packages

When distributing an application that uses runtime packages, make sure that your users have the application’s .EXE file as well as all the library (.BPL or .DLL) files that the application calls. If the library files are in a different directory from the .EXE file, they must be accessible through the user’s Path. You may want to follow the convention of putting library files in the Windows\System directory. If you use InstallShield Express, your installation script can check the user’s system for any packages it requires before blindly reinstalling them.

Distributing packages to other developers

If you distribute runtime or design-time packages to other Delphi developers, be sure to supply both .DCP and .BPL files. You will probably want to include .DCU files as well.

Package collection files

Package collections (.DPC files) offer a convenient way to distribute packages to other developers. Each package collection contains one or more packages, including BPLs and any additional files you want to distribute with them. When a package collection is selected for IDE installation, its constituent files are automatically extracted from their .DPC container; the Installation dialog box offers a choice of installing all packages in the collection or installing packages selectively. Workingwithpackagesandcomponents10-13, DeployingpackagesTo create a package collection, 1 Choose Tools|Package Collection Editor to open the Package Collection editor. 2 Click the Add Package speed button, then select a BPL in the Select Package dialog and click Open. To add more BPLs to the collection, click the Add Package speed button again. A tree diagram on the left side of the Package editor displays the BPLs as you add them. To remove a package, select it and click the Remove Package speed button. 3 Select the Collection node at the top of the tree diagram. On the right side of the Package Collection editor, two fields will appear: • In the Author/Vendor Name edit box, you can enter optional information about your package collection that will appear in the Installation dialog when users install packages. • Under Directory List, list the default directories where you want the files in your package collection to be installed. Use the Add, Edit, and Delete buttons to edit this list. For example, suppose you want all source code files to be copied to the same directory. In this case, you might enter Source as a Directory Name with C:\MyPackage\Source as the Suggested Path. The Installation dialog box will display C:\MyPackage\Source as the suggested path for the directory. 4 In addition to BPLs, your package collection can contain .DCP, .DCU, and .PAS (unit) files, documentation, and any other files you want to include with the distribution. Ancillary files are placed in file groups associated with specific packages (BPLs); the files in a group are installed only when their associated BPL is installed. To place ancillary files in your package collection, select a BPL in the tree diagram and click the Add File Group speed button; type a name for the file group. Add more file groups, if desired, in the same way. When you select a file group, new fields will appear on the right in the Package Collection editor, • In the Install Directory list box, select the directory where you want files in this group to be installed. The drop-down list includes the directories you entered under Directory List in step 3, above. • Check the Optional Group check box if you want installation of the files in this group to be optional. • Under Include Files, list the files you want to include in this group. Use the Add, Delete, and Auto buttons to edit the list. The Auto button allows you to select all files with specified extensions that are listed in the contains clause of the package; the Package Collection editor uses Delphi’s global Library Path to search for these files. 5 You can select installation directories for the packages listed in the requires clause of any package in your collection. When you select a BPL in the tree diagram, four new fields appear on the right side of the Package Collection editor: • In the Required Executables list box, select the directory where you want the .BPL files for packages listed in the requires clause to be installed. (The drop-down list includes the directories you entered under Directory List in step 3, above.) The Package Collection Editor searches for these files using Delphi’s global Library Path and lists them under Required Executable Files. 10-14Developer’ sGuide, Deployingpackages• In the Required Libraries list box, select the directory where you want the .DCP files for packages listed in the requires clause to be installed. (The drop-down list includes the directories you entered under Directory List in step 3, above.) The Package Collection Editor searches for these files using Delphi’s global Library Path and lists them under Required Library Files. 6 To save your package collection source file, choose File|Save. Package collection source files should be saved with the .PCE extension. 7 To build your package collection, click the Compile speed button. The Package Collection editor generates a .DPC file with the same name as your source (.PCE) file. If you have not yet saved the source file, the editor queries you for a file name before compiling. To edit or recompile an existing .PCE file, select File|Open in the Package Collection editor. Workingwithpackagesandcomponents10-15, 10-16Developer’ sGuide,

Chapter

Chapter 11Creating international applications This chapter discusses guidelines for writing applications that you plan to distribute to an international market. By planning ahead, you can reduce the amount of time and code necessary to make your application function in its foreign market as well as in its domestic market.

Internationalization and localization

To create an application that you can distribute to foreign markets, there are two major steps that need to be performed: • Internationalization • Localization

Internationalization

Internationalization is the process of enabling your program to work in multiple locales. A locale is the user’s environment, which includes the cultural conventions of the target country as well as the language. Windows supports a large set of locales, each of which is described by a language and country pair.

Localization

Localization is the process of translating an application to function in a specific locale. In addition to translating the user interface, localization may include functionality customization. For example, a financial application may be modified to be aware of the different tax laws in different countries. Creatinginternationalapplications11-1, Internationalizingapplications

Internationalizing applications

It is not difficult to create internationalized applications. You need to complete the following steps: 1 You must enable your code to handle strings from international character sets. 2 You need to design your user interface so that it can accommodate the changes that result from localization. 3 You need to isolate all resources that need to be localized.

Enabling application code

You must make sure that the code in your application can handle the strings it will encounter in the various target locales. Character sets The United States edition of Windows 95 and Windows NT uses the ANSI Latin-1 (1252) character set. However, other editions of Windows use different character sets. For example, the Japanese version of Windows uses the Shift-Jis character set (code page 932), which represents Japanese characters as 1- or 2-byte character codes. OEM and ANSI character sets It is sometimes necessary to convert between the Windows character set (ANSI) and the character set specified by the code page of the user’s machine (called the OEM character set). Double byte character sets The ideographic character sets used in Asia cannot use the simple 1:1 mapping between characters in the language and the one byte (8-bit) char type. These languages have too many characters to be represented using the 1-byte char. Instead, characters are represented by a mix of 1- and 2-byte character codes. The first byte of every 2-byte character code is taken from a reserved range that depends on the specific character set. The second byte can sometimes be the same as the character code for a separate 1-byte character, or it can fall in the range reserved for the first byte of 2-byte characters. Thus, the only way to tell whether a particular byte in a string represents a single character or part of a 2-byte character is to read the string, starting at the beginning, parsing it into 2-byte characters when a lead byte from the reserved range is encountered. When writing code for Asian locales, you must be sure to handle all string manipulation using functions that are enabled to parse strings into 1- and 2-byte 11-2Developer’ sGuide, Internationalizingapplicationscharacters. Delphi provides you with a number of runtime library functions that allow you to do this. These functions are as follows: AdjustLineBreaks AnsiStrLower ExtractFileDir AnsiCompareFileName AnsiStrPos ExtractFileExt AnsiExtractQuotedStr AnsiStrRScan ExtractFileName AnsiLastChar AnsiStrScan ExtractFilePath AnsiLowerCase AnsiStrUpper ExtractRelativePath AnsiLowerCaseFileName AnsiUpperCase FileSearch AnsiPos AnsiUpperCaseFileName IsDelimiter AnsiQuotedStr ByteToCharIndex IsPathDelimiter AnsiStrComp ByteToCharLen LastDelimiter AnsiStrIComp ByteType StrByteType AnsiStrLastChar ChangeFileExt StringReplace AnsiStrLComp CharToByteIndex WrapText AnsiStrLIComp CharToByteLen Remember that the length of the strings in bytes does not necessarily correspond to the length of the string in characters. Be careful not to truncate strings by cutting a 2-byte character in half. Do not pass characters as a parameter to a function or procedure, since the size of a character can’t be known up front. Instead, always pass a pointer to a character or a string. Wide characters Another approach to working with ideographic character sets is to convert all characters to a wide character encoding scheme such as Unicode. Wide characters are two bytes instead of one, so that the character set can represent many more different characters. Using a wide character encoding scheme has the advantage that you can make many of the usual assumptions about strings that do not work for MBCS systems. There is a direct relationship between the number of bytes in the string and the number of characters in the string. You do not need to worry about cutting characters in half or mistaking the second half of a character for the start of a different character. The biggest disadvantage of working with wide characters is that Windows 95 only supports a few wide character API function calls. Because of this, the VCL components represent all string values as single byte or MBCS strings. Translating between the wide character system and the MBCS system every time you set a string property or read its value would require tremendous amounts of extra code and slow your application down. However, you may want to translate into wide characters for some special string processing algorithms that need to take advantage of the 1:1 mapping between characters and WideChars. Creatinginternationalapplications11-3, Internationalizingapplications

Including bi-directional functionality in applications

Some languages do not follow the left to right reading order commonly found in western languages, but rather read words right to left and numbers left to right. These languages are termed bi-directional (BiDi) because of this separation. The most common bi-directional languages are Arabic and Hebrew, although other Middle East languages are also bi-directional. The Delphi VCL provides support for applications developed for bi-directional localization by using two properties to the VCL: BiDiMode and ParentBiDiMode. The following table lists the VCL objects that have these properties: Table 11.1 VCL objects that support BiDi Component palette page VCL object Standard TButton TCheckBox TComboBox TEdit TGroupBox TLabel TListBox TMainMenu TMemo TPanel TPopupMenu TRadioButton TRadioGroup TScrollBar Additional TBitBtn TCheckListBox TDrawGrid TMaskEdit TScrollBox TSpeedButton TStaticLabel TStringGrid Win32 TDateTimePicker THeaderControl TListView TMonthCalendar TPageControl TRichEdit TStatusBar TTabControl 11-4Developer’ sGuide, InternationalizingapplicationsTable 11.1 VCL objects that support BiDi (continued) Component palette page VCL object Data Controls TDBCheckBox TDBComboBox TDBEdit TDBGrid TDBListBox TDBLookupComboBox TDBLookupListBox TDBMemo TDBRadioGroup TDBRichEdit TDBText QReport TQRDBRichText TQRDBText TQRExpr TQRLabel TQRMemo TQRRichText TQRSysData Other classes TApplication (has no ParentBiDiMode) TForm THintWindow (has no ParentBiDiMode) TStatusPanel THeaderSection Note THintWindow picks up the BiDiMode of the control that activated the hint. Bi-directional properties The objects listed in Table 11.1, “VCL objects that support BiDi,” on page 11-4 have two properties: BiDiMode and ParentBiDiMode.

BiDiMode property

The property BiDiMode is a new enumerated type, TBiDiMode, with four states: bdLeftToRight, bdRightToLeft, bdRightToLeftNoAlign, and bdRightToLeftReadingOnly. bdLeftToRight bdLeftToRight draws text using left to right reading order, and the alignment and scroll bars are not changed. For instance, when entering right to left text, such as Arabic or Hebrew, the cursor goes into push mode and the text is entered right to left. Latin text, such as English or French, is entered left to right. bdLeftToRight is the default value. Creatinginternationalapplications11-5, InternationalizingapplicationsFigure 11.1 TListBox set to bdLeftToRight bdRightToLeft bdRightToLeft draws text using right to let reading order, the alignment is changed and the scroll bar is moved. Text is entered as normal for right-to-left languages such as Arabic or Hebrew. When the keyboard is changed to a Latin language, the cursor goes into push mode and the text is entered left-to-right. Figure 11.2 TListBox set to bdRightToLeft bdRightToLeftNoAlign bdRightToLeftNoAlign draws text using right to left reading order, the alignment is not changed, and the scroll bar is moved. Figure 11.3 TListBox set to bdRightToLeftNoAlign bdRightToLeftReadingOnly bdRightToLeftReadingOnly draws text using right to left reading order, and the alignment and scroll bars are not changed. Figure 11.4 TListBox set to bdRightToLeftReadingOnly ParentBiDiMode property ParentBiDiMode is a Boolean property. When True (the default) the control looks to its parent to determine what BiDiMode to use. If the control is a TForm object, the form uses the BiDiMode setting from Application. If all the ParentBiDiMode properties are True, when you change Application’s BiDiMode property, all forms and controls in the project are updated with the new setting. FlipChildren method The FlipChildren method allows you to flip the position of a container control’s children. Container controls are controls that can accept other controls, such as TForm, TPanel, and TGroupbox. FlipChildren has a single boolean parameter, AllLevels. When False, only the immediate children of the container control are flipped. When True, all the levels of children in the container control are flipped. Delphi flips the controls by changing the Left property and the alignment of the control. If a control’s left side is five pixels from the left edge of its parent control, 11-6Developer’ sGuide, Internationalizingapplicationsafter it is flipped the edit control’s right side is five pixels from the right edge of the parent control. If the edit control is left aligned, calling FlipChildren will make the control right aligned. To flip a control at design-time select Edit|Flip Children and select either All or Selected, depending on whether you want to flip all the controls, or just the children of the selected control. You can also flip a control by selecting the control on the form, right-clicking, and selecting Flip Children from the context menu. Note Selecting an edit control and issuing a Flip Children|Selected command does nothing. This is because edit controls are not containers. Additional methods There are several other methods useful for developing applications for bi-directional users. Method Description OkToChangeFieldAlignment Used with database controls. Checks to see if the alignment of a control can be changed. DBUseRightToLeftAlignment A wrapper for database controls for checking alignment. ChangeBiDiModeAlignment Changes the alignment parameter passed to it. No check is done for BiDiMode setting, it just converts left alignment into right alignment and vice versa, leaving center-aligned controls alone. IsRightToLeft Returns True if any of the right to left options are selected. If it returns False the control is in left to right mode. UseRightToLeftReading Returns True if the control is using right to left reading. UseRightToLeftAlignment Returns True if the control is using right to left alignment. It can be overriden for customization. UseRightToLeftScrollBar Returns True if the control is using a left scroll bar. DrawTextBiDiModeFlags Returns the correct draw text flags for the BiDiMode of the control. DrawTextBiDiModeFlagsReadingOnly Returns the correct draw text flags for the BiDiMode of the control, limiting the flag to its reading order. AddBiDiModeExStyle Adds the appropriate ExStyle flags to the control that is being created.

Locale-specific features

You can add extra features to your application for specific locales. In particular, for Asian language environments, you may want your application to control the input method editor (IME) that is used to convert the keystrokes typed by the user into character strings. VCL components offer you support in programming the IME. Most windowed controls that work directly with text input have an ImeName property that allows you to specify a particular IME that should be used when the control has input focus. They also provide an ImeMode property that specifies how the IME should convertCreatinginternationalapplications11-7, Internationalizingapplicationskeyboard input. TWinControl introduces several protected methods that you can use to control the IME from classes you define. In addition, the global Screen variable provides information about the IMEs available on the user’s system. The global Screen variable also provides information about the keyboard mapping installed on the user’s system. You can use this to obtain locale-specific information about the environment in which your application is running.

Designing the user interface

When creating an application for several foreign markets, it is important to design your user interface so that it can accommodate the changes that occur during translation. Text All text that appears in the user interface must be translated. English text is almost always shorter than its translations. Design the elements of your user interface that display text so that there is room for the text strings to grow. Create dialogs, menus, status bars, and other user interface elements that display text so that they can easily display longer strings. Avoid abbreviations—they do not exist in languages that use ideographic characters. Short strings tend to grow in translation more than long phrases. Table 11.2 provides a rough estimate of how much expansion you should plan for given the length of your English strings: Table 11.2 Estimating string lengths Length of English string (in characters) Expected increase 1-5 100% 6-12 80% 13-20 60% 21-30 40% 31-50 20% over 50 10% Graphic images Ideally, you will want to use images that do not require translation. Most obviously, this means that graphic images should not include text, which will always require translation. If you must include text in your images, it is a good idea to use a label object with a transparent background over an image rather than including the text as part of the image. There are other considerations when creating graphic images. Try to avoid images that are specific to a particular culture. For example, mailboxes in different countries look very different from each other. Religious symbols are not appropriate if your application is intended for countries that have different dominant religions. Even color can have different symbolic connotations in different cultures. 11-8Developer’ sGuide, InternationalizingapplicationsFormats and sort order The date, time, number, and currency formats used in your application should be localized for the target locale. If you use only the Windows formats, there is no need to translate formats, as these are taken from the user’s Windows Registry. However, if you specify any of your own format strings, be sure to declare them as resourced constants so that they can be localized. The order in which strings are sorted also varies from country to country. Many European languages include diacritical marks that are sorted differently, depending on the locale. In addition, in some countries, 2-character combinations are treated as a single character in the sort order. For example, in Spanish, the combination ch is sorted like a single unique letter between c and d. Sometimes a single character is sorted as if it were two separate characters, such as the German eszett. Keyboard mappings Be careful with key-combinations shortcut assignments. Not all the characters available on the US keyboard are easily reproduced on all international keyboards. Where possible, use number keys and function keys for shortcuts, as these are available on virtually all keyboards.

Isolating resources

The most obvious task in localizing an application is translating the strings that appear in the user interface. To create an application that can be translated without altering code everywhere, the strings in the user interface should be isolated into a single module. Delphi automatically creates a .DFM file that contains the resources for your menus, dialogs, and bitmaps. In addition to these obvious user interface elements, you will need to isolate any strings, such as error messages, that you present to the user. String resources are not included in the .DFM file. You can isolate them by declaring constants for them using the resourcestring keyword. For more information about resource string constants, see the Object Pascal Language Guide. It is best to include all resource strings in a single, separate unit.

Creating resource DLLs

Isolating resources simplifies the translation process. The next level of resource separation is the creation of a resource DLL. A resource DLL contains all the resources and only the resources for a program. Resource DLLs allow you to create a program that supports many translations simply by swapping the resource DLL. Thus simplifying deployment of many translations. Use the Resource DLL wizard to create a resource DLL for your program. The Resource DLL wizard requires an open, saved, compiled project. It will create an RC file that contains the string tables from used RC files and resourcestring strings of the project, and generate a project for a resource only DLL that contains the relevant forms and the created RES file. The RES file is compiled from the new RC file. Creatinginternationalapplications11-9, InternationalizingapplicationsYou should create a resource DLL for each translation you want to support. Each resource DLL should have a file name extension specific to the target locale. The first two characters indicate the target language, and the third character indicates the country of the locale. If you use the Resource DLL wizard, this is handled for you. Otherwise, use the following code obtain the locale code for the target translation: unit locales; interface uses Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls; type TForm1 = class(TForm) Button1: TButton; LocaleList: TListBox; procedure Button1Click(Sender: TObject); private { Private declarations } public { Public declarations } end; var Form1: TForm1; implementation {$R *.DFM} function GetLocaleData(ID: LCID; Flag: DWORD): string; var BufSize: Integer; begin BufSize := GetLocaleInfo(ID, Flag, nil, 0); SetLength(Result, BufSize); GetLocaleinfo(ID, Flag, PChar(Result), BufSize); SetLength(Result, BufSize - 1); end; { Called for each supported locale. } function LocalesCallback(Name: PChar): Bool; stdcall; var LCID: Integer; begin LCID := StrToInt('$' + Copy(Name, 5, 4)); Form1.LocaleList.Items.Add(GetLocaleData(LCID, LOCALE_SLANGUAGE)); Result := Bool(1); end; procedure TForm1.Button1Click(Sender: TObject); begin EnumSystemLocales(@LocalesCallback, LCID_SUPPORTED); end; end. 11-10Developer’ sGuide, Internationalizingapplications

Using resource DLLs

The executable, DLLs, and packages that make up your application contain all the necessary resources. However, to replace those resources by localized versions, you need only ship your application with localized resource DLLs that have the same name as your EXE, DLL, or BPL files. When your application starts up, it checks the locale of the local system. If it finds any resource DLLs with the same name as the EXE, DLL, or BPL files it is using, it checks the extension on those DLLs. If the extension of the resource module matches the language and country of the system locale, your application will use the resources in that resource module instead of the resources in the executable, DLL, or package. If there is not a resource module that matches both the language and the country, your application will try to locate a resource module that matches just the language. If there is no resource module that matches the language, your application will use the resources compiled with the executable, DLL, or package. If you want your application to use a different resource module than the one that matches the locale of the local system, you can set a locale override entry in the Windows registry. Under the HKEY_CURRENT_USER\Software\Borland\Locales key, add your application’s path and file name as a string value and set the data value to the extension of your resource DLLs. At startup, the application will look for resource DLLs with this extension before trying the system locale. Setting this registry entry allows you to test localized versions of your application without changing the locale on your system. For example, the following procedure can be used in an install or setup program to set the registry key value that indicates the locale to use when loading Delphi applications: procedure SetLocalOverrides(FileName: string, LocaleOverride: string); var Reg: TRegistry; begin Reg := TRegistry.Create; try if Reg.OpenKey(‘Software\Borland\Locales’, True) then Reg.WriteString(LocalOverride, FileName); finally Reg.Free; end; Within your application, use the global FindResourceHInstance function to obtain the handle of the current resource module. For example: LoadStr(FindResourceHInstance(HInstance), IDS_AmountDueName, szQuery, SizeOf(szQuery)); You can ship a single application that adapts itself automatically to the locale of the system it is running on, simply by providing the appropriate resource DLLs. Creatinginternationalapplications11-11, Localizingapplications

Dynamic switching of resource DLLs

In addition to locating a resource DLL at application startup, it is possible to switch resource DLLs dynamically at runtime. To add this functionality to your own applications, you should include the unit ReInit in your uses statement. In order to switch languages, you should call LoadNewResourceModule, passing the LCID for the new language, and then call ReinitializeForms. For example, the following code switches the interface language to French: const FRENCH = (SUBLANG_FRENCH shl 10) or LANG_FRENCH; if LoadNewResourceModule(FRENCH) <> 0 then ReinitializeForms; The advantage of this technique is that the current instance of the application and all of its forms are used. It is not necessary to update the registry settings and restart the application or reacquire resources required by the application, such as logging in to database servers. When you switch resource DLLs the properties specified in the new DLL overwrite the properties in the running instances of the forms. Note Any changes made to the form properties at runtime will be lost. Once the new DLL is loaded, default values are not reset. Avoid code that assumes that the form objects are reinitialized to the their start-up state, apart from differences due to localization.

Localizing applications

Once your application is internationalized, you can create localized versions for the different foreign markets in which you want to distribute it.

Localizing resources

Ideally your resources have been isolated into a resource DLL that contains DFM files and a RES file. You can then open the forms in the IDE and translate the relevant properties. Note In a resource DLL project, you can not add or delete components. However, it is possible to change properties that could cause runtime errors. Only modify the properties that should be translated. You can open the RC file and translate relevant strings. Use the StringTable editor by opening the RC file from the Project Manager. 11-12Developer’ sGuide,

Chapter

Chapter 12Deploying applications Once your Delphi application is up and running, you can deploy it. That is, you can make it available for others to run. A number of steps must be taken to deploy an application to another computer so that the application is completely functional. The steps required by a given application vary, depending on the type of application. The following sections describe those steps for deploying applications: • Deploying general applications • Deploying database applications • Deploying Web applications • Programming for varying host environments • Software license requirements

Deploying general applications

Beyond the executable file, an application may require a number of supporting files, such as DLLs, package files, and helper applications. In addition, the Windows registry may need to contain entries for an application, from specifying the location of supporting files to simple program settings. The process of copying an application’s files to a computer and making any needed registry settings can be automated by an installation program, such as InstallShield Express. These are the main deployment concerns common to nearly all types of applications: • Using installation programs • Identifying application files Delphi applications that access databases and those that run across the Web require additional installation steps beyond those that apply to general applications. For additional information on installing database applications, see “Deploying database applications” on page 12-4. For more information on installing Web applications, see “Deploying Web applications” on page 12-7. For more information on installing ActiveX controls, see “Deploying an ActiveX control on the Web” on page 47-15. Deployingapplications12-1, Deployinggeneralapplications

Using installation programs

Simple Delphi applications that consist of only an executable file are easy to install on a target computer. Just copy the executable file onto the computer. However, more complex applications that comprise multiple files require more extensive installation procedures. These applications require dedicated installation programs. Setup toolkits automate the process of creating installation programs, often without needing to write any code. Installation programs created with Setup toolkits perform various tasks inherent to installing Delphi applications, including: copying the executable and supporting files to the host computer, making Windows registry entries, and installing the Borland Database Engine for database applications. InstallShield Express is a setup toolkit that is bundled with Delphi. InstallShield Express is certified for use with Delphi and the Borland Database Engine. InstallShield Express is not automatically installed when Delphi is installed, and must be manually installed to be used to create installation programs. Run the installation program from the Delphi CD to install InstallShield Express. For more information on using InstallShield Express to create installation programs, see the InstallShield Express online help. Other setup toolkits are available, however, you should only use those certified to deploy the Borland Database Engine. Identifying application files Besides the executable file, a number of other files may need to be distributed with an application. • Application files, listed by file name extension • Package files • ActiveX controls Application files, listed by file name extension The following types of files may need to be distributed with an application. Table 12.1 Application files Type File name extension Program files .EXE and .DLL Package files .BPL and .DCP Help files .HLP, .CNT, and .TOC (if used) ActiveX files .OCX (sometimes supported by a DLL) Local table files .DBF, .MDX, .DBT, .NDX, .DB, .PX, .Y*, .X*, .MB, .VAL, .QBE Package files If the application uses runtime packages, those package files need to be distributed with the application. InstallShield Express handles the installation of package files the same as DLLs, copying the files and making necessary entries in the Windows registry. Borland recommends installing the runtime package files supplied by 12-2Developer’ sGuide, DeployinggeneralapplicationsBorland in the Windows\System directory. This serves as a common location so that multiple applications would have access to a single instance of the files. For packages of your own making, it is recommended that you install them in the same directory as the application. Only the .BPL files need to be distributed. If you are distributing packages to other developers, supply both the .BPL and the .DCP files. ActiveX controls Certain components bundled with Delphi are ActiveX controls. The component wrapper is linked into the application’s executable file (or a runtime package), but the .OCX file for the component also needs to be deployed with the application. These components include • Chart FX, copyright SoftwareFX Inc. • VisualSpeller Control, copyright Visual Components, Inc. • Formula One (spreadsheet), copyright Visual Components, Inc. • First Impression (VtChart), copyright Visual Components, Inc. • Graph Custom Control, copyright Bits Per Second Ltd. ActiveX controls of your own creation need to be registered on the deployment computer before use. Installation programs such as InstallShield Express automate this registration process. To manually register an ActiveX control, use the TRegSvr demo application or the Microsoft utility REGSERV32.EXE (not included with all Windows versions). DLLs that support an ActiveX control also need to be distributed with an application. Helper applications Helper applications are separate programs without which your Delphi application would be partially or completely unable to function. Helper applications may be those supplied with Windows, by Borland, or they might be third-party products. An example of a helper application is the InterBase utility program Server Manager, which administers InterBase databases, users, and security. If an application depends on a helper program, be sure to deploy it with your application, where possible. Distribution of helper programs may be governed by redistribution license agreements. Consult the documentation for the helper for specific information. DLL locations You can install .DLL files used only by a single application in the same directory as the application. DLLs that will be used by a number of applications should be installed in a location accessible to all of those applications. A common convention for locating such community DLLs is to place them either in the Windows or the Windows\System directory. A better way is to create a dedicated directory for the common .DLL files, similar to the way the Borland Database Engine is installed. Deployingapplications12-3, Deployingdatabaseapplications

Deploying database applications

Applications that access databases involve special installation considerations beyond copying the application’s executable file onto the host computer. Database access is most often handled by a separate database engine, the files of which cannot be linked into the application’s executable file. The data files, when not created beforehand, must be made available to the application. Multi-tier database applications require even more specialized handling on installation, because the files that make up the application are typically located on multiple computers. Two types of installation aspects for database access are: • Providing the database engine • Multi-tiered Distributed Application Services (MIDAS) For information on deploying CORBA applications, “Deploying CORBA applications” on page 27-16.

Providing the database engine

Database access for an application is provided by various database engines. An application can use the Borland Database Engine or a third-party database engine. SQL Links is provided (Client/Server edition and above, only) to enable native access to SQL database systems. The following sections describe installation of the database access elements of an application: • Borland Database Engine • Third-party database engines • SQL Links Borland Database Engine For standard Delphi data components to have database access, the Borland Database Engine (BDE) must be present and accessible. See DEPLOY.TXT for specific rights and limitations on redistributing the BDE. Borland recommends use of InstallShield Express (or other certified installation program) for installing the BDE. InstallShield Express will create the necessary registry entries and define any aliases the application may require. Using a certified installation program to deploy the BDE files and subsets is important because: • Improper installation of the BDE or BDE subsets can cause other applications using the BDE to fail. Such applications include not only Borland products, but many third-party programs that use the BDE. • Under Windows 95 and Windows NT, BDE configuration information is stored in the Windows registry instead of .INI files, as was the case under 16-bit Windows. Making the correct entries and deletions for install and uninstall is a complex task. It is possible to install only as much of the BDE as an application actually needs. For instance, if an application only uses Paradox tables, it is only necessary to install that portion of the BDE required to access Paradox tables. This reduces the disk space needed for an application. Certified installation programs, like InstallShield Express, 12-4Developer’ sGuide, Deployingdatabaseapplicationsare capable of performing partial BDE installations. Care must be taken to leave BDE system files that are not used by the deployed application, but that are needed by other programs. Third-party database engines You can use third-party database engines to provide database access for Delphi applications. Consult the documentation or vendor for the database engine regarding redistribution rights, installation, and configuration. SQL Links SQL Links provides the drivers that connect an application (through the Borland Database Engine) with the client software for an SQL database. See DEPLOY.TXT for specific rights and limitations on redistributing SQL Links. As is the case with the Borland Database Engine (BDE), SQL Links must be deployed using InstallShield Express (or other certified installation program). Note SQL Links only connects the BDE to the client software, not to the SQL database itself. It is still necessary to install the client software for the SQL database system used. See the documentation for the SQL database system or consult the vendor that supplies it for more information on installing and configuring client software. Table 12.2 shows the names of the driver and configuration files SQL Links uses to connect to the different SQL database systems. These files come with SQL Links and are redistributable in accordance with the Delphi license agreement. Table 12.2 SQL Database Client Software Files Vendor Redistributable files Oracle 7 SQLORA32.DLL and SQL_ORA.CNF Oracle8 SQLORA8.DLL and SQL_ORA8.CNF Sybase Db-Lib SQLSYB32.DLL and SQL_SYB.CNF Sybase Ct-Lib SQLSSC32.DLL and SQL_SSC.CNF Microsoft SQL Server SQLMSS32.DLL and SQL_MSS.CNF Informix 7 SQLINF32.DLL and SQL_INF.CNF Informix 9 SQLINF9.DLL and SQL_INF9.CNF DB/2 SQLDB232.DLL and SQL_DB2.CNF InterBase SQLINT32.DLL and SQL_INT.CNF Install SQL Links using InstallShield Express or other certified installation program. For specific information concerning the installation and configuration of SQL Links, see the help file SQLLNK32.HLP, by default installed into the main BDE directory.

Multi-tiered Distributed Application Services (MIDAS)

Multi-tiered Distributed Application Services (MIDAS) consists of the Business Object Broker, OLEnterprise, the Remote DataBroker, and the ConstraintBrokerDeployingapplications12-5, DeployingdatabaseapplicationsManager (SQL Explorer). MIDAS provides multi-tier database capability to Delphi applications. Handle the installation of the executable and related files for a multi-tier application the same as for general applications. Some of the files that comprise MIDAS may need to be installed on the client computer and others on the server computer. For general application installation information, see See “Deploying general applications” on page 12-1. See the text file LICENSE.TXT on the MIDAS CD and the Delphi file DEPLOY.TXT for specific information regarding licensing and redistribution rights for MIDAS. For the Remote DataBroker and ConstraintBroker portions of MIDAS, the file DBCLIENT.DLL must be installed onto the client computer and registered with Windows. On the server computer, the files DBCLIENT.DLL and STDVCL40.DLL must be installed and registered for the Remote DataBroker and DBEXPLOR.EXE for the ConstraintBroker. Installation programs such as InstallShield Express automate the process of registering these DLLs. To manually register the DLLs, use the TRegSvr demo application or the Microsoft utility REGSERV32.EXE (not included with all Windows versions). The MIDAS deployment CD provides install programs for the client and server portions of OLEnterprise and the Business ObjectBroker. Use only the Setup Launcher on the MIDAS CD to install OLEnterprise. Below is a list of the minimum required files to be installed onto the server machine. UNINSTALL.EXE OBJFACT.ICO W32PTHD.DLL NBASE.IDL LICENSE.TXT ODEBKN40.DLL RPMARN40.DLL OBJX.EXE README.TXT ODECTN40.DLL RPMAWN40.DLL OLECFG.EXE OLENTER.HLP RPMEGN40.DLL RPMCBN40.DLL OLEWAN40.CAB OLENTER.CNT ODEDIN40.DLL RPMCPN40.DLL OLENTEXP.EXE FILELIST.TXT ODEEGN40.DLL BROKER.EXE OLENTEXP.HLP SETLOG.TXT ODELTN40.DLL RPMFEN40.DLL OLENTEXP.CNT SETLOG.EXE LIBAVEMI.DLL RPMUTN40.DLL BRKCP.EXE OBJPING.EXE OLEAAN40.DLL RPMFE.CAT BROKER.ICO OBJFACT.EXE OLERAN40.DLL EXPERR.CAT Below is a list of the required files to be installed onto the client machine. NBASE.IDL ODEN40.DLL RPMFEN40.DLL OLENTEXP.EXE ODECTN40.DLL RPMARN40.DLL RPMUTN40.DLL SETLOG.EXE ODEDIN40.DLL RPMAWN40.DLL OLERAN40.DLL OLECFG.EXE ODEEGN40.DLL RPMCBN40.DLL OLEAAN40.DLL W32PTHD.DLL ODELTN40.DLL RPMCPN40.DLL OLEWAN40.CAB ODEMSG.DLL RPMEGN40.DLL OBJX.EXE 12-6Developer’ sGuide, DeployingWebapplications

Deploying Web applications

Some Delphi applications are designed to be run over the World Wide Web, such as those in the form of Server-side Extension (ISAPI) DLLs, CGI applications, and ActiveForms. The steps for installing Web applications are the same as those for general applications, except the application’s files are deployed on the Web server. For information on installing general applications, see See “Deploying general applications” on page 12-1. Here are some special considerations for deploying Web applications: • For database applications, the Borland Database Engine (or alternate database engine) is installed along with the application files on the Web server. • Security for the directories must not be so high that access to application files, the BDE, or database files is not possible. • The directory containing an application must have read and execute attributes. • The application should not use hard-coded paths for accessing database or other files. • The location of an ActiveX control is indicated by the CODEBASE parameter of the HTML tag.

Programming for varying host environments

Due to the nature of the Windows environment, there are a number of factors that vary with user preference or configuration. The following factors can affect an application deployed to another computer: • Screen resolutions and color depths • Fonts • Windows versions • Helper applications • DLL locations

Screen resolutions and color depths

The size of the Windows desktop and number of available colors on a computer is configurable and dependent on the hardware installed. These attributes are also likely to differ on the deployment computer compared to those on the development computer. An application’s appearance (window, object, and font sizes) on computers configured for different screen resolutions can be handled in various ways: • Design the application for the lowest resolution users will have (typically, 640x480). Take no special actions to dynamically resize objects to make them proportional to the host computer’s screen display. Visually, objects will appear smaller the higher the resolution is set. Deployingapplications12-7, Programmingforvaryinghostenvironments• Design using any screen resolution on the development computer and, at runtime, dynamically resize all forms and objects proportional to the difference between the screen resolutions for the development and deployment computers (a screen resolution difference ratio). • Design using any screen resolution on the development computer and, at runtime, dynamically resize only the application’s forms. Depending on the location of visual controls on the forms, this may require the forms be scrollable for the user to be able to access all controls on the forms. Considerations when not dynamically resizing If the forms and visual controls that make up an application are not dynamically resized at runtime, design the application’s elements for the lowest resolution. Otherwise, the forms of an application run on a computer configured for a lower screen resolution than the development computer may overlap the boundaries of the screen. For example, if the development computer is set up for a screen resolution of 1024x768 and a form is designed with a width of 700 pixels, not all of that form will be visible within the Windows desktop on a computer configured for a 640x480 screen resolution. Considerations when dynamically resizing forms and controls If the forms and visual controls for an application are dynamically resized, accommodate all aspects of the resizing process to ensure optimal appearance of the application under all possible screen resolutions. Here are some factors to consider when dynamically resizing the visual elements of an application: • The resizing of forms and visual controls is done at a ratio calculated by comparing the screen resolution of the development computer to that of the computer onto which the application installed. Use a constant to represent one dimension of the screen resolution on the development computer: either the height or the width, in pixels. Retrieve the same dimension for the user’s computer at runtime using the TScreen.Height or the TScreen.Width property. Divide the value for the development computer by the value for the user’s computer to derive the difference ratio between the two computers’ screen resolutions. • Resize the visual elements of the application (forms and controls) by reducing or increasing the size of the elements and their positions on forms. This resizing is proportional to the difference between the screen resolutions on the development and user computers. Resize and reposition visual controls on forms automatically by setting the TCustomForm.Scaled property to True and calling the TCustomForm.ScaleBy method. The ScaleBy method does not change the form’s height and width, though. Do this manually by multiplying the current values for the Height and Width properties by the screen resolution difference ratio. • The controls on a form can be resized manually, instead of automatically with the TCustomForm.ScaleBy method, by referencing each visual control in a loop and setting its dimensions and position. The Height and Width property values for visual controls are multiplied by the screen resolution difference ratio. Reposition visual controls proportional to screen resolution differences by multiplying the Top and Left property values by the same ratio. 12-8Developer’ sGuide, Programmingforvaryinghostenvironments• If an application is designed on a computer configured for a higher screen resolution than that on the user’s computer, font sizes will be reduced in the process of proportionally resizing visual controls. If the size of the font at design time is too small, the font as resized at runtime may be unreadable. For example, the default font size for a form is 8. If the development computer has a screen resolution of 1024x768 and the user’s computer 640x480, visual control dimensions will be reduced by a factor of 0.625 (640 / 1024 = 0.625). The original font size of 8 is reduced to 5 (8 * 0.625 = 5). Text in the application appears jagged and unreadable as Windows displays it in the reduced font size. • Some visual controls, such as TLabel and TEdit, dynamically resize when the size of the font for the control changes. This can affect deployed applications when forms and controls are dynamically resized. The resizing of the control due to font size changes are in addition to size changes due to proportional resizing for screen resolutions. This effect is offset by setting the AutoSize property of these controls to False. • Avoid making use of explicit pixel coordinates, such as when drawing directly to a canvas. Instead, modify the coordinates by a ratio proportionate to the screen resolution difference ratio between the development and user computers. For example, if the application draws a rectangle to a canvas ten pixels high by twenty wide, instead multiply the ten and twenty by the screen resolution difference ratio. This ensures that the rectangle visually appears the same size under different screen resolutions. Accommodating varying color depths To account for all deployment computers not being configured with the same color availability, the safest way is to use graphics with the least possible number of colors. This is especially true for control glyphs, which should typically use 16-color graphics. For displaying pictures, either provide multiple copies of the images in different resolutions and color depths or explain in the application the minimum resolution and color requirements for the application.

Fonts

Windows comes with a standard set of TrueType and raster fonts. When designing an application to be deployed on other computers, realize that not all computers will have fonts outside the standard Windows set. Text components used in the application should all use fonts that are likely to be available on all deployment computers. When use of a nonstandard font is absolutely necessary in an application, you need to distribute that font with the application. Either the installation program or the application itself must install the font on the deployment computer. Distribution of third-party fonts may be subject to limitations imposed by the font creator. Windows has a safety measure to account for attempts to use a font that does not exist on the computer. It substitutes another, existing font that it considers the closest match. While this may circumvent errors concerning missing fonts, the end resultDeployingapplications12-9, Softwarelicenserequirementsmay be a degradation of the visual appearance of the application. It is better to prepare for this eventuality at design time. To make a nonstandard font available to an application, use the Windows API functions AddFontResource and DeleteFontResource. Deploy the .FOT file for the nonstandard font with the application.

Windows versions

When using Windows API functions or accessing areas of the Windows operating system from an application, there is the possibility that the function, operation, or area may not be available on computers with different versions of Windows. For example, Services are only pertinent to the Windows NT operating system. If an application is to act as a Service or interact with one, this would fail if the application is installed under Windows 95. To account for this possibility, you have a few options: • Specify in the application’s system requirements the versions of Windows on which the application can run. It is the user’s responsibility to install and use the application only under compatible Windows versions. • Check the version of Windows as the application is installed. If an incompatible version of Windows is present, either halt the installation process or at least warn the installer of the problem. • Check the Windows version at runtime, just prior to executing an operation not applicable to all versions. If an incompatible version of Windows is present, abort the process and alert the user. Alternately, provide different code to run dependent on different versions of Windows. Some operations are performed differently in Windows 95 than in Windows NT. Use the Windows API function GetVersionEx to determine the Windows version.

Software license requirements

The distribution of some files associated with Delphi applications is subject to limitations or cannot be redistributed at all. The following documents describe the legal stipulations regarding the distribution of these files where limitations exist: • DEPLOY.TXT DEPLOY.TXT covers the some of the legal aspects of distributing of various components and utilities, and other product areas that can be part of or associated with your application. DEPLOY.TXT is a text file installed in the main directory. The topics covered include, but are not limited to • .EXE, .DLL, and .BPL files • Components and design time packages • Borland Database Engine (BDE) 12-10Developer’ sGuide, Softwarelicenserequirements• ActiveX controls • Sample Images • Multi-tiered Distributed Application Services (MIDAS) • SQL Links • README.TXT README.TXT contains last minute information about Delphi, possibly including information that could affect the redistribution rights for components, or utilities, or other product areas. README.TXT is a Windows help file installed into the main Delphi directory. • No-nonsense license agreement The Delphi no-nonsense license agreement, a printed document, covers other legal rights and obligations concerning Delphi. • Third-party product documentation Redistribution rights for third-party components, utilities, helper applications, database engines, and other products are governed by the vendor supplying the product. Consult the documentation for the product or the vendor for information regarding the redistribution of the product with Delphi applications prior to distribution.

DEPLOY.TXT

DEPLOY.TXT covers the some of the legal aspects of distributing of various components and utilities, and other product areas that can be part of or associated with a Delphi application. DEPLOY.TXT is a text file installed in the main Delphi directory. The topics covered include, but are not limited to • .EXE, .DLL, and .BPL files • Components and design time packages • Borland Database Engine (BDE) • ActiveX controls • Sample Images • Multi-tiered Distributed Application Services (MIDAS) • SQL Links

README.TXT

README.TXT contains last minute information about Delphi, possibly including information that could affect the redistribution rights for components, or utilities, or other product areas. README.TXT is a Windows help file installed into the main Delphi directory. Deployingapplications12-11, Softwarelicenserequirements

No-nonsense license agreement

The Delphi no-nonsense license agreement, a printed document, covers other legal rights and obligations concerning Delphi.

Third-party product documentation

Redistribution rights for third-party components, utilities, helper applications, database engines, and other products are governed by the vendor supplying the product. Consult the documentation for the product or the vendor for information regarding the redistribution of the product with Delphi applications prior to distribution. 12-12Developer’ sGuide,

Part II

Part IIDeveloping database applications The chapters in “Developing Database Applications” present concepts and skills necessary for creating Delphi database applications. Note You need the Professional, Client/Server, or Enterprise edition of Delphi to develop database applications. To implement more advanced Client/Server databases, you need the Delphi features available in the Client/Server and Enterprise editions. Developingdatabaseapplications,

Chapter

Chapter 13Designing database applications Database applications allow users to interact with information that is stored in databases. Databases provide structure for the information, and allow it to be shared among different applications. Delphi provides support for relational database applications. Relational databases organize information into tables, which contain rows (records) and columns (fields). These tables can be manipulated by simple operations known as the relational calculus. When designing a database application, you must understand how the data is structured. Based on that structure, you can then design a user interface to display data to the user and allow the user to enter new information or modify existing data. This chapter introduces some common considerations for designing a database application and the decisions involved in designing a user interface.

Using databases

The components on the Data Access page of the Component palette allow your application to read from and write to databases. These components use the Borland Database Engine (BDE) to access database information which they make available to the data-aware controls in your user interface. Depending on your version of Delphi, the BDE includes drivers for different types of databases. While all these types of databases contain tables which store information, different types support additional features such as • Database security • Transactions • Data dictionary • Referential integrity, stored procedures, and triggersDesigningdatabaseapplications13-1, Usingdatabases

Types of databases

You can connect to different types of databases, depending on what drivers you have installed with the BDE. All versions of Delphi include drivers for local databases. In addition, if you have the Client/Server or Enterprise version, you can use the drivers installed with SQL Links to communicate with remote database servers. Choosing what type of database to use depends on several factors. Your data may already be stored in an existing database. If you are creating the tables of information your application uses, you may want to consider the following questions. • How much data will the tables hold? • How many users will be sharing these tables? • What type of performance (speed) do you require from the database? Local databases Local databases reside on your local drive or on a local area network. They have proprietary APIs for accessing the data. Often, they are dedicated to a single system. When they are shared by several users, they use file-based locking mechanisms. Because of this, they are sometimes called file-based databases. Local databases can be faster than remote database servers because they often reside on the same system as the database application. Because they are file-based, local databases are more limited than remote database servers in the amount of data they can store. Therefore, in deciding whether to use a local database, you must consider how much data the tables are expected to hold. Applications that use local databases are called single-tiered applications because the application and the database share a single file system. Examples of local databases include Paradox, dBASE, FoxPro, and Access. Remote database servers Remote database servers usually reside on a remote machine. They use Structured Query Language (SQL) to enable clients to access the data. Because of this, they are sometimes called SQL servers. (Another name is Remote Database Management system, or RDBMS.) In addition to the common commands that make up SQL, most remote database servers support a unique “dialect” of SQL. Remote database servers are designed for access by several users at the same time. Instead of a file-based locking system such as those employed by local databases, they provide more sophisticated multi-user support, based on transactions. Remote database servers hold more data than local databases. Sometimes, the data from a remote database server does not even reside on a single machine, but is distributed over several servers. Applications that use remote database servers are called two-tiered applications or multi-tiered applications because the application and the database operate on independent systems (or tiers). 13-2Developer’ sGuide, UsingdatabasesExamples of SQL servers include Interbase, Oracle, Sybase, Informix, Microsoft SQL server, and DB2. Note You must have the Client/Server or Enterprise version to write applications that use remote database servers.

Database security

Databases often contain sensitive information. Different databases provide security schemes for protecting that information. Some databases, such as Paradox and dBASE, only provide security at the table or field level. When users try to access protected tables, they are required to provide a password. Once users have been authenticated, they can see only those fields (columns) for which they have permission. Most SQL servers require a password and user name to use the database server at all. Once the user has logged in to the database, that username and password determine which tables can be used. For information on providing passwords to SQL servers, see “Controlling server login” on page 17-6. When designing database applications, you must consider what type of authentication is required by your database server. If you do not want your users to have to provide a password, you must either use a database that does not require one or you must provide the password and username to the server programmatically. When providing the password programmatically, care must be taken that security can’t be breached by reading the password from the application. If you are requiring your user to supply a password, you must consider when the password is required. If you are using a local database but intend to scale up to a larger SQL server later, you may want to prompt for the password before you access the table, even though it is not required until then. If your application requires multiple passwords because you must log in to several protected systems or databases, you can have your users provide a single master password which is used to access a table of passwords required by the protected systems. The application then supplies passwords programmatically, without requiring the user to provide multiple passwords. In multi-tiered applications, you may want to use a different security model altogether. You can use CORBA or MTS to control access to middle tiers, and let the middle tiers handle all details of logging into database servers.

Transactions

A transaction is a group of actions that must all be carried out successfully on one or more tables in a database before they are committed (made permanent). If any of the actions in the group fails, then all actions are rolled back (undone). Transactions protect against hardware failures that occur in the middle of a database command or set of commands. They also form the basis of multi-user concurrency control on SQL servers. When each user interacts with the database only throughDesigningdatabaseapplications13-3, Usingdatabasestransactions, one user’s commands can’t disrupt the unity of another user’s transaction. Instead, the SQL server schedules incoming transactions, which either succeed as a whole or fail as a whole. Although transaction support is not part of most local databases, the BDE drivers provide limited transaction support for some of these databases. For SQL servers and ODBC-compliant databases, the database transaction support is provided by the database itself. In multi-tiered applications, you can create transactions that include actions other than database operations or that span multiple databases. For details on using transactions in BDE-based database applications, see “Using transactions” on page 14-4. For details on using transactions in multi-tiered applications, see “Managing transactions in multi-tiered applications” on page 15-35.

The Data Dictionary

No matter what type of database you use, your application has access to the Data Dictionary. The Data Dictionary provides a customizable storage area, independent of your applications, where you can create extended field attribute sets that describe the content and appearance of data. For example, if you frequently develop financial applications, you may create a number of specialized field attribute sets describing different display formats for currency. When you create datasets for your application at design time, rather than using the Object Inspector to set the currency fields in each dataset by hand, you can associate those fields with an extended field attribute set in the data dictionary. Using the data dictionary ensures a consistent data appearance within and across the applications you create. In a Client/Server environment, the Data Dictionary can reside on a remote server for additional sharing of information. To learn how to create extended field attribute sets from the Fields editor at design time, and how to associate them with fields throughout the datasets in your application, see “Creating attribute sets for field components” on page 19-14. To learn more about creating a data dictionary and extended field attributes with the SQL and Database Explorers, see their respective online help files. A programming interface to the Data Dictionary is available in the drintf unit (located in the lib directory). This interface supplies the following methods: Table 13.1 Data Dictionary interface Routine Use DictionaryActive Indicates if the data dictionary is active. DictionaryDeactivate Deactivates the data dictionary. IsNullID Indicates whether a given ID is a null ID. FindDatabaseID Returns the ID for a database given its alias. FindTableID Returns the ID for a table in a specified database. FindFieldID Returns the ID for a field in a specified table. FindAttrID Returns the ID for a named attribute set. 13-4Developer’ sGuide, DatabasearchitectureTable 13.1 Data Dictionary interface (continued) Routine Use GetAttrName Returns the name an attribute set given its ID. GetAttrNames Executes a callback for each attribute set in the dictionary. GetAttrID Returns the ID of the attribute set for a specified field. NewAttr Creates a new attribute set from a field component. UpdateAttr Updates an attribute set to match the properties of a field. CreateField Creates a field component based on stored attributes. UpdateField Changes the properties of a field to match a specified attribute set. AssociateAttr Associates an attribute set with a given field ID. UnassociateAttr Removes an attribute set association for a field ID. GetControlClass Returns the control class for a specified attribute ID. QualifyTableName Returns a fully qualified table name (qualified by user name). QualifyTableNameByName Returns a fully qualified table name (qualified by user name). HasConstraints Indicates whether the dataset has constraints in the dictionary. UpdateConstraints Updates the imported constraints of a dataset. UpdateDataset Updates a dataset to the current settings and constraints in the dictionary.

Referential integrity, stored procedures, and triggers

All relational databases have certain features in common that allow applications to store and manipulate data. In addition, databases often provide other, database- specific, features that can prove useful for ensuring consistent relationships between the tables in a database. These include • Referential integrity. Referential integrity provides a mechanism to prevent master/detail relationships between tables from being broken. When the user attempts to delete a field in a master table which would result in orphaned detail records, referential integrity rules prevent the deletion or automatically delete the orphaned detail records. • Stored procedures. Stored procedures are sets of SQL statements that are named and stored on an SQL server. Stored procedures usually perform common database-related tasks on the server, and return sets of records (datasets). • Triggers. Triggers are sets of SQL statements that are automatically executed in response to a particular command.

Database architecture

Database applications are built from user interface elements, components that manage the database or databases, and components that represent the data contained by the tables in those databases (datasets). How you organize these pieces is the architecture of your database application. Designingdatabaseapplications13-5, DatabasearchitectureBy isolating database access components in data modules, you can develop forms in your database applications that provide a consistent user interface. By storing links to well-designed forms and data modules in the Object Repository, you and other developers can build on existing foundations rather than starting over from scratch for each new project. Sharing forms and modules also makes it possible for you to develop corporate standards for database access and application interfaces. Many aspects of the architecture of your database application depend on the type of database you are using, the number of users who will be sharing the database information, and the type of information you are working with. See “Types of databases” on page 13-2 for more information about different types of databases. When writing applications that use information that is not shared among several users, you may want to use a local database in a single-tiered application. This approach can have the advantage of speed (because data is stored locally), and does not require the purchase of a separate database server and expensive site licences. However, it is limited in how much data the tables can hold and the number of users your application can support. Writing a two-tiered application provides more multi-user support and lets you use large remote databases that can store far more information. Note Support for two-tiered applications requires SQL links, which is only available in the Client/Server and Enterprise versions. When the database information includes complicated relationships between several tables, or when the number of clients grows, you may want to use a multi-tiered application. Multi-tiered applications include middle tiers that centralize the logic which governs your database interactions so that there is centralized control over data relationships. This allows different client applications to use the same data while ensuring that the data logic is consistent. They also allow for smaller client applications because much of the processing is off-loaded onto middle tiers. These smaller client applications are easier to install, configure, and maintain because they do not include the database connectivity software. Multi-tiered applications can also improve performance by spreading the data-processing tasks over several systems. Note Support for multi-tiered applications is only available in the Client/Server and Enterprise versions.

Planning for scalability

The development process can get more involved and expensive as the number of tiers increases. Because of this, you may wish to start developing your application as a single-tiered application. As the amount of data, the number of users, and the number of different applications accessing the data grows, you may later need to scale up to a multi-tiered architecture. By planning for scalability, you can protect your development investment when writing a single- or two-tiered application so that the code can be reused as your application grows. The VCL data-aware components make it easy to write scalable applications by abstracting the behavior of the database and the data stored by the database. 13-6Developer’ sGuide, DatabasearchitectureWhether you are writing a single-tiered, two-tiered, or multi-tiered application, you can isolate your user interface from the data access layer as illustrated inFigure 13.1. Figure 13.1 User-interface to dataset connections in all database applications user data source datasetinterface databasecomponent elements Form Data Module Client Application A form represents the user interface, and contains data controls and other user interface elements. The data controls in the user interface connect to datasets which represent information from the tables in the database. A data source links the data controls to these datasets. By isolating the data source and datasets in a data module, the form can remain unchanged as you scale your application up. Only the datasets must change. Note Some user interface elements require special attention when planning for scalability. For example, different databases enforce security in different ways. See “Database security” on page 13-3 for more information on handling user authentication in a uniform manner as you change databases. When writing BDE-based applications, it is easy to scale from one-tiered to two- tiered. Only a few properties on the dataset must change to direct the dataset to connect to an SQL server rather than a local database. A flat-file database application is easily scaled to the client in a multi-tiered application because both architectures use the same client dataset component. In fact, you can write an application that acts as both a flat-file application and a multi-tiered client (see “Using the briefcase model” on page 14-14). If you plan to scale your application up to a three-tiered architecture eventually, you can write your one- or two-tiered application with that goal in mind. In addition to isolating the user interface, isolate all logic that will eventually reside on the middle tier so that it is easy to replace at a later time. You can even connect your user interface elements to client datasets (used in multi-tiered applications), and connect them to local versions of the BDE-enabled datasets in a separate data module that will eventually move to the middle tier. If you do not want to introduce this artifice of an extra dataset layer in your one- and two-tiered applications, it is still easy to scale up to a three-tiered application at a later date. See “Scaling up to a three-tiered application” on page 14-15 for more information. Designingdatabaseapplications13-7, Databasearchitecture

Single-tiered database applications

In single-tiered database applications, the application and the database share a single file system. They use local databases or files that store database information in a flat- file format. A single application comprises the user interface and incorporates the data access mechanism (either the BDE or a system for loading and saving flat-file database information). The type of dataset component used to represent database tables depends on whether the data is stored in a local database (such as Paradox, dBASE, Access, or FoxPro) or in a flat file. Figure 13.2 illustrates these two possibilities: Figure 13.2 Single-tiered database application architectures user BDE-enabled Borland interface data source dataset Database local elements component Engine database Form Data Module user Client interface data source dataset flat-file elements data Form Data Module For more information on building single-tiered database applications, see Chapter 14, “Building one- and two-tiered applications.”

Two-tiered database applications

In two-tiered database applications, a client application provides a user interface to data, and interacts directly with a remote database server through the BDE. Figure 13.3 illustrates this relationship. 13-8Developer’ sGuide, DatabasearchitectureFigure 13.3 Two-tiered database application architecture user BDE-enabled Borland interface data source dataset Database remote elements component Engine database Form Data Module Client Application In this model, all applications are database clients. A client requests information from and sends information to a database server. A server can process requests from many clients simultaneously, coordinating access to and updating of data. For more information on building two-tiered database applications, see “BDE-based applications” on page 14-1.

Multi-tiered database applications

In multi-tiered database applications, an application is partitioned into pieces that reside on different machines. A client application provides a user interface to data. It passes all data requests and updates through an application server (also called a “remote data broker”). The application server, in turn, communicates directly with a remote database server or some other custom dataset. Usually, in this model, the client application, the application server, and the remote database server are on separate machines. Figure 13.4 illustrates these relationships for multi-tiered application with and without the BDE. Designingdatabaseapplications13-9, DatabasearchitectureFigure 13.4 Multi-tiered database architectures connection component provider user Borland interface data source Database remote elements Engine databaseClient dataset BDE-enabled dataset component Form Data Module Remote data module Client Application Application Server connection component provider user interface data source elements Client dataset custom dataset Form Data Module Remote data module Client Application Application Server You can use Delphi to create both client applications and application servers. The client application uses standard data-aware controls connected through a data source to one or more client dataset components in order to display data for viewing and editing. Each client dataset communicates with an application server through an IProvider interface that is part of the application server’s remote data module. The client application can use a variety of protocols (TCP/IP, DCOM, MTS, or CORBA) to establish this communication. The protocol depends on the type of connection component used in the client application and the type of remote data module used in the server application. The application server creates the IProvider interfaces in one of two ways. If the application server includes any provider objects, then these objects are used to create the IProvider interface. This is the method illustrated in the previous figure. If the application server does not include any provider components, it creates the IProvider interfaces directly from BDE-enabled datasets. Using a provider component gives an application more control over the interface. In either case, all data is passed between the client application and the application server through the interface. The interface receives data from and sends updates to conventional BDE-enabled datasets, and these components communicate with a database server through the BDE. Usually, several client applications communicate with a single application server in the multi-tiered model. The application server provides a gateway to your databases for all your client applications, and it lets you provide enterprise-wide database tasks in a central location, accessible to all your clients. For more information about 13-10Developer’ sGuide, Designingtheuserinterfacecreating and using a multi-tiered database application, see Chapter 15, “Creating multi-tiered applications.”

Designing the user interface

The Data Controls page of the Component palette provides a set of data-aware controls that represent data from fields in a database record, and can permit users to edit that data and post changes back to the database. Using data-aware controls, you can build your database application’s user interface (UI) so that information is visible and accessible to users. For more information on data-aware controls see Chapter 25, “Using data controls.” Data-aware controls get data from and send data to a data source component, TDataSource. A data source component acts as a conduit between the user interface and a dataset component which represents a set of information from the tables in a database. Several data-aware controls on a form can share a single data source, in which case the display in each control is synchronized so that as the user scrolls through records, the corresponding value in the fields for the current record is displayed in each control. An application’s data source components usually reside in a data module, separate from the data-aware controls on forms. The data-aware controls you add to your user interface depend on what type of data you are displaying (plain text, formatted text, graphics, multimedia elements, and so on). In addition, your choice of controls is determined by how you want to organize the information and how (or if) you want to let users navigate through the records of datasets and add or edit data. The following sections introduce the components you can use for various types of user interface.

Displaying a single record

In many applications, you may only want to provide information about a single record of data at a time. For example, an order-entry application may display the information about a single order without indicating what other orders are currently logged. This information probably comes from a single record in an orders dataset. Applications that display a single record are usually easy to read and understand, because all database information is about the same thing (in the previous case, the same order). The data-aware controls in these user interfaces represent a single field from a database record. The Data Controls page of the Component palette provides a wide selection of controls to represent different kinds of fields. For more information about specific data-aware controls, see “Controls that represent a single field” on page 25-8. Designingdatabaseapplications13-11, Designingtheuserinterface

Displaying multiple records

Sometimes you want to display many records in the same form. For example, an invoicing application might show all the orders made by a single customer on the same form. To display multiple records, use a grid control. Grid controls provide a multi-field, multi-record view of data that can make your application’s user interface more compelling and effective. They are discussed in “Viewing and editing data with TDBGrid” on page 25-16 and “Creating a grid that contains other data-aware controls” on page 25-27. You may want to design a user interface that displays both fields from a single record and grids that represent multiple records. There are two models that combine these two approaches: • Master-detail forms: You can represent information from both a master table and a detail table by including both controls that display a single field and grid controls. For example, you could display information about a single customer with a detail grid that displays the orders for that customer. For information about linking the underlying tables in a master-detail form, see “Creating master/detail forms” on page 20-24. • Drill-down forms: In a form that displays multiple records, you can include single field controls that display detailed information from the current record only. This approach is particularly useful when the records include long memos or graphic information. As the user scrolls through the records of the grid, the memo or graphic updates to represent the value of the current record. Setting this up is very easy. The synchronization between the two displays is automatic if the grid and the memo or image control share a common data source. Note It is generally not a good idea to combine these two approaches on a single form. While the result can sometimes be effective, it is usually confusing for users to understand the data relationships.

Analyzing data

Some database applications do not present database information directly to the user. Instead, they analyze and summarize information from databases so that users can draw conclusions from the data. The TDBChart component on the Data Controls page of the Component palette lets you present database information in a graphical format that enables users to quickly grasp the import of database information. In addition, the Client/Server version includes a Decision Cube page on the Component palette. It contains six components that let you perform data analysis and cross-tabulations on data when building decision support applications. For more information about using the Decision Cube components, see Chapter 26, “Using decision support components.” 13-12Developer’ sGuide, DesigningtheuserinterfaceIf you want to build your own components that display data summaries based on various grouping criteria, you can use maintained aggregates with a client dataset. Maintained aggregates and client datasets are only available in the Client/Server version. For more information about using maintained aggregates, see “Using maintained aggregates” on page 23-9.

Selecting what data to show

Often, the data you want to surface in your database application does not correspond exactly to the data in a single database table. You may want to use only a subset of the fields or a subset of the records in a table. You may want to combine the information from more than one table into a single joined view. The data available to your database application is controlled by your choice of dataset component. Datasets abstract the properties and methods of a database table, so that you do not need to make major alterations depending on whether the data is stored in a database table or derived from one or more tables in the database. For more information on the common properties and methods of datasets, see Chapter 18, “Understanding datasets.” Your application can contain more than one dataset. Each dataset represents a logical table. By using datasets, your application logic is buffered from restructuring of the physical tables in your databases. You might need to alter the type of dataset component, or the way it specifies the data it contains, but the rest of your user interface can continue to work without alteration. You can use any of the following types of dataset: • Table components: Tables correspond directly to the underlying tables in the database. You can adjust which fields appear (including adding lookup fields and calculated fields) by using persistent field components. You can limit the records that appear using ranges or filters. Tables are described in more detail in Chapter 20, “Working with tables.” Persistent fields are described in “Persistent field components” on page 19-4. Ranges and filters are described in “Working with a subset of data” on page 20-11. • Query components: Queries provide the most general mechanism for specifying what appears in a dataset. You can combine the data from multiple tables using joins, and limit the fields and records that appear based on any criteria you can express in SQL. For more information on queries, see Chapter 21, “Working with queries.” • Stored procedures: Stored procedures are sets of SQL statements that are named and stored on an SQL server. If your database server defines a remote procedure that returns the dataset you want, you can use a stored procedure component. For more information on stored procedures, see Chapter 22, “Working with stored procedures.” • Client datasets: Client datasets cache the records of the logical dataset in memory. Because of that, they can only hold a limited number of records. You can build smaller applications using client datasets because they do not require the Borland Database Engine, only DBClient.DLL. Client datasets are populated with data inDesigningdatabaseapplications13-13, Designingtheuserinterfaceone of two ways: from an application server or from flat-file data stored on disk. When using a client dataset to represent flat-file data, you must create the underlying table programmatically. For more information about client datasets, see Chapter 23, “Creating and using a client dataset.” • Nested datasets: Nested datasets represent the records in an Oracle8 nested detail set. Delphi does not let you create Oracle8 tables with nested dataset fields, but you can edit and display data from existing dataset fields using nested datasets. The nested dataset gets its data from a dataset field component in a dataset which contains Oracle8 data. See “Working with nested tables” on page 20-26 and “Working with dataset fields” on page 19-26 for more information on using nested datasets to represent dataset fields. • Custom datasets: You can create your own custom descendants of TDataSet to represent a body of data that you create or access in code you write. Writing custom datasets allows you the flexibility of managing the data using any method you choose, while still letting you use the VCL data controls to build your user interface. For more information about creating custom components, see Chapter 30, “Overview of component creation.”

Writing reports

If you want to let your users print database information from the datasets in your application, you can use the report components on the QReport page of the Component palette. Using these components you can visually build banded reports to present and summarize the information in your database tables. You can add summaries to group headers or footers to analyze the data based on grouping criteria. Start a report for your application by selecting the QuickReport icon from the New Items dialog. Select File|New from the main menu, and go to the page labelled Business. Double-click the QuickReport Wizard icon to launch the wizard. 13-14Developer’ sGuide,

Chapter

Chapter 14Building one- and two-tiered applications One- and two-tiered applications include the logic that manipulates database information in the same application that implements the user interface. Because the data manipulation logic is not isolated in a separate tier, these types of applications are most appropriate when there are no other applications sharing the same database information. Even when other applications share the database information, these types of applications are appropriate if the database is very simple, and there are no data semantics that must duplicated by all applications that use the data. You may want to start by writing a one- or two-tiered application, even when you intend to eventually scale up to a multi-tiered model as your needs increase. This approach lets you avoid having to develop data manipulation logic up front so that the application server can be available while you are writing the user interface. It also allows you to develop a simpler, cheaper prototype before investing in a large, multi- system development project. If you intend to eventually scale up to a multi-tiered application, you can isolate the data manipulation logic so that it is easy to move it to a middle tier at a later date. Delphi provides support for two types of single-tiered applications: BDE-based applications, and flat-file database applications. Unless you are coding the database logic yourself in a custom dataset, two-tiered applications always use the BDE.

BDE-based applications

Because the data access components (and Borland Database Engine) handle the details of reading data, updating data, and navigating data, writing BDE-based two- tiered applications is essentially the same as writing BDE-based one-tiered applications. Buildingone- andtwo- tieredapplications14-1, BDE- basedapplicationsWhen deploying BDE-based applications, you must include the BDE with your application. While this increases the size of the application and the complexity of deployment, the BDE can be shared with other BDE-based applications and provides many advantages. BDE-based applications allow you to use the powerful library of Borland Database Engine API calls. Even if you do not want to use the BDE API, writing BDE-based applications gives you support for the following features not available to other applications such as flat-file database application: • Connecting to databases • Using transactions • Caching updates • Creating and restructuring database tables

BDE-based architecture

A BDE-based one- or two-tiered application includes • A user interface containing data-aware controls. • One or more datasets that represent information from the database tables. • A datasource component for each dataset to connect the data-aware controls to the datasets. • Optionally, one or more database components to control transactions in both one- and two-tiered applications and to manage database connections in two-tiered applications. • Optionally, one or more session components to isolate data access operations such as database connections, and to manage groups of databases. The relationships between these elements is illustrated in Figure 14.1: Figure 14.1 Components in a BDE-based application data source DataSet user interface Borland database elements Databasedata source DataSet Engine database Session Form Data Module

Understanding databases and datasets

Databases contain information stored in tables. They may also include tables of information about what is contained in the database, objects such as indexes that are 14-2Developer’ sGuide, BDE- basedapplicationsused by tables, and SQL objects such as stored procedures. See Chapter 17, “Connecting to databases” for more information about databases. The Data Access page of the Component palette contains various dataset components that represent the tables contained in a database or logical tables constructed out of data stored in those database tables. See “Selecting what data to show” on page 13-13 for more information about these dataset components. You must include a dataset component in your application to work with database information. Each BDE-enabled dataset component on the Data Access page has a published DatabaseName property that specifies the database which contains the table or tables that hold the information in that dataset. When setting up your application, you must use this property to specify the database before you can bind the dataset to specific information contained in that database. What value you specify depends on whether • The database has a BDE alias. You can specify a BDE alias as the value of DatabaseName. A BDE alias represents a database plus configuration information for that database. The configuration information associated with an alias differs by database type (Oracle, Sybase, Interbase, Paradox, dBASE, and so on). Use the BDE Administration tool or the SQL explorer to create and manage BDE aliases. • The database is a Paradox or dBASE database. If you are using a Paradox or dBASE database, DatabaseName can specify the directory where the database tables are located. • You are using explicit database components. Database components (TDatabase) represent a database in your application. If you don’t add a database component explicitly, a temporary one is created for you automatically, based on the value of the DatabaseName property. If you are using explicit database components, DatabaseName is the value of the DatabaseName property of the database component. See “Understanding persistent and temporary database components” on page 17-1 for more information about using database components. Using sessions Sessions isolate data access operations such as database connections, and manage groups of databases. All use of the Borland Database Engine takes place in the context of a session. You can use sessions to specify configuration information that applies to all the databases in the session. This allows you to override the default behavior specified using the BDE administration tool. You can use a session to • Manage BDE aliases. You can create new aliases, delete aliases, and modify existing aliases. By default, changes affect only the session, but you can write changes so that they are added to the permanent BDE configuration file. For more information on managing BDE aliases, see “Working with BDE aliases” on page 16-9. • Control when database connections in two-tiered applications are closed. Keeping database connections open when none of the datasets in the database are active ties up resources that could be released, but improves speed and reduces network traffic. To keep database connections open even when there are no active datasets, the KeepConnections property should be True (the default). Buildingone- andtwo- tieredapplications14-3, BDE- basedapplications• Manage access to password-protected Paradox and dBASE files in one-tiered applications. Datasets that access password-protected Paradox and dBASE tables use the session component to supply a password when these tables must be opened. You can override the default behavior (a password dialog that appears whenever a password is needed), to supply passwords programmatically. If you intend to scale your one-tiered application to a two-tiered or multi-tiered application, you can create a common user interface for obtaining user authentication information that need not change when you switch to using remote database servers which require a username and password at the server (rather than table) level. For more information about using sessions to manage Paradox and dBASE passwords, see “Working with password-protected Paradox and dBase tables” on page 16-13. • Specify the location of special Paradox directories. Paradox databases that are shared on a network use a net directory which contains temporary files that specify table and record locking information. Paradox databases also use a private directory where temporary files such as the results of queries are kept. For more information on specifying these directory locations, see “Specifying Paradox directory locations” on page 16-13. If your application may be accessing the same database multiple times simultaneously, you must use multiple sessions to isolate these uses of the database. Failure to do so will disrupt the logic governing transactions on that database (including transactions created for you automatically). Applications risk simultaneous access when running concurrent queries or when using multiple threads. For more information about using multiple sessions, see “Managing multiple sessions” on page 16-16. Unless you need to use multiple sessions, you can use the default session.

Connecting to databases

The Borland Database Engine includes drivers to connect to different databases. The Standard version of Delphi includes only the drivers for local databases: Paradox, dBASE, FoxPro, and Access. With the Professional version, you also get an ODBC adapter that allows the BDE to use ODBC drivers. By supplying an ODBC driver, your application can use any ODBC-compliant database. The Client/Server and Enterprise versions also include drivers for remote database servers. Use the drivers installed with SQL Links to communicate with remote database servers such as Interbase, Oracle, Sybase, Informix, Microsoft SQL server, and DB2. Note The only difference between a BDE-based one-tiered application and a BDE-based two-tiered application is whether it uses local databases or remote database servers.

Using transactions

A transaction is a group of actions that must all be carried out successfully on one or more tables in a database before they are committed (made permanent). If one of the actions in the group fails, then all actions are rolled back (undone). By using 14-4Developer’ sGuide, BDE- basedapplicationstransactions, you ensure that the database is not left in an inconsistent state when a problem occurs completing one of the actions that make up the transaction. For example, in a banking application, transferring funds from one account to another is an operation you would want to protect with a transaction. If, after decrementing the balance in one account, an error occurred incrementing the balance in the other, you want to roll back the transaction so that the database still reflects the correct total balance. By default, the BDE provides implicit transaction control for your applications. When an application is under implicit transaction control, a separate transaction is used for each record in a dataset that is written to the underlying database. Implicit transactions guarantee both a minimum of record update conflicts and a consistent view of the database. On the other hand, because each row of data written to a database takes place in its own transaction, implicit transaction control can lead to excessive network traffic and slower application performance. Also, implicit transaction control will not protect logical operations that span more than one record, such as the transfer of funds described previously. If you explicitly control transactions, you can choose the most effective times to start, commit, and roll back your transactions. When you develop applications in a multi- user environment, particularly when your applications run against a remote SQL server, you should control transactions explicitly. Note You can also minimize the number of transactions you need by caching updates. For more information about cached updates, see Chapter 24, “Working with cached updates.” Explicitly controlling transactions There are two mutually exclusive ways to control transactions explicitly in a BDE- based database application: • Use the methods and properties of the database component, such as StartTransaction, Commit, Rollback, InTransaction, and TransIsolation. The main advantage to using the methods and properties of a database component to control transactions is that it provides a clean, portable application that is not dependent on a particular database or server. • Use passthrough SQL in a query component to pass SQL statements directly to remote SQL or ODBC servers. For more information about query components, see Chapter 21, “Working with queries.” The main advantage to passthrough SQL is that you can use the advanced transaction management capabilities of a particular database server, such as schema caching. To understand the advantages of your server’s transaction management model, see your database server documentation. One-tiered applications can‘t use passthrough SQL. You can use the database component to create explicit transactions for local databases. However, there are limitations to using local transactions. For more information on using local transactions, see “Using local transactions” on page 14-8. When writing two-tiered applications (which require SQL links, available only in the Client/Server and Enterprise versions of Delphi), you can use either a databaseBuildingone- andtwo- tieredapplications14-5, BDE- basedapplicationscomponent or passthrough SQL to manage transactions. For more information about using passthrough SQL, see “Using passthrough SQL” on page 14-8. Using a database component for transactions When you start a transaction, all subsequent statements that read from and write to the database occur in the context of that transaction. Each statement is considered part of a group. Changes must be successfully committed to the database, or every change made in the group must be undone. Ideally, a transaction should only last as long as necessary. The longer a transaction is active, the more simultaneous users that access the database, and the more concurrent, simultaneous transactions that start and end during the lifetime of your transaction, the greater the likelihood that your transaction will conflict with another when you attempt to commit your changes. When using a database component, you code a single transaction as follows: 1 Start the transaction by calling the database’s StartTransaction method: DatabaseInterBase.StartTransaction; 2 Once the transaction is started, all subsequent database actions are considered part of the transaction until the transaction is explicitly terminated. You can determine whether a transaction is in process by checking the database component’s InTransaction property. While the transaction is in process, your view of the data in database tables is determined by you transaction isolation level. For more information about transaction isolation levels, see “Using the TransIsolation property” on page 14-6. 3 When the actions that make up the transaction have all succeeded, you can make the database changes permanent by using the database component’s Commit method: DatabaseInterBase.Commit; Commit is usually attempted in a try...except statement. That way, if a transaction cannot commit successfully, you can use the except block to handle the error and retry the operation or to roll back the transaction. 4 If an error occurs when making the changes that are part of the transaction, or when trying to commit the transaction, you will want to discard all changes that make up the transaction. To discard these changes, use the database component’s Rollback method: DatabaseInterBase.Rollback; Rollback usually occurs in • Exception handling code when you cannot recover from a database error • Button or menu event code, such as when a user clicks a Cancel button Using the TransIsolation property TransIsolation specifies the transaction isolation level for a database component’s transactions. Transaction isolation level determines how a transaction interacts with 14-6Developer’ sGuide, BDE- basedapplicationsother simultaneous transactions when they work with the same tables. In particular, it affects how much a transaction “sees” of other transactions’ changes to a table. The default setting for TransIsolation is tiReadCommitted. The following table summarizes possible values for TransIsolation and describes what they mean: Table 14.1 Possible values for the TransIsolation property Isolation level Meaning tiDirtyRead Permit reading of uncommitted changes made to the database by other simultaneous transactions. Uncommitted changes are not permanent, and might be rolled back (undone) at any time. At this level your transaction is least isolated from the changes made by other transactions. tiReadCommitted Permit reading only of committed (permanent) changes made to the database by other simultaneous transactions. This is the default isolation level. tiRepeatableRead Permit a single, one time reading of the database. Your transaction cannot see any subsequent changes to data by other simultaneous transactions. This isolation level guarantees that once your transaction reads a record, its view of that record will not change. At this level your transaction is most isolated from changes made by other transactions. Database servers may support these isolation levels differently or not at all. If the requested isolation level is not supported by the server, the BDE uses the next highest isolation level. The actual isolation level used by some servers is shown in Table 14.2, “Transaction isolation levels.” For a detailed description of how each isolation level is implemented, see your server documentation. Table 14.2 Transaction isolation levels Server Specified Level Actual Level Oracle tiDirtyRead tiReadCommitted tiReadCommitted tiReadCommitted tiRepeatableRead tiRepeatableRead (READONLY) Sybase, MS-SQL tiDirtyRead tiReadCommitted tiReadCommitted tiReadCommitted tiRepeatableRead Not supported DB2 tiDirtyRead tiDirtyRead tiReadCommitted tiReadCommitted tiRepeatableRead tiRepeatableRead Informix tiDirtyRead tiDirtyRead tiReadCommitted tiReadCommitted tiRepeatableRead tiRepeatableRead InterBase tiDirtyRead tiReadCommitted tiReadCommitted tiReadCommitted tiRepeatableRead tiRepeatableRead Paradox, dBASE, tiDirtyRead tiDirtyRead Access, FoxPro tiReadCommitted Not supported tiRepeatableRead Not supported Note When using transactions with local Paradox, dBASE, Access, and FoxPro tables, set TransIsolation to tiDirtyRead instead of using the default value of tiReadCommitted. ABuildingone- andtwo- tieredapplications14-7, BDE- basedapplicationsBDE error is returned if TransIsolation is set to anything but tiDirtyRead for local tables. If an application is using ODBC to interface with a server, the ODBC driver must also support the isolation level. For more information, see your ODBC driver documentation. Using passthrough SQL With passthrough SQL, you use a TQuery, TStoredProc, or TUpdateSQL component to send an SQL transaction control statement directly to a remote database server. The BDE does not process the SQL statement. Using passthrough SQL enables you to take direct advantage of the transaction controls offered by your server, especially when those controls are non-standard. To use passthrough SQL to control a transaction, you must • Use the Client/Server Suite. You can also use the Enterprise edition when working with BDE-based application servers, but not with Entera data access servers. • Install the proper SQL Links drivers. If you chose the “Typical” installation when installing Delphi, all SQL Links drivers are already properly installed. • Configure your network protocol correctly. See your network administrator for more information. • Have access to a database on a remote server. • Set SQLPASSTHRU MODE to NOT SHARED using the SQL Explorer. SQLPASSTHRU MODE specifies whether the BDE and passthrough SQL statements can share the same database connections. In most cases, SQLPASSTHRU MODE is set to SHARED AUTOCOMMIT. However, you can’t share database connections when using transaction control statements. For more information about SQLPASSTHRU modes, see the help file for the BDE Administration utility. Note When SQLPASSTHRU MODE is NOT SHARED, you must use separate database components for datasets that pass SQL transaction statements to the server and datasets that do not. Using local transactions The BDE supports local transactions against local Paradox, dBASE, Access, and FoxPro tables. From a coding perspective, there is no difference to you between a local transaction and a transaction against a remote database server. When a transaction is started against a local table, updates performed against the table are logged. Each log record contains the old record buffer for a record. When a transaction is active, records that are updated are locked until the transaction is committed or rolled back. On rollback, old record buffers are applied against updated records to restore them to their pre-update states. 14-8Developer’ sGuide, BDE- basedapplicationsLocal transactions are more limited than transactions against SQL servers or ODBC drivers. In particular, the following limitations apply to local transactions: • Automatic crash recovery is not provided. • Data definition statements are not supported. • Transactions cannot be run against temporary tables. • For Paradox, local transactions can only be performed on tables with valid indexes. Data cannot be rolled back on Paradox tables that do not have indexes. • Only a limited number of records can be locked and modified. With Paradox tables, you are limited to 255 records. With dBASE the limit is 100. • Transactions cannot be run against the BDE ASCII driver. • TransIsolation level must only be set to tiDirtyRead. • Closing a cursor on a table during a transaction rolls back the transaction unless: • Several tables are open. • The cursor is closed on a table to which no changes were made.

Caching updates

The Borland Database Engine provides support for caching updates. When you cache updates, your application retrieves data from a database, makes all changes to a local, cached copy of the data, and applies the cached changes to the dataset as a unit. Cached updates are applied to the database in a single transaction. Caching updates can minimize transaction times and reduce network traffic. However, cached data is local to your application and is not under transaction control. This means that while you are working on your local, in-memory, copy of the data, other applications can be changing the data in the underlying database table. They also can’t see any changes you make until you apply the cached updates. Because of this, cached updates may not be appropriate for applications that work with volatile data, as you may create or encounter too many conflicts when trying to merge your changes into the database. You can tell BDE-enabled datasets to cache updates using the CachedUpdates property. When the changes are complete, they can be applied by the dataset component, by the database component, or by a special update object. When changes can’t be applied to the database without additional processing (for example, when working with a joined query), you must use the OnUpdateRecord event to write changes to each table that makes up the joined view. For more information on caching updates, see Chapter 24, “Working with cached updates.” Buildingone- andtwo- tieredapplications14-9, Flat- filedatabaseapplications

Creating and restructuring database tables

In BDE-based applications, you can use the TTable component to create new database tables and to add indexes to existing tables. You can create tables either at design time, in the Forms Designer, or at runtime. To create a table, you must specify the fields in the table using the FieldDefs property, add any indexes using the IndexDefs property, and call CreateTable method (or select the Create Table command from the table’s context menu). For more detailed instructions on creating tables, see “Creating a table” on page 20-17. Note When creating Oracle8 tables, you can’t create object fields (ADT fields, array fields, reference fields, and dataset fields). If you want to restructure a table at runtime (other than by adding indexes), you must use the BDE API DbiDoRestructure. You can add indexes to an existing table using the AddIndex method of TTable. Note At design time, you can use the Database Desktop to create and restructure Paradox and dBASE tables. To create and restructure tables on remote servers, use the SQL Explorer and restructure the table using SQL.

Flat-file database applications

Flat-file database applications are single-tiered applications that use TClientDataSet to represent all of their datasets. The client dataset holds all its data in memory, which means that this type of application is not appropriate for extremely large datasets. Note TClientDataSet is only available in the Client/Server and Enterprise versions. Flat-file database applications do not require the Borland Database Engine (BDE). Instead, they only use DBCLIENT.DLL. This means the application itself does not use as much memory as BDE-based applications. (You may still require a lot of memory on client machines if the datasets are very large.) By using only DBCLIENT.DLL, flat-file applications are also easier to deploy because you do not need to install, configure, and maintain the BDE. Because these applications do not use the BDE, there is no support for multiple users. Instead, the datasets should be dedicated entirely to the application. Data can be saved to flat files on disk, and loaded at a later time, but there is no built-in protection to prevent multiple users from overwriting each other’s data files. Client datasets (located on the MIDAS page of the Component palette) form the basis of flat-file database applications. They provide support for most of the database operations you perform with BDE-enabled datasets. You use the same data-aware controls and data source components that you would use in a BDE-based single-tiered application. You don’t use database components, because there is no database connection to manage, and no transactions to support. You do not need to be concerned with session components unless your application is multi-threaded. For 14-10Developer’ sGuide, Flat- filedatabaseapplicationsmore information about using client datasets, see Chapter 23, “Creating and using a client dataset.” The main differences in writing flat-file database applications and BDE-based applications lie in how you create the datasets and how you load and save data.

Creating the datasets

Because flat-file database applications do not use existing databases, you are responsible for creating the datasets yourself. Once the dataset is created, you can save it to a file. From then on, you do not need to recreate the table, only load it from the file you saved. However, indexes are not saved with the table. You need to recreate them every time you load the table. When beginning a flat-file database application, you may want to first create and save empty files for your datasets before beginning the writing of the application itself. This way, you do not need to define the metadata for your client datasets in the final application. How you create your client dataset depends on whether you are creating an entirely new dataset, or converting an existing BDE-based application. Creating a new dataset using persistent fields The following steps describe how to create a new client dataset using the Fields Editor: 1 From the MIDAS page of the Component palette, add a TClientDataSet component to your application. 2 Right-click the client dataset and select Fields Editor. In the Fields editor, right- click and choose the New Field command. Describe the basic properties of your field definition. Once the field is created, you can alter its properties in the Object Inspector by selecting the field in the Fields editor. Continue adding fields in the fields editor until you have described your client dataset. 3 Right-click the client dataset and choose Create DataSet. This creates an empty client dataset from the persistent fields you added in the Fields Editor. 4 Right-click the client dataset and choose Save To File. (This command is not available unless the client dataset contains data.) 5 In the File Save dialog, choose a file name and save a flat file copy of your client dataset. Note You can also create the client dataset at runtime using persistent fields that are saved with the client dataset. Simply call the CreateDataSet method. Creating a dataset using field and index definitions Creating a client dataset using field and index definitions is much like using a TTable component to create a database table. There is no DatabaseName, TableName, orBuildingone- andtwo- tieredapplications14-11, Flat- filedatabaseapplicationsTableType property to specify, as these are not relevant to client datasets. However, just as with TTable, you use the FieldDefs property to specify the fields in your table and the IndexDefs property to specify any indexes. Once the table is specified, right-click the client dataset and choose Create DataSet at design time, or call the CreateDataSet method at runtime. When defining the index definitions for your client dataset, two properties of the index definition apply uniquely to client datasets. These are TIndexDef.DescFields and TIndexDef.CaseInsFields. DescFields lets you define indexes that sort records in ascending order on some fields and descending order on other fields. Instead of using the ixDescending option to sort in descending order on all the fields in the index, list only those fields that should sort in descending order as the value of DescFields. For example, when defining an index that sorts on Field1, then Field2, then Field3, setting DescFields to Field1;Field3 results in an index that sorts Field2 in ascending order and Field1 and Field3 in descending order. CaseInsFields lets you define indexes that sort records case-sensitively on some fields and case-insensitively on other fields. Instead of using the isCaseInsensitive option to sort case-insensitively on all the fields in the index, list only those fields that should sort case-insensitively as the value of CaseInsFields. Like DescFields, CaseInsFields takes a semicolon-delimited list of field names. You can specify the field and index definitions at design time using the Collection editor. Just choose the appropriate property in the Object Inspector (FieldDefs or IndexDefs), and double-click to display the Collection editor. Use the Collection editor to add, delete, and rearrange definitions. By selecting definitions in the Collection editor you can edit their properties in the Object Inspector. You can also specify the field and index definitions in code at runtime. For example, the following code creates and activates a client dataset in the form’s OnCreate event handler: procedure TForm1.FormCreate(Sender: TObject); begin with ClientDataSet1 do begin with FieldDefs.AddFieldDef do begin DataType := ftInteger; Name := 'Field1'; end; with FieldDefs.AddFieldDef do begin DataType := ftString; Size := 10; Name := 'Field2'; end; with IndexDefs.AddIndexDef do 14-12Developer’ sGuide, Flat- filedatabaseapplicationsbegin Fields := 'Field1'; Name := 'IntIndex'; end; CreateDataSet; end; end; Creating a dataset based on an existing table If you are converting an existing BDE-based application into a single-tiered flat-file application, you can copy existing tables and save them as flat-file tables from the IDE. The following steps indicate how to copy an existing table: 1 From the Data Access page of the Component palette, add a TTable component to your application. Set its DatabaseName and TableName properties to identify the existing database table. Set its Active property to True. 2 From the MIDAS page of the Component palette, add a TClientDataSet component. 3 Right-click the client dataset and select Assign Local Data. In the dialog that appears, choose the table component that you added in step 1. Choose OK. 4 Right-click the client dataset and choose Save To File. (This command is not available unless the client dataset contains data.) 5 In the File Save dialog, choose a file name and save a flat-file copy of your database table.

Loading and saving data

In flat-file database applications, all modifications to the table exist only in an in- memory change log. This log is maintained separately from the data itself, although it is completely transparent to objects that use the client dataset. That is, controls that navigate the client dataset or display its data see a view of the data that includes the changes. If you do not want to back out of changes, however, you should merge the change log into the data of the client dataset by calling the MergeChangeLog method. For more information about the change log, see “Editing data” on page 23-4. Even when you have merged changes into the data of the client dataset, this data still exists only in memory. While it will persist if you close the client dataset and reopen it in your application, it will disappear when your application shuts down. To make the data permanent, it must be written to disk. Write changes to disk using the SaveToFile method. SaveToFile takes one parameter, the name of the file which is created (or overwritten) containing the table. When you want to read a table previously written using the SaveToFile method, use the LoadFromFile method. LoadFromFile also takes one parameter, the name of the file containing the table. When you save a client dataset, the metadata that describes the record structure is saved with the dataset, but not the indexes. Because of this, you may want to addBuildingone- andtwo- tieredapplications14-13, Flat- filedatabaseapplicationscode that recreates the indexes when you load the data from file. Alternately, you might want to write your application so that it always creates indexes on the fly in an as-needed fashion. If you always load to and save from the same file, you can use the FileName property instead of the SaveToFile and LoadFromFile methods. When FileName is set to a valid file name, the data is automatically loaded from the file when the client dataset is opened and saved to the file when the client dataset is closed.

Using the briefcase model

Most of this chapter has described creating and using a client dataset in a one-tiered application. The one-tiered model can be combined with a multi-tiered model to create what is called the briefcase model. In this model, a user starts a client application on one machine and connects over a network to an application server on a remote machine. The client requests data from the application server, and sends updates to it. The updates are applied by the application server to a database that is presumably shared with other clients throughout an organization. Note The briefcase model is sometimes called the disconnected model, or mobile computing. Suppose, however, that your onsite company database contains valuable customer contact data that your sales representatives can use and update in the field. In this case, it would be useful if your sales reps could download some or all of the data from the company database, work with it on their laptops as they fly across the country, and even update records at existing or new customer sites. When the sales reps return onsite, they could upload their data changes to the company database for everyone to use. The ability to work with data off-line and then apply updates online at a later date is known as the “briefcase” model. By using the briefcase model, you can take advantage of the client dataset component’s ability to read and write data to flat files to create client applications that can be used both online with an application server, and off-line, as temporary one-tiered applications. To implement the briefcase model, you must 1 Create a multi-tiered server application as described in “Creating the application server” on page 15-10. 2 Create a flat-file database application as your client application. Add a connection component and set the RemoteServer property of your client datasets to specify this connection component. This allows them to talk to the application server created in step 1. For more information about connection components, see “Connecting to the application server” on page 15-24. 3 In the client application, try on start-up to connect to the application server. If the connection fails, prompt the user for a file and read in the local copy of the data. 4 In the client application, add code to apply updates to the application server. For more information on sending updates from a client application to an application server, see “Updating records” on page 15-30. 14-14Developer’ sGuide, Scalinguptoathree- tieredapplication

Scaling up to a three-tiered application

In a two-tiered client/server application, the application is a client that talks directly to a database server. Even so, the application can be thought of as having two parts: a database connection and a user interface. To make a two-tiered client/server application into a multi-tiered application you must: • Split your existing application into an application server that handles the database connection, and into a client application that contains the user interface. • Add an interface between the client and the application server. There are a number of ways to proceed, but the following sequential steps may best keep your translation work to a minimum: 1 Create a new project for the application server, duplicate the relevant database connection portions of your former two-tiered application, and for each dataset, add a provider component that will act as a data conduit between the application server and the client. For more information on using a provider component, see “Creating a data provider for the application server” on page 15-15. 2 Copy your existing two-tiered project, remove its direct database connections, add an appropriate connection component to it. For more information about creating and using connection components, see “Connecting to the application server” on page 15-24. 3 Substitute a client dataset for each BDE-enabled dataset component in the original project. For general information about using a client dataset component, see Chapter 23, “Creating and using a client dataset.” 4 In the client application, add code to apply updates to the application server. For more information on sending updates from a client application to an application server, see “Updating records” on page 15-30. 5 Move the dataset components to the application server’s data modules. Set the DataSet property of each provider to specify the corresponding datasets. For more information about linking a dataset to a provider component, see “Creating a data provider for the application server” on page 15-15. Buildingone- andtwo- tieredapplications14-15, 14-16Developer’ sGuide,

Chapter

Chapter 15Creating multi-tiered applications This chapter describes how to create a multi-tiered, client/server database application. A multi-tiered client/server application is partitioned into logical units which run in conjunction on separate machines. Multi-tiered applications share data and communicate with one another over a local-area network or even over the Internet. They provide many benefits, such as centralized business logic and thin client applications. In its simplest form, sometimes called the “three-tiered model,” a multi-tiered application is partitioned into thirds: • Client application: provides a user interface on the user’s machine. • Application server: resides in a central networking location accessible to all clients and provides common data services. • Remote database server: provides the relational database management system (RDBMS). In this three-tiered model, the application server manages the flow of data between clients and the remote database server, so it is sometimes called a “data broker.” With Delphi you usually only create the application server and its clients, although, if you are really ambitious, you could create your own database back end as well. In more complex multi-tiered applications, additional services reside between a client and a remote database server. For example, there might be a security services broker to handle secure Internet transactions, or bridge services to handle sharing of data with databases on platforms not directly supported by Delphi. If you have the Enterprise edition, see the Enterprise Developer’s Guide for more information about building such complex multi-tiered applications. Delphi support for multi-tiered applications is based on the Multi-tier Distributed Application Services Suite (MIDAS). This chapter focuses on creating a three-tiered database application using the MIDAS technology. Once you understand how to create and manage a three-tiered application, you can create and add additional service layers based on your needs. Creatingmulti- tieredapplications15-1, Advantagesofthemulti- tiereddatabasemodel

Advantages of the multi-tiered database model

The multi-tiered database model breaks a database application into logical pieces. The client application can focus on data display and user interactions. Ideally, it knows nothing about how the data is stored or maintained. The application server (middle tier) coordinates and processes requests and updates from multiple clients. It handles all the details of defining datasets and interacting with the remote database server. The advantages of this multi-tiered model include the following: • Encapsulation of business logic in a shared middle tier. Different client applications all access the same middle tier. This allows you to avoid the redundancy (and maintenance cost) of duplicating your business rules for each separate client application. • Thin client applications. Your client applications can be written to make a small footprint by delegating more of the processing to middle tiers. Not only are client applications smaller, but they are easier to deploy because they don’t need to worry about installing, configuring, and maintaining the database connectivity software (such as the Borland Database Engine). • Distributed data processing. Distributing the work of an application over several machines can improve performance because of load balancing, and allow redundant systems to take over when a server goes down. • Increased opportunity for security. You can isolate sensitive functionality into tiers that have different access restrictions. This provides flexible and configurable levels of security. Middle tiers can limit the entry points to sensitive material, allowing you to control access more easily. If you are using CORBA or MTS, you can take advantage of the security models they support.

Understanding MIDAS technology

MIDAS provides the mechanism by which client applications and application servers communicate database information. Using MIDAS requires DBCLIENT.DLL, which is used by both client and server applications to manage datasets stored as data packets. MIDAS also includes the SQL explorer to help in database administration and to import server constraints into the Data Dictionary, so that they can be checked at any level of the multi-tiered application. You may also want to use OLEnterprise, which provides a COM-based brokering service (the Business Object Broker) for location transparency, load-balancing, and fail-over. Note You must purchase server licenses for deploying your MIDAS applications. MIDAS-based multi-tiered applications use the components on the MIDAS page of the component palette, plus a remote data module that is created by a wizard on the Multitier page of the New Items dialog. These components are described in Table 15.1: 15-2Developer’ sGuide, UnderstandingMIDAStechnologyTable 15.1 MIDAS components Component Description remote data Specialized data modules that can act as a COM Automation server or modules CORBA server to give client applications access to any providers they contain. Used on the application server. provider A data broker that provides data by creating data packets and resolves component client updates. It exposes these services through the IProvider interface. Used on the application server. client dataset A specialized dataset that uses DBCLIENT.DLL instead of the Borland component Database Engine to manage data stored in data packets. connection A family of components that locate the server, form connections, and make components the IProvider interface available to client datasets. Each connection component is specialized to use a particular communications protocol.

Overview of a MIDAS-based multi-tiered application

The following numbered steps illustrate a normal sequence of events for a MIDAS- based multi-tiered application: 1 A user starts the client application. The client connects to the application server (which can be specified at design time or runtime). If the application server is not already running, it starts. The client receives a provider interface from the application server. 2 The client requests data from the application server. A client may request all data at once, or may request chunks of data throughout the session (fetch on demand). 3 The application server retrieves the data (first establishing a database connection, if necessary), packages it for the client, and returns a data packet to the client. Additional information, (for example, about data constraints imposed by the database) can be included in the metadata of the data packet. This process of packaging data into data packets is called “providing.” 4 The client decodes the data packet and displays the data to the user. 5 As the user interacts with the client application, the data is updated (records are added, deleted, or modified). These modifications are stored in a change log by the client. 6 Eventually the client applies its updates to the application server, usually in response to a user action. To apply updates, the client packages its change log and sends it as a data packet to the server. 7 The application server decodes the package and posts updates in the context of a transaction. If a record can’t be posted to the server (for example, because another application changed the record after the client requested it and before the client applied its updates), the application server either attempts to reconcile the client’s changes with the current data, or saves the records that could not be posted. This process of posting records and caching problem records is called “resolving.” Creatingmulti- tieredapplications15-3, UnderstandingMIDAStechnology8When the application server finishes the resolving process, it returns any unposted records to the client for further resolution. 9 The client reconciles unresolved records. There are many ways a client can reconcile unresolved records. Typically the client attempts to correct the situation that prevented records from being posted or discards the changes. If the error situation can be rectified, the client applies updates again. 10 The client refreshes its data from the server.

The structure of the client application

To the end user, the client application of a multi-tiered application looks and behaves no differently than a traditional two-tiered application. Structurally, the client application looks a lot like a flat-file single-tiered application. User interaction takes place through standard data-aware controls that display data from a client dataset component. For detailed information about using the properties, events, and methods of client datasets, see Chapter 23, “Creating and using a client dataset.” Unlike in a flat-file application, the client dataset in a multi-tiered application obtains its data through an interface on the application server. It uses this interface to post updates to the application server as well. In most cases, this is the IProvider interface. For more information about the IProvider interface, see “Using the IProvider interface” on page 15-7. The client gets this interface from a connection component. Note When using MTS, you may choose not to use the IProvider interface with your client datasets. By writing code to provide data to and apply updates from the client dataset without using the IProvider interface, your MTS application can take advantage of MTS features such as transactions and just-in-time activation. For more information, see “Using MTS” on page 15-5. The connection component establishes the connection to the application server. Different connection components are available for using different communications protocols. These connection components are summarized in the following table: Table 15.2 Connection components Component Protocol TDCOMConnection DCOM TSocketConnection Windows sockets (TCP/IP) TOLEnterpriseConnection OLEnterprise (RPCs) TCorbaConnection CORBA (IIOP) Note Two other connection components, TRemoteServer and TMIDASConnection, are provided for backward compatibility. Once the connection is established, the connection component uses the IDataBroker interface of the application server’s remote data module to get IProvider interfaces for any datasets in the remote data module. For more information about IDataBroker, see “Using the IDataBroker interface” on page 15-6. For more information about using connection components, see “Connecting to the application server” on page 15-24. 15-4Developer’ sGuide, UnderstandingMIDAStechnology

The structure of the application server

The application server includes a remote data module that provides an IDataBroker interface which client applications use to obtain access to data providers. There are three types of remote data modules: • TRemoteDataModule: This is a dual-interface Automation server. Use this type of remote data module if clients use DCOM, sockets, or OLEnterprise to connect to the application server, unless you want to install the application server with MTS. • TMTSDataModule: This is a dual-interface Automation server. Use this type of remote data module if you are creating the application server as an Active Library (.DLL) that is installed with MTS. You can use MTS remote data modules with DCOM, Sockets, or OLEnterprise. • TCorbaDataModule: This is a CORBA server. Use this type of remote data module to provide data to CORBA clients. As with any data module, you can include any nonvisual component in the remote data module. In addition, the remote data module usually includes a provider component for each dataset the application server makes available to client applications. A provider surfaces the IProvider interface so that it can • Receive data requests from the client, retrieve the requested data from the database server, package the data for transmission, and send the data to the client dataset. This activity is called “providing.” • Receive updated data from the client dataset, apply updates to the database, and log any updates that cannot be applied, returning unresolved updates to the client for further reconciliation. This activity is called “resolving.” For detailed information about the IProvider interface, see “Using the IProvider interface” on page 15-7. Note If you do not supply a provider component for a BDE-enabled dataset in the application server, one will be created for you dynamically. However, explicitly adding provider components gives you greater control and lets you provide from other types of dataset. Usually, the provider uses BDE-enabled datasets such as you find in a BDE-based two-tiered application. You can add database and session components as needed, just as in a BDE-based two-tiered application. For more information about BDE- based two-tiered applications, see “BDE-based applications” on page 14-1. Using MTS Using MTS lets your remote data module take advantage of • MTS security. MTS provides role-based security for your application server. Clients are assigned roles, which determine whether they can access the remote data module’s interface. The MTS data module implements the IsCallerInRole method, which you lets you check the role of the currently connected client and conditionally allow certain functions based on that role. For more information about MTS security, see “Role-based security” on page 49-11. Creatingmulti- tieredapplications15-5, UnderstandingMIDAStechnology• Database handle pooling. MTS data modules automatically pool database connections so that when one client is finished with a database connection, another client can reuse it. This cuts down on network traffic, because your middle tier does not need to log off of the remote database server and then log on again. When pooling database handles, your database component should set the KeepConnection property to False, so that your application maximizes the sharing of connections. • MTS transactions. When using MTS, you can integrate your own MTS transactions into the application server to provide enhanced transaction support. MTS transactions can span multiple databases, or include functions that do not involve databases at all. For more information about MTS transactions, see “Managing transactions in multi-tiered applications” on page 15-35. • Just-in-time activation and as-soon-as-possible deactivation. You can write your MTS server so that remote data module instances are activated and deactivated on an as-needed basis. When using just-in-time activation and as-soon-as-possible deactivation, your remote data module is instantiated only when it is needed to handle client requests. This prevents your application server from tying up database handles when they are not in use. Using just-in-time activation and as-soon-as-possible deactivation provides a middle ground between routing all clients through a single remote data module instance, and creating a separate instance for every client connection. With a single remote data module instance, the remote data module must handle all database calls through a single database connection. This acts as a bottleneck, and can impact performance when there are many clients. With multiple instances of the remote data module, each instance can maintain a separate database connection, thereby avoiding the need to serialize database access. However, this monopolizes resources because other clients can’t use the database connection while it is associated with another client’s remote data module. To take advantage of transactions, just-in-time activation, and as-soon-as-possible deactivation, remote data module instances must be stateless. Because the IProvider interface relies on state information, clients can’t use the IProvider interface if you are taking advantage of these features. Instead, you must create your own interface for providing data and applying updates. For more information about creating stateless remote data modules in multi-tiered applications, see “Supporting stateless remote data modules” on page 15-37. Warning When using MTS, database connections should not be opened until the remote data module is activated. While developing your application, be sure that all datasets are not active and the database is not connected before running your application. In the application itself, you must add code to open database connections when the data module is activated and to close them when the data module is deactivated. Using the IDataBroker interface Remote data modules on the application server support the IDataBroker interface. Connection components on client applications look for this interface to form connections. 15-6Developer’ sGuide, UnderstandingMIDAStechnologyIDataBroker defines only one method: GetProviderNames. Connection components call GetProviderNames to obtain a list of provider components on the application server. Client datasets connect to a provider from this list by setting their ProviderName property to one of these names. Note When clients use GetProviderNames to connect client datasets to a provider on the application server, the state of the remote data module must be preserved as long as the client holds a reference to the provider interface. This requirement may interfere with MTS transactions or just-in-time activation, and can cause problems with a single-instance CORBA server.

Using the IProvider interface

IProvider is implemented by a provider component on the application server. Client datasets in the client application use the IProvider interface to communicate with these provider components. Most client applications do not use the IProvider interface directly, but invoke it indirectly through the properties and methods of the client dataset. However, when necessary, you can make direct calls to the IProvider interface by using the Provider property of the client dataset. Table 15.3 lists the properties and methods of the IProvider interface, the corresponding properties and methods in TProvider that implement them, and the corresponding properties and methods in TClientDataSet that use them. Table 15.3 IProvider interface members IProvider TProvider TClientDataset ApplyUpdates method ApplyUpdates method Used by the ApplyUpdates method. Constraints property Constraints property Applications must use the interface directly. Data property Data property Data property. DataRequest method DataRequest method Applications must use the interface directly. Get_Constraints method Constraints property Applications must use the interface directly. Get_Data method Get_Data method Used to implement the Data property. GetMetaData method GetRecords method Used internally. (Count = 0) GetRecords method GetRecords method Used by the GetNextPacket method. Reset method Reset method Used internally. Set_Constraints method Constraints property Applications must use the interface directly. SetParams method SetParams method Used for the Params property. Note All the properties and many of the methods of the IProvider interface rely on state information in the remote data module. Because of this, you may not always want to use this interface for applications that use CORBA or MTS.

Choosing a connection protocol

Each communications protocol you can use to connect your client applications to the application server provides its own unique benefits. Before choosing a protocol, Creatingmulti- tieredapplications15-7, UnderstandingMIDAStechnologyconsider how many clients you expect, how you are deploying your application, and future development plans. Using DCOM connections DCOM provides the most direct approach to communication, requiring no additional runtime applications on the server. However, because DCOM is not included with Windows 95, client machines may not have DCOM installed. DCOM provides the only approach that lets you use MTS security services. MTS security is based on assigning roles to the callers of MTS objects. When calling into MTS using DCOM, DCOM informs MTS about the client application that generated the call. MTS can then accurately determine the role of the caller. When using other protocols, however, there is a runtime executable, separate from the application server, that receives client calls. This runtime executable makes COM calls into the application server on behalf of the client. MTS can’t assign roles to separate clients because, as far as MTS can tell, all calls to the application server are made by the runtime executable. For more information about MTS security, see “Role-based security” on page 49-11. Using Socket connections TCP/IP Sockets let you create lightweight clients. For example, if you are distributing your client application over the Web as an active form, you can’t be sure that client systems support DCOM. Sockets provide a lowest common denominator that you know will be available for connecting to the application server. For more information about Sockets, see Chapter 29, “Working with sockets.” Instead of instantiating the remote data module directly from the client (as happens with DCOM), sockets use a separate application on the server (ScktSrver.exe or ScktSrvc.exe) which accepts client requests and instantiates the remote data module using COM. The connection component on the client and ScktSrvr.exe or ScktSrvc.exe on the server are responsible for marshaling IProvider calls. When using sockets, there is no protection on the server against client systems failing before they release a reference to interfaces on the application server. While this results in less message traffic than when using DCOM (which sends periodic keep- alive messages), this can result in an application server that can’t release its resources because it is unaware that the client has gone away. Using OLEnterprise OLEnterprise lets you use the Business Object Broker instead of relying on client-side brokering. The Business Object Broker provides load-balancing, fail-over, and location transparency. When using OLEnterprise, you must install OLEnterprise runtime on both client and server systems. OLEnterprise runtime handles the marshalling of Automation calls and communicates between the client and server system using remote procedure calls (RPCs). For more information, see the OLEnterprise documentation. 15-8Developer’ sGuide, Buildingamulti- tieredapplicationUsing CORBA connections CORBA lets you integrate your multi-tiered database applications into an environment that is standardized on CORBA. Your client applications and application servers can interoperate seamlessly with CORBA clients and servers built using other products. For more information about using CORBA in Delphi, see Chapter 27, “Writing CORBA applications.” By using CORBA, your application automatically gets the benefits of load-balancing, location transparency, and fail-over from the ORB runtime software. In addition, you can add hooks to take advantage of other CORBA services.

Building a multi-tiered application

The general steps for creating a multi-tiered database application are 1 Create the application server. 2 Register or install the application server. • If the application server uses DCOM, sockets, or OLEnterprise as a communication protocol, it acts as an Automation server and must be registered like any other ActiveX or COM server. For information about registering an application, see “Registering an application as an Automation server” on page 46-5. • If you are using MTS, the application server must be an Active Library rather than an .EXE. Because all COM calls must go through the MTS proxy, you do not register the application server. Instead, you install it with MTS. For information about installing libraries with MTS, see “Installing MTS objects into an MTS package” on page 49-21. • When the application server uses CORBA, registration is optional but highly recommended. If you want to allow client applications to use dynamic (late) binding to your interface, you must install the server’s interface in the Interface Repository. In addition, if you want to allow client applications to launch the application server when it is not already running, it must be registered with the OAD (Object Activation Daemon). For more information about installing a CORBA server in the Interface Repository and Registering it with the OAD, see “Registering server interfaces” on page 27-7. 3 Create a client application. The order of creation is important. You should create and run the application server before you create a client. At design time, you should connect to the application server to test your client. You can, of course, create a client without specifying the application server at design time, and only supply the server name at runtime. However, doing so prevents you from seeing if your application works as expected when you code at design time, and you will not be able to choose servers and providers using the Object Inspector. Note If you are not creating the client application on the same system as the server, you may want to register or install the application server on the client system. This makesCreatingmulti- tieredapplications15-9, Creatingtheapplicationserverthe connection component aware of the application server at design time so that you can choose server names and provider names from a drop-down list in the Object Inspector.

Creating the application server

You create an application server very much as you create most database applications. The major difference is that the application server includes a provider of some type. You can either use a TProvider or TDataSetProvider component, or the Provider property of a TDBDataSet descendant (such as TTable). To create an application server, start a new project, save it, and follow these steps: 1 Add a new remote data module to the project. From the main menu, choose File| New. Choose the Multitier page in the new items dialog, and select • Remote Data Module if you are creating a COM Automation server that clients access using DCOM, sockets, or OLEnterprise. • MTS Data Module if you are creating an Active Library that clients access using MTS. Connections can be formed using DCOM, sockets, or OLEnterprise. • CORBA Data Module if you are creating a CORBA server. For more detailed information about setting up a remote data module, see “Setting up the remote data module” on page 15-11. Note Remote data modules are more than simple data modules. The CORBA remote data module acts as a CORBA server. Other data modules are COM Automation objects. 2 Place the appropriate table, query, and stored procedure components on the data module and set them up to access the database server. 3 Place a provider component or BDE-enabled dataset on the data module for each dataset to access. You must explicitly export each provider in your remote data module so that it is registered in the type library or registered with CORBA (as appropriate). To do this, select each provider component (either a TProvider or a TBDEDataSet), right-click, and in the popup menu select Export From in Data Module. 4 If you use a provider component, set the DataSet property for the provider component to the name of the dataset to access. There are additional properties that you can set for the provider. For more detailed information about setting up a provider, see “Creating a data provider for the application server” on page 15-15. 5 Write application server code to implement events, shared business rules, shared data validation, and shared security. You may want to extend the application server’s interface to provide additional ways that the client application can call the server. (In fact, you will have to do this if you need a stateless application server.) For more information about extending the application server’s interface, see “Extending the application server’s interface” on page 15-33. 15-10Developer’ sGuide, Creatingtheapplicationserver6Save, compile, and register or install the application server. • When the application server uses DCOM, sockets, or OLEnterprise as a communication protocol, it acts as an Automation server and must be registered like any other ActiveX or COM server. For information about registering an application, see “Registering an application as an Automation server” on page 46-5. • If you are using MTS, the application server must be an Active Library rather than an .EXE. Because all COM calls must go through the MTS proxy, you do not register the application server. Instead, you install it with MTS. For information about installing libraries with MTS, see “Installing MTS objects into an MTS package” on page 49-21. • When the application server uses CORBA, registration is optional but highly recommended. If you want to allow client applications to use dynamic (late) binding to your interface, you must install the server’s interface in the Interface Repository. In addition, if you want to allow client applications to launch the application server when it is not already running, it must be registered with the OAD (Object Activation Daemon). For more information about installing a CORBA server in the Interface Repository and Registering it with the OAD, see “Registering server interfaces” on page 27-7. 7 If your server application does not use DCOM, you must install the runtime software that receives client messages, instantiates the remote data module, and marshals interface calls. For TCP/IP sockets this is a socket dispatcher application. Delphi ships with two dispatcher applications: ScktSrvr.exe, a simple, socket- based application that can run on any system, and ScktSrvc.exe, which is an NT service application. For OLEnterprise, this is the OLEnterprise runtime. For CORBA, this is the VisiBroker ORB.

Setting up the remote data module

When you set up and run an application server, it does not establish any connection with client applications. Instead, a client application uses its connection component to establish a connection to the application server and, usually, to select a provider once the connection is established. All of this happens automatically, without you having to write code to manage incoming requests or supply interfaces. Once a connection is established, the interface between the application server and the client dataset is automatic and transparent to the developer and the user. Connection is maintained by client applications. When you create the remote data module, you must provide certain information that indicates how it responds to client requests. This information varies, depending on the type of remote data module. See “The structure of the application server” on page 15-5 for information on what type of remote data module you need. Creatingmulti- tieredapplications15-11, CreatingtheapplicationserverConfiguring TRemoteDataModule To add a TRemoteDataModule component to your application, choose File|New and select Remote Data Module from the Multitier page of the new items dialog. You will see the Remote Data Module wizard. You must supply a class name for your remote data module. This is the base name of a descendant of TRemoteDataModule that your application creates. It is also the base name of the interface for that class. For example, if you specify the class name MyDataServer, the wizard creates a new unit declaring TMyDataServer, a descendant of TRemoteDataModule, which implements IMyDataServer, a descendant of IDataBroker. Note You can add your own properties and methods to your new interface. For more information, see “Extending the application server’s interface” on page 15-33. If you are creating a DLL (Active Library), you must specify the threading model in the Remote Data Module wizard. You can choose Single-threaded, Apartment-threaded, Free-threaded, or Both. • If you choose Single-threaded, COM ensures that only one client request is serviced at a time. You do not need to worry about client requests interfering with each other. • If you choose Apartment-threaded, COM ensures that any instance of your remote data module services one request at a time. When writing code in an Apartment-threaded library, you must guard against thread conflicts if you use global variables or objects not contained in the remote data module. • If you choose Free-threaded, your application can receive simultaneous client requests on several threads. You are responsible for ensuring your application is thread-safe. Because multiple clients can access your remote data module simultaneously, you must guard your instance data (properties, contained objects, and so on) as well as global variables. This model is not recommended because there is no built-in thread support for using the IProvider interface. • If you choose Both, your library works the same as when you choose Free-threaded, with one exception: all callbacks (calls to client interfaces) are serialized for you. If you are creating an EXE, you must specify what type of instancing to use. You can choose Single instance or Multiple instance (Internal instancing applies only if the client code is part of the same process space.) • If you choose Single instance, each client connection launches its own instance of the executable. That process instantiates a single instance of the remote data module, which is dedicated to the client connection. • If you choose Multiple instance, a single instance of the application (process) instantiates all remote data modules created for clients. Each remote data module is dedicated to a single client connection, but they all share the same process space. 15-12Developer’ sGuide, CreatingtheapplicationserverConfiguring TMTSDataModule To add a TMTSDataModule component to your application, choose File|New and select MTS Data Module from the Multitier page of the new items dialog. You will see the MTS Data Module wizard. You must supply a class name for your remote data module. This is the base name of a descendant of TMTSDataModule that your application creates. It is also the base name of the interface for that class. For example, if you specify the class name MyDataServer, the wizard creates a new unit declaring TMyDataServer, a descendant of TMTSDataModule, which implements IMyDataServer, a descendant of IDataBroker. Note You can add your own properties and methods to your new interface. In fact, you must do this if you want to take advantage of MTS transactions or just-in-time activation. For more information, see “Extending the application server’s interface” on page 15-33. MTS applications are always DLLs (Active Libraries). You must specify the threading model in the MTS Data Module wizard. You can choose Single, Apartment, Free, or Both. • If you choose Single, MTS ensures that only one client request is serviced at a time. You do not need to worry about client requests interfering with each other. • If you choose Apartment or Free, you get the same thing: MTS ensures that any instance of your remote data module services one request at a time, but that calls do not always use the same thread. You can’t use thread variables, because there is no guarantee that subsequent calls to the remote data module instance will use the same thread.You must guard against thread conflicts if you use global variables or objects not contained in the remote data module. Instead of using global variables, you can use the shared property manager. For more information on the shared property manager, see “Shared property manager” on page 49-12. • If you choose Both, MTS calls into the remote data module’s interface in the same way as when you choose Apartment or Free. However, any callbacks you make to client applications are serialized, so that you don’t need to worry about them interfering with each other. Note The Apartment and Free models under MTS are different than the corresponding models under DCOM. You must also specify the MTS transaction attributes of your remote data module. You can choose from the following options: • Requires a transaction. When you select this option, every time a client uses your remote data module’s interface, that call is executed in the context of an MTS transaction. If the caller supplies a transaction, a new transaction need not be created. • Requires a new transaction. When you select this option, every time a client uses your remote data module’s interface, a new transaction is automatically created for that call. Creatingmulti- tieredapplications15-13, Creatingtheapplicationserver• Supports transactions. When you select this option, your remote data module can be used in the context of an MTS transaction, but the caller must supply the transaction when it invokes the interface. • Does not support transactions. When you select this option, your remote data module can’t be used in the context of MTS transactions. Configuring TCorbaDataModule To add a TCorbaDataModule component to your application, choose File|New and select CORBA Data Module from the Multitier page of the new items dialog. You will see the CORBA Data Module wizard. You must supply a class name for your remote data module. This is the base name of a descendant of TCorbaDataModule that your application creates. It is also the base name of the interface for that class. For example, if you specify the class name MyDataServer, the wizard creates a new unit declaring TMyDataServer, a descendant of TCorbaDataModule, which implements IMyDataServer, a descendant of IDataBroker. Note You can add your own properties and methods to your new interface. In fact, you must do this if you want to use the single instance model described below. For more information on adding to your data module’s interface, see “Extending the application server’s interface” on page 15-33. The CORBA data module wizard lets you specify how you want your server application to create instances of the remote data module. You can choose either shared or instance-per-client. • When you choose shared, your application creates a single instance of the remote data module that handles all client requests. This is the model used in traditional CORBA development. Because the single remote data module is shared by all clients, it must be stateless. (This means you can’t use the IProvider interface.) For more information about writing stateless remote data modules, see “Supporting stateless remote data modules” on page 15-37. • When you choose instance-per-client, a new remote data module instance is created for each client connection. As long as the connection is open, the remote data module instance persists. When client connections close, the server application frees their remote data module instances. This model lets clients communicate using IProvider interfaces, because state information can be preserved. To guard against client systems failing without releasing the remote data module, the application sends periodic messages to check that the client is still running. You can avoid this message overhead by using a single shared instance. Note Unlike instancing for COM servers, where the model determines the number of instances of the process that run, with CORBA, instancing determines the number of instances created of your object. They are all created within a single instance of the server executable. In addition to the instancing model, you must specify the threading model in the CORBA Data Module wizard. You can choose Single- or Multi-threaded. 15-14Developer’ sGuide, Creatingtheapplicationserver• If you choose Single-threaded, each remote data module instance is guaranteed to receive only one client request at a time. You can safely access the objects contained in your remote data module. However, you must guard against thread conflicts when you use global variables or objects not contained in the remote data module. • If you choose Multi-threaded, each client connection has its own dedicated thread. However, your application may be called by multiple clients simultaneously, each on a separate thread. You must guard against simultaneous access of instance data as well as global memory. Writing Multi-threaded servers is tricky when you are using a shared remote data module instance, because you must protect all use of objects contained in your remote data module.

Creating a data provider for the application server

Each remote data module on an application server usually contains one or more provider components. A provider component responds to the IProvider interface, packaging data into data packets that it sends to clients and applying updates received from the client. Providers work in conjunction with resolver components that handle the details of resolving data to the database (or to a dataset). Note Instead of placing provider components in a remote data module and using them to establish IProvider interfaces that can be used with client datasets, you may choose to use the Provider property of TStoredProc, TQuery, or TTable, bypassing an explicit use of TProvider altogether. When you use a dataset component’s Provider property directly, Delphi creates and manages a provider for you behind the scenes. When you use a provider component, you must associate it with a dataset component on the application server. To do this, set the DataSet property of the provider to the name of the dataset to use. If you are providing from and resolving to a dataset, you can specify any descendant of TDataSet. If you are resolving directly to the database (the default), you must specify a BDE-enabled dataset. At design time, select from available datasets in the DataSet property drop-down list in the Object Inspector. Unless you are creating a stateless remote data module, most of the work of a provider component happens automatically. You need not write any code on the provider to create a fully functioning application server. Using provider components gives your application more direct control over what information is packaged for clients and how your server application responds to client requests. The following topics describe how to use a provider component to control the interaction with client applications. Controlling what information is included in data packets There are a number of ways to control what information is included in data packets that are sent to and from the client. These include • Specifying what fields appear in data packets • Setting Options that influence the data packets • Adding custom information to data packetsCreatingmulti- tieredapplications15-15, CreatingtheapplicationserverSpecifying what fields appear in data packets To control what fields are included in data packets, create persistent fields on the dataset that the provider uses to build the packets. The provider then includes only these fields. Fields whose values are generated dynamically on the server (such as calculated fields or lookup fields) can be included, but appear to client datasets on the receiving end as static read-only fields. For information about creating persistent fields, see “Creating persistent fields” on page 19-5. If the client dataset will be editing the data and applying updates to the application server, you must include enough fields so that there are no duplicate records in the data packet. Otherwise, when the updates are applied, it is impossible to determine which record to update. If you do not want the client dataset to be able to see or use extra fields provided only to ensure uniqueness, set the ProviderFlags property for those fields to include pfHidden. Note Including enough fields to avoid duplicate records is also a consideration when using queries on the application server. The query should include enough fields so that records are unique, even if your application does not use all the fields. Setting options that influence the data packets The Options property of the provider component lets you specify when BLOBs or nested detail tables are sent, whether field display properties are included, and so on. The following table lists the possible values that can be included in Options. Table 15.4 Provider options Value Meaning poFetchBlobsOnDemand BLOB field values are not included in the data packet. Instead, client applications must request these values on an as-needed basis. If the client dataset’s FetchOnDemand property is True, the client requests these values automatically. Otherwise, the client application uses the client dataset’s FetchBlobs method to retrieve BLOB data. poFetchDetailsOnDemand When the provider represents the master of a master/detail relationship, nested detail values are not included in the data packet. Instead, client applications request these on an as-needed basis. If the client dataset’s FetchOnDemand property is True, the client requests these values automatically. Otherwise, the client application uses the client dataset’s FetchDetails method to retrieve nested details. poIncFieldProps The data packet includes the following field properties (where applicable): Alignment, DisplayLabel, DisplayWidth, Visible, DisplayFormat, EditFormat, MaxValue, MinValue, Currency, EditMask, DisplayValues. poCascadeDeletes When the provider represents the master of a master/detail relationship, detail records are deleted by the server automatically when master records are deleted. To use this option, the database server must be set up to perform cascaded deletes as part of its referential integrity. 15-16Developer’ sGuide, CreatingtheapplicationserverTable 15.4 Provider options (continued) Value Meaning poCascadeUpdates When the provider represents the master of a master/detail relationship, key values on detail tables are updated automatically when the corresponding values are changed in master records. To use this option, the database server must be set up to perform cascaded updates as part of its referential integrity. poReadOnly The client dataset can’t apply updates to the provider. Adding custom information to data packets Providers can send application-defined information to the data packets using the OnGetDataSetProperties event. This information is encoded as an OleVariant, and stored under a name you specify. Client datasets in the client application can then retrieve the information using their GetOptionalParam method. You can also specify that the information be included in delta packets that the client dataset sends when updating records. In this case, the client application may never be aware of the information, but the server can send a round-trip message to itself. When adding custom information in the OnGetDataSetProperties event, each individual attribute (sometimes called an “optional parameter”) is specified using a variant array that contains three elements: the name (a string), the value (a Variant), and a boolean flag indicating whether the information should be included in delta packets when the client applies updates. Multiple attributes can be added by creating a variant array of variant arrays. For example, the following OnGetDataSetProperties event handler sends two values, the time the data was provided and the total number of records in the source dataset. Only information about the time the data was provided is returned when clients apply updates: procedure TMyDataModule1.Provider1GetDataSetProperties(Sender: TObject; DataSet: TDataSet; out Properties: OleVariant); begin Properties := VarArrayCreate([0,1], varVariant); Properties[0] := VarArrayOf(['TimeProvided', Now, True]); Properties[1] := VarArrayOf(['TableSize', DataSet.RecordCount, False]); end; When the client applies updates, the time the original records were provided can be read in the provider’s OnUpdateData event: procedure TMyDataModule1.Provider1UpdateData(Sender: TObject; DataSet: TClientDataSet); var WhenProvided: TDateTime; begin WhenProvided := DataSet.GetOptionalParam('TimeProvided'); ... end;

Responding to client data requests

In most multi-tiered applications, client requests for data are handled automatically. A client dataset requests a data packet by calling GetRecords (through the IProvider interface or through a custom interface). The provider responds automatically byCreatingmulti- tieredapplications15-17, Creatingtheapplicationserverfetching data from the associated dataset, creating a data packet, and sending the packet to the client. The provider has the option of editing data after it has been assembled into a data packet but before the packet is sent to the client. For example, the provider might want to encrypt sensitive data before it is sent on to the client, or to remove records from the packet based on some criterion (such as the user’s role in an MTS application). To edit the data packet before sending it on to the client, write an OnGetData event handler. The data packet is provided as a parameter in the form of a client dataset. Using the methods of this client dataset, data can be edited before it is sent to the client. Responding to client update requests A provider applies updates to database records based on a Delta data packet received from a client application. The client requests updates by calling the ApplyUpdates method (through the IProvider interface or through a custom interface). When a provider receives an update request, it generates an OnUpdateData event, where you can edit the Delta packet before it is written to the dataset or influence how updates are applied. After the OnUpdateData event, the provider uses its associated resolver component to write the changes to the database. The resolver component performs the update on a record-by-record basis. Before the resolver applies each record, it generates a BeforeUpdateRecord event on the provider, which you can use to screen updates before they are applied. If an error occurs when updating a record, the resolver calls the provider’s OnUpdateError event handler to resolve the error. Usually errors occur because the change invalidates a server constraint or the database record was changed by a different application subsequent to its retrieval by this client application, but prior to the client’s request to apply updates. Update errors can be processed by either the application server or the client. Application servers should handle all update errors that do not require user interaction to resolve. When the application server can’t resolve an error condition, it temporarily stores a copy of the offending record. When record processing is complete, the application server returns a count of the errors it encountered to the client dataset, and copies the unresolved records into a results data packet that it passes back to the client for further reconciliation. Note Stored procedure components can’t be updated using this mechanism. To apply updates to a stored procedure, you must do one of the following: • Add code to explicitly apply updates in the BeforeUpdateRecord event on the provider. Typically, this involves using other TStoredProc components to insert, delete, or modify records. • Supply a table name that the resolver can use when generating SQL to apply updates. This approach only works when the stored procedure represents the records from a single table. Specify the table name as the ‘Table_Name’ property of the data packet, indicating that it should be included in delta packets. For more 15-18Developer’ sGuide, Creatingtheapplicationserverinformation on adding properties to the data packet, see “Adding custom information to data packets” on page 15-17. The event handlers for all provider events are passed the set of updates as a client dataset. If your event handler is only dealing with certain types of updates, you can filter the dataset on the update status of records so that your event handler does not need to sort through records it won’t be using. To do this, set the StatusFilter property of the client dataset.

Editing delta packets before updating the database

Before the provider applies updates to the database, it generates an OnUpdateData event. The OnUpdateData event handler receives a copy of the Delta packet as a parameter. This is a client dataset. In the OnUpdateData event handler, you can use any of the properties and methods of the client dataset to edit the Delta packet before it is written to the dataset. One particularly useful property is the UpdateStatus property. UpdateStatus indicates what type of modification the current record in the delta packet represents. It can have any of the values in Table 15.5. Table 15.5 UpdateStatus values Value Description usUnmodified Record contents have not been changed usModified Record contents have been changed usInserted Record has been inserted usDeleted Record has been deleted For example, the following OnUpdateData event handler inserts the current date into every new record that is inserted into the database: procedure TMyDataModule1.Provider1UpdateData(Sender: TObject; DataSet: TClientDataSet); begin with DataSet do begin First; while not Eof do begin if UpdateStatus = usInserted then begin Edit; FieldByName('DateCreated').AsDateTime := Date; Post; end; Next; end; end;

Influencing how updates are applied

The OnUpdateData event also gives your provider a chance to indicate how records in the delta packet are applied to the database. Creatingmulti- tieredapplications15-19, CreatingtheapplicationserverBy default, changes in the delta packet are written to the database using automatically generated SQL UPDATE, INSERT, or DELETE statements such as UPDATE EMPLOYEES set EMPNO = 748, NAME = 'Smith', TITLE = 'Programmer 1', DEPT = 52

WHERE

EMPNO = 748 and NAME = 'Smith' and TITLE = 'Programmer 1' and DEPT = 47 Unless you specify otherwise, all fields in the delta packet records are included in the UPDATE clause and in the WHERE clause. However, you may want to exclude some of these fields. One way to do this is to set the UpdateMode property of the provider. UpdateMode can be assigned any of the following values: Table 15.6 UpdateMode values Value Meaning upWhereAll All fields are used to locate fields (the WHERE clause). upWhereChanged Only key fields and fields that are changed are used to locate records. upWhereOnly Only key fields are used to locate records. You might, however, want even more control. For example, with the previous statement, you might want to prevent the EMPNO field from being modified by leaving it out of the UPDATE clause and leave the TITLE and DEPT fields out of the WHERE clause to avoid update conflicts when other applications have modified the data. To specify the clauses where a specific field appears, use the ProviderFlags property. ProviderFlags is a set that can include any of the values in Table 15.7 Table 15.7 ProviderFlags values Value Description pfInWhere The field does not appear in the WHERE clause of generated INSERT, DELETE, and UPDATE statements. pfInUpdate The field does not appear in the UPDATE clause of generated UPDATE statements. pfInKey The field is used in the WHERE clause of a generated SELECT statement that executes when update failures occur. This SELECT statement tries to locate the current value of modified or deleted records, or a record causing key violations when insertions fail. pfHidden The field is included in records to ensure uniqueness, but can’t be seen or used on the client side. Thus, the following OnUpdateData event handler excludes the EMPNO field from the UPDATE clause and the TITLE and DEPT fields from the WHERE clause: procedure TMyDataModule1.Provider1UpdateData(Sender: TObject; DataSet: TClientDataSet); begin with DataSet do begin FieldByName('EMPNO').UpdateFlags := [ufInUpdate]; FieldByName('TITLE').UpdateFlags := [ufInWhere]; FieldByName('DEPT').UpdateFlags := [ufInWhere]; end; end; 15-20Developer’ sGuide, CreatingtheapplicationserverNote You can use the UpdateFlags property to influence how updates are applied even if you are updating to a dataset and not using dynamically generated SQL. These flags still determine which fields are used to locate records and which fields get updated. Screening individual updates Immediately before each update is applied, the provider receives a BeforeUpdateRecord event. You can use this event to edit records before they are applied, similar to the way you can use the OnUpdateData event to edit entire delta packets. In addition, you can use this event to apply updates yourself or to screen and reject updates. The BeforeUpdateRecord event handler lets you signal to the resolver that an update has been handled already and should not be applied. The resolver then skips that record, but does not count it as an update error. For example, this event provides a mechanism for applying updates to a stored procedure (which can’t be updated automatically), allowing the provider to skip any automatic processing once the record is updated from within the event handler. Resolving update errors on the provider When an error condition arises as the application server tries to post a record in the delta packet, an OnUpdateError event occurs. If the application server can’t resolve an update error, it temporarily stores a copy of the offending record. When record processing is complete, the application server returns a count of the errors it encountered to the client dataset, and copies the unresolved records into a results data packet that it passes back to the client for further reconciliation. This mechanism lets you handle any update errors you can resolve mechanically on the application server, while still allowing user interaction on the client application to correct error conditions. The OnUpdateError handler gets a copy of the record that could not be changed, an error code from the database, and an indication of whether the resolver was trying to insert, delete, or update the record. The problem record is passed back in a client dataset. You should never use the data navigation methods on this dataset. However, for each field in the dataset, you can use the NewValue, OldValue, and CurValue properties to determine the cause of the problem and make any modifications to resolve the update error. If the OnUpdateError event handler can correct the problem, it sets the Response parameter so that the corrected record is applied. Responding to client-generated events Provider components implement a general-purpose event that lets you create your own calls from clients directly to the provider. This is the OnDataRequest event. OnDataRequest is not part of the normal functioning of the provider. It is simply a hook to allow your clients to use the IProvider interface to communicate directly with providers on the application server. The event handler takes an OleVariant as an input parameter and returns an OleVariant. By using OleVariants, the interface is sufficiently general to accommodate almost any information you want to pass to or from the provider. Creatingmulti- tieredapplications15-21, CreatingtheapplicationserverTo generate an OnDataRequest event, the client application calls the DataRequest method of the IProvider interface directly. In stateless remote data modules, a custom interface on the remote data module can call the DataRequest method of the provider object. Handling server constraints Most relational database management systems implement constraints on their tables to enforce data integrity. A constraint is a rule that governs data values in tables and columns, or that governs data relationships across columns in different tables. For example, most SQL-92 compliant relational databases support the following constraints: • NOT NULL, to guarantee that a value supplied to a column has a value. • NOT NULL UNIQUE, to guarantee that column value has a value and does not duplicate any other value already in that column for another record. • CHECK, to guarantee that a value supplied to a column falls within a certain range, or is one of a limited number of possible values. • CONSTRAINT, a table-wide check constraint that applies to multiple columns. • PRIMARY KEY, to designate one or more columns as the table’s primary key for indexing purposes. • FOREIGN KEY, to designate one or more columns in a table that reference another table. Note This list is not exclusive. Your database server may support some or all of these constraints in part or in whole, and may support additional constraints. For more information about supported constraints, see your server documentation. Database server constraints obviously duplicate many kinds of data checks that traditional desktop database applications have managed in the past. You can take advantage of server constraints in your multi-tiered database applications without having to duplicate the constraints in application server or client application code. The provider’s Constraints property enables you to replicate and apply server constraints to data passed to and received from client applications. When Constraints is True (the default), your server’s constraints are replicated to clients and affect client attempts to update data. Note There may be times when you do not want to apply server constraints to data sent to a client application. For example, a client application that receives data in packets and permits local updating of records prior to fetching more records may need to disable some server constraints that might be triggered because of the temporarily incomplete set of data upon which updates are performed. To prevent constraint replication from the application server to a client dataset, set Constraints to False. Note, too, that client datasets can disable and enable constraints using the DisableConstraints and EnableConstraints methods. For more information about enabling and disabling constraints from the client dataset, see “Handling constraints” on page 15-29. 15-22Developer’ sGuide, Creatingtheclientapplication

Creating the client application

In most regards, creating a multi-tiered client application is similar to creating a traditional two-tiered client. The major differences are that a multi-tiered client uses • A connection component to establish a conduit to the application server. • One or more TClientDataSet components to link to a data provider on the application server. Data-aware controls on the client are connected through data source components to these client datasets instead of TTable, TQuery, and TStoredProc components. To create a multi-tiered client application, start a new project and follow these steps: 1 Add a new data module to the project. 2 Place a connection component on the data module. The type of connection component you add depends on the communication protocol you want to use. See “The structure of the client application” on page 15-4 for details. 3 Set properties on your connection component to specify the application server with which it should establish a connection. To learn more about setting up the connection component, see “Connecting to the application server” on page 15-24. 4 Set the other connection component properties as needed for your application. For example, you might set the ObjectBroker property to allow the connection component to choose dynamically from several servers. 5 Place as many TClientDataSet components as needed on the data module, and set the RemoteServer property for each component to the name of the connection component you placed in Step 2. For a full introduction to client datasets, see Chapter 23, “Creating and using a client dataset.” 6 Set the ProviderName property for each TClientDataSet component. If your connection component is connected to the application server at design time, you can choose available application server providers from the ProviderName property’s drop-down list. (If you are using a stateless remote data module, you will want to omit this step.) 7 Create the client application in much the same way you would create any other database application. However, you may want to add code or set properties for • Managing server connections • Handling constraints • Updating records • Refreshing records • Getting parameters from the application serverCreatingmulti- tieredapplications15-23, Creatingtheclientapplication

Connecting to the application server

To establish and maintain a connection to an application server, a client application uses one or more connection components. You can find these components on the MIDAS page of the Component palette. Use a connection component to • Identify the protocol for communicating with the application server. Each type of connection component represents a different communication protocol. See “Choosing a connection protocol” on page 15-7 for details on the benefits and limitations of the available protocols. • Indicate how to locate the server machine. The details of identifying the server machine vary depending on the protocol. • Identify the application server on the server machine. If you are not using CORBA, identify the server using the ServerName or ServerGUID property. ServerName identifies the name of the class you specify when creating the remote data module on the application server. See “Setting up the remote data module” on page 15-11 for details on how this value is specified on the server. If the server is registered or installed on the client machine, or if the connection component is connected to the server machine, you can set the ServerName property at design time by choosing from a drop-down list in the Object Inspector. ServerGUID specifies the GUID of the remote data module’s interface. You can look up this value using the type library editor. If you are using CORBA, identify the server using the RepositoryID property. RepositoryID specifies the Repository ID of the application server’s factory interface, which appears as the third argument in the call to TCorbaVCLComponentFactory.Create that is automatically added to the initialization section of the CORBA server’s implementation unit. You can also set this property to the base name of the CORBA data module’s interface (the same string as the ServerName property for other connection components), and it is automatically converted into the appropriate Repository ID for you. • Manage server connections. Connection components can be used to create or drop connections and to call application server interfaces. Usually the application server is on a different machine from the client application, but even if the server resides on the same machine as the client application (for example, during the building and testing of the entire multi-tier application), you can still use the connection component to identify the application server by name, specify a server machine, and use the application server interface. Specifying a connection using DCOM When using DCOM to communicate with the application server, client applications include a TDCOMConnection component for connecting to the application server. TDCOMConnection uses the ComputerName property to identify the machine on which the server resides. 15-24Developer’ sGuide, CreatingtheclientapplicationWhen ComputerName is blank, the DCOM connection component assumes that the application server resides on the client machine or that the application server has a system registry entry. If you do not provide a system registry entry for the application server on the client when using DCOM, and the server resides on a different machine from the client, you must supply ComputerName. Note Even when there is a system registry entry for the application server, you can specify ComputerName to override this entry. This can be especially useful during development, testing, and debugging. If you supply the name of a host computer or server that cannot be found, the DCOM connection component raises an exception when you try to open the connection. If you have multiple servers that your client application can choose from, you can use the ObjectBroker property instead of specifying a value for ComputerName. For more information, see “Brokering connections” on page 15-26. Specifying a connection using sockets You can establish a connection to the application server using sockets on any machine that has a TCP/IP address. This method has the advantage of being applicable to more machines, but does not provide for using any security protocols. When using sockets, include a TSocketConnection component for connecting to the application server. TSocketConnection identifies the server machine using the IP Address or host name of the server system, and the port number of the socket dispatcher program (Scktsrver.exe or Scktsrvc.exe) that is running on the server machine. For more information about IP addresses and port values, see “Describing sockets” on page 29-3. Three properties of TSocketConnection specify this information: • Address specifies the IP Address of the server. • Host specifies the host name of the server. • Port specifies the port number of the socket dispatcher program on the application server. Address and Host are mutually exclusive. Setting one unsets the value of the other. For information on which one to use, see “Describing the host” on page 29-4. If you have multiple servers that your client application can choose from, you can use the ObjectBroker property instead of specifying a value for Address or Host. For more information, see “Brokering connections” on page 15-26. By default, the value of Port is 211, which is the default port number of the socket dispatcher programs supplied with Delphi. If the socket dispatcher has been configured to use a different port, set the Port property to match that value. Note You can configure the port of the socket dispatcher by providing a command line argument to ScktSrvr.exe or by right-clicking the tray icon for ScktSrvc.exe. Creatingmulti- tieredapplications15-25, CreatingtheclientapplicationSpecifying a connection using OLEnterprise When using OLEnterprise to communicate with the application server, client applications should include a TOLEnterpriseConnection component for connecting to the application server. When using OLEnterprise, you can either connect directly to the server machine, or you can use the Business Object Broker. • To use OLEnterprise without going through a Business Object Broker, set the ComputerName property to the name of the server machine, just as you would use the ComputerName property for a DCOM connection. • To use the load-balancing and fail-over services of the Business Object Broker, set the BrokerName property to the name of the Business Object Broker. ComputerName and BrokerName are mutually exclusive. Setting the value of one unsets the value of the other. For more information about using OLEnterprise, see the OLEnterprise documentation. Specifying a connection using CORBA Only the RepositoryID property is necessary in order to specify a CORBA connection. This is because a Smart Agent on the local network automatically locates an available server for your CORBA client. However, you can limit the possible servers to which your client application connects by the other properties of the CORBA connection component. If you want to specify a particular server machine, rather than letting the CORBA Smart Agent locate any available server, use the Host property. If there is more than one object that implements your server interface, you can specify which object you want to use by setting the ObjectName property. The TCorbaConnection component obtains an interface to the CORBA data module on the application server in one of two ways: • If you are using early (static) binding, you must add the _TLB.pas file (generated by the type library editor) to your client application. Early binding is highly recommended, both for compile-time type checking and because it is much faster than late (dynamic) binding. • If you are using late (dynamic) binding, the interface must be registered with the Interface Repository. For more information about registering an interface with the Interface Repository, see “Registering interfaces with the Interface Repository” on page 27-8. For more information on early vs. late binding, see “Calling server interfaces” on page 15-28. Brokering connections If you have multiple servers that your client application can choose from, you can use an Object Broker to locate an available server system. The object broker maintains a list of servers from which the connection component can choose. When the connection component needs to connect to an application server, it asks the Object 15-26Developer’ sGuide, CreatingtheclientapplicationBroker for a computer name (or IP address or host name). The broker supplies a computer name, and the connection component forms a connection. If the supplied name does not work (for example, if the server is down), the broker supplies another name, and so on, until a connection is formed. Once the connection component has formed a connection with a name supplied by the broker, it saves that name as the value of the appropriate property (ComputerName, Address, or Host). If the connection component closes the connection later, and then needs to reopen the connection, it tries using this property value, and only requests a new name from the broker if the connection fails. Use an Object Broker by specifying the ObjectBroker property of your connection component. When the ObjectBroker property is set, the connection component does not save the value of ComputerName, Address or Host. Note Do not use the ObjectBroker property with OLEnterprise connections or CORBA connections. Both of these protocols have their own brokering services.

Managing server connections

The main purpose of connection components is to locate and connect to the application server. Because they manage server connections, you use connection components to call the methods of the application server’s interface. Connecting to the server To locate and connect to the application server, you must first set the properties of the connection component to identify the application server. This process is described in “Connecting to the application server” on page 15-24. In addition, before opening the connection, any client datasets that use the connection component to obtain an IProvider interface should indicate this by setting their RemoteServer property to specify the connection component. Client datasets then set their ProviderName property before opening the connection so that when the connection is opened, they can obtain the appropriate IProvider interface. The connection is opened automatically when client datasets try to access the IProvider interface. For example, setting the Active property of the client dataset to True opens the connection. If you are not linking client datasets to the connection component so that they can use the IProvider interface, you can open the connection by setting the Connected property of the connection component to True. Before a connection component establishes a connection to an application server, it generates a BeforeConnect event. You can perform any special actions prior to connecting in a BeforeConnect handler that you code. After establishing a connection, the connection component generates an AfterConnect event for any special actions. Creatingmulti- tieredapplications15-27, CreatingtheclientapplicationDropping or changing a server connection A connection component drops a connection to the application server when you • Set the Connected property to False. • Free the connection component. A connection object is automatically freed when a user closes the client application. • Change any of the properties that identify the application server (ServerName, ServerGUID, ComputerName, and so on). Changing these properties allows you to switch among available application servers at runtime. The connection component drops the current connection and establishes a new one. Note Instead of using a single connection component to switch among available application servers, a client application can instead have more than one connection component, each of which is connected to different application servers. Before a connection component drops a connection, it automatically calls its BeforeDisconnect event handler, if one is provided. If you want to perform any special actions prior to disconnecting, you must provide them in an BeforeDisconnect handler. Similarly, after dropping the connection, the AfterDisconnect event handler is called. If you want to perform any special actions after disconnecting, you must provide them there.

Calling server interfaces

Most applications do not need to call the IProvider interface directly, because the appropriate calls are made automatically when you use the properties and methods of the client dataset. The one notable exception is when you want to use the DataRequest method to let clients make custom calls directly to a provider on the application server. See “Responding to client-generated events” on page 15-21 for more information on using DataRequest. When you need to call a provider’s interface, you can use the Provider property of a client dataset that represents the data from the provider. While client applications do not need to call the IDataBroker interface of the application server’s remote data module, you may have added your own extensions to the remote data module’s interface. When you extend the application server’s interface, you need a way to call those extensions using the connection created by your connection component. You can do this using the AppServer property of the connection component. For more information about extending the application server’s interface, see “Extending the application server’s interface” on page 15-33. AppServer is a Variant that represents the application server’s interface. You can call an interface method using AppServer by writing a statement such as MyConnection.AppServer.SpecialMethod(x,y); However, this technique provides late (dynamic) binding of the interface call. That is, the SpecialMethod procedure call is not bound until runtime when the call is executed. Late binding is very flexible, but by using it you lose many benefits such as code insight and type checking. In addition, late binding is slower than early binding, 15-28Developer’ sGuide, Creatingtheclientapplicationbecause the compiler generates additional calls to the server to set up interface calls before they are invoked. When you are using DCOM or CORBA as a communications protocol, you can use early binding of AppServer calls. Use the as operator to cast the AppServer variable to the IDataBroker descendant you created when you created the remote data module. For example: with MyConnection.AppServer as IMyAppServer do SpecialMethod(x,y); provides early binding for a DCOM connection while with IUnknown(MyConnection.AppServer) as IMyAppServer do SpecialMethod(x,y); provides early binding for a CORBA connection. To use early binding under DCOM, the server’s type library must be registered on the client machine. You can use TRegsvr.exe, which ships with Delphi to register the type library. Note See the TRegSvr demo (which provides the source for TRegsvr.exe) for an example of how to register the type library programmatically. To use early binding with CORBA, you must add the _TLB unit that is generated by the type library editor to your project. To do this, add this unit to the uses clause of your unit. When you are using TCP/IP or OLEnterprise, you can’t use true early binding, but because the remote data module uses a dual interface, you can use the application server’s dispinterface to improve performance over simple late-binding. The dispinterface has the same name as the remote data module’s interface, with the string ‘Disp’ appended. You can assign the AppServer property to a variable of this type to obtain the dispinterface. Thus: var TempInterface: IMyAppServerDisp; begin TempInterface := MyConnection.AppServer; ... TempInterface.SpecialMethod(x,y); ... end; Note To use the dispinterface, you must add the _TLB unit that is generated when you save the type library to the uses clause of your client module.

Handling constraints

A database server’s constraints and default expressions can be imported into the DataDictionary using the SQL Explorer. Constraints and default expressions in the Data Dictionary are automatically made available to BDE-enabled datasets in the application server. By default, server constraints and default expressions are passed to client datasets by the application server, where they can be imposed on user dataCreatingmulti- tieredapplications15-29, Creatingtheclientapplicationediting. When constraints are in effect, user edits to data in a client application that would violate server constraints are enforced on the client side, and are never passed to the application server for eventual rejection by the database server. This means that fewer updates generate error conditions during the updating process. While importing of server constraints and expressions is an extremely valuable feature that enables a developer to preserve data integrity across platforms and applications, there may be times when an application needs to disable constraints on a temporary basis. For example, if a server constraint is based on the current maximum value in a field, but the client dataset fetches multiple packets of records, the current maximum value in a field on the client may differ from the maximum value on the database server, and constraints may be invoked differently. In another case, if a client application applies a filter to records when constraints are enabled, the filter may interfere in unintended ways with constraint conditions. In each of these cases, an application may disable constraint-checking. To disable constraints temporarily, call a client dataset’s DisableConstraints method. Each time DisableConstraints is called, a reference count is incremented. While the reference count is greater than zero, constraints are not enforced on the client dataset. To reenable constraints for the client dataset, call the dataset’s EnableConstraints method. Each call to EnableConstraints decrements the reference count. When the reference count is zero, constraints are enabled again. Tip Always call DisableConstraints and EnableConstraints in paired blocks to ensure that constraints are enabled when you intend them to be.

Updating records

When a client application is connected to an application server, client datasets work with a local copy of data passed to them by the application server. The user sees and edits these copies in the client application’s data-aware controls. If server constraints are enabled on the client dataset, a user’s edits are constrained accordingly. User changes are temporarily stored by the client dataset in an internally maintained change log. The contents of the change log are stored as a data packet in the Delta property. To make the changes in Delta permanent, the client dataset must apply them to the database. When a client applies updates to the server using the IProvider interface, the following steps occur: 1 The client application calls the ApplyUpdates method of a client dataset object. This method passes the contents of the client dataset’s Delta property to the application server through the IProvider interface currently associated with the client dataset. Delta is a data packet that contains a client dataset’s updated, inserted, and deleted records. 2 The application server’s provider component applies the updates to the database, caching any problem records that it can’t resolve at the server level. See “Responding to client update requests” on page 15-18 for details on how the server applies updates. 15-30Developer’ sGuide, Creatingtheclientapplication3The application server’s provider component returns all unresolved records to the client inaaResult data packet. The Result data packet contains all records that were not updated. It also contains error information, such as error messages and error codes. 4 The client application attempts to reconcile update errors returned in the Result data packet on a record-by-record basis. Note If you are using MTS transactions or just-in-time, you can’t use the IProvider interface to apply updates. Instead, you must direct this process using the application server’s interface. For more information on how to do this, see “Supporting stateless remote data modules” on page 15-37. Applying updates Changes made to the client dataset’s local copy of data are not sent to the application server until the client application calls the ApplyUpdates method for the dataset. ApplyUpdates takes a single parameter, MaxErrors, which indicates the maximum number of errors that the application server should tolerate before aborting the update process. ApplyUpdates returns the number of actual errors encountered, which is always less than or equal to MaxErrors plus one. This value is set to indicate the number of records it could not write to the database. The application server also returns those records to the client dataset in the dataset. The client dataset is responsible for reconciling records that generate errors. ApplyUpdates actually calls the client dataset’s own Reconcile method to send updates to the application server, and Reconcile, in turn, triggers the client dataset’s OnReconcileError event handler, if it exists, once for each record that generated an error on the server. Reconciling update errors The provider on the application server returns error records and error information to the client dataset in a result data packet. If the application server returns an error count greater than zero, then for each record in the result data packet, the client dataset’s OnReconcileError event occurs. You should always code the OnReconcileError event handler, even if only to discard the records returned by the application server. The OnReconcileError event handler is passed four parameters: • DataSet: the client dataset to which updates are applied. You can use client dataset methods to obtain information about problem records and to make changes to the record in order to correct any problems. In particular, you will want to use the CurValue, OldValue, and NewValue properties of the fields in the current record to determine the cause of the update problem. However, you must not call any client dataset methods that change the current record in an OnReconcileError event handler. • E: An EReconcileError object that represents the problem that occurred. You can use this exception to extract an error message or to determine the cause of the update error. Creatingmulti- tieredapplications15-31, Creatingtheclientapplication• UpdateKind: the type of update that generated the error. UpdateKind can be ukModify (the problem occurred updating an existing record that was modified), ukInsert (the problem occurred inserting a new record), or ukDelete (the problem occurred deleting an existing record). • Action: a var parameter that lets you indicate what action to take when the OnReconcileError handler exits. On entry into the handler, Action is set to the action taken by the resolution process on the server. In your event handler, you set this parameter to • Skip this record, leaving it in the change log. (raSkip) • Stop the entire reconcile operation. (raAbort) • Merge the modification that failed into the corresponding record from the server. (raMerge) This only works if the server did not change any of the fields modified by the user. • Replace the current update in the change log with the value of the record in the event handler (which has presumably been corrected). (raCorrect) • Back out the changes for this record on the client dataset, reverting to the originally provided values. (raCancel) • Update the current record value to match the record on the server. (raRefresh) The following code shows an OnReconcileError event handler that uses the reconcile error dialog from the RecError unit which ships in the object repository directory. (To use this dialog, add RecError to your uses clause.) procedure TForm1.ClientDataSetReconcileError(DataSet: TClientDataSet; E: EReconcileError; UpdateKind: TUpdateKind, var Action TReconcileAction); begin Action := HandleReconcileError(DataSet, UpdateKind, E); end;

Refreshing records

Client applications work with an in-memory snapshot of the data on the application server. As time elapses, other users may modify that data, so that the data in the client application becomes a less and less accurate picture of the underlying data. Like any other dataset, client datasets have a Refresh method that updates its records to match the current values on the server. However, calling Refresh only works if there are no edits in the change log. Calling Refresh when there are unapplied edits results in an exception. Client applications can also update the data while leaving the change log intact. To do this, call the client dataset’s RefreshRecord method. Unlike the Refresh method, RefreshRecord updates only the current record in the client dataset. RefreshRecord changes the record value originally obtained from the application server but leaves any changes in the change log. Warning It may not always be appropriate to call RefreshRecord. If the user’s edits conflict with changes to the underlying dataset made by other users, calling RefreshRecord will 15-32Developer’ sGuide, Customizingapplicationserversmask this conflict. When the client application applies its updates, no reconcile error will occur and the application can’t resolve the conflict. In order to avoid masking update errors, client applications may want to check that there are no pending updates before calling RefreshRecord. For example, the following code raises an exception if an attempt is made to refresh a modified record: if ClientDataSet1.UpdateStatus <> usUnModified then raise Exception.Create('You must apply updates before refreshing the current record.'); ClientDataSet1.RefreshRecord;

Getting parameters from the application server

There are two circumstances when the client application needs to obtain parameter values from the application server: • The client needs to know the value of output parameters on a stored procedure. • The client wants to initialize the input parameters of a query or stored procedure to the current values of a TQuery or TStoredProc on the application server. The client requests parameter values from the application server by calling the FetchParams method. The parameters are returned in a data packet from the application server and assigned to the client dataset’s Params property. At design time, the Params property can be initialized by right-clicking the client dataset and choosing Fetch Params. Note The FetchParams method (or the Fetch Params command) will only work if the client dataset is connected to a provider that knows how to supply parameters. TProvider can supply parameter values if it represents a TQuery or TStoredProc. The Params property can also be used to send parameter values to the application server. For more information, see “Passing parameters to the application server” on page 23-14.

Customizing application servers

The MIDAS architecture is sufficiently flexible that you can customize the application server to suit the specific needs of your application. For example, you can • Extend the interface of the application server • Provide from and resolve to arbitrary datasets

Extending the application server’s interface

Client applications interact with the application server by creating or connecting to an instance of the remote data module. They use this interface as the basis of all communication with the application server. (One exception to this rule is when client datasets bypass the remote data module and talk directly to provider components using the IProvider interface.) Creatingmulti- tieredapplications15-33, CustomizingapplicationserversWhen using transactions or just-in-time activation under MTS, it is especially important that all communication with the application server goes through the remote data module’s interface. Any other communication bypasses the MTS proxy, which can invalidate transactions. If you are using a stateless MTS data module, bypassing the MTS proxy can lead to crashes because you can’t guarantee that the remote data module is active. Similarly, it is important to extend the application interface for single-instance CORBA servers, which must also be stateless. You can add to your remote data module’s interface to provide additional support for your client applications. This interface is a descendant of IDataBroker and is created for you automatically by the wizard when you create the remote data module. To add to the remote data module’s interface, you can • Choose the Add to Interface command from the Edit menu in the IDE. Indicate whether you are adding a procedure, function, or property, and enter its syntax. When you click OK, you will be positioned in the code editor on the implementation of your new interface member. • Use the type library editor. Select the interface for your application server in the type library editor, and click the tool button for the type of interface member (method or property) that you are adding. Give your interface member a name in the Attributes page, specify parameters and type in the Parameters page, and then refresh the type library. For more information about using the type library editor, see Chapter 48, “Working with type libraries.” Note that many of the features you can specify in the type library editor (such as help context, version, and so on) do not apply to CORBA interfaces. Any values you specify for these in the type library editor are ignored. What Delphi does when you add new entries to the interface depends on whether you are creating a COM-based (TRemoteDataModule or TMTSDataModule) or CORBA (TCorbaDataModule) server. • When you add to a COM interface, your changes are added to your unit source code and the type library file (.TLB). • When you add to a CORBA interface, your changes are reflected in your unit source code and the automatically generated _TLB unit. The _TLB unit is added to the uses clause of your unit. You must add this unit to the uses clause in your client application if you want to take advantage of early binding. In addition, you can save an .IDL file from the type library editor using File|Save As. The .IDL file is needed for registering the interface with the Interface Repository and Object Activation Daemon. Note The TLB file is not saved until you save it from the type library editor or choose File| Save All in the IDE. Once you have added to your remote data module’s interface, locate the properties and methods that were added to your remote data module’s implementation. Add code to finish this implementation. 15-34Developer’ sGuide, Managingtransactionsinmulti- tieredapplicationsClient applications call your interface extensions using the AppServer property of their connection component. For more information on how to do this, see “Calling server interfaces” on page 15-28.

Providing from and resolving to a dataset

The provider components on the component palette fetch their data from the dataset specified by the DataSet property. By default, however, when they apply updates and resolve update errors, they communicate directly with the database server using dynamically generated SQL statements. This approach has the advantage that your server application does not need to merge updates twice (first to the dataset, and then to the remote server). However, when your provider component applies updates and resolves errors using SQL, the DataSet property must specify a BDE-enabled dataset. You can tell the provider component to apply updates and resolve errors directly to a dataset on the application server instead. Using this approach, you can use a client dataset or a custom dataset on your application server. This lets you represent data that can’t be accessed through any of the BDE drivers. To resolve to a dataset instead of using dynamically generated SQL, set the ResolveToDataSet property of your TProvider component to True. If your application server does not otherwise use the Borland Database Engine, you can use a TDataSetProvider component instead of a TProvider component. Unlike TProvider, TDataSetProvider has no dependency on the BDE. By using this component your server application does not need to include the BDE unnecessarily.

Managing transactions in multi-tiered applications

When client applications apply updates to the application server, the provider component automatically wraps the process of applying updates and resolving errors in a transaction. This transaction is committed if the number of problem records does not exceed the MaxErrors value specified as an argument to the ApplyUpdates method. Otherwise, it is rolled back. In addition, you can add transaction support to your server application by adding a database component or using passthrough SQL. This works the same way that you would manage transactions in a two-tiered application. For more information about this sort of transaction control, see “Using transactions” on page 14-4. If you are using MTS, you can broaden your transaction support by using MTS transactions. MTS transactions can include any of the business logic on your application server, not just the database access. In addition, because they support two-phase commits, MTS transactions can span multiple databases. Note Two-phase commit is fully implemented only on Oracle7 and MS-SQL databases. If your transaction involves multiple databases, and some of them are remote servers other than Oracle7 or MS-SQL, your transaction runs a small risk of only partiallyCreatingmulti- tieredapplications15-35, Supportingmaster/ detailrelationshipssucceeding. Within any one database, however, you will always have transaction support. To use MTS transactions, extend the application server’s interface to include method calls that encapsulate the transaction. When configuring the MTS remote data module indicate that it must participate in transactions. When a client calls a method on your application server’s interface, it is automatically wrapped in a transaction. All client calls to your application server are then enlisted in that transaction until you indicate that the transaction is complete. These calls either succeed as a whole or are rolled back. Note Do not combine MTS transactions with explicit transactions created by a database component or using passthrough SQL. When your remote data module is enlisted in an MTS transaction, it automatically enlists all of your database calls in the transaction as well. For more information about using MTS transactions, see “MTS transaction support” on page 49-7.

Supporting master/detail relationships

You can create master/detail relationships between client datasets in your client application in the same way you set up master/detail forms in one- and two-tiered applications. For more information about setting up master/detail forms, see “Creating master/detail forms” on page 20-24. However, this approach has two major drawbacks: • The detail table must fetch and store all of its records from the application server even though it only uses one detail set at a time. This problem can be mitigated by using parameters. For more information, see “Limiting records with parameters” on page 23-15. • It is very difficult to apply updates, because client datasets apply updates at the dataset level and master/detail updates span multiple datasets. Even in a two- tiered environment, where you can use the database to apply updates for multiple tables in a single transaction, applying updates in master/detail forms is tricky. See “Applying updates for master/detail tables” on page 24-6 for more information on applying updates in traditional master/detail forms. In multi-tiered applications, you can avoid these problems by using nested tables to represent the master/detail relationship. To do this, set up a master/detail relationship between the tables on the application server. Then set the DataSet property of your provider component to the master table. When clients call the GetRecords method of the provider, it automatically includes the detail datasets as a DataSet field in the records of the data packet. When clients call the ApplyUpdates method of the provider, it automatically handles applying updates in the proper order. See “Representing master/detail relationships” on page 23-3 for more information on using nested datasets to support master/detail relationships in client datasets. 15-36Developer’ sGuide, Supportingstatelessremotedatamodules

Supporting stateless remote data modules

When using a single-instance model for CORBA or transactions and just-in-time activation for MTS, you can’t use the IProvider interface. This is because • The IProvider interface relies on setting state information. For example, when setting parameters or requesting data using incremental fetching, the provider on the application server must “remember” previous IProvider calls so that it can provide the expected data. • Under MTS, when the remote data module is deactivated, the client dataset’s Provider property becomes invalid. You can still use the provider on the application server, however, to provide data to your client datasets and apply updates from them. To do this, you must write your own stateless interface. Each interface call must do the same thing no matter when it is called. If you need to include state information, you must add parameters to the interface calls or include state information in your data packets. See “Adding custom information to data packets” on page 15-17 for details on how to add custom information to the data packets. Note Under MTS, each method of your interface must call SetComplete to tell MTS when it is finished so that transactions can complete and so the remote data module can be deactivated. For obtaining records from the provider, extend the application server’s interface to include a call to get records from a specific provider on the application server. This method could work something like the following MTS data module method: function TMyRemoteDataModule.GetCustomerRecords(MetaData: Boolean; out RecsOut: Integer): OleVariant; begin try if MetaData then Result := CustomerProvider.GetRecords(0, RecsOut); else Result := CustomerProvider.GetRecords(-1, RecsOut); SetComplete; except SetAbort; end; end; On the client side, you can assign the value returned by this method directly to the Data property of the client dataset: ClientDataSet1.Data := MyConnectionComponent.AppServer.GetCustomerRecords(False, RecsOut); Creatingmulti- tieredapplications15-37, DistributingaclientapplicationasanActiveXcontrolFor applying updates, add a method to the application server’s interface such as the following: function TMyRemoteDataModule.ApplyCustomerUpdates(Delta: OleVarant; MaxErrors: Integer; out ErrorCount: Integer); OleVariant; begin try Result := CustomerProvider.ApplyUpdates(Delta, MaxErrors, ErrorCount); SetComplete; except SetAbort; end; end; On the client side, instead of calling the ApplyUpdates method of the client dataset, you must call this interface method: with ClientDataSet1 do begin CheckBrowseMode; if ChangeCount > 0 then Reconcile(MyConnectionComponent.AppServer.ApplyCustomerUpdates(Delta, MaxErrors, ErrCount)); end;

Distributing a client application as an ActiveX control

Delphi’s distributed database architecture can be combined with its ActiveX features to distribute a client application as an ActiveX control. When you distribute your client application as an ActiveX control, create the application server as you would for any other multi-tiered application. The only limitation is that you will want to use DCOM or sockets as a communications protocol, because you can’t count on client machines having installed the OLEnterprise or CORBA runtime software. For details on creating the application server, see “Creating the application server” on page 15-10. When creating the client application, you must use an Active Form as the basis instead of an ordinary form. See “Creating an Active Form for the client application” on page 15-39 for details. Once you have built and deployed your client application, it can be accessed from any ActiveX-enabled Web browser on another machine. For a Web browser to successfully launch your client application, the Web server must be running on the machine that has the client application. If the client application uses DCOM to communicate between the client application and the application server, the machine with the Web browser must be enabled to work with DCOM. If the machine with the Web browser is a Windows 95 machine, it must have installed DCOM95, which is available from Microsoft. 15-38Developer’ sGuide, DistributingaclientapplicationasanActiveXcontrol

Creating an Active Form for the client application

1 Because the client application will be deployed as an ActiveX control, you must have a Web server that runs on the same system as the client application. You can use a ready-made server such as Microsoft’s IIS or Netscape’s server or you can write your own Web server using the socket components described in Chapter 29, “Working with sockets.” 2 Create the client application following the steps described in “Creating the client application” on page 15-23, except start by choosing File|New|Active Form, rather than beginning the client project as an ordinary Delphi project. 3 If your client application uses a data module, add a call to explicitly create the data module in the active form initialization. 4 When your client application is finished, compile the project, and select Project| Web Deployment Options. In the Web Deployment Options dialog, you must do the following: 1 On the Project page, specify the Target directory, the URL for the target directory, and the HTML directory. Typically, the Target directory and the HTML directory will be the same as the projects directory for your Web Server. The target URL is typically the name of the server machine that is specified in the Windows Network|DNS settings. 2 On the Additional Files page, include dbclient.dll and stdvcl32.dll with your client application. 5 Finally, select Project|WebDeploy to deploy the client application as an active form. Any Web browser that can run Active forms can run your client application by specifying the .HTM file that was created when you deployed the client application. This .HTM file has the same name as your client application project, and appears in the directory specified as the Target directory. Creatingmulti- tieredapplications15-39, 15-40Developer’ sGuide,

Chapter

Chapter 16Managing database sessions Both standalone, full client database applications and database application servers communicate with databases through the Borland Database Engine (BDE). An application’s database connections, drivers, cursors, queries, and so on are maintained within the context of one or more BDE sessions. Sessions isolate a set of database access operations, such as database connections, without the need to start another instance of the application. In an application, you can manage BDE sessions using the TSession and TSessionList components. Each TSession component in an application encapsulates a single BDE session. All sessions within an application are managed by a single TSessionList component. All database applications automatically include a session component, named Session, that encapsulates the default BDE session. Applications can declare, create, and manipulate additional session components as needed. All database applications also automatically include a session list component, named Sessions, that enables the application to manage all of its session components. This chapter describes the session and session list components and explains how to use them to control BDE sessions in your full client database applications and database application servers. Note The default session and session list components provide a widely applicable set of defaults that can be used as is by most applications. Only applications that use multiple sessions (for example, because of the need to run concurrent queries against a single database) may need to manipulate its session and session list components.

Working with a session component

A session component provides global management for a group of database connections within an application. When you create a full client database application or an application server, your application automatically contains a sessionManagingdatabasesessions16-1, Workingwithasessioncomponentcomponent, named Session. As you add database and dataset components to the application, they are automatically associated with this default session. It also controls access to password protected Paradox files, and it specifies directory locations for sharing Paradox files over a network. Applications can control database connections and access to Paradox files by using the properties, events, and methods of the session. You can use the default session to control all database connections in an application. Alternatively, you can add additional session components at design time or create them dynamically at runtime to control a subset of database connections in an application. Some applications require additional session components, such as applications that run concurrent queries against the same database. In this case, each concurrent query must run under its own session. Multi-threaded database applications also require multiple sessions. Applications that use multiple sessions must manage those sessions through the session list component, Sessions. For more information about managing multiple sessions see, “Managing multiple sessions” on page 16-16.

Using the default session

All database applications automatically include a default session. Delphi creates a default session component named Session for a database application each time it runs (note that its SessionName is “Default”). The default session provides global control over all database components not associated with another session, whether they are temporary (created by the session at runtime when a dataset is opened that is not associated with a database component you create) or persistent (explicitly created by your application). The default session is not visible in your data module or form at design time, but you can access its properties and methods in your code at runtime. When you create a database component, it is automatically associated with the default session. You need only associate a database component with an explicitly named session if the component performs a simultaneous query against a database already opened by the default session. When creating a multi-threaded database application, you must create one additional session to handle each additional thread. To use the default session, you need write no code unless your application must • Modify the properties of the session, such as specifying when database connections for automatically generated database components should automatically be kept or dropped. • Respond to session events, such as when the application attempts to access a password-protected Paradox file. • Execute a session’s methods, such as opening or closing a database in response to user-initiated actions. • Set the NetFileDir property to access Paradox tables on a network and set the PrivateDir property to a local hard drive to speed performance. Whether you add database components to an application at design time or create them dynamically at runtime, they are automatically associated with the default session unless you specifically assign them to a different session. If your application 16-2Developer’ sGuide, Workingwithasessioncomponentopens a dataset that is not associated with a database component, Delphi automatically • Creates a database component for it at runtime. • Associates the database component with the default session. • Initializes some of the database component’s key properties based on the default session’s properties. Among the most important of these properties is KeepConnections, which determines when database connections are maintained or dropped by an application. For more information about KeepConnections, see “Specifying default database connection behavior” on page 16-6. Additional properties, events, and methods of the TSession component which apply to the default session and any additional sessions you create in your applications are described in the rest of this chapter.

Creating additional sessions

You can create sessions to supplement the default session. At design time, you can place additional sessions on a data module (or form), set their properties in the Object Inspector, write event handlers for them, and write code that calls their methods. You can also create sessions, set their properties, and call their methods at runtime. This section describes how to create and delete sessions at runtime. Note Keep in mind that creating additional sessions is optional unless an application runs concurrent queries against a database or the application is multi-threaded. To enable dynamic creation of a session component at runtime, follow these steps: 1 Declare a pointer to a TSession variable. 2 Instantiate a new session by calling the Create constructor. The constructor sets up an empty list of database components for the session, sets up an empty list of BDE callbacks for the session, sets the KeepConnections property to True, and adds the session to the list of sessions maintained by the application’s session list component. 3 Set the SessionName property for the new session to a unique name. This property is used to associate database components with the session. For more information about the SessionName property, see “Naming a session” on page 16-4. 4 Activate the session and optionally adjust its properties. Note Never delete the default session. You can also manage creating and opening of sessions using the OpenSession method of TSessionList. Using OpenSession is safer than calling Create, because OpenSession only creates a session if it does not already exist. For more information about using OpenSession, see “Managing multiple sessions” on page 16-16. Managingdatabasesessions16-3, WorkingwithasessioncomponentThe following code creates a new session component, assigns it a name, and opens the session for database operations that follow (not shown here). After use, it is destroyed with a call to the Free method. var SecondSession: TSession; begin SecondSession := TSession.Create; with SecondSession do try SessionName := 'SecondSession'; KeepConnections := False; Open; ... finally SecondSession.Free; end; end;

Naming a session

A session’s SessionName property is used to name the session so that you can associate databases and datasets with it. For the default session, SessionName is “Default.” For each additional session component you create, you must set its SessionName property to a unique value. Database and dataset components have SessionName properties that correspond to the SessionName property of a session component. If you leave the SessionName property blank for a database or dataset component it is automatically associated with the default session. You can also set SessionName for a database or dataset component to a name that corresponds to the SessionName of a session component you create. For more information about using the TSessionList component—and Sessions in particular—to control multiple sessions, see “Managing multiple sessions” on page 16-16. The following code uses the OpenSession method of the default TSessionList component, Sessions, to open a new session component, sets its SessionName to “InterBaseSession”, activate the session, and associate an existing database component Database1 with that session: var IBSession: TSession; ... begin IBSession := Sessions.OpenSession('InterBaseSession'); Database1.SessionName := 'InterBaseSession'; end; 16-4Developer’ sGuide, Workingwithasessioncomponent

Activating a session

Active is a Boolean property that determines if database and dataset components associated with a session are open. You can use this property to read the current state of a session’s database and dataset connections, or to change it. To determine the current state of a session, check Active. If Active is False (the default), all databases and datasets associated with the session are closed. If True, databases and datasets are open. Setting Active to True triggers a session’s OnStartup event, sets the NetFileDir and PrivateDir properties if they are assigned values, and sets the ConfigMode property. You can write an OnStartup event handler to perform other specific database start-up activities. For more information about OnStartup, see “Working with password- protected Paradox and dBase tables” on page 16-13. The NetFileDir and PrivateDir properties are only used for connecting to Paradox tables. For more information about them, see “Specifying the control file location” on page 16-13 and “Specifying a temporary files location” on page 16-13. ConfigMode determines how the BDE handles BDE aliases created within the context of the session. For more information about ConfigMode, see “Specifying alias visibility” on page 16-10. To open database components within a session, see “Creating, opening, and closing database connections” on page 16-6. After you activate a session, you can open its database connections by calling the OpenDatabase method. For session components you place in a data module or form, setting Active to False when there are open databases or datasets closes them. At runtime, closing databases and datasets may invoke events associated with them. Note You cannot set Active to False for the default session at design time. While you can close the default session at runtime, it is not recommended. For session components you create, use the Object Inspector to set Active to False at design time to disable all database access for a session with a single property change. You might want to do this if, during application design, you do not want to receive exceptions because a remote database is temporarily unavailable. You can also use a session’s Open and Close methods to activate or deactivate sessions other than the default session at runtime. For example, the following single line of code closes all open databases and datasets for a session: Session1.Close; This code sets Session1’s Active property to False. When a session’s Active property is False, any subsequent attempt by the application to open a database or dataset resets Active to True and calls the session’s OnStartup event handler if it exists. You can also explicitly code session reactivation at runtime. The following code reactivates Session1: Session1.Open; Note If a session is active you can also open and close individual database connections. For more information, see “Closing a single database connection” on page 16-7. Managingdatabasesessions16-5, Workingwithasessioncomponent

Customizing session start-up

You can customize a session’s start-up behavior by writing an OnStartup event handler for it. OnStartup is triggered when a session is activated. A session is activated when it is first created, and subsequently, whenever its Active property is changed to True from False (for example, when a database or dataset is associated with a session is opened and there are currently no other open databases or datasets).

Specifying default database connection behavior

KeepConnections provides the default value for the KeepConnection property of temporary database components created at runtime. KeepConnection specifies what happens to a database connection established for a database component when all its datasets are closed. If True (the default), a constant, or persistent, database connection is maintained even if no dataset is active. If False, a database connection is dropped as soon as all its datasets are closed. Note Connection persistence for a database component you explicitly place in a data module or form is controlled by that database component’s KeepConnection property. If set differently, KeepConnection for a database component always overrides the KeepConnections property of the session. For more information about controlling individual database connections within a session, see “Creating, opening, and closing database connections” on page 16-6. KeepConnections should always be set to True for applications that frequently open and close all datasets associated with a database on a remote server. This setting reduces network traffic and speeds data access because it means that a connection need only be opened and closed once during the lifetime of the session. Otherwise, every time the application closes or reestablishes a connection, it incurs the overhead of attaching and detaching the database. Note Even when KeepConnections is True for a session, you can close inactive database connections for all temporary database components, and then free the temporary database components by calling the DropConnections method. For more information about DropConnections, see “Dropping temporary database connections” on page 16-8. For example, the following code drops inactive connections and frees all temporary database components for the default session: Session.DropConnections;

Creating, opening, and closing database connections

To open a database connection within a session, call the OpenDatabase method. OpenDatabase takes one parameter, the name of the database to open. This name is a BDE alias or the name of a database component. For Paradox or dBASE, the name can also be a fully qualified path name. For example, the following statement uses the 16-6Developer’ sGuide, Workingwithasessioncomponentdefault session and attempts to open a database connection for the database pointed to by the DBDEMOS alias: var DBDemosDatabase: TDatabase; begin DBDemosDatabase := Session.OpenDatabase('DBDEMOS'); ... OpenDatabase makes its own session active if it is not already active, and then determines if the specified database name matches the DatabaseName property of any database components for the session. If the name does not match an existing database component, OpenDatabase creates a temporary database component using the specified name. Each call to OpenDatabase increments a reference count for the database by 1. As long as this reference count remains greater than 0, the database is open. Finally, OpenDatabase calls the Open method of the database component to connect to the server. Closing a single database connection You can close an individual database connection with the CloseDatabase method, or close all connections for a session at once with the Close method. When you call CloseDatabase, a reference count for the database is decremented by 1. When the reference count for the database is 0, the database is closed and freed. CloseDatabase takes one parameter, the database to close. For example, the following statement closes the database connection opened in the example in the previous section: Session.CloseDatabase(DBDemosDatabase); If the specified database name is associated with a temporary database component, and the session’s KeepConnections property is False, the temporary database component is freed, effectively closing the connection. Note If KeepConnections is False temporary database components are closed and freed automatically when the last dataset associated with the database component is closed. An application can always call CloseDatabase prior to that time to force closure. To free temporary database components when KeepConnections is True, call the database component’s Close method, and then call the session’s DropConnections method. If the database component is persistent (meaning that the application specifically declares the component and instantiates it), and the session’s KeepConnections property is False, CloseDatabase calls the database component’s Close method to close the connection. Note Calling CloseDatabase for a persistent database component does not actually close the connection. To close the connection, call the database component’s Close method directly. Closing all database connections You can close all database connections in two ways: • Set the Active property for the session to False. • Call the Close method for the session. Managingdatabasesessions16-7, WorkingwithasessioncomponentWhen you set Active to False, Delphi automatically calls the Close method. Close disconnects from all active databases by freeing temporary database components and calling each persistent database component’s Close method. Finally, Close sets the session’s BDE handle to nil.

Dropping temporary database connections

If the KeepConnections property for a session is True (the default), then database connections for temporary database components are maintained even if all the datasets used by the component are closed. You can eliminate these connections and free all inactive temporary database components for a session by calling the DropConnections method. For example, the following code frees all inactive, temporary database components for the default session: Session.DropConnections; Temporary database components for which one or more datasets are active are not dropped or freed by this call. To free these components, call Close.

Searching for a database connection

Use a session’s FindDatabase method to determine whether or not a specified database component is already associated with a session. FindDatabase takes one parameter, the name of the database to search for. This name is a BDE alias or database component name. For Paradox or dBASE, the name can also be a fully- qualified path name. FindDatabase returns the database component if it finds a match. Otherwise it returns nil. The following code searches the default session for a database component using the DBDEMOS alias, and if it is not found, creates one and opens it: var DB: TDatabase; begin DB := Session.FindDatabase('DBDEMOS'); if (DB = nil) then { database doesn't exist for session so,} DB := Session.OpenDatabase('DBDEMOS'); { create and open it} if Assigned(DB) and DB.Active then begin DB.StartTransaction; ... end; end;

Retrieving information about a session

You can retrieve information about a session and its database components by using a session’s informational methods. For example, one method retrieves the names of all aliases known to the session, and another method retrieves the names of tables 16-8Developer’ sGuide, Workingwithasessioncomponentassociated with a specific database component used by the session. Table 16.1 summarizes the informational methods to a session component: Table 16.1 Database-related informational methods for session components Method Purpose GetAliasDriverName Retrieves the BDE driver for a specified alias of a database. GetAliasNames Retrieves the list of BDE aliases for a database. GetAliasParams Retrieves the list of parameters for a specified BDE alias of a database. GetConfigParams Retrieves specific configuration information from the BDE configuration file. GetDatabaseNames Retrieves the list of BDE aliases and the names of any TDatabase components currently in use. GetDriverNames Retrieves the names of all currently installed BDE drivers. GetDriverParams Retrieves the list of parameters for a specified BDE driver. GetStoredProcNames Retrieves the names of all stored procedures for a specified database. GetTableNames Retrieves the names of all tables matching a specified pattern for a specified database. Except for GetAliasDriverName, these methods return a set of values into a string list declared and maintained by your application. (GetAliasDriverName returns a single string, the name of the current BDE driver for a particular database component used by the session.) For example, the following code retrieves the names of all database components and aliases known to the default session: var List: TStringList; begin List := TStringList.Create; try Session.GetDatabaseNames(List); ... finally List.Free; end; end; For a complete description of a session’s informational methods, see the TSession topic in the VCL online help reference.

Working with BDE aliases

Because a session usually encapsulates a series of database connections, one property and many of a session component’s methods work with BDE aliases. Each database component associated with a session has a BDE alias (although optionally a fully- Managingdatabasesessions16-9, Workingwithasessioncomponentqualified path name may be substituted for an alias when accessing Paradox and dBASE tables. BDE aliases and the associated TSession methods have three main uses: • Determining visibility • Retrieving alias and driver information • Creating, modifying, and deleting aliases The following sections describe these functional areas. Specifying alias visibility A session’s ConfigMode property determines what BDE aliases are visible to the session. ConfigMode is a set that describes which types of sessions are visible. The default setting is cmAll, which translates into the set [cfmVirtual, cfmPersistent]. If ConfigMode is cmAll, a session can see all aliases created within the session, all aliases in the BDE configuration file on a user’s system, and all aliases that the BDE maintains in memory. The main purpose of ConfigMode is to enable an application to specify and restrict alias visibility at the session level. For example, setting ConfigMode to cfmSession restricts a session’s view of aliases to those created within the session. All other aliases in the BDE configuration file and in memory are not available. For a full description of ConfigMode and its settings, see the online reference for the object and component library. Making session aliases visible to other sessions and applications When an alias is created during a session, the BDE stores a copy of the alias in memory. By default this copy is local only to the session in which it is created. Another session in the same application can only see the alias if its ConfigMode property is cmAll or cfmPersistent. To make an alias available to all sessions and to other applications, use the session’s SaveConfigFile method. SaveConfigFile writes aliases in memory to the BDE configuration file where they can be read and used by other BDE-enabled applications. Determining known aliases, drivers, and parameters Five methods for session components enable an application to retrieve information about BDE aliases, including parameter information and driver information. They are: • GetAliasNames, to list the aliases to which a session has access. • GetAliasParams, to list the parameters for a specified alias. • GetAliasDriverName, to return a string containing the name of the BDE driver used by the alias. • GetDriverNames, to return a list of all BDE drivers available to the session. • GetDriverParams, to return driver parameters for a specified driver. For more information about using a session’s informational methods, see “Retrieving information about a session” on page 16-8. For more information about BDE aliases, parameters, and drivers, see the online BDE help file, BDE32.HLP. 16-10Developer’ sGuide, WorkingwithasessioncomponentCreating, modifying, and deleting aliases A session can create, modify, and delete aliases during its lifetime. The AddAlias method creates a new BDE alias for an SQL database sever. AddStandardAlias creates a new BDE alias for Paradox, dBASE, or ASCII tables. AddAlias takes three parameters: a string containing a name for the alias, a string that specifies the SQL Links driver to use, and a string list populated with parameters for the alias. For more information about AddAlias, see the topic for this method in the online VCL reference. For more information about BDE aliases and the SQL Links drivers, see the BDE online help, BDE32.HLP. AddStandardAlias takes three string parameters: the name for the alias, the fully-qualified path to the Paradox or dBASE table to access, and the name of the default driver to use when attempting to open a table that does not have an extension. For more information about AddStandardAlias, see the online reference for the object and component libraries. For more information about BDE aliases, see the BDE online help, BDE32.HLP. Note When you add an alias to a session, it is only available to this session and any other sessions with cfmPersistent included in the ConfigMode property. To make a newly created alias available to all applications, call SaveConfigFile after creating the alias. For more information about ConfigMode, see “Working with BDE aliases” on page 16-9. After you create an alias, you can make changes to its parameters by calling ModifyAlias. ModifyAlias takes two parameters: the name of the alias to modify and a string list containing the parameters to change and their values. To delete an alias previously created in a session, call the DeleteAlias method. DeleteAlias takes one parameter, the name of the alias to delete. DeleteAlias makes an alias unavailable to the session. Note DeleteAlias does not remove an alias from the BDE configuration file if the alias was written to the file by a previous call to SaveConfigFile. To remove the alias from the configuration file after calling DeleteAlias, call SaveConfigFile again. The following statements use AddAlias to add a new alias for accessing an InterBase server to the default session: var AliasParams: TStringList; begin AliasParams := TStringList.Create; try with AliasParams do begin Add('OPEN MODE=READ'); Add('USER NAME=TOMSTOPPARD'); Add('SERVER NAME=ANIMALS:/CATS/PEDIGREE.GDB'); end; Session.AddAlias('CATS', 'INTRBASE', AliasParams); ... finally AliasParams.Free; end; end; Managingdatabasesessions16-11, WorkingwithasessioncomponentThe following statement uses AddStandardAlias to create a new alias for accessing a Paradox table: AddStandardAlias('MYDBDEMOS', 'C:\TESTING\DEMOS\', 'Paradox'); The following statements use ModifyAlias to change the OPEN MODE parameter for the CATS alias to READ/WRITE in the default session: var List: TStringList; begin List := TStringList.Create; with List do begin Clear; Add('OPEN MODE=READ/WRITE'); end; Session.ModifyAlias('CATS', List); List.Free; ...

Iterating through a session’s database components

Two session component properties, Databases and DatabaseCount, enable you to cycle through all the database components associated with a session. Databases is an array of all currently active database components associated with a session. Used with the DatabaseCount property, Databases can be used to iterate through all active database components to perform a selective or universal action. DatabaseCount is an Integer property that indicates the number of currently active databases associated with a session. As connections are opened or closed during a session’s life-span, this number can change. For example, if a session’s KeepConnections property is False and all database components are created as needed at runtime, each time a unique database is opened, DatabaseCount increases by one. Each time a unique database is closed, DatabaseCount decreases by one. If DatabaseCount is zero, there are no currently active database components for the session. DatabaseCount is typically used with the Databases property to perform actions common to active database components. The following example code sets the KeepConnection property of each active database in the default session to True: var MaxDbCount: Integer; begin with Session do if (DatabaseCount > 0) then for MaxDbCount := 0 to (DatabaseCount - 1) do Databases[MaxDbCount].KeepConnection := True; end; 16-12Developer’ sGuide, Workingwithasessioncomponent

Specifying Paradox directory locations

Two session component properties, NetFileDir and PrivateDir, are specific to applications that work with Paradox tables. NetFileDir specifies the directory that contains the Paradox network control file, PDOXUSRS.NET. This file governs sharing of Paradox tables on network drives. All applications that need to share Paradox tables must specify the same directory for the network control file (typically a directory on a network file server). PrivateDir specifies the directory for storing temporary table processing files, such as those generated by the BDE to handle local SQL statements. Specifying the control file location Delphi derives a value for NetFileDir from the Borland Database Engine (BDE) configuration file for a given database alias. If you set NetFileDir yourself, the value you supply overrides the BDE configuration setting, so be sure to validate the new value. At design time, you can specify a value for NetFileDir in the Object Inspector. You can also set or change NetFileDir in code at runtime. The following code sets NetFileDir for the default session to the location of the directory from which your application runs: Session.NetFileDir := ExtractFilePath(Application.EXEName); Note NetFileDir can only be changed when an application does not have any open Paradox files. If you change NetFileDir at runtime, verify that it points to a valid network directory that is shared by your network users. Specifying a temporary files location If no value is specified for the PrivateDir property, the BDE automatically uses the current directory at the time it is initialized. If your application runs directly from a network file server, you can improve application performance at runtime by setting PrivateDir to a user’s local hard drive before opening the database. Note Do not set PrivateDir at design time if you intend to open the database. Doing so generates a “Directory busy” error. The following code changes the setting of the default session’s PrivateDir property to a user’s C:\TEMP directory: Session.PrivateDir := 'C:\TEMP'; Important Do not set PrivateDir to a root directory on a drive. Always specify a subdirectory.

Working with password-protected Paradox and dBase tables

A session component provides four methods and one event that are exclusively used to manage access to password-protected Paradox and dBase files. The methods are AddPassword, GetPassword, RemoveAllPasswords, and RemovePassword. The event isManagingdatabasesessions16-13, WorkingwithasessioncomponentOnPassword. The PasswordDialog function is also available for adding and removing one or more passwords from a session. Using the AddPassword method The TSession.AddPassword method provides an optional way for an application to provide a password for a session prior to opening an encrypted Paradox or dBase table that requires a password for access. AddPassword takes one parameter, a string containing the password to use. You can call AddPassword as many times as necessary to add passwords to access files protected with different passwords. var Passwrd: String; begin Passwrd := InputBox('Enter password', 'Password:', ''); Session.AddPassword(Passwrd); try Table1.Open except ShowMessage('Could not open table!'); Application.Terminate; end; end; Use of the InputBox function, above, is for demonstration purposes. In a real-world application, use password entry facilities that mask the password as it is entered, such as the PasswordDialog function or a custom form. On a custom password entry form, use a TEdit with the PasswordChar set to an asterisk (“*”). The Add button of the PasswordDialog function dialog has the same effect as the AddPassword method. if PasswordDialog(Session) then Table1.Open else ShowMessage('No password given, could not open table!'); end; Note You need to call AddPassword to specify one or more passwords (one at a time) to use when accessing password-protected files. If you do not, when your application attempts to open a password-protected table, a dialog box prompts the user for a password. Using the RemovePassword and RemoveAllPasswords methods TSession.RemovePassword deletes a previously added password from memory. RemovePassword takes one parameter, a string containing the password to delete. Session.RemovePassword(‘secret’); TSession.RemoveAllPasswords deletes all previously added passwords from memory. Session.RemoveAllPasswords; 16-14Developer’ sGuide, Workingwithasessioncomponent

Using the GetPassword method and OnPassword event

TSession.GetPassword triggers the TSession.OnPassword event for a session. The OnPassword event is called only when an application attempts to open a Paradox or dBase table for the first time and the BDE reports insufficient access rights. You can code an OnPassword event handler and provide a password to the BDE, or you can choose to use the default password handling, which displays a dialog box prompting the user for a password. In the example below, the procedure Password is designated as the handler for the OnPassword event by assigning the procedure’s name to the TSession.OnPassword property. procedure TForm1.FormCreate(Sender: TObject); begin Session.OnPassword := Password; end; In the Password procedure, the InputBox function is used to prompt the user for a password. The TSession.AddPassword method is used to programmatically supply the password entered in the dialog to the session. procedure TForm1.Password(Sender: TObject; var Continue: Boolean); var Passwrd: String; begin Passwrd := InputBox('Enter password', 'Password:', ''); Continue := (Passwrd > ''); Session.AddPassword(Passwrd); end; The Password procedure is triggered by an attempt to open a password-protected table, as shown below. Exception handling can be used to accommodate a failed attempt to open the table. Even though the user is prompted for a password in the OnPassword event handler, the open attempt can still fail if they enter an invalid password or something else goes wrong. procedure TForm1.OpenTableBtnClick(Sender: TObject); const CRLF = #13 + #10; begin try Table1.Open; { this line triggers the OnPassword event } except on E:Exception do begin { exception if cannot open table } ShowMessage('Error!' + CRLF + { display error explaining what happened } E.Message + CRLF + 'Terminating application...'); Application.Terminate; { end the application } end; end; end; Managingdatabasesessions16-15, Managingmultiplesessions

Managing multiple sessions

If you create a single application that uses multiple threads to perform database operations, you must create one additional session for each thread. The Data Access page on the Component palette contains a session component that you can place in a data module or on a form at design time. Important When you place a session component, you must also set its SessionName property to a unique value so that it does not conflict with the default session’s SessionName property. Placing a session component at design time presupposes that the number of threads (and therefore sessions) required by the application at runtime is static. More likely, however, is that an application needs to create sessions dynamically. To create sessions dynamically, call the global function Sessions.OpenSession at runtime. Sessions.OpenSession requires a single parameter, a name for the session that is unique across all session names for the application. The following code dynamically creates and activates a new session with a uniquely generated name: Sessions.OpenSession('RunTimeSession' + IntToStr(Sessions.Count + 1)); This statement generates a unique name for a new session by retrieving the current number of sessions, and adding one to that value. Note that if you dynamically create and destroy sessions at runtime, this example code will not work as expected. Nevertheless, this example illustrates how to use the properties and methods of Sessions to manage multiple sessions. Sessions is a variable of type TSessionList that is automatically instantiated for database applications. You use the properties and methods of Sessions to keep track of multiple sessions in a multi-threaded database application. Table 16.2 summarizes the properties and methods of the TSessionList component: Table 16.2 TSessionList properties and methods Property or Method Purpose Count Returns the number of sessions, both active and inactive, in the sessions list. FindSession Searches the session list for a session with a specified name, and returns a pointer to the session component, or nil if there is no session with the specified name. If passed a blank session name, FindSession returns a pointer to the default session, Session. GetSessionNames Populates a string list with the names of all currently instantiated session components. This procedure always adds at least one string, “Default” for the default session (note that the default session’s name is actually a blank string). List Returns the session component for a specified session name. If there is no session with the specified name, an exception is raised. OpenSession Creates and activates a new session or reactivates an existing session for a specified session name. Sessions Accesses the session list by ordinal value. 16-16Developer’ sGuide, UsingasessioncomponentindatamodulesAs an example of using Sessions properties and methods in a multi-threaded application, consider what happens when you want to open a database connection. To determine if a connection already exists, use the Sessions property to walk through each session in the sessions list, starting with the default session. For each session component, examine its Databases property to see if the database in question is open. If you discover that another thread is already using the desired database, examine the next session in the list. If an existing thread is not using the database, then you can open the connection within that session. If, on the other hand, all existing threads are using the database, you must open a new session in which to open another database connection. If you are replicating a data module that contains a session in a multi-threaded application, where each thread contains its own copy of the data module, you can use the AutoSessionName property to make sure that all datasets in the data module use the correct session. Setting AutoSessionName to True causes the session to generate its own unique name dynamically when it is created at runtime. It then assigns this name to every dataset in the data module, overriding any explicitly set session names. This ensures that each thread has its own session, and each dataset uses the session in its own data module.

Using a session component in data modules

You can safely place session components in data modules. If you put a data module that contains one or more session components into the Object Repository, however, make sure to set the AutoSessionName property to True to avoid namespace conflicts when users inherit from it. Managingdatabasesessions16-17, 16-18Developer’ sGuide,

Chapter

Chapter 17Connecting to databases When a Delphi application connects to a database, that connection is encapsulated by a TDatabase component. A database component encapsulates the connection to a single database in the context of a Borland Database Engine (BDE) session in an application. This chapter describes database components and how to manipulate database connections. Database components are also used to manage transactions in BDE-based applications. For more information about using databases to manage transactions, see “Using transactions” on page 14-4. Another use for database components is applying cached updates for related tables. For more information about using a database component to apply cached updates, see“Applying cached updates with a database component method” on page 24-5.

Understanding persistent and temporary database components

Each database connection in an application is encapsulated by a database component whether or not you explicitly provide a database component at design time or create it dynamically at runtime. When an application attempts to connect to a database, it either uses an explicitly instantiated, or persistent, database component, or it generates a temporary database component that exists only for the lifetime of the connection. Temporary database components are created as necessary for any datasets in a data module or form for which you do not create yourself. Temporary database components provide broad support for many typical desktop database applications without requiring you to handle the details of the database connection. For most client/server applications, however, you should create your own database components instead of relying on temporary ones. You gain greater control over your databases, including the ability to • Create persistent database connections • Customize database server loginsConnectingtodatabases17-1, Understandingpersistentandtemporarydatabasecomponents• Control transactions and specify transaction isolation levels • Create BDE aliases local to your application

Using temporary database components

Temporary database components are automatically generated as needed. For example, if you place a TTable component on a form, set its properties, and open the table without first placing and setting up a TDatabase component and associating the table component with it, Delphi creates a temporary database component for you behind the scenes. Some key properties of temporary database components are determined by the session to which they belong. For example, the controlling session’s KeepConnections property determines whether a database connection is maintained even if its associated datasets are closed (the default), or if the connections are dropped when all its datasets are closed. Similarly, the default OnPassword event for a session guarantees that when an application attempts to attach to a database on a server that requires a password, it displays a standard password prompt dialog box. Other properties of temporary database components provide standard login and transaction handling. For more information about sessions and session control over temporary database connections, see “Working with a session component” on page 16-1. The default properties created for temporary database components provide reasonable, general behaviors meant to cover a wide variety of situations. For complex, mission-critical client/server applications with many users and different requirements for database connections, however, you should create your own database components to tune each database connection to your application’s needs.

Creating database components at design time

The Data Access page of the Component palette contains a database component you can place in a data module or form. The main advantages to creating a database component at design time are that you can set its initial properties and write OnLogin events for it. OnLogin offers you a chance to customize the handling of security on a database server when a database component first connects to the server. For more information about managing connection properties, see “Connecting to a database server” on page 17-6. For more information about server security, see “Controlling server login” on page 17-6.

Creating database components at runtime

You can create database components dynamically at runtime. An application might do this when the number of database components needed at runtime is unknown, and your application needs explicit control over the database connection. In fact, this is how Delphi itself creates temporary database components as needed at runtime. When you create a database component at runtime, you need to give it a unique name and to associate it with a session. 17-2Developer’ sGuide, UnderstandingpersistentandtemporarydatabasecomponentsYou create the component by calling the TDatabase.Create constructor. Given both a database name and a session name, the following function creates a database component at runtime, associates it with a specified session (creating a new session if necessary), and sets a few key database component properties: function RunTimeDbCreate(const DatabaseName, SessionName: string): TDatabase; var TempDatabase: TDatabase; begin TempDatabase := nil; try { If the session exists, make it active; if not, create a new session } Sessions.OpenSession(SessionName); with Sessions do with FindSession(SessionName) do Result := FindDatabase(DatabaseName); if Result = nil then begin { Create a new database component } TempDatabase := TDatabase.Create(Self); TempDatabase.DatabaseName := DatabaseName; TempDatabase.SessionName := SessionName; TempDatabase.KeepConnection := True; end; Result := OpenDatabase(DatabaseName); end; end; except TempDatabase.Free; raise; end; end; The following code fragment illustrates how you might call this function to create a database component for the default session at runtime: var MyDatabase: array [1..10] of TDatabase; MyDbCount: Integer; begin { Initialize MyDbCount early on } MyDbCount := 1; ƒ { Later, create a database component at runtime } begin MyDatabase[MyDbCount] := RunTimeDbCreate('MyDb' + IntToStr(MyDbCount), ''); Inc(MyDbCount); end; ƒ end; Connectingtodatabases17-3, Controllingconnections

Controlling connections

Whether you create a database component at design time or runtime, you can use the properties, events, and methods of TDatabase to control and change its behavior in your applications. The following sections describe how to manipulate database components. For details about all TDatabase properties, events, and methods, see the online reference for the object and component libraries.

Associating a database component with a session

All database components must be associated with a BDE session. Two database component properties, Session and SessionName, establish this association. SessionName identifies the session alias with which to associate a database component. When you first create a database component at design time, SessionName is set to “Default”. Multi-threaded or reentrant BDE applications may have more than one session. At design time, you can pick a valid SessionName from the drop- down list in the Object Inspector. Session names in the list come from the SessionName properties of each session component in the application. The Session property is a runtime, read-only property that points to the session component specified by the SessionName property. For example, if SessionName is blank or “Default”, then the Session property references the same TSession instance referenced by the global Session variable. Session enables applications to access the properties, methods, and events of a database component’s parent session component without knowing the session’s actual name. This is useful when a database component is assigned to a different session at runtime. For more information about BDE sessions, see Chapter 16, “Managing database sessions.”

Specifying a BDE alias

AliasName and DriverName are mutually-exclusive BDE-specific properties. AliasName specifies the name of an existing BDE alias to use for the database component. The alias appears in subsequent drop-down lists for dataset components so that you can link them to a particular database component. If you specify AliasName for a database component, any value already assigned to DriverName is cleared because a driver name is always part of a BDE alias. Note You create and edit BDE aliases using the Database Explorer or the BDE Administration utility. For more information about creating and maintaining BDE aliases, see the online documentation for these utilities. DatabaseName enables you to provide an application-specific name for a database component. The name you supply is in addition to AliasName or DriverName, and is local to your application. DatabaseName can be a BDE alias, or, for Paradox and dBASE files, a fully-qualified path name. Like AliasName, DatabaseName appears in subsequent drop-down lists for dataset components to enable you to link them to a database component. 17-4Developer’ sGuide, ControllingconnectionsDriverName is the name of a BDE driver. A driver name is one parameter in a BDE alias, but you may specify a driver name instead of an alias when you create a local BDE alias for a database component using the DatabaseName property. If you specify DriverName, any value already assigned to AliasName is cleared to avoid potential conflicts between the driver name you specify and the driver name that is part of the BDE alias identified in AliasName. At design time, to specify a BDE alias, assign a BDE driver, or create a local BDE alias, double-click a database component to invoke the Database Properties editor. You can enter a DatabaseName in the Name edit box in the properties editor. You can enter an existing BDE alias name in the Alias name combo box for the Alias property, or you can choose from existing aliases in the drop-down list. The Driver name combo box enables you to enter the name of an existing BDE driver for the DriverName property, or you can choose from existing driver names in the drop- down list. Note The Database Properties editor also enables you to view and set BDE connection parameters, and set the states of the LoginPrompt and KeepConnection properties. To work with connection parameters, see “Setting BDE alias parameters” on page 17-5. To set the state of LoginPrompt, see “Controlling server login” on page 17-6, and to set KeepConnection see “Connecting to a database server” on page 17-6. To set DatabaseName, AliasName, or DriverName at runtime, include the appropriate assignment statement in your code. For example, the following code uses the text from an edit box to create a local alias for the database component Database1: Database1.DatabaseName := Edit1.Text;

Setting BDE alias parameters

At design time you can create or edit connection parameters in three ways: • Use the Database Explorer or BDE Administration utility to create or modify BDE aliases, including parameters. For more information about these utilities, see their online Help files. • Double-click the Params property in the Object Inspector to invoke the String List editor. To learn more about the String List editor, see “Working with string lists” in the on-line help. • Double-click a database component in a data module or form to invoke the Database Properties editor. All of these methods edit the Params property for the database component. Params is a string list containing the database connection parameters for the BDE alias associated with a database component. Some typical connection parameters include path statement, server name, schema caching size, language driver, and SQL query mode. When you first invoke the Database Properties editor, the parameters for the BDE alias are not visible. To see the current settings, click Defaults. The current parameters are displayed in the Parameter overrides memo box. You can editConnectingtodatabases17-5, Controllingconnectionsexisting entries or add new ones. To clear existing parameters, click Clear. Changes you make take effect only when you click OK. At runtime, an application can set alias parameters only by editing the Params property directly. For more information about parameters specific to using SQL Links drivers with the BDE, see your online SQL Links help file.

Controlling server login

Most remote database servers include security features to prohibit unauthorized access. Generally, the server requires a user name and password login before permitting database access. At design time, if a server requires a login, a standard Login dialog box prompts for a user name and password when you first attempt to connect to the database. At runtime, there are three ways you can handle a server’s request for a login: • Set the LoginPrompt property of a database component to True (the default). Your application displays the standard Login dialog box when the server requests a user name and password. • Set the LoginPrompt to False, and include hard-coded USER NAME and PASSWORD parameters in the Params property for the database component. For example: USER NAME=SYSDBA PASSWORD=masterkey Important Note that because the Params property is easy to view, this method compromises server security, so it is not recommended. • Write an OnLogin event for the database component, and use it to set login parameters at runtime. OnLogin gets a copy of the database component’s Params property, which you can modify. The name of the copy in OnLogin is LoginParams. Use the Values property to set or change login parameters as follows: LoginParams.Values['USER NAME'] := UserName; LoginParams.Values['PASSWORD'] := PasswordSearch(UserName); On exit, OnLogin passes its LoginParams values back to Params, which is used to establish a connection.

Connecting to a database server

There are two ways to connect to a database server using a database component: • Call the Open method. • Set the Connected property to True. Setting Connected to True executes the Open method. Open verifies that the database specified by the DatabaseName or Directory properties exists, and if an OnLogin event exists for the database component, it is executed. Otherwise, the default Login dialog box appears. 17-6Developer’ sGuide, ControllingconnectionsNote When a database component is not connected to a server and an application attempts to open a dataset associated with the database component, the database component’s Open method is first called to establish the connection. If the dataset is not associated with an existing database component, a temporary database component is created and used to establish the connection. Once a database connection is established the connection is maintained as long as there is at least one active dataset. When there are no more active datasets, whether or not the connection is dropped depends on the setting of the database component’s KeepConnection property. KeepConnection determines if your application maintains a connection to a database even when all datasets associated with that database are closed. If True, a connection is maintained. For connections to remote database servers, or for applications that frequently open and close datasets, make sure KeepConnection is True to reduce network traffic and speed up your application. If False a connection is dropped when there are no active datasets using the database. If a dataset is later opened which uses the database, the connection must be reestablished and initialized.

Special considerations when connecting to a remote server

When you connect to a remote database server from an application, the application uses the BDE and the Borland SQL Links driver to establish the connection. (The BDE can communicate with an ODBC driver that you supply.) You need to configure the SQL Links or ODBC driver for your application prior to making the connection. SQL Links and ODBC parameters are stored in the Params property of a database component. For information about SQL Links parameters, see the online SQL Links User’s Guide. To edit the Params property, see “Setting BDE alias parameters” on page 17-5. Working with network protocols As part of configuring the appropriate SQL Links or ODBC driver, you may need to specify the network protocol used by the server, such as SPX/IPX or TCP/IP, depending on the driver’s configuration options. In most cases, network protocol configuration is handled using a server’s client setup software. For ODBC it may also be necessary to check the driver setup using the ODBC driver manager. Establishing an initial connection between client and server can be problematic. The following troubleshooting checklist should be helpful if you encounter difficulties: • Is your server’s client-side connection properly configured? • If you are using TCP/IP: • Is your TCP/IP communications software installed? Is the proper WINSOCK.DLL installed? • Is the server’s IP address registered in the client’s HOSTS file? • Is the Domain Name Services (DNS) properly configured? • Can you ping the server? • Are the DLLs for your connection and database drivers in the search path? Connectingtodatabases17-7, ControllingconnectionsFor more troubleshooting information, see the online SQL Links User’s Guide and your server documentation. Using ODBC An application can use ODBC data sources (for example, Btrieve). An ODBC driver connection requires • A vendor-supplied ODBC driver. • The Microsoft ODBC Driver Manager. • The BDE Administration utility. To set up a BDE alias for an ODBC driver connection, use the BDE Administration utility. For more information, see the BDE Administration utility’s online help file.

Disconnecting from a database server

There are two ways to disconnect a server from a database component: • Set the Connected property to False. • Call the Close method. Setting Connected to False calls Close. Close closes all open datasets and disconnects from the server. For example, the following code closes all active datasets for a database component and drops its connections: Database1.Connected := False; Note Close disconnects from a database server even if KeepConnection is True.

Closing datasets without disconnecting from a server

There may be times when you want to close all datasets without disconnecting from the database server. To close all open datasets without disconnecting from a server, follow these steps: 1 Set the database component’s KeepConnection property to True. 2 Call the database component’s CloseDataSets method.

Iterating through a database component’s datasets

A database component provides two properties that enable an application to iterate through all the datasets associated with the component: DataSets and DataSetCount. DataSets is an indexed array of all active datasets (TTable, TQuery, and TStoredProc) for a database component. An active dataset is one that is currently open. DataSetCount is a read-only integer value specifying the number of currently active datasets. 17-8Developer’ sGuide, UnderstandingdatabaseandsessioncomponentinteractionsYou can use DataSets with DataSetCount to cycle through all currently active datasets in code. For example, the following code cycles through all active datasets to set the CachedUpdates property for each dataset of type TTable to True: var I: Integer; begin for I := 0 to DataSetCount - 1 do if DataSets[I] is TTable then DataSets[I].CachedUpdates := True; end;

Understanding database and session component interactions

In general, session component properties, such as KeepConnection, provide global, default behaviors that apply to all temporary database components created as needed at runtime. Session methods apply somewhat differently. TSession methods affect all database components, regardless of database component status. For example, the session method DropConnections closes all datasets belonging to a session’s database components, and then drops all database connections, even if the KeepConnection property for individual database components is True. Database component methods apply only to the datasets associated with a given database component. For example, suppose the database component Database1 is associated with the default session. Database1.CloseDataSets() closes only those datasets associated with Database1. Open datasets belonging to other database components within the default session remain open.

Using database components in data modules

You can safely place database components in data modules. If you put a data module that contains a database component into the Object Repository, however, and you want other users to be able to inherit from it, you must set the HandleShared property of the database component to True to prevent global name space conflicts. Connectingtodatabases17-9, 17-10Developer’ sGuide,

Chapter

Chapter 18Understanding datasets In Delphi, the fundamental unit for accessing data is the dataset family of objects. Your application uses datasets for all database access. Generally, a dataset object represents a specific table belonging to a database, or it represents a query or stored procedure that accesses a database. All dataset objects that you will use in your database applications descend from the virtualized dataset object, TDataSet, and they inherit data fields, properties, events, and methods from TDataSet. This chapter describes the functionality of TDataSet that is inherited by the dataset objects you will use in your database applications. You need to understand this shared functionality to use any dataset object. Figure 18.1 illustrates the hierarchical relationship of all the dataset components: Figure 18.1 Delphi dataset hierarchy TDataSet TNestedTable TClientDataSet TBDEDataSet TQuery TDBDataSet TStoredProc TTableUnderstandingdatasets18-1, WhatisTDataSet?

What is TDataSet?

TDataSet is the ancestor for all dataset objects you use in your applications. It defines a set of data fields, properties, events, and methods shared by all dataset objects. TDataSet is a virtualized dataset, meaning that many of its properties and methods are virtual or abstract. A virtual method is a function or procedure declaration where the implementation of that method can be (and usually is) overridden in descendant objects. An abstract method is a function or procedure declaration without an actual implementation. The declaration is a prototype that describes the method (and its parameters and return type, if any) that must be implemented in all descendant dataset objects, but that might be implemented differently by each of them. Because TDataSet contains abstract methods, you cannot use it directly in an application without generating a runtime error. Instead, you either create instances of TDataSet’s descendants, such as TTable, TQuery, TStoredProc, and TClientDataSet, and use them in your application, or you derive your own dataset object from TDataSet or its descendants and write implementations for all its abstract methods. Nevertheless, TDataSet defines much that is common to all dataset objects. For example, TDataSet defines the basic structure of all datasets: an array of TField components that correspond to actual columns in one or more database tables, lookup fields provided by your application, or calculated fields provided by your application. For more information about TField components, see Chapter 19, “Working with field components.” The following topics are discussed in this chapter: • Types of datasets • Opening and closing datasets • Determining and setting dataset states • Navigating datasets • Searching datasets • Displaying and editing a subset of data using filters • Modifying data • Using dataset events • Using BDE-enabled datasets

Types of datasets

To understand the concepts common to all dataset objects, and to prepare for developing your own custom dataset objects that bypass the Borland Database Engine (BDE), read this chapter. To develop traditional, two-tier client/server database applications using the Borland Database Engine (BDE), see “Overview of BDE-enablement” discussed later in this chapter. That section introduces TBDEDataSet and TDBDataSet, and focuses 18-2Developer’ sGuide, Openingandclosingdatasetson the shared features of TQuery, TStoredProc, and TTable, the dataset components used most commonly in all database applications. If you have Delphi Client/Server or Delphi Enterprise, you can develop multi-tier database applications using distributed datasets. To learn about working with client datasets in multi-tiered applications, see Chapter 15, “Creating multi-tiered applications.” That chapter discusses how to use TClientDataSet and how to connect the client to an application server.

Opening and closing datasets

To read or write data in a table or through a query, an application must first open a dataset. You can open a dataset in two ways, • Set the Active property of the dataset to True, either at design time in the Object Inspector, or in code at runtime: CustTable.Active := True; • Call the Open method for the dataset at runtime, CustQuery.Open; You can close a dataset in two ways, • Set the Active property of the dataset to False, either at design time in the Object Inspector, or in code at runtime, CustQuery.Active := False; • Call the Close method for the dataset at runtime, CustTable.Close; You may need to close a dataset when you want to change certain of its properties, such as TableName on a TTable component. At runtime, you may also want to close a dataset for other reasons specific to your application.

Determining and setting dataset states

The state—or mode—of a dataset determines what can be done to its data. For example, when a dataset is closed, its state is dsInactive, meaning that nothing can be done to its data. At runtime, you can examine a dataset’s read-only State property to determine its current state. The following table summarizes possible values for the State property and what they mean: Table 18.1 Values for the dataset State property Value State Meaning dsInactive Inactive DataSet closed. Its data is unavailable. dsBrowse Browse DataSet open. Its data can be viewed, but not changed. This is the default state of an open dataset. Understandingdatasets18-3, DeterminingandsettingdatasetstatesTable 18.1 Values for the dataset State property (continued) Value State Meaning dsEdit Edit DataSet open. The current row can be modified. dsInsert Insert DataSet open. A new row is inserted or appended. dsSetKey SetKey TTable and TClientDataSet only. DataSet open. Enables setting of ranges and key values used for ranges and GotoKey operations. dsCalcFields CalcFields DataSet open. Indicates that an OnCalcFields event is under way. Prevents changes to fields that are not calculated. dsCurValue CurValue Internal use only. dsNewValue NewValue Internal use only. dsOldValue OldValue Internal use only. dsFilter Filter DataSet open. Indicates that a filter operation is under way. A restricted set of data can be viewed, and no data can be changed. When an application opens a dataset, it appears by default in dsBrowse mode. The state of a dataset changes as an application processes data. An open dataset changes from one state to another based on either the • code in your application, or • built-in behavior of data-related components. To put a dataset into dsBrowse, dsEdit, dsInsert, or dsSetKey states, call the method corresponding to the name of the state. For example, the following code puts CustTable into dsInsert state, accepts user input for a new record, and writes the new record to the database: CustTable.Insert; { Your application explicitly sets dataset state to Insert } AddressPromptDialog.ShowModal; if AddressPromptDialog.ModalResult := mrOK then CustTable.Post; { Delphi sets dataset state to Browse on successful completion } else CustTable.Cancel; {Delphi sets dataset state to Browse on cancel } This example also illustrates that the state of a dataset automatically changes to dsBrowse when • The Post method successfully writes a record to the database. (If Post fails, the dataset state remains unchanged.) • The Cancel method is called. Some states cannot be set directly. For example, to put a dataset into dsInactive state, set its Active property to False, or call the Close method for the dataset. The following statements are equivalent: CustTable.Active := False; CustTable.Close; The remaining states (dsCalcFields, dsCurValue, dsNewValue, dsOldValue, and dsFilter) cannot be set by your application. Instead, the state of the dataset changes automatically to these values as necessary. For example, dsCalcFields is set when a 18-4Developer’ sGuide, Determiningandsettingdatasetstatesdataset’s OnCalcFields event is called. When the OnCalcFields event finishes, the dataset is restored to its previous state. Note Whenever a dataset’s state changes, the OnStateChange event is called for any data source components associated with the dataset. For more information about data source components and OnStateChange, see “Using data sources” on page 25-5. The following sections provide overviews of each state, how and when states are set, how states relate to one another, and where to go for related information, if applicable.

Inactivating a dataset

A dataset is inactive when it is closed. You cannot access records in a closed dataset. At design time, a dataset is closed until you set its Active property to True. At runtime, a dataset is initially closed until an application opens it by calling the Open method, or by setting the Active property to True. When you open an inactive dataset, its state automatically changes to the dsBrowse state. The following diagram illustrates the relationship between these states and the methods that set them. Figure 18.2 Relationship of Inactive and Browse states Close Inactive Browse Open To make a dataset inactive, call its Close method. You can write BeforeClose and AfterClose event handlers that respond to the Close method for a dataset. For example, if a dataset is in dsEdit or dsInsert modes when an application calls Close, you should prompt the user to post pending changes or cancel them before closing the dataset. The following code illustrates such a handler: procedure CustTable.VerifyBeforeClose(DataSet: TDataSet) begin if (CustTable.State = dsEdit) or (CustTable.State = dsInsert) then begin if MessageDlg('Post changes before closing?', mtConfirmation, mbYesNo, 0) = mrYes then CustTable.Post; else CustTable.Cancel; end; end; To associate a procedure with the BeforeClose event for a dataset at design time: 1 Select the table in the data module (or form). 2 Click the Events page in the Object Inspector. 3 Enter the name of the procedure for the BeforeClose event (or choose it from the drop-down list). Understandingdatasets18-5, Determiningandsettingdatasetstates

Browsing a dataset

When an application opens a dataset, the dataset automatically enters dsBrowse state. Browsing enables you to view records in a dataset, but you cannot edit records or insert new records. You mainly use dsBrowse to scroll from record to record in a dataset. For more information about scrolling from record to record, see “Navigating datasets” on page 18-9. From dsBrowse all other dataset states can be set. For example, calling the Insert or Append methods for a dataset changes its state from dsBrowse to dsInsert (note that other factors and dataset properties, such as CanModify, may prevent this change). Calling SetKey to search for records puts a dataset in dsSetKey mode. For more information about inserting and appending records in a dataset, see “Modifying data” on page 18-20. Two methods associated with all datasets can return a dataset to dsBrowse state. Cancel ends the current edit, insert, or search task, and always returns a dataset to dsBrowse state. Post attempts to write changes to the database, and if successful, also returns a dataset to dsBrowse state. If Post fails, the current state remains unchanged. The following diagram illustrates the relationship of dsBrowse both to the other dataset modes you can set in your applications, and the methods that set those modes. Figure 18.3 Relationship of Browse to other dataset states dsInactive Insert Open Close Append Edit dsInsert dsBrowse dsEdit Post Post (success) Post (success) Post (unsuccessful) Cancel Cancel (unsuccessful) Delete Delete SetKey, EditKey Post, Cancel, SetRange GotoKey, FindKey ApplyRange, CancelRange dsSetKey 18-6Developer’ sGuide, Determiningandsettingdatasetstates

Enabling dataset editing

A dataset must be in dsEdit mode before an application can modify records. In your code you can use the Edit method to put a dataset into dsEdit mode if the read-only CanModify property for the dataset is True. CanModify is True if the database underlying a dataset permits read and write privileges. On forms in your application, some data-aware controls can automatically put a dataset into dsEdit state if: • The control’s ReadOnly property is False (the default), • The AutoEdit property of the data source for the control is True, and • CanModify is True for the dataset. Important For TTable components, if the ReadOnly property is True, CanModify is False, preventing editing of records. Similarly, for TQuery components, if the RequestLive property is False, CanModify is False. Note Even if a dataset is in dsEdit state, editing records may not succeed for SQL-based databases if your application user does not have proper SQL access privileges. You can return a dataset from dsEdit state to dsBrowse state in code by calling the Cancel, Post, or Delete methods. Cancel discards edits to the current field or record. Post attempts to write a modified record to the dataset, and if it succeeds, returns the dataset to dsBrowse. If Post cannot write changes, the dataset remains in dsEdit state. Delete attempts to remove the current record from the dataset, and if it succeeds, returns the dataset to dsBrowse state. If Delete fails, the dataset remains in dsEdit state. Data-aware controls for which editing is enabled automatically call Post when a user executes any action that changes the current record (such as moving to a different record in a grid) or that causes the control to lose focus (such as moving to a different control on the form). For a complete discussion of editing fields and records in a dataset, see “Modifying data” on page 18-20.

Enabling insertion of new records

A dataset must be in dsInsert mode before an application can add new records. In your code you can use the Insert or Append methods to put a dataset into dsInsert mode if the read-only CanModify property for the dataset is True. CanModify is True if the database underlying a dataset permits read and write privileges. On forms in your application, the data-aware grid and navigator controls can put a dataset into dsInsert state if • The control’s ReadOnly property is False (the default), • The AutoEdit property of the data source for the control is True, and • CanModify is True for the dataset. Important For TTable components, if the ReadOnly property is True, CanModify is False, preventing editing of records. Similarly, for TQuery components, if the RequestLive property is False, CanModify is False. Understandingdatasets18-7, DeterminingandsettingdatasetstatesNote Even if a dataset is in dsInsert state, inserting records may not succeed for SQL-based databases if your application user does not have proper SQL access privileges. You can return a dataset from dsInsert state to dsBrowse state in code by calling the Cancel, Post, or Delete methods. Delete and Cancel discard the new record. Post attempts to write the new record to the dataset, and if it succeeds, returns the dataset to dsBrowse. If Post cannot write the record, the dataset remains in dsInsert state. Data-aware controls for which inserting is enabled automatically call Post when a user executes any action that changes the current record (such as moving to a different record in a grid). For more discussion of inserting and appending records in a dataset, see “Modifying data” on page 18-20.

Enabling index-based searches and ranges on tables

You can search against any dataset using the Locate and Lookup methods of TDataSet. TTable components, however, provide an additional family of GotoKey and FindKey methods that enable you to search for records based on an index for the table. To use these methods on table components, the component must be in dsSetKey mode. dsSetKey mode applies only to TTable components. You put a dataset into dsSetKey mode with the SetKey method at runtime. The GotoKey, GotoNearest, FindKey, and FindNearest methods, which carry out searches, returns the dataset to dsBrowse state upon completion of the search. For more information about searching a table based on its index, see “Searching for records based on indexed fields” on page 20-6. You can temporarily view and edit a subset of data for any dataset by using filters. For more information about filters, see “Displaying and editing a subset of data using filters” on page 18-16. TTable components also support an additional way to access a subset of available records, called ranges. To create and apply a range to a table, a table must be in dsSetKey mode. For more information about using ranges, see “Working with a subset of data” on page 20-11.

Calculating fields

Delphi puts a dataset into dsCalcFields mode whenever an application calls the dataset’s OnCalcFields event handler. This state prevents modifications or additions to the records in a dataset except for the calculated fields the handler is designed to modify. The reason all other modifications are prevented is because OnCalcFields uses the values in other fields to derive values for calculated fields. Changes to those other fields might otherwise invalidate the values assigned to calculated fields. When the OnCalcFields handler finishes, the dataset is returned to dsBrowse state. For more information about creating calculated fields and OnCalcFields event handlers, see “Using OnCalcFields” on page 18-25. 18-8Developer’ sGuide, Navigatingdatasets

Filtering records

Delphi puts a dataset into dsFilter mode whenever an application calls the dataset’s OnFilterRecord event handler. This state prevents modifications or additions to the records in a dataset during the filtering process so that the filter request is not invalidated. For more information about filtering, see “Displaying and editing a subset of data using filters” on page 18-16. When the OnFilterRecord handler finishes, the dataset is returned to dsBrowse state.

Updating records

When performing cached update operations, Delphi may put the dataset into dsNewValue, dsOldValue, or dsCurValue states temporarily. These states indicate that the corresponding properties of a field component (NewValue, OldValue, and CurValue, respectively) are being accessed, usually in an OnUpdateError event handler. Your applications cannot see or set these states. For more information about using cached updates, see Chapter 24, “Working with cached updates.”

Navigating datasets

Each active dataset has a cursor, or pointer, to the current row in the dataset. The current row in a dataset is the one whose values can be manipulated by edit, insert, and delete methods, and the one whose field values currently show in single-field, data-aware controls on a form, such as TDBEdit, TDBLabel, and TDBMemo. You can change the current row by moving the cursor to point at a different row. The following table lists methods you can use in application code to move to different records: Table 18.2 Navigational methods of datasets Method Description First Moves the cursor to the first row in a dataset. Last Moves the cursor to the last row in a dataset. Next Moves the cursor to the next row in a dataset. Prior Moves the cursor to the previous row in a dataset. MoveBy Moves the cursor a specified number of rows forward or back in a dataset. The data-aware, visual component TDBNavigator encapsulates these methods as buttons that users can click to move among records at runtime. For more information about the navigator component, see Chapter 25, “Using data controls.” Understandingdatasets18-9, NavigatingdatasetsIn addition to these methods, the following table describes two Boolean properties of datasets that provide useful information when iterating through the records in a dataset. Table 18.3 Navigational properties of datasets Property Description Bof (Beginning-of-file) True: the cursor is at the first row in the dataset. false: the cursor is not known to be at the first row in the dataset. Eof (End-of-file) True: the cursor is at the last row in the dataset. false: the cursor is not known to be at the last row in the dataset.

Using the First and Last methods

The First method moves the cursor to the first row in a dataset and sets the Bof property to True. If the cursor is already at the first row in the dataset, First does nothing. For example, the following code moves to the first record in CustTable: CustTable.First; The Last method moves the cursor to the last row in a dataset and sets the Eof property to True. If the cursor is already at the last row in the dataset, Last does nothing. The following code moves to the last record in CustTable: CustTable.Last; Note While there may be programmatic reasons to move to the first or last rows in a dataset without user intervention, you should enable your users to navigate from record to record using the TDBNavigator component. The navigator component contains buttons that when active and visible enables a user to move to the first and last rows of an active dataset. The OnClick events for these buttons call the First and Last methods of the dataset. For more information about making effective use of the navigator component, see Chapter 25, “Using data controls.”

Using the Next and Prior methods

The Next method moves the cursor forward one row in the dataset and sets the Bof property to False if the dataset is not empty. If the cursor is already at the last row in the dataset when you call Next, nothing happens. For example, the following code moves to the next record in CustTable: CustTable.Next; The Prior method moves the cursor back one row in the dataset, and sets Eof to False if the dataset is not empty. If the cursor is already at the first row in the dataset when you call Prior, Prior does nothing. For example, the following code moves to the previous record in CustTable: CustTable.Prior; 18-10Developer’ sGuide, Navigatingdatasets

Using the MoveBy method

MoveBy enables you to specify how many rows forward or back to move the cursor in a dataset. Movement is relative to the current record at the time that MoveBy is called. MoveBy also sets the Bof and Eof properties for the dataset as appropriate. This function takes an integer parameter, the number of records to move. Positive integers indicate a forward move and negative integers indicate a backward move. MoveBy returns the number of rows it moves. If you attempt to move past the beginning or end of the dataset, the number of rows returned by MoveBy differs from the number of rows you requested to move. This is because MoveBy stops when it reaches the first or last record in the dataset. The following code moves two records backward in CustTable: CustTable.MoveBy(-2); Note If you use MoveBy in your application and you work in a multi-user database environment, keep in mind that datasets are fluid. A record that was five records back a moment ago may now be four, six, or even an unknown number of records back because several users may be simultaneously accessing the database and changing its data.

Using the Eof and Bof properties

Two read-only, runtime properties, Eof (End-of-file) and Bof(Beginning-of-file), are useful for controlling dataset navigation, particularly when you want to iterate through all records in a dataset. Eof When Eof is True, it indicates that the cursor is unequivocally at the last row in a dataset. Eof is set to True when an application • Opens an empty dataset. • Calls a dataset’s Last method. • Calls a dataset’s Next method, and the method fails (because the cursor is currently at the last row in the dataset. • Calls SetRange on an empty range or dataset. Eof is set to False in all other cases; you should assume Eof is False unless one of the conditions above is met and you test the property directly. Eof is commonly tested in a loop condition to control iterative processing of all records in a dataset. If you open a dataset containing records (or you call First) Eof is False. To iterate through the dataset a record at a time, create a loop that terminates when Eof is True. Inside the loop, call Next for each record in the dataset. Eof remains True until you call Next when the cursor is already on the last record. Understandingdatasets18-11, NavigatingdatasetsThe following code illustrates one way you might code a record-processing loop for a dataset called CustTable: CustTable.DisableControls; try CustTable.First; { Go to first record, which sets EOF False } while not CustTable.EOF do { Cycle until EOF is True } begin { Process each record here } ƒ CustTable.Next; { EOF False on success; EOF True when Next fails on last record } end; finally CustTable.EnableControls; end; Tip This example also demonstrates how to disable and enable data-aware visual controls tied to a dataset. If you disable visual controls during dataset iteration, it speeds processing because Delphi does not have to update the contents of the controls as the current record changes. After iteration is complete, controls should be enabled again to update them with values for the new current row. Note that enabling of the visual controls takes place in the finally clause of a try...finally statement. This guarantees that even if an exception terminates loop processing prematurely, controls are not left disabled.

Bof

When Bof is True, it indicates that the cursor is unequivocally at the first row in a dataset. Bof is set to True when an application • Opens a dataset • Calls a dataset’s First method • Calls a dataset’s Prior method, and the method fails (because the cursor is currently at the first row in the dataset • Calls SetRange on an empty range or dataset Bof is set to False in all other cases; you should assume Bof is False unless one of the conditions above is met and you test the property directly. LikeEof, Bof can be in a loop condition to control iterative processing of records in a dataset. The following code illustrates one way you might code a record-processing loop for a dataset called CustTable: CustTable.DisableControls; { Speed up processing; prevent screen flicker } try while not CustTable.BOF do { Cycle until BOF is True } begin { Process each record here } ƒ CustTable.Prior; { BOF False on success; BOF True when Prior fails on first record } end; finally CustTable.EnableControls; { Display new current row in controls } end; 18-12Developer’ sGuide, Navigatingdatasets

Marking and returning to records

In addition to moving from record to record in a dataset (or moving from one record to another by a specific number of records), it is often also useful to mark a particular location in a dataset so that you can return to it quickly when desired. TDataSet and its descendants implement a bookmarking feature that enables you to tag records and return to them later. The bookmarking feature consists of a Bookmark property and five bookmark methods. The Bookmark property indicates which bookmark among any number of bookmarks in your application is current. Bookmark is a string that identifies the current bookmark. Each time you add another bookmark, it becomes the current bookmark. TDataSet implements virtual bookmark methods. While these methods ensure that any dataset object derived from TDataSet returns a value if a bookmark method is called, the return values are merely defaults that do not keep track of the current location. Descendants of TDataSet, such as TBDEDataSet, reimplement the bookmark methods to return meaningful values as described in the following list: • BookmarkValid, for determining if a specified bookmark is in use. • CompareBookmarks, to test two bookmarks to see if they are the same. • GetBookmark, to allocate a bookmark for your current position in the dataset. • GotoBookmark, to return to a bookmark previously created by GetBookmark • FreeBookmark, to free a bookmark previously allocated by GetBookmark. To create a bookmark, you must declare a variable of type TBookmark in your application, then call GetBookmark to allocate storage for the variable and set its value to a particular location in a dataset. The TBookmark variable is a pointer (void *). Before calling GotoBookmark to move to a specific record, you can call BookmarkValid to determine if the bookmark points to a record. BookmarkValid returns True if a specified bookmark points to a record. In TDataSet, BookmarkValid is a virtual method that always returns False, indicating that the bookmark is not valid. TDataSet descendants reimplement this method to provide a meaningful return value. You can also call CompareBookmarks to see if a bookmark you want to move to is different from another (or the current) bookmark. TDataSet.CompareBookmarks always returns 0, indicating that the bookmarks are identical. TDataSet descendants reimplement this method to provide a meaningful return value. When passed a bookmark, GotoBookmark moves the cursor for the dataset to the location specified in the bookmark. TDataSet.GotoBookmark calls an internal pure virtual method which generates a runtime error if called. TDataSet descendants reimplement this method to provide a meaningful return value. FreeBookmark frees the memory allocated for a specified bookmark when you no longer need it. You should also call FreeBookmark before reusing an existing bookmark. Understandingdatasets18-13, SearchingdatasetsThe following code illustrates one use of bookmarking: procedure DoSomething (const Tbl: TTable) var Bookmark: TBookmark; begin Bookmark := Tbl.GetBookmark; { Allocate memory and assign a value } Tbl.DisableControls; { Turn off display of records in data controls } try Tbl.First; { Go to first record in table } while not Tbl.EOF do {Iterate through each record in table } begin { Do your processing here } ƒ Tbl.Next; end; finally Tbl.GotoBookmark(Bookmark); Tbl.EnableControls; { Turn on display of records in data controls, if necessary } Tbl.FreeBookmark(Bookmark); {Deallocate memory for the bookmark } end; end; Before iterating through records, controls are disabled. Should an error occur during iteration through records, the finally clause ensures that controls are always enabled and that the bookmark is always freed even if the loop terminates prematurely.

Searching datasets

You can search any dataset for specific records using the generic search methods Locate and Lookup. These methods enable you to search on any type of columns in any dataset.

Using Locate

Locate moves the cursor to the first row matching a specified set of search criteria. In its simplest form, you pass Locate the name of a column to search, a field value to match, and an options flag specifying whether the search is case-insensitive or if it can use partial-key matching. For example, the following code moves the cursor to the first row in the CustTable where the value in the Company column is “Professional Divers, Ltd.”: var LocateSuccess: Boolean; SearchOptions: TLocateOptions; begin SearchOptions := [loPartialKey]; LocateSuccess := CustTable.Locate('Company', 'Professional Divers, Ltd.', SearchOptions); end; 18-14Developer’ sGuide, SearchingdatasetsIf Locate finds a match, the first record containing the match becomes the current record. Locate returns True if it finds a matching record, False if it does not. If a search fails, the current record does not change. The real power of Locate comes into play when you want to search on multiple columns and specify multiple values to search for. Search values are variants, which enables you to specify different data types in your search criteria. To specify multiple columns in a search string, separate individual items in the string with semicolons. Because search values are variants, if you pass multiple values, you must either pass a variant array type as an argument (for example, the return values from the Lookup method), or you must construct the variant array on the fly using the VarArrayOf function. The following code illustrates a search on multiple columns using multiple search values and partial-key matching: with CustTable do Locate('Company;Contact;Phone', VarArrayOf(['Sight Diver','P']), loPartialKey); Locate uses the fastest possible method to locate matching records. If the columns to search are indexed and the index is compatible with the search options you specify, Locate uses the index.

Using Lookup

Lookup searches for the first row that matches specified search criteria. If it finds a matching row, it forces the recalculation of any calculated fields and lookup fields associated with the dataset, then returns one or more fields from the matching row. Lookup does not move the cursor to the matching row; it only returns values from it. In its simplest form, you pass Lookup the name of field to search, the field value to match, and the field or fields to return. For example, the following code looks for the first record in the CustTable where the value of the Company field is “Professional Divers, Ltd.”, and returns the company name, a contact person, and a phone number for the company: var LookupResults: Variant; begin with CustTable do LookupResults := Lookup('Company', 'Professional Divers, Ltd.', 'Company; Contact; Phone'); end; Lookup returns values for the specified fields from the first matching record it finds. Values are returned as variants. If more than one return value is requested, Lookup returns a variant array. If there are no matching records, Lookup returns a Null variant. For more information about variant arrays, see the online help. The real power of Lookup comes into play when you want to search on multiple columns and specify multiple values to search for. To specify strings containing multiple columns or result fields, separate individual fields in the string items with semi-colons. Understandingdatasets18-15, DisplayingandeditingasubsetofdatausingfiltersBecause search values are variants, if you pass multiple values, you must either pass a variant array type as an argument (for example, the return values from the Lookup method), or you must construct the variant array on the fly using the VarArrayOf function. The following code illustrates a lookup search on multiple columns: var LookupResults: Variant; begin with CustTable do LookupResults := Lookup('Company; City', VarArrayOf(['Sight Diver', 'Christiansted']), 'Company; Addr1; Addr2; State; Zip'); end; Lookup uses the fastest possible method to locate matching records. If the columns to search are indexed, Lookup uses the index.

Displaying and editing a subset of data using filters

An application is frequently interested in only a subset of records within a dataset. For example, you may be interested in retrieving or viewing only those records for companies based in California in your customer database, or you may want to find a record that contains a particular set of field values. In each case, you can use filters to restrict an application’s access to a subset of all records in the dataset. A filter specifies conditions a record must meet to be displayed. Filter conditions can be stipulated in a dataset’s Filter property or coded into its OnFilterRecord event handler. Filter conditions are based on the values in any specified number of fields in a dataset whether or not those fields are indexed. For example, to view only those records for companies based in California, a simple filter might require that records contain a value in the State field of “CA”. Note Filters are applied to every record retrieved in a dataset. When you want to filter large volumes of data, it may be more efficient to use a query to restrict record retrieval, or to set a range on an indexed table rather than using filters.

Enabling and disabling filtering

Enabling filters on a dataset is a three-step process: 1 Create a filter. 2 Set filter options for string-based filter tests, if necessary. 3 Set the Filtered property to True. When filtering is enabled, only those records that meet the filter criteria are available to an application. Filtering is always a temporary condition. You can turn off filtering by setting the Filtered property to False. 18-16Developer’ sGuide, Displayingandeditingasubsetofdatausingfilters

Creating filters

There are two ways to create a filter for a dataset: • Specify simple filter conditions in the Filter property. Filter is especially useful for creating and applying filters at runtime. • Write an OnFilterRecord event handler for simple or complex filter conditions. With OnFilterRecord, you specify filter conditions at design time. Unlike the Filter property, which is restricted to a single string containing filter logic, an OnFilterRecord event can take advantage of branching and looping logic to create complex, multi-level filter conditions. The main advantage to creating filters using the Filter property is that your application can create, change, and apply filters dynamically, (for example, in response to user input). Its main disadvantages are that filter conditions must be expressible in a single text string, cannot make use of branching and looping constructs, and cannot test or compare its values against values not already in the dataset. The strengths of the OnFilterRecord event are that a filter can be complex and variable, can be based on multiple lines of code that use branching and looping constructs, and can test dataset values against values outside the dataset, such as the text in an edit box. The main weakness of using OnFilterRecord is that you set the filter at design time and it cannot be modified in response to user input. (You can, however, create several filter handlers and switch among them in response to general application conditions.) The following sections describe how to create filters using the Filter property and the OnFilterRecord event handler.

Setting the Filter property

To create a filter using the Filter property, set the value of the property to a string that contains the filter conditions. The string contains the filter’s test condition. For example, the following statement creates a filter that tests a dataset’s State field to see if it contains a value for the state of California: Dataset1.Filter := '''State'' = ''CA'''; You can also supply a value for Filter based on the text entered in a control. For example, the following statement assigns the text in an edit box to Filter: Dataset1.Filter := Edit1.Text; You can, of course, create a string based on both hard-coded text and data entered by a user in a control: Dataset1.Filter := '''State'' = ' + Edit1.Text; After you specify a value for Filter, to apply the filter to the dataset, set the Filtered property to True. Understandingdatasets18-17, DisplayingandeditingasubsetofdatausingfiltersYou can also compare field values to literals, and to constants using the following comparison and logical operators: Table 18.4 Comparison and logical operators that can appear in a filter Operator Meaning < Less than > Greater than >= Greater than or equal to <= Less than or equal to = Equal to <> Not equal to AND Tests two statements are both True NOT Tests that the following statement is not True OR Tests that at least one of two statements is True By using combinations of these operators, you can create fairly sophisticated filters. For example, the following statement checks to make sure that two test conditions are met before accepting a record for display: (Custno > 1400) AND (Custno < 1500); Note When filtering is on, user edits to a record may mean that the record no longer meets a filter’s test conditions. The next time the record is retrieved from the dataset, it may therefore “disappear.” If that happens, the next record that passes the filter condition becomes the current record.

Writing an OnFilterRecord event handler

A filter for a dataset is an event handler that responds to OnFilterRecord events generated by the dataset for each record it retrieves. At the heart of every filter handler is a test that determines if a record should be included in those that are visible to the application. To indicate whether a record passes the filter condition, your filter handler must set an Accept parameter to True to include a record, or False to exclude it. For example, the following filter displays only those records with the State field set to “CA”: procedure TForm1.Table1FilterRecord(DataSet: TDataSet; var Accept: Boolean); begin Accept := DataSet['State'] = 'CA'; end; When filtering is enabled, an OnFilterRecord event is generated for each record retrieved. The event handler tests each record, and only those that meet the filter’s conditions are displayed. Because the OnFilterRecord event is generated for every record in a dataset, you should keep the event handler as tightly-coded as possible to avoid adversely affecting the performance of your application. 18-18Developer’ sGuide, Displayingandeditingasubsetofdatausingfilters

Switching filter event handlers at runtime

You can code any number of filter event handlers and switch among them at runtime. To switch to a different filter event handler at runtime, assign the new event handler to the dataset’s OnFilterRecord property. For example, the following statements switch to an OnFilterRecord event handler called NewYorkFilter: DataSet1.OnFilterRecord := NewYorkFilter; Refresh;

Setting filter options

The FilterOptions property enables you to specify whether or not a filter that compares string-based fields accepts records based on partial comparisons and whether or not string comparisons are case-sensitive. FilterOptions is a set property that can be an empty set (the default), or that can contain either or both of the following values: Table 18.5 FilterOptions values Value Meaning foCaseInsensitive Ignore case when comparing strings. foPartialCompare Disable partial string matching (i.e., do not match strings ending with an asterisk (*)). For example, the following statements set up a filter that ignores case when comparing values in the State field: FilterOptions := [foCaseInsensitive]; Filter := '''State'' = ''CA''';

Navigating records in a filtered dataset

There are four dataset methods that enable you to navigate among records in a filtered dataset. The following table lists these methods and describes what they do: Table 18.6 Filtered dataset navigational methods Method Purpose FindFirst Move to the first record in the dataset that matches the current filter criteria. The search for the first matching record always begins at the first record in the unfiltered dataset. FindLast Move to the last record in the dataset that matches the current filter criteria. FindNext Moves from the current record in the filtered dataset to the next one. FindPrior Move from the current record in the filtered dataset to the previous one. For example, the following statement finds the first filtered record in a dataset: DataSet1.FindFirst; Understandingdatasets18-19, ModifyingdataProvided that you set the Filter property or create an OnFilterRecord event handler for your application, these methods position the cursor on the specified record whether or not filtering is currently enabled for the dataset. If you call these methods when filtering is not enabled, then they 1 Temporarily enable filtering. 2 Position the cursor on a matching record if one is found. 3 Disable filtering. Note If filtering is disabled and you do not set the Filter property or create an OnFilterRecord event handler, these methods do the same thing as First(), Last(), Next(), and Prior(). All navigational filter methods position the cursor on a matching record (if one if found) make that record the current one, and return True. If a matching record is not found, the cursor position is unchanged, and these methods return False. You can check the status of the Found property to wrap these calls, and only take action when Found is True. For example, if the cursor is already on the last matching record in the dataset, and you call FindNext, the method returns False, and the current record is unchanged.

Modifying data

You can use the following dataset methods to insert, update, and delete data: Table 18.7 Dataset methods for inserting, updating, and deleting data Method Description Edit Puts the dataset into dsEdit state if it is not already in dsEdit or dsInsert states. Append Posts any pending data, moves current record to the end of the dataset, and puts the dataset in dsInsert state. Insert Posts any pending data, and puts the dataset in dsInsert state. Post Attempts to post the new or altered record to the database. If successful, the dataset is put in dsBrowse state; if unsuccessful, the dataset remains in its current state. Cancel Cancels the current operation and puts the dataset in dsBrowse state. Delete Deletes the current record and puts the dataset in dsBrowse state.

Editing records

A dataset must be in dsEdit mode before an application can modify records. In your code you can use the Edit method to put a dataset into dsEdit mode if the read-only CanModify property for the dataset is True. CanModify is True if the table(s) underlying a dataset permits read and write privileges. On forms in your application, some data-aware controls can automatically put a dataset into dsEdit state if • The control’s ReadOnly property is False (the default), • The AutoEdit property of the data source for the control is True, and • CanModify is True for the dataset. 18-20Developer’ sGuide, ModifyingdataImportant For TTable components with the ReadOnly property set to True and TQuery components with the RequestLive property set to False, CanModify is False, preventing editing of records. Note Even if a dataset is in dsEdit state, editing records may not succeed for SQL-based databases if your application’s user does not have proper SQL access privileges. Once a dataset is in dsEdit mode, a user can modify the field values for the current record that appears in any data-aware controls on a form. Data-aware controls for which editing is enabled automatically call Post when a user executes any action that changes the current record (such as moving to a different record in a grid). If you provide a navigator component on your forms, users can cancel edits by clicking the navigator’s Cancel button. Cancelling edits returns a dataset to dsBrowse state. In code, you must write or cancel edits by calling the appropriate methods. You write changes by calling Post. You cancel them by calling Cancel. In code, Edit and Post are often used together. For example, with CustTable do begin Edit; FieldValues['CustNo'] := 1234; Post; end; In the previous example, the first line of code places the dataset in dsEdit mode. The next line of code assigns the number 1234 to the CustNo field of the current record. Finally, the last line writes, or posts, the modified record to the database. Note If the CachedUpdates property for a dataset is True, posted modifications are written to a temporary buffer. To write cached edits to the database, call the ApplyUpdates method for the dataset. For more information about cached updates, see Chapter 24, “Working with cached updates.”

Adding new records

A dataset must be in dsInsert mode before an application can add new records. In code, you can use the Insert or Append methods to put a dataset into dsInsert mode if the read-only CanModify property for the dataset is True. CanModify is True if the database underlying a dataset permits read and write privileges. On forms in your application, the data-aware grid and navigator controls can put a dataset into dsInsert state if • The control’s ReadOnly property is False (the default), and • CanModify is True for the dataset. Once a dataset is in dsInsert mode, a user or application can enter values into the fields associated with the new record. Except for the grid and navigational controls, there is no visible difference to a user between Insert and Append. On a call to Insert, an empty row appears in a grid above what was the current record. On a call to Append, the grid is scrolled to the last record in the dataset, an empty row appears atUnderstandingdatasets18-21, Modifyingdatathe bottom of the grid, and the Next and Last buttons are dimmed on any navigator component associated with the dataset. Data-aware controls for which inserting is enabled automatically call Post when a user executes any action that changes which record is current (such as moving to a different record in a grid). Otherwise you must call Post in your code. Post writes the new record to the database, or, if cached updates are enabled, Post writes the record to a buffer. To write cached inserts and appends to the database, call the ApplyUpdates method for the dataset. For more information about cached updates, see Chapter 24, “Working with cached updates.” Inserting records Insert opens a new, empty record before the current record, and makes the empty record the current record so that field values for the record can be entered either by a user or by your application code. When an application calls Post (or ApplyUpdates when cached updating is enabled), a newly inserted record is written to a database in one of three ways: • For indexed Paradox and dBASE tables, the record is inserted into the dataset in a position based on its index. • For unindexed tables, the record is inserted into the dataset at its current position. • For SQL databases, the physical location of the insertion is implementation- specific. If the table is indexed, the index is updated with the new record information. Appending records Append opens a new, empty record at the end of the dataset, and makes the empty record the current one so that field values for the record can be entered either by a user or by your application code. When an application calls Post (or ApplyUpdates when cached updating is enabled), a newly appended record is written to a database in one of three ways: • For indexed Paradox and dBASE tables, the record is inserted into the dataset in a position based on its index. • For unindexed tables, the record is added to the end of the dataset. • For SQL databases, the physical location of the append is implementation-specific. If the table is indexed, the index is updated with the new record information.

Deleting records

A dataset must be active before an application can delete records. Delete deletes the current record from a dataset and puts the dataset in dsBrowse mode. The record that followed the deleted record becomes the current record. If cached updates are enabled for a dataset, a deleted record is only removed from the temporary cache buffer until you call ApplyUpdates. 18-22Developer’ sGuide, ModifyingdataIf you provide a navigator component on your forms, users can delete the current record by clicking the navigator’s Delete button. In code, you must call Delete explicitly to remove the current record.

Posting data to the database

The Post method is central to a Delphi application’s interaction with a database table. Post writes changes to the current record to the database, but it behaves differently depending on a dataset’s state. • In dsEdit state, Post writes a modified record to the database (or buffer if cached updates is enabled). • In dsInsert state, Post writes a new record to the database (or buffer if cached updates is enabled). • In dsSetKey state, Post returns the dataset to dsBrowse state. Posting can be done explicitly, or implicitly as part of another procedure. When an application moves off the current record, Post is called implicitly. Calls to the First, Next, Prior, and Last methods perform a Post if the table is in dsEdit or dsInsert modes. The Append and Insert methods also implicitly post any pending data. Note The Close method does not call Post implicitly. Use the BeforeClose event to post any pending edits explicitly.

Canceling changes

An application can undo changes made to the current record at any time, if it has not yet directly or indirectly called Post. For example, if a dataset is in dsEdit mode, and a user has changed the data in one or more fields, the application can return the record back to its original values by calling the Cancel method for the dataset. A call to Cancel always returns a dataset to dsBrowse state. On forms, you can allow users to cancel edit, insert, or append operations by including the Cancel button on a navigator component associated with the dataset, or you can provide code for your own Cancel button on the form.

Modifying entire records

On forms, all data-aware controls except for grids and the navigator provide access to a single field in a record. In code, however, you can use the following methods that work with entire record structures provided that the structure of the database tables underlying the dataset is stable and does not change. The following table summarizes the methods available for working with entire records rather than individual fields in those records: Understandingdatasets18-23, ModifyingdataTable 18.8 Methods that work with entire records Method Description AppendRecord([array of Appends a record with the specified column values at the end of values]) a table; analogous to Append. Performs an implicit Post. InsertRecord([array of values]) Inserts the specified values as a record before the current cursor position of a table; analogous to Insert. Performs an implicit Post. SetFields([array of values]) Sets the values of the corresponding fields; analogous to assigning values to TFields. Application must perform an explicit Post. These method take an array of TVarRec values as an argument, where each value corresponds to a column in the underlying dataset. Use the ARRAYOFCONST macro to create these arrays. The values can be literals, variables, or NULL. If the number of values in an argument is less than the number of columns in a dataset, then the remaining values are assumed to be NULL. For unindexed datasets, AppendRecord adds a record to the end of the dataset and InsertRecord inserts a record after the current cursor position. For indexed tables, both methods places the record in the correct position in the table, based on the index. In both cases, the methods move the cursor to the record’s position. SetFields assigns the values specified in the array of parameters to fields in the dataset. To use SetFields, an application must first call Edit to put the dataset in dsEdit mode. To apply the changes to the current record, it must perform a Post. If you use SetFields to modify some, but not all fields in an existing record, you can pass NULL values for fields you do not want to change. If you do not supply enough values for all fields in a record, SetFields assigns NULL values to them. NULL values overwrite any existing values already in those fields. For example, suppose a database has a COUNTRY table with columns for Name, Capital, Continent, Area, and Population. If a TTable component called CountryTable were linked to the COUNTRY table, the following statement would insert a record into the COUNTRY table: CountryTable.InsertRecord(['Japan', 'Tokyo', 'Asia']); This statement does not specify values for Area and Population, so NULL values are inserted for them. The table is indexed on Name, so the statement would insert the record based on the alphabetic collation of “Japan”. To update the record, an application could use the following code: with CountryTable do begin if Locate('Name', 'Japan', loCaseInsensitive) then; begin Edit; SetFields(nil, nil, nil, 344567, 164700000); Post; end; end; 18-24Developer’ sGuide, UsingdataseteventsThis code assigns values to the Area and Population fields and then posts them to the database. The three NULL pointers act as place holders for the first three columns to preserve their current contents. Warning When using NULL pointers with SetFields to leave some field values untouched, be sure to cast the NULL to a void *. If you use NULL as a parameter without the cast, you will set the field to a blank value.

Using dataset events

Datasets have a number of events that enable an application to perform validation, compute totals, and perform other tasks. The events are listed in the following table. Table 18.9 Dataset events Event Description BeforeOpen, AfterOpen Called before/after a dataset is opened. BeforeClose, AfterClose Called before/after a dataset is closed. BeforeInsert, AfterInsert Called before/after a dataset enters Insert state. BeforeEdit, AfterEdit Called before/after a dataset enters Edit state. BeforePost, AfterPost Called before/after changes to a table are posted. BeforeCancel, AfterCancel Called before/after the previous state is canceled. BeforeDelete, AfterDelete Called before/after a record is deleted. OnNewRecord Called when a new record is created; used to set default values. OnCalcFields Called when calculated fields are calculated. For more information about events for the TDataSet component, see the online VCL Reference.

Aborting a method

To abort a method such as an Open or Insert, call the Abort procedure in any of the Before event handlers (BeforeOpen, BeforeInsert, and so on). For example, the following code requests a user to confirm a delete operation or else it aborts the call to Delete: procedure TForm1.TableBeforeDelete (Dataset: TDataset)begin if MessageDlg('Delete This Record?', mtConfirmation, mbYesNoCancel, 0) <> mrYes then Abort; end;

Using OnCalcFields

The OnCalcFields event is used to set the values of calculated fields. The AutoCalcFields property determines when OnCalcFields is called. Understandingdatasets18-25, UsingBDE- enableddatasetsIf AutoCalcFields is True, then OnCalcFields is called when • A dataset is opened. • Focus moves from one visual component to another, or from one column to another in a data-aware grid control and the current record has been modified. • A record is retrieved from the database. OnCalcFields is always called whenever a value in a non-calculated field changes, regardless of the setting of AutoCalcFields. Caution OnCalcFields is called frequently, so the code you write for it should be kept short. Also, if AutoCalcFields is True, OnCalcFields should not perform any actions that modify the dataset (or the linked dataset if it is part of a master-detail relationship), because this can lead to recursion. For example, if OnCalcFields performs a Post, and AutoCalcFields is True, then OnCalcFields is called again, leading to another Post, and so on. If AutoCalcFields is False, then OnCalcFields is not called when individual fields within a single record are modified. When OnCalcFields executes, a dataset is in dsCalcFields mode, so you cannot set the values of any fields other than calculated fields. After OnCalcFields is completed, the dataset returns to dsBrowse state.

Using BDE-enabled datasets

BDE-enabled datasets provide functionality to the dataset components that use the Borland Database Engine (BDE) to access data. Support for BDE-enablement occurs in TBDEDataSet, which is a direct descendant of TDataSet. Additional database and session control features occur in TDBDataSet, which is a direct descendant of TBDEDataSet. Figure 18.4 Dataset component hierarchy TDataSet TNestedTable TClientDataSet TBDEDataSet TQuery TDBDataSet TStoredProc TTable 18-26Developer’ sGuide, UsingBDE- enableddatasetsThis section introduces the dataset features provided by TBDEDataSet and TDBDataSet. It assumes you are already familiar with TDataSet discussed earlier in this chapter. For a general understanding of dataset components descended from TDataSet, see the beginning of this chapter. Note Although you need to understand the functionality provided by TBDEDataSet and TDBDataSet, unless you develop your own custom BDE-enabled datasets, you never use TBDEDataSet and TDBDataSet directly in your applications. Instead, you use the direct descendants of TDBDataSet: TQuery, TStoredProc, and TTable. For specific information about using TStoredProc, see Chapter 22, “Working with stored procedures.” For specific information about using TQuery, see Chapter 21, “Working with queries.” For specific information about TTable, see Chapter 20, “Working with tables.”

Overview of BDE-enablement

The TBDEDataSet component implements the abstract methods of TDataSet that control record navigation, indexing, and bookmarking. It also reimplements many of TDataSet’s virtual methods and events to take advantage of the BDE. The BDE-specific implementations of TDataSet’s features do not depart from the general description about using these features with TDataSet, so for more information about them, see at the beginning of this chapter. In addition to BDE-specific features common to all datasets, TBDEDataSet introduces new properties, events, and methods for handling BLOBs, cached updates, and communicating with a remote database server. TDBDataSet introduces a method and properties for handling database connections and associating a dataset with a BDE session. The following sections describe these features and point to other chapters in the Developer’s Guide that are also relevant to using them.

Handling database and session connections

The TDBDataSet component introduces the following properties and function for working with database and session connections: Table 18.10 TDBDataSet database and session properties and function Function or property Purpose CheckOpen function Determines if a database is open. Returns true if the connection is active, false otherwise. Database Identifies the database component with which the dataset is associated. DBHandle Specifies the BDE database handle for the database component specified in the Database property. Used only when making some direct BDE API calls. DBLocale Specifies the BDE locale information for the database component specified in the Database property. Used only when making some direct BDE API calls. Understandingdatasets18-27, UsingBDE- enableddatasetsTable 18.10 TDBDataSet database and session properties and function (continued) Function or property Purpose DBSession Specifies the BDE session handle for the session component specified by the SessionName property. Used only when making some direct BDE API calls. DatabaseName Specifies the BDE alias or database component name for the database used by this dataset. If the dataset is a Paradox or dBASE table, DatabaseName can be a full path specification for the database’s directory location. SessionName Specifies the session with which this dataset component is associated. If you use both database and session components with a dataset, the setting for SessionName should be the same as the database component’s SessionName property.

Using the DatabaseName and SessionName properties

Of the TDBDataSet database and session properties, the most commonly used are DatabaseName and SessionName. If you work with databases on a remote database server, such as Sybase, Oracle, or InterBase, your application usually maintains that connection through a TDatabase component. You should set the DatabaseName property of each dataset to match the name of the database component that establishes the database connection used by the dataset. If you do not use database components, DatabaseName should be set to a BDE alias (or, optionally, a full path specification for dBASE and Paradox). SessionName indicates the BDE session with which to associate a dataset. If you do not use explicit session components in your application, you do not have to provide a value for this property. It is supplied for you. If your application provides more than one session, you can set a dataset’s SessionName property to match the SessionName property of the appropriate session component in your application. If your application uses both multiple session components and one or more database components, the SessionName property for a dataset must match the SessionName property for the database component with which the dataset is associated. For more information about handling database connections with TDatabase, see Chapter 17, “Connecting to databases.” For more information about managing sessions with TSession and TSessionList, see Chapter 16, “Managing database sessions.”

Working with BDE handle properties

Unless you bypass the built-in functionality of dataset components and make direct API calls to the BDE, you do not need to use the DBHandle, DBLocale DBLocale, and DBSession properties. These properties are read-only properties that are automatically assigned to a dataset when it is connected to a database server through the BDE. These properties are provided as a resource for application developers who need to make direct API calls to BDE functions, some of which take handle parameters. For more information about the BDE API, see the online help file, BDE32.HLP. 18-28Developer’ sGuide, UsingBDE- enableddatasets

Working with a provider component

For dataset components that are part of an application server in a multi-tiered database application, TDBDataSet includes a Provider property. Provider is a read- only property that points to the IProvider interface which is used by the client application to communicate with the dataset in the application server. When a provider component specifies the dataset on the application server from which it requests data, and to which it sends client application updates, the dataset’s Provider property is automatically set to the provider’s IProvider interface. Provider components are only supplied in Client/Server Suite (TProvider) and Enterprise edition (TProvider and TEnteraProvider). For more information about creating and using provider components in a multi-tiered database application, see Chapter 15, “Creating multi-tiered applications.”

Working with cached updates

Cached updates enable you to retrieve data from a database, cache and edit it locally, and then apply the cached updates to the database as a unit. When cached updates are enabled, updates to a dataset (such as posting changes or deleting records) are stored in an internal cache instead of being written directly to the dataset’s underlying table. When changes are complete, your application calls a method that writes the cached changes to the database and clears the cache. Implementation of cached updating occurs in TBDEDataSet. The following table lists the properties, events, and methods for cached updating: Table 18.11 Properties, events, and methods for cached updates Property, event, or method Purpose CachedUpdates property Determines whether or not cached updates are in effect for the dataset. If true, cached updating is enabled. If false, cached updating is disabled. UpdateObject property Indicates the name of the TUpdateSQL component used to update datasets based on queries. UpdatesPending property Indicates whether or not the local cache contains updated records that need to be applied to the database. true indicates there are records to update. false indicates the cache is empty. UpdateRecordTypes property Indicates the kind of updated records to make visible to the application during the application of cached updates. UpdateStatus method Indicates if a record is unchanged, modified, inserted, or deleted. OnUpdateError event A developer-created procedure that handles update errors on a record-by-record basis. OnUpdateRecord event A developer-created procedure that processes updates on a record-by-record basis. ApplyUpdates method Applies records in the local cache to the database. CancelUpdates method Removes all pending updates from the local cache without applying them to the database. Understandingdatasets18-29, UsingBDE- enableddatasetsTable 18.11 Properties, events, and methods for cached updates (continued) Property, event, or method Purpose CommitUpdates method Clears the update cache following successful application of updates. FetchAll method Copies all database records to the local cache for editing and updating. RevertRecord method Undoes updates to the current record if updates are not yet applied on the server side. Using cached updates and coordinating them with other applications that access data in a multi-user environment is an advanced topic that is fully covered in Chapter 24, “Working with cached updates.”

Caching BLOBs

TBDEDataSet provides the CacheBlobs property to control whether BLOB fields are cached locally by the BDE when an application reads BLOB records. By default, CacheBlobs is true, meaning that the BDE caches a local copy of BLOB fields. Caching BLOBs improves application performance by enabling the BDE to store local copies of BLOBs instead of fetching them repeatedly from the database server as a user scrolls through records. In applications and environments where BLOBs are frequently updated or replaced, and a fresh view of BLOB data is more important than application performance, you can set CacheBlobs to false to ensure that your application always sees the latest version of a BLOB field. 18-30Developer’ sGuide,

Chapter

Chapter 19Working with field components This chapter describes the properties, events, and methods common to the TField object and its descendants. Descendants of TField represent individual database columns in datasets. This chapter also describes how to use descendant field components to control the display and editing of data in your applications. You never use a TField component directly in your applications. By default, when you first place a dataset in your application and open it, Delphi automatically assigns a dynamic, data-type-specific descendant of TField to represent each column in the database table(s). At design time, you can override dynamic field defaults by invoking the Fields editor to create persistent fields that replace these defaults. The following table lists each descendant field component, its standard purpose, and, where appropriate, the range of values it can represent: Table 19.1 Field components Component name Purpose TADTField An ADT (Abstract Data Type) field. TAggregateField A maintained aggregate in a client dataset. TArrayField An array field. TAutoIncField Whole number with a range of -2,147,483,648 to 2,147,483,647. Used in Paradox for fields whose values are automatically incremented. TBCDField Real number with a fixed number of decimal places, accurate to 18 digits. Range depends on the number of decimal places. TBooleanField True or False values. TBlobField Binary data: Theoretical maximum limit: 2GB. TBytesField Binary data: Theoretical maximum limit: 2GB. TCurrencyField Real numbers with a range of 5.0 * 10-324 to 1.7 * 10308. Used in Paradox for fields with two decimals of precision. TDataSetField Nested data set value. TDateField Date value. Workingwithfieldcomponents19-1, UnderstandingfieldcomponentsTable 19.1 Field components (continued) Component name Purpose TDateTimeField Date and time value. TFloatField Real numbers with a range of 5.0 * 10-324 to 1.7 * 10308. TBytesField Binary data: Maximum number of bytes: 255. TIntegerField Whole number with a range of -2,147,483,648 to 2,147,483,647. TLargeintField Whole number with a range of -263 to 2 63. TMemoField Text data: Theoretical maximum limit: 2GB. TNumericField Real numbers with a range of 3.4 * 10-4932 to 1.1 * 104932 TReferenceField A pointer to an object relational database object. TSmallintField Whole number with a range of -32,768 to 32,768. TStringField String data: Maximum size in bytes: 8192, including a null termination character. TTimeField Time value. TVarBytesField Binary data: Maximum number of bytes: 255. TWordField Whole numbers with a range of 0 to 65,535. This chapter discusses the properties and methods all field components inherit from TField. In many cases, TField declares or implements standard functionality that the descendant objects override. When several descendant objects share overridden functionality, that functionality is also described in this chapter and noted for your convenience. For complete information about individual field components, see the online VCL Reference.

Understanding field components

Like all Delphi data access components, field components are nonvisual. Field components are also not directly visible at design time. Instead they are associated with a dataset component and provide data-aware components such as TDBEdit and TDBGrid access to database columns through that dataset. Generally speaking, a single field component represents the characteristics of a single column in a database field, such as its data type and size. It also represents the field’s display characteristics, such as alignment, display format, and edit format. Finally, as you scroll from record to record within a dataset, a field component also enables you to view and change the value for that field in the current record. For example, a TFloatField component has four properties that directly affect the appearance of its data: Table 19.2 TFloatField properties that affect data display Property Purpose Alignment Specifies whether data is displayed left-aligned, centered, or right-aligned. DisplayWidth Specifies the number of digits to display in a control at one time. 19-2Developer’ sGuide, UnderstandingfieldcomponentsTable 19.2 TFloatField properties that affect data display (continued) Property Purpose DisplayFormat Specifies data formatting for display (such as how many decimal places to show). EditFormat Specifies how to display a value during editing. Field components have many properties in common with one another (such as DisplayWidth and Alignment), and they have properties specific to their data types (such as Precision for TFloatField). Each of these properties affect how data appears to an application’s users on a form. Some properties, such as Precision, can also affect what data values the user can enter in a control when modifying or entering data. All field components for a dataset are either dynamic (automatically generated for you based on the underlying structure of database tables), or persistent (generated based on specific field names and properties you set in the Fields editor). Dynamic and persistent fields have different strengths and are appropriate for different types of applications. The following sections describe dynamic and persistent fields in more detail and offer advice on choosing between them.

Dynamic field components

Dynamically generated field components are the default. In fact, all field components for any dataset start out as dynamic fields the first time you place a dataset on a data module, associate the dataset with a database, and open it. A field component is dynamic if it is created automatically based on the underlying physical characteristics of the columns in one or more database tables accessed by a dataset. Delphi generates one field component for each column in the underlying tables or query. The exact TField descendant created for each column in an underlying database table is determined by field type information received from the Borland Database Engine (BDE) or (in multi-tiered applications) a provider component. A field component’s type determines its properties and how data associated with that field is displayed in data-aware controls on a form. Dynamic fields are temporary. They exist only as long as a dataset is open. Each time you reopen a dataset that uses dynamic fields, Delphi rebuilds a completely new set of dynamic field components for it based on the current structure of the database tables underlying the dataset. If the columns in those database tables are changed, then the next time you open a dataset that uses dynamic field components, the automatically generated field components are also changed to match. Use dynamic fields in applications that must be flexible about data display and editing. For example, to create a database exploration tool like the SQL Explorer, you must use dynamic fields because every database table has different numbers and types of columns. You might also want to use dynamic fields in applications where user interaction with data mostly takes place inside grid components and you know that the database tables used by the application change frequently. Workingwithfieldcomponents19-3, UnderstandingfieldcomponentsTo use dynamic fields in an application: 1 Place datasets and data sources in a data module. 2 Associate the datasets with database tables and queries, and associate the data sources with the datasets. 3 Place data-aware controls in the application’s forms, add the data module to each uses clause for each form’s unit, and associate each data-aware control with a data source in the module. In addition, associate a field with each data-aware control that requires one. 4 Open the datasets. Aside from ease of use, dynamic fields can be limiting. Without writing code, you cannot change the display and editing defaults for dynamic fields, you cannot safely change the order in which dynamic fields are displayed, and you cannot prevent access to any fields in the dataset. You cannot create additional fields for the dataset, such as calculated fields or lookup fields, and you cannot override a dynamic field’s default data type. To gain control and flexibility over fields in your database applications, you need to invoke the Fields editor to create persistent field components for your datasets.

Persistent field components

By default, dataset fields are dynamic. Their properties and availability are automatically set and cannot be changed in any way. To gain control over a field’s properties and events so that you can set or change the field’s visibility or display characteristics at design time or runtime, create new fields based on existing fields in a dataset, or validate data entry, you must create persistent fields for the dataset. At design time, you can—and should—use the Fields editor to create persistent lists of the field components used by the datasets in your application. Persistent field component lists are stored in your application, and do not change even if the structure of a database underlying a dataset is changed. Creating persistent field components offers the following advantages. You can: • Restrict the fields in your dataset to a subset of the columns available in the underlying database. • Add field components to the list of persistent components. • Remove field components from the list of persistent components to prevent your application from accessing particular columns in an underlying database. • Define new fields—usually to replace existing fields—based on columns in the table or query underlying a dataset. • Define calculated fields that compute their values based on other fields in the dataset. • Define lookup fields that compute their values based on fields in other datasets. • Modify field component display and edit properties. 19-4Developer’ sGuide, CreatingpersistentfieldsApersistent field is one that Delphi generates based on field names and properties you specify in the Fields editor. Once you create persistent fields with the Fields editor, you can also create event handlers for them that respond to changes in data values and that validate data entries. Note When you create persistent fields for a dataset, only those fields you select are available to your application at design time and runtime. At design time, you can always choose Add Fields from the Fields editor to add or remove persistent fields for a dataset. All fields used by a single dataset are either persistent or dynamic. You cannot mix field types in a single dataset. If you create persistent fields for a dataset, and then want to revert to dynamic fields, you must remove all persistent fields from the dataset. For more information about dynamic fields, see “Dynamic field components” on page 19-3. Note One of the primary uses of persistent fields is to gain control over the appearance and display of data. You can also control data appearance in other ways. For example, you can use the Data Dictionary to assign field attributes to a field component. You can also control the appearance of columns in data-aware grids. For more information about the Data Dictionary, see “Creating attribute sets for field components” on page 19-14. To learn about controlling column appearance in grids, see “Creating a customized grid” on page 25-17.

Creating persistent fields

Persistent field components created with the Fields editor provide efficient, readable, and type-safe programmatic access to underlying data. Using persistent field components guarantees that each time your application runs, it always uses and displays the same columns, in the same order even if the physical structure of the underlying database has changed. Data-aware components and program code that rely on specific fields always work as expected. If a column on which a persistent field component is based is deleted or changed, Delphi generates an exception rather than running the application against a nonexistent column or mismatched data. To create persistent fields for a dataset: 1 Place a dataset in a data module. 2 Set the DatabaseName property for the dataset. 3 Set the TableName property (for a TTable), or the SQL property (for a TQuery). 4 Double-click the dataset component in the data module to invoke the Fields editor. The Fields editor contains a title bar, navigator buttons, and a list box. The title bar of the Fields editor displays both the name of the data module or form containing the dataset, and the name of the dataset itself. For example, if you open the Customers dataset in the CustomerData data module, the title bar displays ‘CustomerData.Customers,’ or as much of the name as fits. Below the title bar is a set of navigation buttons that enable you to scroll one-by- one through the records in an active dataset at design time, and to jump to the firstWorkingwithfieldcomponents19-5, Arrangingpersistentfieldsor last record. The navigation buttons are dimmed if the dataset is not active or if the dataset is empty. The list box displays the names of persistent field components for the dataset. The first time you invoke the Fields editor for a new dataset, the list is empty because the field components for the dataset are dynamic, not persistent. If you invoke the Fields editor for a dataset that already has persistent field components, you see the field component names in the list box. 5 Choose Add Fields from the Fields editor context menu. 6 Select the fields to make persistent in the Add Fields dialog box. By default, all fields are selected when the dialog box opens. Any fields you select become persistent fields. The Add Fields dialog box closes, and the fields you selected appear in the Fields editor list box. Fields in the Fields editor list box are persistent. If the dataset is active, note, too, that the Next and Last navigation buttons above the list box are enabled. From now on, each time you open the dataset, Delphi no longer creates dynamic field components for every column in the underlying database. Instead it only creates persistent components for the fields you specified. Each time you open the dataset, Delphi verifies that each non-calculated persistent field exists or can be created from data in the database. If it cannot, it raises an exception warning you that the field is not valid, and does not open the dataset.

Arranging persistent fields

The order in which persistent field components are listed in the Fields editor list box is the default order in which the fields appear in a data-aware grid component. You can change field order by dragging and dropping fields in the list box. To change the order of fields: 1 Select the fields. You can select and order one or more fields at a time. 2 Drag the fields to a new location. If you select a noncontiguous set of fields and drag them to a new location, they are inserted as a contiguous block. Within the block, the order of fields does not change. Alternatively, you can select the field, and use Ctrl+Up and Ctrl+Dn to change an individual field’s order in the list. 19-6Developer’ sGuide, Definingnewpersistentfields

Defining new persistent fields

Besides making existing dataset fields into persistent fields, you can also create special persistent fields as additions to or replacements of the other persistent fields in a dataset. The following table lists the types of additional fields you can create: Table 19.3 Special persistent field kinds Field kind Purpose Data Replaces an existing field (for example to change its data type, based on columns in the table or query underlying a dataset.) Calculated Displays values calculated at runtime by a dataset’s OnCalcFields event handler. InternalCalc Displays values calculated at runtime by a client dataset and stored with its data. Lookup Retrieve values from a specified dataset at runtime based on search criteria you specify. Aggregate Displays a summary value of the data in a set of records. These types of persistent fields are only for display purposes. The data they contain at runtime are not retained either because they already exist elsewhere in your database, or because they are temporary. The physical structure of the table and data underlying the dataset is not changed in any way. To create a new persistent field component, invoke the context menu for the Fields editor and choose New field. The New Field dialog box appears. The New Field dialog box contains three group boxes: Field properties, Field type, and Lookup definition. The Field type radio group enables you to specify the type of new field component to create. The default type is Data. If you choose Lookup, the Dataset and Source Fields edit boxes in the Lookup definition group box are enabled. You can also create Calculated fields, and if you’re working with a TClientDataSet component, you can also create InternalCalc fields. The Field properties group box enables you to enter general field component information. Enter the component’s field name in the Name edit box. The name you enter here corresponds to the field component’s FieldName property. Delphi uses this name to build a component name in the Component edit box. The name that appears in the Component edit box corresponds to the field component’s Name property and is only provided for informational purposes (Name contains the identifier by which you refer to the field component in your source code). Delphi discards anything you enter directly in the Component edit box. The Type combo box in the Field properties group enables you to specify the field component’s data type. You must supply a data type for any new field component you create. For example, to display floating-point currency values in a field, select Currency from the drop-down list. The Size edit box enables you to specify the maximum number of characters that can be displayed or entered in a string-based field, or the size of Bytes and VarBytes fields. For all other data types, Size is meaningless. Workingwithfieldcomponents19-7, DefiningnewpersistentfieldsThe Lookup definition group box is only used to create lookup fields. For more information, see “Defining a lookup field” on page 19-10.

Defining a data field

A data field replaces an existing field in a dataset. For example, for programmatic reasons you might want to replace a TSmallIntField with a TIntegerField. Because you cannot change a field’s data type directly, you must define a new field to replace it. Important Even though you define a new field to replace an existing field, the field you define must derive its data values from an existing column in a table underlying a dataset. To create a replacement data field for a field in a table underlying a dataset, follow these steps: 1 Remove the field from the list of persistent fields assigned for the dataset, and then choose New Field from the context menu. 2 In the New Field dialog box, enter the name of an existing field in the database table in the Name edit box. Do not enter a new field name. You are actually specifying the name of the field from which your new field will derive its data. 3 Choose a new data type for the field from the Type combo box. The data type you choose should be different from the data type of the field you are replacing. You cannot replace a string field of one size with a string field of another size. Note that while the data type should be different, it must be compatible with the actual data type of the field in the underlying table. 4 Enter the size of the field in the Size edit box, if appropriate. Size is only relevant for fields of type TStringField, TBytesField, and TVarBytesField. 5 Select Data in the Field type radio group if it is not already selected. 6 Choose OK. The New Field dialog box closes, the newly defined data field replaces the existing field you specified in Step 1, and the component declaration in the data module or form’s type declaration is updated. To edit the properties or events associated with the field component, select the component name in the Field editor list box, then edit its properties or events with the Object Inspector. For more information about editing field component properties and events, see “Setting persistent field properties and events” on page 19-12.

Defining a calculated field

A calculated field displays values calculated at runtime by a dataset’s OnCalcFields event handler. For example, you might create a string field that displays concatenated values from other fields. To create a calculated field in the New Field dialog box: 1 Enter a name for the calculated field in the Name edit box. Do not enter the name of an existing field. 2 Choose a data type for the field from the Type combo box. 19-8Developer’ sGuide, Definingnewpersistentfields3Enter the size of the field in the Size edit box, if appropriate. Size is only relevant for fields of type TStringField, TBytesField, and TVarBytesField. 4 Select Calculated in the Field type radio group. 5 Choose OK. The newly defined calculated field is automatically added to the end of the list of persistent fields in the Field editor list box, and the component declaration is automatically added to the form’s type declaration in the source code. 6 Place code that calculates values for the field in the OnCalcFieldsevent handler for the dataset. For more information about writing code to calculate field values, see “Programming a calculated field” on page 19-9. Note To edit the properties or events associated with the field component, select the component name in the Field editor list box, then edit its properties or events with the Object Inspector. For more information about editing field component properties and events, see “Setting persistent field properties and events” on page 19-12. If you are working with a client dataset or query component, you can also create an InternalCalc field. You create and program an internally calculated field just like you do a calculated field. For a client dataset, the significant difference between these types of calculated fields is that the values calculated for an InternalCalc field are stored and retrieved as part of the client dataset’s data. To create an InternalCalc field, select the InternalCalc radio button in the Field type group.

Programming a calculated field

After you define a calculated field, you must write code to calculate its value. Otherwise, it always has a null value. Code for a calculated field is placed in the OnCalcFields event for its dataset. To program a value for a calculated field: 1 Select the dataset component from the Object Inspector drop-down list. 2 Choose the Object Inspector Events page. 3 Double-click the OnCalcFields property to bring up or create a CalcFields procedure for the dataset component. 4 Write the code that sets the values and other properties of the calculated field as desired. For example, suppose you have created a CityStateZip calculated field for the Customers table on the CustomerData data module. CityStateZip should display a company’s city, state, and zip code on a single line in a data-aware control. To add code to the CalcFields procedure for the Customers table, select the Customers table from the Object Inspector drop-down list, switch to the Events page, and double-click the OnCalcFields property. Workingwithfieldcomponents19-9, DefiningnewpersistentfieldsThe TCustomerData.CustomersCalcFields procedure appears in the unit’s source code window. Add the following code to the procedure to calculate the field: CustomersCityStateZip.Value := CustomersCity.Value + ', ' + CustomersState.Value + ' ' + CustomersZip.Value;

Defining a lookup field

A lookup field is a read-only field that displays values at runtime based on search criteria you specify. In its simplest form, a lookup field is passed the name of an existing field to search on, a field value to search for, and a different field in a lookup dataset whose value it should display. For example, consider a mail-order application that enables an operator to use a lookup field to determine automatically the city and state that correspond to the zip code a customer provides. The column to search on might be called ZipTable.Zip, the value to search for is the customer’s zip code as entered in Order.CustZip, and the values to return would be those for the ZipTable.City and ZipTable.State columns of the record where the value of ZipTable.Zip matches the current value in the Order.CustZip field. To create a lookup field in the New Field dialog box: 1 Enter a name for the lookup field in the Name edit box. Do not enter the name of an existing field. 2 Choose a data type for the field from the Type combo box. 3 Enter the size of the field in the Size edit box, if appropriate. Size is only relevant for fields of type TStringField, TBytesField, and TVarBytesField. 4 Select Lookup in the Field type radio group. Selecting Lookup enables the Dataset and Key Fields combo boxes. 5 Choose from the Dataset combo box drop-down list the dataset in which to look up field values. The lookup dataset must be different from the dataset for the field component itself, or a circular reference exception is raised at runtime. Specifying a lookup dataset enables the Lookup Keys and Result Field combo boxes. 6 Choose from the Key Fields drop-down list a field in the current dataset for which to match values. To match more than one field, enter field names directly instead of choosing from the drop-down list. Separate multiple field names with semicolons. If you are using more than one field, you must use persistent field components. 7 Choose from the Lookup Keys drop-down list a field in the lookup dataset to match against the Source Fields field you specified in step 6. If you specified more than one key field, you must specify the same number of lookup keys. To specify more than one field, enter field names directly, separating multiple field names with semicolons. 8 Choose from the Result Field drop-down list a field in the lookup dataset to return as the value of the lookup field you are creating. 19-10Developer’ sGuide, DefiningnewpersistentfieldsWhen you design and run your application, lookup field values are determined before calculated field values are calculated. You can base calculated fields on lookup fields, but you cannot base lookup fields on calculated fields. You can use the LookupCache property to hone this behavior. LookupCache determines whether the values of a lookup field are cached in memory when a dataset is first opened, or looked up dynamically every time the current record in the dataset changes. Set LookupCache to true to cache the values of a lookup field when the LookupDataSet is unlikely to change and the number of distinct lookup values is small. Caching lookup values can speed performance, because the lookup values for every set of LookupKeyFields values are preloaded when the DataSet is opened. When the current record in the DataSet changes, the field object can locate its Value in the cache, rather than accessing the LookupDataSet. This performance improvement is especially dramatic if the LookupDataSet is on a network where access is slow. Tip You can use a lookup cache to provide lookup values programmatically rather than from a secondary dataset. Create a TLookupList object at runtime, and use its Add method to fill it with lookup values. Set the LookupList property of the lookup field to this TLookupList object and set its LookupCache property to true. If the other lookup properties of the field are not set, the field will use the supplied lookup list without overwriting it with values from a lookup dataset. If every record of DataSet has different values for KeyFields, the overhead of locating values in the cache can be greater than any performance benefit provided by the cache. The overhead of locating values in the cache increases with the number of distinct values that can be taken by KeyFields. If LookupDataSet is volatile, caching lookup values can lead to inaccurate results. Call tRefreshLookupLis to update the values in the lookup cache. RefreshLookupList regenerates the LookupList property, which contains the value of the LookupResultField for every set of LookupKeyFields values. When setting LookupCache at runtime, call RefreshLookupList to initialize the cache.

Defining an aggregate field

An aggregate field displays values from a maintained aggregate in a client dataset. An aggregate is a calculation that summarizes the data in a set of records. To create an aggregate field in the New Field dialog box: 1 Enter a name for the aggregate field in the Name edit box. Do not enter the name of an existing field. 2 Choose aggregate data type for the field from the Type combo box. 3 Select Aggregate in the Field type radio group. 4 Choose OK. The newly defined aggregate field is automatically added to the client dataset’s Aggregates is automatically updated to include the appropriate aggregate specification, and the component declaration is automatically added to the form’s type declaration in the source code. Workingwithfieldcomponents19-11, Settingpersistentfieldpropertiesandevents5Place the calculation for the aggregate in the ExprText property of the newly created aggregate field. For more information about defining an aggregate, see “Specifying aggregates” on page 23-9. Once a persistent TAggregateField is created, a TDBText control can be bound to the aggregate field. The TDBText control will then display the value of the aggregate field that is relevant to the current record of the underlying client data set.

Deleting persistent field components

Deleting a persistent field component is useful for accessing a subset of available columns in a table, and for defining your own persistent fields to replace a column in a table. To remove one or more persistent field components for a dataset: 1 Select the field(s) to remove in the Fields editor list box. 2 Press Del. Note You can also delete selected fields by invoking the context menu and choosing Delete. Fields you remove are no longer available to the dataset and cannot be displayed by data-aware controls. You can always re-create persistent field components that you delete by accident, but any changes previously made to its properties or events is lost. For more information, see “Creating persistent fields” on page 19-5. Note If you remove all persistent field components for a dataset, the dataset reverts to using dynamic field components for every column in the underlying database table.

Setting persistent field properties and events

You can set properties and customize events for persistent field components at design time. Properties control the way a field is displayed by a data-aware component, for example, whether it can appear in a TDBGrid, or whether its value can be modified. Events control what happens when data in a field is fetched, changed, set, or validated. To set the properties of a field component or write customized event handlers for it, select the component in the Fields editor, or select it from the component list in the Object Inspector.

Setting display and edit properties at design time

To edit the display properties of a selected field component, switch to the Properties page on the Object Inspector window. The following table summarizes display properties that can be edited. 19-12Developer’ sGuide, SettingpersistentfieldpropertiesandeventsTable 19.4 Field component properties Property Purpose Alignment Left justifies, right justifies, or centers a field contents within a data-aware component. ConstraintErrorMessage Specifies the text to display when edits clash with a constraint condition. CustomConstraint Specifies a local constraint to apply to data during editing. Currency Numeric fields only. true: displays monetary values. False (default): does not display monetary values. DisplayFormat Specifies the format of data displayed in a data-aware component. DisplayLabel Specifies the column name for a field in a data-aware grid component. DisplayWidth Specifies the width, in characters, of a grid column that display this field. EditFormat Specifies the edit format of data in a data-aware component. EditMask Limits data-entry in an editable field to specified types and ranges of characters, and specifies any special, non-editable characters that appear within the field (hyphens, parentheses, and so on). FieldKind Specifies the type of field to create. FieldName Specifies the actual name of a column in the table from which the field derives its value and data type. HasConstraints Indicates whether or not there are constraint conditions imposed on a field. ImportedConstraint Specifies an SQL constraint imported from the Data Dictionary or an SQL server. Index Specifies the order of the field in a dataset. LookupDataSet Specifies the table used to look up field values when Lookup is true. LookupKeyFields Specifies the field(s) in the lookup dataset to match when doing a lookup. LookupResultField Specifies the field in the lookup dataset from which to copy values into this field. MaxValue Numeric fields only. Specifies the maximum value a user can enter for the field. MinValue Numeric fields only. Specifies the minimum value a user can enter for the field. Name Specifies the component name of the field component within Delphi. Origin Specifies the name of the field as it appears in the underlying database. Precision Numeric fields only. Specifies the number of significant digits. ReadOnly True: Displays field values in data-aware components, but prevents editing. False (the default): Permits display and editing of field values. Size Specifies the maximum number of characters that can be displayed or entered in a string-based field, or the size, in bytes, of TBytesField and TVarBytesField fields. Tag Long integer bucket available for programmer use in every component as needed. Transliterate True (default): specifies that translation to and from the respective locales will occur as data is transferred between a dataset and a database. False: Locale translation does not occur. Visible True (the default): Permits display of field in a data-aware grid component. False: Prevents display of field in a data-aware grid component. User-defined components can make display decisions based on this property. Workingwithfieldcomponents19-13, SettingpersistentfieldpropertiesandeventsNot all properties are available for all field components. For example, a field component of type TStringField does not have Currency, MaxValue, or DisplayFormat properties, and a component of type TFloatField does not have a Size property. While the purpose of most properties is straightforward, some properties, such as Calculated, require additional programming steps to be useful. Others, such as DisplayFormat, EditFormat, and EditMask, are interrelated; their settings must be coordinated. For more information about using DisplayFormat, EditFormat, and EditMask, see “Controlling and masking user input” on page 19-15.

Setting field component properties at runtime

You can use and manipulate the properties of field component at runtime. For example, the following code sets the ReadOnly property for the CityStateZip field in the Customers table to True: CustomersCityStateZip.ReadOnly := True; And this statement changes field ordering by setting the Index property of the CityStateZip field in the Customers table to 3: CustomersCityStateZip.Index := 3;

Creating attribute sets for field components

When several fields in the datasets used by your application share common formatting properties (such as Alignment, DisplayWidth, DisplayFormat, EditFormat, MaxValue, MinValue, and so on), it is more convenient to set the properties for a single field, then store those properties as an attribute set in the Data Dictionary. Attribute sets stored in the data dictionary can be easily applied to other fields. To create an attribute set based on a field component in a dataset: 1 Double-click the dataset to invoke the Fields editor. 2 Select the field for which to set properties. 3 Set the desired properties for the field in the Object Inspector. 4 Right-click the Fields editor list box to invoke the context menu. 5 Choose Save Attributes to save the current field’s property settings as an attribute set in the Data Dictionary. The name for the attribute set defaults to the name of the current field. You can specify a different name for the attribute set by choosing Save Attributes As instead of Save Attributes from the context menu. Note You can also create attribute sets directly from the SQL Explorer. When you create an attribute set from the data dictionary, it is not applied to any fields, but you can specify two additional attributes: a field type (such as TFloatField, TStringField, and so on) and a data-aware control (such as TDBEdit, TDBCheckBox, and so on) that is automatically placed on a form when a field based on the attribute set is dragged to the form. For more information, see the online help for the SQL Explorer. 19-14Developer’ sGuide, Settingpersistentfieldpropertiesandevents

Associating attribute sets with field components

When several fields in the datasets used by your application share common formatting properties (such as Alignment, DisplayWidth, DisplayFormat, EditFormat, MaxValue, MinValue, and so on), and you have saved those property settings as attribute sets in the Data Dictionary, you can easily apply the attribute sets to fields without having to recreate the settings manually for each field. In addition, if you later change the attribute settings in the Data Dictionary, those changes are automatically applied to every field associated with the set the next time field components are added to the dataset. To apply an attribute set to a field component: 1 Double-click the dataset to invoke the Fields editor. 2 Select the field for which to apply an attribute set. 3 Invoke the context menu and choose Associate Attributes. 4 Select or enter the attribute set to apply from the Associate Attributes dialog box. If there is an attribute set in the Data Dictionary that has the same name as the current field, that set name appears in the edit box. Important If the attribute set in the Data Dictionary is changed at a later date, you must reapply the attribute set to each field component that uses it. You can invoke the Fields editor to multi-select field components within a dataset to which to reapply attributes.

Removing attribute associations

If you change your mind about associating an attribute set with a field, you can remove the association by following these steps: 1 Invoke the Fields editor for the dataset containing the field. 2 Select the field or fields from which to remove the attribute association. 3 Invoke the context menu for the Fields editor and choose Unassociate Attributes. Important Unassociating an attribute set does not change any field properties. A field retains the settings it had when the attribute set was applied to it. To change these properties, select the field in the Fields editor and set its properties in the Object Inspector.

Controlling and masking user input

The EditMask property provides a way to control the type and range of values a user can enter into a data-aware component associated with TStringField, TDateField, TTimeField, and TDateTimeField components. You can use existing masks, or create your own. The easiest way to use and create edit masks is with the Input Mask editor. You can, however, enter masks directly into the EditMask field in the Object Inspector. Note For TStringField components, the EditMask property is also its display format. Workingwithfieldcomponents19-15, SettingpersistentfieldpropertiesandeventsTo invoke the Input Mask editor for a field component: 1 Select the component in the Fields editor or Object Inspector. 2 Click the Properties page in the Object Inspector. 3 Double-click the values column for the EditMask field in the Object Inspector, or click the ellipsis button. The Input Mask editor opens. The Input Mask edit box enables you to create and edit a mask format. The Sample Masks grid lets you select from predefined masks. If you select a sample mask, the mask format appears in the Input Mask edit box where you can modify it or use it as is. You can test the allowable user input for a mask in the Test Input edit box. The Masks button enables you to load a custom set of masks—if you have created one—into the Sample Masks grid for easy selection.

Using default formatting for numeric, date, and time fields

Delphi provides built-in display and edit format routines and intelligent default formatting for TFloatField, TCurrencyField, TIntegerField, TSmallIntField, TWordField, TDateField, TDateTimeField, and TTimeField components. To use these routines, you need do nothing. Default formatting is performed by the following routines: Table 19.5 Field component formatting routines Routine Used by… FormatFloat TFloatField, TCurrencyField FormatDateTime TDateField, TTimeField, TDateTimeField FormatCurr TCurrencyField Only format properties appropriate to the data type of a field component are available for a given component. Default formatting conventions for date, time, currency, and numeric values are based on the Regional Settings properties in the Control Panel. For example, using the default settings for the United States, a TFloatField column with the Currency property set to True sets the DisplayFormat property for the value 1234.56 to $1234.56, while the EditFormat is 1234.56. At design time or runtime, you can edit the DisplayFormat and EditFormat properties of a field component to override the default display settings for that field. You can also write OnGetText and OnSetText event handlers to do custom formatting for field components at runtime. For more information about setting field component properties at runtime, see “Setting field component properties at runtime” on page 19-14. 19-16Developer’ sGuide, Workingwithfieldcomponentmethodsatruntime

Handling events

Like most components, field components have event handlers associated with them. By writing these handlers you can control events that affect data entered in fields through data-aware controls. The following table lists the events associated with field components: Table 19.6 Field component events Event Purpose OnChange Called when the value for a field changes. OnGetText Called when the value for a field component is retrieved for display or editing. OnSetText Called when the value for a field component is set. OnValidate Called to validate the value for a field component whenever the value is changed because of an edit or insert operation. OnGetText and OnSetText events are primarily useful to programmers who want to do custom formatting that goes beyond the built-in formatting functions. OnChange is useful for performing application-specific tasks associated with data change, such as enabling or disabling menus or visual controls. OnValidate is useful when you want to control data-entry validation in your application before returning values to a database server. To write an event handler for a field component: 1 Select the component. 2 Select the Events page in the Object Inspector. 3 Double-click the Value field for the event handler to display its source code window. 4 Create or edit the handler code.

Working with field component methods at runtime

Field components methods available at runtime enable you to convert field values from one data type to another, and enable you to set focus to the first data-aware control in a form that is associated with a field component. Controlling the focus of data-aware components associated with a field is important when your application performs record-oriented data validation in a dataset event handler (such as BeforePost). Validation may be performed on the fields in a record whether or not its associated data-aware control has focus. Should validation fail for a particular field in the record, you want the data-aware control containing the faulty data to have focus so that the user can enter corrections. You control focus for a field’s data-aware components with a field’s FocusControl method. FocusControl sets focus to the first data-aware control in a form that is associated with a field. An event handler should call a field’s FocusControl methodWorkingwithfieldcomponents19-17, Displaying, converting, andaccessingfieldvaluesbefore validating the field. The following code illustrates how to call the FocusControl method for the Company field in the Customers table: CustomersCompany.FocusControl; The following table lists some other field component methods and their uses. For a complete list and detailed information about using each method, see the entries for TField and its descendants in the online VCL Reference. Table 19.7 Selected field component methods Method Purpose AssignValue Sets a field value to a specified value using an automatic conversion function based on the field’s type. Clear Clears the field and sets its value to NULL. GetData Retrieves unformatted data from the field. IsValidChar Determines if a character entered by a user in a data-aware control to set a value is allowed for this field. SetData Assigns unformatted data to this field.

Displaying, converting, and accessing field values

Data-aware controls such as TDBEdit and TDBGrid automatically display the values associated with field components. If editing is enabled for the dataset and the controls, data-aware controls can also send new and changed values to the database. In general, the built-in properties and methods of data-aware controls enable them to connect to datasets, display values, and make updates without requiring extra programming on your part. Use them whenever possible in your database applications. For more information about data-aware control, see Chapter 25, “Using data controls.” Standard controls can also display and edit database values associated with field components. Using standard controls, however, may require additional programming on your part.

Displaying field component values in standard controls

An application can access the value of a database column through the Value property of a field component. For example, the following statement assigns the value of the CustomersCompany field to the text in a TEdit control: Edit3.Text := CustomersCompany.Value; This method works well for string values, but may require additional programming to handle conversions for other data types. Fortunately, field components have built- in functions for handling conversions. Note You can also use variants to access and set field values. Variants are a new and flexible data type. For more information about using variants to access and set field values, see “Accessing field values with the default dataset property” on page 19-20. 19-18Developer’ sGuide, Displaying, converting, andaccessingfieldvalues

Converting field values

Conversion functions attempt to convert one data type to another. For example, the AsString function converts numeric and Boolean values to string representations. The following table lists field component conversion functions, and which functions are recommended for field components by field-component type: Table 19.8 Field component conversion functions Function AsVariant ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ AsString ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ AsInteger ✓ ✓ ✓ ✓ AsFloat ✓ ✓ ✓ ✓ ✓ ✓ ✓ AsCurrency ✓ ✓ ✓ ✓ ✓ ✓ ✓ AsDateTime ✓ AsBoolean ✓ Note that the AsVariant method is recommended to translate among all data types. When in doubt, use AsVariant. In some cases, conversions are not always possible. For example, AsDateTime can be used to convert a string to a date, time, or datetime format only if the string value is in a recognizable datetime format. A failed conversion attempt raises an exception. In some other cases, conversion is possible, but the results of the conversion are not always intuitive. For example, what does it mean to convert a TDateTimeField value into a float format? AsFloat converts the date portion of the field to the number of days since 12/31/1899, and it converts the time portion of the field to a fraction of 24 hours. Table 19.9 lists permissible conversions that produce special results: Table 19.9 Special conversion results Conversion Result String to Boolean Converts “True,” “False,” “Yes,” and “No” to Boolean. Other values raise exceptions. Float to Integer Rounds float value to nearest integer value. DateTime to Float Converts date to number of days since 12/31/1899, time to a fraction of 24 hours. Boolean to String Converts any Boolean value to “True” or “False.” In other cases, conversions are not possible at all. In these cases, attempting a conversion also raises an exception. TStringField TIntegerField TSmallintField TWordField TFloatField TCurrencyField TBCDField TDateTimeField TDateField TTimeField TBooleanField TBytesField TVarBytesField TBlobField TMemoField TGraphicFieldWorkingwithfieldcomponents19-19, Displaying, converting, andaccessingfieldvaluesYou use a conversion function as you would use any method belonging to a component: append the function name to the end of the component name wherever it occurs in an assignment statement. Conversion always occurs before an actual assignment is made. For example, the following statement converts the value of CustomersCustNo to a string and assigns the string to the text of an edit control: Edit1.Text := CustomersCustNo.AsString; Conversely, the next statement assigns the text of an edit control to the CustomersCustNo field as an integer: MyTableMyField.AsInteger := StrToInt(Edit1.Text); An exception occurs if an unsupported conversion is performed at runtime.

Accessing field values with the default dataset property

The preferred method for accessing a field’s value is to use variants with the FieldValues property. For example, the following statement puts the value of an edit box into the CustNo field in the Customers table: Customers.FieldValues['CustNo'] := Edit2.Text; For more information about variants, see the online help.

Accessing field values with a dataset’s Fields property

You can access the value of a field with the Fields property of the dataset component to which the field belongs. Accessing field values with a dataset’s Fields property is useful when you need to iterate over a number of columns, or if your application works with tables that are not available to you at design time. To use the Fields property you must know the order of and data types of fields in the dataset. You use an ordinal number to specify the field to access. The first field in a dataset is numbered 0. Field values must be converted as appropriate using the field component’s conversion routine. For more information about field component conversion functions, see “Converting field values” on page 19-19. For example, the following statement assigns the current value of the seventh column (Country) in the Customers table to an edit control: Edit1.Text := CustTable.Fields[6].AsString; Conversely, you can assign a value to a field by setting the Fields property of the dataset to the desired field. For example: begin Customers.Edit; Customers.Fields[6].AsString := Edit1.Text; Customers.Post; end; 19-20Developer’ sGuide, Checkingafield’ scurrentvalue

Accessing field values with a dataset’s FieldByName method

You can also access the value of a field with a dataset’s FieldByName method. This method is useful when you know the name of the field you want to access, but do not have access to the underlying table at design time. To use FieldByName, you must know the dataset and name of the field you want to access. You pass the field’s name as an argument to the method. To access or change the field’s value, convert the result with the appropriate field component conversion function, such as AsString or AsInteger. For example, the following statement assigns the value of the CustNo field in the Customers dataset to an edit control: Edit2.Text := Customers.FieldByName('CustNo').AsString; Conversely, you can assign a value to a field: begin Customers.Edit; Customers.FieldByName('CustNo').AsString := Edit2.Text; Customers.Post; end;

Checking a field’s current value

If your application uses TClientDataSet or administers a dataset that is the source dataset for a TProvider component on an application server, and you encounter difficulties when updating records, you can use the CurValue property to examine the field value in the record causing problems. CurValue represents the current value of the field component including changes made by other users of the database. Use CurValue to examine the value of a field when a problem occurs in posting a value to the database. If the current field value is causing a problem, such as a key violation, when posting the value to the database, an OnReconcileError occurs. In an OnReconcileError event handler, NewValue is the unposted value that caused the problem, OldValue is the value that was originally assigned to the field before any edits were made, and CurValue is the value that is currently assigned to the field. CurValue may differ from OldValue if another user changed the value of the field after OldValue was read.

Setting a default value for a field

You can specify how a default value for a field should be calculated at runtime using the DefaultExpression property. DefaultExpression can be any valid SQL value expression that does not refer to field values. If the expression contains literals other than numeric values, they must appear in quotes. For example, a default value of noon for a time field would be ‘12:00:00’ including the quotes around the literal value. Workingwithfieldcomponents19-21, Workingwithconstraints

Working with constraints

Field components can use SQL server constraints. In addition, your applications can create and use custom constraints that are local to your application. All constraints are rules or conditions that impose a limit on the scope or range of values that a field can store. The following sections describe working with constraints at the field component level.

Creating a custom constraint

A custom constraint is not imported from the server like other constraints. It is a constraint that you declare, implement, and enforce in your local application. As such, custom constraints can be useful for offering a pre-validation enforcement of data entry, but a custom constraint cannot be applied against data received from or sent to a server application. To create a custom constraint, set the CustomConstraint property to specify a constraint condition, and set ConstraintErrorMessage to the message to display when a user violates the constraint at runtime. CustomConstraint is an SQL string that specifies any application-specific constraints imposed on the field’s value. Set CustomConstraint to limit the values that the user can enter into a field. CustomConstraint can be any valid SQL search expression such as x > 0 and x < 100 The name used to refer to the value of the field can be any string that is not a reserved SQL keyword, as long as it is used consistently throughout the constraint expression. Custom constraints are imposed in addition to any constraints to the field’s value that come from the server. To see the constraints imposed by the server, read the ImportedConstraint property.

Using server constraints

Most production SQL databases use constraints to impose conditions on the possible values for a field. For example, a field may not permit NULL values, may require that its value be unique for that column, or that its values be greater than 0 and less than 150. While you could replicate such conditions in your client applications, Delphi offers the ImportedConstraint property to propagate a server’s constraints locally. ImportedConstraint is a read-only property that specifies an SQL clause that limits field values in some manner. For example: Value > 0 and Value < 100 Do not change the value of ImportedConstraint, except to edit nonstandard or server- specific SQL that has been imported as a comment because it cannot be interpreted by the database engine. 19-22Developer’ sGuide, UsingobjectfieldsTo add additional constraints on the field value, use the CustomConstraint property. Custom constraints are imposed in addition to the imported constraints. If the server constraints change, the value of ImportedConstraint also changed but constraints introduced in the CustomConstraint property persist. Removing constraints from the ImportedConstraint property will not change the validity of field values that violate those constraints. Removing constraints results in the constraints being checked by the server instead of locally. When constraints are checked locally, the error message supplied as the ConstraintErrorMessage property is displayed when violations are found, instead of displaying an error message from the server.

Using object fields

Object field descendents support the field types ADT (Abstract Data Type), Array, DataSet, and Reference. These field types contain or reference child fields and other data sets. An ADT field contains child fields, which can be of any scalar type or an object type itself. An array field contains an array of child fields, all of the same type. A dataset field provides access to a nested data set. A reference field stores a pointer (reference) to another persistent object (ADT). Table 19.10 Types of object field components Component name Purpose TADTField Represents an ADT (Abstract Data Type) field. TArrayField Represents an array field. TDataSetField Represents a field that contains a nested data set reference. TReferenceField Represents a REF field, a pointer to an ADT. Object field descendents contain the functionality to contain or reference child fields and datasets. If a dataset contains any object fields, persistent object fields of the correct type are automatically created when adding fields with the Fields editor of the dataset. Adding persistent object fields to a dataset automatically sets the dataset’s ObjectView property to True. When ObjectView is True, fields are stored hierarchically rather than flattened out. The following properties are common to all object field descendents and provide the functionality to handle child fields and datasets. Table 19.11 Common object field descendent properties Property Purpose Fields Contains the child fields belonging to the object field. ObjectType Classification of the object field. FieldCount Number of child fields belonging to the object field. FieldValues Provides access to the values of the child fields of the object field. Workingwithfieldcomponents19-23, Usingobjectfields

Displaying ADT and array fields

Both ADT and array fields contain child fields that can be displayed through data- aware controls. Data-Aware controls such as TDBEdit and TDBGrid automatically display ADT and array field types. Data-aware controls with a DataField property automatically displays any ADT and array fields and their child fields in the drop-down list. When an ADT or array field is bound to a data-aware control, the child fields appear in an uneditable comma delimited string in the control. A child field is bound to the control as a normal data field. A TDBGrid control displays ADT and array field data differently, depending on the value of the dataset’s ObjectView property. When ObjectView is False, each child field appears in a single column. When ObjectView is True, an ADT or array field can be expanded and collapsed by clicking on the arrow in the title bar of the column. When the field is expanded, each child field appears in its own column and title bar, all below the title bar of the ADT or array itself. When the ADT or array is collapsed, only one column appears with an uneditable comma delimited string containing the child fields.

Working with ADT fields

ADTs are user defined types created on the server, and are similar to structures. An ADT can contain most scalar field types, array fields, reference fields, and nested ADTs. Accessing ADT field values There are a variety of ways to access the data in ADT field types. Creating and using persistent fields is strongly recommended. The following examples assign a child field value to an edit box called CityEdit, and uses the following ADT structure, Address Street City State Zip and the following persistent fields created for the Customer table component, CustomerAddress: TADTField; CustomerAddrStreet: TStringField; CustomerAddrCity: TStringField; CustomerAddrState: TStringField; CustomerAddrZip: TStringField; This line of code uses a persistent field and demonstrates the preferred method of accessing data in ADT fields. CityEdit.Text := CustomerAddrCity.AsString; 19-24Developer’ sGuide, UsingobjectfieldsThe following code examples require that the dataset’s ObjectView property be set to True in order to compile. They don’t require persistent fields. This example uses a fully qualified name with the FieldByName method on the dataset. CityEdit.Text := Customer.FieldByName(‘Address.City’).AsString; You can access the value of a child field with the TADTField’s FieldValuesproperty. FieldValues accepts and returns a Variant, so it can handle and convert fields of any type. The index parameter takes an integer value which specifies the offset of the field. It is also the default property on TObjectField, and can therefore be omitted. For example, CityEdit.Text := TAdtField(Customer.FieldByName('Address'))[1]; which is the same as, CityEdit.Text := TAdtField(Customer.FieldByName('Address')).FieldValues[1]; This code uses the Fields property of the TADTField component. CityEdit.Text := TAdtField(Customer.FieldByName(‘Address’)).Fields[1].AsString; This code uses the Fields property of the TADTField component with FieldByName of both the dataset and the TFields object. CityEdit.Text := TADTField(Customer.FieldByName(‘Address’)).Fields.FieldByName(‘City’).AsString; As you can see from this last example, accessing the field’s data through persistent fields is much simpler. These additional access methods are primarily useful when the structure of the database table is not fixed or known at design time. ADT field values can also be accessed with a dataset’s FieldValues property: Customer.Edit; Customer['Address.City'] := CityEdit.Text; Customer.Post; The next statement reads a string value from the City child field of the ADT field Address into an edit box: CityEdit.Text := Customer['Address.City']; Note The dataset’s ObjectView property can be either True or False for these lines of code to compile.

Working with array fields

Array fields consist of a set of fields of the same type. The field types can be scalar (e.g. float, string), or non-scalar (an ADT), but an array field of arrays is not permitted. The SparseArrays property of TDataSet determines whether a unique TField object is created for each element of the array field. Workingwithfieldcomponents19-25, Usingobjectfields

Accessing array field values

There are a variety of ways to access the data in array field types. The following example populates a list box with all of the non-null array elements. var OrderDates: TArrayField; I: Integer; begin for I := 0 to OrderDates.Size - 1 do begin if OrderDates.Fields[I].IsNull then Break; OrderDateListBox.Items.Add(OrderDates[I]); end; end; The following examples assign a child field value to an edit box called TelEdit, and uses the array TelNos_Array, which is a six element array of strings. The following persistent fields created for the Customer table component are used by the following examples: CustomerTelNos_Array: TArrayField; CustomerTelNos_Array0: TStringField; CustomerTelNos_Array1: TStringField; CustomerTelNos_Array2: TStringField; CustomerTelNos_Array3: TStringField; CustomerTelNos_Array4: TStringField; CustomerTelNos_Array5: TStringField; This line of code uses a persistent field to assign an array element value to an edit box. TelEdit.Text := CustomerTelNos_Array0.AsString; The following code examples require that the dataset’s ObjectViewproperty be set to True in order to compile. They don’t require persistent fields. You can access the value of a child field with the dataset’s FieldValues property. FieldValues accepts and returns a Variant, so it can handle and convert fields of any type. For example, TelEdit.Text := TArrayField(Table1.FieldByName('TelNos_Array'))[1]; which is the same as, TelEdit.Text := TArrayField(Table1.FieldByName('TelNos_Array')).FieldValues[1]; This code uses the Fieldsproperty of the TArrayField component. TelEdit.Text := TArrayField(Customer.FieldByName(‘TelNos_Array’)).Fields[1].AsString;

Working with dataset fields

Dataset fields provide access to data stored in nested datasets or tables. Each record in the dataset contains different data in the nested dataset. The NestedDataSet property contains the nested dataset. The data in the nested dataset is then accessed through the field objects of the nested dataset. 19-26Developer’ sGuide, UsingobjectfieldsDisplaying dataset fields A dataset field is not normally bound to a data aware control. In a TDBGrid control a dataset field is designated in each cell of the dataset column, with (DataSet) and, at runtime, an ellipsis button to the right. At runtime, clicking on the ellipsis brings up a new form with a grid displaying the dataset associated with the current record’s dataset field. This form can also be brought up programmatically with the DB grid’s ShowPopupEditor method. For example, if the seventh column in the grid represents a dataset field, the following code will display the dataset associated with that field for the current record. DBGrid1.ShowPopUpEditor(DbGrid1.columns[7]); Accessing data in a nested dataset To access the data in a dataset field you first create a persistent TField, and then link to this field using the DatasetField property on a TNestedTable or TClientDataSet. If the reference is assigned, the nested dataset will contain a single record with the referenced data. If the reference is null, the nested dataset will be empty. Before inserting records into a nested dataset, you should be sure to post the corresponding record in the master table, if it has just been inserted. If the inserted record is not posted, it will be automatically posted before the nested dataset posts.

Working with reference fields

Reference fields store a pointer or reference to another ADT object. This ADT object is a single record of another object table. Reference fields always refer to a single record in a dataset (object table). The data in the referenced object is actually returned in a nested dataset, but can also be accessed via the Fields property on the TReferenceField. Displaying reference fields In a TDBGrid control a reference field is designated in each cell of the dataset column, with (Reference) and, at runtime, an ellipsis button to the right. At runtime, clicking on the ellipsis brings up a new form with a grid displaying the object associated with the current record’s reference field. This form can also be brought up programmatically with the DB grid’s ShowPopupEditor method. For example, if the seventh column in the grid represents a reference field, the following code will display the object associated with that field for the current record. DBGrid1.ShowPopUpEditor(DbGrid1.columns[7]); Accessing data in a reference field To access the data in a reference field you first create a persistent TField, and then link to this field using the DatasetField property on a TNestedTable or TClientDataSet. If the reference is assigned, the reference will contain a single record with the referenced data. If the reference is null, the reference will be empty. Workingwithfieldcomponents19-27, UsingobjectfieldsThe following examples are equivalent and assign data from the reference field CustomerRefCity to an edit box called CityEdit: CityEdit.Text := CustomerRefCity.Fields[1].AsString; CityEdit.Text := CustomerRefCity.NestedDataSet.Fields[1].AsString; When data in a reference field is edited, it is actually the referenced data that is modified. To assign a reference field, you need to first use a SELECT statement to select the reference from the table, and then assign. For example: var AddressQuery: TQuery; CustomerAddressRef; TReferenceField; begin Address.SQL.Text := ‘SELECT REF(A) FROM AddressTable A WHERE A.City = ‘’San Francisco’’’’; AddressQuery.Open; CustomerAddressRef.Assign(AddressQuery.Fields[0]); end; 19-28Developer’ sGuide,

Chapter

Chapter 20Working with tables This chapter describes how to use the TTable dataset component in your database applications. A table component encapsulates the full structure of and data in an underlying database table. A table component inherits many of its fundamental properties and methods from TDataSet, TBDEDataSet, and TDBDataSet. Therefore, you should be familiar with the general discussion of datasets in“Understanding datasets,” and the BDE-specific discussion of datasets in “Using BDE-enabled datasets” before reading about the unique properties and methods of table components discussed here.

Using table components

A table component gives you access to every row and column in an underlying database table, whether it is from Paradox, dBASE, Access, FoxPro, an ODBC- compliant database, or an SQL database on a remote server, such as InterBase, Sybase, or SQL Server. You can view and edit data in every column and row of a table. You can work with a range of rows in a table, and you can filter records to retrieve a subset of all records in a table based on filter criteria you specify. You can search for records, copy, rename, or delete entire tables, and create master/detail relationships between tables. Note A table component always references a single database table. If you need to access multiple tables with a single component, or if you are only interested in a subset of rows and columns in one or more tables, you should use a query component instead of a table component. For more information about query components, see Chapter 21, “Working with queries.” Workingwithtables20-1, Settingupatablecomponent

Setting up a table component

The following steps are general instructions for setting up a table component at design time. There may be additional steps you need to tailor a table’s properties to the requirements of your application. To create a table component, 1 Place a table component from the Data Access page of the Component palette in a data module or on a form, and set its Name property to a unique value appropriate to your application. 2 Set the DatabaseName of the component to the name of the database to access. 3 Set the TableName property to the name of the table in the database. You can select tables from the drop-down list if the DatabaseName property is already specified. 4 Place a data source component in the data module or on the form, and set its DataSet property to the name of the table component. The data source component is used to pass a result set from the table to data-aware components for display. To access the data encapsulated by a table component, 1 Place a data source component from the Data Access page of the Component palette in the data module or form, and set its DataSet property to the name of the table component. 2 Place a data-aware control, such as TDBGrid, on a form, and set the control’s DataSource property to the name of the data source component placed in the previous step. 3 Set the Active property of the table component to True.

Specifying a database location

The DatabaseName property specifies where the table component looks for a database table. For Paradox and dBASE, DatabaseName can be a Borland Database Engine (BDE) alias, or an explicit directory path. For SQL tables, DatabaseName must be a BDE alias. The advantage of using BDE aliases in all cases is that you can change the data source for an entire application by simply changing the alias definition in the SQL Explorer. To change the alias definition using SQL explorer, right click the SQL explorer and select Rename. This displays the BDE Administration Tool. For more information about setting and using BDE aliases, see the online help for the SQL Explorer. To set the DatabaseName property, 1 Set the table’s Active property to False if necessary. 2 Specify the BDE alias or directory path in the DatabaseName property. Tip If your application uses database components to control database transactions, DatabaseName can be set to a local alias defined for the database component instead. 20-2Developer’ sGuide, SettingupatablecomponentFor more information about database components, see Chapter 17, “Connecting to databases.”

Specifying a table name

The TableName property specifies the table in a database to access with the table component. To specify a table, follow these steps: 1 Set the table’s Active property to False, if necessary. 2 Set the DatabaseName property to a BDE alias or directory path. For more information about setting DatabaseName, see “Specifying a database location” on page 20-2. 3 Set the TableName property to the table to access. At design time you can choose from valid table names in the drop-down list for the TableName property in the Object Inspector. At runtime, you must specify a valid name in code. Once you specify a valid table name, you can set the table component’s Active property to True to connect to the database, open the table, and display and edit data. At runtime, you can set or change the table associated with a table component by: • Setting Active to False. • Assigning a valid table name to the TableName property. For example, the following code changes the table name for the OrderOrCustTable table component based on its current table name: with OrderOrCustTable do begin Active := False; {Close the table} if TableName = 'CUSTOMER.DB' then TableName := 'ORDERS.DB' else TableName := 'CUSTOMER.DB'; Active := True; {Reopen with a new table} end;

Specifying the table type for local tables

If an application accesses Paradox, dBASE, FoxPro, or comma-delimited ASCII text tables, then the BDE uses the TableType property to determine the table’s type (its expected structure). TableType is not used when an application accesses SQL-based tables on database servers. By default TableType is set to ttDefault. When TableType is ttDefault, the BDE determines a table’s type from its file-name extension. Table 20.1 summarizes the fileWorkingwithtables20-3, Controllingread/ writeaccesstoatablename extensions recognized by the BDE and the assumptions it makes about a table’s type: Table 20.1 Table types recognized by the BDE based on file extension Extension Table type No file extension Paradox .DB Paradox .DBF dBASE .TXT ASCII text If your local Paradox, dBASE, and ASCII text tables use the file extensions as described in Table 20.1, then you can leave TableType set to ttDefault. Otherwise, your application must set TableType to indicate the correct table type. Table 20.2 indicates the values you can assign to TableType: Table 20.2 TableType values Value Table type ttDefault Table type determined automatically by the BDE ttParadox Paradox ttDBase dBASE ttFoxPro FoxPro ttASCII Comma-delimited ASCII text

Opening and closing a table

To view and edit a table’s data in a data-aware control such as TDBGrid, open the table. There are two ways to open a table. You can set its Active property to True, or you can call its Open method. Opening a table puts it into dsBrowse state and displays data in any active controls associated with the table’s data source. To end display and editing of data, or to change the values for a table component’s fundamental properties (e.g., DatabaseName, TableName, and TableType), first post or discard any pending changes. If cached updates are enabled, call the ApplyUpdates method to write the posted changes to the database. Finally, close the table. There are two ways to close a table. You can set its Active property to False, or you can call its Close method. Closing a table puts the table into dsInactive state. Active controls associated with the table’s data source are cleared.

Controlling read/write access to a table

By default when a table is opened, it requests read and write access for the underlying database table. Depending on the characteristics of the underlying database table, the requested write privilege may not be granted (for example, when 20-4Developer’ sGuide, Searchingforrecordsyou request write access to an SQL table on a remote server and the server restricts the table’s access to read only). There are three properties for table components that can affect an application’s read and write access to a table: CanModify, ReadOnly, and Exclusive. CanModify is a read-only property that specifies whether or not a table component is permitted read/write access to the underlying database table. After you open a table at runtime, your application can examine CanModify to test whether or not the table has write access. If CanModify is False, the application cannot write to the database. If CanModify is True, your application can write to the database provided that the table’s ReadOnly property is False. ReadOnly determines whether or not a user can both view and edit data. When ReadOnly is False (the default), a user can both view and edit data. To restrict a user to viewing data, set ReadOnly to True before opening a table. Exclusive controls whether or not an application gains sole read/write access to a Paradox, dBASE, or FoxPro table. To gain sole read/write access for these table types, set the table component’s Exclusive property to True before opening the table. If you succeed in opening a table for exclusive access, other applications cannot read data from or write data to the table. Your request for exclusive access is not honored if the table is already in use when you attempt to open it. The following statements open a table for exclusive access: CustomersTable.Exclusive := True; {Set request for exclusive lock} CustomersTable.Active := True; {Now open the table} Note You can attempt to set Exclusive on SQL tables, but some servers may not support exclusive table-level locking. Others may grant an exclusive lock, but permit other applications to read data from the table. For more information about exclusive locking of database tables on your server, see your server documentation.

Searching for records

You can search for specific records in a table in various ways. The most flexible and preferred way to search for a record is to use the generic search methods Locate and Lookup. These methods enable you to search on any type of fields in any table, whether or not they are indexed or keyed. • Locate finds the first row matching a specified set of criteria and moves the cursor to that row. • Lookup returns values from the first row that matches a specified set of criteria, but does not move the cursor to that row. You can use Locate and Lookup with any kind of dataset, not just TTable. For a complete discussion of Locate and Lookup, see Chapter 18, “Understanding datasets.” Table components also support the Goto and Find methods. While these methods are documented here to allow you to work with legacy applications, you should always use Lookup and Locate in your new applications. You may see performance gains in existing applications if you convert them to use the new methods. Workingwithtables20-5, Searchingforrecords

Searching for records based on indexed fields

Table components support a set of Goto search methods for backward compatibility. Goto methods enable you to search for a record based on indexed fields, referred to as a key, and make the first record found the new current record. For Paradox and dBASE tables, the key must always be an index, which you can specify in a table component’s IndexName property. For SQL tables, the key can also be a list of fields you specify in the IndexFieldNames property. You can also specify a field list for Paradox or dBASE tables, but the fields must have indexes defined on them. For more information about IndexName and IndexFieldNames, see “Searching on alternate indexes” on page 20-8. Tip To search on nonindexed fields in a Paradox or dBASE table, use Locate. Alternatively, you can use a TQuery component and a SELECT statement to search on nonindexed fields in Paradox and dBASE fields. For more information about TQuery, see Chapter 21, “Working with queries.” The following table summarizes the six related Goto and Find methods an application can use to search for a record: Table 20.3 Legacy TTable search methods Method Purpose EditKey Preserves the current contents of the search key buffer and puts the table into dsSetKey state so your application can modify existing search criteria prior to executing a search. FindKey Combines the SetKey and GotoKey methods in a single method. FindNearest Combines the SetKey and GotoNearest methods in a single method. GotoKey Searches for the first record in a dataset that exactly matches the search criteria, and moves the cursor to that record if one is found. GotoNearest Searches on string-based fields for the closest match to a record based on partial key values, and moves the cursor to that record. SetKey Clears the search key buffer and puts the table into dsSetKey state so your application can specify new search criteria prior to executing a search. GotoKey and FindKey are Boolean functions that, if successful, move the cursor to a matching record and return True. If the search is unsuccessful, the cursor is not moved, and these functions return False. GotoNearest and FindNearest always reposition the cursor either on the first exact match found or, if no match is found, on the first record that is greater than the specified search criteria.

Executing a search with Goto methods

To execute a search using Goto methods, follow these general steps: 1 Specify the index to use for the search in the IndexName property, if necessary. (For SQL tables, list the fields to use as a key in IndexFieldNames instead.) If you use a table’s primary index, you do not need to set these properties. 20-6Developer’ sGuide, Searchingforrecords2Open the table. 3 Put the table in dsSetKey state with SetKey. 4 Specify the value(s) to search on in the Fields property. Fields is a string list that you index into with ordinal numbers corresponding to columns. The first column number in a table is 0. 5 Search for and move to the first matching record found with GotoKey or GotoNearest. For example, the following code, attached to a button’s OnClick event, moves to the first record containing a field value that exactly matches the text in an edit box on a form: procedure TSearchDemo.SearchExactClick(Sender: TObject); begin Table1.SetKey; Table1.Fields[0].AsString := Edit1.Text; if not Table1.GotoKey then ShowMessage('Record not found'); end; GotoNearest is similar. It searches for the nearest match to a partial field value. It can be used only for string fields. For example, Table1.SetKey; Table1.Fields[0].AsString := 'Sm'; Table1.GotoNearest; If a record exists with “Sm” as the first two characters, the cursor is positioned on that record. Otherwise, the position of the cursor does not change and GotoNearest returns False. Executing a search with Find methods To execute a search using Find methods, follow these general steps: 1 Specify the index to use for the search in the IndexName property, if necessary. (For SQL tables, list the fields to use as a key in IndexFieldNames instead.) If you use a table’s primary index, you do not need to set these properties. 2 Open the table. 3 Search for and move to the first or nearest record with FindKey or FindNearest. Both methods take a single parameter, a comma-delimited list of field values, where each value corresponds to an indexed column in the underlying table. Note FindNearest can only be used for string fields.

Specifying the current record after a successful search

By default, a successful search positions the cursor on the first record that matches the search criteria. If you prefer, you can set the KeyExclusive property for a table component to True to position the cursor on the next record after the first matching record. Workingwithtables20-7, SearchingforrecordsBy default, KeyExclusive