1.0 Introduction
A shortcut or, in a programmer's jargon, a shell link is a very important object in the PC world. It is used extensively for easy access to “referenced object” (object like file, folder, drive, printer, etc. which the shortcut is pointing to) from anywhere within the local system or network. Installers also use it to reference programs they installed from the desktop. Please note, though several people have reported success in creating a shortcut that points to a URL (Internet Shortcut) using the techniques discussed in this article, however, this is not the ideal way to create an Internet Shortcut. To see how to make an Internet Shortcut, read my article (tip) "A shortcut for your website".
This article will show how to create a shortcut
- To a file base object (local and network files, folders, drives, etc.)
- To a non-file base object (printer as a case study)
2.0 Prerequisite
An insight into Win32, OLE COM programming and C++ is essential
3.0 Creating the shortcut
Shortcuts are simply binary files with .lnk extension which contains the location (path) to a referenced object and some other information needed to access and or describe the object.
Shortcut creation is achieved in Win32
by the use of OLE COM IShellLink
interface to create the shortcut and the IPersistFile
interface to save it to disk (Persistent Storage).
3.1 Using the code sample
Grab the source code attached to this article. These contains two files, a header file CreateShortCut.h and a source file “CreateShortCut.cpp”. Add the two to your project and #include
the CreateShortCut.h to your project
#include "CreateShortCut.h"
3.1.1 File base
To create shortcut to a file base object
CreateShortCut csc;
ih.CreateLinkFileBase("C:\\somedirectory\\afile.exe", CSC_DESKTOP, "The Comment", MAKEWORD(0x41, HOTKEYF_ALT + HOTKEYF_CONTROL), "-help");
ih.CreateLinkFileBase("C:\\somedirectory\\afile.exe", "C:\\a-non-existing-directory\\", "The Comment", MAKEWORD(0x41, HOTKEYF_ALT + HOTKEYF_CONTROL), "-help", TRUE);
csc.CreateLinkFileBase(L"C:\\somedirectory\\afile.exe",
L"C:/Users/somedirectory/Desktop", L"The comment to this file",
MAKEWORD(0x41, HOTKEYF_ALT + HOTKEYF_CONTROL), L"-help");
csc.CreateLinkFileBase("C:\\somedirectory\\afile.exe",
"C:/Users/somedirectory/Desktop", "The comment to this file",
MAKEWORD(0x41, HOTKEYF_ALT + HOTKEYF_CONTROL), "-help");
With this a shortcut named afile.lnk which points to C:\\somedirectory\\afile.exe will be created within the directory pointed to by the pathToLink parameter which has “The comment to this file” has it comment and Ctrl + Alt + A has it shortcut key.
The prototype for CreateLinkFileBase is ::CreateLinkFileBase(LPCWSTR pathToObj, LPCWSTR pathToLink, LPCWSTR description, WORD hotkey, LPCWSTR cmdLine, BOOL forceCreate = FALSE);
for the Wide Character Version and ::CreateLinkFileBase(LPCSTR pathToObj, LPCSTR pathToLink, LPCSTR description, WORD hotkey, LPCSTR cmdLine
, BOOL forceCreate = FALSE)
for the ANSI version
pathToObj
: a null terminated string (wide or ANSI) that contains the path to the referenced object. It must be an absolute path. This could be a path to a file e.g "C:/somedirectory/afile.exe"
, a directory e.g "C:/somedirectory/"
, a drive e.g., "c:/"
, a network file e.g., "\\\\computername\\directory\\file.txt"
, a network folder e.g., "\\\\computername\\directory\\"
. Important: The path to a network folder must always end with a double backlash ”\\\\computername\\directory\\”. pathToLink
: a null terminated string (wide or ANSI) or the CSC_*
constant that contains the path to the directory the shortcut will be saved to. The ending director separator can be omitted i.e. "c:/directory"
and "c:/directory/"
are both correct.
The CSC_*
constants include:
CSC_DESKTOP
- path to the file system directory used to physically store file objects on the desktop
CSC_ALL_DESKTOP
- path to the file system directory that contains files and folders that appear on the desktop for all users
CSC_PROGRAMFILES
- path to the Program Files folder
CSC_PROGRAMFILESX86
- path to the Program Files(x86) folder (on a 64bit system)
CSC_STARTUP
- path to the file system directory that corresponds to the user's Startup program group
CSC_SENDTO
- path to the file system directory that contains Send To menu items
CSC_WINDOWS
- path to the Windows directory
CSC_SYSTEM32
- path to the System32 directory
CSC_STARTMENU
- path to the file system directory that contains Start menu items
description
: a null terminated string (wide or ANSI) that contains the description of the referenced object. This text appears in the comment of the link file and the tooltip when the user mouseover the shortcut. This can be NULL. hotkey
: this is a WORD. It represents the key combination to activate the shortcut. For more on how to MAKEWORD
for hotkey, HOTKEYF_ALT
is for the ALT key, HOTKEYF_CONTROL
is for the CTRL key and HOTKEYF_SHIFT
for the SHIFT key, 0x41 is A, 0x42 is B, ... This can be NULL
. cmdLine
: a null terminated string (wide or ANSI) that contains the command line arguments to be inputted to the referenced object e.g., "-help"
. This can be NULL
.forceCreate
: this is an optional parameter which default to FALSE
. If set to TRUE
, the directory entered into pathToLink
will be created if it does not exist.
3.1.2 Non-File Base
Shortcut can also be created for non-file base object. The installed printers are chosen as a case study. To create a shortcut to the printer named "Fax"
CreateShortCut csc;
ih.CreateLinkFileBase("fax", CSC_DESKTOP, "The Printer named Fax", MAKEWORD(0x43, HOTKEYF_ALT + HOTKEYF_CONTROL));
ih.CreateLinkFileBase("fax", "
C:\\a-non-existing-directory\\
", "The Printer named Fax", MAKEWORD(0x43, HOTKEYF_ALT + HOTKEYF_CONTROL), TRUE);
csc.CreateLinkToPrinter(L"fax",
L"C:/somedirectory/", L"", MAKEWORD(0x43, HOTKEYF_ALT + HOTKEYF_CONTROL));
csc.CreateLinkToPrinter("fax", "C:/somedirectory/",
"", MAKEWORD(0x43, HOTKEYF_ALT + HOTKEYF_CONTROL));
The prototype for the CreateLinkToPrinter
method is ::CreateLinkToPrinter(LPCWSTR printerName, LPCWSTR pathToLink, LPCWSTR description, WORD hotkey, BOOL forceCreate = FALSE);
and ::CreateLinkToPrinter(LPCSTR printerName, LPCSTR pathToLink, LPCSTR description, WORD hotkey, BOOL forceCreate = FALSE);
for ANSI version
printerName
: a null terminated string (wide or ANSI) that contains the name of the installed printer. This is case insensitive so "Fax" and "fax" are the same. pathToLink
: a null terminated string (wide or ANSI) or the CSC_*
constant that contains the path to the directory the shortcut will be saved to. The ending director separator can be omitted i.e. "c:/directory"
and "c:/directory/"
are both correct.
The CSC_*
constants include:
CSC_DESKTOP
- path to the file system directory used to physically store file objects on the desktop
CSC_ALL_DESKTOP
- path to the file system directory that contains files and folders that appear on the desktop for all users
CSC_PROGRAMFILES
- path to the Program Files folder
CSC_PROGRAMFILESX86
- path to the Program Files(x86) folder (on a 64bit system)
CSC_STARTUP
- path to the file system directory that corresponds to the user's Startup program group
CSC_SENDTO
- path to the file system directory that contains Send To menu items
CSC_WINDOWS
- path to the Windows directory
CSC_SYSTEM32
- path to the System32 directory
CSC_STARTMENU
- path to the file system directory that contains Start menu items.
description
: a null terminated string (wide or ANSI) that contains the description of the referenced object. This text appears in the comment of the link file and the tooltip when the user mouseover the shortcut. This can be NULL
. hotkey
: this is a WORD
. It represents the key combination to activate the shortcut. For more on how to MAKEWORD
for hotkey, HOTKEYF_ALT
is for the ALT key, HOTKEYF_CONTROL
is for the CTRL key and HOTKEYF_SHIFT
for the SHIFT key, 0x41 is A, 0x42 is B,... This can be NULL.forceCreate
: this is an optional parameter which default to FALSE
. If set to TRUE
, the directory entered into pathToLink
will be created if it does not exist.
4.0 Code Explanation
For those that wants to know how the whole class works
4.1 File base object
The file base object shortcut creation is handled by the overloaded ::CreateLinkFileBase
method. The ANSI version simply convert it parameters into Wide Characters and call the Wide Character version with them. The concept is to firstly used the _wstat()
function to verify if the object pathToObj
is a folder or a (file or drive) and CoCreateInstance()
is called based on the result.
According to MSDN documentation, CoCreateInstance()
should be called with CLSID_FolderShortCut
as the first parameter if the reference object is a folder but the result was not satisfactory to me. The shortcut was created alright but
- Windows Explorer shows the .lnk extension even when all extension is set to hidden
- When the shortcut is double clicked, it opened the folder it points to quite alright but the address bar in Windows explorer points to the shortcut location instead of the folder location
Leaving the first parameter of CoCreateInstance
as CLSID_ShellLink
against MSDN advice solved the two problems.
.
.
.
if(isFile)
hRes = CoCreateInstance(CLSID_ShellLink, NULL,
CLSCTX_INPROC_SERVER, IID_IShellLink, (LPVOID*)&psl);
else
{
hRes = CoCreateInstance(CLSID_ShellLink, NULL,
CLSCTX_INPROC_SERVER, IID_IShellLink, (LPVOID*)&psl);
}
.
.
.
A IShellLink
pointer is created and values are set accordingly and object queried for IPersistFile
interface that is used to save the shortcut to persistence storage.
BOOL isFile = FALSE;
struct _stat s;
if(_wstat(pathToObj, &s) == 0)
{
if( s.st_mode & S_IFDIR )
isFile = FALSE;
else if( s.st_mode & S_IFREG )
isFile = TRUE;
else
{
ReportError("Not file, device, or folder");
return 0;
}
}
else
{
ReportError("Path not found");
return 0;
}
memset(path, '\0', MAX_PATH);
_wmakepath_s(path, MAX_PATH, NULL, pathToLink, NULL, NULL);
if(forceCreate)
{
createDir(pathToLink);
}
HRESULT hRes;
IShellLink* psl;
if(isFile)
hRes = CoCreateInstance(CLSID_ShellLink, NULL, CLSCTX_INPROC_SERVER, IID_IShellLink, (LPVOID*)&psl);
else
{
hRes = CoCreateInstance(CLSID_ShellLink, NULL, CLSCTX_INPROC_SERVER, IID_IShellLink, (LPVOID*)&psl);
}
if(SUCCEEDED(hRes))
{
IPersistFile* ppf;
psl->SetPath(pathToObj);
psl->SetDescription(description);
psl->SetHotkey(hotkey);
psl->SetArguments(cmdLine);
hRes = psl->QueryInterface(IID_IPersistFile, (LPVOID*)&ppf);
if(SUCCEEDED(hRes))
{
wchar_t* fname = new wchar_t[MAX_PATH];
wchar_t* drive = new wchar_t[MAX_PATH];
_wsplitpath(pathToObj, drive, NULL, fname,NULL);
if(wcscmp(fname,L"") != 0) {
wcscat_s(path, MAX_PATH, fname);
}
else if(wcscmp(drive,L"") != 0) {
wchar_t d[2];
d[0] = drive[0];
d[1] = '\0';
wcscat_s(path, MAX_PATH, d);
}
delete [] drive;
delete [] fname;
wcscat_s(path, MAX_PATH, L".lnk");
ppf->Save(path, TRUE);
ppf->Release();
}
psl->Release();
}
else
{
ReportError("CoCreateInstance Error");
return 0;
}
return 1;
4.2 Non file base
The non file base shortcut creation is handled by the overloaded ::CreateLinkToPrinter
method. SHGetSpecialFolderLocation()
is used to get the location of installed printers by setting it second parameter to CSIDL_PRINTERS
. An IShellFolder
object is BindToObject()
to the LPITEMIDLIST
set by the SHGetSpecialFolderLocation()
function.
The IShellFolder
object is enumerated and each name of installed printer (which is gotten from the GetDisplayNameOf()
of the IShellFolder
) is compared with the input name (printerName
) and if they match, the LPITEMIDLIST
of the printer is appended (using the ::Append method) to the LPITEMIDLIST
of the printer folder.
Appending the two <code>LPITEMIDLIST
is very essential because the SetIDList
of IShellLink
interface requires an absolute LPITEMIDLIST
but the one gotten from the enumeration of the IShellFolder
object is relative to the printer folder.
According to MSDN, to create a shortcut to a non-file object, set theSetIDList
to the LPITEMIDLIST
of the object instead of setting the SetPath()
in case of file base object and all other things remain the same.
HRESULT hr = S_OK;
ULONG celtFetched;
LPITEMIDLIST pidlItems = NULL;
LPITEMIDLIST netItemIdLst = NULL;
IShellFolder* pPrinterFolder = NULL;
IEnumIDList* pEnumIds = NULL;
IShellFolder* pDesktopFolder = NULL;
LPITEMIDLIST full_pid;
hr = SHGetMalloc(&pMalloc);
hr = SHGetDesktopFolder( &pDesktopFolder );
hr = SHGetSpecialFolderLocation( NULL, CSIDL_PRINTERS, &netItemIdLst );
hr = pDesktopFolder->BindToObject( netItemIdLst, NULL, IID_IShellFolder, (void **)&pPrinterFolder );
memset(path, '\0', MAX_PATH);
_wmakepath_s(path, MAX_PATH, NULL, pathToLink, NULL, NULL);
if(forceCreate)
{
createDir(pathToLink);
}
if(SUCCEEDED(hr))
{
hr = pPrinterFolder->EnumObjects( NULL, SHCONTF_NONFOLDERS, &pEnumIds );
if(SUCCEEDED(hr))
{
STRRET strDisplayName;
while((hr = pEnumIds->Next( 1, &pidlItems, &celtFetched)) == S_OK && celtFetched > 0)
{
hr = pPrinterFolder->GetDisplayNameOf(pidlItems, SHGDN_NORMAL, &strDisplayName);
if(SUCCEEDED(hr))
{
if(_wcsicmp(strDisplayName.pOleStr, printerName) == 0)
{
wcscat_s(path, MAX_PATH, strDisplayName.pOleStr);
wcscat_s(path, MAX_PATH, L".lnk");
full_pid=Append(netItemIdLst, pidlItems);
CreateLink(full_pid, path, description, hotkey);
}
}
}
}
}
return 0;
5.0 Conclusion
The ::ReportError
is used to track error. It only print the error message to screen using printf()
, you can rewrite it to suit your need.
6.0 History
19 July, 2013:
- Location constants (
CSC_*
) were introduced. - Argument
BOOL forceCreate
(which default to FALSE) was introduced
11 July, 2013:
- Full ANSI and Wide Character versions included.
- Early unloading of some DLLs was detected when a single object of the class was used more than once. This will cause application to stop working with debug information "An invalid exception handler routine has been detected (parameters: 0x00000003)". To fix this
CoInitializeEx
was moved to the class constructor and CoUninitialize
to the destructor.
22 May, 2013:
- Initial project.