Hello everyone,
I was given a vb proggie that reads data from a database end uses ole automation to print, through an ole server, some labels.
They asked me a complete rewrite of this program and as usual I've done everything in asm.
I've almost completed it, but i cannot figure out how to handle the ole routines. This is totally new to me!

I've tried to search around (msdn included) but I didn't find any reference to automation api's, but objet programing for vb and c++.

Can anyone help me or give me some hints about api based ole automation?

The vb code uses functions like these:

- Set OLE_Server = CreateObject("XXXXXXX.Application")
- OLE_Server.ActiveDocument.CopyToClipboard
- PrintLab = OLE_Server.ActiveDocument.PrintLabel(Num)
etc.

Thanx a lot,
acab
Posted on 2002-02-24 19:58:33 by acab
IN asm, it would be:




.data
pOLE_Server DWORD 0
pszAppName BYTE "XXXXXXX.Application", 0
pCLSID DWORD 0
pActiveDoc DWORD 0
IID_IOLE_Server GUID <need to look this up for the interface you need>
IID_IActiveDocument GUID <this one too>

; you also need to define the vtable structure of the interfaces

.code

invoke CLSIDFromProgID, ADDR pszAppName, ADDR pCLSID
invoke CoGetClassObject, ADDR pCLSID, CLSCTX_SERVER, NULL, \
ADDR IID_IOLE_Server, ADDR pOLE_Server
mov edx, pOLE_Server
mov edx, [edx]
invoke [edx].IOLE_Server.ActiveDocument, pOLE_Server, ADDR pActiveDoc
mov edx, pActiveDoc
mov edx, [edx]
invoke [edx].IActiveDocument.CopyToClipboard, pActiveDoc
mov edx, pActiveDoc
mov edx, [edx]
invoke [edx].IActiveDocument.Release, pActiveDoc


and so on....

It looks completely different because VB has been doing a lot of the messy COM details for you. For example,



OLE_Server.ActiveDocument.CopyToClipboard


Really means "call the ActiveDocument method of the OLE_Server interface, get another interface back, and use th new interface to call its CopyToClipboard method. Then release this new interface."

You'll need to curl up with the best COM reference you can find, for C or C++ (C++ actually being easier to understand for COM), and make sure the reference doesn't use any MFC or ATL. "Inside COM" is the best book on this I can recommend. "Inside OLE" (available online frrom MSDN) is acceptable, but not as directed as the first book. "Automation Proggamers Reference" is also handy, it used to be online, but I found it in the bookstores. It has some goodies about how VB does its COM stuff.

I have some COM stuff on my website (link somewhere over there by my name), but it USES COM, doesn't teach the basics, or teach how to translate VB into COM into asm.

You'll also find some macros and other goodies to make this easier on you (like a coinvoke macro to make a COM call all in one line).

But it IS possible, even easy once you know the pattern.
Posted on 2002-02-24 22:46:47 by Ernie
Hi Ernie,
thanx a lot for your replay and for your site as well.
I've downloaded your tutorials about com objects and I'm starting reading them.
About your code, I'm trying to understand it properly to port it to tasm, but firstly I've got to learn many things, including what the hell a vTable is...

Phanx again,
acab
Posted on 2002-02-25 03:41:07 by acab
I post you an example with some more basic assembler code.
There is only one macro, which is defined at begin. (the names of the VARIANT may differ from yours)

The AutoWrap function does all the things to "call" the given OLE automation function for you. Parameters and return values are transfered via VARIANT data structures.

(hope this text is not too long ...)


callother MACRO object, method
mov eax, object
mov eax, [eax]
push object
call method[eax]
ENDM

.data

hcomActive: dd ?
hcomDispatch: dd ?

xResult VARIANT <?>
szActive: db "ActiveDocument", 0
szCopy: db "CopyToClipboard", 0

clsidAppl CLSID <?> ; fill in your apps class id
idINull GUID <0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0>
idIDispatch GUID <00020400h, 0, 0, 0C0h, 00, 00, 00, 00, 00, 00, 46h>


.code

;----------------- start application

lea eax, hcomDispatch
lea ebx, clsidAppl

xor ecx, ecx
push eax
push offset idIDispatch
push CLSCTX_LOCAL_SERVER
push ecx
push ebx
call CoCreateInstance
cmp eax, S_OK
jne ippDone


