Athena

Delphi VCL Sourcery

Brian Long (www.blong.com)

If you find this article useful then please consider making a donation. It will be appreciated however big or small it might be and will encourage Brian to continue researching and writing about interesting subjects in the future.


Introduction

Some Delphi users know the Visual Component Library (VCL) source code like the back of their hand. Lucky them.

Some Delphi users can locate what they need within the VCL source if they really need it, with prudent use of GREP-like tools.

Some Delphi users have occasionally taken a sneak peek inside the odd VCL source file to see what’s around and ran away spontaneously shaking and grinding their teeth.

This session leads you by the hand and takes you for a viewing of the VCL from the comfort of a chair in a darkened room. We peruse the source code supplied with all versions of Delphi and see what insights can be gained.

Because of the dynamic nature of the VCL, and the way it expands with each new release of Delphi, some of these points are only applicable to more recent versions of the product. These will be noted where applicable.

Click here to download the files associated with this paper.

Directory Structure

What are all those source directories for? Well, assuming you have either the Professional or Client/Server Suite version of Delphi 1, 2, 3 or 4, or the Professional or Enterprise version of Delphi 5, you have a directory hierarchy containing quite a number of source files. Here’s a summary.

Beneath the main Delphi installation directory (under C:\Program Files by default), you will find a Source directory. Unsurprisingly this is where we find (practically) all the supplied source code. Beneath here we have the following directories.

Source\Samples

Here we find the source code to the components that live on the Components page of the component palette. The reason the source for these components is separated from the main VCL components is that these components have not been developed by Inprise R&D. Instead they come from Developer Support and maybe QA. The implication is that since R&D did not produce them, they may not be as resilient or well written as other components.

Source\VCL

This contains the source to almost all the main VCL components and support routines. This means that just about every other component found on the component palette, with the exception of anything on the VBX page (Delphi 1), OCX page (Delphi 2) or ActiveX page (Delphi 3 and higher), is implemented in the files found here.

The VBX, OCX and ActiveX components are not native components and do not come with source.

The Decision Cube components in Delphi 3 and 4 have no source supplied, but Delphi 5 changes this (see below).

The Automation components found on Delphi 5’s Servers component palette page do not have source supplied under the Source directory. Instead, these files can be found in Delphi’s OCX\Servers directory. Their DCU files are in Delphi’s Imports directory.

Delphi 1 stores files from the Open Tools API in this directory as well. Delphi 2 (and later) has a separate location for these files.

Source\ToolsAPI

This contains the Open Tools API source files in Delphi 2 or later. Developers who write property editors, component editors, experts, wizards, version control systems, etc. will need to study and use these files. Basically, these files are used in projects which are ultimately designed for the IDE to use – design-time enhancements.

Source\Internet

Delphi 3 Professional & Client/Server Suite introduced support for building Web server-based applications. The relevant source can be found here.

Source\Decision Cube

Delphi 5 Enterprise (and later) stores the source for the components on the Decision Cube component palette page in this directory.

Source\WebMIDAS

Delphi 5 Enterprise (and later) added support for building HTML-based Web applications using special MIDAS components (called Internet Express). The pertinent source is located here.

Source\Property Editors

Here lies the source code for many standard Delphi property/component editors including the collection editor, string list editor, picture editor and ActiveX component editor. This directory is new in Delphi 5, but Delphi 1 supplied some similar files in Source\Lib. Also, both Delphi 1 and 2 included the source to a few other design-time editors (including for the database components) in Delphi’s Lib directory.

Source\Lib

Delphi 1 stores the source for some common property/component editors here, including the TOpenDialog Filter property editor, the masked edit Mask and Text property editors, TStrings editor, picture editor and TNotebook editor.

Source\RTL70

Delphi 1 stored some historical (and obsolete) Borland Pascal 7 files in this directory.

Source\RTL

This is the entry point to the directory structure containing the source for the Delphi run-time library (RTL). Since some of the RTL is made of assembly files, this directory contains a make file that can be used to rebuild the RTL with or without debug information included.

Source\RTL\SYS

The core run-time library files are found here, both Pascal and assembler files. Delphi 3 (and later) also stores COM support units here.

Source\RTL\Win

Import units for the Windows API are placed here.

Source\RTL\CORBA

Support units for CORBA applications are found here in Delphi 4 and later.

The RTL & VCL

The distinction between the RTL (Run-Time Library) & VCL (Visual Component Library) is reasonably simple. Delphi can create any type of Windows application. This includes console mode, a normal GUI-based application using the usual RAD style of visual development, or a GUI-based application using standard Win32 principles and involving linking in various Windows resources.

