![]() |
|
Published 1999-09-07 Printer-friendly version
If given a task of creating or modifying a file created by another application or the operating system, most programmers would search for the file format and the true meaning of each and every byte in the file. Or at least those who are bit twiddlers from way back and workaholics who never see the sun would do this. If they are lucky enough to find the information, then the task is reduced to a series of API calls to open, read, modify and write the file. Heck, no programmer wants the weekend off anyway!
If the tried and true programmer is successful in reading and writing the file, he or she then either tries to get a new job before the program version changes or gets the rare honor of going back and redoing the code for the new version. At least that's what seems to happen to me.
On the other hand, if instead of getting down into the bits and bytes I could only find some existing code that would take my information in a version independent manner and write or modify the file for me, why I could go home, get reacquainted with my wife and maybe have some time left over for some two-stepping (the national dance of the proud country of Texas).
Well, I guess you could say I'm at the fork in the road. I
need to write a Windows shortcut (.lnk) file, and I'm not
familiar with the format. There are a few web sites that cracked
the file format for .lnk files, but the problem is that this format
may change in the future. There is, however, also an OLE interface
called IShellLink that understands the format and can
write the file for me plus relieves me of the burden of version
updates.
Fortunately, being a Clarion Magazine charter subscriber, I know there was an interesting article recently that described how to call an OLE interface. While I didn't really understand all that pointer to a pointer to a something or other stuff, it came with the code I need so why don't I just plug it in and see what else I have to do to get the job done once via OLE and let version upgrades take care of themselves.
Most COM programs follow a similar pattern. If I was really clever, I'd probably write a class that would encapsulate the OLE code I use in program after program, but I'm in a hurry so I'll just write it as straight code to get familiar with this new-fangled technology. Maybe next time I'll write it in a reusable fashion provided this COM stuff actually works and I don't need to figure out the magical byte pattern and do it the old way.
After some reading I find that I need to take the following steps to create a Windows shortcut:
CoInitialize)CoCreateInstance and
QuerryInterface)IShellLink Methods)IPersistFile
Methods)UnCoInitialize)Since steps 1 and 6 are application global - COM gets initialized once no matter how many procedures call an interface or use COM - I'll put that part in a separate module. I'll call those source files JJKOLE.INC and JJKOLE.CLW to avoid any potential name conflicts. The .inc file will contain prototypes and equates and the .clw file will contain code. Listing 1 shows the code for steps 1 and 6:
Module('API')
CoInitialize(long AlwaysNull=0),Pascal,Long,proc
CoUnInitialize(),pascal
CoCreateInstance(long AddrClsid, long ClsContext,
long ServerType,long addrIID, *long lpVtable)
,pascal,long,proc
MultiByteToWideChar(ulong codepage, ulong dwFlags,
*Cstring MBS,Long LenMBS, long addrWStr, long lenWideStr)
,pascal,long,proc,raw
End
Module('JJKOLE.CLW')
InitOle(),byte
KillOle()
End
Module('ICall.a')
ICall0P(long lpVTable, long VtableOffset),long,pascal,
proc,Name('ICall')
ICall5P(long lpVTable, long VtableOffset, Long p1,long p2,
long p3,long p4,long p5),long,pascal,proc,Name('ICall')
end
!Data and Code:
fInitComm byte(0)
!handy dandy equates for return code
Return:Benign equate(0)
Return:fatal equate(3)
Return:Notify equate(5)
InitOle Procedure()
hr Long,auto
code
If fInitComm then
return(Return:Benign)
end
hr = CoInitialize(0)
If hr < 0 then
fInitComm = False
Return(Return:Fatal)
else
fInitComm = True
Return(Return:Benign)
end
KillOle Procedure()
Code
If fInitComm then
CoUnInitialize()
end
Return
As you can see there is just one variable involved that prevents
the initialization code from being called more than once and
keeps the uninitialization code from being called if the init
wasn't done. With this structure, any COM procedure can call
InitOLE automatically and if InitOle has
already been called by another procedure, it just returns success.
Success is indicated by a return value of 0. Well that's good
news! You've survived the first battle with OLE. You could pop
(bring back fond memories of last month's lesson on
assembler?) the .inc into the Before Global Includes embed , the
prototypes into the global map and add the source module plus
Icall.a from last time to the external source section of the
project tree and compile. Use 32 bit only please.
To create the shortcut you need to use the
IShellLink object. When you want to call a COM object,
you need to know its name, which is called a CLSID
(Class ID). These names come from .h (header) files or COM viewer
programs. Why IShellLink would not do as a name
I'll never know, but all COM objects have a unique 128 bit
name that can be expressed in Clarion with this specific
structure:
!IShellLink
ClsId_ShellLink Group
data1 ulong(21401H)
data2 ushort(0)
data3 ushort(0)
data41 byte(0C0H)
data42 byte(0)
data43 byte(0)
data44 byte(0)
data45 byte(0)
data46 byte(0)
data47 byte(0)
data48 byte(46H)
end
(Next time you complain about the name your parents gave you consider the alternative possibilities if Bill had been your daddy!).
The IShellLink object contains two interfaces
called IShellLink and IPersistFile. Every
interface has a name in the same 128 bit format called an
IID or Interface ID. As hopefully you recall from our
last exciting adventure in assembler land, the other piece of
information you need is the offset down the Vtable for
each method. This can be gotten from a .h file or from .idl, or
.old files, or from COM Viewer utility programs (like OLEView which
is free from Microsoft's web
site) just by counting by 4 in hex down the list of methods.
For example, if you look in SHLOBJ.H at the IShellLink
definition, and copy down the names of the methods in the order
they are defined and then count by 4 you get Listing 2.
IShellLink_QuerryInterface Equate(0) IShellLink_AddRef Equate(4) IShellLink_Release Equate(8) IShellLink_GetPath Equate(0CH) IShellLink_GetIDList Equate(10H) IShellLink_SetIDList Equate(14H) IShellLink_GetDescrip Equate(18H) IShellLink_SetDescrip Equate(1CH) IShellLink_GetWorkDir Equate(20H) IShellLink_SetWorkDir Equate(24H) IShellLink_GetArguments Equate(28H) IShellLink_SetArguments Equate(2CH) IShellLink_GetHotKey Equate(30H) IShellLink_SetHotKey Equate(34H) IShellLink_GetShowCmd Equate(38H) IShellLink_SetShowCmd Equate(3CH) IShellLink_GetIconLoc Equate(40H) IShellLink_SetIconLoc Equate(44H) IShellLink_GetRelPath Equate(48H) IShellLink_Resolve Equate(4CH) IShellLink_SetPath Equate(50H)
This is all a bit tedious, but since you only need to count by
four and most people have five digits on an appendage it usually
works okay. In case you have not had enough, all the data required
for IShellLink is contained in the file IShellLK.inc
and all the non-IShellLink specific constants are in
JJKOLE.INC. Please have them memorized by noon tomorrow.
Okay, enough tedium! Time for some magic. I'll drop the
IShellLink CSLID and IID
into the API call CoCreateInstance and produce before
your eyes a ppVtable_IShellLink:
ppVtable_IShellLink LONG(0)
!Get the IShellLink ppVtable
hr = CoCreateInstance( Address(CLSID_ShellLink), NULL, |
CLSCTX_INPROC_SERVER, Address(IID_IShellLink),|
ppVtable_IShellLink)
if hr < 0 then...
Since the CLSID and IID are 128 bits
or 16 bytes and the stack is only 32 bits wide, these numbers are
passed by address. Hence the use of the Clarion
Address() function.
Almost all COM APIs and interfaces return a result code often
referred to as a Hresult. Here I named it
Hr. A value of less than zero is a bad thing. Zero or
greater is a good thing. If the result code returned is zero or
greater, then the long variable contains the keys to the kingdom of
COM: the exalted pointer to a pointer to a vtable, affectionately
known here as ppVtable_IShellLink. Most importantly
the Icall functions know how to use it.
For my next trick, I'll use the ppVtable to
call the QuerryInterface method and get a
ppVtable for IPersistFile. This method
requires two parameters: the IID of the interface I
want, and the address of the variable to store the
ppVtable in should the function call succeed. Seems
fair enough, so let's do it.
From IShellLk.clw...
hr = ICall2p(ppVtable_IShellLink, IShellLink_QuerryInterface, | Address(IID_IPersistFile), Address(ppVtable_IPersistFile)) If hr < 0 then....
I use my Icall function from last time with the
ppVtable and offset in my .inc file. Again the
hresult is returned and I test for errors. Boy is this
getting old fast! Wouldn't it be nice if I could write the
code to do the error checking and orderly close down once and use
it many times? Maybe DAB knows a way to write once and reuse.
I'll ask him before next time.
Now that all the interface pointers have been obtained they can
be used to call the "set" methods of IShellLink. The
data passed to my COM procedure is pretty mundane (vocabulary word
from my Garfield calendar) except for one call
SpecialLocation. Say you want your shortcut to appear
on your user's desktop. The directory structures in Windows
'95 and higher for different users and different desktops get
a little wild. Fortunately COM can refer to special places like the
desktop, recycle bin, and My Computer with other than a directory
name. That way, no matter how the user's computer is
configured or what the drive letter, the created shortcut gets to
the correct place. Once again you would think the simple string
'DESKTOP' would be an adequate identifier
but brother Bill says "unless I make it complicated every one would
be a programmer." So he set up a list of equates called
CSIDLs (not CLSIDs - try keeping that
straight) for all the special places. The equate for
CSIDL_DESKTOP happens to be 0. If a
SpecialLocation is passed you use the pair of
APIs:
!PIDL = pointer to a Identifier List hr = ShGetSpecialFolderLocation(0,SCS.SpecialLocation,lppidl) hr = ShGetPathFromIDList(lpPidl,szPath)
to convert the SpecialLocation code to first a
PIDL and then onto a CSTRING path called
szPath. Each step of the way you do some error
checking on the hrs returned. Although perhaps one of
the goofiest names in all of COM, PIDLs are fairly
important in the windows shell API. The Win95 and higher shells,
and Explorer in particular, show everything under the sun as if it
was a file in its tree structure. That includes "special places,"
printers, disk drives...you name it.
While files have a path and name to identify them other objects
may not. PIDLs were created to provide an identifier for everything
Explorer or the shell has to deal with. While it may be a bit of
overkill for files, it does serve a purpose as a generic
identifier. The functions listed above provide conversions from a
special location equate to PIDL and from
PIDL to filename. In Clarion, a pointer to a
IDL or a PIDL can simply be prototyped as
a LONG.
In any case, whether a CSTRING with the .lnk file
name was passed or whether a special location equate was passed, I
now have the path to store the .lnk at the location contained in
the variable szpath. In this case that location is the
desktop.
Unfortunately COM uses some data types that Clarion programmers may not yet be familiar with. Common COM data types include BStrings, Wide Strings, and Variants. I'll save a discussion of these data types for another day but the more you use COM the more you'll run into these data types. It sure would be nice to have some reusable code to convert from Clarion data types to COM data types and back....
For today though I'll just take the CSTRING
for the path szPath and convert it to a Wide String
called wszLinkFile. The CP_ACP means use
the ANSI code page. I have no idea what MB_Precomposed
means but it always works.
!Now convert to a wide string:
MultiByteToWideChar(CP_ACP, MB_PRECOMPOSED, szPath, -1, | Address(wszLinkFile), File:MaxFilePath);
Lastly I take my wide string, ppVtable for
IPersistFile, and the offset to the SAVE
method and save the lnk file who's path is stored in
wszLinkFIle to disk where it belongs. Then I test the
hr code. I can hear that two-step music starting up
now! Almost done.
!Save the shortcut to disk then cleanup and exit: hr = ICall2p(ppVtable_IPersistFile, IPersistFile_Save, | Address(wszLinkFile), True) ! The True above means clear the file is dirty flag. ! It would be false if this was a SaveAs if hr < 0 then...
If at any time during the procedure I get an hr
code of less than zero or at the end when I'm done
(Step 5) I need to call the Release method of any
interface for which I got a ppVtable. COM uses a
simple counting method to know when all users are done with an
object. Addref is automatically called for you when
you call CoCreateInstance or
QueryInterface to get the exalted
ppVtable, but when done you should be a nice person
and call Release so the COM code, usually in a DLL,
can be unloaded as soon as possible to conserve resources. In any
case, calling Release is easy. No parameters are
required (which kind of explains the use of CallOP);
just call with the ppVtable you are done with and the
Vtable offset for Release. The Vtable offset for
Release is always 8 since it is always the
3rd method. (All COM interfaces start with
QuerryInterface (+0), AddRef(+4) and
Release(+8).)
if ppVtable_IPersistFile then iCall0P(ppVtable_IPersistFile,IPersistFile_Release) end If ppVtable_IShellLink then iCall0P(ppVtable_IShellLink,IShellLink_Release) end
If I had stored my ppVtable in a Queue I could
really automate this cleanup stuff since xxx_Release
is always 8H...food for thought.
Well, there you have it. Load up the ShortCutStruct
defined in IShellLk.inc with the address of the string data and
other data you want for your .lnk file and call the
CreateShortCut function I just wrote. The reusable
once per app code is in jjkole.inc/clw and the IShellLink specific
code is in Ishelllk.inc/clw. The code to glue the demo all together
is in Shortcut.clw. I decided to use .prj file rather than an .app
file so I would not be Clarion version specific. Listing 3 shows
the demo code and instructions for putting it together. If you
prefer to use AppGen, the equivalent embed points are
presented.
Program
Map
! Add the needed prototypes in the global map
Include('JJKOLE.INC','OLEPROTOTYPES') !General OLE Prototypes
Include('IShellLk.inc','IShellLkProto') ! IShellLink prototypes
end
! Add the needed equates
! In the After Global Includes embed
Include('JJKOLE.INC', 'OLEEquates')
include('IShellLk.inc','IShellLkEquates')
! JJKOLE.Clw, and ICall.A have been added to the project
! These are required in all OLE apps
! IShellLk.Clw is also added - this has IShellLink Specific code
! for creating shortcuts
! Data for the test
ShortCutStruct like(ShortCutStructType)
Target cstring('C:\Windows\Notepad.exe')
Desc cstring('Description')
LinkFileName cstring('NewNotePadLink.LNK')
WorkingDir cstring('C:\Windows\Temp')
code
! Load up the structure with the data declared above and you're off
Clear(ShortCutStruct)
! Required Parameter pgm to run when shortcut clicked
ShortCutStruct.lpTarget = Address(Target)
! Optional descriptions - this text is NEVER
! seen by the user - It's a secret.
ShortCutStruct.lpDesc = Address(Desc)
! Required Parameter
ShortCutStruct.lpLinkFileName = Address(LinkFileName)
!Optional working directory
ShortCutStruct.lpWorkingDir = Address(WorkingDir)
! Required, Take icon from target file, iconindex=0
ShortCutStruct.lpIconPath = ShortCutStruct.lpTarget
! Let's put it on the desktop
ShortCutStruct.SpecialLocation = CSIDL_DESKTOP
If CreateShortCut(ShortCutStruct)
Message('Create Shortcut Failed')
else
Message('Create Shortcut Worked')
end
! Any number of other OLE actions - more shortcuts or what ever
KillOle()
Return
Rather than slugging out the file structure of a .lnk file and writing the .lnk using the brute force method, I invoked the operating system interface for creating a shortcut and let the operating system handle the details of the file.
The rules of COM (or OLE: the two are synonyms - thought
I'd wait to the end to tell you that) are fairly simple.
Initialize COM at the start and uninitialize when done. Get all the
interface pointers needed but for every one you collect when done
call its Release method at offset 8. Once you have
interface pointers, use the Icall code and offset to
the particular method you want to in order to call the interface
method of your dreams. Before a call to an interface method,
convert from Clarion data types to COM data types. After a call to
an interface method either ignore the hresult code if
its not important or test to see if its less than zero indicating
an error. On error or when done, clean up thy mess.
If you've gotten this far and are still awake, you've
come far. You've taken the seemingly insurmountable goal of
calling a COM interface and you've worked through the
methodology. All you need now is the raw ammo of
CLSIDs, IIDs, CSLIDs, and
Vtable offsets and some reusable code for error checking, cleanup,
initializing com, getting ppVtables, and converting
COM to Clarion datatypes back and forth. I'll save that for
another day. Time to go dance!
Jim Kane was not born any where near a log cabin. In fact he was born in New York City. After attending college at New York University, he went on to dental school at Harvard University. Troubled by vast numbers of unpaid bills, he accepted a U.S. Air Force Scholarship for dental school, and after graduating served in the US Air Force. He is now retired from the Air Force and writing software for ProDoc Inc., developer of legal document automation systems. In his spare time, he runs a computer consulting service, Productive Software Solutions. He is married to the former Jane Callahan of Cando, North Dakota. Jim and Jane have two children, Thomas and Amy.
Copyright © 1999-2008 by CoveComm Inc. All Rights Reserved. Reproduction in any form without the express written consent of CoveComm Inc., except as described in the subscription agreement, is prohibited.
Clarion Magazine ISSN 1718-9942
One year: $189
(includes all back issues since '99)
Renewals from $139
Two years: $289
Renewals from $239