;----------------- get active document object

lea ecx, xResult
mov [ecx.var_vt], VT_EMPTY

xor eax, eax
lea ecx, xResult

push eax
push eax
push offset szActive
push hcomDispatch
push ecx
push DISPATCH_PROPERTYGET
call AutoWrap
test eax, eax
jne ippClose

lea ecx, xResult
mov eax, [ecx.var_pdispVal]
mov hcomActive, eax


;----------------- copy to clipboard

xor eax, eax
push eax
push eax
push offset szCopy
push hcomActive
push ecx
push DISPATCH_METHOD
call AutoWrap
test eax, eax
jne ippClsPres

;; success


;----------------- free objects

callother hcomActive, iunk_Release
callother hcomDispatch, iunk_Release

ret


;*********************************************************************
; does: find a named C++ method or attribute and call it *
; last update: 08-05-2001 - Scholz *
; parameters: dwError AutoWrap (dwType, lpvResult, hObject, lpszName, dwArgs, lpxArgs) *
; returns: zero for ok, else errorcode *
;*********************************************************************

AutoWrap PROC dwType:DWORD, lpvResult:DWORD, hObject:DWORD, lpszName:DWORD, dwArgs:DWORD, lpxArgs:DWORD
LOCAL lpWide:DWORD, idProp:DWORD, lpxParams:DWORD, dwPut:DWORD

sub esp, MAX_PATH
mov lpWide, esp
sub esp, sizeof DISPPARAMS
mov lpxParams, esp

mov eax, hObject
test eax, eax
je auwParam


;----------------- convert name to unicode

push MAX_PATH
push lpWide
push -1
push lpszName
push MB_PRECOMPOSED
push CP_ACP
call MultiByteToWideChar
test eax, eax
je auwError


;----------------- get id for the name

lea eax, idProp
lea ecx, lpWide

push eax
push LOCALE_USER_DEFAULT
push 1
push ecx
push offset idINull
callother hObject, idisp_GetIDsOfNames

test eax, eax
jne auwExit


;----------------- fill param struc

xor edx, edx
mov ebx, lpxArgs
mov eax, lpxParams
mov ecx, dwArgs

mov [eax.dispp_cArgs], ecx
mov [eax.dispp_rgvarg], ebx
mov [eax.dispp_cNamedArgs], edx
mov [eax.dispp_rgdispidNamedArgs], edx

test dwType, DISPATCH_PROPERTYPUT
je auwInvoke

lea ecx, dwPut
mov dwPut, DISPID_PROPERTYPUT
mov [eax.dispp_cNamedArgs], 1
mov [eax.dispp_rgdispidNamedArgs], ecx


;----------------- invoke object

auwInvoke: xor eax, eax
push eax
push eax
push lpvResult
push lpxParams
push dwType
push LOCALE_USER_DEFAULT
push offset idINull
push idProp
callother hObject, idisp_Invoke

test eax, eax
jne auwExit

xor eax, eax
jmp auwExit


;----------------- exit cases

auwParam: mov eax, E_INVALIDARG
jmp auwExit

auwError: call GetLastError

auwExit: add esp, sizeof DISPPARAMS
add esp, MAX_PATH
ret

AutoWrap ENDP
Posted on 2002-02-25 04:08:53 by beaster
Phanx a lot beaster,
your code is quite clear to me!
I'll try some coding tonight.
Bye for now,
acab
Posted on 2002-02-25 09:26:08 by acab
A vtable (table of virtual functions) isn't so hard, it's just poorly named, since it xcomes from how C++ makes the members of a class.

Its just a table of function pointers, one pointer to each method of the class. They appear in the order the methods are declares, meaning the first method of the class has its pointer first, the 2nd 2nd, and so on.

When you make an object from the class, a small blob of memory is declared to be the object. The class code knows the layout of the object since the class defined the object in its creation method. The user of the object gets the address location where part of it resides.

Now, stored in that address location given for the object is another pointer, and that pointer is to the vtable. That is why every COM call shown above has the little two-step dance first:



mov edx, pObject ; gets the location of the object
mov edx, [edx] ; gets the location of the vtable
call [edx].InterfaceName.MethodName


