Friday, July 13, 2007

Variants and COM

Empty and Null


Variants can handle several datatypes and some special states as EMPTY and NULL. Before you
assign a value to a variant, it has the state EMPTY. There are a lot of functions to check the
state or type of a variant and there are predefined variants you can use.


uses Variants;

procedure VariantDemo;
var
vDemo: Variant;
bTest: Boolean;
begin
// EMPTY
vDemo := Unassigned; // assign EMPTY to variant
bTest := VarIsEmpty(vDemo); // check if variant is EMPTY
// NULL
vDemo := NULL; // assign NULL to variant
bTest := VarIsNull(vDemo); // check if variant is NULL
// numeric
vDemo := 8.8; // assign a float to variant
bTest := VarIsNumeric(vDemo); // check if variant is numeric
// text
vDemo := 'demo'; // assign a string to variant
bTest := VarIsStr(vDemo); // check if variant contains text
// COM methods can define obtional parameters. if you are
// working with typelibraries you have to pass a parameter
// nevertheless, then you can pass "EmptyParam"
vDemo := EmptyParam;
bTest := VarIsEmptyParam(vDemo);
end;

How to register a COM server


Before you can use and test your new created COM server, you have to register it. You can do
this with the Delphi menu Start\Register ActiveX-Server or you can register it by
yourself. It depends on the kind of server you have (in-process *.dll or out-of-process *.exe)
how to register the server.





















  Register Unregister
in-process (MyServer.dll) regsvr32 MyServer.dll regsvr32 /u MyServer.dll
out-of-process (MyServer.exe) MyServer.exe /regserver MyServer.exe /unregserver

If you often have to do with COM servers, it is very useful to be able to (un)register them
in the explorer. It's not difficult to extend the explorer's context menu, you can use this
small reg file to add a "Register" and an "UnRegister" entry.








Download regfile StoComRegister.zip

User actions while (un)registering a COM server


With an in-process server (DLL), you have the possiblility to run additional code at the
time of (un)registering. Creating a COM library, the delphi IDE will produce a projectfile like
this:


library Project1;

uses
ComServ;

exports
DllGetClassObject,
DllCanUnloadNow,
DllRegisterServer,
DllUnregisterServer;

{$R *.RES}

begin
end.

The functions "DllRegisterServer" and "DllUnregisterServer" are exported, and will be called
when the user or the setup registers the server. You can detour this call to take your own
actions, but make sure no errors can occur in this place.


library Project1;

uses
ComServ;

function CustomDllRegisterServer: HResult; stdcall;
begin
// call the standard function
Result := DllRegisterServer;
// execute your own code
// ...
end;

function CustomDllUnregisterServer: HResult; stdcall;
begin
// call the standard function
Result := DllUnregisterServer;
// execute your own code
// ...
end;

exports
DllGetClassObject,
DllCanUnloadNow,
CustomDllRegisterServer name 'DllRegisterServer',
CustomDllUnregisterServer name 'DllUnregisterServer';

{$R *.RES}

begin
end.


Modal forms in COM


When you are using modal forms in a COM server, you will miss the support of the menu
shortcuts and the automatic navigation between the controls with the TAB key. this is because
the "Application" object doesn't handle the windows messages, it is the window of the client
application.


// if you display a form from inside a COM server, you will miss the
// automatic navigation between the controls with the "TAB" key.
// the "KeyPreview" property of the form has to be set to "True".
procedure TForm1.FormKeyPress(Sender: TObject; var Key: Char);
var
bShift: Boolean;
begin
// check for tab key and switch focus to next or previous control.
// handle this in the KeyPress event, to avoid a messagebeep.
if (Ord(Key) = VK_TAB) then
begin
bShift := Hi(GetKeyState(VK_SHIFT)) <> 0;
SelectNext(ActiveControl, not(bShift), True);
Key := #0; // mark as handled
end;
end;

// if you display a form from inside a COM server, you will miss the
// support of the menu- and action- shortcuts like "<Ctrl><S>".
// the "KeyPreview" property of the form has to be set to "True".
procedure TForm1.FormKeyUp(Sender: TObject; var Key: Word; Shift: TShiftState);
const
AltMask = $20000000;
var
myMessage: TWMKey;
begin
// recreate the original "KeyUp" message
FillChar(myMessage, SizeOf(TWMKey), 0);
myMessage.Msg := WM_KEYUP;
myMessage.CharCode := Key;
if (ssAlt in Shift) then
myMessage.KeyData := AltMask;
// find and execute matching shortcut
if IsShortCut(myMessage) then
Key := 0; // mark as handled
end;

Using interfaces without COM


Normally you will use interfaces in combination with COM objects. In contrast to
conventional objects, a COM object supports reference counting and will free itself when the
last reference is released.



You can use interfaces for your conventional objects too, without supporting reference
counting and automatically freeing. Doing this, you have to pay attention to some special
facts.


ITest = interface(IInterface)
// press <ctrl><shift><g> to create your own GUID for each interface.
// this is necessary to implement the "QueryInterface" method.
['{CA51B752-0DF5-40D2-945C-A5CF2EAA3B31}']
procedure ShowText;
end;

TTest = class(TObject, ITest)
protected
FText: String;
// IInterface
function QueryInterface(const IID: TGUID; out Obj): HResult; stdcall;
function _AddRef: Integer; stdcall;
function _Release: Integer; stdcall;
// ITest
procedure ShowText;
end;

Because every interface inherites from the parent interface "IInterface" (same as windows
specific "IUnknown"), you have to support at least the three methods of "IInterface". This
example shows a standard implementation.


function TTest._AddRef: Integer;
begin
Result := -1; // no reference counting supported
end;

function TTest._Release: Integer;
begin
Result := -1; // no reference counting supported
end;

function TTest.QueryInterface(const IID: TGUID; out Obj): HResult;
begin
if GetInterface(IID, Obj) then
Result := S_OK
else
Result := E_NOINTERFACE;
end;

procedure TTest.ShowText;
begin
ShowMessage(FText);
end;

When you later use the object, you have to be careful, that at the moment of destruction, no
reference to the interface remains.



procedure WellUsed;
var
myTestObject: TTest;
pTestInterface: ITest;
begin
// creating the object
myTestObject := TTest.Create;
// get a reference to the interface of the object, this will implicitly call "_AddRef"
pTestInterface := myTestObject;
// ...
// release the reference to the interface, this will implicitly call "_Release"
pTestInterface := nil;
// freeing the object itself
myTestObject.Free;
end;

If you free the object, before the last reference to the interface is released, then the
implicitly call to "_Release" will call to a not existing object (Delphi will release the
interface automatically, if you don't do it yourself).


procedure WrongUsed;
var
myTestObject: TTest;
pTestInterface: ITest;
begin
myTestObject := TTest.Create;
pTestInterface := myTestObject;
// ...
// freeing the object with an existing reference
myTestObject.Free;
// this releasing of the interface will implicitly call "_Release", but there
// is no living object anymore.
pTestInterface := nil;
end;

Normally, calling a method of a not existing object will cause a runtime error, not in our
example. That's because no member of the object is used inside "_Release", as soon as you
access a member, you will get the expected error. So, first make sure you don't call a
"_Release" on a not existing object, then don't access members inside of "_Release".


function TTest._Release: Integer;
begin
// this will cause an error, if the reference is released after the object was freed.
FText := '';
Result := -1; // no reference counting supported
end;

No comments: