The Other Way To Use OLE - Part 2

by Jim Kane

Published 1999-09-07    Printer-friendly version

Read Part 1

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:

  1. Initialize COM one time per application (CoInitialize)
  2. Get my interface pointers (CoCreateInstance and QuerryInterface)
  3. Use my interface pointers to create the shortcut (IShellLink Methods)
  4. Save the shortcut to disk (IPersistFile Methods)
  5. Release any interface pointers I got. (Release method of respective interfaces)
  6. Uninitialize COM one time per application (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:

Listing 1. Code to initialize/uninitialize COM.
  Module('API')
    CoInitialize(long AlwaysNull=0),Pascal,Long,proc
    CoUnInitialize(),pascal
    CoCreateInstance(long AddrClsid, long ClsContext,cr.gif (850 bytes)
      long ServerType,long addrIID, *long lpVtable)cr.gif (850 bytes)
      ,pascal,long,proc
    MultiByteToWideChar(ulong codepage, ulong dwFlags,cr.gif (850 bytes)
      *Cstring MBS,Long LenMBS, long addrWStr, long lenWideStr)cr.gif (850 bytes)
      ,pascal,long,proc,raw
  End
  Module('JJKOLE.CLW')
    InitOle(),byte
    KillOle()
  End
  Module('ICall.a')
    ICall0P(long lpVTable, long VtableOffset),long,pascal,cr.gif (850 bytes)
      proc,Name('ICall')
    ICall5P(long lpVTable, long VtableOffset, Long p1,long p2,cr.gif (850 bytes)
      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.

Listing 2. The IShellLink methods.
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.

Listing 3. The demo code.
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

Summary

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!

Download the source code

Read Part 3


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.

Printer-friendly version

Reader Comments

To add a comment to this article you must log in.

 
 

Search

 

Advanced Search
Topical Index

Related Articles

Subscribe to
ClarionMag

One year: $189

(includes all back issues since '99)

Renewals from $139

Two years: $289

Renewals from $239

More Info

Subscribe Now!

ClarionMag Blog

RSS Feeds

Updates via Email

Enter your Email


Powered by FeedBlitz

Quick Links