Using IShellLink
Shell links seem kind of magical: You right-click on the desktop, create a new shortcut, and something happens that causes an icon to appear on the desktop. That something is actually a pretty mundane occurrence once you know what's going on. A shell link is actually just a file with an .LNK extension that lives in some particular directory. When Windows starts, it looks in certain directories for LNK files, which represent links residing in different shell folders. These shell folders (or special folders) include items such as Network Neighborhood, Send To, Startup, the Desktop, and so on. The shell stores the link/folder correspondence in the System Registrythey're found mostly under the following key if you're interested in looking:
HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer [ccc]\Shell Folders
Creating a shell link in a special folder is just a matter of placing a link file in a particular directory. Rather than spelunking through the Registry, you can use SHGetSpecialFolderPath() to obtain the directory path for the various special folders. This method is defined as follows:
function SHGetSpecialFolderPath(hwndOwner: HWND; lpszPath: PChar; nFolder: Integer; fCreate: BOOL): BOOL; stdcall;
hwndOwner contains the handle of a window that will serve as the owner to any dialogs the function might invoke.
lpszPath is a pointer to a buffer to receive the path. This buffer must be at least MAX_PATH characters in length.
nFolder identifies the special folder for which you want to obtain the path.
fCreate indicates whether a folder should be created if it doesn't exist.
Creating a Shell Link
The IShellLink interface is an encapsulation of a shell link object, but it has no concept of how to read or write itself to a file on disk. However, implementers of the IShellLink interface are also required to support the IPersistFile interface in order to provide file access. IPersistFile is an interface that provides methods for reading and writing to and from disk, and it's defined as follows:
type IPersistFile = interface(IPersist) ['{0000010B-0000-0000-C000-000000000046}'] function IsDirty: HResult; stdcall; function Load(pszFileName: POleStr; dwMode: Longint): HResult; stdcall; function Save(pszFileName: POleStr; fRemember: BOOL): HResult; stdcall; function SaveCompleted(pszFileName: POleStr): HResult; stdcall; function GetCurFile(out pszFileName: POleStr): HResult; stdcall; end;
Because the class that implements IShellLink is also required to implement IPersistFile, you can QueryInterface the IShellLink instance for an IPersistFile instance using the as operator, as shown here:
var SL: IShellLink; PF: IPersistFile; begin OleCheck(CoCreateInstance(CLSID_ShellLink, nil, CLSCTX_INPROC_SERVER, IShellLink, SL)); PF := SL as IPersistFile; // use PF and SL end;
Using COM interface objects works the same as using normal Object Pascal objects. The following code, for example, creates a desktop shell link to the Notepad application:
procedure MakeNotepad; const // NOTE: Assumed location for Notepad: AppName = 'c:\windows\notepad.exe'; var SL: IShellLink; PF: IPersistFile; LnkName: WideString; begin OleCheck(CoCreateInstance(CLSID_ShellLink, nil, CLSCTX_INPROC_SERVER, IShellLink, SL)); { IShellLink implementers are required to implement IPersistFile } PF := SL as IPersistFile; OleCheck(SL.SetPath(PChar(AppName))); // set link path to proper file { create a path location and filename for link file } LnkName := GetFolderLocation('Desktop') + '\' + ChangeFileExt(ExtractFileName(AppName), '.lnk'); PF.Save(PWideChar(LnkName), True); // save link file end;
In this procedure, the SetPath() method of IShellLink is used to point the link to an executable file or document (Notepad in this case). Then a path and filename for the link is created using the path returned by GetFolderLocation('Desktop') and by using the ChangeFileExt() function to change the extension of Notepad from .EXE to .LNK. This new filename is stored in LnkName. After that, the Save() method saves the link to a disk file. When the procedure terminates and the SL and PF interface instances fall out of scope, their respective references will be released.
Getting and Setting Link Information
As you can see from the definition of the IShellLink interface, it contains a number of GetXXX() and SetXXX() methods that allow you to get and set different aspects of the shell link. Consider the following record declaration, which contains fields for each of the possible values that can be set or retrieved:
type TShellLinkInfo = record PathName: string; Arguments: string; Description: string; WorkingDirectory: string; IconLocation: string; IconIndex: Integer; ShowCmd: Integer; HotKey: Word; end;
Given this record, you can create functions that retrieve the settings of a given shell link to the record or that set a link's values to those indicated by the record's contents. Such functions are shown in Listing 1. WinShell.pas is a unit that contains the complete source for these functions.
Listing 1WinShell.pasUnit Containing Functions That Operate on Shell Links
unit WinShell; interface uses SysUtils, Windows, Registry, ActiveX, ShlObj; type EShellOleError = class(Exception); TShellLinkInfo = record PathName: string; Arguments: string; Description: string; WorkingDirectory: string; IconLocation: string; IconIndex: integer; ShowCmd: integer; HotKey: word; end; TSpecialFolderInfo = record Name: string; ID: Integer; end; const SpecialFolders: array[0..29] of TSpecialFolderInfo = ( (Name: 'Alt Startup'; ID: CSIDL_ALTSTARTUP), (Name: 'Application Data'; ID: CSIDL_APPDATA), (Name: 'Recycle Bin'; ID: CSIDL_BITBUCKET), (Name: 'Common Alt Startup'; ID: CSIDL_COMMON_ALTSTARTUP), (Name: 'Common Desktop'; ID: CSIDL_COMMON_DESKTOPDIRECTORY), (Name: 'Common Favorites'; ID: CSIDL_COMMON_FAVORITES), (Name: 'Common Programs'; ID: CSIDL_COMMON_PROGRAMS), (Name: 'Common Start Menu'; ID: CSIDL_COMMON_STARTMENU), (Name: 'Common Startup'; ID: CSIDL_COMMON_STARTUP), (Name: 'Controls'; ID: CSIDL_CONTROLS), (Name: 'Cookies'; ID: CSIDL_COOKIES), (Name: 'Desktop'; ID: CSIDL_DESKTOP), (Name: 'Desktop Directory'; ID: CSIDL_DESKTOPDIRECTORY), (Name: 'Drives'; ID: CSIDL_DRIVES), (Name: 'Favorites'; ID: CSIDL_FAVORITES), (Name: 'Fonts'; ID: CSIDL_FONTS), (Name: 'History'; ID: CSIDL_HISTORY), (Name: 'Internet'; ID: CSIDL_INTERNET), (Name: 'Internet Cache'; ID: CSIDL_INTERNET_CACHE), (Name: 'Network Neighborhood'; ID: CSIDL_NETHOOD), (Name: 'Network Top'; ID: CSIDL_NETWORK), (Name: 'Personal'; ID: CSIDL_PERSONAL), (Name: 'Printers'; ID: CSIDL_PRINTERS), (Name: 'Printer Links'; ID: CSIDL_PRINTHOOD), (Name: 'Programs'; ID: CSIDL_PROGRAMS), (Name: 'Recent Documents'; ID: CSIDL_RECENT), (Name: 'Send To'; ID: CSIDL_SENDTO), (Name: 'Start Menu'; ID: CSIDL_STARTMENU), (Name: 'Startup'; ID: CSIDL_STARTUP), (Name: 'Templates'; ID: CSIDL_TEMPLATES)); function CreateShellLink(const AppName, Desc: string; Dest: Integer): string; function GetSpecialFolderPath(Folder: Integer; CanCreate: Boolean): string; procedure GetShellLinkInfo(const LinkFile: WideString; var SLI: TShellLinkInfo); procedure SetShellLinkInfo(const LinkFile: WideString; const SLI: TShellLinkInfo); implementation uses ComObj; function GetSpecialFolderPath(Folder: Integer; CanCreate: Boolean): string; var FilePath: array[0..MAX_PATH] of char; begin { Get path of selected location } SHGetSpecialFolderPathW(0, FilePath, Folder, CanCreate); Result := FilePath; end; function CreateShellLink(const AppName, Desc: string; Dest: Integer): string; { Creates a shell link for application or document specified in } { AppName with description Desc. Link will be located in folder } { specified by Dest, which is one of the string constants shown } { at the top of this unit. Returns the full path name of the } { link file. } var SL: IShellLink; PF: IPersistFile; LnkName: WideString; begin OleCheck(CoCreateInstance(CLSID_ShellLink, nil, CLSCTX_INPROC_SERVER, IShellLink, SL)); { The IShellLink implementer must also support the IPersistFile } { interface. Get an interface pointer to it. } PF := SL as IPersistFile; OleCheck(SL.SetPath(PChar(AppName))); // set link path to proper file if Desc <> '' then OleCheck(SL.SetDescription(PChar(Desc))); // set description { create a path location and filename for link file } LnkName := GetSpecialFolderPath(Dest, True) + '\' + ChangeFileExt(AppName, 'lnk'); PF.Save(PWideChar(LnkName), True); // save link file Result := LnkName; end; procedure GetShellLinkInfo(const LinkFile: WideString; var SLI: TShellLinkInfo); { Retrieves information on an existing shell link } var SL: IShellLink; PF: IPersistFile; FindData: TWin32FindData; AStr: array[0..MAX_PATH] of char; begin OleCheck(CoCreateInstance(CLSID_ShellLink, nil, CLSCTX_INPROC_SERVER, IShellLink, SL)); { The IShellLink implementer must also support the IPersistFile } { interface. Get an interface pointer to it. } PF := SL as IPersistFile; { Load file into IPersistFile object } OleCheck(PF.Load(PWideChar(LinkFile), STGM_READ)); { Resolve the link by calling the Resolve interface function. } OleCheck(SL.Resolve(0, SLR_ANY_MATCH or SLR_NO_UI)); { Get all the info! } with SLI do begin OleCheck(SL.GetPath(AStr, MAX_PATH, FindData, SLGP_SHORTPATH)); PathName := AStr; OleCheck(SL.GetArguments(AStr, MAX_PATH)); Arguments := AStr; OleCheck(SL.GetDescription(AStr, MAX_PATH)); Description := AStr; OleCheck(SL.GetWorkingDirectory(AStr, MAX_PATH)); WorkingDirectory := AStr; OleCheck(SL.GetIconLocation(AStr, MAX_PATH, IconIndex)); IconLocation := AStr; OleCheck(SL.GetShowCmd(ShowCmd)); OleCheck(SL.GetHotKey(HotKey)); end; end; procedure SetShellLinkInfo(const LinkFile: WideString; const SLI: TShellLinkInfo); { Sets information for an existing shell link } var SL: IShellLink; PF: IPersistFile; begin OleCheck(CoCreateInstance(CLSID_ShellLink, nil, CLSCTX_INPROC_SERVER, IShellLink, SL)); { The IShellLink implementer must also support the IPersistFile } { interface. Get an interface pointer to it. } PF := SL as IPersistFile; { Load file into IPersistFile object } OleCheck(PF.Load(PWideChar(LinkFile), STGM_SHARE_DENY_WRITE)); { Resolve the link by calling the Resolve interface function. } OleCheck(SL.Resolve(0, SLR_ANY_MATCH or SLR_UPDATE or SLR_NO_UI)); { Set all the info! } with SLI, SL do begin OleCheck(SetPath(PChar(PathName))); OleCheck(SetArguments(PChar(Arguments))); OleCheck(SetDescription(PChar(Description))); OleCheck(SetWorkingDirectory(PChar(WorkingDirectory))); OleCheck(SetIconLocation(PChar(IconLocation), IconIndex)); OleCheck(SetShowCmd(ShowCmd)); OleCheck(SetHotKey(HotKey)); end; PF.Save(PWideChar(LinkFile), True); // save file end; end.
One method of IShellLink that has yet to be explained is the Resolve() method. Resolve() should be called after the IPersistFile interface of IShellLink is used to load a link file. This searches the specified link file and fills the IShellLink object with values specified in the file.