![]() |
|
Published 1999-11-02 Printer-friendly version
When I was growing up my father often said to me he couldn't wait until I had a son of my own and he did to me some of the things that I did to my father. Well, my father got his wish and I have a son. Sometimes it seems my son's main goal in life is to help my father get even.
Not long ago I had the privilege of helping my son with a math assignment involving multiplication. While he was doing his work he kept longing to use a calculator and be done with it. As often as I explained the importance and virtue of doing it long hand so he learned the technique, the use of the calculator beckoned. As annoyed as I got at his insistence at using the calculator, I could not help but remember another little boy some 35 years earlier who thought memorizing multiplication tables was insane, and begged his father to let him use a calculator for math assignments.
Well, for much the same reason, in part one of this series I presented the assembler calling convention for calling OLE interfaces, and the Icall.A assembler procedure to call interfaces. Then in part two I outlined the needed API calls for calling interfaces. Now its time to "get out the calculator" and use the Icall.A assembler and some of the code and constants presented in part two to make a reusable OLE class for calling interfaces. While it isn't necessary to understand the material in parts one and two, it certainly would make your life much fuller and richer if you did!
To review from part two, the basic steps in calling an interface to create a Windows shortcut are:
CoInitialize or
CoInitializeEX)CoCreateInstance and
QuerryInterface)IShellLink methods)IPersistFile
methods)UnCoInitialize)Time to make a plan of attack for creating the class. As I read through the list, the data item that keeps popping up in the list is interface pointers. Managing these pointers and making sure they are released when I'm done is a constant headache with this kind of work. In addition, there are a few constants associated with each interface that are not much fun to track. Although not mentioned in the list specifically, in the code from part two there was a lot of work expended in detecting and reporting errors. Effectively managing the interface pointers would help with this.
Above and beyond the sexy interface pointers part of the
operation are step 1 and 6. Gee, they certainly look like simple
steps. Anyone thinking they are simple clearly forgot Microsoft is
involved! In part two, I used CoInitialize to
initialize the windows OLE subsystem. Actually the preferred API
according to Microsoft is
CoIntializeEX(0,ThreadingModel).
The two most common threading models are Apartment and
Multithreaded. In this case, IShellLink uses the
apartment model. How do I know? By the apartment number in the
phone book address? Not quite. Either threading model is known via
documentation or the information is available in the registry.
To make matters worse, depending on the threading model the initialization goes in different places. For the Multithreaded model, initialization code can be done one time per application, so it's easiest to call it in the frame procedure. For the apartment model, the code needs to go in the same apartment or thread where the OLE code lives. For that model it's best to call the initialization code just before calling the OLE interface(s), and uninitialize just after.
Well, surely those two "simple" steps are done now. Guess again!
There's one more wrinkle. CoInitializeEX() is not
included in the TopSpeed lib files so you need to make your own
library from Ole32.dll before CoInitializeEX() will
link. One is included with the downloadable zip. With all this in
mind, the class OLECl.CLW was written to carry out steps 1 and
6.
CoInit_ApartmentThreaded equate(2)
CoInit_MultiThreaded equate(0)
OLEClType Class,type,module('OLECl.CLW'),
LINK('OLECl.CLW',_ABCLinkMode_),DLL(_ABCDllMode_)
!Member Data
fInitComm long(0)
ThreadType long(-1)
! 0=Apartment model; 2=Multithreaded; -1=illegalvalue
!Methods
InitOLE Procedure(long threadingModel),byte,proc
!0 is good, else if fatal error
KILLOLE Procedure()
GetOleInitializedCount Procedure(),byte
GetThreadingType Procedure(),long
End
The InitOLE method calls
CoInitializeEX and increases fInitComm.
The KillOLE method does the opposite: it calls
UnCoInitialize and decreases fInitComm.
On a good day, when your program is done, the count of calls to
CoInitializeEX should exactly equal count of calls to
UnCoIntialize and GetOleInitializedCount
should return 0. That's on a good day. InitOLE
should be called with a threadingModel parameter of 0
or 2 for apartment model or multithreaded respectively. Neither of
the last two methods are needed in production code but may be
valuable for debugging. Since it seems I spend most of my life
debugging, that should make them very valuable indeed.
Well wait just a minute. Didn't I imply in the introduction that this was going to be easy? Lets take inventory. There's the Icall.A assembler module, the OLE32.Lib, OleCl.inc and Ole.Clw for initialization, plus if we ever get rolling here there will be one or so more classes to deal with those sexy interface pointers. Time to reach for Clarion's ace in the hole: templates. Also in the downloadable zip is OLETPL.TPL which adds all these bits and pieces to any application that will call an interface.
The template only has two prompts. First it asks for the threading model: Apartment or Multithreaded. In this case pick Apartment. There you have it, drop the OleTpl global extension template on an app and it's ready to call OLE interfaces. Oh, I did forget to mention one other detail: the second prompt asks for the name of the class that does the work of managing interface pointers. Looks like I'd better get busy building that piece.
The main goal of the following class is to manage all the
interface pointers, being sure to release all it acquires and
manage any errors and the constants associated with the interfaces.
All the code for managing the interface pointers and errors can be
put into a general purpose helper OLE class. I decided to name it
holeClType , short for helper OLE class. Nothing in
that class is specific to this project; it's just general
purpose interface pointer management.
The basis of the system is a series of equates, three queues and some beer:
!Interface equates
IShellLink Equate(20)
IPersistFile Equate(19)
!method equates with Method offset in the Vtable.
IShellLink_SetDescrip Equate(IShellLink*100H + 1CH)
IShellLink_SetWorkDir Equate(IShellLInk*100H + 24H)
VtableQType Queue,type
VtableID Long
pVtable Long
end
IIDQType Queue,Type ! pairs an interface with its IID
eInterface long
IID_Address long
InterfaceName string(eDebugLabelLen)
end
MethodNameQType queue,type
eMethod long
MethodName string(eDebugLabelLen)
end
IBeerDrink equate('JimKane')
One equate is created for each interface and for each method. In
the code when I want to do any thing with an interface I use the
symbolic name: IShellLink or
IPersistFile. If I need to store or retrieve a pointer
to pointer to the interface (see part one for an explanation)
VtableQ.VtableID holds the equate and
VtableQ.pVtable holds the pointer. The importance of
this is seen in the release method code. When the job is complete
and it is time to release all remaining pointers, all that needs to
be done is loop through VtableQ and call the release
method for each pointer that was acquired, like this:
hOleClType.ReleaseAll Procedure()
I long
Recs long
Code
Recs = Records(SELF.VTableQ)
Loop I = Recs to 1 by -1
Get(SELF.VTableQ, I)
If Errorcode() then break.
SELF.Release(SELF.VTableQ.VtableID)
end
Once all the interface pointers are stored in a queue, managing them is pretty easy.
Likewise in part two I had code that typically went like this:
!Get IPersistFile Interface by calling ! QueryInterface in IShellLink hr = ICall2p(ppVtable_IShellLink, IShellLink_QueryInterface,| Address(IID_IPersistFile), Address(ppVtable_IPersistFile)) If hr<0 then Clear(ppVtable_IPersistFile) Res = Return:Fatal Do ProcedureReturn end
One of the problems is if an error happens (and it always does
if someone is with you watching), and do
ProcedureReturn is called, you don't know which
call failed. Ideally, if you could pair a string containing the
interface name with the pointer to the interface you could make a
more meaningful error message. The remaining two queues pair one of
the equates for an interface or method with its name stored in a
string. The IIDQ also stores the IID for
the interface along with the string name. IIDs are
needed in several of the API calls. The information is loaded by
calling these methods:
SELF.AddIID(IShellLink,Address(IID_IShellLink),'IShellLink') ! pair equate IShellLink with its IID ! and name stored as a string SELF.AddMethodName(IShellLink_SetPath,'IShellLink_SetPath') ! pair IShellLink_SetPath equate ! for the IShellLink SetPath ! method with its name stored in a string
Now use the information stored in the management queues to redo
the above call to QueryInterface:
hOLEClType.QueryInterface Procedure(long eInterfaceToQuery,Long pIIDNeeded, long eInterfaceNeeded ) pVtableNeeded Long(0) pVtableToQuery long(0) hr long(0) code !using the interface equate, get the pointer pVtableToQuery = SELF.GetVtable(eInterfaceToQuery) If ~pVtableToQuery then Return Return:Fatal. !abort on error HR = ICall2p(pVtableToQuery, eQueryInterface, pIIDNeeded,| Address(pVTableNeeded)) If SELF.Failed(HR,'QueryInterface', eInterfaceToQuery, | eQueryInterface) !make the call and handle errors Return(Return:Fatal) !on failure return an error code else ! On success add the new interface pointer to the vTableQ Return(SELF.AddVtable(eInterfaceNeeded, pVtableNeeded)) end
Notice the Failed method is called with the equates
for the interface and method so it can use the information in the
queues to manufacture a decent error message or progress message.
Depending on a simple setting, Failed will display a
message on both success and failure so you can follow the progress
of the code, on error only for debugging, or display no messages
for released code. For details examine the code for the
Failed method in the downloadable code.
Notice in the above discussion of the three principle queues
nothing was specific to the current project of creating a shortcut.
For any specific project just take holeClType and
derive the class to add the code specific to your problem.
HoleClType also contains another helper class called
strcl, short for string class, that helps with wide
string to C or Clarion string conversions which you will also need.
It also can handle bstrings but that code will not be needed for
this project.
While I will not specifically discuss the string class one
method needs special mention. When presented with a cstring that
needs to be converted to a wide string, there is always a problem
knowing how much larger a buffer or string to allocate at compile
time for the wide string. To solve that problem the
CtoWStrAlloc method dynamically allocates memory for
the wide string output, and the caller is required to dispose of
it. While not revolutionary, it is a valuable technique that may be
of interest to some.
Very often when I write a class that I know will be derived each time it's used, I write a starter class that reminds me how to derive the class and of some things I may want to do. If the reader (that would be you) would like to follow along on how to build the final class, open the downloadable zip file and load OLEStart.Inc and OleStart.Clw. That is my "cheat sheet" for starting an OLE interface project. Just unzip the downloadable zip file to an new directory, register the template if it is not already registered then rename OleStart.clw and OleStart.inc to something related to your project.
In this case I used scut.clw and scut.inc. So the first task is
to rename OleStart then do a search and replace for
OLEStart and replace with ScutCl through
both files. Notice Scut.Inc includes holeCl.inc and strcl.inc. As a
result, I don't need to add those files to the project, just
Scut.inc which in turn will add holecl.inc and strcl.inc. Notice
also the Class(HoleClType) which tells the compiler
this class is derived from HoleClType. With that done,
Scut.Inc looks like this:
!ABCIncludeFile
OMIT('_EndOfInclude_'_SCutPresent_)
_SCutPresent_ EQUATE(1)
!Other Classes
Include('HoleCl.Inc')
Include('StrCl.inc')
!Equates - will be seen by using program
ScutClType Class(hOleClType),type,module('Scut.CLW'),
LINK('SCut.CLW',_ABCLinkMode_),DLL(_ABCDllMode_)
Init Procedure(byte pDebugMode=0),byte,proc
Kill Procedure()
end
!_EndOfInclude_
As can be seen, there is nothing in this class other than the
Init and Kill methods! The method I need
is:
CreateShortCut Procedure(ShortCutStructType
ShortCutStruct),byte
In support of that prototype I also need to add the
ShortCutStructType that collects the information
needed to create a shortcut:
ShortCutStructType Group,type
lpTarget long !can't be 0
lpDesc long !can be 0
hotkey ushort !can be 0 or a keycode
lpIconPath long !May not be null, but can use
! same value as lptarget
IconIndex ushort !0 is the 1st icon
lpWorkingDir long !Can be 0 or NULL
lpLinkFileName long !file name with optional path for
! the .lnk file to create
SpecialLocation long !special location to
! create shortcut, 0=desktop
end
With that added before the class definition, Scut.inc is ready!
Now, open Scut.clw and change OLEStart to
ScutCl as before. Notice OleStart had
places reserved to add the interface and method equates:
!Equates for Interfaces
!1-19 is reserved for hOleCl interfaces
Itemize(20)
!List all interfaces called here
end
!Vtable Offset - List all methods called
!Method equate(Interface*100H + 0CH)
The method equate is manufactured from its interface name plus
the offset down the vtable required to get to the
method. This serves to make a unique equate for the method and
stores the offset. To complete this task, fill in the list of
interfaces in the itemize list and the list of methods in the
method list. Once again, the method offsets come from .h files (Ask
Bill syndrome again!).
The Init and Kill methods are fairly
self explanatory. Since Scutcl is derived from
hOleClType to be sure the hOleClType class's Init and Kill
methods are called. The format is as follows:
ScutClType.Init Procedure(byte pDebugMode=0)
Res byte,auto
code
Clear(SELF)
res = Parent.Init(pDebugMode)
If ~Res then
SELF.AddIID(IShellLink,Address(IID_IShellLink),|
'IShellLink')
SELF.AddMethodName(IShellLink_SetPath,|
'IShellLink_SetPath')
!Other interfaces and methods deleted for clarity
end
Return(Res)
ScutClType.Kill Procedure()
code
Parent.Kill()
Return
Notice the general pattern of:
code !code before parent call !Parent call - calls corresponding hOleCl method !Code after call to parent return
This ensures the parent holecl methods are called. As you may
guess, on Init hOleClType needs to create the queues I
described and destroy them on Kill. One of the side
benefits of deriving a class is all these mundane details can be
hidden in the base class or down in the 'hole' in this
case! I just love to put the busy work out of site so I can
concentrate on the rest.
After CreateShortCut validates the data, the next
task is to get the two interfaces needed by calling
CoCreateInstance and then QueryInterface
method to get the second interface. Since calling
CoCreateInstance and QueryInterface is
something that is very common and happens many times in any OLE
program, there are special methods for each in hOleClType. The code
is as follows:
!Get the IShellLink Interface with CoCreate If SELF.CoCreate(Address(Clsid_IShellLink), IShellLink) | then return(Return:Fatal). !Get Ipersist_File from QueryInterface If SELF.QueryInterface(IShellLink, Address(IID_IPersistFile)| , IPersistFile ) then Return(Return:Fatal).
Take a minute and compare these two calls with the originals in
part two. They're much less complicated. Progress! I may get
out of this alive after all. The really nice thing is all the busy
work not specific to this project is hidden down in
holeClType and can be reused in any OLE project. In
fact, I've used it in several projects already. Upon
inspection holeClType has several "hidden" features
not needed for this project but which are so commonly needed on
larger similar projects that they are included.
The next section of code tells the IShellLink
interface different properties needed for the short cut. This
involves using the Icall.A assembler piece. However, you need not
panic; it's well wrapped in holeClType so it
should not be too scary.
!Set Short Cut Attributes
If SELF.ICall(1, IShellLink, IShellLink_SetPath, |
SCS.lpTarget) Then Return(Return:Fatal).
If SELF.ICall(2, IShellLink, IShellLink_SetIconLoc,|
SCS.lpIconPath, SCS.IconIndex) Then Return(Return:Fatal).
The first parameter (1 or 2) above tells Icall how
many parameters will be passed to the interface. This is followed
by the equate for the interface and method to call. In other words,
the first line above calls
IShellLink.SetPath(SCS.lpTarget) where
SCS.lpTarget is the address of the file name to which
the shortcut should point. This information is passed to the
CreateShortcut method. All the details of getting the
vtable pointers and method offset are taken care of in
the holeClType never to be written again! On error,
just return with a result code of return:fatal which
signals the error condition.
The next section of code gets the path to the location where the
shortcutshould go. This is often the desktop or some other special
place. After calling two APIs to get the path, the code stores the
path in the local variable szpath, a cstring.
Unfortunately the last interface call requires a wide string. Since
the length of the path string is unknown at compile time, the code
will allocate heap memory for the wide string. The alternative
would be to allocate a very large buffer certain to take care of
any path. This is the ctowstralloc call in the string
class mentioned earlier:
!Now convert to a wide string wszLinkFile &= SELF.StrCl.CtoWideStrAlloc(szPath) !Last but not least, save the shortcut to disk ! then cleanup and exit: Res = SELF.ICall(2,IPersistFile, IPersistFile_Save, | Address(wszLinkFile), True) Dispose(wszLinkFile) If res then Return Return:Fatal.
Notice after the final interface call that rather than testing
immediately for an error, the dynamically allocated wide string is
disposed of and after that a test for error on the last
Icall is done.
As soon as the last call completes the shortcut is created. All
that remains is to release all the interface pointers and clean up.
Because all the interfaces are neatly stored in queues, to release
them just loop through the queue and call the release method. This
is already coded in the Kill method so all you have to
do is call scut.Kill(). The code that actually goes in
the application to create the shortcutis as follows:
OLECl.InitOLE(CoInit_ApartmentThreaded)
ScutCl.init(2) !0=no errormessages, 1=msg on error only,
!2=verbose=msg on sucess and fail
Clear(ShortCutStruct)
!pgm to run when shortcut clicked
ShortCutStruct.lpTarget = Address(Target)
!Optional descriptions
ShortCutStruct.lpDesc = Address(Desc)
!Shortcut .lnk file name
ShortCutStruct.lpLinkFileName = Address(LinkFileName)
!Optional working directory
ShortCutStruct.lpWorkingDir = Address(WorkingDir)
!Take icon from target file
ShortCutStruct.lpIconPath = ShortCutStruct.lpTarget
!0=Desktop
ShortCutStruct.SpecialLocation = SpecialLocation
If ScutCl.CreateShortCut(ShortCutStruct)
Message('Create Shortcut Failed')
else
Message('Create Shortcut Worked')
end
SCutCl.Kill()
OLECl.killOLE()
When the demo SCUTABC.EXE is run the success (or failure) of
each and every call is reported in a message since the ScutCl.init
method was set for verbose. There are quite a few messages. To bad
we don't get to bill customers by the step or message box!
When ever you get tired of the messages just recompile with a debug
setting in scut.init of 0 or 1. There are a few other goodies in
holeClType for common OLE tasks such as memory
allocation and expanded error handling which are available should
you need them.
The nice thing is the next time out there is much less work now that the helper OLE Class is done. Just add the template, create the top level project specific class from OLEStart.CLW and call OLECL.INIT to initialize OLE. Then initialize the top level class and off you go.
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