The VCL is required for the RAD-based GUI application development. It defines all the classes that make forms work, and contains all the code for reading forms off disk.

On the other hand, API-based GUI apps, or console apps can be written without any of that code. All that is needed is a core set of functionality (found in the System and maybe in the SysUtils unit) and the declarations of the Windows APIs. This core functionality is also needed in a RAD-based GUI application, as the VCL makes heavy use of it.

So the RTL represents code that is essential to any application that can be built with Delphi, whilst the VCL is an object-oriented layer (built out of many source files) that is available for rapidly building GUI applications with. However, since the VCL absolutely requires the RTL, some people consider the VCL to be a combination of the RTL files and the VCL files.

The RTL source code, as mentioned earlier, is found under Delphi’s Source\RTL directory whilst the VCL source code is found under Delphi’s Source\VCL directory.

Compiling The VCL

By default, you can debug any of your files in your Delphi project, simply because the compiler sees the source code for them, and the project option to generate debug information is enabled.

The VCL & RTL source, however, is a different matter. Whilst their source is available in the various directories described above, the compiler does not look in any of them. Instead, it looks in Delphi’s Lib directory, where it finds pre-compiled DCU files for all the RTL & VCL units, which contain no debug information.

To be able to get debug info for VCL units, you will need to direct the compiler at the VCL source directory. Finding all the source files, it will recompile them, generating debug information along the way.

Delphi 1

Choose Options | Project…, go to the Directories/Conditionals page and select the Search path: option. Enter the full path for the VCL source directory (C:\Delphi\Source\VCL by default) and press OK.

To prove things are working, choose Options | Environment… and enable the Show compiler progress option on the Preferences page. Now choose Compile | Build All, and you should see all the VCL files being compiled along with your own units.

Delphi 2

Because Delphi 2 does some excessive unit caching, you may need to close and re-start Delphi before expecting these steps to work. Choose Project | Options…, go to the Directories/Conditionals page and select the Search path: option. Enter the full path for the VCL source directory (C:\Program Files\Delphi2.0\Source\VCL by default) and press OK.

To prove things are working, choose Tools | Options… and enable the Show compiler progress option on the Preferences page. Now choose Project | Build All, and you should see all the VCL files being compiled along with your own units. The VCL units will generate various hints and warnings, which you should try your best to ignore.

Delphi 3

In order to compile the VCL into your project, you will need to ensure you are not compiling with packages: Project | Options…, go to the Packages page and uncheck the Build with runtime packages option. Still in the project options dialog, go to the Directories/Conditionals page and select the Search path: option. Enter the full path for the VCL source directory (C:\Program Files\Delphi3\Source\VCL by default) and press OK.

To prove things are working, choose Tools | Environment Options… and enable the Show compiler progress option on the Preferences page. Now choose Project | Build All, and you should see all the VCL files being compiled along with your own units.

Delphi 4

In order to compile the VCL into your project, you will need to ensure you are not compiling with packages: Project | Options…, go to the Packages page and uncheck the Build with runtime packages option. Still in the project options dialog, go to the Directories/Conditionals page and select the Search path: option. Enter the path for the VCL source directory, taking advantage of the $(DELPHI) macro which expands to the Delphi installation directory: $(DELPHI)\Source\VCL.

To prove things are working, choose Tools | Environment Options… and enable the Show compiler progress option on the Preferences page. Now choose Project | Build, and you should see all the VCL files being compiled along with your own units.

Delphi 5

In order to debug into the VCL, you will need to ensure you are not compiling with packages: Project | Options…, go to the Packages page and uncheck the Build with runtime packages option. Still in the project options dialog, go to the Compiler page and check the Use Debug DCUs option. This causes the compiler to use additional precompiled DCU files from Delphi’s Lib\Debug directory.

It is more difficult to gain debug information for the RTL, as many of the units include object files that need to be compiled from supplied assembly files. This is what the make file in the RTL directory is for, but requires you to have Turbo Assembler.

The Run-Time Library (RTL)

Now it’s time to start looking through some of the source files and see what we can learn. Firstly, the primary RTL units, System and SysUtils.

To open up either of these units in the code editor in Delphi 3 (or later), either find, or fabricate a reference to them (e.g. in a uses clause), right-click and choose Find Declaration. You will never find System in a uses clause, as it is implicitly used by all units anyway. To add it to a uses clause would cause a compiler error.

In earlier versions of Delphi, just open the unit from Delphi’s Source\RTL\Sys directory in the normal way.

