[All]
Opening Doors: Notes On the Delphi ToolsAPI by its Creator - Part 2
By: Allen Bauer
Abstract: The second Delphi Open ToolsAPI article, by Delphi R&D member Allen Bauer, gives developers a "cookbook" format, with step-by-step instructions, for creating an IDE extension in Delphi.
| Opening Doors |
|
Getting inside the IDE
by Allen Bauer, Staff Engineer/Delphi&C++Builder R&D Manager |
| On to the starting line |
|
In my last
article, I gave the reader a little bit of and overview and some of
the design goals of the Borland Delphi/C++Builder IDE Open Tools
API. In this installment, I will get you out of the starting gate
and heading down the track toward writing your own IDE extensions.
If you have not done so, please take some time to scan through the
ToolsAPI.pas file in the Sourcetoolsapi directory. This file ships
with only the Professional and Enterprise versions of Delphi. Also,
I will not be covering any of the old non-interface based
Delphi2.0-3.0 OTA. This article presumes that the reader has a good
understanding of Delphi interfaces and their use. If you are
uncomfortable with or have never used Delphi interfaces, I suggest reading
one of several of the Delphi books available such as Charlie Calvert's
Delphi Unleashed. |
| Gentlemen, start your engines... |
|
There are a couple of ways in which an
extension can be added to the IDE. The first is to create a
traditional Windows DLL that exports a specific entry point. The
reference to this DLL is then added to the registry under a specific key
used by the IDE during startup. The IDE then calls the entry point
with a parameter that allows the add-in DLL to "register" its
extensions with the IDE.
The second mechanism is one that I prefer because the it
allows an IDE extension to be loaded and unloaded on demand by the
user. This involves creating a design-time package as if one were
creating components to be added to the IDE's tool palette. The other
very good reason to use the package approach is that accessing the
internals of the IDE is much easier since the boundaries between the
package and the IDE are much less. For instance, it is possible to
actually create a new TMenuItem component and simply insert it into the
IDE's main menu. This is made possible by the fact that both the core
of the IDE and the add-in package both must share the exact same in-memory
copy of both the RTL and VCL. Also in future articles when I cover
creation of a dockable IDE add-in forms, this mechanism is essential.
While the flexibility of using packages is great, it does place a burden
on the programmer in that they must be prepared to be removed from memory at
any time. This means that everything done when the add-in was
initialized, must now be undone and in many cases in the reverse
order. You should not really view this as a burden, but simply
enforcement of good programming practices.
The Open Tools API is divided into different sub-systems
loosely referred to as "Services." For example, if you look
in ToolsAPI.pas, you will see several interfaces named IOTAxxxxServices.
These interfaces provide the top-level access to each of the
specific subsystems. Many of these you'll recognize, like
IOTADebuggerServices, IOTAKeyboardServices or IOTAMessageServices.
From one of these interfaces, a lot of information can be accessed.
You can also attach "notifier" interfaces to these sub-systems
in order for your add-in to be notifications of important events as they
occur within the IDE.
So how do I get one of these "Services" interfaces? If you
again refer to ToolsAPI.pas, you'll notice that immediately above the implementation
keyword there is a global variable called BorlandIDEServices. If
you notice, the type of this variable is simply IUnknown. That
isn't very interesting...or is it? You see, the key here is the QueryInterface
method. Or, as the interface savvy Delphi programmer would notice, as
is the key.
var
DebuggerServices: IOTADebuggerServices;
begin
DebuggerServices := BorlandIDEServices as IOTADebuggerServices;
...
end;
Remember, since the ToolsAPI unit is also used by the IDE, it actually
resides in VCL40 or VCL50. You won't get anywhere if you simply
compile ToolsAPI.pas into your application since the BorlandIDEServices
variable is initialized deep in the core of the IDE. |
| And they're off... |
|
Let's do something interesting, if not really useful.
In this example, I'll create an add-in that adds a notifier to the IOTAServices
interface in order to get notifications of files opening and closing
within the IDE. Then, in these notifications, the add-in will
simply insert messages into the IDE's message view indicating the event.
To start this off right, we need a package. I usually just use
the File|New dialog and select Package. This creates a
Package1.dpk. You should go ahead and save this new project and give
it a meaningful name, like SillyOTAExample.dpk.

