|
Creare una Shell Extension non è sicuramente la cosa più facile da fare
soprattutto per le conoscenze tecniche che occorre acquisire. Per chi volesse
risparmiarsi un bel pò di tempo, la soluzione è sicuramente la libreria
EasyNSE, VCL
Open Source realizzata in Delphi e veramente di ottima fattura. Tuttavia è
interessante analizzare come creare da zero questo tipo di applicativi. In questo
articolo vado a riportare un articolo di vecchia data (Gennaio 1998) pubblicato
sul sito www.delphibydesign.com che
oramai non esiste più. Nonostante si faccia riferimento a Delphi 3 ed ai sistemi
di quel tempo (ossia ovviamente Windows 95 e Windows NT 4.0), il contenuto è
tuttora valido (sorgenti di esempio compilati con successo con Delphi 7 e
risultato correttamente eseguito su Windows XP). Si tratta di un Context Menù
Handler relativo ai file .dfm (Delphi Form): in pratica viene aggiunta una voce
al menù contestuale (pulsante destro del mouse) in corrispondenza dei file .dfm;
al click su tale voce viene aperto un editor che visualizza il contenuto del dfm.
Nel seguito andrò a riportare l'articolo nella sua versione originale.
Shell Extensions in Delphi 3
by Ray Konopka
December/January 1998, Vol. 8, No. 5
The Explorer is the central user interface metaphor in Windows, and your
Delphi applications can embed special hooks right into Explorer.
Welcome to the third part of my series on COM support in Delphi 3. In part one,
I introduced object interfaces and explained how they are defined using the
interface keyword. In part two, I described how interfaces affect the creation
of automation servers and controllers in Delphi 3. In this issue, I will
describe how to extend the Windows Explorer shell by creating custom COM servers
that implement specific, you guessed it, interfaces.
What Are Shell Extensions?
Shell extensions, as the term implies, provide a way to extend the abilities of
the Windows 95 or NT 4.0 Explorer. There are several different types of shell
extensions, which are categorized into two groups. While Table 1 summarizes the
extensions that are associated with a particular file type, Table 2 lists the
ones that are associated with file operations.