The System Unit

This unit is unique, as the compiler has inherent knowledge about its contents. In fact in Delphi 1, the command-line compiler could only compile the System unit if it had an accompanying SYSTEM.TPS file. The 32-bit command-line compiler uses a special –Y switch to compile the System unit.

In Delphi 1, the first things you see in this unit are a large number of compiler directives used to link in compiled assembly modules. Conditional compilation ensures the right files are brought in for the various platforms supported. Even though Delphi is a Windows tool, it had a certain amount of support for developing DOS and protected mode DOS applications.

Following this are commented declarations of the base TObject class and the TVarRec variant record. This record is used when a routine has an open array parameter declared as array of const, e.g. the second parameter in Format. These types are commented in Delphi because the compiler has these details programmed into itself.

In 32-bit Delphi, we firstly see constants defined for use in the internal representation for a Variant variable. Internally, a Variant is stored as a TVarData record. The TObject class is then declared, along with anything else needed for it to compile. Delphi 3 and later also define some core interface types here, as well a class inherited from TObject that implements the IUnknown interface. This class provides a useful bas class for an object that implements any other interfaces based upon IUnknown.

All this is followed by a variety of data types used to support Variant handling. Some pointer types then precede the TVarRec type, and then we see a couple of records used by the memory-management code in a Delphi application.

Delphi 3 and later then define some structures used by applications that use packages.

Shortly after, you see a block of global variables (some of which you will not be able to modify). These warrant some closer examination.

HPrevInst exists in all Delphi versions, but is only useful in a 16-bit application. It gives the instance handle of the previous running instance of your application. If it is non-zero, then you are not the first instance of your application.

Whilst ParamStr allows you to read individual command-line parameters, CmdLine is a PChar containing the whole command-line given to your application.

RandSeed is the random seed number initialised by Randomize and used by Random to generate random numbers.

There are several Boolean information values. IsLibrary let’s you know if you are in a DLL or application, IsConsole lets you know if there is a UI and IsMultiThread lets you know if your application has more than one thread.

Several of these variables are declared as simple pointers. They are provided to allow programmers to change what many developers consider to be fixed behaviour in Delphi.

For example, when an application calls a method of an object that has been declared abstract and not overridden, you get an error. This is not hard-coded.

In fact, if you have a Delphi 3 (or later) application cause this error, where SysUtils is not used by anything (as in Listing 1) you get one error: Runtime error 210 at 00003C16. If you then add SysUtils in a uses clause, a different error is generated: Except EAbstractError in module PROJECT1.EXE at 000069D9. Abstract error.

Listing 1: Generating an abstract method call error

program Project1;
type
  TTest = class
  public
    class procedure Foo; virtual; abstract;
  end;

begin
  TTest.Foo
end.

This change in behaviour occurs because the SysUtils initialisation code assigns the address of one of its own routines (AbstractErrorHandler) to the AbstractErrorProc pointer to customise the behaviour. Since this pointer did not exist in Delphi 1 or 2, the same runtime error occurs regardless of the inclusion of SysUtils in applications compiled in those versions.

In this particular case, unless you wanted to produce a different message than the SysUtils one, there is little point paying attention to this variable. However, we might find more use with some of the others.

Easy Debug Support With Custom Assertion Handler

Delphi 3 introduced support for assertions. An assertion is a neat way of generating an exception if a certain condition is (unexpectedly) false, where the exception provides details about the file and line number of the failure. A statement like this:

Assert(I < 0, 'Oh dear. I < 0');

will produce an EAssertionFailed exception with this message, assuming the criterion fails and I is not less than zero: Oh dear. I < 0 (C:\Dev\Unit1.pas, line 29). If I is indeed less than zero, the Assert statement does nothing.

If you wish, you can strip all assertions, and their associated strings from your EXE by turning off the Assertions option on the Compiler page of the project options dialog and rebuilding all your source code.

Like the abstract error situation described above, if SysUtils is not in the uses clause, the statement will instead produce runtime error 227. SysUtils is writing the address of one of its own routines (AssertErrorHandler) in the System unit’s AssertErrorProc pointer variable.

Looking at the definition of AssertErrorHandler in SysUtils, you can see that it takes four parameters: the descriptive message, the file name, the line number and the error address.

Taking all this into account, you should see that you can write a replacement routine, assign its address to AssertErrorProc and then all calls to Assert go to your code and not to SysUtils. What you put in the routine is up to you, but it could be a good way of implementing a debug trace. If the routine writes the supplied details to a file, or builds a string and gives it to OutputDebugString, you can pepper it with calls like:

