Delphi で CreateOleObject のイベント処理

Delphi の CreateOleObject で呼び出したオートメーションサーバのイベント処理

タイプライブラリから .pas を生成しろというコメントが散見されるほど、資料が少なく面倒でした.該当する GUID や DispID を調べる必要があります

ScriptControl を例に、TScriptControlEventSink クラスを作成して、OnError と OnTimeout イベントを関連付けしています

CreateOleObject と TScriptControlEventSink を利用したイベント追加
var
    ScriptControl: OleVariant;
    ScriptControlEventSink: TScriptControlEventSink;
begin
    ScriptControl := CreateOleObject('ScriptControl');
    try
        ScriptControl.Language := 'VBScript';
        ScriptControl.Timeout := 2000;
        ScriptControl.AllowUI := False;

        // Event
        ScriptControlEventSink := TScriptControlEventSink.Create(ScriptControl);
        try
            ScriptControlEventSink.OnError := Form1.OnScriptError;
            ScriptControlEventSink.OnTimeout := Form1.OnScriptTimeout;
        
            // ScriptControl.Run(...);
        
        finally
            ScriptControlEventSink.Free;
        end;
    finally
        ScriptControl := Unassigned;
    end;
end;

ScriptControlEventSync.pas
unit ScriptControlEventSync;

interface

uses
    ActiveX, Windows, ComObj, SysUtils, Classes;

type
    IScriptControlEvents = interface(IDispatch)
        ['{8B167D60-8605-11D0-ABCB-00A0C90FFFC0}']
        // procedure Error; safecall;
        // procedure Timeout; safecall;
    end;


    TScriptControlEventSink = class(TInterfacedObject, IUnknown, IDispatch)
    private
        FDispatch: IDispatch;
        FIID: TGUID;
        FConnection: integer;
        FOnError, FOnTimeout: TNotifyEvent;
    protected
        function QueryInterface(const IID: TGUID; out Obj): HResult; stdcall;
        function _AddRef: integer; stdcall;
        function _Release: integer; stdcall;
        function GetTypeInfoCount(out Count: integer): HResult; stdcall;
        function GetTypeInfo(Index, LocaleID: integer; out TypeInfo): HResult; stdcall;
        function GetIDsOfNames(const IID: TGUID; Names: Pointer; NameCount, LocaleID: integer; DispIDs: Pointer): HResult; stdcall;

        function Invoke(DispID: Integer; const IID: TGUID; LocaleID: Integer; Flags: Word; 
        	var Params; VarResult, ExcepInfo, ArgErr: Pointer): HResult; stdcall;
    public
        constructor Create(ADispatch: IDispatch);
        destructor Destroy; override;
        property OwnerDispatch: IDispatch read FDispatch;

        property OnError: TNotifyEvent read FOnError write FOnError;
        property OnTimeout: TNotifyEvent read FOnTimeout write FOnTimeout;
    end;


implementation

{ TScriptControlEventSink }

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

    if IsEqualGUID(IID, FIID) and GetInterface(IDispatch, Obj) then begin
        Result := S_OK;
    end;
end;

function TScriptControlEventSink._AddRef: integer;
begin
    Result := 2;
end;

function TScriptControlEventSink._Release: integer;
begin
    Result := 1;
end;

function TScriptControlEventSink.GetIDsOfNames(const IID: TGUID; Names: Pointer; NameCount, LocaleID: integer; DispIDs: Pointer): HResult;
begin
    Result := E_NOTIMPL;
end;

function TScriptControlEventSink.GetTypeInfo(Index, LocaleID: integer; out TypeInfo): HResult;
begin
    Result := E_NOTIMPL;
end;

function TScriptControlEventSink.GetTypeInfoCount(out Count: integer): HResult;
begin
    Count := 0;
    Result := S_OK;
end;



constructor TScriptControlEventSink.Create(ADispatch: IDispatch);
var
    ConnectionPointContainer: IConnectionPointContainer;
    ConnectionPoint: IConnectionPoint;
begin
    inherited Create;
    FDispatch := ADispatch;
    FIID := IScriptControlEvents;

    // InterfaceConnect(FDispatch, FIID, Self, FConnection);

    OleCheck(FDispatch.QueryInterface(IConnectionPointContainer, ConnectionPointContainer));
    OleCheck(ConnectionPointContainer.FindConnectionPoint(FIID, ConnectionPoint));
    OleCheck(ConnectionPoint.Advise(Self, FConnection));
end;

destructor TScriptControlEventSink.Destroy;
begin
    InterfaceDisconnect(FDispatch, FIID, FConnection);
    inherited Destroy;
end;


function TScriptControlEventSink.Invoke(DispID: Integer; const IID: TGUID; LocaleID: Integer; 
	Flags: Word; var Params; VarResult, ExcepInfo, ArgErr: Pointer): HResult;
begin
    case DispID of
        3000:
            if Assigned(FOnError) then begin
                FOnError(Self);
            end;
        3001:
            if Assigned(FOnTimeout) then begin
                FOnTimeout(Self);
            end;
    end;
    Result := S_OK;
end;

end.

EventSink 作成に必要なもの
サーバごとの イベント GUIDと各イベント名(無くても動きます)
(IScriptControlEvents で使用)

各イベントに割り当てられた DispID
(Invokeメソッドで使用)

これらの情報は、ScriptControl の場合、タイプライブラリから生成した MSScriptControl_TLB.pas のイベントのみが記載された部分にあります
イベントの Sender 
基本的に イベント発生時の Sender は任意で、元のフォームでもよいです.
上記例ではイベント発生時の Sender を Self = TScriptControlEventSink として、
TScriptControlEventSink.OwnerDispatch で元の Dispatchインターフェイスを呼び出せるようにしてます.イベント関数から元の ScriptControl  インターフェイスへ容易に接続できるのが目的です.
procedure TForm1.OnScriptTimeout(Sender: TObject);
var Timeout: Integer;
begin
    Timeout := OleVariant(TScriptControlEventSink(Sender).OwnerDispatch).Timeout;
    MsgDlg(Format('Script Timeout: %d msec', [Timeout]));
end;
    
引数のあるイベント
上記例では引数のないイベントのため、System.Classes.TNotifyEvent を使用していますが、引数がある場合、イベントの引数定義とInvokeでパラメータ処理が必要になります → 参考

InterfaceConnect の利用
System.Win.ComObj.InterfaceConnect は、QueryInterface, FindConnectionPoint, Advise を一度に処理できる便利な関数ですが、GUIDやイベント名が正しくない場合にエラーとして上がってきません.
処理が完了したかどうかは、最後の引数(Connection <> 0) を調べる必要があります.

参考文献

コメント