来源:
http://www.gexperts.org/opentools/
Erik's Open Tools API FAQ and Resources
If you have additions or corrections, please contact me. But, please do not send me questions about how to use the Open Tools API - instead, use the resources listed below.
What is the Open Tools API?
The Open Tools API (OTA) is a set of interfaces that allow developers to add features to the Delphi and C++Builder IDEs. These additions are called wizards or experts. Wizards can use the OTA interfaces to modify the IDE, obtain information about the IDE's state, and receive notification of important events. To create wizards, you should first get the Professional or Enterprise edition of Delphi or C++Builder, since these versions include the interface definitions in ToolsAPI.pas that will make your programming easier.
Where is the Open Tools API documented?
Starting with C++Builder 6 and the latest help updated for Delphi 6, the OTA is fairly well documented. Open the *iota.hlp file and look at the index there for details. For C#Builder and Delphi for .NET, also see my article on the BDN.
Where can I get help with my Open Tools API questions?
<IdeRoot>\Source\ToolsApi\ToolsAPI.pas
A good place to learn about the Open Tools API is is the ToolsAPI.pas unit itself. It is available in the Professional and Enterprise editions only. All of the interfaces are defined there, and many of them have comments about their purpose and usage.
Newsgroup Search Engines
There are several free web services that allow you to search for answers to previously asked questions in the Open Tools API newsgroup. I recommend you search at least one of these before posting, as it generally gives multiple answers to the most common questions. Try one of the following:
Search Site Newsgroups Date Range Search Features Speed
Google Groups All May 1981 - Now Good Fast
Mers Borland May 1997 - Now Limited Slow
Tamarack Associates Borland Oct. 1997 - Now Moderate Moderate
Developers.href.com (Down?) Many June 1997 - Now Moderate Moderate
The Open Tools API Newsgroup
Borland runs a newsgroup server that has an Open Tools API discussion group on it. Before posting, please read the newsgroup guidelines, and check the newsgroup search engines above and the previous messages in the group for answers to your questions. You can access the newsgroup at
news://newsgroups.borland.com/borland.public.delphi.opentoolsapi
Example Code
Delphi Super Page (US Mirror)
Torry's Delphi Pages (US Mirror)
Other Web Sites
Ray Lischner's Open Tools Resources - Ray's web site is a bit out-of-date and only covers the interfaces through Delphi 3/4, but might still be useful to some people.
Open Tools Interfaces by Rappido - Descriptions of how to obtain a few of the interfaces.
Open Tools Tutorial by Cyril J. Jandia - Covers packages, portability, window types, installation, custom modules, docking forms, module editors, sprigs, etc.
OTA Help File by Pete Fraser - A limited help file that lists the OTA interfaces (still under construction)
What is the "old" OTA and what is the "new" OTA? Which one should I use?
This FAQ only covers the "new" Open Tools API (OTA), which consists only of the ToolsAPI.pas unit in Delphi 4 or greater, and C++Builder 4 or greater. The older OTA is depreciated and should no longer be used except to maintain compatibility with older IDE versions. Support for the old OTA will likely be dropped in a future version of Delphi, and bugs in it are not being fixed. The old Open Tools API consists of the following units: ExptIntf, FileIntf, IStreams, ToolIntf, VcsIntf, VirtIntf.
Where can I get a simple wizard/expert to customize?
Here is the Pascal source for the simplest "Hello World!" wizard using the Open Tools API. Just compile and install this package (DPK) into the IDE, and try out the new menu item on the Help menu.
Can I create wizards in C++Builder?
Yes. The Open Tools API was originally designed with Delphi in mind, so wizards might be easier to create in Delphi, but C++Builder works fine.
Can I install a Delphi-created wizard into C++Builder?
Yes, GExperts is one example of an expert written in Delphi that can be compiled and installed into C++Builder.
Should I ever call Release on an interface obtained from the IDE?
It is not necessary to call Release on an IDE interface obtained via the Open Tools API. The interfaces are reference counted for you, and the associated memory will be freed as soon as all interface references go out of scope. Note that you can force the IDE to release an interface by setting all references to nil.
How can I add published properties to a TForm descendent?
Add published properties to a regular TForm
Add the form to the Object Repository (Project menu)
Add the form to an existing design time package (such as Borland User Components) or to a new design-time package.
Add DsgnIntf/DesignIntf to the implementation uses clause of some unit in the package, and add a register procedure as follows:
procedure Register;
begin
RegisterCustomModule(TMyForm, TCustomModule);
end;
Finally, inherit from your form in the repository inside a project and the new published properties will show up.
There is also a much more complex method involving writing a module creation expert, a repository expert, and using CreateModuleEx and different streams, but is much more error-prone and for most people, has no distinct advantages.
Note that the IDE won't allow you to add both published properties and components to a custom module at the same time. The workaround is to create a form with your custom properties in a package, and then have a descendent form in the repository which adds the components you want there by default.
How do I obtain the current project interface?
You need to iterate through all of the modules to find the project group and then get that group's active project:
// Modified from code posted by Ray Lischner (www.tempest-sw.com)
function GetCurrentProject: IOTAProject;
var
Services: IOTAModuleServices;
Module: IOTAModule;
Project: IOTAProject;
ProjectGroup: IOTAProjectGroup;
MultipleProjects: Boolean;
I: Integer;
begin
Result := nil;
MultipleProjects := False;
Services := BorlandIDEServices as IOTAModuleServices;
for I := 0 to Services.ModuleCount - 1 do
begin
Module := Services.Modules[I];
if Module.QueryInterface(IOTAProjectGroup, ProjectGroup) = S_OK then
begin
Result := ProjectGroup.ActiveProject;
Exit;
end
else if Module.QueryInterface(IOTAProject, Project) = S_OK then
begin
if Result = nil then
// Found the first project, so save it
Result := Project
else
MultipleProjects := True;
// It doesn't look good, but keep searching for a project group
end;
end;
if MultipleProjects then
Result := nil;
end;
How do I obtain the current project group interface?
You need to iterate through all of the modules to find the one that implements IOTAProjectGroup:
function GetCurrentProjectGroup: IOTAProjectGroup;
var
IModuleServices: IOTAModuleServices;
IModule: IOTAModule;
IProjectGroup: IOTAProjectGroup;
i: Integer;
begin
Result := nil;
IModuleServices := BorlandIDEServices as IOTAModuleServices;
for i := 0 to IModuleServices.ModuleCount - 1 do
begin
IModule := IModuleServices.Modules
;
if IModule.QueryInterface(IOTAProjectGroup, IProjectGroup) = S_OK then
begin
Result := IProjectGroup;
Break;
end;
end;
end;
How can I obtain the IOTAProjectResource interface for a given project?
function GetProjectResource(Project: IOTAProject): IOTAProjectResource;
var
i: Integer;
IEditor: IOTAEditor;
begin
Result := nil;
for i:= 0 to (Project.GetModuleFileCount - 1) do
begin
IEditor := Project.GetModuleFileEditor(i);
if Supports(IEditor, IOTAProjectResource, Result) then
Break;
end;
end;
How can I get a list of all installed components?
See IOTAPackageServices.GetComponentName in ToolsAPI.pas.
Is there any OTA support for creating type libraries or controlling the type library editor?
If you look at IOTATypeLibEditor and IOTATypeLibModule, you'll see there might be internal plans to add some support, but it isn't implemented yet. For now those interfaces can only be used to see if a file is a type library or not. Apparently, COM provides some services to create type libraries.
Is there any OTA support for parsing a source file or obtaining Code Insight information?
C#Builder and Delphi 8+ support obtaining the .NET CodeDom for C# code using IOTACodeDomProvider, but older IDEs do not expose unit structure details or Code Insight information such as method parameter lists, symbol declaration locations, class members, and symbol table information. As a result, you will need to parse the source yourself or use an existing language parser such as those at Torry's Delphi Pages (search for mwDelPar or mwPasPar, for examples) or the sample code at the Inner Circle Project. Starting with Delphi 7, you can implement your own code completion and parameter hints, but the source parsing is still up to you.
Should I compile my wizard as a DLL or a Package?
Packages are easier to load and unload without restarting the IDE (and hence easier to debug), but they can create unit naming conflicts in the IDE. Conflicts happen when the name a wizard's unit matches the name of a unit in another loaded design-time package. In this case, both packages can not be loaded at the same time. The recommended workaround is to prefix all of your unit names with a "unique" prefix. GExperts, for example, uses "GX_" as the name prefix for its units.
Why does my wizard have to dynamically link to the VCL/DesignIde packages?
If you want access to a useful BorlandIDEServices global variable from ToolsAPI.pas, you must compile your wizard linking to the VCL (Delphi 4/5) or DesignIde (Delphi 6+) package. See the Packages tab in the Project Options dialog for help on compiling with packages.
How do I get a hold of the designer for a DataModule?
The owner of the datamodule at design-time is a TCustomForm which has a Designer property.
How do a I get a reference to an IOTAXxxx interface?
In general, just search ToolsAPI.pas for a method that returns the interface type you are looking for. But, sometimes things are a little trickier, and you have to use QueryInterface or Supports to find what you want:
Interface Obtained From
INTAComponent IOTAComponent
INTAFormEditor IOTAFormEditor
IFormDesigner INTAFormEditor
IOTAKeyboardDiagnostics BorlandIDEServices
IOTAEditActions IOTAEditView
How can I debug a DLL wizard?
Exit your IDE
Remove any registry entries which load the expert DLL into your IDE. Look in HKEY_CURRENT_USER\Software\Borland\Delphi\X.0\Experts.
Start your IDE, and verify the expert is not loaded.
Compile your expert DLL. In the project options, be sure to turn on debug information, stack frames, reference info, etc. Turn optimizations off.
Re-register the DLL with the IDE by adding an entry to HKEY_CURRENT_USER\Software\Borland\Delphi\X.0\Experts.
Select Run, Parameters from the IDE menu. Enter the IDE's executable as the host application for your DLL.
Run the host application (F9), and another copy of your IDE should appear with the expert loaded.
You can now debug the DLL as it were a normal program (watches, breakpoints, inspections, tooltip evaluation, etc.).
Note that library debugging does not work well in Delphi 4 and BCB 4. Both will lockup fairly often when debugging DLLs and packages. Delphi 3, 5, and 6 are much more stable in this respect.
How can I debug a package wizard?
In the project options for your package, turn on debug information, stack frames, reference info, etc. Turn optimizations off.
Uncheck your package in the Project Options Packages tab, if necessary.
Build your package (don't install it).
Select Run, Parameters from the IDE menu. Enter the IDE's executable as the host application for your package.
Run the host application (F9), and another copy of your IDE should appear.
In the second copy of the IDE, open up the Project Options and load your expert package into the IDE.
You can now debug the package as it were a normal program (watches, breakpoints, inspections, tooltip evaluation, etc.).
Note that library debugging does not work well in Delphi 4 and BCB 4. Both will lockup fairly often when debugging DLLs and packages. Delphi 3, 5, and 6 are much more stable in this respect.
How do I implement an IDE notifier (IOTAIDENotifier)?
Create an object that implements all of the IOTAIDENotifier and descendent interface methods. Then register the notifier using IOTAServices.AddNotifier and watch for notifications from the IDE. Be sure to call IOTAServices.RemoveNotifier when you are done. Here is a Delphi 5/6 example IOTAIDENotifier.
How do I implement a form notifier (IOTAFormNotifier)?
Declare something like this:
TMyFormNotifier = class(TNotifierObject, IOTANotifier, IOTAFormNotifier)
protected
procedure FormActivated;
procedure FormSaving;
procedure ComponentRenamed(ComponentHandle: TOTAHandle;
const OldName, NewName: string);
end;
Implement all of the above methods, even if they are blank. Finally, add your notifier using IOTAModule.GetModuleFileEditor(0).AddNotifier. In Delphi 5, form notifiers won't actually fire notifications for BeforeSave or AfterSave (use a module notifier for this). It should fire the notifications in the declaration above, as well as Destroyed and Modified from IOTANotifier.
How can I get notified when a source file has changed?
Don't use IOTAEditorNotifier since, surprisingly, it never fires in Delphi 4/5. Instead, attach a module notifier using IOTAModule.AddNotifier and watch for IOTAModuleNotifier.Modified. Starting in Delphi 6, IOTAEditorNotifier works as expected.
How can I get notified when a form designer or component is selected?
When a form is activated, IOTAFormNotifier.FormActivated fires. If you are willing to add a notifier to every open form, this event will provide you with direct notifications. If you would rather not install a notifier for every open form, try using IDesignNotification.SelectionChanged. You will need to register your IDesignNotification interface with the IDE by caling RegisterDesignNotification and check the active form in each SelectionChanged callback.
How can I add a menu item to the IDE's main menu?
Use INTAServices.GetMainMenu to obtain a reference to the IDE's TMainMenu component. Iterate through all of the top-level menu items and find the parent menu item you want to add a menu item to. Then, use MyMenuItem.Insert() to add the menu item. GExperts has an IDE menu item expert that shows all IDE main menu items and their names. You may also find this tutorial by Miha Remec useful.
How can I add a shortcut to my main menu item?
In Delphi 5+, you should register a shortcut for your menu item using the keybinding interfaces:
Implement IOTAKeyboardBinding and make sure GetBindingType returns btPartial
Add your binding to the IDE using IOTAKeyboardServices.AddKeyboardBinding
In the IOTAKeyboardBinding.BindKeyboard callback, use the passed in IOTAKeyBindingServices reference to call BindingServices.AddKeyBinding for each menu item, as described below:
Pass in a TKeyBindingProc type procedure callback method for when your shortcuts are presssed
procedure Callback(const Context: IOTAKeyContext; KeyCode: TShortcut; var BindingResult: TKeyBindingResult);
Pass in the menu item's name as the last parameter (the ill named "HotKey" in Delphi 5)
The call should look similar to this:
AddKeyBinding([ShortCut(Ord('G'), [ssCtrl])], Callback,
nil, 0, '', 'MyMenuItem');
In Delphi 4, you will need to wait a few seconds after the IDE starts and set the menu item's ShortCut property. A timer works well to create the delay.
How can I paint the palette bitmap for a specific component?
In Delphi 4/5 you need to use the LibIntf unit, which is unsupported and undocumented, but try:
LibIntf.DelphiIDE.GetPaletteItem(GetClass('TButton')).Paint
In Delphi 6+ you can try to load the component bitmaps from the .bpl package resources manually (by name) or see if the ComponentDesigner unit implements this functionality somehow.
How do I implement a module creator (IOTAModuleCreator/IOTAFormWizard)?
Descend from TInterfacedObject and implement all of the methods in IOTACreator and IOTAModuleCreator as follows:
TGxModuleCreator = class(TInterfacedObject, IOTACreator, IOTAModuleCreator)
Here is sample code for a Delphi 5/6 module creator that resides in the repository by implementing IOTARepositoryWizard.
How do I implement a project creator (IOTAProjectCreator)?
Descend from TInterfacedObject and implement all of the methods in IOTACreator and IOTAProjectCreator as follows:
TGxProjectCreator = class(TInterfacedObject, IOTACreator, IOTAProjectCreator)
Here is sample code for a project creator that installs itself into the Help menu.
How can I add menu items to the code editor's popup menu?
IOTAEditView.GetEditWindow.Form.FindComponent('EditorLocalMenu') will return the editor's TPopupMenu component that you can add to. Your added menu items will work best if you add them to the end of the popup menu and may not work at all if you associate an action with them. Note that you might want an IOTAEditorNotifier to determine when to add your new menu items to new editor windows.
How can I iterate over all units/forms in a project?
IOTAProject.ModuleCount can be used to iterate over all modules, and IOTAProject.GetModule returns a reference to IOTAModuleInfo which gives you the FileName, FormName, etc.
How can I publish a property of type T[Custom]Form?
It doesn't work very well, but you can try to publish the property normally. The problem is that only currently created forms will be shown in the property editor, and storing an internal reference to one of those forms will often cause AVs when the target form is later closed. As a workaround, you can create a custom property editor that uses IOTAProject as above to get the class names of all forms in the project and insert them into the dropdown list for the property editor. Then store the form reference internally as a class name string, and use something like GetClass and RegisterClass to map a class name to a class type. With the class type, you can create the form at runtime. If you are sure your target form will always exist at runtime, another option to map from classes to instances is to search the Screen.Forms array.
How can I create a form that docks into the IDE like the Object Inspector?
You need to descend from TDockableForm in the DesignIDE package's DockForm unit and perform some magic to register your dockable form with the IDE. Here is a Delphi 7 docking form example that shows how to create a basic docking form. Compile the included package, install it into the IDE, and then look at the new menu item in the Help menu. Also, see Allen Bauer's article on the Borland web site for an overview and a Delphi 5 example.
How can I obtain the currently active form editor (IOTAFormEditor)?
function GetActiveFormEditor: IOTAFormEditor;
var
Module: IOTAModule;
Editor: IOTAEditor;
i: Integer;
begin
Result := nil;
Module := (BorlandIDEServices as IOTAModuleServices).CurrentModule;
if Module <> nil then
begin
for i := 0 to Module.GetModuleFileCount - 1 do
begin
Editor := Module.GetModuleFileEditor(i);
Editor.QueryInterface(IOTAFormEditor, Result);
if Result <> nil then
Break;
end;
end;
end;
How can I obtain the current form designer interface (IFormDesigner)?
function GetActiveFormDesigner: IFormDesigner;
var
FormEditor: IOTAFormEditor;
begin
Result := nil;
FormEditor := GetActiveFormEditor;
if FormEditor <> nil then
Result := (FormEditor as INTAFormEditor).FormDesigner;
end;
How can I get a form editor from a module interface?
You should iterate through all of the ModuleFileEditors, to see which one implements IOTAFormEditor:
function GetFormEditorFromModule(IModule: IOTAModule): IOTAFormEditor;
var
i: Integer;
IEditor: IOTAEditor;
begin
Result := nil;
if IModule = nil then
Exit;
for i := 0 to IModule.GetModuleFileCount - 1 do
begin
IEditor := IModule.GetModuleFileEditor(i);
if Supports(IEditor, IOTAFormEditor, Result) then
Break;
end;
end;
Is there a way to determine if the user is editing a form or working in the code editor?
You can't do this using only the new Open Tools API in Delphi 5, but you can use the old API to get this information from ToolServices.GetCurrentFile. Starting with Delphi 6, there is IOTAModule.GetCurrentEditor for this purpose. If you find these methods don't work for .h files in C++Builder try IOTAEditorServices.TopBuffer.FileName.
How can I tell when a module was removed from a project?
The old OTA had an fnRemovedFromProject notification flag, but this doesn't exist in the new OTA. As a workaround, you can watch for changes to the project file using an IOTAEditorNotifier and the iterate over all modules to see which one might have been deleted, if any.
How can I obtain the Name property of a form?
If the form is part of a loaded project, use IOTAModuleInfo.GetFormName. If the form is not part of a loaded project, you can try IOTAFormEditor.GetRootComponent.GetIComponent.Name, but it isn't always reliable (it can cause AVs of the form is not visible). A more reliable method might be to force the form visible before calling GetRootComponent or you could manually scan the associated DFM stream for the form's Name property.
How can I create a method and then assign an event handler at design-time?
You need to use I[Form]Designer.CreateMethod from the DsgnIntf/DesignIntf unit and then SetMethodProp from the TypInfo unit. Here is a simple Delphi 5/6 example that uses both of these functions to create and assign an OnClick handler for the current form.
How can I force the code editor to show a specific file tab?
You can either use IOTASourceEditor.Show or call IOTAActionServices.OpenFile and pass in the full path and filename of the file tab to activate.
Known bugs in the Delphi 7 Open Tools API (some apply to Delphi/BCB 6):
IOTAComponent.GetParent always returns nil.
Calling IOTAEditView.SetTempMsg makes the code editor's Diagram tab disappear when clicking back in the source code editor.
Several of the IOTAProjectOptions do not work such as IncludeVersionInfo and ModuleAttribs. Also, some useful options are missing such as BreakOnException. Some options such as LibraryPath are not persisted across sessions.
The HowMany parameter of IOTAEditPosition.Delete is ignored. As a result, the method always deletes one character.
IOTASourceEditor.SetSyntaxHighlighter is deprecated and can no longer be used
Setting IOTAEditView.CursorPos doesn't update the edit window's cursor position in the status bar.
The IDE does not remove instances of IOTACustomMessage from the message view before unloading an expert. This can result in crashes as the IDE calls back into an unloaded library. The workaround is to call ClearToolMessages before your expert unloads, if it added custom messages.
IOTAToDoManager.ProjectChanged is never called.
You can't add a keyboard binding for keys like Ctrl+/ and Ctrl+K.
IOTAResourceEntry.DataSize must be divisible by 4 (aligned to a 4-byte boundary), or you will get an RLINK32 error when compiling.
Known bugs in the Delphi 6 Open Tools API (some apply to Delphi/BCB 5):
TIModuleInterface.GetFormInterface is deprecated and always returns nil. You must use IOTAFormEditor instead.
The Open Tools keybinding interfaces sometimes raise AVs when using IOTAKeyBoardServices.AddKeyboardBinding.
IOTAEditView.PosToCharPos raises an AV in dfwedit.dll every time it is used.
IOTAEditorServices.TopView raises an AV in the coride package if called with no files open.
Known bugs in the C++Builder 5.01 Open Tools API:
Given a regular unit without an associated form, calling IOTAModule.GetModuleFileCount returns 2 but IOTAModule.GetModuleFileEditor called with index 1 results in an AV and index 2 returns the .H file.
Setting the LibDir project option using IOTAProjectOptions.Values results in an AV.
Known bugs in the Delphi 5.01 Open Tools API:
Calling IOTAModuleServices.OpenProject on a BPG file will crash the IDE. Instead, use IOTAModuleServices.OpenFile.
When querying IOTAProjectGroup.FileName, you won't get a full pathname. Instead query the IOTAModule that implements IOTAProjectGroup for its FileName, and it will contain a full path.
The project options MajorVersion, MinorVersion, Release, and Build don't update the project options dialog when set.
You can not use the keybinding interfaces to bind actions to keystrokes such as Ctrl+Enter, Shift+Enter, and non-shifted alpha-numeric characters.
When opening a BPG file, IOTAIDENotifier will send a blank filename parameter along with ofnFileOpened into the FileNotification method.
The IDE AVs or produces an I/O Error when specifying a file name without a complete path when implementing IOTAProjectCreator.GetFileName.