天天看点

Plug-in, Switch On, Fall Over: Adventures with the Open Tools APIPlug-in, Switch On, Fall Over:Adventures with the Open Tools API

Plug-in, Switch On, Fall Over: Adventures with the Open Tools APIPlug-in, Switch On, Fall Over:Adventures with the Open Tools API

Email: info at xpro.com.au

Phone: +61 3 9479 3988 (Don) +61 3 9479 3932 (Paul)

Fax: +61 3 9479 1675

Street/Postal:

Suite 3B La Trobe R&D Park,

2 Research Ave, Bundoora Vic 3083, Australia

Home| About| Support| OpenTools| Freeware| Presentations
Download paper, slides and code (2753kB)

Plug-in, Switch On, Fall Over:

Adventures with the Open Tools API

by Paul Spain,  The Excellent Programming Company, Melbourne, Australia.

Contact: [email protected]  Internet: http://www.xpro.com.au/  

Introduction

The Open Tools API, a.k.a. Open Tools, or simply OTA, is the third-party plug-in architecture provided by Borland for its Rapid Application Development (RAD) tools. There is a common API for the Delphi, Kylix and C++Builder tools, and a distinct, yet similarly named, API for the JBuilder product. Open Tools for JBuilder is not discussed here.

Using the OTA, you can write modules that extend and customise the Integrated Development Environment (IDE) of Borland’s RAD Tools. These modules take the form of dynamic link libraries (DLLs) or design-time packages for the Win32 products, and shared objects under Linux.

As DLLs are a standard binary format, you can write Win32 plug-ins in any language that supports function pointers, and associated tools that, obviously, can produce DLLs. However, there are significant advantages in producing your plug-in as a proprietary Borland design-time package - more on this later…

Unless noted otherwise, all subsequent references to the OTA indicate the version shipped with Delphi 5.

I use the term “plug-in” here, whereas Borland has used “expert” and more recently “wizard” to describe these modules. To my mind, “wizard” implies a particular cookbook type of plug-in, which automates a manual process; but the OTA has evolved into something much broader. Also, the term “plug-in” has much wider currency in the developer community.

Code examples

There is an associated zip archive containing all the examples referenced in this paper. After unzipping the archive, refer to the file BorCon2001/OpenTools/README.TXT for instructions on loading and running the examples.

Most of the examples are licensed under the Lesser GPL Open Source licence, available for your perusal at http://www.gnu.org/copyleft/lesser.html.

…Approximating in plain English: You are free to use, modify and distribute this software in open source and proprietary applications,  provided you make this source code, including any modifications you make, freely available under the same Lesser GPL licence.

History

Delphi 1 shipped with a simple Expert API, manifested as four files within the Visual Component Library (VCL) source directory. It facilitated the creation of custom forms and project types, and other Experts that appeared as new items on the IDE Help submenu. There was also an interface for a Version Control System, which simply created a new submenu on the IDE main menu. Experts gained access to IDE facilities through a global ToolServices object.

Delphi 2 saw the birth of the OTA, in its own ToolsAPI source directory. Four new files augmented Delphi 1’s Expert API, with support for form designers, property editors, new menu items and virtual file systems. It also provided access to the contents of the file buffers within the IDE, and provided notifications for IDE events, such as files and projects opening and closing.

Delphi 3 introduced packages, some new methods, and some reorganisation.

Delphi 4 saw a move from an object-based API, to interfaces, the new language feature. The interface names and methods closely followed the classes of the earlier OTA. There was a jargon switch from Experts to Wizards, and the monolithic ToolServices object was split into a collection of smaller Services interfaces. These are accessed by querying the global interface variable, BorlandIDEServices. New features included debugger integration with access to the CallStack, breakpoints, and processes and threads on local and remote hosts. The MessageView was surfaced, and new Notifier interfaces to deal with IDE events, and closure of transient IDE artefacts. The new interfaces were wholly contained in the new file ToolsAPI.pas. Apart from DsgnIntf.pas, which contained the property editor and form designer code, all the files for the class-based OTA were deprecated.

Delphi 5 saw new interfaces introduced for keyboard bindings, search and replace, editor form access, selected text blocks, IDE automation, extended breakpoint support and ToDo lists.

Kylix introduced CLX (cross-platform component library) support.

I have had little time to explore the Delphi 6 OTA, but the first thing I noticed was the removal of the deprecated class-based OTA.

Future