Assert(False, 'Approaching polynomial evaluation section');

and you get a good history of what is happening in the program. When it comes to deployment, you can turn Assertion support off, rebuild the project, and all the excess text descriptions are stripped from the binary. This

Alternatively, if you wanted to aid support of the application in the field, you could leave assertions on. To ensure the performance does not degrade excessively, you could control whether assertions call your routine by writing extra code in the initialisation section of your unit.

In short, if some appropriate command-line is not found, do not assign the address of your trace routine to AssertErrorProc. Listing 2 shows a possible unit that would add in all the relevant functionality for this. You can find this code in the file AssertionHandlerU.pas that accompanies this paper.

Listing 2: A customised Assert handler, for debugging purposes

unit AssertionHandlerU;

interface

implementation

uses
  SysUtils, Windows;

procedure AssertErrorHandler(const Message, Filename: string;
  LineNumber: Integer; ErrorAddr: Pointer);
var
  S: String;
begin
  S := Format('%s (%s, line %d, address $%x)',
    [Message, Filename, LineNumber, Pred(Integer(ErrorAddr))]);
  OutputDebugString(PChar(S));
end;

procedure AssertErrorNoHandler(const Message, Filename: string;
  LineNumber: Integer; ErrorAddr: Pointer);
begin
end;

initialization
  if FindCmdLineSwitch('Debug', ['/', '-'], True) then
    AssertErrorProc := @AssertErrorHandler
  else
    AssertErrorProc := @AssertErrorNoHandler
end.

Simply adding this unit to the project file’s uses clause will re-route the assertion handler, assuming the /Debug command-line switch was passed to the program.

Note, though, that even though assertions exist in Delphi 3, this assertion replacement only works correctly in Delphi 4 and later.

Optional Parameters For COM Automation

Automation through Variant variables (supported since Delphi 2) allows optional parameters to be omitted, where you are happy with their default values. So, if you are automating Microsoft Word, and you wish to save a document to disk you can call the SaveAs method of the document object, and ignore most of the eleven parameters that it takes, as shown in Listing 3.

Listing 3: Automation using a Variant

uses
  ComObj;

procedure TForm1.Button1Click(Sender: TObject);
var
  WordApplication, WordDocument: Variant;
begin
  WordApplication := CreateOleObject('Word.Application');
  WordDocument := WordApplication.Documents.Add;
  WordApplication.Selection.TypeText('Hello world');
  WordDocument.SaveAs(FileName := 'C:\Doc.Doc', AddToRecentFiles := False);
  WordApplication.Quit(False)
end;

When Automating through proper COM interfaces (supported since Delphi 3), Delphi does not allow you to miss out any arguments (unfortunate when there are many of them). However, Delphi 4 does provide an undocumented variable that you can pass in place of those you have no interest in. This EmptyParam variable is defined in the block of global variables we have been looking at, and is used in Listing 4.

Listing 4: Automation using a COM interface, and using EmptyParam

uses
  Word_TLB;

procedure TForm1.Button1Click(Sender: TObject);
var
  WordApplication: _Application;
  WordDocument: _Document;
  TmpVariant, TmpVariant2: OleVariant;
begin
  WordApplication := CoApplication.Create;
  WordDocument := WordApplication.Documents.Add(EmptyParam, EmptyParam);
  WordApplication.Selection.TypeText('Hello world');
  TmpVariant := 'C:\Doc.Doc';
  TmpVariant2 := False;
  WordDocument.SaveAs(TmpVariant, EmptyParam, EmptyParam,
    EmptyParam, TmpVariant2, EmptyParam, EmptyParam, 
    EmptyParam, EmptyParam, EmptyParam, EmptyParam);
  WordApplication.Quit(TmpVariant2, EmptyParam, EmptyParam);
end;

Delphi 5 helps a bit further. Firstly, it wraps Automation servers into components, and pre-installs several common instances onto the component palette. Secondly, it overloads the methods that take optional parameters and defines various versions with varying numbers of parameters. This means you can call the appropriate overloaded version and omit the last n parameters.

Listing 5 works after dropping a TWordApplication component on the form, with its ConnectKind property set to ckNewInstance, and then dropping a TWordDocument on the form with ConnectKind set to ckAttachToInterface.

Listing 5: Automation using Delphi 5's new Automation server components

procedure TForm1.Button1Click(Sender: TObject);
var
  TmpVariant, TmpVariant2: OleVariant;
