ComHelper

COM and ACTIVEX Programming made easier in HotBasic

Part III – Using a helper file with the Use Invoke option

General Summary

Part I – Getting started

Part II – Using ComHelper

Part III – Using a helper file with the Use Invoke option

Part IV – Using a helper file without the Use Invoke option

Part V – Using an ActiveX helper file

 Part VI – Using COM events

 

Contents of the Invoke-based helper file

By default, the Use Invoke option is checked. In this case, the helper file contains:

- some generic declarations for COM programming (unless you unchecked the Include COM Generics option),
- the declaration of the data types you chose to include (if any),
- the declaration of the constants you chose to include (if any),
- templates to help you use the interfaces you selected.

Note that the interface part is totally generic, that is, no specific code is generated. The interface part of the helper file simply consists of templates for you to copy and paste.

Include the helper file into your main program, by adding:
$include "XXX.inc"
where XXX is the name you gave your helper file.

Note: if you want to use COM objects from different files, you can generate a helper file for each of them and include them all in your main program.

 

Declaring an Invoke-based Interface

An Interface is declared as a HotBasic INTERFACE variable.

dim intf as INTERFACE

For the interface to be functional, it must refer to an existing object. You typically do that by using CreateObject:

Intf.CreateObject("Word.Application")

By the way, you can notice in this example that in the philosophy of COM programming, an application is considered an object! That's because you can run an instance of the application, and there is a set of subs and functions (in other words an interface) that works with it.

There are other ways to initialize an interface. For example, some functions return interfaces. (More on this below).

 

Using an Invoke-based Interface

You access an object’s subs and functions with 3 HotBasic keywords: invoke, getnum and getstr.

Invoke is used to make a call that doesn’t return a result. You specify the name of the method to call as the first argument of invoke. (“Method” is a synonym of sub). Example:

intf.invoke("Quit")

Getnum is used when the call returns a number. Example:

x=intf.getnum("Height")

Sometimes the result is a pointer to another object. Example:

newdocument=docs.getnum("Add", 1) 'new document

Getstr is used when the call returns a string. Example:

s$=intf.getstr("Name")

The helper file contains templates for all the object’s methods and properties, indicating their syntax. What some methods do is pretty straightforward from their name and the kind of arguments they take. For some others, it is not obvious and you should look for some documentation or some third-party code to help you.

