Chapter 3
Common programming tasks

This chapter discusses the fundamentals for some of the common programming tasks in Delphi:

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

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 places where 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

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.

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.

...
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 allocates memory, 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?

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:

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:

{ 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?

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.

Table 3.1   RTL exceptions

Error type

Cause

Meaning

Input/output

Error accessing a file or I/O device

Most I/O exceptions are related to error codes returned by Windows when accessing a file.

Heap

Error using dynamic memory

Heap errors can occur when there is insufficient memory available, or when an application disposes of a pointer that points outside the heap.

Integer math

Illegal operation on integer-type expressions

Errors include division by zero, numbers or expressions out of range, and overflows.

Floating-point math

Illegal operation on real-type expressions

Floating-point errors can come from either a hardware coprocessor or the software emulator. Errors include invalid instructions, division by zero, and overflow or underflow.

Typecast

Invalid typecasting with the as operator

Objects can only be typecast to compatible types.

Conversion

Invalid type conversion

Type-conversion functions such as IntToStr, 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:

try
  { 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 <type of exception> do <statement>;

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, you 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.

Providing 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 it finds. 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 error. 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;

For more information, search for exception handling routines in the Help index.

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

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 VCL by 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,

IPaint = 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 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" and "Memory management of interface objects".

Using 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:

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.

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", 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.

When you use the as operator for dynamic binding on an interface, you should be aware of the following requirements:

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:

unit cadapt;

type
IRGB8bit = interface
    ['{1d76360a-f4f5-11d1-87d4-00c04fb17199}']
    function Red: Byte;
    function Green: Byte;
    function Blue: Byte;
  end;

IColorRef = 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 an object 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:

function 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.

Note: 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" 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 "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 "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.

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 3.2   Object Pascal character types

Type

Bytes

Contents

Purpose

Char

1

Holds a single ANSI character.

Default character type.

AnsiChar

1

Holds a single ANSI character.

8-bit Ansi character standard on Windows.

WideChar

2

Holds a single Unicode character.

16-bit Unicode standard on Windows.

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 and 256 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 Memory Management). 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 AnsiString. 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:

PChars are, with short strings, one of the original Object Pascal string types. They were created primarily as a C language and Windows API compatibility type.

OpenString

An OpenString is obsolete, but you may see it in older code. It is for 16-bit compatibility and is allowed only in parameters. OpenString was used, before long strings were implemented, to allow a short string of an unspecified length string to be passed as a parameter. 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 in "Compiler directives for strings".

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-terminated 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:

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:

Where appropriate, the tables also provide columns indicating whether or not a routine satisfies the following criteria.

TABLE comparison

Table 3.3   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 Case conversion

Table 3.4   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

no

TABLE Modification

Table 3.5   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 Sub-string

Table 3.6   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;

Similarly, 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. Some of the differences between strings and PChars can make conversions problematic:

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:

// 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 compiler directives affect character and string types.

Strings and characters: related topics

The following Object Pascal Language Guide topics discuss strings and character sets. Also see "Creating international applications."

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:

type 
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 3.7   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\delphi4\bin\*.*', faAnyFile, SearchRec);
  Label1.Caption := SearchRec.Name + ' is ' + IntToStr(SearchRec.Size) + ' bytes in size';
end;
procedure 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!');

You 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

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 describes 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 obsolete and are incompatible with Windows file handles. If you need to work with the old file types, see the Object Pascal Language Guide.

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 FileRead 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.

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.

The 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 perform the following:

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.

The 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. 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 a variable.

The most commonly used type when working with strings is a long string. However, passing a long string as the Buffer parameter does not produce the correct result. Long strings contain a size, a reference count, and a pointer to the characters in the string. Consequently, dereferencing a long string does 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('Temp.txt', fmCreate or fmOpenWrite);
  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: