![]() |
![]() ![]() |
Neil J. Rubenking
Creating COM objects in Delphi is quite different from creating COM objects in C++, although similarities do exist: COM objects support one or more COM interfaces, and a single COM interface can be represented by a Delphi object or a C++ object. When a COM object needs to support multiple interfaces, a C++ implementation uses multiple inheritance to derive an object that inherits from all of the necessary interfaces. Multiple inheritance is not a feature of Delphi, so the Delphi implementation needs to take a different approach.
A Delphi-based, multi-interface COM object must be built from several separate objects. Each required COM interface is represented by a satellite object, descended from the Delphi-supplied IUnknown object type. The satellite object implements the IUnknown interface. The COM object as a whole is represented by a container object, also descended from IUnknown. The container object, which contains instances of the satellite objects as data fields, returns a pointer to the requested interface when its QueryInterface function is called. The first part of this article presented these concepts, along with their implementation, in the ISatelliteUnknown and IContainerUnknown objects. Now we'll use these objects to create shell-extension COM objects for Windows 95.
We'll demonstrate the creation of four Windows 95 shell extensions in Delphi: a context-menu handler, a property-sheet handler, a drag-and-drop handler, and an icon handler. The example shell extensions act on an imaginary file type, DelShellFile, associated with the extension .DEL. A DelShellFile's single line of text represents a whole number; in a real program this would be replaced by some more complex attribute of the file. The four shell extensions will interact with this "magic number." You'll also find a copy-hook-handler shell extension in the source code for this article. But because its implementation didn't require use of the container/satellite system, that extension won't be discussed in the article itself. All code mentioned in this article is available for download from PC Magazine Online. (See the sidebar "Getting the Files" in the Utilities column for details.)
Figure 1 represents the hierarchy of supporting objects that we'll create. The solid lines define a standard object hierarchy, with the Delphi-defined IUnknown object at the top. Beneath each object's name is a list of the interfaces it supports, omitting the ubiquitous IUnknown interface. The dotted lines represent the container/satellite relationship on which this entire system is based.
The context menu, property sheet, and drag-and-drop-handler shell extensions rely on the IShellExtInit interface for initialization. The icon handler relies on the IPersistFile interface for the same purpose. Figure 2 shows the declarations for satellite objects that implement these two helper interfaces and for container objects that are pre-initialized to handle these satellite objects.
type IMyShellExtInit = class(ISatelliteUnknown) public function Initialize(pidlFolder:PItemIDList; lpdobj: IDataObject; hKeyProgID:HKEY):HResult; virtual; stdcall; end; IMyPersistFile = class(ISatelliteUnknown) public function GetClassID(var classID: TCLSID): HResult; virtual; stdcall; function IsDirty: HResult; virtual; stdcall; function Load(pszFileName: POleStr; dwMode: Longint): HResult; virtual; stdcall; function Save(pszFileName: POleStr; fRemember: BOOL): HResult; virtual; stdcall; function SaveCompleted(pszFileName: POleStr): HResult; virtual; stdcall; function GetCurFile(var pszFileName: POleStr): HResult; virtual; stdcall; end; ISEIContainer = class(IContainerUnknown) protected FShellExtInit : IMyShellExtInit; // Satellite interface public FNumFiles : Integer; FInitFiles : TStringList; FIDPath : String; Constructor Create; destructor Destroy; override; function QueryInterface(const WantIID: TIID; var ReturnedObject): HResult; override; end; IPFContainer = class(IContainerUnknown) protected FPersistFile : IMyPersistFile; // Satellite interface public FPFFilename : String; Constructor Create; destructor Destroy; override; function QueryInterface(const WantIID: TIID; var ReturnedObject): HResult; override; end;
The IMyShellExtInit object adds the method Initialize, which implements the IShellExtInit interface's Initialize function. It inherits ISatelliteUnknown's handling of the QueryInterface, AddRef, and Release methods. Thus IMyShellExtInit's virtual method table matches perfectly with the set of functions that defines an IShellExtInit interface. The Initialize method extracts a list of files from data supplied by the calling program and stores it in a data field of its container object, which must be of the type ISEIContainer.
ISEIContainer inherits the AddRef and Release methods of IContainerUnknown. In its implementation of QueryInterface, ISEIContainer first calls the QueryInterface method inherited from IContainerUnknown. If that method doesn't return S_OK, the overriding QueryInterface method checks to see if IShellExtInit is being requested; if so, QueryInterface passes back a pointer to its protected data field FShellExtInit, which is an object of type IMyShellExtInit. ISEIContainer also defines data fields to hold a list of files, the number of files, and a path. Its Create constructor initializes the file list and FShellExtInit objects, and its Destroy destructor frees the memory used by those two objects.
The IMyPersistFile object looks a lot more complicated than IMyShellExtInit, but five of the six methods that implement functions of the IPersistFile interface simply return the result code E_FAIL. IMyPersistFile's Load method receives a filename in Unicode form; it converts this filename to an ANSI string and stores it in a data field of its container object, which must be of type IPFContainer. Like ISEIContainer, IPFContainer overrides QueryInterface. If the inherited QueryInterface method fails, it checks to see if IPersistFile is being requested. If so, it passes back a pointer, of type IMyPersistFile, to its protected data field FPersistFile. The container object's constructor and destructor methods create and destroy the FPersistFile object as well.
Now we're ready to build the shell extensions themselves.
When you right-click on a file in Windows 95 Explorer, the system checks to see whether a context-menu handler is defined for that file's type. If one is, the system creates an instance of the context-menu handler COM object and passes a list of selected files to the Initialize function of the object's IShellExtInit interface. Then it calls the QueryContextMenu function of the IContextMenu interface. The function uses standard Windows API functions such as InsertMenu to insert menu items or separators; the function's return value is the number of items added, not including separators. The IContextMenu interface's InvokeCommand function is called when the user selects one of the added menu items, and the GetCommandString function is called to supply an explanation of the menu item in Explorer's status bar.
The Delphi objects used to define and initialize a context-menu handler are IMyContextMenu, IDSContextMenu, and ICMClassFactory. IMyContextMenu is an ISatelliteUnknown descendant that implements the three IContextMenu functions. IDSContextMenu is a descendant of ISEIContainer, so it already supports IShellExtInit. IDSContextMenu adds a protected data field, FContextMenu, of type IMyContextMenu. As before, IDSContextMenu's constructor and destructor create and destroy the satellite object, and its QueryInterface method passes back a pointer to the FContextMenu object when IContextMenu is requested.
This unit also defines ICMClassFactory, a descendant of IMyClassFactory that specifically returns an instance of IDSContextMenu. The CreateInstance method creates and returns the requested instance, but only if the interface being requested is one that IDSContextMenu supports. Each of the shell extensions will have a nearly identical IMyClassFactory descendant.
The QueryContextMenu method checks whether or not multiple files are selected. For a single file, it adds a menu item titled Magic Number; for multiple files, it adds an item titled Average Magic Number. The InvokeCommand method displays the requested number in a simple message box after validating its arguments. And the GetCommandString method returns a single-word name for the menu item or a descriptive string, depending on which was requested.
A drag-and-drop handler is quite similar to a context-menu handler--in fact, it even supports the same IContextMenu interface. The drag-and-drop extension, however, is invoked when a file is dragged with the right mouse button onto a folder, and it is registered to the folder file type, not to the file type of the dragged file. The IMyDragDrop satellite object implements the methods QueryContextMenu, InvokeCommand, and GetCommandString.
The QueryContextMenu method first flips through the list of files supplied by the system and checks whether the files are all of type DelShellFile. If they are, the method adds a menu item named Count Files, along with a separator, and returns 1; if not, it does nothing and returns 0. When the menu item is chosen, the InvokeCommand method counts the files in the dropped-on folder and adds the number of files to the magic number of each selected DelShellFile. Because a DelShellFile's icon depends on its magic number, a call to the API function SHChangeNotify tells the system to redisplay each of the files.
The IDSDragDrop container object is functionally identical to the IDSContextMenu object. It simply maintains a satellite object of type IMyDragDrop rather than IMyContextMenu.
When the user selects Properties from the context menu for one or more selected files of the same file type, the system checks to see if a property-sheet handler is defined for that file type. If so, the system creates an instance of the shell extension and initializes it with a list of files via the IShellExtInit interface's Initialize function. The system also calls the AddPages function of the IShellPropSheetExt interface to allow the property-sheet handler to add one or more property pages. The other IShellPropSheetExt interface function, ReplacePages, is normally not implemented.
Delphi programmers will suddenly find themselves in terra incognita when implementing AddPages. In order to create the property-sheet page, you must supply a dialog-box template resource and a dialog-box function. Only old-time Windows programmers will remember those hoary precursors to today's visual development style. You can use a resource-creation tool like Borland's Resource Workshop to create the dialog template, or create the resource script as text and compile it with the BRCC.EXE resource compiler that comes with Delphi. The resource script that defines the DelShellFile property sheet is included with the downloadable source code.
This resource script defines two statics (labels), a list box, and a button. The constants IDC_Static, IDC_ListBox, and IDC_Button, used as control ID numbers, are defined in the shared include file, SHEET.INC.
The AddPages method initializes various fields of a TPropSheetPage structure, including the dialog-box template, the dialog-box procedure, and the program-defined lParam. Here, lParam holds the list of files passed by the shell. The callback function serves to ensure that this list gets deallocated. A call to CreatePropertySheetPage creates a page based on the TPropSheetPage structure, and a call to the shell-supplied lpfnAddPage function adds the page to the Properties dialog.
The dialog-box procedure handles two specific messages. On a WM_INITDIALOG message, it adds to the list box the list of files pointed to by the lParam field of the property sheet page, preceding each with its magic number. The procedure sets the static control to reflect the number of files selected. It then disposes of the file list, and sets the field that held the file list to 0.
When the user clicks the Zero Out button, the dialog-box procedure receives a WM_COMMAND message with the low word of the wParam set to the button's ID. The dialog-box procedure steps through the list of files, sets each file's magic number to 0, and calls the SHChangeNotify API function to signal a need to redisplay the file's icon. Virtually every property-sheet dialog-box procedure will need to respond to WM_INITDIALOG in order to initialize its controls. If it does more than just display information, it will need to respond to WM_COMMAND messages from particular controls as well.
In most cases, the Windows 95 shell gets the icon for a file by checking the DefaultIcon key below the Registry key for the file's type. But if DefaultIcon is set to %1, the shell will call the file's icon-handler shell extension instead. The system calls the Load function of the icon handler's IPersistFile interface, passing the name of the file. The icon handler can supply a file-specific icon through the IExtractIcon interface's GetIconLocation and Extract functions, either by giving the filename and index for an icon resource or by creating an icon on demand.
The example IMyExtractIcon satellite object implements both possibilities. If the conditional compilation directive UseResource is defined, the GetIconLocation method copies the name of the DLL containing the IMyExtractIcon object into the szIconFile argument, then calculates the value of the piIndex argument from the file's magic number. The method sets the pwFlags argument to include GIL_PERINSTANCE, meaning each file may have a different icon, and GIL_DONTCACHE, meaning the system should not cache the icon. The Extract method is not used; it simply returns S_FALSE.
When the UseResource conditional compilation directive is not defined, the IMyExtractIcon satellite object creates an icon for each file. The GetIconLocation method stores the file's magic number in the piIndex argument and uses the same flags, plus the GIL_NOTFILENAME flag. The shell calls the Extract method, which creates both a large and a small icon for the file. The height of the red bar within the icon's rectangle is determined by the file's magic number. The source code demonstrates on-the-fly icon creation, but since this is tangential to the article's topic, we won't discuss its details here.
To put your shell extensions to work, you must compile them into a DLL that implements the standard functions DLLGetClassObject and DLLCanUnloadNow. The code that defines this DLL is included with the article's source code. DLLGetClassObject determines which object is being requested, creates a class factory to match, and returns the object created by the class factory. The code that defines this DLL is included with the article's source code, a simple console application that handles registering and unregistering all of the example shell extensions.
Before building your own shell extensions based on the examples, be sure to generate new Globally Unique Identifiers (GUIDs) to replace all of the GUIDs in the code. You can do this with the GUIDS program presented in the previous installment of this column.
Neil J. Rubenking is contributing technical editor ofPC Magazine.Most modern development environments include an integrated debugging feature that lets you step through or trace code, set breakpoints, and watch variables. But when your code resides in a DLL rather than in an executable program, integrated debuggers can't help you. Even if you use a 32-bit standalone debugger, COM objects won't be easily accessible, because they operate in the memory context of the object or program that calls them. For example, if the COM objects in question are Windows 95 shell extensions, they run in Windows Explorer's memory space.
In many cases, the questions you'd want a debugger to answer about your COM objects are quite simple: Was the DLL activated at all? Did the system try to create an instance of the COM object? Which interface was requested? Such questions can be handled by a simple message-logging technique in which the COM object broadcasts a status message that is received by a separate logging program. The unit DllDebug, available from PC Magazine Online, implements the broadcast side of this process.
The unit's initialization section sets the WM_LOGGIT variable to the unique message identifier that's obtained by passing the string Debugging Status Message to the RegisterWindowMessage function. The first call to RegisterWindowMessage using a given string will return a unique message number; subsequent calls using the same string will return the same message number.
Because 32-bit programs use separate memory contexts, the Loggit function can't simply pass a pointer to the status message string itself. The pointer would be invalid in the receiving program's memory context. Instead, the Loggit function adds the status message to the global atom table. It calls SendMessage, passing -1 as the window handle, WM_LOGGIT as the message number, and the atom as the wParam. SendMessage doesn't return until all top-level windows have had a chance to process the message. At that point, the atom can safely be deleted.
The NameOfIID function, also included in the DLLDebug unit, will come in handy when composing status messages. As written, it reports the name of IIDs related to shell extensions, but you can add any system IID values that are relevant to your project. So, for example, within a QueryInterface method you might insert the line:
Loggit(Format('QueryInterface: %s requested',
[NameOfIID(WantIID)]));
Broadcasting the WM_LOGGIT message is half the job; now we need a program to receive and log the status message. The Logger program, also available online, demonstrates one way to handle that task.
Since the value of the WM_LOGGIT message isn't known until run time, it's not possible to set up a standard message-handling method. Instead, the program overrides the form's DefaultHandler method. When a WM_LOGGIT message comes through, this method extracts the status message from the passed atom and adds it to a list box. Besides this core functionality, the program includes three buttons that allow you to insert a comment, clear the list box, and save the logged status messages to a file. Figure A shows the Logger program in action.
In this dialog box, the QueryInterface methods of several Delphi-based COM objects have been "instrumented" with a line that logs the name of the requested interface. This list of requests occurred when Explorer initially extracted a particular file's icon, after which the user right-clicked the file and viewed its properties; everything worked correctly. If the logger shows unexpected results, you'll add more calls to the Loggit function around the problem area and try again until you've identified the source of the problem.
FIGURE 1: This is the object hierarchy for the Delphi shell extension objects we'll create. Supported interfaces (other than IUnknown) are listed in italics beneath each object name, and dotted lines represent container/satellite relationships. FIGURE 2:Two satellite objects implement the helper interfaces required by context-menu, property-sheet, drag-and-drop, and icon-handler Windows 95 shell extensions. FIGURE A: This simple debug message logger adds received status messages to a list box.
|
TOP | ![]() Copyright (c) 1997 Ziff-Davis Inc. |