In this chapter I’m going to show you how to create projects in the IDE. In the next chapter I’ll extend this to add modules to those projects.
Firstly, for the purposes of doing something useful we need a form which allow the user to select what they want to be created in a new project. We going to extend the project repository wizard so that you can create templates for Open Tools API projects. Below is the form that I’ve design and below that I’ll explain some of the code behind it as this will help aid us in implementing the project and module creators.
In order to help with the configuration of this I’ve created some enumerates and sets as below. These define the types of project that can be generated (Package or DLL) and the modules that should be included in the project (based on the stuff we’ve covered so far).
TProjectType = (ptPackage, ptDLL); TAdditionalModule = ( amInitialiseOTAInterface, amUtilityFunctions, amCompilerNotifierInterface, amEditorNotifierInterface, amIDENotfierInterface, amKeybaordBindingsInterface, amReportioryWizardInterface, amProjectCreatorInterface ); TAdditionalModules = Set Of TAdditionalModule;
Next we define a class method that can be used to invoke the dialogue, configure the form and finally return the users selected results as follows:
Class Function TfrmRepositoryWizard.Execute(var strProjectName : String; var enumProjectType : TProjectType; var enumAdditionalModules : TAdditionalModules): Boolean; Const AdditionalModules : Array[Low(TAdditionalModule)..High(TAdditionalModule)] Of String = ( 'Initialise OTA Interface (Default)', 'OTA Utility Functions (Default)', 'Compiler Notifier Interface Template', 'Editor Notifier Interface Template', 'IDE Notifier Interface Template', 'Keyboard Bindings Interface Template', 'Repository Wizard Interface Template', 'Project Creator Interface Template' ); Var i : TAdditionalModule; iIndex: Integer; Begin Result := False; With TfrmRepositoryWizard.Create(Nil) Do Try edtProjectName.Text := 'MyOTAProject'; rgpProjectType.ItemIndex := 0; // Default Modules enumAdditionalModules := [amInitialiseOTAInterface..amUtilityFunctions]; For i := Low(TAdditionalModule) To High(TAdditionalModule) Do Begin iIndex := lbxAdditionalModules.Items.Add(AdditionalModules[i]); lbxAdditionalModules.Checked[iIndex] := i In enumAdditionalModules; End; If ShowModal = mrOK Then Begin strProjectName := edtProjectName.Text; Case rgpProjectType.ItemIndex Of 0: enumProjectType := ptPackage; 1: enumProjectType := ptDLL; End; For i := Low(TAdditionalModule) To High(TAdditionalModule) Do If lbxAdditionalModules.Checked[Integer(i)] Then Include(enumAdditionalModules, i) Else Exclude(enumAdditionalModules, i); Result := True; End; Finally Free; End; End;
I find the use of enumerates and sets in the manner set out above a very useful and flexible way of configuring boolean options as it becomes very easy to add another option without having to reconfigure the dialogue with check boxes.
Next we need to validate the input of the form so that we do not get erroneous information. This is achieved in 3 parts. Firstly, an OnClickCheck event handler for the checked list box to ensure that the first 2 modules are always checked as they are needed for all other modules:
procedure TfrmRepositoryWizard.lbxAdditionalModulesClickCheck(Sender: TObject); begin // Always ensure the default modules are Checked! lbxAdditionalModules.Checked[0] := True; lbxAdditionalModules.Checked[1] := True; end;
Next there is an OnKeyPress event handler for the edit box to allow only valid identifier characters as follows:
procedure TfrmRepositoryWizard.edtProjectNameKeyPress(Sender: TObject; var Key: Char); begin {$IFNDEF D2009} If Not (Key In ['a'..'z', 'A'..'Z', '0'..'9', '_']) Then {$ELSE} If Not CharInSet(Key, ['a'..'z', 'A'..'Z', '0'..'9', '_']) Then {$ENDIF} Key := #0; end;
Finally an OnClick event handler for the OK button to ensure the follow:
- To ensure that the project name is not null;
- To ensure the project name starts with a letter or underscore (identifier requirement);
- To ensure the project name does not already exist in the IDE (requirement of the IDE from 2009 onwards).
procedure TfrmRepositoryWizard.btnOKClick(Sender: TObject); Var boolProjectNameOK: Boolean; PG : IOTAProjectGroup; i: Integer; begin If Length(edtProjectName.Text) = 0 Then Begin MessageDlg('You must specify a name for the project.', mtError, [mbOK], 0); ModalResult := mrNone; Exit; End; {$IFNDEF D2009} If edtProjectName.Text[1] In ['0'..'9'] Then {$ELSE} If CharInSet(edtProjectName.Text[1], ['0'..'9']) Then {$ENDIF} Begin MessageDlg('The project name must start with a letter or underscore.', mtError, [mbOK], 0); ModalResult := mrNone; Exit; End; boolProjectNameOK := True; PG := ProjectGroup; For i := 0 To PG.ProjectCount - 1 Do If CompareText(ChangeFileExt(ExtractFileName(PG.Projects[i].FileName), ''), edtProjectName.Text) = 0 Then Begin boolProjectNameOK := False; Break; End; If Not boolProjectNameOK Then Begin MessageDlg(Format('There is already a project named "%s" in the project group!', [edtProjectName.Text]), mtError, [mbOK], 0); ModalResult := mrNone; End; end;
The code for this can be found the the chapter download at the end of this article.
Next we need to look at the IOTAProjectCreator interface for creating the project itself. Below is the definition of a class that implements this interface (and its descendants):
TProjectCreator = Class(TInterfacedObject, IOTACreator,IOTAProjectCreator {$IFDEF D0005}, IOTAProjectCreator50 {$ENDIF} {$IFDEF D0008}, IOTAProjectCreator80 {$ENDIF} ) {$IFDEF D2005} Strict {$ENDIF} Private FProjectName : String; FProjectType : TProjectType; {$IFDEF D2005} Strict {$ENDIF} Protected Public Constructor Create(strProjectName : String; enumProjectType : TProjectType); // IOTACreator Function GetCreatorType: String; Function GetExisting: Boolean; Function GetFileSystem: String; Function GetOwner: IOTAModule; Function GetUnnamed: Boolean; // IOTAProjectCreator Function GetFileName: String; Function GetOptionFileName: String; {$IFNDEF D0005} Deprecated; {$ENDIF} Function GetShowSource: Boolean; Procedure NewDefaultModule; {$IFNDEF D0005} Deprecated; {$ENDIF} Function NewOptionSource(Const ProjectName: String): IOTAFile; {$IFNDEF D0005} Deprecated; {$ENDIF} Procedure NewProjectResource(Const Project: IOTAProject); Function NewProjectSource(Const ProjectName: String): IOTAFile; {$IFDEF D0005} // IOTAProjectCreator50 Procedure NewDefaultProjectModule(Const Project: IOTAProject); {$ENDIF} {$IFDEF D2005} // IOTAProjectCreator80 Function GetProjectPersonality: String; {$ENDIF} End;
This method is not part of any of the interfaces but is simply a constructor to save the project name and project type so that these can be passed to other functions during the creation process.
constructor TProjectCreator.Create(strProjectName: String; enumProjectType : TProjectType); begin FProjectName := strProjectName; FProjectType := enumProjectType; end;
IOTACreator Methods
All the below methods are common to creators, i.e. will be required by Project and Module creators. These methods are called by the IDE as the item is being created.
The GetCreatorType
method tells the IDE what type of information is to be returned. Since we are going to create the source ourselves then we return an empty string to signify this. If you want default source files generated by the IDE then you need to return the following strings for the following project types and return NIL
from NewProjectSource
.
- Package:
sPackage
; - DLL:
sLibary
; - GUI Project:
sApplication
; - Console Project:
sConsole
.
function TProjectCreator.GetCreatorType: String; begin Result := ''; end;
The GetExisting
method tells the IDE is this is an existing project or a new project. We need a new project so we return False
.
function TProjectCreator.GetExisting: Boolean; begin Result := False; end;
The GetFileSystem
method returns the file system to be used. In our case were return an empty string for the default file system.
function TProjectCreator.GetFileSystem: String; begin Result := ''; end;
The GetOwner
method needs to return the project owner. In our case the current project group, so we pass it the result of our utility function ProjectGroup.
function TProjectCreator.GetOwner: IOTAModule; begin Result := ProjectGroup; end;
The GetUnnamed
method determines whether the IDE will display the SaveAs dialogue on the first occasion when the file needs to be saved thus allowing the user to change the file name and path.
function TProjectCreator.GetUnnamed: Boolean; begin Result := True; end;
IOTAProjectCreator Methods
The below methods are common to Project Creators and are specific to the creation of a new project in the IDE. These methods are called by the IDE as the project is being created.
The GetFileName
method must returns a fully qualified path for the module’s file name. I made a mistake when coding this and did not append the file name with the correct file extension for the DLL and BPL files (i.e. .dpr and .dpk respectively). This caused the IDE to throw an access violation.
function TProjectCreator.GetFileName: String; begin Case FProjectType Of ptPackage: Result := GetCurrentDir + '\' + FProjectName + '.dpk'; ptDLL: Result := GetCurrentDir + '\' + FProjectName + '.dpr'; Else Raise Exception.Create('Unhandled project type in TProjectCreator.GetFileName.'); End; end;
The GetOptionFileName
method is depreciated in later version of Delphi as the option information is stored in the DPROJ file rather than in separate DOF files. This method is to be used to specifying the DOF file.
function TProjectCreator.GetOptionFileName: String; begin Result := ''; end;
The GetShowSource
method simply tells the IDE whether to show the module source once created in the IDE.
function TProjectCreator.GetShowSource: Boolean; begin Result := False; end;
The NewDefaultModule
method is a location where we can create the new modules for the project. Since is doesn’t provide the project reference (IOTAProject
) for the new project I will implement this elsewhere in the next chapter.
procedure TProjectCreator.NewDefaultModule; begin // end;
The GetOptionSource
method allows you to specify the information in the options file defined above by returning a IOTAFile
interface. For an example of how to do this please see below the method NewProjectSource
.
function TProjectCreator.NewOptionSource(const ProjectName: String): IOTAFile; begin Result := Nil; end;
The NewProjectResource
method allows you to create or modify the project resource associated with the passed Project
reference.
procedure TProjectCreator.NewProjectResource(const Project: IOTAProject); begin // end;
Finally, the NewProjectSource
method is where you can specify the custom source code for your project by returning a IOTAFile
interface. We will cover this in a few minutes below.
function TProjectCreator.NewProjectSource(const ProjectName: String): IOTAFile; begin Result := TProjectCreatorFile.Create(FProjectName, FProjectType); end;
IOTAProjectCreator50 Methods
The below methods were introduced in Delphi 5. This method is meant to supersede the NewDefaultModule
. This is where in the next chapter we will create the modules required for this OTA project.
{$IFDEF D0005} procedure TProjectCreator.NewDefaultProjectModule(const Project: IOTAProject); begin // end; {$ENDIF}
IOTAProjectCreator80 Methods
The below methods were introduced in Delphi 2005. This method is required to define the IDE personality under which the project is created as in the Galileo IDEs from 2005, multiple languages are supported. The function should return one of the pre-defined sXxxxxxPersonality
strings defiend in ToolsAPI.pas as below:
- Delphi:
sDelphiPersonality
; - Delphi .NET:
sDelphiDotNetPersonality
; - C++ Builder:
sCBuilderPersonality
; - C#:
sCSharpPersonality
; - Visual Basic:
sVBPersonality
; - Design:
sDesignPersonality
; - Generic:
sGenericPersonality
.
Please note that not all of these personalities are available in later version of the IDE.
{$IFDEF D2005} function TProjectCreator.GetProjectPersonality: String; begin Result := sDelphiPersonality; end; {$ENDIF}
Above we said in the NewProjectSource
method that we needed to return a IOTAFile
interface for the new custom source code. In order to do this we need to create an instance of a class which implements the IOTAFile
interface as follows:
TProjectCreatorFile = Class(TInterfacedObject, IOTAFile) {$IFDEF D2005} Strict {$ENDIF} Private FProjectName : String; FProjectType : TProjectType; Public Constructor Create(strProjectName : String; enumProjectType : TProjectType); function GetAge: TDateTime; function GetSource: string; End;
The IOTAFile
interface as 2 methods as below that need to be implemented and which are called by the IDE during creation:
The Create
method here is simply a constructor that allows us to store information in the class for generating the source code.
constructor TProjectCreatorFile.Create(strProjectName: String; enumProjectType : TProjectType); begin FProjectName := strProjectName; FProjectType := enumProjectType; end;
The GetAge
is to return the file age of the source code. For our purposes we will return -1 signifying that the file has not been saved and is a new file.
function TProjectCreatorFile.GetAge: TDateTime; begin Result := -1; end;
The GetSource
method does the heart of the work for the creation of a new project source. Here I’ve stored a text file of the project source for both libraries and packages in the plugins resource file (see previous posts on how this is achieved or the code at the end of the article). We extract the source from the resource file (with a unique name) and put it in a stream. We then convert the stream to a string. Note this is done in 2 different ways here due to me catering for non-Unicode and Unicode versions of Delphi.
function TProjectCreatorFile.GetSource: string; Const strProjectTemplate : Array[Low(TProjectType)..High(TProjectType)] Of String = ( 'OTAProjectPackageSource', 'OTAProjectDLLSource'); ResourceString strResourceMsg = 'The OTA Project Template ''%s'' was not found.'; Var Res: TResourceStream; {$IFDEF D2009} strTemp: AnsiString; {$ENDIF} begin Res := TResourceStream.Create(HInstance, strProjectTemplate[FProjectType], RT_RCDATA); Try If Res.Size = 0 Then Raise Exception.CreateFmt(strResourceMsg, [strProjectTemplate[FProjectType]]); {$IFNDEF D2009} SetLength(Result, Res.Size); Res.ReadBuffer(Result[1], Res.Size); {$ELSE} SetLength(strTemp, Res.Size); Res.ReadBuffer(strTemp[1], Res.Size); Result := String(strTemp); {$ENDIF} Finally Res.Free; End; Result := Format(Result, [FProjectName]); end;
Now we have the code to implement the new project sources we need to tell the IDE how to invokes this. The below code is a modified version of the Execute
method from the Repository Wizard Interface which displays the custom form we’ve created for asking the user what they want and then calls a new method CreateProject
with the returned values.
Procedure TRepositoryWizardInterface.Execute; Var strProjectName : String; enumProjectType : TProjectType; enumAdditionalModules : TAdditionalModules; Begin If TfrmRepositoryWizard.Execute(strProjectName, enumProjectType, enumAdditionalModules) Then CreateProject(strProjectname, enumProjectType, enumAdditionalModules); End;
Finally, the implementation of CreateProject
below creates the project in the IDE.
procedure TRepositoryWizardInterface.CreateProject(strProjectName : String; enumProjectType : TProjectType; enumAdditionalModules : TAdditionalModules); Var P: TProjectCreator; begin P := TProjectCreator.Create(strProjectName, enumProjectType); FProject := (BorlandIDEServices As IOTAModuleServices).CreateModule(P) As IOTAProject; end;
Now we can create either Packages or DLLs for our Open Tools API plugins.
All the files associated with this and all previous chapters can be downloaded here (OTAChapter13.zip).
Hope this is useful.
Dave 🙂