begin
  WordDocument.ConnectTo(WordApplication.Documents.Add(EmptyParam, EmptyParam));
  WordApplication.Selection.TypeText('Hello world');
  TmpVariant := 'C:\Doc.Doc';
  TmpVariant2 := False;
  WordDocument.SaveAs(TmpVariant, EmptyParam,
    EmptyParam, EmptyParam, TmpVariant2);
  WordApplication.Quit(TmpVariant2);
end;

Application Memory Consumption

Two more global variables that appear in amongst these global variables (in 32-bit versions of Delphi) are useful in keeping track of how much memory your application is using at any given time. AllocMemCount keeps a record of the number of memory allocations still in use. AllocMemSize represents the number of bytes of memory that have yet to be released back to the system. If this value keeps rising unexpectedly, look for some memory leaks.

More Debug Support through Variants

Automation through a Variant variable relies on compiler magic. Ultimately, code in the ComObj unit (in Delphi 3 or later) or OleAuto unit (in Delphi 2) picks up all the methods, properties and parameters you pass, and sends them off to the Automation server. If you are not interested in Automating, you can still use a Variant and pick up this information yourself, giving you another debug trace option.

Past the block of global variables in the System unit, after the list of standard RTL routine and Variant routine declarations, you find another one of these pointer variables called VarDispProc.

When you perform Automation, you use the ComObj (or OleAuto) unit to call CreateOleObject. The startup code for this unit also assigns the address of the workhorse Automation interpreter routine to VarDispProc.

If you want to use assertions in your program, but you like the idea of a cheap debug trace utility, this variable offers us another choice. We can write a small unit with a replacement routine whose address can be assigned to VarDispProc. This routine will be passed all details of anything that looks (to the compiler) like an attempt at late-bound Automation through a Variant variable.

To see the layout of the routine, check the ComObj source file in Delphi’s RTL\Sys directory. The routine takes a pointer to a Variant, which is normally used to store a result if appropriate. Also passed are the Variant that represents the Automation server, a pointer to a TCallDesc record containing information about the Automation call, and a pointer that points to any parameters passed.

So with this set up, you can take any Variant, pretend it represents an Automation server, call any made-up method, passing a string parameter. In the new pick-up routine, the Params parameter will point to the string and, for example, it can be passed to OutputDebugString.

Listing 6 shows a possible implementation of such a unit, set up to only work if the /Debug command-line switch. You can find the code in the VarDispHandlerU.pas unit that accompanies this paper. The code can be triggered using the variable declared in the interface section (or any other Variant variable) like this:

Msg.DoIt('Hello world')

Listing 6: Using Variant Automation logic for debugging purposes

unit VarDispHandlerU;

interface

var
  Msg: Variant;

implementation

uses
  Windows, SysUtils;

type
  { Dispatch call descriptor }
  PCallDesc = ^TCallDesc;
  TCallDesc = packed record
    CallType: Byte;
    ArgCount: Byte;
    NamedArgCount: Byte;
    ArgTypes: array[0..255] of Byte;
  end;

procedure VarDispDebugHandler(Result: PVariant; const Instance: Variant;
  CallDesc: PCallDesc; Params: Pointer); cdecl;
begin
  OutputDebugString(PChar(Params))
end;

procedure VarDispNoDebugHandler(Result: PVariant; const Instance: Variant;
  CallDesc: PCallDesc; Params: Pointer); cdecl;
begin
end;

initialization
  if FindCmdLineSwitch('Debug', ['/', '-'], True) then
    VarDispProc := @VarDispDebugHandler
  else
    VarDispProc := @VarDispNoDebugHandler;
end.

The SysUtils Unit

Now let’s turn our attention to the other primary RTL unit, SysUtils.

As you go scrolling through the file, you can see constants that can be assigned to the FileMode system variable. This allows you to dictate the sharing modes of files access through standard file variables and opened with Reset. These constants are also used when opening files via file handles, with FileOpen.

Next come the constants that can be passed to FindFirst, shortly followed by some constants that represent the number of seconds and milliseconds in a calendar day. These constants are followed by simple records that can be used as typecasts against integral numbers to access their individual bytes, words or double words.

Further down you see some of the core exception classes being defined. Unless SysUtils is used in your project (which it is for all VCL applications), you get no exception handling support.

Identifying Windows Version

Further down the interface section in 32-bit Delphi versions you can see an under-used variable, Win32Platform, that can tell you which Windows platform you are running on (Windows or Windows NT). This helps when calling OS-dependant Win32 APIs.

