 |
 |
 |
Delphi CompileTime
Marco Wobben 6 aug 2002
[download]
[codecentral]
[email]
Introduction
Since a few Delphi versions we
are able to put version information in our executables and the list of features
has grown to language modules which can be dynamically loaded, multitier
data transfer, soap, xml and more and more and more. Yet a simple thing as
a compiletime to be available for the application itself has not yet been
added to the new features of Delphi.
This
article describes how an extension to Delphi is made to generate an sources
with compiletime information (to be used in the project you're working at).
Along the way I will show you several implementations demonstrating how to
extend Delphi using different Tools Api Interfaces. The final output will
look something like this:
const CompileTime: TDateTime = 37462.6273997917; const CompileProject = 'C:\...\CompileTime.dpk'; const CompileUnits = 6; const CompileForms = 0; const CompileBy = 'Marco Wobben'; const CompilePC = 'ARIES'; const CompileEMail = 'marcow@bcp-software.nl'; const CompileURL = 'www.bcp-software.nl';
const CompileOS = 'Windows NT v5.0 build 2195';
|
OpenTools API
A
well documented api is the OpenTools api in Delphi. Well documented by Ray
Lischner at the time of Delphi 3. Although major changes have been made to
these api's the book Ray wrote still stands for the major part and is a good
book to read. Based upon insights from this book I finally went on building
an extension for Delphi to accomplish a compiler detection system to include
some timestamp in code to be compiled in the application.
TIAddInNotifier
All I've done is assemble a new Delphi designtime package with a single unit
in it. This unit contains the single class which does the work. It descents
from TIAddInNotifier and overrides some methods:
uses Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, DesignIntf, ToolIntf;
type TCompileNotifier = class(TIAddInNotifier) public constructor Create; virtual; destructor destroy; override; procedure EventNotification( NotifyCode: TEventNotification; var Cancel: boolean); override; procedure FileNotification( NotifyCode: TFileNotification; const FileName: string; var Cancel: boolean); override; end;
|
Before Compile
The interesting method is the EventNotification. This has an NotifyCode as
parameter which we can use to detect the instance just before the compilation
of our program takes place. This is the place to be to generate an include
file to be used in our project.
Note: This code snippet is simplified and the actual
implementation is a tiny bit more complex to service configuration options;
but the idea below is the basic functionality.
procedure TCompileNotifier.EventNotification( NotifyCode: TEventNotification; var Cancel: boolean); var P: string; T: TStringList; N: TDateTime; by, pc: string; L: Cardinal; WasOpen: boolean; begin if (NotifyCode = enBeforeCompile) and (ToolServices <> nil) then begin // retrieve project directory, generate include file P := ToolServices.GetProjectName; // if an unsaved project then abort if not FileExists(P) or (ExtractFilePath(P) = '') then Exit; // create an include file besides the projec file // so the project may find it P := IncludeTrailingPathDelimiter( ExtractFilePath(P))+ ctFile; // remember if the include file was already open WasOpen := ToolServices.IsFileOpen(P); // generate the contents for the include file T := TStringList.Create; try N := Now; ... // generate the include files content here
... // save the new include file if (not FileExists(P)) or (not FileIsReadOnly(P)) then try T.SaveToFile(P); except on E: Exception do MessageDlg('Can not write to :'#13#10+ P+#13#10+E.Message, mtError, [mbOK], 0); end finally T.Free; // reload the include file if it was already open if WasOpen then ToolServices.ReloadFile(P); end; end; end;
|
Include
Now the include file the expert generates is simple
and contains somewhat of the following information. (But off course one may
alter depending on the need for information.) Once the package is loaded
inro Delphi every project that gets compiled will generate an include file
with information about this moment in time. Load your project in Delphi and
open the aboutbox form (or something else to display the information). and
Do something similar as this:
implementation
{$R *.dfm}
{$I CompileTime.Inc}
procedure TAboutBox.FormCreate(Sender: TObject); begin Label1.Caption := 'Compiled at '+DateTimeToStr(CompileTime)+ ' by '+CompileBy; end;
|
Configuration
Everybody want personalization. And so we add more
configuration options. The list of settings is extended offering EMail and
URL information to be added to the include file. Also we offer a convenient
configuration form in the Tools menu of Delphi. To enable this the code unit
is extended with some records to contain personal information which controls
the include file content. On the other hand we need the configuration form
itself:

Wizard container
For this we need to create a simple object descendant which will contain
the menuitem and the action to be hooked into the IDE and a Form to configure
the records with personal information. And a minor detail add persistency
to the settings. Important in the design is to leave the core unit as it
is, so one does not need the configuration form to compile the expert.
TCompileTimeWizard = class private FConfigAction: TAction; FConfigMenuItem: TMenuItem; protected procedure OnConfigExecute(Sender: TObject); procedure RemoveMenuItem; procedure RemoveToolBar; public constructor Create; destructor Destroy; override; end
|
Wizard create
This wizards life time cycle is managed from the initialization and finalization
of the unit containing this class declaration. In turn this instance manages
the FConfigAction and the FConfigMenuItem. The code snippet below shows how
the wizard prepares the action and adds a menuitem to the Tools menu.
constructor TCompileTimeWizard.Create; var I: integer; Services: INTAServices; Bmp: TBitmap; begin inherited; Supports(BorlandIDEServices, INTAServices, Services); // register the new button on the imagelist Bmp := TBitmap.Create; Bmp.LoadFromResourceName(hInstance, 'COMPILETIMEBTN'); I := Services.AddMasked( Bmp, Bmp.TransparentColor, 'CompileTime.WizardButton'); Bmp.Free; // create a new action FConfigAction := TAction.Create(nil); FConfigAction.Category := 'Tools'; FConfigAction.Caption := 'CompileTime Options...'; FConfigAction.Hint := FConfigAction.Caption; FConfigAction.ImageIndex := I; FConfigAction.OnUpdate := nil; FConfigAction.OnExecute := OnConfigExecute; // create a menuitem hooked to the action for I := 0 to Services.MainMenu.Items.Count - 1 do begin with Services.MainMenu.Items[I] do begin if (CompareText(Name, 'ToolsMenu') = 0) then begin FConfigMenuItem := TMenuItem.Create(nil); FConfigMenuItem.Action := FConfigAction; Insert(0, FConfigMenuItem); Break; end; end; end; end;
|
Execute and save The
Actions execute simple starts the form in a modal way and the form takes it
from there. Once the ok is pressed the settings are saved back to the records
and to the registry using the Services interface to locate the registry entry
for the current Delphi version. Note that for the similar operation LoadSettings
is not called in the form but by the initialization part of this unit. This
way the personal information records are initialized when the package is
loaded into the IDE and the core unit is properly setup.
procedure TCompileTimeWizard.OnConfigExecute( Sender: TObject); begin TCompileTimeFrm.Create(nil).ShowModal; end;
...
procedure TCompileTimeFrm.SaveSettings; var Reg: TRegistry; Services: IOTAServices; begin Supports(BorlandIDEServices, IOTAServices, Services); // convert form controls to settings variable Settings.FileType := TGenerateFileType(FileTypeGrp.ItemIndex); ... PersonalInfo.URL := UrlEdt.Text; // save the settings to the current_user registry Reg := TRegistry.Create; try Reg.RootKey := HKEY_CURRENT_USER; if Reg.OpenKey( Services.GetBaseRegistryKey+'\CompileTime', True) then begin Reg.WriteInteger('FileType', Ord(Settings.FileType)); ... Reg.WriteString('URL', PersonalInfo.URL); end; finally Reg.Free; end; end;
|
Wizard destroy
According to the Delphi Help we only need to destroy the MenuItem and the
menu no longer has any reference to this wizard. For toolbar buttons however
this is different. We manually have to search for any toolbar buttons in
all toolbars in the IDE to locate any instance which points to our action
and remove them. Since this will lead to disappearing toolbar buttons once
the package is unloaded (or if the IDE is started while the package is not
yet loaded) we will prevent this from ever happening. We simply do not assign
any ActionList to the action, this will make sure the action can not be placed
on a toolbar. Note if you wish to experiment uncomment the FConfigAction.ActionList assignment line.
Conclusion In a simple unit in a single package one can create
impressive behaviour improvements which give greater feedback about the executable
a user is working with. No more file timestamps to check, simply add the
include file to your about box, display some constant values and all the
information your helpdesk requires is available.
If
you want this available in your Delphi version, open the Package from the
zipfile and press the install button from the Package Manager or simply add the bpl file itself. Now you're
ready, open a project and go include.
Copyright © 2002 Bommeljé Crompvoets en partners
Het is toegestaan dit artikel in zijn geheel te kopiëren en te verspreiden,
mits de tekst woordelijk in tact blijft en deze notitie bevat. Het is toegestaan
om deze tekst te citeren, of te wijzigen, mits de oorspronkelijke auteur
en houder van het copyright vermeld worden.
You may republish this paper verbatim, including this
notation. You may update, correct, or expand the material, provided that
you include a notation stating the original author and copyright holder.
|