In MSM (I don't know nuthin about TASM), InterfaceName.MethodName would be a structure followed by an element of that structure. InterfaceName is the list of methods of the COM class you are using, MethodName is the name of the method. The compiler will change that to a simple number.

Calling the first method would just "call " directly. Let's say for arguments sake you wish to execute the 2nd method of a class (or interface). That would compile down to this:



mov edx, pObject ; gets the location of the object
mov edx, [edx] ; gets the location of the vtable
call [edx + 4] ; which is 1 * SIZEOF DWORD


Hope this helps. Good luck.
Posted on 2002-02-26 08:34:45 by Ernie
Hello Masters,
I've studied the lesson, or at list the basis...
What I think I've learned is (please correct if I'm wrong!):

- A COM server is registered inside the registry and associated to a univoque CLISID
I can get the CLSID of a server through CLSIDFromProgId which uses Unicode style strings.

- A COM server exposes Interfaces identified each by its unique GUID
A minimal com implementation requires at least IUnknown an IDispatch.
BTW which is the GUID of IUnknown???

- I can get a ppv to an Interface passing the Server CLSID and the Interface GUID to CoCreateInstance.

- If I know nothing about a server I need to use its IDispatch interface.
So if I wanted to Set a Property I would nedd to:
Get the server CLSID
Get the ppv of its IDispatch (GUID={00020400-0000-0000-C000-000000000046})
Get the pointer to IDispatch::GetIDsOfName
Call GetIDsOfName passing the name of the property to set
Get the pointer to IDispatch::Invoke
Pass to Invoke the Member as returned from GetIDsOfName, an array of args together with the flag PROPERTY_GET

Is everything ok?

Thanks a lot
acab
Posted on 2002-02-26 13:37:50 by acab
Correct me if I'm wrong but if I remember correctly IDispatch is not required in a COM object. It is however required in an OLE Automation COM object since they require a dual-interface (so that VB and scripting languages can use them)