Delphi 3 added some additional support variables: Win32MajorVersion, Win32MinorVersion and Win32BuildNumber, along with Win32CSDVersion.

If Win32Platform says you are running Windows (not NT), then you can use the version number variables to distinguish between Windows 95 and Windows 98. If Win32MajorVersion is more than 4, or if it is 4 and Win32MinorVersion is more than 0 (in other words your complete version number is greater than or equal to 4.0), you are running Windows 98, otherwise you are running Windows 95.

Listing 7 shows how to distinguish between the current platform versions, although this will need extending once the version number for Windows Millennium Edition is known.

Listing 7: Detecting the current Windows version

type
  TWin32Version = (wvUnknown, wvWin95, wvWin98, wvWinNT, wvWin2000);

function Win32Version: TWin32Version;
begin
  Result := wvUnknown;
  if Win32Platform = VER_PLATFORM_WIN32_WINDOWS then
    if (Win32MajorVersion > 4) or
       ((Win32MajorVersion = 4) and (Win32MinorVersion > 0)) then
      Result := wvWin98
    else
      Result := wvWin95
  else
    if Win32MajorVersion <= 4 then
      Result := wvWinNT
    else
      if Win32MajorVersion = 5 then
        Result := wvWin2000
end;

Additional Memory Management

There is more to RTL heap support than GetMem/FreeMem and New/Dispose. A little further down in the unit is the declaration of AllocMem. This is very much like GetMem, but initialises the allocated block of memory with zero byte values, saving you the trouble. Memory allocated with AllocMem must be freed with FreeMem.

API Call Exception Producer

As we proceed down the unit, we encounter a large number of string, file, disk and date/time manipulation routines. You may be unfamiliar with a lot of these (such as StrToIntDef, FloatToStrF, FormatFloat and FileSearch) so they are worth a glance through, and you will find them all very healthily commented.

After these you will see a function called SysErrorMessage. This takes a Win32 error code as returned from GetLastError (which you typically call after an API returns False), and asks Windows to produce a string describing the problem.

Delphi 3 (and later) also has a couple of extra related routines. RaiseLastWin32Error can be called after an API fails, and will call raise an EWin32Error exception which will describe the API failure (thanks to it calling SysErrorMessage). Win32Check takes a Boolean parameter. It is designed to take the return value of a Win32 API. If the return is False, it calls RaiseLastWin32Error for you, otherwise it returns the API return value.

Large Disk Space

Delphi 4 adds in a wrapper for a new Win32 API to calculate free disk space. The normal API, GetFreeDiskSpace is not able to report values greater than 2 gigabytes, so Windows 95 OSR2 and Windows NT 4.0 added a new routine called GetFreediskSpaceEx. Since the API may not exist, SysUtils declares GetDiskFreeSpaceEx as a function pointer that either gets set to point at the new API, or if it does not exists, it is set to point at an internal routine that calls the original API.

Thread Helper Object

Delphi 4 also added a new utility class to SysUtils to help when writing multi-threaded applications that make use of shared resources. TMultiReadExclusiveWriteSynchronizer is a class that will allow many read operations (through calls to BeginRead and EndRead), but only allows one write operation at a time. Any calls to BeginWrite whilst a client is already writing will be blocked until the writer calls EndWrite.

This class can be considered an associate of the TCriticalSection class from the SyncObjs unit, which allows exclusive access to be gained and released using its Acquire and Release methods. The TCriticalSection class is a shallow wrapper around a Windows TRTLCriticalSection.

The VCL uses instances of this object for various lists that may be used from multiple threads in the context of form loading, and the COM class manager

Lesser Known Utility Routines

Delphi 4 adds StringReplace (convenient customisable search and replace within a string variable) and FindCmdLineSwitch (makes a doddle out of looking for command-line parameters). Delphi 5 adds FreeAndNil (to both call an object’s destructor and set the object reference back to nil) and SafeLoadLibrary. This last one disables any Windows error dialogs whilst loading a specified DLL and also preserves the FPU control word, to protect you against DLLs that change it during initialisation, which is common in Microsoft DLLs.

The Visual Component Library (VCL)

Miscellaneous

"Cheats" Used By Inprise R&D

Sometimes, you need access to the protected members of an object that is defined in some unit somewhere, but you will only be allowed access to the public and published members, as per the rules of the language.

However, if you examine the definition of the protected section, you will see that anything in the same unit as a class can access the protected members. Consequently, you can define a new class that inherits from the class in question, but adds nothing to it. This class can be used to typecast the object whose protected members you seek, and so the compiler will then give you access to them.