Now we need a place to put our code. Since this is
going to be a design-time package, a unit with a global procedure named Register
must exist in the package in order for things to work correctly. By
using the Register procedure, the precise moment when it is
safe to "muck" with things is guaranteed because the IDE ensures
things are ready when it calls this procedure. Since there is no
corresponding Unregister procedure, the unit finalization
section will be used for any cleanup that may be necessary.
I'm going to cheat a little and use the New Component Wizard to generate
this for me. Click the little Add Button,
on the package manager and enter information similar to the following....
(Note you can ignore the path information since that is specific to my
machine and happens to be ahem.... my C++Builder IDE dev tree.. ;-)

|
| Gaining Speed |
|
By now you should have a new package with a new unit that
contains a skeleton class derived from TNotifierObject. If you were
to compile this package now.... well let's just say, it will die a
horrible death at the hands of the compiler. We need to make a few tweaks
under the hood... The TNotifierObject is a little helper
class in ToolsAPI.pas that simplifies the implementation of the IOTANotifer
interface. Many times the IOTANotifier methods are not called in certain circumstances so rather than
always having to declare all these methods each time IOTANotifier is
implemented, it is a simple shortcut to use the TNofitierObject
as the ancestor class.
The first tweak I'm going to make is to move the entire declaration of the
class to the implementation section. This is
because there is no need to access this class outside this unit or even
outside this package. By moving the class entirely to the implementation
section, I can reduce the number of exported symbols from the package,
thus reducing its size.
I also need to update the uses list to be better suited for what I
need. Oh, and I don't need that RegisterComponents call
or the classes' published section
either. Here's what the unit now looks like:
unit SillyOTAObject;
interface
procedure Register;
implementation
uses
Windows, SysUtils, Classes, ToolsAPI;
type
TSillyOTAObject = class(TNotifierObject)
private
{ Private declarations }
protected
{ Protected declarations }
public
{ Public declarations }
end;
procedure Register;
begin
end;
end.
|
| Rounding Turn One... |
|
Let's put some meat on this skeleton. As
stated in the original design, we want to output a message in the message
view when a file open or close event occurs. To do this, we need to
implement the IOTAIDENotifier interface. This interface
then needs to be added to the IDE's internal list of notifiers on the IOTAServices
interface. An item worth noting that is common about all the various
AddNotifier methods, is its return value. You, the
programmer, are responsible for adding and removing your notifiers from
the internal notifier list by using the AddNotifier and RemoveNotifier
methods. AddNotifier returns an Integer value
that is unique to that notifier. This value is actually an internal
index into an array of notifier interfaces to call whenever the desired
event happens. RemoveNotifier in turn, uses this index
to know which array slot to release. When we call AddNotifier,
the return value must be saved someplace so it can be used when this
package is unloaded to call RemoveNotifier to cause the IDE
to release its reference to our notifier implementation. Here's how
the class now looks with only stub implementations for the methods:
type
TSillyOTAObject = class(TNotifierObject, IOTANotifier, IOTAIDENotifier)
private
{ Private declarations }
protected
procedure FileNotification(NotifyCode: TOTAFileNotification;
const FileName: string;
var Cancel: Boolean);
procedure BeforeCompile(const Project: IOTAProject;
var Cancel: Boolean); overload;
procedure AfterCompile(Succeeded: Boolean); overload;
public
{ Public declarations }
end;
{ TSillyOTAObject }
procedure TSillyOTAObject.AfterCompile(Succeeded: Boolean);
begin
{ Not interested in AfterCompile at this time }
end;
procedure TSillyOTAObject.BeforeCompile(const Project: IOTAProject;
var Cancel: Boolean);
begin
{ Not interested in BeforeCompile at this time }
end;
procedure TSillyOTAObject.FileNotification(
NotifyCode: TOTAFileNotification; const FileName: string;
var Cancel: Boolean);
begin
end;
Now let's get this notifier into the right place... The fun
really starts in the Register procedure. Here's how it
looks:
var
Index: Integer;
procedure Register;
begin
Index := (BorlandIDEServices as IOTAServices).AddNotifier(TSillyOTAObject.Create);
end;
initialization
finalization
(BorlandIDEServices as IOTAServices).RemoveNotifier(Index);
end.
Another item to note here is that I hold no reference to the TSillyOTAObject
class. This is because at this point there is no need
to. In fact it could be dangerous to do so because since this object
implements interfaces and is fully lifetime managed, explicitly
freeing the object could have disastrous consequences. All that is
needed is the index value returned from the AddNotifier
method. Make sure that the index value received is the only
value used in the call to RemoveNotifier. I'm sure
other OTA developers would not appreciate having their notifier interface
removed by some errant OTA extension without their
knowledge...
|
| On the Back Stretch... |
|
Now let's finish this thing up. So far
we've created a new package, added a skeleton unit and class declaration,
and munged it around to fit our needs. Then we implemented the IOTANotifier
and IOTAIDENotifier interfaces, and registered them with the IOTAServices
interface. The cool stuff now happens in the FileNotification
method when various file oriented events occur. These include
opening/closing of files, saving/loading of desktop state, etc... This
example will also demonstrate a simple example of using the
IOTAMessageServices. So to keep from boring you too much, here's
what the body of the FileNotification method looks like:
procedure TSillyOTAObject.FileNotification(
NotifyCode: TOTAFileNotification; const FileName: string;
var Cancel: Boolean);
begin
case NotifyCode of
ofnFileOpened: (BorlandIDEServices as IOTAMessageServices).AddTitleMessage(
Format('%s Opened', [FileName]));
ofnFileClosing: (BorlandIDEServices as IOTAMessageServices).AddTitleMessage(
Format('%s Closed', [FileName]));
end;
end;
Now comes the moment of truth. Press the Install Button
on
the package manager window. This will compile the package and
install the package into the IDE. If all went as planned you should
see no noticeable change. It things didn't go so well... you may
have crashed the IDE... Well, that is one of the pitfalls of
developing an OTA extension. Since you are playing around in the
IDE's sandbox (or process space), it is now at the mercy of whatever code
you decide to put in your extension. By the same token, that is also
where the full power of the OTA is realized.
Assuming that things went OK, try opening a file.. say ToolsAPI.pas.
Right click on the editor and select Message View. There should now
be at least one message in the message view that indicates what file was
opened. Congratulations, you have just built an OTA IDE extension!
|
| The Home Stretch... |
|
You may not be to excited about this particular
example, but it does demonstrate the power of packages, coupled with the
OTA. One thing you may find rather interesting is how the IDE is
able to automatically unload a package allowing you to recompile it. Then
reload that package once it is successfully compiled. Once the
package is installed, the IDE really tries to help speed your development
by not requiring the IDE to be shut-down just to recompile the
package. Say you wanted to modify the message that was output to the
message view. Just modify the SillyOTAUnit source code, recompile
the package and... BINGO! your code changes are immediately reflected in
the IDE. Again, as stated before, any logic errors that cause
crashes, could have an adverse affect on the IDE, so I would suggest that
when developing an OTA extension in this manner that you enable the Auto
save files option. See the Tools|Environment Options dialog.
|
| And the Winner Is..... |
|
Things are going to start getting better from
here, so stay tuned for the next installment where we'll investigate
CustomModules and creating your own source code wizards similar to the Web
Server Application wizard and the TWebModules. Oh and I haven't
forgotten about my promise to show you how to create your own forms in the
IDE that dock like the rest of the built-in IDE forms. That will be
coming in another future article. Hopefully that should be enough
for you to chew on for a while...
With the holidays fast approaching, this will probably be my last
article until the new year (millennium or century, unless you want
to treat 2001 as the new century/millennium...;-).
Happy Holidays.
Allen Bauer.
|
|
|
|