As for you problem here: I think it would be faster going through COM directly then using the IDispatch interface (it's just another layer to find the same stuff)

I'm sure Ernie will correct any mistakes I've written here.

Good luck.
Posted on 2002-02-26 15:58:29 by Hiroshimator
To Ernie:

Sorry Ernie,
but I didn't notice your message at the time of my last post.
"The little two-step dance" is now perfectly clear to me (after reading your access.doc - tnx a lot) and the syntax you used is ok even from a tasm point of view (after all asm is always asm).
What I still don't understand is how do I reconstruct the vtable if this is not provided me by the author of the ole server.
Is there a simpler way other than going through IDispatch?
And this other way will still be valid after a version updrade of the server? It seems to me (correct me if i'm wrong) that if now I can "call " meaning "call .ActiveDocument.Print", this won't be true any more after a version upgrade if the elements in the table are in the same position.

Phanx again, you've been very helpful to me!


To Hiroshimator:

I think it would be faster going through COM directly then using the IDispatch interface


Do you mean using vtable?
Posted on 2002-02-26 18:33:33 by acab
acab,

Excellent questions, I can see your really getting into this.

First off, understand that COM is first of all a communications standard, it defines how different blobs of code will talk to each other, no matter what language each blob is written in. Thats why VB can work with VC can work with Java can work with Delphi can work with...

Part of what is refered to as the COM 'contract' (meaning the mutual agreement how the communication is done) is once an interface is published, that interface shall not change. Ever!

This explains why you'll sometimes see SomeInterface and also SomeInterface2, where SomeInterface2 has everything SomeInterface has, plus a new method or two. The orgional interface is left alone, and a brand new interface is defined.

Thus no code will break, old code uses the orgional interface, new code can take advantage of the newer methods. So you do not have to worry about some new version changing the vtable. The COM contract demands thou shall NOT change the vtable.



So let's look at the vtable for IUnknown, since another part of the COM contract states everything MUST inherite from IUnknown.

IUnknown has three member functions: QueryInterface, AddRef and Release. If I was to define this interface, I would do so as follows:



IUnknown STRUCT
QueryInterface DWORD ?
AddRef DWORD ?
Release DWORD ?
IUnknown ENDS


This establishes the relationship of the members of the vtable, each member is a DWORD away from the previous one. If any methods are added, they MUST be added to the end of this table (and they frequently are added, that is how an interface inherits from another interface).

That will work fine, but is a little limited, we can do better if we want to use the MASM invoke macro to call a COM interface:



QueryInterfaceProto typedef proto :DWORD, :DWORD
AddRefProto typedef proto
ReleaseProto typedef proto

pIUnknown_QueryInterface typedef ptr QueryInterfaceProto
pIUnknown_AddRef typedef ptr AddRefProto
pIUnknown_Release typedef ptr ReleaseProto

IUnknown STRUCT
IUnknown_QueryInterface pIUnknown_QueryInterface ?
IUnknown_AddRef pIUnknown_AddRef ?
IUnknown_Release pIUnknown_Release ?
IUnknown ENDS


wow... that's a lot in one piece. But it tells the compiler to expect a function pointer inside the vtable, and also gives it the count of parameters (which MASMs invoke needs, and will generate errors if you forget a parameter).

Also, there is some name Decoration there, each member function has the interface name tacked on. This is what lets polymorphism work. Polymorphism is just a fancy word for two different interfaces with the same member method name. By adding the interface name like that, we're pretty much guarnteed unique method names every time.

(The only exception is when two interfaces have the same name, which is actually legal, as COM only regognises the interface GUID, or REFID. You could use the REFID for a decoration, but it will make your code even more unreadable.)

(Also, I've heard the way I define interfaces cannot be traced with a debugger. This never bothered me, as I don't use debuggers, if you do use one, you'll have to find out how this is done on your own.)

If your Interfaces are defined like this then you can construct a fancy macro to do a lot of the work in calling a method, such as you will see in the coinvoke macro.

The REFID for IUnknown is {000000000H, 00000H, 00000H, {0C0H, 000H, 000H, 000H, 000H, 000H, 000H, 046H}}. You'll find lots of those in my include files, hopefully they will translate to TASM without too much trouble.

Interfaces are defined for the whole world thru type libraries. They are not eazy to read directly, but MSVC includes a viewer (OLE View). That will give you the layout of any interface on your computer. GUIDs, constants, and other goodies can also be learned from the type lib.


Additionally, MSDN documents the methods of an interface in vtable order, so if all else fails...


Also: My copy of MSDN states CLSIDFromProgID takes a lpszProgID, which is an ASCII zero terminated string. I would guess NT has a CLSIDFromProgIDW version that uses Unicode.






To Hiro:

Yes, the only interface a COM object is required to have is IUnknown. Mind you, thats a pretty useless object, as all it can do is say "No, I don't do that."

IDispatch is also known as the Automation interface, no prior knowedge of an interfaces vtable is necessary if it inherits from IDispatch, it can be driven at runtime by method names alone.

And yes, going directly to the vtable is much much faster then going thru IDispatch. An interface that inherits from IDispatch and has a vtable is known as a dual interface.

I don't see anything to correct you on. ;-)
Posted on 2002-02-26 19:24:00 by Ernie
Hi Ernie,
tnx to this board (and to your dox too) COMinASM seems now quite unharmful.
I still need to go a little deep into coding, but my ten line tests seeem to have no problem.

Also: My copy of MSDN states CLSIDFromProgID takes a lpszProgID, which is an ASCII zero terminated string. I would guess NT has a CLSIDFromProgIDW version that uses Unicode.

This is what I thought as well, but in my tests I've found out that
OleServerName db 'Codesoft2.Application',0

returns error 800401f3 (Invalid class string)
while
OleServerName db 'C',0,'o',0,'d',0,'e',0,'S',0,'o',0,'f',0,'t',0,'2',0,'.',0,'A',0,'p',0,'p',0,'l',0,'i',0,'c',0,'a',0,'t',0,'i',0,'o',0,'n',0,0,0

works ok. And dunno why?!?!?!

Thanx again for your help: hopefully I won't bother you for a while;)

acab
Posted on 2002-02-27 03:01:30 by acab
in the second example you effectively define it as unicode :)

unicode is just that after all: a word/char
Posted on 2002-02-27 03:54:58 by Hiroshimator
Hi there, sorry for bothering again!!!!!
I've still got some problems...

I've got an Interface named "IApplication" that exposes a few properties and methods.
For testing purposes I'm just interested in two of them: the property "Visible" that wants 1 bool arg and is supposed to show/hide the server and the method "Quit" that shoud shutdown the server and that needs no args.

I firstly tried changing the Application.Visible to TRUE (the server hides itself by default) but my code crashes.
The property "Visible" is ID#01 so it shoud be the first entry in the vtrable.



mov edx, dword ptr [ppvIApplication]
mov eax, dword ptr [edx]
push TRUE
push edx
call [eax]


This gives me a prot fault in ole32...
I thought this was caused by an incorrect method of setting a property (your samples only explains methods). Are you so kind to expain how to get/set a Property?
So I've tried with Application.Quit mehod that have ID#17h



mov edx, dword ptr [ppvIApplication]
mov eax, dword ptr [edx]
push TRUE
push edx
call [eax+(16h*4)]


This time no crash and the call returns eax=0 but if I hit Ctrl-Atl-Del I can see the program's still running...

What am I doing wrong this time?????

Thanks again
acab
Posted on 2002-02-27 07:30:11 by acab
did you call Release on the interface?

Also about the TRUE: if you observe VB, you'll see that true translates there to -1 (FFFF), I think true here is defined as 1.

This might be a problem if it's not expecting it :/
Posted on 2002-02-27 07:52:53 by Hiroshimator
acab,

the dispid of a property has nothing to do with the position in the vtable.

If IApplication inherits from IDispatch (which I assume), you can set a property with



local variant:VARIANT
local disp:DISPPARAMS

mov variant.vt,VT_I4 ; I assume property is "integer"
mov eax,y ;<- new value of property
mov variant.lVal,eax

lea eax,variant
mov disp.rgvarg,eax
mov disp.rgdispidNamedArgs,NULL
mov disp.cArgs,1
mov disp.cNamedArgs,0

coinvoke pIapplication,IApplication,Invoke_,\
DISPID_xxx, addr IID_NULL, LOCALE_USER_DEFAULT,\
DISPATCH_PROPERTYSET,addr disp,NULL,NULL,NULL


But check structure DISPPARAMS first. There exist some with wrong format. It should be



DISPPARAMS struct
rgvarg LPVARIANT ?
rgdispidNamedArgs LPDISPID ?
cArgs DWORD ?
cNamedArgs DWORD ?
DISPPARAMS ends



japheth
Posted on 2002-02-27 08:12:51 by japheth
Ok, I think I'm missing something!

I'll start from the beginning, please correct me

1) First I need to get the server's CLSID
This can be done through CLSIDFromProgID api.
And this is ok.

2) Then i need a ppv for IApplication:
to do this I can use CoCreateInstance or CoGetClassObject.
Still dunno the difference however they both work perfectly.
The server is loaded and the ppv is returned.