You can find many definitions of these shallow access classes or hack classes spread across the VCL source, including the Forms, VCLCOM, AxCtrls, ComCtrls, DBCtrls, and ExtCtrls units . In Delphi 3 or later, you could choose Search | Find in Files… (or use a GREP-like tool in earlier versions) and search through the VCL source for the string: Access = class(T.

For example, in the 32-bit DBCtrls unit, there is an access class defined as follows:


type
  TWinControlAccess = class(TWinControl);

This is then used by the TPaintControl class to call the protected CreateParams method of its owner (a TWinControl).

The Classes Unit

This unit defines the base TComponent class, and also houses all the code that reads and writes form files. So you will find the stream classes here as well as a lot of code used to parse binary stream files.

Windows Support Routines And Others

Many VCL and Windows API routines require TPoint or TRect records. To save declaring local variables and filling in the fields, you can use the set of routines declared in this unit. Point takes an X and Y co-ordinate, and produces a TPoint. Rect takes the left, top, right and bottom co-ordinates of a rectangle and manufactures a TRect record. Bounds is very similar to Rect but takes left, top, width and height information.

In Delphi 4 (and later), you will also find the undocumented BinToHex and HexToBin. These translate between binary data and a textual representation of it. They exist in all versions of Delphi, but Delphi 4 is the first to surface them outside the unit.

For example, an Extended variable is 10 bytes in size. Each byte is represented as two hexadecimal characters, so 20 characters are needed to display its contents in pure text. Listing 8 translates the 10 bytes of an Extended into a string (PChar) and then back again.

Listing 8: Using BinToHex and HexToBin

//Buffer is binary data,
//Text is target text buffer (assumed to be big enough),
//BufSize is size of binary data block
//procedure BinToHex(Buffer, Text: PChar; BufSize: Integer);

//Text is textual representation of binary data,
//Buffer is target binary data buffer
//BufSize is size of textual data buffer
//function HexToBin(Text, Buffer: PChar; BufSize: Integer): Integer;

procedure TForm1.Button1Click(Sender: TObject);
var
  E: Extended;
  //Make sure there is room for null terminator
  Buf: array[0..SizeOf(Extended) * 2] of Char;
begin
  E := Pi;
  Label1.Caption := Format('E starts off as %.15f', [E]);
  BinToHex(@E, Buf, SizeOf(E));
  //Slot in the null terminator for the PChar, so we can display it easily
  Buf[SizeOf(Buf) - 1] := #0;
  Label2.Caption := Format('As text, the binary contents of E look like %s', [Buf]);
  //Translate just the characters, not the null terminator
  HexToBin(Buf, @E, SizeOf(Buf) - 1);
  Label3.Caption := Format('Back from text to binary, E is now %.15f', [E]);
end;

Turning A Component Into a String And Vice Versa

Translating a component’s definition (i.e. its published properties) to text and back to a component is a doddle with some routines found in here. Doing this allows you to write a component’s state to a text file, to a memo, or to easily send it across a COM link (COM objects can deal with strings easily, but you cannot easily pass a Delphi object across a COM link).

The routines in question are ObjectBinaryToText and ObjectTextToBinary. The Comp2Str project that accompanies this paper uses the two button event handlers (shown below in Listing 9) to turn a memo component into a textual version (placed in another memo) and then turn the textual version back into the real memo. This allows you to change properties of the memo (in the text version) and have them made real. The code uses PChar types and memory allocation, rather than Pascal strings, to ensure the code works in all versions of Delphi. Delphi 1 strings are limited to 255 characters.

Listing 9: Translating a component to a string and vice versa

procedure TForm1.btnComponentToStringClick(Sender: TObject);
var
  InputStream, OutputStream: TMemoryStream;
  CompAsString: PChar;
begin
  InputStream := TMemoryStream.Create;
  OutputStream := TMemoryStream.Create;
  try
    { Put component on stream }
    InputStream.WriteComponent(memGuineaPig);
    { Reset pointer to beginning of input stream }
    InputStream.Seek(0, soFromBeginning);
    { Turn component into string }
    ObjectBinaryToText(InputStream, OutputStream);
    { Allocate a buffer to store string characters along with a null terminator }
    GetMem(CompAsString, OutputStream.Size + 1);
    try
      { Deal with terminator }
      CompAsString[OutputStream.Size] := #0;
      { Copy string to PChar buffer }
      Move(OutputStream.Memory^, CompAsString^, OutputStream.Size);
      { Get string displayed in memo }
      memComponentString.SetTextBuf(CompAsString)
    finally
      FreeMem(CompAsString, OutputStream.Size + 1)
    end
  finally
    InputStream.Free;
    OutputStream.Free;
  end;
end;

procedure TForm1.btnStringToComponentClick(Sender: TObject);
var
  InputStream, OutputStream: TMemoryStream;
begin
  InputStream := TMemoryStream.Create;
  OutputStream := TMemoryStream.Create;
  try
    { Make room for memo text and null terminator}
    InputStream.SetSize(memComponentString.GetTextLen + 1);
    { Copy memo text into stream }
    memComponentString.GetTextBuf(InputStream.Memory, InputStream.Size);
    { Go back to start of input stream }
    InputStream.Seek(0, soFromBeginning);
    { Turn string into component }
    ObjectTextToBinary(InputStream, OutputStream);
    { Go back to start of output stream }
    OutputStream.Seek(0, soFromBeginning);
    { Overwrite the old component properties with the new ones }
    OutputStream.ReadComponent(memGuineaPig)
  finally
    InputStream.Free;
    OutputStream.Free
  end
end;

The Forms Unit

This is where TApplication, TForm and TScreen are defined.

Getting A Simple Window

Sometimes, you need a small window to pick up messages from, without going to the extent of a form. Certainly the VCL clipboard and timer support code (amongst other areas) do. This unit has (undocumented) routines to do just that.

Assuming you have a method in an object that will act as the window procedure, and takes a TMessage record as a var parameter, you can pass it to AllocateHWnd. You get back a HWnd (window handle) that needs to be passed to DeallocateHWnd when you are done. You can then do what you like with the window handle, passing it to whomever you like, and the method will be invoked for all messages that are posted or sent to it.

The Dialogs Unit

This is where all the common dialog components live, along with the VCL message box routines.

More Ways To Show A Dialog

Browsing through the interface section of this unit, you will go past the common dialog component definitions and then hit a number of utility routines. Whilst you may be familiar with ShowMessage and MessageDlg as ways of getting a simple message box in the centre of the screen, you may not have encountered MessageDlgPos or ShowMessagePos. These allow you to place the dialog anywhere you like on the screen.

Additionally, since Delphi 3.01 you have the option of using ShowMessageFmt. This routine has the same parameter list as Format. It builds up a string with the parameters and passes it to ShowMessage, saving you nesting a call to Format within the call to ShowMessage yourself.

CreateMessageDialog is the underlying function of all these dialog routines. It takes a message string, a dialog type, and a set of button constants and creates the message box form. Instead of displaying the form, it simply returns it. You can then display it as often as you like, by calling the form’s ShowModal method. If you use some message boxes regularly, this routine will be more efficient.

Also new to some people will be InputQuery and InputBox, which produce simple customisable input dialogs, designed to get one string from the user.

Delphi 5.0 also adds a new global variable called ForceCurrentDirectory. This can be set to True to avoid Windows 98 and Windows 2000 defaulting to the My Documents folder when the initial directory is blank. Instead, this causes them to stick to the current directory.

Summary

In this session we looked at some of the VCL (and RTL) source code to see if there are useful variables, constants, subroutines and techniques that we can take advantage of.

Clearly, in just one hour, the extent of our travels through the source is fairly limited. But hopefully, for those who have not previously done so, just browsing through a few VCL source files will have removed some of the mystique or fear-factor that may have been present.

The VCL source is provided for your reference and for your education. Reading it, becoming familiar with it and using it will help you become a better Delphi developer, more at ease with the wealth of functionality available to you.

Click here to download the files associated with this paper.

About Brian Long

Brian Long used to work at Borland UK, performing a number of duties including Technical Support on all the programming tools. Since leaving in 1995, Brian has spent the intervening years as a trainer, trouble-shooter and mentor focusing on the use of the C#, Delphi and C++ languages, and of the Win32 and .NET platforms. In his spare time Brian actively researches and employs strategies for the convenient identification, isolation and removal of malware. If you need training in these areas or need solutions to problems you have with them, please get in touch or visit Brian's Web site.

Brian authored a Borland Pascal problem-solving book in 1994 and occasionally acts as a Technical Editor for Wiley (previously Sybex); he was the Technical Editor for Mastering Delphi 7 and Mastering Delphi 2005 and also contributed a chapter to Delphi for .NET Developer Guide. Brian is a regular columnist in The Delphi Magazine and has had numerous articles published in Developer's Review, Computing, Delphi Developer's Journal and EXE Magazine. He was nominated for the Spirit of Delphi award in 2000.