A shell extension is implemented as a COM object that resides inside an
in-process COM server. Recall from part one that an in-process server is just a
DLL, and that Delphi 3 calls them ActiveX Libraries. The Windows Explorer
accesses the shell extension through interfaces. Consider the following: In the
example that I presented in the last issue, the automation controller called the
methods defined in the IViewer interface supported by the automation
server. For shell extensions, the Windows Explorer itself functions as the
controller and calls the methods in the COM object by using an interface
reference.
Since Windows itself is calling the methods of a shell extension, the interfaces
that Windows uses for accessing shell extensions are predefined. So, instead of
defining new methods and properties of an interface like we do when creating an
automation server, creating a shell extension involves implementing the
appropriate methods defined by the shell extension interfaces. Unfortunately,
actually implementing the methods can be a challenge. We’ll cover the details of
these interfaces shortly.
Creating a Shell Extension
Unlike automation objects, there is no expert defined in Delphi that makes it
easy to create a shell extension COM object. In fact, we have to code everything
by hand. To demonstrate the process, we’ll construct a new context menu handler
that displays a “View as Text” menu item when the user clicks on a Delphi form
file using mouse button 2. If the user selects this menu item, the form file
viewer application that I presented in part two is used to display the text
representation of the selected form file.
To create a shell extension, you must first create the COM server that will hold
the extension, or handler. Select the File|New menu item and then switch to the
ActiveX page. Select the ActiveX Library icon and then click the OK button to
instruct Delphi to create a new project file for the library. Next, create a new
unit to contain the implementation of the shell extension. Select the File|New
menu item and then double-click on the Unit icon.
Once the new unit is created, we need to declare the COM object class—that is,
we must declare a new class that supports the particular interfaces of the shell
extension. For a context menu, the class must support the IShellExtInit
and IContextMenu interfaces. Of course, each of the methods in these
interfaces must be defined in the implementation section of the unit.
Listing 1 is the source code for the FormView ActiveX library, and Listing 2 is
the complete source code for the FormViewImpl unit, which implements the
TFormViewContextMenu class. As you can see, this class descends from
TComObject, which is defined in the ComObj unit, and lists IShellExtInit
and IContextMenu as its supported interfaces. Recall that all interfaces
are derived from IUnknown, so this class also supports the IUnknown
interface. In addition, all interfaces listed in TComObject are supported.
However, for our shell extension, all we need to be concerned with are the
methods in IShellExtInit and IContextMenu.
The IShellExtInit Interface
To initialize a context menu shell extension, Windows uses the IShellExtInit
interface. In addition to the standard IUnknown methods, IShellExtInit
declares an Initialize method. However, the TFormViewContextMenu
class does not define an Initialize method. Instead, it defines the
ShellInit method and uses a "method resolution clause" to map the
IShellExtInit.Initialize method to ShellInit. The reason for doing
this is because the TComObject class defines a virtual Initialize
method that is not compatible with IShellExtInit. (Method resolution
clauses were covered in part one of this series.)
IShellExtInit.Initialize (and ShellInit) receive three parameters.
The first identifies the folder containing the file objects being manipulated.
The second parameter is a reference to an IDataObject interface, which is
used to obtain the name of the selected files. The third parameter is the ProgID
for the type of file that is selected.
Although the selected file names are stored in the same way that Windows stores
file names during a drag operation, extracting the name of the selected file is
not as simple as calling DragQueryFile. First, the information referenced
by the DataObject must be converted into a storage medium that can be
referenced within the shell extension. This is accomplished by setting up a
TFormatEtc record and then calling the GetData method of the
DataObject. Now the DragQueryFile can be called to obtain the name of
the selected file, which is then stored in the private FFileName field.
Notice in Listing 2 that we obtain the file name only if one file is selected.
The IContextMenu Interface
After the context menu handler is initialized via the IShellExtInit
interface, Windows uses the IContextMenu interface to call the other
methods of our context menu handler. Specifically, it will call
QueryContextMenu, GetCommandString, and InvokeCommand. Let’s
take a closer look at each one.
The QueryContextMenu method is called just before Windows displays a file
object’s context menu. Implementing this method allows your shell extension to
add menu items to the context menu. This is accomplished by calling the
InsertMenu function and passing it the handle to the context menu, which is
the first parameter to QueryContextMenu.
Although having access to the menu’s handle would suggest that you have complete
access to the menu, you don’t. In fact, there are several restrictions in what
you can do with the context menu. First, you can only add items (no moving or
deleting items), and you are only allowed to add string items. In addition, you
must insert the new items starting at the position indicated by the Index
parameter. And finally, you must use the CmdFirst parameter as the
starting menu identifier for any menu items added.
The Flags parameter passed to QueryContextMenu must be checked to
make sure it is safe to add new menu items. The Flags parameter must
contain at least one of the following values before a new menu item can be added:
CMF_NORMAL, CMF_VERBSONLY, and CMF_EXPLORE. The
CMF_NORMAL flag indicates that the selected file is located on the Windows
Desktop. The CMF_VERBSONLY flag indicates that the selected file object
is actually a shortcut to another file. The CMF_EXPLORE flag indicates
that the file is being selected within a separate instance of Windows Explorer.
To demonstrate the effect of each flag, the QueryContextMenu method of
the form viewer context menu alters the menu string depending on which flag is
specified. Each situation is illustrated in Figures 1 through 3.

Figure 1: Selecting a Form File within the Explorer
Figure 2: Selecting a Form File on the Desktop

Figure 3: selecting a shortcut to a Form File
Take a closer look at the Explorer status bar in Figure 1. Notice that the
status bar displays a help string associated with the selected item in the
context menu. The Explorer requests this string from our context menu handler by
calling the GetCommandString method. The Cmd parameter to this
method indicates which menu item is selected, and the Flags parameter is
used to determine when to return a help string. In particular, the help string
is returned in the Name parameter only when Flags is equal to
GCS_HELPTEXT.
When the user selects one of the menu items added by a context menu handler,
Windows calls the InvokeCommand method. This method receives a single
record parameter specifying the command to invoke and other useful information
such as a window handle to use when displaying message boxes.
The InvokeCommand method for our form viewer context menu uses the
CreateProcess function to start the FormFileViewer application that I
presented in the last issue. The location of the viewer is found in the Registry,
which the viewer updates whenever it is executed. Therefore, the shell extension
will always be able to find the viewer. The file name of the select form file is
passed to the viewer on the command line. The viewer processes the command line
and displays the contents of the form file as shown in Figure 4.

