HomePage Delphi Library
Getting Windows to Share Some of Its Secrets
You may have stumbled across the Windows API function SHChangeNotify. According to the Windows docs, it notifies the system of any events that may affect the shell. Well, that's very nice for the shell. There are many interesting events the system keeps track of: file and directory changes, media insertion and removal, disk free-space updates, etc. For example, you're probably familiar with having Windows Explorer update the CD drive's icon when you insert or remove a CD. That's an instance of shell notifications at work. Very useful, but wouldn't it be nice if you could see those notifications as well?
Until now, tapping into this mechanism required insider knowledge. For some reason, Microsoft decided not to reveal the methods by which Windows Explorer receives these notifications. This article exposes those secrets, and provides a nifty component to give you a head start in using this exciting technique the Delphi way.
A Brief Digression
Before getting wrapped up in the details of signing up for shell notification, you need to know just a little bit about the PItemIDList record type. This is a type defined in the standard ShlObj unit, and refers to the construct known to shell programmers as a PIDL (pronounced "piddle"). The nature of PIDLs is a broad topic, more than enough to occupy its own article. For the purposes of shell notifications, however, a deep exposure isn't necessary.
Basically, a PIDL is the shell version of the DOS path. If you look at the folder tree in the left pane of Windows Explorer, you can see all the file system folders. You can also see folders that aren't part of the file system, such as Control Panel. Some means was necessary to identify all folders uniquely, whether they were part of the file system or not. Microsoft's solution was the PIDL, a sort of turbo-charged path. It's not a simple string; rather, it's a pointer to a chain of structures that contain identifying information for a given folder. The contents of a PIDL are largely opaque; they weren't intended for direct display or manipulation.
One tricky aspect of PIDLs is that they must often be allocated in one module, and freed in a module written by a different party. This can be problematic, as different development environments often use different memory allocation schemes. For example, using Delphi's FreeMem procedure to free memory originally allocated by some C compiler's malloc RTL function would most likely end up corrupting the heap. As a result, the memory buffers to contain PIDLs must be allocated and freed by the shell task allocator. This ensures the PIDL's memory will always be allocated and freed using the same scheme, regardless of the development environment used for the module. This functionality is implemented through a COM interface named IMalloc. Using anything but this global allocation engine to allocate and free PIDLs is a quick route to an abnormal termination. You can use the IMalloc interface directly for this, but we have opted instead to use some "cheater" functions, which you'll find defined in the kbsnPIDL unit in the sample files included with this article. (All source in this article is available for download; see end of article for details.)
The upshot of all this is that every file system object can be represented either as a PIDL or a path. In addition, many non-file system objects also exist that can't be identified by anything but a PIDL. Many shell functions, therefore, require PIDLs as parameters instead of traditional paths, or return PIDLs allocated from within the shell function that you may need to free later. For the purposes of this article, you may consider a PIDL to be a pointer - supplied by the shell - that points to arbitrary data that should not be modified in any way. Functions have been provided in unit kbsnPIDL that will convert a file system path to a PIDL, and vice versa. The only unusual aspect is that you should never free a PIDL with the usual VCL functions, such as FreeMem; you must use only the FreePIDL function provided in unit kbsnPIDL.
Getting "In the Loop"
The key to receiving shell change notifications is the SHChangeNotifyRegister function. Here's its prototype:
function SHChangeNotifyRegister(Window: HWND; Flags: DWORD;
EventMask: ULONG; MessageID: UINT; ItemCount: DWORD;
var Items: TNotifyRegister): THandle; stdcall;
It's used to register a window with the shell, which will then be notified of all subsequent SHChangeNotify events. It's exported from SHELL32.DLL. Like most undocumented functions, it's not exported by name. Therefore, it's necessary to link using the function ordinal. The export ordinal for SHChangeNotifyRegister is 2.
The Window parameter specifies the handle of the window that should receive the notification messages. This can be any window you desire, but it's usually best to create an invisible window whose only responsibility is handling the notification messages.
The EventMask parameter is a bit-mask of all the events you are interested in. You can use any combination of the SHCNE_xxx constants, the same ones that are used for the SHChangeNotify function, combined with a logical or operation. Figure 1 provides a complete listing of these constants.
Constants associated with single events
SHCNE_ASSOCCHANGED
A file-type association has changed.
SHCNE_ATTRIBUTES
The attributes of an item or folder have changed.
SHCNE_CREATE
A non-folder item has been created.
SHCNE_DELETE
A non-folder item has been deleted.
SHCNE_DRIVEADD
A drive has been added.
SHCNE_DRIVEADDGUI
A drive has been added via the shell.
SHCNE_DRIVEREMOVED
A drive has been removed.
SHCNE_EXTENDED_EVENT
Not currently used.
SHCNE_FREESPACE
The amount of free space on a drive has changed.
SHCNE_MEDIAINSERTED
Storage media has been inserted into a drive
SHCNE_MEDIAREMOVED
Storage media has been removed from a drive.
SHCNE_MKDIR
A folder has been created.
SHCNE_NETSHARE
A folder on the local computer is being shared via the network.
SHCNE_NETUNSHARE
A folder on the local computer is no longer being shared via the network.
SHCNE_RENAMEFOLDER
The name of a folder has changed.
SHCNE_RENAMEITEM
The name of a non-folder item has changed.
SHCNE_RMDIR
A folder has been removed.
SHCNE_SERVERDISCONNECT
The computer has disconnected from a server.
SHCNE_UPDATEDIR
The contents of an existing folder changed, but the folder wasn't renamed.
SHCNE_UPDATEIMAGE
An image from the system image list has changed.
SHCNE_UPDATEITEM
An existing non-folder item changed, but the item wasn't renamed.
Constants that combine multiple event types
SHCNE_ALLEVENTS
Specifies a combination of all possible event identifiers.
SHCNE_DISKEVENTS
Specifies a combination of all of the disk event identifiers.
SHCNE_GLOBALEVENT
Specifies a combination of all of the global event identifiers.
Flag used with event constants
SHCNE_INTERRUPT
The event occurred as a result of a system interrupt.
Figure 1: Shell event constants.
The Flags parameter allows you to specify optional behavior for the notifications. You may filter out interrupt or non-interrupt events, and decide whether to use a proxy window under NT. See Figure 2 for a list of these flags. Typically, both interrupt and non-interrupt flags should be set, as you generally don't care about the ultimate source of the event. At any rate, interrupt events are extremely rare. The SHCNF_NO_PROXY flag allows you to handle the notification more efficiently on Windows NT, but it complicates the message handling procedure, and requires a couple more undocumented functions. We'll explain the whole situation with NT later.
Flag
Value
SHCNF_ACCEPT_INTERRUPTS
$0001
SHCNF_ACCEPT_NON_INTERRUPTS
$0002
SHCNF_NO_PROXY
$8000
Figure 2: SHChangeNotifyRegister flags.
The MessageID parameter is the identifier of the message that will be sent to that window. It's recommended you use a value derived from WM_USER for the value of MessageID to avoid conflicts with system messages. If you use a single-purpose window as previously described, the value of WM_USER itself is fine. The details of the message handling are explained later.
The ItemCount parameter specifies the number of paths you wish to monitor. Usually, this will be 1, but it's possible to monitor many different paths via the Items parameter.
The Items parameter is a pointer to a record of type TNotifyRegister. The following is the definition of this record type:
TNotifyRegister = packed record
pidlPath: PItemIDList;
bWatchSubtree: BOOL;
end;
If the pidlPath data member of this record specifies a valid PIDL for a valid folder, you'll receive events that affect the folder itself, as well as any items in the folder. If you set the bWatchSubtree data member to True, you'll receive events for the entire sub-tree rooted at the specified folder, e.g. all folders and items below the specified folder in addition to the folder itself. If you set the pidlPath data member to nil, you'll receive events for every folder and item on the system. If the value of the ItemCount parameter is greater than 1, you must supply the same number of TNotifyRegister records to the Items parameter in the form of a vector, one record packed after another into a buffer large enough to hold all of them.
If the SHChangeNotifyRegister function succeeds, the return value is a handle of a shell change notification object. You should save this handle for later use. If the function fails, the return value is 0.
Shut It Off
When you're finished monitoring the notification events, you should pass the notification handle returned by SHChangeNotifyRegister to SHChangeNotifyDeregister. The export ordinal value of SHChangeNotifyDeregister is 4, and the function declaration is as follows:
function SHChangeNotifyDeregister(Notification: THandle):
BOOL; stdcall;
The Notification parameter takes the handle of a shell change notification object returned by a successful call to SHChangeNotifyRegister. As you might guess, if the function succeeds, the return value is True; if it fails, the return value is False.
Getting the Message
After registering to receive shell notification messages, the shell will send its notification messages to the window you specified in the Window parameter of the SHChangeNotifyRegister function. You must then crack the relevant data out of the message to get useful information. Delphi doesn't define a special message record type for these messages, so simply use the standard TMessage type. This is a variant record type, but you should consider it to be defined as shown here:
TMessage = record
Msg: DWORD;
WParam: DWORD;
LParam: DWORD;
Result: DWORD);
end;
The Msg data member of the message record will be set to whatever value you specified in the MessageID parameter you passed to the SHChangeNotifyRegister function. Use this parameter to recognize notification messages, as opposed to the myriad other miscellaneous messages the Windows system will send to your window.
The LParam data member will be set to the ID of the event that occurred. This will be one of the SHCNE_xxx values shown in Figure 1. It's possible that multiple event IDs could be or'ed together, so it would be best to test for the presence of a given ID value, using a logical and operation, rather than via an equality test using the = operator.
The WParam will be a pointer to a record of type TTwoPIDLArray. The name WParam, which is C-style shorthand for Word Parameter, is an anachronism. It's now actually a 32-bit long type, just like LParam, and so can store a pointer. This record points to an array containing the two PIDLs associated with the event, as shown here:
TTwoPIDLArray = packed record
PIDL1: PItemIDList;
PIDL2: PItemIDList;
end;
Those of you familiar with the SHChangeNotify function may be wondering why you only receive PIDLs from shell change notification messages, even though the SHChangeNotify function accepts PChar and DWORD data types, as well as PIDLs. The reason is that all the data types are automatically converted to PIDLs by the shell before being sent anywhere. For SHCNF_PATH and SHCNF_PRINTER types, this seems obvious enough. For the SHCNF_DWORD type, a 10-byte "fake" PIDL is created, with the two DWORD items immediately following the cb data member. Ignore the cb data member; it has no use in this situation except as a placeholder. This encoding scheme is encapsulated for your convenience by the TDWORDItemID record type, as shown in the following:
TDWORDItemID = packed record
cb: Word; { Ignore }
dwItem1: DWORD;
dwItem2: DWORD;
end;
The SHCNE_FREESPACE event is handled as a special case. When the drive is passed in as a path or a PIDL, it's converted to a DWORD contained by the above encoding scheme, with drives A: to Z: mapping onto bits 0 to 25. For example, drive D: would map to bit 3, so the value of the dwItem1 data member would have a value of 8. If there are two drives specified for the event, then two bits will be set in the DWORD. The value of the second DWORD in this case appears to be meaningless.
Windows NT and Memory Maps
This all seems simple enough, at least by the standards of Windows shell programming, but on Windows NT, there is a bit of a problem. NT maintains a careful separation of memory used by different processes. One process attempting to directly access memory owned by another process puts you in the express lane to your favorite exception, the General Protection Fault. You can't simply send a message to a window in a different process, and still expect the structure pointer contained in that message to be accessible.
To get around this, NT actually dumps all the relevant data into a memory-mapped file, which is accessible from any process, then sends the memory map handle and a process ID as the parameters to the message. To remain compatible with Windows 95, somebody obviously has to extract the information from that memory map on the other side. The way this works is that NT automatically creates a hidden "proxy" window whenever you call SHChangeNotifyRegister. It is the proxy window that receives the notification message containing the memory map. Its message handler then extracts all the information, and passes on the correct message with the expected data to your window.
Of course, this is not exactly efficient, which is where the SHCNF_NO_PROXY flag comes in. By specifying that flag when calling SHChangeNotifyRegister, you're telling NT to not create the proxy window, so the memory map handle gets passed directly to your window in the notification message. It's then up to you to extract the relevant information from the memory map. Fortunately, there are two functions that do all the work for you: SHChangeNotification_Lock and SHChangeNotification_Unlock.
The export ordinal value of SHChangeNotification_Lock is 644, and the function declaration is shown here:
function SHChangeNotification_Lock(MemoryMap: THandle;
ProcessID: DWORD; var PIDLs: PTwoPIDLArray;
var EventID: ULONG): THandle; stdcall;
The MemoryMap parameter is a handle to a chunk of memory allocated by the NT system. This handle will be contained in the WParam data member of the shell notification message.
The ProcessID parameter takes the ID of the process that generated the memory map. This value is contained in the LParam data member of the shell notification message.
The PIDLs parameter is output-only, and takes a variable of type pointer to a record of type TTwoPIDLArray. You may initialize the pointer variable to nil before calling the function. Do not allocate an actual TTwoPIDLArray record. When the function returns, this pointer will point to a TTwoPIDLArray record that contains the two PIDLs for the notification message, which are normally passed via the WParam data member of the message. Don't try to free this pointer directly, either.
The EventID parameter is also output-only, and takes a variable of type ULONG, or Longint, if you prefer. You may initialize this variable to 0 before calling the function. When the function returns, this variable will contain the EventID of the event for this message, which is normally passed by the LParam data member of the message.
The return value is a handle to the memory map, which you should save; you'll need it later. If by some strange circumstance the function should fail, it returns a 0.
When you have finished working with the data extracted via SHChangeNotification_Lock, you should unlock the memory map so NT can properly dispose of it. This is the purpose of the SHChangeNotification_Unlock function. The export ordinal value of SHChangeNotification_Unlock is 645, and the function declaration is as follows:
function SHChangeNotification_Unlock(Lock: THandle):
BOOL; stdcall;
The Lock parameter is the handle you obtained from the call to the SHChangeNotification_Lock function. The function returns True if successful, and False on failure.
It's important to note that these functions exist only in Windows NT. If you attempt to link to them while running Windows 95, you'll experience a link failure. Therefore, it's impossible to use the Delphi external method for linking, unless you are completely sure your program will never run on anything but NT. You should use dynamic linking instead by calling the GetProcAddress Windows API function after testing which operating system is running. See Figure 3 for an example of using these NT mapping functions.
var
PIDLs: PTwoPIDLArray;
EventId: DWORD;
Lock: THandle;
begin
// If NT, use the memory map to access the PIDL data.
if (SysUtils.Win32Platform = VER_PLATFORM_WIN32_NT) then
begin
Lock := SHChangeNotification_Lock(THandle(
TheMessage.wParam), DWORD(TheMessage.lParam),
PIDLs, EventId);
if (Lock <> 0) then
try
ProcessEvent(EventId, PIDLs);
finally
SHChangeNotification_Unlock(Lock);
end;
end
else
// If this isn't NT, access the PIDL data directly.
begin
EventId := DWORD(TheMessage.lParam);
PIDLs := PTwoPIDLArray(TheMessage.wParam);
ProcessEvent(EventId , PIDLs);
end;
end;
Figure 3: Example of using SHChangeNotification_Lock.
The Origin of Events
So, now you know how to receive all these shell notifications that are floating around, but who is actually generating them? According to the Windows documentation, "An application should use this function (SHChangeNotify) if it performs an action that may affect the shell." That seems to be a bit of wishful thinking. We can't imagine there are many application developers who really give a hoot whether the shell is kept informed of their actions.
Fortunately, the shell seems to generate most of the notifications itself. Sometimes, it may be directly responsible for an event, in which case, it's easy enough for it to make the call to SHChangeNotify. However, for things likely to originate in another application, such as file creation, it would presumably have to be monitoring the system somehow to generate the event. The result is that these notifications can be a bit unreliable, and often, there is a noticeable delay between the event and the notification. Also, the shell has only a 10-item event buffer, and may decide to consolidate a number of events with a generic SHCNE_UPDATEDIR in case of an overflow.
In short, don't depend on these notifications for mission-critical applications.
Don't Believe Everything You Read
Another problem is that the Windows documentation isn't always completely accurate in its descriptions of the various events. Following are variations from the documentation we've observed after actual implementation.
A SHCNE_ATTRIBUTES is supposed to happen when "the attributes of an item or folder have changed." However, we have only witnessed an SHCNE_ATTRIBUTES event occurring when the printer status changed. Changing file and folder attributes produces an SHCNE_UPDATEITEM event instead.
SHCNE_NETSHARE and SHCNE_NETUNSHARE are supposed to occur when you share or unshare a folder. However, on Windows NT, the SHCNE_NETUNSHARE event never occurs. You get a SHCNE_NETSHARE event on both occasions. On Windows 95, they appear to work as advertised.
An SHCNE_UPDATEIMAGE event is claimed to signify that an image in the system image list has changed. However, images in the system image list should never change. What the event really means is that something that was using that particular icon index in the system image list is now using something else. Typical uses of SHCNE_UPDATEIMAGE include the Recycle Bin changing between empty and full, and the icon associated with a CD drive when a CD is inserted or removed. Document icons, which change as a result of changing a file-type association, do not generate an SHCNE_UPDATEIMAGE. They will produce an SHCNE_ASSOCCHANGED event instead.
SHCNE_MEDIAINSERTED and SHCNE_MEDIAREMOVED aren't generated in response to inserting or removing standard floppy diskettes. The disk drive hardware apparently doesn't support this information.
If you're deleting files into a Recycle Bin, you won't get an SHCNE_DELETE method, as you might expect. You'll actually get a SHCNE_RENAMEITEM. The SHCNE_DELETE comes only after you empty the Recycle Bin. This makes sense if you think about it, because you're actually moving the file from its old path to the Recycle Bin's path, but it might not be completely intuitive at first.
Some events can fire multiple times. This seems to apply to most file events. For example, if you delete a file, you'll probably get notified twice with identical messages. Be prepared for that.
If you want to know more, it's probably best that you test the items your particular application will be using, on as many platforms as possible.
The Delphi Way
So much for the messy details demanded by the Windows API. Let's design a component that will hide all this minutiae from those Delphi developers who have better things to worry about.
First, we'll define the component's public interface. There are two main issues with this component: where to watch, and what to watch for. The API provides the capability to filter a few criteria: event type, interrupt or non-interrupt, the folder to watch for events, and whether that folder's sub-folders should also be watched. Naturally, we also need some means of turning this thing on or off.
Identifying the folders to watch is the only unusual aspect of all this. As we mentioned previously, not all folders can be identified by file system paths. Using PIDLs to identify these folders is impractical for the design-time property editor, however, because PIDLs are opaque data types and can't be manually edited by a developer.
The solution is to use two properties. One property is a new enumerated type, TkbSpecialLocation, which encapsulates the list of Windows API constants that correspond to various "special" folders, e.g. Control Panel. These constants can be used with the SHGetSpecialFolderLocation API function to obtain a PIDL to that folder. By setting a property to one of these enumerated values, special locations can be selected without requiring the developer to type in the data for the PIDL. One of the values of TkbSpecialLocation is kbslPath. Setting this value will enable a second property to allow the developer to enter a specific file system path to monitor. Here's the final list of published properties:
property Active: Boolean
property HandledEvents: TkbShellNotifyEventTypes
property InterruptOptions: TkbInterruptOptions
property RootFolder: TkbSpecialLocation
property RootPath: TFileName
property WatchChildren: Boolean;
For those developers who like to get down and dirty, we'll surface a couple of run-time properties that allow them to meddle at the API level. There are really only two bits of information we can provide: the notification handle, and the actual root PIDL. These properties are shown in the following:
property Handle: THandle
property RootPIDL: PItemIDList
Because this component encapsulates a notification mechanism, it should be clear that events are at its heart. We'll want to pre-crack the event data for the developer's convenience, of course. Every event will include the Sender parameter, as usual, and a Boolean value identifying whether the event was generated by an event. Some review of the API documentation reveals that we have five basic patterns of "data" parameters:
No extra parameters.
One DWORD.
One non-nil PIDL, which might represent a path.
Two non-nil PIDLs, which might represent paths.
"Generic" events that have two raw PIDLs, either of which might be nil.
The definitions of the procedural types representing these five event categories are shown in Figure 4. You may notice that PIDL parameters are represented as Pointer types, rather than PItemIDList types. This is because PItemIDList is defined in the ShlObj unit, which is not added automatically to form units when the component is dropped. This has the annoying quality of causing compiler errors when an event is assigned, unless unit ShlObj is manually added to the form's interface part uses clause.
TkbShellNotifySimpleEvent = procedure(Sender: TObject;
IsInterrupt: Boolean) of object;
TkbShellNotifyIndexEvent = procedure(Sender: TObject;
Index: LongInt; IsInterrupt: Boolean) of object;
TkbShellNotifyGeneralEvent = procedure(Sender: TObject;
PIDL: Pointer; Path: TFileName;
IsInterrupt: Boolean) of object;
TkbShellNotifyRenameEvent = procedure(Sender: TObject;
OldPIDL: Pointer; OldPath: TFileName; NewPIDL: Pointer;
NewPath: TFileName; IsInterrupt: Boolean) of object;
TkbShellNotifyGenericEvent = procedure(Sender: TObject;
EventType: TkbShellNotifyEventType; PIDL1: Pointer;
PIDL2: Pointer; IsInterrupt: Boolean) of object;
Figure 4: Event procedure type definitions.
Deciding what the events will be was fairly simple. There should be a component event for each possible shell event. In addition, we'll define events to encapsulate the three "collective" events defined by the Windows API (for those hardy souls who like working with raw Windows data). The list of events and their definitions are found in Figure 5.
property OnAnyEvent: TkbShellNotifyGenericEvent
property OnDiskEvent: TkbShellNotifyGenericEvent
property OnGlobalEvent: TkbShellNotifyGenericEvent
property OnAssociationChanged: TkbShellNotifySimpleEvent
property OnAttributesChanged: TkbShellNotifyGeneralEvent
property OnDriveAdded: TkbShellNotifyGeneralEvent
property OnDriveRemoved: TkbShellNotifyGeneralEvent
property OnExtendedEvent: TkbShellNotifyGenericEvent
property OnFolderCreated: TkbShellNotifyGeneralEvent
property OnFolderDeleted: TkbShellNotifyGeneralEvent
property OnFolderRenamed: TkbShellNotifyRenameEvent
property OnFolderUpdated: TkbShellNotifyGeneralEvent
property OnFreespaceChanged: TkbShellNotifyGeneralEvent
property OnImageUpdated: TkbShellNotifyIndexEvent
property OnItemCreated: TkbShellNotifyGeneralEvent
property OnItemDeleted: TkbShellNotifyGeneralEvent
property OnItemRenamed: TkbShellNotifyRenameEvent
property OnItemUpdated: TkbShellNotifyGeneralEvent
property OnMediaInserted: TkbShellNotifyGeneralEvent
property OnMediaRemoved: TkbShellNotifyGeneralEvent
property OnNetworkDriveAdded: TkbShellNotifyGeneralEvent
property OnResourceShared: TkbShellNotifyGeneralEvent
property OnResourceUnshared: TkbShellNotifyGeneralEvent
property OnServerDisconnected: TkbShellNotifyGeneralEvent
Figure 5: Design-time published events.
The last area of the public interface to consider, the run-time methods, lends a few candidates for consideration. It's handy to have auxiliary methods for setting the Active property on and off. Also, virtually any component that handles system data needs some sort of reset capability. These methods are shown here:
procedure Activate;
procedure Deactivate;
procedure Reset;
There are, of course, many details to implementing a component that go beyond the core functions that the component encapsulates. As there are many other excellent references that cover the details of implementing custom components in Delphi, this article will not cover them here. Instead, it will concentrate only on those pieces of the component's implementation that directly relate to the specific problem of shell notifications.
The implementation of shell notifications revolves around the calls to SHChangeNotifyRegister and SHChangeNotifyDeregister. Everything we do in this component will be in support of those function calls. Let's outline how the public properties and methods relate to those functions.
The property most directly linked to the calls is Active. Setting this property to True will cause SHChangeNotifyRegister to be invoked with parameters governed by the other four published properties. As you might guess, setting it to False will cause SHChangeNotifyDeregister to terminate the notifications.
The mechanics of calling the API functions are delegated to two private methods, StartWatching and StopWatching, which will be discussed later. Meanwhile, here's a code snippet from the private property writer method, SetActive, which illustrates the logic at work:
// Do nothing if the new value is the same as the old.
if (NewValue <> Self.FActive) then
{ If we're activating, start watching. }
if (NewValue) then
Self.StartWatching;
else { If we're deactivating, stop watching. }
Self.StopWatching;
The other four properties (besides Active) can substantially change the notification model, if they are modified. There is no way to update these "on the fly" when Active is True, so it's necessary to "reset" the shell notification if these properties are changed while Active is True. This is the purpose of the Reset method. It simply calls the private methods StopWatching and StartWatching, if the component is Active. This has the effect of stopping the notifications with SHChangeNotifyDeregister, and calling SHChangeNotifyRegister with the current values of the component's properties. The property writer methods for these four properties call the Reset method after updating the component's internal data member corresponding to that property.
A Window All Our Own
Next, we consider how to manage the shell notification messages, which will result from the call to SHChangeNotifyRegister. A window must be available to process all the notification messages generated by the shell. How best to provide this window? We could use the application's main form, but that would require hooking that form's window procedure, a messy undertaking at best. It seems simplest to generate our own invisible window, whose sole purpose is to handle those notification messages, and over whose destiny we have absolute control. This step eliminates problems with conflicting messages and clashing hooks.
The Delphi VCL thoughtfully provides a couple of functions to facilitate this scheme. They are AllocateHWnd and DeallocateHWnd, found in the Forms unit. AllocateHWnd's entire purpose in life is to generate a handle to an invisible window, using a window message-handling procedure you provide. Exactly what we need! Now, in the component's constructor, we can get and store a handle to a window that has nothing better to do than manage our icon's messages. Here's a call to this handy method:
{ Allocate a message-handling window. }
Self.FMessageWindow := AllocateHWnd(Self.HandleMessage);
As you can see, the call is trivial. What's important is the message-handling procedure we passed. This is where the notification messages from the shell are received, and where we have the opportunity to dispatch them. AllocateHWnd takes a single argument of TWndMethod, a class-member procedure that takes a single argument of type TMessage. It's up to us to provide that procedure. Figure 6 shows our component's message-handling procedure, to give you the idea.
procedure TkbShellNotify.HandleMessage(
var TheMessage: TMessage);
var
PIDLs: PTwoPIDLArray;
EventId: DWORD;
Lock: THandle;
begin
{ Handle only the WM_SHELLNOTIFY message. }
if (TheMessage.Msg = WM_SHELLNOTIFY) then
begin
{ If this is NT, use the memory map to access the
PIDL data. }
if SysUtils.Win32Platform=VER_PLATFORM_WIN32_NT then
begin
Lock := SHChangeNotification_Lock(THandle(
TheMessage.wParam), DWORD(TheMessage.lParam),
PIDLs, EventId);
if (Lock <> 0) then
try
Self.ProcessEvent(EventId, PIDLs);
finally
SHChangeNotification_Unlock(Lock);
end;
end
{ If this is not NT, access the PIDL data directly. }
else
begin
EventId := DWORD(TheMessage.lParam);
PIDLs := PTwoPIDLArray(TheMessage.wParam);
Self.ProcessEvent(EventID, PIDLs);
end;
end { if }
{ Call the default windows proc for any other message. }
else
TheMessage.Result := DefWindowProc(Self.FMessageWindow,
TheMessage.Msg,TheMessage.wParam,TheMessage.lParam);
end;
Figure 6: An example message-handling method.
As we discussed earlier, we first check the message identifier to verify that this incoming message is a WM_SHELLNOTIFY message. We ignore all others, and send them to default handling, because none of the miscellaneous messages typically broadcast to every window in the system interest us. WM_SHELLNOTIFY, if you were wondering, is a constant we define ourselves, not one provided by Windows. Setting it equal to WM_USER is the easiest thing to do, and perfectly safe because we use this window only for handling shell notification messages.
Once we're satisfied this is indeed a notification message, we decode the LParam and WParam values to determine which event the message is relating to us, and the associated PIDL data. Notice how we extract the data using the SHChangeNotification_Lock API call if the component is running on NT. You'll also see that the actual detailed handling of the message is delegated to another private method, ProcessEvent, to avoid repeating that rather substantial bit of code.
The private method ProcessEvent (see Figure 7) is where we crack the PIDLs out of the TTwoPIDLArray record, convert them to file-system paths if possible, and dispatch them to the appropriate event handler. The centerpiece of this method is a case statement that calls the event-handling method corresponding to the event type. These event-handling methods are necessary because events are often left unassigned by the developer, and calling these nil handlers directly would cause exception faults. There is one such method for each published event that accepts the raw PIDLs and paths, performs any additional processing that may be required to extract useful information, and calls the event handler if it has been assigned.
procedure TkbShellNotify.ProcessEvent(EventID: DWORD;
PIDLs: PTwoPIDLArray);
var
EventType: TkbShellNotifyEventType;
PIDL1: PItemIDList;
PIDL2: PItemIDList;
Path1: TFileName;
Path2: TFileName;
IsInterrupt: Boolean;
begin
{ Crack open the Two-PIDL array. }
PIDL1 := PIDLs.PIDL1;
PIDL2 := PIDLs.PIDL2;
{ Try to convert PIDLs to Paths }
Path1 := GetPathFromPIDL(PIDL1);
Path2 := GetPathFromPIDL(PIDL2);
{ Determine if event is interrupt-caused. }
IsInterrupt := Boolean(EventID and SHCNE_INTERRUPT);
{ Iterate through possible events & fire as appropriate.
This is necessary because event IDs are flags, and
there may be more than one in a particular message. }
for EventType := Low(TkbShellNotifyEventType) to
High(TkbShellNotifyEventType) do begin
{ Skip the "multi" event types.
They will be fired as needed below. }
if (EventType in [kbsnAnyEvent, kbsnDiskEvent,
kbsnGlobalEvent]) then
Continue;
{ If the current event type is flagged... }
if ((ShellNotifyEnumToConst(EventType) and
EventID) <> 0) then begin
{ Fire appropriate "multi" events for this event. }
Self.AnyEvent(EventType, PIDL1, PIDL2, IsInterrupt);
if ((ShellNotifyEnumToConst(kbsnGlobalEvent) and
ShellNotifyEnumToConst(EventType)) <> 0) then
Self.GlobalEvent(EventType, PIDL1,
PIDL2, IsInterrupt);
if ((ShellNotifyEnumToConst(kbsnDiskEvent) and
ShellNotifyEnumToConst(EventType)) <> 0) then
Self.DiskEvent(EventType,PIDL1,PIDL2,IsInterrupt);
{ Fire specific event. }
case (EventType) of
kbsnAssociationChanged:
Self.AssociationChanged(IsInterrupt);
{ Other event-handling methods with
appropriate parameters... }
kbsnServerDisconnected:
Self.ServerDisconnected(PIDL1,Path1,IsInterrupt);
end; { case }
end; { if }
end; { for }
end;
Figure 7: An abbreviated event-processing method.
The other wrinkle is the for loop, which iterates through the possible event types, comparing each to the EventID parameter using a logical and comparison to see if the matching event handler should be fired. This is necessary because the EventID parameter is actually a bitmap of flags, and as such, might include more than one event flag. This prohibits a simple equality comparison using the = operator.
The Heart of the Matter
Now, with all these supporting tasks worked out, we can finally get to the heart of the matter - the long-awaited call to SHChangeNotifyRegister. It might seem a bit anticlimactic, but this function is found in only one place throughout the entire component. This place, of course, is the StartWatching private method. Let's work our way through it, as shown in Figure 8.
procedure TkbShellNotify.StartWatching;
var
NotifyPathData: TNotifyRegister;
Flags: DWORD;
EventType: TkbShellNotifyEventType;
EventMask: DWORD;
begin
{ Initialize Flags. }
Flags := SHCNF_NO_PROXY;
if kbioAcceptInterrupts in Self.InterruptOptions then
Flags := Flags or SHCNF_ACCEPT_INTERRUPTS;
if kbioAcceptNonInterrupts in Self.InterruptOptions then
Flags := Flags or SHCNF_ACCEPT_NON_INTERRUPTS;
{ Initialize EventMask. }
EventMask := 0;
for EventType := Low(TkbShellNotifyEventType) to
High(TkbShellNotifyEventType) do
if (EventType in Self.HandledEvents) then
EventMask :=
EventMask or ShellNotifyEnumToConst(EventType);
{ Initialize Notification Path data. }
NotifyPathData.pidlPath := Self.RootPIDL;
NotifyPathData.bWatchSubtree := Self.WatchChildren;
{ Register for notification and store the handle. }
Self.FHandle := SHChangeNotifyRegister(
Self.FMessageWindow, Flags, EventMask, WM_SHELLNOTIFY,
1, NotifyPathData);
{ If registration failed, set Active to False. }
if (Self.Handle = 0) then
Self.Deactivate;
end;
Figure 8: The StartWatching private method.
First, we initialize the flags. We'll always specify SHCNF_NO_PROXY, because we're prepared to handle the message correctly on NT. We'll also set the SHCNF_ACCEPT_INTERRUPTS and SHCNF_ACCEPT_NON_INTERRUPTS flags, as determined by the value of the InterruptOptions property.
Next, we initialize the EventMask parameter to specify the shell events we're interested in monitoring. This is accomplished by iterating through all possible values of the TkbShellNotifyEventType enumerated type, and setting the corresponding flag bits using a logical or operation each time we find a value, which is set in the HandledEvents property.
The last parameter to set up is the folder path information. We simply fill the necessary TNotifyRegister record with the values found in the RootPIDL and WatchChildren properties.
Finally, we can make the call to SHChangeNotifyRegister and save the handle returned by that function.
The StopWatching method is quite simple by comparison. It merely calls the SHChangeNotifyDeregister function, giving it the handle saved from the call to SHChangeNotifyRegister, and resets the internal data member containing the handle value to zero. Here's the entire method:
procedure TkbShellNotify.StopWatching;
begin
{ Deregister the notification handle
and set the handle to nil. }
SHChangeNotifyDeregister(Self.FHandle);
Self.FHandle := 0;
end;
The end product is a component that makes shell notifications almost trivial to access. Go ahead. Try it out with the sample event monitor provided with this article's companion source code. Feel free to forget all that Windows API complexity.
The Bottom Line
The Shell Notifications API gives you powerful insight into the internal workings of the Win32 shell. Your applications can use it to react to all sorts of important system events, giving you that extra edge over the competition. The TkbShellNotify component gives you this API in a convenient and simple package, making these services almost trivial to exploit. Now, get out there and write something amazing!
All source referenced in this article is available for download.
Kevin Bluck is an independent contractor specializing in Delphi development. He lives in Sacramento, CA with his lovely wife Natasha. He spends his spare time chasing weather balloons and rockets as a member of JP Aerospace (http://www.jpaerospace.com), a group striving to be the first amateur organization to send a rocket into space. Kevin can be reached via e-mail at mailto:[email protected].
James Holderness is a software developer specializing in C/C++ Windows applications. He also runs a Web site on undocumented functions in Windows 95 (http://www.geocities.com/SiliconValley/4942). He is currently working for FerretSoft LLC (http://www.ferretsoft.com), where he helps to create the Ferret line of Internet search tools. James can be reached via e-mail at mailto:[email protected] or mailto:[email protected].
--------------------------------------------------------------------------------
Informant Communications Group, Inc.
10519 E. Stockton Blvd., Suite 100
Elk Grove, CA 95624-9703
Phone: (916) 686-6610 • Fax: (916) 686-8497
Copyright (c) 2000 Informant Communications Group. All Rights Reserved. • Site Use Agreement • Send feedback to the Webmaster • Important information about privacy