(I suggest you familiarize with COM programming by porting code from other languages to HotBasic with the help of ComHelper. This way you'll get a feeling of how COM works.)

 

Passing arguments to a method

Some methods take arguments. Just add them as additional parameters after the method name. Example:

wb.invoke("Navigate", a$, 1)

Some arguments are optional, that is, you may omit them. In the corresponding template, those arguments bear the [optional] comment. In order to omit an optional parameter, you must pass void instead. Void is a pre-defined variant used for that purpose. Example:

dd.invoke("Add", void, void, void, void)

Note: HotBasic seems touchy about parameter passing. That means that some things don’t work. For example, I happened to see an Invoke call fail because it was of the form:

intf2.invoke ("Method", intf1.pointer)

However the following worked:

defint x=intf1.pointer : intf2.invoke ("Method", x)

So if you experience strange failures, review the arguments you pass to interface methods.

 

(advanced) Filling the interface pointer

An interface has a .pointer property, which is an integer initialized with 0. Actually you can do nothing with the interface until you have somehow filled its .pointer with a value. An interface with .pointer=0 is like a knob without a door, that is, it is not connected to an object and therefore does nothing useful. It can cause nice application crashes, though.

There are 4 ways to set .pointer:

1 - some interfaces are associated to objects (they are their "default interfaces")

In that case, use the CreateObject function to create the corresponding object and return an interface to it.

Example:

dim cal as INTERFACE
cal.CreateObject(
"Microsoft.Calendar")

In the example above, “Microsoft.Calendar” is the identifier of the object type. It is called the object’s ProgID.

Alternatively, you can pass the object’s CLSID. This is an uncivilized-looking string between curly brackets. The result is the same. Example:

cal.CreateObject("{8E27C92B-1264-101C-8A2F-040224009C02}")

2 - some interface properties are pointers to other interfaces.

For instance, the Word Application interface has a property called Documents that returns a Documents interface. Example:

$include "word.inc" 'file generated by ComHelper
dim a as INTERFACE  'Application
dim dd as INTERFACE  'Documents
a.CreateObject(
"Word.Application")
dd.pointer=a.getnum(
"Documents") 'result is an interface pointer

This is the equivalent of Visualbasic's "Set" statement: Set dd=a.Documents

3 - some API functions create objects and return an interface pointer

For instance the CreateILockBytesOnHGlobal function creates a memory object and returns a pointer to an ILockBytes interface.So you can write:

declare function CreateILockBytesOnHGlobal lib ...
$include "lockbytes.inc" 'file generated by ComHelper
dim o as INTERFACE
o.pointer = CreateILockBytesOnHGlobal (...)

Then you're ready to use the interface.

4 – the GetObject returns an interface pointer

GetObject is a ComHelper function meant to provide the functionality of Visual Basic’s GetObject function. It is used to retrieve an interface pointer from an identifier name. You can then use the interface’s Invoke, Getnum and Getstr. An example can be found in the Examples folder.

 

(advanced) Enumerating the members of a collection

Some objects really represent a set of other objects. They are called “collections”. If you’ve done some Visual Basic programming in Microsoft Office, you’ve probably encountered them: collections like Documents, Worksheets, etc.

You sometimes need to loop through all the elements in a collection. In Visual Basic, this is done with the For Each keyword:

For each Doc in Documents ... Next

This is not supported in Hot Basic. However, ComHelper provides you with an object called ENUM that helps you automate that. The syntax is the following.

Dim foreach as ENUM 'an object of the ENUM type
Dim item as INTERFACE 'item will receive the pointers to the objects in the collection, one by one
foreach.Enum(Collection) 'Collection is the INTERFACE you want to enumerate.
    item.pointer=foreach.NextItem 'get the next item in the collection
    if item.pointer then [use this item to do what you need] 'You MUST test for .pointer<>0 before using it
foreach.EndEnum 'end of the loop

To what objects does this apply? It applies to Collections. Technically speaking, that means interfaces that support the _NewEnum method. If you try ENUM on an interface that doesn’t support it, you’ll get a warning message.

 

Application crashes

COM programming is not very simple so when you venture into it, you may experience some application crashes when debugging your programs. Don't worry, this is I think almost inevitable. Personally I've gone through hundreds of them and, thanks to the stability of recent Windows systems, I've never lost data or needed to restart my computer as a consequence of such a crash.

What application crashes indicate is VERY OFTEN that you've used an Interface while its .pointer is zero. So when you initialize an interface variable, it is wise to check that .pointer did receive a non-zero value before proceeding.

 

COMRESULT variable

The ComHelper include file declares a global variable (integer) called COMRESULT. When you access an interface property or method, COMRESULT is set so as to indicate if the operation succeeded or not.

The code for success is ZERO. Other values indicate that an error has occurred.

ComHelper also provides you with the WinErrorMsg function that translates the error code to a text message.

Example:
myInterface.invoke("DoThis")
if COMRESULT then showmessage WinErrorMsg(COMRESULT)

 

Unicode strings

Unicode has nothing to do with unicorns. It is a standard for coding character strings. This standard is different from the ANSI code that HotBasic uses.

Unfortunately, COM interfaces normally only recognize unicode strings. So when you use a string as a COM function parameter, it must be unicode. And likewise, the result of a COM function, when it is a string, is coded in unicode.

That means some coding and decoding is needed. ComHelper takes care of that conversion. It means that in simple cases, you don't need to worry. Examples:
tli.pointer=tli.getnum("TypeLibInfoFromFile", filename)
defstr sname=tli.getstr("Name")

Just use your normal HotBasic "filename" string. The conversion to unicode is included in ComHelper. Similarly, "Name" returns an ANSI string that you can display, write to a text file, etc. The conversion is already taken care of.

HOWEVER, things may not always be so simple. For instance:

 - some information might be lost in those ANSI <--> unicode conversions if special characters or foreign characters are used;

 - as an exception to the rule, some COM objects may actually use ANSI strings and they'll be surprised to receive Unicode (so far I haven’t seen any but who knows?).

So if you're getting strange results when working with strings, you may want to try and disable the unicode conversion by tweaking the routines inside the COM Generics routines.

Using foreign character sets

You may want to send text with foreign encoding to an interface. For instance, sending a string of Chinese characters to a speech synthesizer. In order to do that, you must specify that strings will have a specific encoding. Every encoding has a serial number, called its code page. Look up on the internet or somewhere what the code page is. For instance, it is 0 for ANSI strings, 1200 for unicode, 936 for Chinese GB, 65001 for UTF-8, etc.

Then set the COMencoding internal variable to that value. ComHelper will perform the necessary conversions from and to unicode. Example:
COMencoding = 936 'Chinese GB encoding
defint x = voice.getnum("Speak", "ÎÒÊÇ·¨¹úÈË¡£", 0)
COMencoding = 0
'reset to default

 

Conclusion of this section

If you've read through this section, unless you're already a COM expert, you should feel a little dizzy. That's normal: COM programming is oh so powerful but not so simple. At least, you should now be ready to take a look at the examples that come with ComHelper and, more important, start experimenting by yourself.

And remember: the internet is an invaluable source of code snippets and documentation to start with.

Now you may want more info on:

. Using a helper file without the Use Invoke option

. Using an ActiveX helper file

. Using COM events