Figure 4: Form Viewer started by a Shell Extension
Now that all of the necessary methods have been implemented, there is one
more item that must be added to the FormViewImpl unit.
Creating a COM Object Factory
In order for our COM object to be created whenever the FormView DLL is loaded,
we need to create an instance of a class factory that specifically creates an
instance of our shell extension object. Recall from part one that I prefer the
term object factory instead of class factory because a class factory creates
objects and not classes.
In the initialization section of the unit implementing the shell extension COM
object, you need to create an instance of the object factory. Fortunately, this
is quite easy, thanks to the predefined classes in Delphi 3. In particular, we
use the TComObjectFactory class defined in the ComObj unit. Notice that the
class name and ClassID of the shell extension are passed to the constructor of
the factory.
Because shell extensions are implemented as COM objects, and because Windows
itself accesses them, we must generate a ClassID for any new shell extensions.
But how do we do that? In the examples presented in part two of this series,
Delphi automatically generated ClassIDs whenever one was needed. Fortunately,
Delphi 3 provides the means to manually generate a ClassID within the code
editor. Simply press the Ctrl+Shift+G key combination and Delphi automatically
inserts a new GUID into the code editor. This is how the TGUID constant declared
at the beginning of the FormViewImpl unit was created.
Registering a Shell Extension
Now that the FormViewImpl unit is completed, we can compile the FormView project
and generate the COM Server DLL that Windows will load whenever the user clicks
on a Delphi form file using mouse button 2. But how does Windows know that it
should load the FormView.dll? As usual, Windows relies on specific entries in
the Registry to accomplish this.
There are three ways the entries can be made in the Registry. First, they can be
entered manually using RegEdit, but this is not recommended because incorrectly
editing the Registry can cause system problems.
The second option is to merge the entries into the Registry. This involves
creating a text file (with a REG extension) that contains the new entries.
Clicking mouse button 2 on the file displays a context menu containing a Merge
command. Selecting this command merges the items in the file into the Registry.
This approach is safer than using RegEdit but requires customizing the file for
each user because the complete path of the DLL must be specified in the REG
file.
The third option is to add the entries programmatically. Most installation
programs (for example, WISE Installation System) provide a way to modify the
Registry, and since the installation program knows where the DLL is installed,
the correct path can be determined during the installation process. This makes
this approach the most appropriate for registering shell extensions.
Another alternative to using a custom installation program is to create a simple
Delphi program that makes the necessary Registry entries. This is the approach I
took for this article. Listing 3 shows the source code for the RegMain unit,
which is the main form file unit in the Form-ViewReg program. Simply run this
program from the same folder that contains the FormView.dll and the required
entries are made in the Registry. Note that if you decide to move the
FormView.dll, you will need to rerun the FormViewReg program.
The actual entries that need to be added depend on the type of shell extension,
but all shell extensions require some basic settings. First, the ClassID of your
extension must be registered under the HKEY_CLASSES_ROOT\CLSID key. The CLSID
key contains a list of class identifier keys. Within each ClassID key, you must
add an InProcServer32 key that specifies the location of the shell extension
DLL.
The information that the shell uses to associate a shell extension with a
particular file type is stored under the shellex key, which is under the ProgID
for the file type. Context menu handlers are registered under the
ContextMenuHandlers key. Within this key, the ClassID of the handler is listed.
The FormCreate event handler in Listing 3 shows the entries that must be
made for the Delphi form viewer context menu.
For More Information
I relied on two sources of information in developing the shell extension for
this article. The first one is the Microsoft Development Network (MSDN). Since
Microsoft defines the shell extension interfaces, this is the best place to find
detailed information about what the methods of each interface must accomplish.
The second source of information came from the Delphi 3 source code—in
particular, the ShlObj unit, which has quite a few comments regarding the shell
extension interfaces.
On the Drawing Board
Over the past three installments of “Delphi by Design,” the focus has been on
COM support in Delphi 3. Along the way, we’ve seen how interfaces are used to
manipulate objects, how interfaces define the abilities of automation servers,
and how Windows itself relies on them. All of this information serves as a
foundation for the topic I’ll be covering in the next issue: How to convert a
native Delphi component into an ActiveX control. v
Copyright © 1998 The Coriolis Group, Inc. All rights reserved.
Listing 1 - FormView.dpr
library FormView;
uses
ComServ,
FormViewImpl in 'FormViewImpl.pas';
exports
DllGetClassObject,
DllCanUnloadNow,
DllRegisterServer,
DllUnregisterServer;
{$R *.RES}
begin
end.
Listing 2 - FormViewImpl.pas
{===============================================================
FormViewImpl Unit
This unit implements the TFormViewContentMenu class, which is
a COM object that supports the IShellExt and IContextMenu
interfaces.
===============================================================}
unit FormViewImpl;
interface
uses
Windows, Forms, StdCtrls, ShellApi, SysUtils, Classes, Controls,
ComServ, ComObj, ShlObj, ActiveX;
const
CLSID_DelphiFormViewerContextMenu: TGUID =
'{F169D961-B907-11D0-B8FA-A85800C10000}';
type
TFormViewContextMenu = class( TComObject,
IShellExtInit, IContextMenu )
private
FFileName: string;
public
// IShellExtInit Methods
// Use a Method Resolution Clause because Initialize is
// defined as a virtual method in TComObject
function IShellExtInit.Initialize = ShellInit;
function ShellInit( Folder: PItemIDList;
DataObject: IDataObject;
ProgID: HKEY ): HResult; stdcall;
// IContextMenu Methods
function QueryContextMenu( Menu: HMENU;
Index, CmdFirst, CmdLast,
Flags: UINT ): HResult; stdcall;
function GetCommandString( Cmd, Flags: UINT;
Reserved: PUINT; Name: LPSTR;
MaxSize: UINT ): HResult; stdcall;
function InvokeCommand( var CommandInfo: TCMInvokeCommandInfo ):
HResult; stdcall;
end;
implementation
uses
Registry;
{==================================}
{== TFormViewContextMenu Methods ==}
{==================================}
function TFormViewContextMenu.ShellInit( Folder: PItemIDList;
DataObject: IDataObject;
ProgID: HKEY ): HResult;
var
Medium: TStgMedium;
FE: TFormatEtc;
begin
if DataObject = nil then
begin
Result := E_FAIL;
Exit;
end;
with FE do
begin
cfFormat := CF_HDROP;
ptd := nil;
dwAspect := DVASPECT_CONTENT;
lindex := -1;
tymed := TYMED_HGLOBAL;
end;
// Transfer the data referenced by the IDataObject reference to
// an HGLOBAL storage medium in CF_HDROP format.
Result := DataObject.GetData( FE, Medium );
if Failed( Result ) then
Exit;
try
// If only one file is selected, retrieve the file name and
// store it in FileName. Otherwise fail.
if DragQueryFile( Medium.hGlobal, $FFFFFFFF, nil, 0) = 1 then
begin
SetLength( FFileName, MAX_PATH );
DragQueryFile( Medium.hGlobal, 0, PChar(FFileName), MAX_PATH);
Result := NOERROR;
end
else
Result := E_FAIL;
finally
ReleaseStgMedium( Medium );
end;
end;
function TFormViewContextMenu.QueryContextMenu( Menu: HMENU;
Index, CmdFirst, CmdLast, Flags: UINT ): HResult;
var
MenuText: string;
AddMenuItem: Boolean;
begin
AddMenuItem := True;
if ( Flags and $000F ) = CMF_NORMAL then
MenuText := 'View (Form File on Desktop) as Text'
else if ( Flags and CMF_VERBSONLY ) <> 0 then
MenuText := 'View (Form File via Shortcut) as Text'
else if ( Flags and CMF_EXPLORE ) <> 0 then
MenuText := 'View (Form File in Explorer) as Text'
else
AddMenuItem := False;
if AddMenuItem then
begin
InsertMenu( Menu, Index, mf_String or mf_ByPosition,
CmdFirst, PChar( MenuText ) );
Result := 1; // Return number of menu items added
end
else
Result := NOERROR;
end; {= TFormViewContextMenu.QueryContextMenu =}
function TFormViewContextMenu.GetCommandString( Cmd, Flags: UINT;
Reserved: PUINT; Name: LPSTR; MaxSize: UINT ): HResult;
begin
case Cmd of
0:
begin
if Flags = GCS_HELPTEXT then
begin
// Return the string to be displayed in the Explorer
// status bar when the menu item is selected
StrCopy(Name, 'View the selected Delphi form file as text');
end;
Result := NOERROR;
end;
else // Invalid menu item
Result := E_INVALIDARG;
end;
end; {= TFormViewContextMenu.GetCommandString =}
function GetViewerPath: string;
var
R: TRegIniFile;
begin
R := TRegIniFile.Create( '\Software\Raize\FormFileViewer' );
try
Result := R.ReadString( 'Program', 'Path', '' );
Result := '"' + Result + '" "%s"';
finally
R.Free;
end;
end;
function TFormViewContextMenu.InvokeCommand( var CommandInfo:
TCMInvokeCommandInfo ): HResult;
var
Success: Boolean;
CmdLine: string;
SI: TStartupInfo;
PI: TProcessInformation;
begin
// Make sure we are not being called by an application
if HiWord( Integer( CommandInfo.lpVerb ) ) <> 0 then
begin
Result := E_FAIL;
Exit;
end;
// Execute the command specified by CommandInfo.lpVerb
case LoWord( CommandInfo.lpVerb ) of
0:
begin
FillChar( SI, SizeOf( SI ), #0 );
SI.cb := SizeOf( SI );
SI.wShowWindow := sw_ShowNormal;
SI.dwFlags := STARTF_USESHOWWINDOW;
CmdLine := Format( GetViewerPath, [ FFileName ] );
Success := CreateProcess( nil, PChar( CmdLine ), nil, nil,
True, 0, nil, nil, SI, PI );
if not Success then
begin
MessageBox( CommandInfo.hWnd,
'Could not start the Form File Viewer.',
'Error', mb_IconError or mb_OK );
end;
Result := NOERROR;
end;
else // Invalid menu item
Result := E_INVALIDARG;
end; { case }
end; {= TFormViewContextMenu.InvokeCommand =}
initialization
// Create a COM object factory which will be responsible for
// creating instances of our shell extension. ComServer is
// declared in ComServ unit.
TComObjectFactory.Create( ComServer, TFormViewContextMenu,
CLSID_DelphiFormViewerContextMenu,
'', 'View Delphi Form Files',
ciMultiInstance );
end.
Listing 3 - RegMain.pas
unit RegMain;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
ExtCtrls, StdCtrls;
type
TForm1 = class(TForm)
Button1: TButton;
Panel1: TPanel;
Label1: TLabel;
Image1: TImage;
procedure Button1Click(Sender: TObject);
procedure FormCreate(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
{$R *.DFM}
uses
Registry;
const
FormViewerClassID = '{F169D961-B907-11D0-B8FA-A85800C10000}';
procedure TForm1.FormCreate(Sender: TObject);
var
Reg: TRegistry;
begin
Reg := TRegistry.Create;
try
with Reg do
begin
RootKey := HKEY_CLASSES_ROOT;
OpenKey( '\CLSID\' + FormViewerClassID, True );
WriteString( '',
'Delphi Form Viewer Context Menu Shell Extension');
OpenKey( '\CLSID\' + FormViewerClassID + '\InProcServer32',
True );
WriteString( '', ExtractFilePath( Application.ExeName ) +
'\FormView.dll' );
WriteString( 'ThreadingModel', 'Apartment' );
CreateKey( '\DelphiForm\shellex\ContextMenuHandlers\' +
FormViewerClassID );
end;
finally
Reg.Free;
end;
end;
procedure TForm1.Button1Click(Sender: TObject);
begin
Close;
end;
end.
<<<Fine articolo originale>>> Behh... siamo arrivati alla fine, non resta che scaricarsi i sorgenti: ho incluso un readme che descrive in linea di massima il pacchetto.
DfmViewer.7z
|