Delphi tools: Compiler commands

Erik Stok (aka Baldo)

Delphi biedt voor het compileren van projecten in een project group geen andere opdachten dan ‘build all’ en ‘build current project’. In Delphi 7 is eindelijk de optie ‘build all from current project’ erbij gekomen, maar daar hebben gebruikers van eerdere Delphi versies zoals ik vrij weinig aan. Een reden om weer eens een blik te werpen op de open tools api.

Het idee

Als je met Delphi aan het werk bent en je werkt met een project group dan blijkt dat je nogal eens zit te wisselen tussen projecten om het betreffende project gecompileerd te krijgen. Als je een wijziging maakt in een unit die niet in je actieve project zit dan moet je eerst het project van de betreffende unit actief maken, vervolgens compileren, en daarna eventueel het vorige project weer actief maken omdat dat degene is die je wilt starten met de optie ‘run’.

Ook mist de faciliteit om vanaf een bepaald project te kunnen bouwen. Als een project group bijvoorbeeld afhankelijke packages bevat in hiërarchische volgorde, dan zou het wel handig zijn als er vanaf een bepaald package een build kan worden gedaan. Dit komt zeker van pas bij dynamische applicaties.

Het idee is dus om twee opties voor de compiler erbij te maken: “build project of current unit” en “build from current project”.

De OTA

De open tools api biedt via het IOTAProjectBuilder interface mogelijkheden om de Delphi project builder aan het werk te zetten. Een referentie naar deze project builder kan worden verkregen via het IOTAProject interface van een project. Via het IOTAProjectGroup interface kan toegang worden verkregen tot de verschillende projecten in de project group. Al deze interfaces staan uitvoerig beschreven in de Delphi help (zoek op één van de interface namen), dus op de details van deze interfaces zal ik verder niet ingaan. Het zijn wel de noodzakelijke ingrediënten voor de IDE uitbreiding die we gaan maken, dus het gebruik ervan zal ik wel degelijk bespreken.

Build project of current unit

Om het project uit de project group te kunnen compileren waarin de huidige unit zich bevindt zal eerst moeten worden bepaald welk project dit is. Daarvoor zal door alle projecten van de project group gelopen moeten worden om te bepalen in welk project de huidige unit zich bevindt.

Zoals in het vorige open tools api artikel al te lezen was, kent een project in de open tools api geen units, alleen maar modules. Ik heb toen ook behandeld hoe een referentie naar de huidige module verkregen kan worden. Van de huidige module zal dus moeten worden bepaald in welk project deze zich bevindt.

De CurrentProjectGroup functie geeft een referentie terug naar de project group en ziet er als volgt uit:

function GetProjectGroup: IOTAProjectGroup;
var
ModuleServices : IOTAModuleServices;
i : Integer;
begin
// Standaard resultaat
Result := nil;

// Verkrijg een referentie naar de moduloe services
ModuleServices := (BorlandIDEServices as IOTAModuleServices);
// Loop door de modules
for i := 0 to ModuleServices.ModuleCount - 1 do
begin
// Als een van de modules het IOTAProjectGroup interface implementeert,
// dan is er een project group gevonden
if (ModuleServices.Modules[i].QueryInterface( IOTAProjectGroup, Result) = S_OK) then
Break;
end;
// Laat referentie naar interface los
ModuleServices := nil;
end;

Zoals te zien is, is de project group ook gewoon een van de modules die geopend is in de Delphi IDE. Door aan een module te vragen of deze het IOTAProjectGroup interface implementeert, komen we te weten of het de project group module is. Er is gelukkig altijd maar één project group module en dat vereenvoudigd het zoeken aanzienlijk.

Om te bepalen of een module zich in een project uit de project group bevindt, kan worden gekeken naar het IOTAProject interface van elk project in de project group. Door gebruik te maken van de GetModuleCount method van het IOTAProject interface kan het aantal modules van een project worden bepaald. Door vervolgens gebruik te maken van de GetModule method kan van iedere module een interface referentie worden opgevraagd. Door de Filename property van de huidige module en de module uit het project te vergelijken kan worden bepaald of het dezelfde module betreft en dus of de module in het betreffende project is opgenomen.

// Tot dusverre is er geen project gevonden waarin de huidige module
// zich bevindt
ModuleProjectIndex := -1;
// Loop door alle projecten van de projectgroup
for p := 0 to ProjectGroup.ProjectCount - 1 do
begin // Loop door alle modules van het project
for m := 0 to ProjectGroup.Projects[p].GetModuleCount - 1 do
begin // Als de filename van de module module overeenkomst met de filename
// van de huidige module dan wordt het project als gevonden beschouwd
if SameText(ProjectGroup.Projects[p].GetModule(m).FileName, Module.FileName) then
begin
// Onthou index
ModuleProjectIndex := p;
// Stop loop
Break;
end;
end; // Als er iets gevonden is, stop dan de loop
if ModuleProjectIndex <> -1 then
Break;
end;