3) If i need to call a method i can do this via vtable:
mov edx, dword ptr
mov eax, dword ptr
push edx
call

4) If i want to set a property I have to go through IDispatch.
Or at least i think this is the meaning of the previous post...

5) When I've done with the ole server I need to call the Release method.
And that's another issue: browsing the IAppication interface with OleViewer I found out it doesn't expose any Release method.
Trying to call the Quit method doesn't work: the server process is still active in background.
Is there any other api i need to call to free the server process?!

Thanx for your help
Posted on 2002-02-27 09:44:28 by acab
whenever you create ANY COM interface, it calls AddRef. So whenever you wish to quit using an interface you call Release.

EVERY interface has Release, since every interface inherits from IUnknown (which has only 3 methods: QueryInterface, AddRef, Release)

When you clone a pointer it's usually best to call AddRef on it (don't forget the corresponding release, whenever you call AddRef, code a Release as well)

properties usually have correspondent putPropertyName and getPropertyName methods, so they can be used via a vtable.
Posted on 2002-02-27 10:12:46 by Hiroshimator
So if any com interface is inherited from IUnknown:


IUnknown STRUCT
IUnknown_QueryInterface DWORD ?
IUnknown_AddRef DWORD ?
IUnknown_Release DWORD ?
IUnknown ENDS


and my IApplication interface exposes just 3 methods (methodA, methodB, methodC), its table should be:



IApplication STRUCT
IApplication_QueryInterface DWORD ?
IApplication_AddRef DWORD ?
IApplication_Release DWORD ?
IApplication_MethodA DWORD ?
IApplication_MethodB DWORD ?
IApplication_MethodC DWORD ?
IApplication ENDS


So that to call IApplication.MethodA I would do:



call CoGetClassObject,....
mov edx, dword ptr [ppvIApplication]
mov eax, dword ptr [edx]
push edx
call [eax+0ch]


Am I right?!

BTW: does CoGetClassObject and CoCreateInstance internally call AddRef?

