|
Implementing a Plug-in
Framework
by Binh Ly
Download the source code for this article
A plug-in (or snap-in) framework provides
a
standard mechanism for developing applications that need to be easily evolved and
customized. For example, you might want to develop a commercial application for
different audience levels/versions (say Standard, Professional, and Enterprise).
Instead of maintaining 3 separate code bases for the different versions, you can
simply create a single shell/host application that incorporates the features
available for each version as incremental plug-ins. Using this idea, the Standard version will have
the host application + standard plug-ins, the Professional version will have the
host application + standard + professional plug-ins, and the Enterprise version
will have the host application + standard + professional + enterprise plug-ins.
Another reason to use plug-ins is to allow your clients to create customized sub-modules that
plug in to your (host) application. This is very common for utility applications
such as operating systems or operating system shells. For instance, the Win32
shell provides a slew of APIs and COM interfaces that allow you to write your own
shell extensions or shell plug-ins.
Incidentally, COM is a perfect solution
for implementing plug-in frameworks. This is made possible because of COM's binary
interoperability aspect. In simple terms, you can use COM to create a
framework consisting of host and
plug-in applications, possibly written in different languages (Visual Basic, Delphi, C++, etc.),
and be able to have them all work together seamlessly. In this tutorial I'll
show you how to implement a simple plug-in framework based on COM.
A Universal Explorer
Basically, a plug-in framework consists
of 2 parties: the host and the plug-in. The host is the application that
"contains" the plug-in. The host exposes a standard interface,
IHost, that the plug-in uses when talking to the host. Similarly, the
plug-in also exposes a standard interface, IPlugin, that the host uses when
talking to the plug-in. For our purposes, we'll create a simple application
called a universal Explorer. Explorer is a host application that can be used to explore
any type of information that can be presented in a hierarchical manner. This
information can be your
computer's file system, database master-detail relationships, organizational
charts, your family tree, etc. Explorer will expose a host interface called
IExplorer.
Explorer's plug-ins will be modules that
know how to navigate hierarchical information. For instance, we can create a
file system plug-in for exploring our computer's file system. In fact, we'll
create a file system plug-in to demonstrate Explorer's capabilities. Each of
Explorer's plug-ins will expose a plug-in interface called IExplorable.
Interface Design
We'll want each plug-in to be able to
describe itself and to hold a pointer back to its IExplorer host. Thus:
IExplorable
= interface
procedure SetExplorer(Explorer); //called by IExplorer host to set
IExplorable's back pointer to host
function GetDescription : string; //Explorable
describes itself
end;
For any given node in the hierarchy, the
Explorer host will want to be able to display its children. For this to work, there
has to be way to iterate that node's child information. For instance, in a file system,
if C:\ contains 5 files, there has to be a way for Explorer to ask the plug-in what the 5
files under C:\ are. IExplorable provides this
facility through a second "iterator" interface, ISubItems:
ISubItems =
interface
function GetCount : integer; //returns sub-item count for a given node
function GetItem (Index) : string; //returns each sub-item element
(0-based index)
end;
Given ISubItems, Explorer can obtain how
many sub-items (Count) and what the sub-items are (Item []) under any given node in the hierarchy.
Therefore, we'll need to add a method to IExplorable that returns ISubItems
given a node:
IExplorable
= interface
procedure SetExplorer(Explorer);
function GetDescription : string;
function GetSubItems (Path) : ISubItems; //returns ISubItems
iterator given a node
end;
The Path parameter is simply a full path
(from the root) to the node where we want to obtain sub-items. For example,
GetSubItems ("c:\") would return sub-files/folders at the C:\ folder level
whereas GetSubItems ("c:\Level1\Level2\Level3") would return
sub-files/folder at the c:\Level1\Level2\Level3 folder level.
We also want to obtain information about
a given node in the hierarchy. After all, what good is an explorer if we can't
get detailed information at each node. For simplicity we're only interested in
information that can be presented as name-value strings. For instance,
information on a node in a file system might look like this:
| Property Name |
Value |
| Type |
File (or Folder) |
| Name |
Filename.txt |
| Size |
1234 |
| Date/Time |
1/1/2000 12:00 AM |
| Attributes |
Archive, Hidden, System |
Name-value pairs like this are really
node properties, so we'll just add a GetProperties method to IExplorable:
IExplorable
= interface
procedure SetExplorer(Explorer);
function GetDescription : string;
function GetSubItems (Path) : ISubItems;
function GetProperties (Path) : Array; //returns property
name-value pairs for given node
end;
As you'll see later, we'll implement
GetProperties by using a
2-dimensional array to return the name-value table I've shown you above.
Last but not least, we'll also get a bit
fancy and provide a context menu capability for Explorer. This enables Explorer
to perform any
kind of custom operation on any node in the hierarchy that our plug-in supports.
For our purposes we'll allow the user to be able to right-mouse-click on any node
(file/folder) in the file system hierarchy and have our file system plug-in
provide a context menu action that enables the user to rename a file:

Figure: Explorer context menu
(consisting of Rename action)
| Note that we'll allow renaming of files with *only* the Archive attribute bit set so
that you don't accidentally go off and rename one of your system or hidden
files. However, you can easily tweak the code to circumvent my safety
protection scheme but you do so at your own risk. |
Executing a custom context menu action consists of
2 stages: 1 to display the action in a popup menu and 2 to actually execute
that action that the user selected from the menu. In other words, on a
right-mouse-click, Explorer will first ask our plug-in for a list of action menu items
that can be done on a given node. This list will be an action name-ID pair like
this:
| Action Name |
Action ID |
| Rename |
1 |
| Delete |
2 |
| View |
3 |
| ... |
... |
Given this table, Explorer will show the
action names in the context menu. After that, whenever the user selects an item
in the menu, Explorer will then ask the plug-in to execute the action
(specifically the action ID) on the selected node. In other words:
IExplorable
= interface
procedure SetExplorer(Explorer);
function GetDescription : string;
function GetSubItems (Path) : ISubItems;
function GetProperties (Path) : Array;
function GetMenuActions (Path) : Array; //returns
context menu actions for given node
function DoMenuAction (Path; ActionId); //executes selected
action ID on given node
end;
To reiterate, Explorer will call
GetMenuActions to populate the context menu with action information. Once the
user selects an item from the menu, it then
calls DoMenuAction to trigger the action associated with that menu item.
That's it for IExplorable. Let's now look
at what we can do with the host interface, IExplorer.
For simplicity, I added a method to
IExplorer that our file system plug-in can call after a successful file rename
action:
IExplorer =
interface(IUnknown)
procedure RenamePath (OldPath; NewPath); //called by plug-in
when a node's name changes
end;
RenamePath is used by the plug-in to
notify the Explorer host that a name change has happened to one of its nodes.
This enables the host
to perform any necessary action accordingly. More specifically, Explorer can implement
RenamePath to refresh it's UI view of the hierarchy to reflect the name change.
That's it for the simple IExplorer.
Component Categories
One of the most common questions when
designing a plug-in framework is "What is the standard mechanism
of how to determine what plug-ins are available for a given host?" This question
arises from the fact that everytime we run the host application, we need to determine
its available (compatible)
plug-ins. Do
the plug-ins announce their availability by writing entries to a common registry
location? An INI-file? A common database file? Somewhere?
Fortunately, COM provides a standard
mechanism that enables a plug-in to announce its capabilities (i.e. what type of plug-in
it is) and for a host application to announce what it offers (i.e. what can a
plug-in expect from it). This mechanism is called component categories.
To further illustrate, our Explorer can only host plug-ins that are explorable
(i.e. supports IExplorable). We can group the set of all explorable plug-ins
together and give it a name: "Explorable Plugins". In COM, when we
group objects together based on some commonality, the group that we come up with
is called a component category. Therefore, we say that the set of all
explorable plug-ins is the "Explorable Plugins" component category.
COM component categories are stored in a
distinct location in the registry (HCKR\Component Categories). Just like interfaces, coclasses, and
other COMisms, each category is uniquely identified by a GUID - more
specifically a category ID or CATID. By now, it should be clear
that we need a CATID for "Explorable Plugins". I've defined this CATID
as follows:
//category
ID for explorable
plug-ins
CATID_Explorable = "{5111C0AC-7397-11D3-A801-0000B4552A26}";
The next step is to get this CATID
registered. Again, COM can help a bit here by providing standard interfaces (and
implementation) for managing component categories. In particular, there's 2
interfaces that you'll be friends with: ICatRegister and ICatInformation. ICatRegister, as the name implies, provides mechanisms
for registering (and
unregistering) component categories. ICatInformation, as the name implies,
provides mechanisms for obtaining registered category information.
ICatInformation and ICatRegister are both
implemented by a COM coclass defined by CLSID_StdComponentCategoryMgr. When we
want ICatInformation, we simply create this coclass asking for ICatInformation,
and when we want ICatRegister, we simply create this coclass asking for
ICatRegister.
When registering a CATID into HKCR\Component
Categories, we'd be interested in ICatRegister.RegisterCategories:
ICatRegister =
interface (IUnknown)
function RegisterCategories (
cCategories: UINT; //number of CATIDs to register
rgCategoryInfo: PCATEGORYINFO //array of
category information to register
): HResult; stdcall;
... other methods omitted ...
end;
The rgCategoryInfo part simply points to
an array of category information (CATEGORYINFO) records each of which looks like
this:
TCATEGORYINFO = record
catid: TGUID; //category ID
lcid: UINT; //locale ID, for multi-language support
szDescription: array[0..127] of WideChar; //category
description
end;
Since we're only registering 1 category,
we'll simply fill our 1 category information record as follows:
//declare
variable ExplorableCategoryInfo of type TCATEGORYINFO record
var ExplorableCategoryInfo : TCATEGORYINFO;
//initialize ExplorableCategoryInfo record
ExplorableCategoryInfo.catid
= CATID_Explorable;
ExplorableCategoryInfo.lcid = LOCALE_SYSTEM_DEFAULT; //dummy
ExplorableCategoryInfo.szDescription = "Explorable Plugins";
With this, registering our category would
simply be:
var CatReg :
ICatRegister;
//create standard component cat manager asking for ICatRegister
CatReg = GetStdComponentCategoryMgr (ICatRegister);
//register CATID_Explorable category
CatReg.RegisterCategories (1, ExplorableCategoryInfo); //only 1 catinfo
record to register
So far, we've just registered our new
component category under HKCR\Component Categories. The next step is to mark our
plug-in implementation (coclass) as supporting this category. After all, a host
needs to determine which plug-ins implement a certain component category. Assuming that our
file system explorer plug-in is implemented by a coclass defined by this CLSID:
CLSID_FileSystemExplorable
= "{8B9A0689-7434-11D3-A802-0000B4552A26}";
The convention for announcing that this
coclass implements our "Explorable Plugins" component category is to
write registry subkeys under this coclass as follows:
HKCR\{8B9A0689-7434-11D3-A802-0000B4552A26}
//CLSID_FileSystemExplorable
Implemented Categories //indicates we implement some categories
{5111C0AC-7397-11D3-A801-0000B4552A26}
//indicates we implement CATID_Explorable
... list more implemented CATIDs
here if any ...
InprocServer32
...
As you might have guessed, getting these
entries into the registry can be done using ICatRegister. In particular,
ICatRegister.RegisterClassImplCategories registers information about a coclass's
implemented categories:
ICatRegister =
interface (IUnknown)
function RegisterClassImplCategories (
const rclsid: TGUID; //CLSID of coclass that implements
categories
cCategories: UINT; //count of category
information to register for this coclass
rgcatid:
Pointer //points to array
of TCATEGORYINFO records containing categories to register
): HResult; stdcall;
... other methods omitted ...
end;
With this, registering our
FileSystemExplorable coclass as implementing the "Explorable Plugins"
category can be done as follows:
//declare
variable ExplorableCategoryInfo of type TCATEGORYINFO record
var ExplorableCategoryInfo : TCATEGORYINFO;
//initialize ExplorableCategoryInfo record
ExplorableCategoryInfo.catid
= CATID_Explorable;
ExplorableCategoryInfo.lcid = LOCALE_SYSTEM_DEFAULT; //dummy
ExplorableCategoryInfo.szDescription = "Explorable Plugins";
var CatReg : ICatRegister;
//create standard component cat manager asking for ICatRegister
CatReg = GetStdComponentCategoryMgr (ICatRegister);
//register implemented categories for FileSystemExplorable coclass
CatReg.RegisterClassImplCategories (
CLSID_FileSystemExplorable, //FileSystemExplorable coclass
1,
//implements 1 category
ExplorableCategoryInfo //1 categoryinfo record
);
We can also unregister (remove from the
registry) a coclass's implemented categories. This is useful when we want to
unregister our plug-in from the system. To unregister, we simply use the
(obvious) ICatRegister.UnRegisterClassImplCategories method:
ICatRegister =
interface (IUnknown)
function UnRegisterClassImplCategories (
const rclsid: TGUID; //CLSID of coclass that implements
categories
cCategories: UINT; //count of category
information to register for this coclass
rgcatid:
Pointer //points to array
of TCATEGORYINFO records containing categories to unregister
): HResult; stdcall;
... other methods omitted ...
end;
As you can see, the unregistration syntax
is exactly the same as calling RegisterClassImplCategories. The only difference
here is that UnRegisterClassImplCategories will remove the registry category
information from under the "Implemented Categories" subkey of your
CLSID.
Now that we can annotate each plug-in
coclass in the registry to support any category, our host application can then
simply scan the registry CLSID entries and check each of the "Implemented Categories"
subkey to determine which coclasses are valid plug-ins (for a given category).
Scanning the registry doesn't have to be manual - we again use our
friend interface, ICatInformation. In particular, ICatInformation allows us to
get a list of all coclasses that implement a particular CATID. This is accomplished
through the ICatInformation.EnumClassessOfCategories method:
ICatInformation =
interface (IUnknown)
function EnumClassesOfCategories (
cImplemented: UINT; //count of implemented categories
we're interested in
rgcatidImpl: Pointer; //array of implemented
categories we're interested in
cRequired: UINT; //count
of required categories we're interested in
rgcatidReq: Pointer; //array of required
categories we're interested in
out ppenumClsid: IEnumGUID //return CLSID/GUID
enumerator of matching coclasses
): HResult; stdcall;
... other methods omitted ...
end;
The parameters we're most interested in
are cImplemented, rgcatidImpl, and ppenumClsid. In our case, we're only
interested in 1 category so cImplemented=1, and rgcatidImpl would point to our
single TCATEGORYINFO record. Upon a successful scan, EnumClassesOfCategories
returns an enumerator in ppenumClsid. We can then use ppenumClsid to enumerate the list
of CLSIDs of each of the matching plug-in coclasses. This enumerator has the
same usage rules as any other standard COM enumerator (IEnumXXX) so I won't go into the
details of how to perform the enumeration - you can simply check the source code
to see how it's done.
| In a similar
respect to "implemented categories", a plug-in may also register
itself as requiring the host to implement certain interfaces (or
categories). This way, a host can also tell whether or not it is possible
to host a particular plug-in based on what the plug-in expects of the
host. This information is stored in another subkey, "Required
Categories", under each plug-in's CLSID registry key.
Although I won't be showing how to
implement "required categories" in this tutorial, it's good to
know that such a facility exists and is also part of COM component
categories. For a complete reference, you can check MSDN online for
details. |
So far, we can register component
categories (CATIDs), we can register our coclasses as implementing a
particular category, and we can enumerate all coclasses that
implement/support a given category. With this, we're now ready to implement
Explorer and our FileSystemExplorable plug-in.
Before we proceed, I just want to make a
few comments about the implementation:
- Our Explorer host will be an MDI
application wherein each MDI child form loads a separate explorable plug-in.
Each explorable plug-in is presented in a simplistic tree view UI, although
it's certainly possible to develop a more sophisticated UI that accurately
represents your hierarchical data.
- Our file system explorable plug-in is
implemented as a DLL server. Plug-ins are normally implemented as such. In
addition, the file system plug-in coclass is implemented as a lightweight
COM object instead of an automation object. This is simply because we have
no need of automation capabilities from our plug-in coclasses.
- The IExplorable plug-in interface is
implemented as IUnknown-based (instead of the more familiar IDispatch). In
the type library, I've marked IExplorable with the [oleautomation] flag for
it to be standard/type-library marshaled. I did this because most plug-in
frameworks you'll encounter will consist of plug-in interfaces that are
IUnknown-based - this way, you won't be surprised once you start
implementing plug-ins for other vendor's applications/frameworks.
Because IExplorable is IUnknown-based, each method is implemented as
returning an HRESULT (this is good programming practice). So in the actual implementation, methods that return
values actually return the values through an extra [out] parameter.
That's it. Shall we?!
Defining the Plug-in Framework
Interfaces
The first thing we need to do is to
define our plug-in framework interfaces in a type library. From there, we can
later use the type library definitions when implementing Explorer and the
explorable plug-ins. To do this, we create a type library (select File | New
menu and pick Type Library on the ActiveX tab in the dialog) and name it
Explorer.tlb. Then we go into Explorer.tlb and define IExplorer (host
interface), IExplorable (plug-in interface), and ISubItems (IExplorable helper),
as discussed earlier. I won't bore you with the step-by-step details on how to do
this - you can simply inspect the final Explorer.tlb file from the source
download to see how I did this. After inserting the interfaces, we register the
type library (click the Register Type Library icon in the Type Library Editor (TLE))
ready for COM usage.
With the registered interfaces/type
library in place, let's now proceed with the real implementation.
Delphi
Implementation
C++ Builder Implementation
Implementing the File System Plug-in
In Delphi, we create an in-process (DLL) COM
server project using the wizard accessible from the File | New menu and
by picking ActiveX Library on the ActiveX tab in the dialog. Let's name
this project "FileSystemPlugin" - Delphi will produce a file named
FileSystemPlugin.dll as our plug-in server.
Then we create our plug-in coclass. To do
this, we select File | New and pick COM Object in the ActiveX tab
in the dialog, name the object "FileSystemExplorable", and save the
new module as FileSystemExplorable.pas.
| In Delphi 4/5,
make sure you uncheck the "Include Type Library" checkbox when
generating the FileSystemExplorable class. We're simply creating a
lightweight COM object and we don't need any information entered into a
type library.
Also, Delphi 3 does not have the
create "COM Object" option under the ActiveX tab. However, you
can manually create the FileSystemExplorable class that looks like the one
Delphi 4/5 produces. Don't worry, the VCL classes you need are there -
it's just that D3 doesn't give you the wizard. |
By doing this, Delphi creates a plain
lightweight COM object like this:
type
TFileSystemExplorable = class (TComObject)
end;
const
Class_FileSystemExplorable: TGUID = '{8B9A0689-7434-11D3-A802-0000B4552A26}';
Since we're creating a plug-in, we want
FileSystemExplorable to implement the IExplorable plug-in interface. To do that,
we manually add IExplorable to TFileSystemExplorable as follows:
type
TFileSystemExplorable = class(TComObject, IExplorable)
protected
//IExplorable methods
function SetExplorer(const Explorer: IExplorer): HResult; stdcall;
function GetDescription(out Description: WideString): HResult; stdcall;
function GetSubItems(const Path: WideString; out SubItems: ISubItems): HResult; stdcall;
function GetMenuActions(const Path: WideString; out Actions: OleVariant): HResult; stdcall;
function DoMenuAction(const Path: WideString; ActionId: Integer): HResult; stdcall;
function GetProperties(const Path: WideString; out Properties:
OleVariant): HResult; stdcall;
protected
FExplorer : IExplorer;
end;
Then we implement the methods. Let's
start with the trivial SetExplorer and GetDescription:
function
TFileSystemExplorable.SetExplorer (const Explorer: IExplorer): HResult;
begin
FExplorer := Explorer; //FExplorer is a member field of
TFileSystemExplorable
Result := S_OK;
end;
function TFileSystemExplorable.GetDescription (out Description: WideString): HResult;
begin
Description := 'File System (Delphi version)';
Result := S_OK;
end;
Nothing complicated here. Let's do
GetSubItems instead.
Remember, GetSubItems is called by
Explorer to obtain an ISubItems interface to enumerate sub-items of any node in
the hierarchy. For us, that means that we have to scan the folder (specified by
the node/path) using the FindFirst and FindNext functions. Let's encapsulate
this process in a TSubItems class:
type
TSubItems = class (TInterfacedObject)
protected
FSubItems : TStringList;
procedure LoadSubItems (const Path : string);
procedure LoadDrives;
procedure LoadFiles (Path : string);
public
constructor Create (const Path : string);
end;
constructor TSubItems.Create(const Path: string);
begin
inherited Create;
FSubItems := TStringList.Create;
LoadSubItems (Path);
end;
//loads file system subitems given a path
procedure TSubItems.LoadSubItems(const Path: string);
begin
//reset list
FSubItems.Clear;
//if path is root, load system drives else load path as folder
if (Path = '') then
LoadDrives
else
LoadFiles (Path);
end;
//load system drives into FSubItems list
procedure TSubItems.LoadDrives;
begin
//implementation in pseudocode
Find all drives;
For each drive found
Add drive name into FSubItems list;
end;
//load folder subfiles (and subfolders) into FSubItems list
procedure TSubItems.LoadFiles(Path: string);
begin
//implementation in pseudocode
Find all files (and folders) under Path (using FindFirst/FindNext)
For each file (and folder) found
Add file name into FSubItems list;
end;
Constructing a TSubItems takes care of
loading the sub-files for a given path. The important method here is
LoadSubItems. In LoadSubItems, if the path specified is blank, then we load the
sub-items list with all the drives on your system, otherwise we simply do a
FindFirst-FindNext file scan of the given path.
Since FileSystemExplorable must hand out
ISubItems to Explorer, we simply implement ISubItems into TSubItems:
type
TSubItems = class (TInterfacedObject, ISubItems)
protected
//ISubItems methods
function GetCount(out Count: Integer): HResult; stdcall;
function GetItem(Index: Integer; out Item: WideString):
HResult; stdcall;
protected
FSubItems : TStringList;
...
end;
//return sub-item count
function TSubItems.GetCount(out Count: Integer): HResult;
begin
Count := FSubItems.Count;
Result := S_OK;
end;
//return sub-item text by index
function TSubItems.GetItem(Index: Integer; out Item: WideString): HResult;
begin
Item := FSubItems [Index];
Result := S_OK;
end;
Finally, we can go back to
TFileSystemExplorable and implement GetSubItems:
function TFileSystemExplorable.GetSubItems(const Path: WideString;
out SubItems: ISubItems): HResult;
begin
//ask TSubItems to return a subitems list for a given path
SubItems := TSubItems.Create (Path);
Result := S_OK;
end;
The next step is to implement
GetProperties. If you recall, GetProperties is called by Explorer to obtain
name-value pairs of information for any given node. More specifically,
GetProperties returns an array of name-value pairs. In COM, we can simply
implement this using a variant array. Thus:
function TFileSystemExplorable.GetProperties(const Path: WideString;
out Properties: OleVariant): HResult;
begin
Result := S_OK;
//Properties is a 2-dim array:
//
// | Property Name 1 | Property Value 1 |
// | Property Name 2 | Property Value 2 |
//
Properties := VarArrayCreate ([
0, 4, //row bounds
0, 1 //column bounds
],
varOleStr //string elements
);
//Type: File or Folder?
Properties [0, 0] := 'Type';
if IsFolder (Path) then
Properties [0, 1] := 'Folder'
else
Properties [0, 1] := 'File';
//Name
Properties [1, 0] := 'Name';
Properties [1, 1] := NameOfFile (Path);
//Size
Properties [2, 0] := 'Size';
Properties [2, 1] := IntToStr (SizeOfFile (Path));
//Date/Time
Properties [3, 0] := 'Date/Time';
Properties [3, 1] := DateTimeToStr (DateTimeOfFile (Path));
//Attributes
Properties [4, 0] := 'Attributes';
Properties [4, 1] := AttributesOfFile (Path);
end;
What we're doing here is simply returning
a 2-dim variant array (of 5 rows indexed from 0 to 4) consisting of the
following file properties: Type, Name, Size, Date/Time, and Attributes. I'm not
going to bore you with details of how to physically obtain each property value
because that's not relevant - feel free to check the source code for
details.
The last 2 methods we need to implement
are GetMenuActions and DoMenuAction. If you recall, these methods are for the
context sensitive custom operations that can be done on any node in your
hierarchy. GetMenuActions returns an array of action-ID pairs to Explorer and
DoMenuAction executes the user-selected action on a given node.
Also, if you recall, we only wanted to
implement a single menu action - Rename File. And we specifically want to rename
files with only the Archive bit set. Thus:
const ACTION_RENAME = 1;
function TFileSystemExplorable.GetMenuActions(const Path: WideString;
out Actions: OleVariant): HResult;
var
ActionCount : integer;
begin
Result := S_OK;
//this ActionCount business demonstrates how menu item actions can be dynamic
//depending on context
ActionCount := 0;
//Actions is a 2-dim array:
//
// | Action Name 1 | Action ID 1 |
// | Action Name 2 | Action ID 2 |
//
Actions := VarArrayCreate ([
0, 0, //row bounds
0, 1 //column bounds
],
varVariant //variant elements
);
//Rename
//allow rename on files with archive attribute only
if (FileOnlyHasArchiveAttributeSet (Path)) then
begin
//add Rename action
Actions [ActionCount, 0] := 'Rename';
Actions [ActionCount, 1] := ACTION_RENAME; //integer
constant defined as 1
//one action in!
inc (ActionCount);
end;
//you can add more custom action logic here...
//if no Actions in context, clear it
if (ActionCount <= 0) then Actions := Unassigned;
end;
As you can see, we're simply creating a
2-dim variant array to store the action-ID pairs. Since we're only interested in
a rename action, we only have at most 1 row element in the array (hence the row
bounds 0 to 0).
With this, whenever Explorer gets its
action list, it will populate its context menu with it. After that, if the
user selects the rename action, Explorer will then call DoMenuAction in our
plug-in, passing in the ACTION_RENAME action ID constant. Thus, we can simply implement DoMenuAction to process the rename action
as follows:
function TFileSystemExplorable.DoMenuAction(const Path:
WideString; ActionId: Integer): HResult;
var
NewName : string;
begin
Result := S_OK;
//case out ActionID
case ActionID of
ACTION_RENAME :
//rename and notify host if renamed
if (TfrmRename.Rename (Path, NewName)) then
//notify explorer host after rename action
FExplorer.RenamePath (Path, NewName);
//implement other ActionIDs here, if any...
end;
end;
In here, we check the
ActionID parameter and perform the corresponding action on the given path. For
ACTION_RENAME, we simply run a form (TfrmRename) that takes care of presenting
the user with a simple dialog box that allows the user to input a new name for
the given file. On a successful rename, we then proceed to call
FExplorer.RenamePath to tell our host Explorer that a certain node has changed
its name. This is done so that Explorer can then accordingly update its UI to
reflect the name change.
| This is an example
of how a plug-in may need to communicate back to its host. Normally, this
process is done so that the plug-in and the host can both synchronize
themselves to reflect state change in the data manipulated by
both the plug-in and the host. |
So much for IExplorable. The last thing we need to do is to somehow incorporate the code that registers
and unregisters
component categories into our plug-in server. The easiest way to do this is that
whenever our plug-in gets registered, we also register the category information
along with it, and whenever it gets unregistered, we also unregister the
category information appropriately. For DLL servers, the registration entry points
of interested are the
exported functions: DLLRegisterServer (when registering) and DLLUnregisterServer
(when unregistering).
Let's first take care of registration:
In
FileSystemPlugin.dpr
library FileSystemPlugin;
//overidden to include categories registration
function DllRegisterServer: HResult; stdcall;
begin
//call default implementation
Result := ComServ.DllRegisterServer;
//register as Explorable plugin
RegisterAsExplorableClass (Class_FileSystemExplorable, True);
//True means register
end;
exports
DllGetClassObject,
DllCanUnloadNow,
DllRegisterServer,
DllUnregisterServer;
...
In here, we're simply
"overriding" DLLRegisterServer to also include our category
information registration routine. RegisterAsExplorableClass is defined as
follows:
//registers a given class as an explorable server
procedure RegisterAsExplorableClass (const CLSID : TCLSID; Register : boolean);
var
CatReg : ICatRegister;
begin
CatReg := StdComponentCategoryMgr as ICatRegister;
if (Register) then
begin
//first register CATID_Explorable category
//ExplorableCategoryInfo is filled with cat info as discussed
earlier
OleCheck (CatReg.RegisterCategories (1, @ExplorableCategoryInfo));
//then register CLSID as supporting that category
OleCheck (CatReg.RegisterClassImplCategories (CLSID, 1, @ExplorableCategoryInfo));
end
else begin
//perform unregistration here
end;
end;
//returns standard component category manager
function StdComponentCategoryMgr : IUnknown;
begin
Result := CreateComObject (CLSID_StdComponentCategoryMgr);
end;
What we're doing here is simply
registering the "Explorable Plugins" category and then registering the
FileSystemExplorable coclass as implementing that category. We've discussed
these details before so there shouldn't be anything new here.
For the unregistration part, we simply
hook DLLUnRegisterServer and reverse what we've done in DLLRegisterServer. Thus:
In
FileSystemPlugin.dpr
library FileSystemPlugin;
//overidden to include categories unregistration
function DllUnregisterServer: HResult; stdcall;
begin
//unregister as Explorable plugin
RegisterAsExplorableClass (Class_FileSystemExplorable, False);
//False means unregister
//pass on to default unregistration handler
Result := ComServ.DllUnregisterServer;
end;
exports
DllGetClassObject,
DllCanUnloadNow,
DllRegisterServer,
DllUnregisterServer;
...
Accordingly, the unregister part in
RegisterAsExplorableClass is defined as:
//registers a given class as an explorable server
procedure RegisterAsExplorableClass (const CLSID : TCLSID; Register : boolean);
var
CatReg : ICatRegister;
begin
CatReg := StdComponentCategoryMgr as ICatRegister;
if (Register) then
begin
//perform registration here
end
else begin
//note that we never unregister CATID_Explorable because other servers might
//still use it!
//unregister CLSID as supporting that category
OleCheck (CatReg.UnregisterClassImplCategories (CLSID, 1, @ExplorableCategoryInfo));
DeleteRegKey ('CLSID\' + GuidToString (CLSID) + '\' + 'Implemented Categories');
end;
end;
//returns standard component category manager
function StdComponentCategoryMgr : IUnknown;
begin
Result := CreateComObject (CLSID_StdComponentCategoryMgr);
end;
Note that we do an extra DeleteRegKey
call to remove the "Implemented Categories" subkey to completely
remove our coclass from the registry.
| Another way to
perform registration of the category information is to create our own
customized class factory that inherits from TComObjectFactory (or
descendants). TComObjectFactory has an virtual UpdateRegistry method
that we can override to perform our own custom registration and
unregistration. |
Implementing the Explorer Host
With the plug-in in place (oops, don't
forget to register your plug-in), it's now time to look at the Explorer host. As
I mentioned earlier, our Explorer host is an MDI application wherein each MDI
child window hosts an explorable plug-in. To do this, when Explorer loads up,
it finds all registered explorable plug-ins and adds them all to a menu list.
Whenever the user selects a particular plug-in from the menu, we instantiate an
MDI child form (TfrmExplorerHost) and host the selected plug-in into that form.

Figure: "Explorable Plugins"
menu list containing a list of registered plug-ins
To obtain a list of explorable plug-ins,
we simply use COM's component category facilities as described earlier. In
particular, we use ICatInformation to obtain all registered coclasses that
implement the "Explorable Plugins" category. Thus:
procedure
TfrmMain.LoadExplorableClasses;
var
Count, i : integer;
Explorable : IExplorable;
Description : widestring;
MenuItem : TMenuItem;
begin
//get explorable server list
//FExplorableClasses is an array of CLSIDs
Count := GetExplorableClasses (FExplorableClasses);
if (Count > 0) then
begin
//get description for each and populate Explore >> submenu
for i := 1 to Count do
begin
//create Explorable plugin to obtain plug-in
description
Explorable := CreateComObject (FExplorableClasses [i]) as IExplorable;
//get plug-in description
OleCheck (Explorable.GetDescription (Description));
//create new menu item
MenuItem := TMenuItem.Create (Self);
MenuItem.Caption := Description;
//hide FExplorableClasses array index in Tag
MenuItem.Tag := i;
//add to Explore >> submenu
miExplore.Add
(MenuItem);
end;
end;
end;
What's happening here is that we're
loading all explorable coclass CLSIDs into the FExplorableClasses array. Then we
iterate the array and load each plug-in's description into a menu list.
Here's what GetExplorableClasses really
looks like:
type
TExplorableClasses = array [1..50] of TCLSID; //50 is arbitrary!
//returns Explorable server CLSIDs
function GetExplorableClasses (var ExplorableClasses : TExplorableClasses) : integer;
var
CatInfo : ICatInformation;
Enum : IEnumGuid;
Fetched : UINT;
begin
Result := 0;
//get standard category information manager
CatInfo := StdComponentCategoryMgr as ICatInformation;
//get an enumeration of all registered Explorable classes
//ExplorableCategoryInfo is filled with cat info as discussed earlier
OleCheck (CatInfo.EnumClassesOfCategories (1, @ExplorableCategoryInfo, 0, NIL, Enum));
//enumerate Explorable classes into ExplorableClasses array
if (Enum <> NIL) then
begin
OleCheck (Enum.Reset);
//fill ExplorableClasses array
//note that if Fetched >= High (ExplorableClasses), then we may have more!
//for our purposes, this sample code will do
OleCheck (Enum.Next (High (ExplorableClasses), ExplorableClasses [1], Fetched));
Result := Fetched;
end;
end;
As we've learned earlier,
ICatInformation.EnumClassesOfCategories is used to obtain all coclasses that
implement a particular category. Then we use the enumerator result, Enum, to iterate
matching CLSIDs and populate the ExplorableClasses array accordingly.
So far, we're able to populate a menu
list of explorable plug-ins. Now we have to determine what to do when the user
selects a plug-in from the list. Thus:
//called
when user selects a plug-in from the explorable plug-ins menu
procedure TfrmMain.miExplorableItemClick(Sender: TObject);
var
ExplorableClass : TCLSID;
Explorable : IExplorable;
begin
//get selected Explorable class
//Tag contains index into FExplorableClasses array - this is established in
//proc LoadExplorableClasses
ExplorableClass := FExplorableClasses [(Sender as TMenuItem).Tag];
//create Explorable plugin
Explorable := CreateComObject (ExplorableClass) as IExplorable;
//load Explorable server into new explorer host form
TfrmExplorerHost.Load (Explorable);
end;
What we're doing here is instantiating
the correct plug-in coclass based on the selected menu item. Then we load the
new plug-in into the explorer host form.
The explorer host form simply consists of
2 panes: a left tree view (tvwExplorer) and a right list view (lvwProperties).
The tree view displays the explorable plug-in's hierarchy while the list view
displays the properties whenever a node is focused/selected in the tree view. In
other words, for our FileSystemPlugin, the tree view displays your file system
hierarchy and the list view displays file property information (Name, Type,
Size, etc.) for the currently selected file/folder in the tree view:

Figure: Tree view and list view
panes of the Explorer host form
Let's see what we can do in the tree
view. First, we need to be able to expand nodes and display a node's children
(sub-items) correspondingly. Recall that we had to implement the IExplorable.GetSubItems method for this. This is how
Explorer would now make use
of GetSubItems:
procedure
TfrmExplorerHost.tvwExplorerExpanding(Sender: TObject;
Node: TTreeNode; var AllowExpansion: Boolean);
begin
//clear children then repopulate
ClearNodeChildren (Node);
ExpandNode (Node);
end;
procedure TfrmExplorerHost.ExpandNode(Node: TTreeNode);
var
SubItems : ISubItems;
Count, i : integer;
ItemText : widestring;
ChildNode : TTreeNode;
begin
//get subitems
if Succeeded (FExplorable.GetSubItems (NodePath (Node), SubItems)) then
if (SubItems <> NIL) then
begin
//load subitems
SubItems.GetCount (Count);
for i := 0 to Count - 1 do
begin
SubItems.GetItem (i, ItemText);
ChildNode := tvwExplorer.Items.AddChild (Node,
ItemText);
end;
end;
end;
In ExpandNode, we first call GetSubItems
to obtain an ISubItems list. We then iterate ISubItems and for each element we
find, we
add it as a child node underneath the current node.
When we click/select a node in the tree
view, we also want to display the node's properties in the list view pane. Thus:
procedure
TfrmExplorerHost.tvwExplorerChange(Sender: TObject;
Node: TTreeNode);
begin
LoadNodeProperties (Node);
end;
procedure TfrmExplorerHost.LoadNodeProperties(Node: TTreeNode);
var
Properties : olevariant;
Count, i : integer;
ListItem : TListItem;
begin
//clear
lvwProperties.Items.BeginUpdate;
lvwProperties.Items.Clear;
lvwProperties.Items.EndUpdate;
//get properties
//Properties is a 2-dim array
//
// | Property Name 1 | Property Value 1 |
// | Property Name 2 | Property Value 2 |
//
if Succeeded (FExplorable.GetProperties (NodePath (Node), Properties)) then
//load properties
if not VarIsEmpty (Properties) then
begin
//assume row-dimension is 0-based
Count := VarArrayHighBound (Properties, 1) + 1;
for i := 0 to Count - 1 do
begin
//add new property
ListItem := lvwProperties.Items.Add;
//get property name
ListItem.Caption := Properties [i, 0];
//get property value
ListItem.SubItems.Add (Properties [i, 1]);
end;
end;
end;
There's nothing hard here. We simply call
GetProperties to retrieve the properties array for the selected node. Then we
populate the list view based on the array.
Finally, we implement menu actions. On a
right-mouse-click, we first build a list of context sensitive menu actions based
on the current node. This is accomplished by calling IExplorable.GetMenuActions.
Thus:
procedure
TfrmExplorerHost.tvwExplorerMouseDown(Sender: TObject;
Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
var
Node : TTreeNode;
pt : TPoint;
begin
//respond to right mouse click
if (Button = mbRight) then
begin
//select node where mouse was right clicked
Node := tvwExplorer.GetNodeAt (X, Y);
if (Node = NIL) then Exit;
tvwExplorer.Selected := Node;
//initialize popup menu
if (InitializePopupMenu (Node)) then
begin
//show popup menu
pt := ClientToScreen (Point (X, Y));
PopupMenu.Popup (pt.X, pt.Y);
end;
end;
end;
function TfrmExplorerHost.InitializePopupMenu(Node: TTreeNode): boolean;
var
MenuItem : TMenuItem;
Actions : olevariant;
Count, i : integer;
begin
//get menu actions
//Actions is a 2-dim array:
//
// | Action Description 1 | Action ID 1 |
// | Action Description 2 | Action ID 2 |
//
if Succeeded (FExplorable.GetMenuActions (NodePath (Node), Actions)) then
begin
//add items
if not VarIsEmpty (Actions) then
begin
//assume row-dimension is 0-based
Count := VarArrayHighBound (Actions, 1) + 1;
for i := 0 to Count - 1 do
begin
//add menu item
MenuItem := TMenuItem.Create (Self);
MenuItem.Caption := Actions [i, 0];
//hide ActionID in Tag
MenuItem.Tag := Actions [i, 1];
//add to popup menu
PopupMenu.Items.Add (MenuItem);
end;
end;
end;
//a-ok if popup menu has items
Result := (PopupMenu.Items.Count > 0);
end;
In here, we simply call GetMenuActions to
obtain the menu actions array for the selected node. Then we populate the
context menu based on the array. Note how we assign the ActionID into the Tag
property of each menu item. We'll use that to determine which action ID to use
when a user selects an action from the menu. Thus:
procedure
TfrmExplorerHost.miExplorerNodeActionClick(Sender: TObject);
var
ActionID : integer;
begin
//get ActionID from menu item Tag - this was set proc InitializePopupMenu
ActionID := (Sender as TMenuItem).Tag;
//invoke ActionID
OleCheck (FExplorable.DoMenuAction (NodePath (tvwExplorer.Selected),
ActionID));
end;
Here, we obtain the action ID back from
the Tag property. Then we invoke the plug-in's DoMenuAction method passing in
the current selected node and the action ID.
The one last thing I haven't shown you is
the host's (IExplorer) implementation of RenamePath. Recall that RenamePath is
called by the plug-in to notify Explorer after a name change in a node. For our
Explorer host form, we simply refresh the tree view node caption with the new name, as
well as the corresponding properties list for that node (the properties list
needs to be refreshed because it includes the name). Thus:
function TfrmExplorerHost.RenamePath(const
OldPath, NewPath: WideString): HResult;
var
Node : TTreeNode;
begin
//find terminal node that corresponds to OldPath
Node := FullPathToNode (RootNode, OldPath);
if (Node <> NIL) then
begin
//change node text to NewPath terminal node name
Node.Text := ExtractLastNodeName (NewPath);
//reload node properties
LoadNodeProperties (Node);
end;
Result := S_OK;
end;
I won't go into the details of the
FullPathToNode and ExtractLastNodeName functions because they're not very
relevant - feel free to inspect the source code for more details.
Proceed to
conclusion >>
Implementing the File System Plug-in
In CBuilder, we create an in-process (DLL) COM
server project using the wizard accessible from the File | New menu and
by picking ActiveX Library on the ActiveX tab in the dialog. Let's name
this project "FileSystemPlugin" - CBuilder will produce a file named
FileSystemPlugin.dll as our plug-in server.
Then we create our plug-in coclass. To do
this, we select File | New and pick COM Object in the ActiveX tab
in the dialog, name the object "FileSystemExplorable", and save the
new module as FileSystemExplorable.cpp.
| Since we're simply creating a
lightweight COM object and we don't need any information entered into a
type library, I've removed the IFileSystemExplorable interface and the
FileSystemExplorable coclass entries from the type library file. However,
I've taken FileSystemExplorable's CLSID and embedded it directly in
FileSystemExplorable.cpp/FileSystemExplorable.h. Although
I didn't need to remove the information from the type library, I did this
to illustrate that the type library entries are not necessary for
FileSystemExplorable to work. |
By doing this, CBuilder creates a plain
lightweight COM object like this:
class ATL_NO_VTABLE TFileSystemExplorableImpl :
public CComObjectRootEx<CComSingleThreadModel>,
public CComCoClass<TFileSystemExplorableImpl, &CLSID_FileSystemExplorable>
{
...
};
const GUID CLSID_FileSystemExplorable = //'{8B9A0689-7434-11D3-A802-0000B4552A26}';
{0x8B9A0689, 0x7434, 0x11D3,{ 0xA8, 0x02, 0x00, 0x00, 0xB4, 0x55, 0x2A, 0x26} };
Note that because
TFileSystemExplorableImpl is not dependent on the type library, I've also
changed the UpdateRegistry method accordingly:
class ATL_NO_VTABLE TFileSystemExplorableImpl :
public CComObjectRootEx<CComSingleThreadModel>,
public CComCoClass<TFileSystemExplorableImpl, &CLSID_FileSystemExplorable>
{
public:
static HRESULT WINAPI UpdateRegistry(BOOL bRegister)
{
//changed from TTypedComServerRegistrarT because we don't need type library
//information for our explorable plugin
TComServerRegistrarT<TFileSystemExplorableImpl>
regObj(GetObjectCLSID(), GetProgID(), GetDescription());
return regObj.UpdateRegistry(bRegister);
}
...
};
Since we're creating a plug-in, we want
FileSystemExplorable to implement the IExplorable plug-in interface. To do that,
we manually add IExplorable to TFileSystemExplorableImpl as follows:
class ATL_NO_VTABLE TFileSystemExplorableImpl :
public CComObjectRootEx<CComSingleThreadModel>,
public CComCoClass<TFileSystemExplorableImpl, &CLSID_FileSystemExplorable>,
public IExplorable
{
protected:
BEGIN_COM_MAP(TFileSystemExplorableImpl)
COM_INTERFACE_ENTRY (IExplorable)
END_COM_MAP()
//IExplorable methods
STDMETHOD (SetExplorer) (Explorer_tlb::IExplorerPtr Explorer);
STDMETHOD (GetDescription) (BSTR* Description);
STDMETHOD (GetSubItems) (BSTR Path, Explorer_tlb::ISubItemsPtr* SubItems);
STDMETHOD (GetMenuActions) (BSTR Path, TVariant* Actions);
STDMETHOD (DoMenuAction) (BSTR Path, long ActionId);
STDMETHOD (GetProperties) (BSTR Path, TVariant* Properties);
protected:
IExplorerPtr mExplorer;
};
Then we implement the methods. Let's
start with the trivial SetExplorer and GetDescription:
STDMETHODIMP TFileSystemExplorableImpl::SetExplorer (
Explorer_tlb::IExplorerPtr Explorer)
{
mExplorer = Explorer; //mExplorer is a member field of
TFileSystemExplorableImpl
return S_OK;
}
STDMETHODIMP TFileSystemExplorableImpl::GetDescription (BSTR* Description)
{
*Description = WideString ("File System (C++ Builder version)").Detach ();
return S_OK;
}
Nothing complicated here. Let's do
GetSubItems instead.
Remember, GetSubItems is called by
Explorer to obtain an ISubItems interface to enumerate sub-items of any node in
the hierarchy. For us, that means that we have to scan the folder (specified by
the node/path) using the FindFirst and FindNext functions. Let's encapsulate
this process in a TSubItemsImpl class:
#include
<vector.h>
class ATL_NO_VTABLE TSubItemsImpl :
public CComObjectRoot
{
protected:
vector <String> mSubItems;
void LoadDrives ();
void LoadFiles (String &Path);
public:
void Initialize (String &Path);
};
//load system drives into mSubItems list
void TSubItemsImpl::LoadDrives ()
{
//implementation in pseudocode
Find all drives;
For each drive found
Add drive name into mSubItems list;
}
//load folder subfiles (and subfolders) into mSubItems list
void TSubItemsImpl::LoadFiles (String &Path)
{
//implementation in pseudocode
Find all files (and folders) under Path (using FindFirst/FindNext)
For each file (and folder) found
Add file name into mSubItems list;
}
//loads file system subitems given a path
void TSubItemsImpl::Initialize (String &Path)
{
//reset list
mSubItems.clear ();
//if path is root, load system drives else load path as folder
if (Path == "")
LoadDrives ();
else
LoadFiles (Path);
}
Constructing a TSubItemsImpl and calling
Initialize takes care of
loading the sub-files for a given path. The important method here is Initialize. In
Initialize, if the path specified is blank, then we load the
sub-items list with all the drives on your system, otherwise we simply do a
FindFirst-FindNext file scan of the given path.
Since FileSystemExplorable must hand out
ISubItems to Explorer, we simply implement ISubItems into TSubItemsImpl:
class ATL_NO_VTABLE TSubItemsImpl :
public CComObjectRoot,
public ISubItems
{
protected:
BEGIN_COM_MAP (TSubItemsImpl)
COM_INTERFACE_ENTRY (ISubItems)
END_COM_MAP ()
//ISubItems methods
STDMETHOD (GetCount) (long* Count);
STDMETHOD (GetItem) (long Index, BSTR* Item);
...
};
//return sub-item count
STDMETHODIMP TSubItemsImpl::GetCount (long* Count)
{
*Count = mSubItems.size ();
return S_OK;
}
//return sub-item text by index
STDMETHODIMP TSubItemsImpl::GetItem (long Index, BSTR* Item)
{
*Item = WideString (mSubItems [Index]).Detach ();
return S_OK;
}
Finally, we can go back to
TFileSystemExplorableImpl and implement GetSubItems:
STDMETHODIMP TFileSystemExplorableImpl::GetSubItems (BSTR Path,
Explorer_tlb::ISubItemsPtr* SubItems)
{
//initialize new subitems collection based on Path
CComObject <TSubItemsImpl> *Items;
CComObject <TSubItemsImpl>::CreateInstance (&Items);
Items->AddRef ();
Items->Initialize (String (Path));
*SubItems = Items;
return S_OK;
}
The next step is to implement
GetProperties. If you recall, GetProperties is called by Explorer to obtain
name-value pairs of information for any given node. More specifically,
GetProperties returns an array of name-value pairs. In COM, we can simply
implement this using a variant array. Thus:
STDMETHODIMP TFileSystemExplorableImpl::GetProperties (BSTR Path, TVariant* Properties)
{
//Properties is a 2-dim array:
//
// | Property Name 1 | Property Value 1 |
// | Property Name 2 | Property Value 2 |
//
Variant vProperties;
vProperties = VarArrayCreate (OPENARRAY (int, (
0, 4, //row bounds
0, 1 //column bounds
)),
varOleStr //string elements
);
//Type: File or Folder?
String FileOrFolder;
vProperties.PutElement ("Type", 0, 0);
if (IsFolder (FullPath))
vProperties.PutElement ("Folder", 0, 1);
else
vProperties.PutElement ("File", 0, 1);
//Name
vProperties.PutElement ("Name", 1, 0);
vProperties.PutElement (NameOfFile (Path), 1, 1);
//Size
vProperties.PutElement ("Size", 2, 0);
vProperties.PutElement (IntToStr (SizeOfFile (Path)), 2, 1);
//Date/Time
vProperties.PutElement ("Date/Time", 3, 0);
vProperties.PutElement (DateTimeToStr (DateTimeOfFile (Path)), 3, 1);
//Attributes
vProperties.PutElement ("Attributes", 4, 0);
vProperties.PutElement (AttributesOfFile (Path), 4, 1);
//transfer properties storage to output
*Properties = vProperties;
return S_OK;
}
What we're doing here is simply returning
a 2-dim variant array (of 5 rows indexed from 0 to 4) consisting of the
following file properties: Type, Name, Size, Date/Time, and Attributes. I'm not
going to bore you with details of how to physically obtain each property value
because that's not relevant - feel free to check the source code for
details.
The last 2 methods we need to implement
are GetMenuActions and DoMenuAction. If you recall, these methods are for the
context sensitive custom operations that can be done on any node in your
hierarchy. GetMenuActions returns an array of action-ID pairs to Explorer and
DoMenuAction executes the user-selected action on a given node.
Also, if you recall, we only wanted to
implement a single menu action - Rename File. And we specifically want to rename
files with only the Archive bit set. Thus:
const int ACTION_RENAME = 1;
STDMETHODIMP TFileSystemExplorableImpl::GetMenuActions (BSTR Path, TVariant* Actions)
{
//this ActionCount business demonstrates how menu item actions can be dynamic
//depending on context
int ActionCount = 0;
//Actions is a 2-dim array:
//
// | Action Name 1 | Action ID 1 |
// | Action Name 2 | Action ID 2 |
//
Variant vActions;
vActions = VarArrayCreate (OPENARRAY (int, (
0, 0, //row bounds
0, 1 //column bounds
)),
varVariant //variant elements
);
if (FileOnlyHasArchiveAttributeSet
(Path))
{
//add Rename action
vActions.PutElement ("Rename", ActionCount, 0);
vActions.PutElement (ACTION_RENAME, ActionCount, 1);
//one action in!
ActionCount++;
}
//if Actions in context are available, return it to client
VariantClear (*Actions);
if (ActionCount > 0) *Actions = vActions;
return S_OK;
}
As you can see, we're simply creating a
2-dim variant array to store the action-ID pairs. Since we're only interested in
a rename action, we only have at most 1 row element in the array (hence the row
bounds 0 to 0).
With this, whenever Explorer gets its
action list, it will populate its context menu with it. After that, if the
user selects the rename action, Explorer will then call DoMenuAction in our
plug-in, passing in the ACTION_RENAME action ID constant. Thus, we can simply implement DoMenuAction to process the rename action
as follows:
STDMETHODIMP TFileSystemExplorableImpl::DoMenuAction (BSTR Path, long ActionId)
{
//case out ActionID
switch (ActionId)
{
case ACTION_RENAME :
{
String NewName;
//rename and notify host if renamed
if (TfrmRename::Rename (String (Path), NewName))
//notify explorer host after rename action
mExplorer->RenamePath (Path, WideString (NewName).Detach ());
break;
}
}
return S_OK;
}
In here, we check the
ActionID parameter and perform the corresponding action on the given path. For
ACTION_RENAME, we simply run a form (TfrmRename) that takes care of presenting
the user with a simple dialog box that allows the user to input a new name for
the given file. On a successful rename, we then proceed to call
mExplorer.RenamePath to tell our host Explorer that a certain node has changed
its name. This is done so that Explorer can then accordingly update its UI to
reflect the name change.
| This is an example
of how a plug-in may need to communicate back to its host. Normally, this
process is done so that the plug-in and the host can both synchronize
themselves to reflect state change in the data manipulated by
both the plug-in and the host. |
So much for IExplorable. The last thing we need to do is to somehow incorporate the code that registers
and unregisters
component categories into our plug-in server. The easiest way to do this is that
whenever our plug-in gets registered, we also register the category information
along with it, and whenever it gets unregistered, we also unregister the
category information appropriately. For DLL servers, the registration entry points
of interested are the
exported functions: DLLRegisterServer (when registering) and DLLUnregisterServer
(when unregistering).
Let's first take care of registration:
In
FileSystemPlugin.cpp
...
//overidden to include categories registration
STDAPI __export DllRegisterServer(void)
{
HRESULT Result = _Module.RegisterServer(TRUE);
//manually register category information
//note: ATL 3 contains category map macros that will eliminate all this
//hard-work. Unfortunately, BCB 4 is not on par with ATL 3 yet. :(
RegisterAsExplorableClass (CLSID_FileSystemExplorable, true);
//true means register
return Result;
}
...
In here, we're simply
"overriding" DLLRegisterServer to also include our category
information registration routine. RegisterAsExplorableClass is defined as
follows:
//un/registers a class as explorable
void RegisterAsExplorableClass (REFCLSID clsid, bool Register)
{
HRESULT hr = S_OK;
ICatRegister *CatReg = NULL;
hr = GetStdComponentCategoryMgr ((void**)&CatReg, IID_ICatRegister);
if (SUCCEEDED (hr))
{
//initialize our category
TCATEGORYINFO ExplorableCategoryInfo;
InitializeExplorableCategoryInfo (ExplorableCategoryInfo);
if (Register)
{
//first register CATID_Explorable category
CatReg->RegisterCategories (1, &ExplorableCategoryInfo);
//then register CLSID as supporting that category
CatReg->RegisterClassImplCategories (clsid, 1, &ExplorableCategoryInfo);
}
else
{
//perform unregistration here
}
CatReg->Release ();
}
}
//returns standard component category manager
HRESULT GetStdComponentCategoryMgr (void **ppv, REFIID iid = IID_IUnknown)
{
//create standard COM component category manager
return CoCreateInstance (CLSID_StdComponentCategoryMgr, NULL, CLSCTX_ALL,
iid, ppv);
}
void InitializeExplorableCategoryInfo (TCATEGORYINFO &ExplorableCategoryInfo)
{
//fill category info record
ExplorableCategoryInfo.catid = CATID_Explorable;
ExplorableCategoryInfo.lcid = LOCALE_SYSTEM_DEFAULT;
StringToWideChar (
ExplorableCategoryDescription,
ExplorableCategoryInfo.szDescription,
sizeof (ExplorableCategoryInfo.szDescription) / sizeof (ExplorableCategoryInfo.szDescription [0])
);
}
What we're doing here is simply
registering the "Explorable Plugins" category and then registering the
FileSystemExplorable coclass as implementing that category. We've discussed
these details before so there shouldn't be anything new here.
For the unregistration part, we simply
hook DLLUnRegisterServer and reverse what we've done in DLLRegisterServer. Thus:
In
FileSystemPlugin.cpp
...
//overidden to include categories unregistration
STDAPI __export DllUnregisterServer(void)
{
//manually unregister category information
//note: ATL 3 contains category map macros that will eliminate all this
//hard-work. Unfortunately, BCB 4 is not on par with ATL 3 yet. :(
RegisterAsExplorableClass (CLSID_FileSystemExplorable, false);
//false means unregister
return _Module.UnregisterServer();
}
...
Accordingly, the unregister part in
RegisterAsExplorableClass is defined as:
//un/registers a class as explorable
void RegisterAsExplorableClass (REFCLSID clsid, bool Register)
{
HRESULT hr = S_OK;
ICatRegister *CatReg = NULL;
hr = GetStdComponentCategoryMgr ((void**)&CatReg, IID_ICatRegister);
if (SUCCEEDED (hr))
{
//initialize our category
TCATEGORYINFO ExplorableCategoryInfo;
InitializeExplorableCategoryInfo (ExplorableCategoryInfo);
if (Register)
{
//perform registration here
}
else
{
//note that we never unregister CATID_Explorable because other servers might
//still use it!
//unregister CLSID as supporting that category
CatReg->UnRegisterClassImplCategories (clsid, 1, &ExplorableCategoryInfo);
DeleteRegKey ("CLSID\\" + GUIDToString (clsid) + "\\" + "Implemented Categories");
}
CatReg->Release ();
}
}
Note that we do an extra DeleteRegKey
call to remove the "Implemented Categories" subkey to completely
remove our coclass from the registry.
| In ATL 3, another way to
perform registration of the category information is to use the component
category map macros (BEGIN_CATEGORY_MAP, etc.). However CBuilder 4 doesn't
support ATL 3 yet so we'll have to make do with the hard way.
On a different note, the COM
component category constants and interfaces are supposed to be defined in
comcat.h. However, by CBuilder incorporating Delphi's ActiveX.hpp module,
the CBuilder compiler generates a conflict between same named interfaces
in both comcat.h and ActiveX.hpp. What I ended up doing was totally
getting rid of comcat.h and just using ActiveX.hpp. However, there were a
few snags in doing this because ActiveX.hpp really came from Delphi so
we're forced to use some Delphisms to compensate. You'll see what I mean
when you take a closer look at the source code.
|
Implementing the Explorer Host
With the plug-in in place (oops, don't
forget to register your plug-in), it's now time to look at the Explorer host. As
I mentioned earlier, our Explorer host is an MDI application wherein each MDI
child window hosts an explorable plug-in. To do this, when Explorer loads up,
it finds all registered explorable plug-ins and adds them all to a menu list.
Whenever the user selects a particular plug-in from the menu, we instantiate an
MDI child form (TfrmExplorerHost) and host the selected plug-in into that form.

Figure: "Explorable Plugins"
menu list containing a list of registered plug-ins
To obtain a list of explorable plug-ins,
we simply use COM's component category facilities as described earlier. In
particular, we use ICatInformation to obtain all registered coclasses that
implement the "Explorable Plugins" category. Thus:
void TfrmMain::LoadExplorableClasses ()
{
//get explorable server list
int Count = GetExplorableClasses (mExplorableClasses);
if (Count > 0)
{
WideString Description;
//get description for each and populate Explore >> submenu
for (int i = 0; i < Count; i++)
{
//create Explorable plugin
IExplorablePtr Explorable;
Explorable.CreateInstance (mExplorableClasses [i], NULL, CLSCTX_ALL);
//get plugin description
Explorable->GetDescription (&Description);
//create new menu item
TMenuItem *MenuItem = new TMenuItem (this);
MenuItem->Caption = Description;
//hide mExplorableClasses array index in Tag
MenuItem->Tag = i;
//add to Explore >> submenu
miExplore->Add (MenuItem);
}
}
}
What's happening here is that we're
loading all explorable coclass CLSIDs into the mExplorableClasses array. Then we
iterate the array and load each plug-in's description into a menu list.
Here's what GetExplorableClasses really
looks like:
//explorable classes storage array
typedef CLSID TExplorableClasses [50]; //50 is arbitrary!
//returns Explorable server CLSIDs
int GetExplorableClasses (TExplorableClasses &ExplorableClasses)
{
int Result = 0;
//get standard category information manager
HRESULT hr = S_OK;
ICatInformation *CatInfo = NULL;
hr = GetStdComponentCategoryMgr ((void**)&CatInfo, IID_ICatInformation);
if (SUCCEEDED (hr))
{
//get an enumeration of all registered Explorable classes
//this _di_IEnumGUID thingy is part of Delphi/ActiveX.hpp workaround
_di_IEnumGUID Enum = NULL;
//initialize our category
TCATEGORYINFO ExplorableCategoryInfo;
InitializeExplorableCategoryInfo (ExplorableCategoryInfo);
hr = CatInfo->EnumClassesOfCategories (1, &ExplorableCategoryInfo, 0, NULL, Enum);
if (SUCCEEDED (hr))
{
//enumerate Explorable classes into ExplorableClasses array
hr = Enum->Reset ();
if (SUCCEEDED (hr))
{
//fill ExplorableClasses array
//note that if Fetched >= ExplorableClassesMax, then we may have more!
//for our purposes, this sample code will do
int ExplorableClassesMax = sizeof (ExplorableClasses) / sizeof (ExplorableClasses [0]);
UINT Fetched;
hr = Enum->Next (ExplorableClassesMax, ExplorableClasses [0], Fetched);
//save count
if (SUCCEEDED (hr)) Result = Fetched;
}
}
CatInfo->Release ();
}
//return registered plugin count
return Result;
}
As we've learned earlier,
ICatInformation.EnumClassesOfCategories is used to obtain all coclasses that
implement a particular category. Then we use the enumerator result, Enum, to iterate
matching CLSIDs and populate the ExplorableClasses array accordingly.
So far, we're able to populate a menu
list of explorable plug-ins. Now we have to determine what to do when the user
selects a plug-in from the list. Thus:
//called
when user selects a plug-in from the explorable plug-ins menu
void __fastcall TfrmMain::miExplorableItemClick(TObject *Sender)
{
//get selected Explorable class
//Tag contains index into FExplorableClasses array - this is established in
//proc LoadExplorableClasses
CLSID ExplorableClass = mExplorableClasses [(dynamic_cast <TMenuItem*>(Sender))->Tag];
//create Explorable plugin
IExplorablePtr Explorable;
Explorable.CreateInstance (ExplorableClass, NULL, CLSCTX_ALL);
//load Explorable server into new explorer host form
TfrmExplorerHost::Load (Explorable);
}
What we're doing here is instantiating
the correct plug-in coclass based on the selected menu item. Then we load the
new plug-in into the explorer host form.
The explorer host form simply consists of
2 panes: a left tree view (tvwExplorer) and a right list view (lvwProperties).
The tree view displays the explorable plug-in's hierarchy while the list view
displays the properties whenever a node is focused/selected in the tree view. In
other words, for our FileSystemPlugin, the tree view displays your file system
hierarchy and the list view displays file property information (Name, Type,
Size, etc.) for the currently selected file/folder in the tree view:

Figure: Tree view and list view
panes of the Explorer host form
Let's see what we can do in the tree
view. First, we need to be able to expand nodes and display a node's children
(sub-items) correspondingly. Recall that we had to implement the IExplorable.GetSubItems method for this. This is how
Explorer would now make use
of GetSubItems:
void __fastcall TfrmExplorerHost::tvwExplorerExpanding(TObject *Sender,
TTreeNode *Node, bool &AllowExpansion)
{
//clear children then repopulate
ClearNodeChildren (Node);
ExpandNode (Node);
}
void TfrmExplorerHost::ExpandNode (TTreeNode *Node)
{
//get subitems
ISubItemsPtr SubItems;
HRESULT hr = mExplorable->GetSubItems (
WideString (NodePath (Node)), (ISubItemsPtr*)&SubItems);
if (SUCCEEDED (hr) && SubItems.IsBound ())
{
//load subitems
long Count;
SubItems->GetCount (&Count);
for (int i = 0; i <= Count - 1; i++)
{
WideString ItemText;
SubItems->GetItem (i, &ItemText);
TTreeNode *ChildNode = tvwExplorer->Items->AddChild (Node,
ItemText);
}
}
}
In ExpandNode, we first call GetSubItems
to obtain an ISubItems list. We then iterate ISubItems and for each element we
find, we
add it as a child node underneath the current node.
When we click/select a node in the tree
view, we also want to display the node's properties in the list view pane. Thus:
void __fastcall TfrmExplorerHost::tvwExplorerChange(TObject *Sender,
TTreeNode *Node)
{
LoadNodeProperties (Node);
}
void TfrmExplorerHost::LoadNodeProperties(TTreeNode *Node)
{
lvwProperties->Items->BeginUpdate ();
lvwProperties->Items->Clear ();
lvwProperties->Items->EndUpdate ();
//get properties
//Properties is a 2-dim array
//
// | Property Name 1 | Property Value 1 |
// | Property Name 2 | Property Value 2 |
//
TVariant vProperties;
HRESULT hr = mExplorable->GetProperties (WideString (NodePath (Node)),
&vProperties);
if (SUCCEEDED (hr))
{
//load properties
Variant Properties = vProperties;
if (!VarIsEmpty (Properties))
{
//assume row-dimension is 0-based
int Count = VarArrayHighBound (Properties, 1) + 1;
for (int i = 0; i <= Count - 1; i++)
{
//skip uninitialized properties
if (VarIsEmpty (Properties.GetElement (i, 0))) continue;
//add new property
TListItem *ListItem = lvwProperties->Items->Add ();
//get property name
ListItem->Caption = Properties.GetElement (i, 0);
//get property value
ListItem->SubItems->Add (Properties.GetElement (i, 1));
}
}
}
}
There's nothing hard here. We simply call
GetProperties to retrieve the properties array for the selected node. Then we
populate the list view based on the array.
Finally, we implement menu actions. On a
right-mouse-click, we first build a list of context sensitive menu actions based
on the current node. This is accomplished by calling IExplorable.GetMenuActions.
Thus:
void __fastcall TfrmExplorerHost::tvwExplorerMouseDown(TObject *Sender,
TMouseButton Button, TShiftState Shift, int X, int Y)
{
//respond to right mouse click
if (Button == mbRight)
{
//select node where mouse was right clicked
TTreeNode *Node = tvwExplorer->GetNodeAt (X, Y);
if (Node == NULL) return;
tvwExplorer->Selected = Node;
//initialize popup menu
if (InitializePopupMenu (Node))
{
//show popup menu
TPoint pt = ClientToScreen (Point (X, Y));
PopupMenu->Popup (pt.x, pt.y);
}
}
}
bool TfrmExplorerHost::InitializePopupMenu (TTreeNode *Node)
{
//get menu actions
//Actions is a 2-dim array:
//
// | Action Description 1 | Action ID 1 |
// | Action Description 2 | Action ID 2 |
//
TVariant vActions;
HRESULT hr = mExplorable->GetMenuActions (WideString (NodePath (Node)), &vActions);
//just don't do anything if not successful
if (SUCCEEDED (hr))
{
//add items
Variant Actions = vActions;
if (!VarIsEmpty (Actions))
{
//assume row-dimension is 0-based
int Count = VarArrayHighBound (Actions, 1) + 1;
for (int i = 0; i <= Count - 1; i++)
{
//skip uninitialized actions
if (VarIsEmpty (Actions.GetElement (i, 0))) continue;
//add menu item
TMenuItem *MenuItem = new TMenuItem (this);
MenuItem->Caption = Actions.GetElement (i, 0);
//hide ActionID in Tag
MenuItem->Tag = Actions.GetElement (i, 1);
//add to popup menu
PopupMenu->Items->Add (MenuItem);
}
}
}
//a-ok if popup menu has items
return (PopupMenu->Items->Count > 0);
}
In here, we simply call GetMenuActions to
obtain the menu actions array for the selected node. Then we populate the
context menu based on the array. Note how we assign the ActionID into the Tag
property of each menu item. We'll use that to determine which action ID to use
when a user selects an action from the menu. Thus:
void __fastcall
TfrmExplorerHost::miExplorerNodeActionClick(
TObject *Sender)
{
//get ActionID from menu item Tag - this was set proc InitializePopupMenu
int ActionID = dynamic_cast <TMenuItem*>(Sender)->Tag;
//invoke ActionID
mExplorable->DoMenuAction (
WideString (NodePath (tvwExplorer->Selected)), ActionID);
}
Here, we obtain the action ID back from
the Tag property. Then we invoke the plug-in's DoMenuAction method passing in
the current selected node and the action ID.
The one last thing I haven't shown you is
the host's (IExplorer) implementation of RenamePath. Recall that RenamePath is
called by the plug-in to notify Explorer after a name change in a node. For our
Explorer host form, we simply refresh the tree view node caption with the new name, as
well as the corresponding properties list for that node (the properties list
needs to be refreshed because it includes the name). Thus:
STDMETHODIMP
TfrmExplorerHost::RenamePath (BSTR OldPath,
BSTR NewPath)
{
//find terminal node that corresponds to OldPath
TTreeNode *Node = FullPathToNode (RootNode (), String (OldPath));
if (Node != NULL)
{
//change node text to NewPath terminal node name
Node->Text = ExtractLastNodeName (String (NewPath));
//reload node properties
LoadNodeProperties (Node);
}
return S_OK;
}
I won't go into the details of the
FullPathToNode and ExtractLastNodeName functions because they're not very
relevant - feel free to inspect the source code for more details.
| Note that in the
source code, I actually implemented IExplorer in a separate class,
TExplorerHost, contained inside of TfrmExplorerHost. This is because of
multiple inheritance limitations in the VCL.
If you inspect the source, you'll
notice that TExplorerHost is an ATL-based class. Creating ATL classes in a
client application may not be as simple as you think. You'll need to
create a dummy TComModule class and assign it to the ATL global _Module
variable, and a dummy object map (BEGIN_OBJECT_MAP, END_OBJECT_MAP).
You'll also need to include ATL modules such as atlmod.h and atlimpl.h.
You can verify all this in CBuilderClient.cpp. In addition, I also had to
include at least the "USING_ATL" conditional define (under
Project | Options | Directories/Conditionals) to enable usage of the ATL
files. |
In this tutorial, I've shown you a very
simple implementation of a plug-in framework. We've learned some important
concepts like component categories, how to design plug-in interfaces, and how to
implement a host and a plug-in. Plug-in frameworks are very useful for
developing highly modularized and customizable applications that need to evolve
through time. I hope you've learned enough of the basic concepts to start
thinking about how you can apply this into your
own domains.
A Word of Caution
All source code presented here is not
necessarily production quality code. In particular, tasks like error handling,
multithreading, optimizations, etc. were not taken into consideration because
they are not relevant to the tutorial topic. Therefore, I do not make any
guarantees that the code will work for you, and you cannot hold me liable for
any damages resulting from the use (or misuse) of this material. In other words,
don't blame me if you get ****ed!
|