Zodra er een project gevonden is waar de actieve module zich in bevindt weten we dat dit het project is dat gecompileerd moet worden. Er kan nu via de ProjectBuilder property van het project interface opdracht gegeven worden om te builden. Er moet daarbij wel rekening gehouden worden met het feit dat het package waarin deze functionaliteit geïmplementeerd wordt zelf natuurlijk niet gecompileerd wordt, want dat zou natuurlijk niet werken. Voor deze vergelijking vind ik een controle op de bestandsnaam van het package voldoende, hoewel het niet honderd procent waterdicht is. De kans dat er andere packages die precies op ‘idemenu.dpk’ eindigen acht ik klein genoeg.

if Pos('IDEMENU.DPK', 
         UpperCase(
           ExtractFileName(
             ProjectGroup.Projects[ModuleProjectIndex].FileName))) = 0 then
ProjectGroup.Projects[ModuleProjectIndex].ProjectBuilder.BuildProject( cmOTAMake, False);

Build from current project

Volgens een zelfde strategie kan worden bepaald wat het huidige project is en welke projects er dan gebuild moeten worden als er vanaf het huidige project gebuild moet worden. Een referentie verkrijgen naar de project group verloopt op dezelfde wijze als bij ‘build project of current unit’. Aangezien de CurrentProjectGroup functie dubbel is opgenomen is het wellicht handig om een aparte unit met open tools api functies te beginnen waarin al dergelijke functies zijn opgenomen. Om de eenvoud van distributie van de individuele IDE menu uitbreidingen niet te verhogen laat ik dat hier achterwege.

Nadat een referentie naar de project group verkregen is moet het huidige project bepaald worden. Daar heeft de project group gelukkig een property voor, de ActiveProject property. We willen echter niet het IOTAProject interface van het huidige project weten, maar de index van het huidige project in de lijst van projecten in de project group. Daar biedt het IOTAProjectGroup interface geen faciliteiten voor, dus er zit niets anders op dan één voor één door de projecten te lopen om het actieve project te vinden.

// Loop door alle projecten van de project group
for p := 0 to ProjectGroup.ProjectCount - 1 do
begin
// Vergelijk of het project dezelfde filename heeft als het actieve project
if SameText(ProjectGroup.Projects[p].FileName, ProjectGroup.ActiveProject.FileName) then
begin
// Zo ja dan is het huidige project gevonden. Onthou de index.
CurrentProjectIndex := p;
// Doorbreek de loop
Break;
end; end;

Zodra de index van het actieve project bekend is, kan de lijst van de projecten vanaf dat punt worden afgelopen om elk van de projecten te builden. Omdat ik niet na het builden van ieder project op OK wil klikken, geef ik alleen na het builden van het laatste project een boodschap weer. Wederom geldt dat het IdeMenu package zelf niet gecompileerd mag worden.

// Loop door de projecten van de project group
for p := CurrentProjectIndex to ProjectGroup.ProjectCount - 1 do
begin
// Compileer nooit het package waarin deze code zit. Geef alleen een
// boodschap weer bij het laatste package.
if Pos('IDEMENU.DPK', UpperCase(ExtractFileName(ProjectGroup.Projects[p].FileName))) = 0 then ProjectGroup.Projects[p].ProjectBuilder.BuildProject( cmOTAMake, p = (ProjectGroup.ProjectCount - 1) end;

De menu opties

Om de nieuwe compiler opdrachten beschikbaar te maken wordt weer gebruik gemaakt van de IDE menu uitbreiding die in een eerder artikel al beschreven staat. Wederom geldt dat het toevoegen van unit uIdeMenuBuildFromCurrent en unit uIdeMenuBuildUnitProject aan de uses clause van unit uIdeMenuExpert en het aanmaken van een instance van beide classes in de constructor van de expert voldoende is. Natuurlijk is voor de mensen die liever niet zelf de code aanpassen ook een aangepaste versie van het IdeMenu package beschikbaar.

Conclusie

Delphi mist duidelijk een aantal manieren om de compiler aan te roepen. Gelukkig is de open tools api echt “open”, zodat we zelf in staat zijn de compiler opdrachten te geven. Door gebruik te maken van wat de open tools api interfaces aanbieden en wat handige mogelijkheden om het Delphi IDE menu uit te breiden is Delphi voorzien van wederom een handige toevoeging.