Tnx for your patience
Posted on 2002-02-27 10:53:47 by acab
I see a lot of points to cover here, let me hit some of them quick.

Methods and properties are both actually methods. In VB, on may say an object exposes some properties, but it exposes each by two methods, a get method, and a put method. VB does this to keep things as simple as possible for the programmer.

CoGetClassObject and CoCreateInstance are very similar, with CoGetClassObject being the more general of the two, as it can reach into other computers on a network to instance objects.

Defining the correct vtable and parameters for each method is quite importaint. A mistake in either is a sure fire way to cause a GPF. If you can post (or email me direct) the program with the IApplication I'll peek at it and see what I can find to help define the interface (and then tell you how I found it out). If its a popular program, maybe I already have it.

The ID_OF_THE_METHOD has no realation to the vtable order, so if "call " happens to work, it is by pure chance.

You're making some really good mistakes now, you'll really learn some good stuff when you work thru them.



"If a fool would just persist in his folly, he would become wise."
--William Blake
Posted on 2002-02-27 22:13:21 by Ernie
Hi Ernie,
it seems I'm keeping on bothering you... :(

But I keep on learning as well...

1) You say:
Defining the correct vtable and parameters for each method is quite importaint. A mistake in either is a sure fire way to cause a GPF. If you can post (or email me direct) the program with the IApplication I'll peek at it and see what I can find to help define the interface (and then tell you how I found it out). If its a popular program, maybe I already have it.


I realized this through tens of GPF+Reboot...:(
The app is a little too big for my poor dialup (5 megs for the main exe + a dozen of libs - sh*tty VB crap)
Instead you colud explain me how to get a really usable vtable from any com app. I would be really glad if you'd do this:)
msdn only reports that the first 7 entries are those of IUnknown (3) and IDispatch (4) and my oleviewer just reports props and methos sorted by ID.


2) Now my tests are producing some results. Although I lack a right vtable and so I need to use IDispatch, I can now get ppv's of Interfaces, get properties and call Methods without any argument.
What I'm still trying (without success) is passing arguments to IDispatch::Invoke.
I think I've got the whole serie of 800xxxxx errors ;)
What I thing I'm doing wrong is filling in the various structures.
Here's a little piece of code, could you please check it?




; First the structures
; Tasm syntax here, sorry, but it's quite similar to masm

; This is the DISPPARAMS struct
DISPPARAMS struc
rgvarg dd ?
rgdispidNamedArgs dd ?
cArgs dd ?
cNamedArgs dd ?
DISPPARAMS ends

; This is the VARIANTARG struct
; Last field is defined as DWORD for my convenience
; (tasm doesn't like unions)
; I fix the clSize by hand inside the CODE section according to vt type
VARIANTARG struc
clSize dd ?
rpcReserved dd ?
vt dw ?
wReserved1 dw ?
wReserved2 dw ?
wReserved3 dw ?
vvariant dd ?
VARIANTARG ends

VT_BOOL equ 11
VVT_TRUE equ 0ffffh
DISPID_PROPERTYPUT equ -3

; Now the DATA section
; Tasm syntax again: just subst "offset" with "ADDR"
vOneArg DISPPARAMS <offset vVariant, offset vDispidPutProp, 1, 1>
vDispidPutProp dd DISPID_PROPERTYPUT
vVariant VARIANTARG <>
iidINull GUID <0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0>

; The CODE section at last

mov ebx, [ppvIDispatch]
mov ebp, [ebx]

mov [vVariant.clSize], size VARIANTARG-2
; the last "-2" is required 'cause VT_BOOL uses a WORD value
mov [vVariant.rpcReserved], 0
mov [vVariant.vt], VT_BOOL
mov [vVariant.wReserved1], 0
mov [vVariant.wReserved2], 0
mov [vVariant.wReserved3], 0
mov [vVariant.vvariant], VVT_TRUE

push 0
push 0
push 0
push offset vOneArg
push DISPATCH_PROPERTYPUT
push LOCALE_USER_DEFAULT
push offset iidINull
push L OLE_VISIBLE ; This is the id of the "Visible" property
push ebx
call [ebp+24] ; = IDispatch::Invoke


Thanks a lot again
acab

Bye for now; i'll keep on experimenting! Hope at last i'll get wise instead of crazy;)
Posted on 2002-02-28 09:07:54 by acab