Coding with the OTA will give you a different perspective on your Borland RAD tool. These polished performers can look pretty flaky from a plug-in developers perspective. Open Tools is an under-resourced area of Borland R&D. It appears to be a very part-time project of one (albeit very talented) engineer, Allen Bauer, kept otherwise busy as the Delphi/C++Builder IDE R&D Manager. Developer relations toes the very reasonable line that they will devote resources in accordance with customer requests. This leaves the OTA in a Catch-22 situation: the unimplemented features, long-standing bugs and dearth of current documentation make it difficult to generate the critical mass of third-party developers necessary to properly resource OTA R&D. Furthermore, the OTA is essentially unused within the RAD tools. Compare this with the robust JBuilder Open Tools API, which is the backbone of that very cool product.

However, information available via the Borland Community web site (http://community.borland.com), surrounding the release of Kylix and Delphi 6 indicates that there is a progressive conversion to interfaces within these products. We can hope that these interfaces will closely align with the OTA, or eventually be surfaced as a replacement. I have also heard at BorCon Asia Pacific 2001 that Open Tools will receive more attention in the Delphi 7 release, so we shall have to wait and see.

DLLs or Packages?

In common with most plug-in architectures, OTA plug-ins are dynamically linked binary modules. As previously mentioned, these are either common DLLs or Borland design-time packages, as introduced in Delphi 3. The advantages of DLLs:

Use non-Borland languages and tools to produce a plug-in DLL.

Legacy support for Delphi 2 through to Delphi 5 from a single code base using the deprecated class-based OTA.

Port plug-ins written in C++Builder to Delphi.

Separate language namespace. Packages share namespace with the VCL and all other installed packages. Religiously using a unique identifier prefix and/or extracting common code to a run-time package can help to solve this problem.

The advantages of design-time packages:

They can be loaded and unloaded repeatedly from the IDE, which makes the edit-compile-run cycle considerably shorter. Conversely, DLL plug-ins are fixed in memory for the lifetime of the RAD host application.

Access to the same instances of the VCL and run-time libraries as your RAD host – you can directly manipulate global objects and data, such as the Application object. Such direct manipulation is not officially encouraged… of course!

Access to NTA (Native Tools API) interfaces for directly manipulating IDE VCL objects  …the nice way!

Access to data members exposed by other packages, whereas common DLLs only export functions/procedures.

Which tool for development?

Delphi, C++Builder and Kylix support the OTA, and all products use packages. However, as I understand it, packages created in Delphi will run without modification in C++Builder, whereas packages created in C++Builder won’t work in Delphi. Kylix is another kettle of fish. The process involves creating your plug-in in Delphi, and recompiling in Kylix, but you should read Ray Lischner’s article on the Borland Community web site. See the Resources section at the end of this paper.

Bottom line… if you want to easily write plug-ins that will work in Delphi, C++Builder and Kylix, develop them in Delphi.

All the examples in this paper are design-time packages for Delphi 5 and 6. Usually, wherever I refer to Delphi in the text, you can substitute your RAD tool of choice.

Structural outline

The following diagram represents the relationship between design-time package plug-ins and the RAD tool IDE. Design-time packages share the libraries linked to the IDE, but can only access the non-visual artefacts of the IDE via the Open Tools API.
Plug-in, Switch On, Fall Over: Adventures with the Open Tools APIPlug-in, Switch On, Fall Over:Adventures with the Open Tools API

Plugging-in without the OTA

As mentioned earlier, a feature of design-time packages is sharing your RAD host’s VCL and run-time libraries. This feature allows us to plunder all the global objects and variables in the libraries, such as Application, Screen, and Database, without writing a jot of OTA code. This is something of a double-edged sword, as there is no guarantee of the continuity of the IDE implementation between versions, or even point releases within a version.

The entry-point for all packages is a global procedure,

Register

, (case-sensitive) [1] . You could also use an initialisation section, but the IDE is guaranteed to be ready for you by the time it calls your

Register

procedure. Any cleanup required for the package that isn’t handled in destructors should occur in a finalization section.

Example: NoOTA.dpk

This package changes some of the global objects and variables in the VCL and RTL that are shared with the host application, Delphi. In the

Register

procedure in

main.pas

, we change the colour of Delphi’s main form, the menu bar font, and the Windows build number as reported in the Delphi About… box. These changes take effect as the package is loaded by Delphi. The changes are reverted by the

finalization

section, which executes when the package is unloaded.

interface

// Standard Delphi package entry point

procedure

Register;

implementation

uses

  Graphics,     // TColor   SysUtils,     // Win32BuildNumber   Forms;        // Application

procedure

Register;  

begin

  // Preserve original values

  …

  //   // Let's give Delphi a facial and upgrade Windows!   //

  // Manipulate global VCL global Application object shared with Delphi

  Application.MainForm.Color := clLime;

  // Manipulate global VCL global Screen object shared with Delphi

  Screen.MenuFont.Name := 'Showcard Gothic';   Screen.MenuFont.Height := 24;

  // Manipulate global RTL global variable shared with Delphi

  SysUtils.Win32BuildNumber :=  3000;  

end

;

initialization

finalization

// This code will execute when the package is unloaded // Restore old values and free resources

end

.

Playing safe …and smart

An interface is a contract between the implementer and the user. The contents of an interface should be immutable once it has been published. But… the OTA is no exception to the list of exceptions! Regardless, using the OTA interfaces is your best bet for minimising the amount of work required to port your plug-in across versions and tools.

Having said that, it’s time to explore the landscape that is the Open Tools API!

OTA Architecture

The OTA is a collection of source files, stored in the ToolsAPI subdirectory of the

Source

directory of Delphi or C++Builder. For Delphi 4&5, the only relevant files are

ToolsAPI.pas

and

DsgnIntf.pas

. All the other files are the previous class-based OTA, now deprecated, and finally expunged in Delphi 6.

ToolsAPI.pas

is your first port of call. It is a bulging swag of interfaces, a helper base class, and a few global variables and functions. Of primary concern are the interfaces. Some are implemented by your RAD tool and accessed via the Services interfaces. Others, including Wizards and Notifiers, are implemented by you, the plug-in author.

Let’s start with a simple example OTA plug-in…

Example: HelloWorld.dpk  …like there’s a choice here!

This plug-in is as simple as it gets, but demonstrates the essential elements for integrating with the Borland IDE. It installs a new menu item on Delphi’s Help submenu. Choosing that menu item will add a ‘Hello World!’ message to Delphi’s MessageView window.

I will use the term “wizard” to describe the plug-in class herein, as it ties closely to the OTA naming scheme. These code fragments are taken from

Simple.pas

.

interface

The entry point for any and all Borland packages,

Register

procedure

Register;

implementation

All the Open Tools interfaces are declared in one file

, $(DELPHI)/Source/Toolsapi/ToolsAPI.pas

. The path to this file is not included in the factory-default search or library paths. You’ll need to add it to the Search Path in Project Options for each plug-in you write, or you can add it to the Library Path in your Environment Options, and forget about it thereafter. If you plan to distribute your source to other developers, add it to the Search Path in Project Options and distribute your packages’s CFG and DOF files.

uses

ToolsAPI;

Note that our wizard,

TSimpleWizard,

is declared within the implementation section of the unit. It is never used outside the scope of this unit, so it can be declared here to maximise encapsulation. It also reduces the package size, as there are fewer exported symbols. This is not a necessary requirement for a wizard, just good style, IMHO!

TSimpleWizard

inherits from

TNotifierObject

, a helper class defined in

ToolsAPI.pas

, which inherits an

IUnknown

implementation and provides a stubbed (empty methods) implementation of

IOTANotifier

. This is a very helpful helper class, as most wizards only need to meaningfully implement a subset of

IOTANotifier

. Our wizard explicitly implements

IOTAWizard

and

IOTAMenuWizard

, as we want the wizard to appear as a menu item (on the Help submenu)

Type

  TSimpleWizard =

class

(TNotifierObject, IOTAWizard, IOTAMenuWizard)

Private

As a rule of thumb preventive measure, I implement interface methods as private or protected methods within the implementing class. Visibility doesn’t matter for interface implementation methods, so we are only blocking direct public usage of the object methods. Allowing access to the implementing object within the same scope as the interface will often lead to invalid reference bugs.

    // IOTAWizard implementation

function

GetIDString:

string

;    

function

GetName:

string

;    

function

GetState: TWizardState;    

procedure

Execute;

    // IOTAMenuWizard implementation

function

GetMenuText:

string

;    

end

;

As previously noted,

Register

is the preferred entry point for the IDE. Here we are calling the global procedure,

ToolsAPI.RegisterPackageWizard

, passing an

IOTAWizard

interface to the IDE. The compiler retrieves the interface from the object (

TSimpleWizard

instance). As there is no local reference to the interface, the IDE controls the lifetime of the wizard.

Where possible, avoid holding a reference to an interface also held by the IDE, as you could interfere with the operation of the IDE by stopping destruction of the underlying objects. There are solutions for these situations, but they are sometimes difficult to detect, and why introduce unnecessary complexity?

procedure

Register;  

begin

  ToolsAPI.RegisterPackageWizard(TSimpleWizard.Create);  

end

;

GetIDString

must return a unique identifier. By convention, the identifier has two parts: organization & a wizard name.

function

TSimpleWizard.GetIDString:

string

;  

begin

  Result := 'BorCon 2001.Simple Wizard';  

end

;

GetName

returns a user-friendly name for the wizard. Delphi displays the name in error messages, and for repository wizards, in the Object Repository.

function

TSimpleWizard.GetName:

string

;  

begin

  Result := 'Simple Wizard';  

end

;

Execute

only fires for menu, form, and project wizards. It is invoked by selection of the menu item or Object Repository entry respectively. Here, we write a message in Delphi’s MessageView. First, we reference

BorlandIDEServices

, which is the universal entry point for the OTA. This is an interface of type

IBorlandIDEServices

, an empty descendant of

IUnknown

, so we query it for the interface we are interested in,

IOTAMessageServices

, and invoke the interface method,

AddTitleMessage

.

Supports

[2] is a convenience function, which combines the querying with assignment of the resultant interface (the third (out) parameter,

MessageView

), and returns a boolean.

procedure

TSimpleWizard.Execute;  

var

  MessageView: IOTAMessageServices;

begin

if

SysUtils.Supports(BorlandIDEServices, IOTAMessageServices, MessageView)

then

    MessageView.AddTitleMessage('Hello World!');

end

;

GetState

only fires for menu wizards. It describes the state of the menu item, enabled/disabled and/or checked.

function

TSimpleWizard.GetState: TWizardState;  

begin

  Result := [wsEnabled];  

end

;

GetMenuText

is the sole method of the

IOTAMenuWizard

interface, and returns the label for the associated menu item.

function

TSimpleWizard.GetMenuText:

string;

  begin

Result := 'Greetings from BorCon 2001!'

;

  end;

Add any cleanup code for wizard unloading to the

finalization

section, or, better yet, handle it in the wizard destructor. There is no

Unregister

equivalent for the

Register

procedure. Remember that the IDE frees the wizard object we created in

Register

!

initialization

finalization

end.

Notifiers

Many IDE artefacts, CodeEditor modules and breakpoints for example, are transient in nature. It is therefore hazardous to hold references to these things, as the reference may be invalid when you attempt to use it. The OTA has provided a solution to this problem through a hierarchy of notifiers, an implementation of the Observer pattern. These are interfaces, which you, the plug-in author, must implement. You register a notifier with the IDE, which will subsequently call the methods of your interface.

Each notifier is associated with another OTA interface. For example

IOTAModuleNotifier

is associated with

IOTAModule40

. When an

IOTAModule

is about to be destroyed, all its observers (who have registered notifiers) will be notified.

This notification mechanism has been leveraged throughout the notifier hierarchy to inform plug-ins of other IDE events, such as editor windows receiving focus, or files being renamed. The base interface of the notifier hierarchy is

IOTANotifier

.

IOTANotifier =

interface

(IUnknown)   ['{F17A7BCF-E07D-11D1-AB0B-00C04FB16FB3}']  

procedure

AfterSave;  

procedure

BeforeSave;  

procedure

Destroyed;  

procedure

Modified;  

end

;

The method called by the IDE for most implementations of this interface is

Destroyed

. This occurs when the notifying or observed IDE artefact is being destroyed, oddly enough! Any interfaces you are holding for that artefact must be released in this method. This practice is fundamental to the health of your plug-in and its host application.

The other methods of

IOTANotifier

are only called for some notifiers. This lack of optimisation is apparent in several OTA interfaces. I suspect it is largely a legacy issue - to remain backwardly compatible with older plug-ins as the OTA evolves and grows.

Note that from Delphi 5 onwards, there is a helper class,

TNotifierObject

, which inherits from

TInterfacedObject

, and provides a stubbed implementation of

IOTANotifier

. This is a good base class for plug-ins which need to implement

IOTANotifier

. Just remember that it may be necessary to reimplement

IOTANotifier.Destroyed

if you are holding IDE interfaces. You will need to declare

IOTANotifier

in your plug-in’s interface implementation list in that case.

Wizards

Wizard plug-ins date back to Delphi 1, when they were known as Experts. There are three flavours of wizard: menu (a.k.a standard), form and project.

As of Delphi 4, to write a wizard, you must write a class which implements several wizard interfaces, and register your wizard in your package’s

Register

implementation by calling

ToolsAPI.RegisterPackageWizard.

Stubbed implementations will suffice for some wizard interface methods. Refer to the comments for the relevant interfaces in ToolsAPI.pas.

Menu Wizards

Menu Wizards appear as new menu items. Unfortunately, they are always added to the Help menu, so they are not generally useful for much besides demo-ware and testing. In your wizard class, you implement

IOTANotifier

,

IOTAWizard

and

IOTAMenuWizard

.
Example: EPCErrorTextWizard.dpk - error codes as text

This wizard creates a Help menu item “ErrorCode As Text…”. Choosing this item displays a modal dialogue that will convert numeric (decimal or hex) Win32 and BDE error codes to their textual equivalents - much easier to comprehend for mortals!

To install a new menu item elsewhere, manipulate the host application’s main menu, surfaced on the

INTAServices

interface. The next example uses this technique.

Form and Project Wizards

Form Wizards and Project Wizards both produce new items in the Object Repository. Both wizard types must implement

IOTANotifier

,

IOTAWizard

and

IOTARepositoryWizard

. Furthermore, Form Wizards must implement

IOTAFormWizard

, and Project Wizards

IOTAProjectWizard

.

By default, Form Wizards will appear as a new item on the Forms page of the repository (File|New…|Forms). Selecting this item will create a new form in the IDE, in accordance with your implementation.

Likewise, the default location for a Project Wizard is on File|New…|Projects. This will create a new IDE project in accordance with your wizard.

Example: EPCSourceTemplatesWizard.dpk - generate new forms and units from your template(s).

This is a Form Wizard that also creates a new menu item on the File menu. You can use, modify add or delete new templates for units and forms, editing their source in the IDE. The provided templates generate licence and copyright information, class and interface skeletons, and specific mark-up for CVS version control.

This wizard uses

IOTAModuleServices.CreateModule

to create the new units and forms.

CreateModule

expects an

IOTAModuleCreator

argument.

procedure

TSTBaseWizard.WizardFormCreateClick(Sender: TObject);  

begin

  (BorlandIDEServices

as

IOTAModuleServices).CreateModule(self);  

end

;

TSTBaseWizard

, passed in as “

self

” above, implements

IOTAModuleCreator

and its ancestor interface,

IOTACreator

. The content of the files is returned via the following method implementations for

IOTAModuleCreator

. Note that the file content returned is expected as an

IOTAFile

implementation. Here, I have delegated that responsibility to other classes,

TSTFormFile

and

TSTUnitFile

for form and unit content respectively.

function

TSTBaseWizard.NewFormFile(  

const

FormIdent, AncestorIdent: string): IOTAFile;  

begin

  Result := TSTFormFile.Create(WizardForm);  

end

;

function

TSTBaseWizard.NewImplSource(  

const

ModuleIdent, FormIdent, AncestorIdent: string): IOTAFile;  

begin

  Result := TSTUnitFile.Create(WizardForm);  

end

;

The template files are edited within the IDE editor by calling on

IOTAActionServices.OpenFile

.

procedure

TSTBaseWizard.WizardEditTemplateClick(Sender: TObject);  

begin

  (BorlandIDEServices

as

IOTAActionServices).OpenFile(WizardForm.EditTemplate);  

end

;

Services

As previously seen, the entry point for OTA services is the global interface variable

,

BorlandIDEServices

. This variable is of type

IBorlandIDEServices

, an empty descendant of

IUnknown

. To do anything useful, you must query

BorlandIDEServices

for one of the following interfaces:
Interface Facilities

INTAServices

Access the host application’s main menu, action list, image list and toolbars.

IOTAActionServices

Open, close, save and reload files.

Open projects.

IOTADebuggerServices

Enumerate, create and attach to processes on local and/or remote hosts.

Create and access address and source breakpoints.

Create module load breakpoints.

Add and remove notifiers.

IOTAEditorServices

Get and set Editor Options.

Retrieve iterator to access edit buffers.

Get active editor buffer and view.

IOTAKeyBindingServices

[3]

Add custom IDE behaviour.

Bind new or existing IDE behaviour to keyboard shortcuts.

IOTAKeyboardServices

Add and remove keyboard bindings.

Record, play and delete editor macros.

Push and pop keyboards (collections of keybindings).

Iterate, lookup and execute keybindings.

IOTAMessageServices

Add messages to the Message View of type:

Custom: customised behaviour and/or drawing.

Tool: standardised display and context behaviour.

Title: bold text with no associated context behaviour.

IOTAModuleServices

Close and save all modules.

Create and manipulate modules.

Add, retrieve and delete virtual file systems.

IOTAPackageServices

Retrieve names of loaded packages and names of components contained therein.

IOTAServices

Get Environment options.

Get base Registry key and product ID.

Get Application handle (HWND).

Add and remove notifier for general IDE events.

IOTAToDoServices

Retrieve ToDo items by index.

Add and remove collections (Managers) of ToDo items.

Add and remove notifiers.

IOTAWizardServices

Add and remove IOTAWizards and descendants.

Debugging plug-ins

Debugging is the dark side of plug-in development - it puts the “F” into  “Fall Over”. However, with some care and thought, you can save yourself a lot of grief.

As previously mentioned, your package shares its instance of the VCL and RTL with your RAD tool’s IDE, and also with any other installed design-time packages. This is the key to much of the power of the OTA, but one slip-up by any of these players is the difference between soufflé and omelette, between a Jumbo Jet and a flying brick, between… you get the picture!

So, obviously, you want to isolate yourself from the effects of other players. Initially, uninstall as many third-party components and third-party design-time packages as is practicable.

The host application for your package is also your debugging tool, ie you’ll be debugging Delphi with Delphi. Running an untested package into your development/debugging environment is something you do only once …hopefully. After you’ve mopped up the blood and brains from the Delphi explosion, you’ll realise there has to be another way. The solution involves running two concurrent copies of your RAD tool.

If you have a well-behaved package and Delphi installation, you can proceed as for any package or DLL:

Plug-in, Switch On, Fall Over: Adventures with the Open Tools APIPlug-in, Switch On, Fall Over:Adventures with the Open Tools API

Ensure that your package is not checked in the design packages list in the Project Options dialog. (Project|Options..|Packages|Design packages). Save your project to persist this setting.

As mentioned, you debug your package or DLL in the context of its host application. To specify the host application, go to the Parameters dialog off the Run menu (Run|Parameters…|Local|Host Application). For Delphi, specify delphi32.exe, found in the Bin subdirectory of your Delphi installation.

I have usually found it necessary to restart Delphi to properly persist the above settings.

Compile your package with debug information on and optimisation off. Add debug DCUs, stack frames, range checking, overflow checking etc. to taste. All these options are on the Compile tab of the Project Options dialog (Select Options.. off the Project menu). My preferences are shown in the above screenshot.

Having two copies of Delphi running can get a bit confusing, so I check “Minimize on run” in the Environment Options (Tools|Environment Options..|Preferences|Compiling and running|Minimize on run). Seeing the debugger copy shrink down and pop up keeps me sane for a bit longer.

The Register global procedure in your package is the official entry point for your plug-in, so I tend to put a breakpoint on the first line of Register.

Now run the host application (Run|Parameters…|Load, F9 key, toolbar button, whatever…). When the second copy of the IDE appears, add your package to the list of design packages, if not already present, and enable it (Project|Options..|Packages|Design packages) or (Component|Install Packages..).

All going well, the first (debugger) copy of the IDE should now pop up, with execution halted on your breakpoint in Register. You can now step through code, set breakpoints, set watches, or whatever your heart desires. Note that enabling “debug DCUs” in your Compiler settings links a debug version of the VCL. Much of the code you will be executing is within the Delphi binary and associated packages, which you won’t be able to step through.

If you have an ill-behaved package, using the integrated debugger can be difficult or impossible.

In that situation, it is wise to run two copies of the IDE, one for coding, the other for testing. You will need to resort to old-fashioned debugging techniques such as log messages to determine execution flow and parameter values.

If you have a really badly behaved package, which is enabled on the design-time package list (Component|Install Packages…), it is possible to crash the IDE during startup.

The solution is to remove or disable the offending package in the design-time packages list via some Registry hacking, using the Windows utility, regedit.exe. Using Delphi 6 as our example, the design-time packages loaded at start-up are listed under the key

HKEY_CURRENT_USER/Software/Borland/Delphi/6.0/Known Packages

, as seen below:
Plug-in, Switch On, Fall Over: Adventures with the Open Tools APIPlug-in, Switch On, Fall Over:Adventures with the Open Tools API
Deleting an entry from this key will remove the associated package from the list. To only disable the package, add a similar entry for the package to

HKEY_CURRENT_USER/Software/Borland/Delphi/6.0/Disabled Packages

, visible towards the middle of the above figure.

Example: EPCCodeJumpWizard.dpk – IDE editor enhancement module.

We will now look at a more extensive example that adds new behaviour to the IDE editor. This plug-in makes use of the keyboard facilities introduced in Delphi 5. The keyboard architecture can be conceptualised as a collection of key-mapping modules , and a list of enhancement modules

, each module containing a collection of behaviours and associated keystroke bindings.

This is presented in the IDE in Tools|Editor Properties…|Key Mappings

Key-mapping modules must provide a handler for all keystrokes, a full keyboard implementation. The current key-mapping module can be changed via the dialog mentioned above.

Enhancement modules, by definition, provide a partial keyboard implementation. Multiple enhancement modules can be installed and ordered, overlying the current key-mapping module.

Keystrokes entered into the IDE editor filter down the list of enhancement modules, till a matching keystroke binding is encountered. The key-mapping module, at the bottom, supplies the default behaviour.

This example implements an enhancement module. It provides block-end matching for all Object Pascal block statement constructs up to and including Delphi 6 (excluding

repeat/until

)  - toggling the cursor position to either end of the block, or highlighting the enclosing block. It also provides cyclic jumps between all

uses

clauses and the current position, and cyclic jumps to the

interface

,

implementation

,

initialisation

,

finalization

sections and the current position.
Keybinding Description
Alt+Shift+Home Acts as a toggle. The first press will move the cursor from current position to the start of the minimally enclosing block statement. The second press restores the previous cursor position.
Alt+Shift+End Acts as a toggle. The first press will move the cursor from current position to the end of the minimally enclosing block statement. The second press restores the previous cursor position.
Alt+Shift+B Acts as a toggle. The first press will select all the minimally enclosing block statement for the current cursor position. The second press unselects the block and restores the previous cursor position.
Alt+Shift+Left Moves the cursor back to the previous position after one of the following jumps:
Alt+Shift+I Repetitive presses cycles through: INTERFACE, IMPLEMENTATION, INITIALIZATION, FINALIZATION sections, current position
Alt+Shift+U Repetitive presses cycles through: INTERFACE USES clause, IMPLEMENTATION USES clause, current position
The OpenTools-related code all resides in the

XPCodeJumpWizard.pas

unit.

This plug-in works by scanning the currently focussed IDE editor buffer, when it either changes or is modified. One token event handler for the scanner,

TCodeJumpWizard.BlocksParser

, parses for block syntax and creates a mapping of all language block constructs and their endpoints in the current buffer. Another,

TCodeJumpWizard.JumpsParser

, parses for uses clause and section heading syntax, also creating a mapping to buffer positions. The keybinding handlers perform look-ups against these mappings.

We need to determine when the current IDE editor buffer changes, or is closed. For this we implement

IOTAModuleNotifier

, and register it with the current module. There is also an

IOTAEditorNotifier

interface, but these are never called by the IDE. We determine the current module, and all other context-related properties, through the

IOTAKeyContext

variable passed in to all keybinding handlers. The signature for those handlers looks like:

type

TKeyBindingProc =

procedure

(

const

Context: IOTAKeyContext; KeyCode:   TShortcut;

var

BindingResult: TKeyBindingResult)

of

object

;

An enhancement or key-mapping module must implement the

IOTAKeyboardBinding

interface.

  TCodeJumpWizard =

class

(TNotifierObject, IOTAKeyboardBinding,     IOTANotifier, IOTAModuleNotifier)

Return

btPartial

or

btComplete

to indicate an enhancement module or key-mapping module respectively.

function

GetBindingType: TBindingType;

Return user-friendly and unique name (typically

Organisation.BindingName

) respectively.

function

GetDisplayName: string;    

function

GetName: string;

Bind locally defined handler functions to keystroke combinations. Look over the examples in

Demos/ToolsAPI/Editor Keybinding

in your product installation or CD. All keystrokes are not the same! Also, avoid any keystrokes with only an Alt modifier, eg Alt+X. These may cause access violations when your plug-in is loaded or unloaded. Other combinations, eg Alt+Shift+X are safe. Unused key combinations are a scarce commodity, so you may end up overriding some other IDE behaviour, or in turn, get clobbered by another enhancement module. Do a bit of research before you choose.

procedure

BindKeyboard(

const

BindingServices: IOTAKeyBindingServices);

IOTAKeyBindingServices,

passed as an argument above, is the one Services interface not accessible through querying the

BorlandIDEServices

global interface variable.

The plug-in also handles syntactically incomplete units, adding context-jumpable messages to the IDE’s MessageView, similar to compiler messages. See

TCodeJumpWizard.CheckBlocks

.

There is another class declared in

XPCodeJumpWizard.pas.

This is TCJOwnerWizard which acts as a container for a TCodeJumpWizard instance. The reasons are explained in the code and relate to reference counting and unloading of the plug-in mid-session. Using a container interface is often a good solution for reference count problems.

Example: EPCBufferList.dpk – dockable IDE Buffers ListView.

This example is a work in progress. It implements a dockable list-view of all the open buffers in the IDE. Stemming from a dockable IDE example published on the Borland Community web site (see Opening Doors – Article 3 in Resources section below), the desired outcome was a better UI than the tabbed notebook of the IDE editor. That interface is fine for a handful of open files, but useless for managing lots of open files.

The list view is sortable in both directions via file name and location. Select or multi-select entries to focus or close buffers. It uses the

INTAServices

to install the menu item, and

IOTAServices

to receive notifications of files opening or closing and synchronise the display. Unfortunately, this doesn’t provide individual file notifications when the encompassing project or project group is closed, so an

IOTAModuleNotifier

approach will be required for a complete solution.

I would advise you to read the article mentioned above, as there is a lot of dummy code to get the undocumented IDE package to work with the plug-in in the form designer. The active code for this plug-in is contained in the file

XPBufferListToolbarForm

.

pas

.

Resources

Borland Community site (community.borland.com)

"Opening doors (Getting inside the IDE)"

by Allen Bauer, Staff Engineer/Delphi&C++Builder R&D Manager

Article 1: http://community.borland.com/article/0,1410,20360,00.html

Article 2: http://community.borland.com/article/0,1410,20419,00.html

Article 3: http://community.borland.com/article/0,1410,21114,00.html

Interview with Allen Bauer

Talking about OTA enhancements for Delphi 5. This download appears corrupted nowadays.

http://community.borland.com/article/0,1410,21046,00.html

Open Tools API "Live Chat" transcript (Nov 18, 1999)

http://community.borland.com/article/1,1410,20099,00.html

"Using the Open Tools API in Kylix"

by Ray Lischner, Tempest Software  (Author of "Hidden Paths of Delphi 3", and others)

http://community.borland.com/article/0,1410,27205,00.html

Open Tools API

by Ray Lischner, Tempest Software.

Covers Delphi 3 & 4, with good bug coverage: http://www.tempest-sw.com/opentools

GExperts Open Tools API FAQ

Lots of howtos and bug documentation: http://www.gexperts.org/opentools

Newsgroups

The OpenTools API newsgroup. An excellent

resource for answers to specific questions. Ray Lischner (Hidden Paths) and Erik Berry (GExperts) are regular posters here…

news://newsgroups.borland.com:119/borland.public.delphi.opentoolsapi

Formerly DejaNews, acquired by Google. A searchable archive of the borland.public.delphi.opentoolsapi newsgroup

http://groups.google.com/groups?hl=en&lr=lang_en&safe=off&group=borland.public.delphi.opentoolsapi

Books

“Hidden Paths of Delphi 3” by Ray Lischner, Informant Press 1997

ISBN 0-9657366-0-1

Software

GExperts

An open source collection of Delphi wizards:http://www.gexperts.org

Ray Lischner's Expert building components

Unsupported freeware with source

Delphi 3: http://www.tempest-sw.com/freeware/Delphi/etk10.exe

Delphi 4: http://www.tempest-sw.com/freeware/Delphi/etk40.exe

Other shareware and freeware

Delphi Super Pages (Australian mirror): http://mirror.aarnet.edu.au/pub/delphi

Torry’s Delphi Pages: http://www.torry.net

[1]   There is much urban myth about the reason for the case-sensitivity of

Register

in the otherwise case-insensitive Delphi, but Allen Bauer sets the record straight in his third “Opening Doors” article on the Borland Community website:

”The actual reason is that with Win32, all exports from a DLL are case-sensitive. When Delphi loads a design-time package it looks in a special compiler generated resource in the package that describes its contents. This resource lists the unit names within the package. The IDE then scans through this list creating a string containing the unit name and the

Register

procedure then does a

GetProcAddress

call on that string. If it locates that entry-point it is called. Sure, we could try all possible combinations, but that would be 256 combinations checked for each unit in that package!”

[2]   Supports can also tolerate a nil first argument, whereas QueryInterface, the function it wraps, will cause an exception when applied to an unassigned interface.

[3] IOTAKeyBindingServices is not accessible directly through the BorlandIDEServices global variable. Instead, it is passed as an argument to the IOTAKeyboardBinding.BindKeyboard method. Keyboard bindings are manipulated via the IOTAKeyboardServices interface, which is accessible via BorlandIDEServices.

Plug-in, Switch On, Fall Over: Adventures with the Open Tools APIPlug-in, Switch On, Fall Over:Adventures with the Open Tools API

Copyright © 2001-2003, The Excellent Programming Company

<script language=JavaScript> </script> Last updated: 02/18/2003 21:15:46

继续阅读