Использование Microsoft ScriptControl
Пишем Invoke
Первая часть задачи выполнена: мы проинформировали OLE о наличии в нашем сервере автоматизации поддерживаемых функций. Теперь необходимо реализовать метод Invoke для выполнения этих функций. Из соображений модульности Invoke выполняет подготовительную работу со списком параметров и вызывает метод DoInvoke, в котором мы осуществляем трансляцию DispID в обращения к методам класса VCL.
В методе используются три служебные функции:
- проверяет количество переданных аргументов.
- проверяет соответствие аргумента с заданным индексом заданному типу.
- получает целое число из аргумента с заданным индексом.
TVCLProxy.DoInvoke(DispID: Integer; IID: TGUID; LocaleID: Integer; Flags: Word; dps: TDispParams; pDispIds: PDispIdList; VarResult, ExcepInfo, ArgErr: Pointer ): HResult; S: ; Put: Boolean; I: Integer; P: TPersistent; B: Boolean; OutValue: OleVariant;
Result := S_OK; DispId
Для функции Controls мы должны проверить, что передан один параметр. Если он строковый — поиск дочернего компонента будет происходить по имени, в противном случае — по индексу. Если компонент найден – вызывается функция FScriptControl.GetProxy, которая проверяет наличие «представителя» у этого компонента, при необходимости создает его и возвращает интерфейс IDispatch. Такой алгоритм необходим для корректной работы оператора VBScript Is, который сравнивает две ссылки на объект и выдает истину в случае, если речь идет об одном и том же объекте, например:
Dim A Dim B Set A = C Set B = C If A is B Then ...
Если создавать экземпляр класса TVCLProxy каждый раз, когда запрашивается ссылка, эти экземпляры окажутся разными и оператор Is не будет работать.
DISPID_CONTROLS: // Вызвана функция Controls FOwner TWinControl // Проверяем параметр CheckArgCount(dps.cArgs, [1], TRUE); P := ; _ValidType(0, VT_BSTR, FALSE) // Если параметр - строка - ищем дочерний компонент // с таким именем S := dps.rgvarg^[pDispIds^[0]].bstrVal; I := 0 Pred(ControlCount) CompareText(S, Controls[I].Name) = 0 P := Controls[I]; Break; ; // Иначе - параметр - число, берем компонент по индексу I := _IntValue(0); P := Controls[I]; ; Assigned(P) // Компонент не найден EInvalidParamType.Create(''); // Возвращаем интерфейс IDispatch для найденного компонента OleVariant(VarResult^) := FScriptControl.GetProxy(P); ; ;
Функция Count должна вызываться без параметров и призвана возвращать количество элементов в запрашиваемом объекте.
DISPID_COUNT: // Вызвана функция Count // Проверяем, что не было параметров CheckArgCount(dps.cArgs, [0], TRUE); FOwner TWinControl // Возвращаем количество дочерних компонентов OleVariant(VarResult^) := TWinControl(FOwner).ControlCount; FOwner TCollection // Возвращаем количество элементов коллекции OleVariant(VarResult^) := TCollection(FOwner).Count FOwner TStrings // Возвращаем количество строк OleVariant(VarResult^) := TStrings(FOwner).Count; ;
Метод Add добавляет элемент к объекту-владельцу «представителя». Обратите внимание на реализацию необязательных параметров для TWinControl и TStrings.
DISPID_ADD: // Вызвана функция Add FOwner TWinControl // Проверяем количество аргументов CheckArgCount(dps.cArgs, [2,3], TRUE); // Проверяем типы обязательных аргументов _ValidType(0, VT_BSTR, TRUE); _ValidType(1, VT_BSTR, TRUE); // Третий аргумент - необязательный, если он не задан - // полагаем FALSE (dps.cArgs = 3) _ValidType(2, VT_BOOL, TRUE) B := dps.rgvarg^[pDispIds^[0]].vbool B := FALSE; // Вызываем метод для создания компонента DoCreateControl(dps.rgvarg^[pDispIds^[0]].bstrVal, dps.rgvarg^[pDispIds^[1]].bstrVal, B); FOwner TCollection // Добавляем компонент P := TCollection(FOwner).Add; // И возвращаем его интерфейс IDispatch OleVariant(varResult^) := FScriptControl.GetProxy(P); FOwner TStrings // Проверяем наличие аргументов CheckArgCount(dps.cArgs, [1,2], TRUE); // Проверяем, что аргумент – строка _ValidType(0, VT_BSTR, TRUE); dps.cArgs = 2 then // Второй аргумент - позиция в списке I := _IntValue(1) // Если его нет - вставляем в конец I := TStrings(FOwner).Count; // Добавляем строку TStrings(FOwner).Insert(I, dps.rgvarg^[pDispIds^[0]].bstrVal); ; ;
И наконец, функция HasProperty проверяет наличие у объекта VCL опубликованного свойства с заданным именем.
DISPID_HASPROPERTY: // Вызвана функция HasProperty // Проверяем наличие аргумента CheckArgCount(dps.cArgs, [1], TRUE); // Проверяем тип аргумента _ValidType(0, VT_BSTR, TRUE); S := dps.rgvarg^[pDispIds^[0]].bstrVal; // Возвращаем True, если свойство есть OleVariant(varResult^) := Assigned(GetPropInfo(FOwner.ClassInfo, S)); ;
Если ни один из DispID не обработан — значит DispID содержит адрес структуры TPropInfo свойства VCL
// Это не наша функция, значит это свойство // Проверяем Flags, чтобы узнать, устанавливается значение // или получается Put := (Flags DISPATCH_PROPERTYPUT) <> 0; Put // Устанавливаем значение // Проверяем наличие аргумента CheckArgCount(dps.cArgs, [1], TRUE); // И устанавливаем свойство Result := SetVCLProperty(PPropInfo(DispId), dps.rgvarg^[pDispIds^[0]]) // Получаем значение DispId = 0 // DispId = 0 - требуется свойство по умолчанию // Возвращаем свой IDispatch OleVariant(VarResult^) := Self IDispatch; Exit; ; // Получаем значение свойства Result := GetVCLProperty(PPropInfo(DispId), dps, pDispIds, OutValue); Result = S_OK // Получили успешно - сохраняем результат OleVariant(VarResult^) := OutValue; ; ; ;
Добавление собственных функций
Для добавления функций, которые требуются для решения ваших задач, необходимо выполнить ряд простых шагов:
- В методе GetIdsOfNames проанализировать имя запрашиваемой функции и определить, может ли она быть вызвана для объекта, на который ссылается FOwner.
- Если функция может быть вызвана, вы должны вернуть уникальный DispID, в противном случае – присвоить Result := DISP_E_UNKNOWNNAME.
- В методе Invoke необходимо обнаружить свой DispID, проверить корректность переданных параметров, получить их значения и выполнить действие.
Обработка событий в компонентах VCL
Важным дополнением к реализуемой функциональности является возможность ассоциировать процедуру на VBScript с событием в компоненте VCL, таким как OnEnter, OnClick или OnTimer. Для этого добавим в компонент TVCLScriptControl методы, которые будут служить обработчиками созданных в коде скрипта компонентов.
TVCLScriptControl = (TScriptControl) … OnChangeHandler(Sender: TObject); OnClickHandler(Sender: TObject); OnEnterHandler(Sender: TObject); OnExitHandler(Sender: TObject); OnTimerHandler(Sender: TObject); ;
В методе DoCreateControl, который вызывается из DoInvoke при обработке метода «Add», реализуем подключение соответствующих обработчиков событий создаваемого компонента к созданным методам.
TVCLProxy.DoCreateControl(AName, AClassName: WideString; WithEvents: Boolean); SetHandler(Control: TPersistent; Owner: TObject; Name: String); // Функция устанавливает обработчик события Name на метод формы // с именем Name + 'Handler' Method: TMethod; PropInfo: PPropInfo; // Получаем информацию RTTI PropInfo := GetPropInfo(Control.ClassInfo, Name); Assigned(PropInfo) // Получаем адрес обработчика Method.Code := FScriptControl.MethodAddress(Name + 'Handler'); Assigned(Method.Code) // Обработчик есть Method.Data := FScriptControl; // Устанавливаем обработчик SetMethodProp(Control, PropInfo, Method); ; ; ;
ThisClass: TControlClass; C: TComponent; NewOwner: TCustomForm; // Назначаем свойство Owner на форму (FOwner TCustomForm) NewOwner := GetParentForm(FOwner TControl) NewOwner := FOwner TCustomForm; // Получаем класс создаваемого компонента ThisClass := TControlClass(GetClass(AClassName)); // Создаем компонент C := ThisClass.Create(NewOwner); // Назначаем имя C.Name := AName; C TControl // Назначаем свойство Parent TControl(C).Parent := FOwner TWinControl; WithEvents // Устанавливаем обработчики SetHandler(C, NewOwner, 'OnClick'); SetHandler(C, NewOwner, 'OnChange'); SetHandler(C, NewOwner, 'OnEnter'); SetHandler(C, NewOwner, 'OnExit'); SetHandler(C, NewOwner, 'OnTimer'); ; // Создаем класс, реализующий интерфейс Idispatch, и добавляем его // в пространство имен TScriptControl FScriptControl.RegisterClass(AName, C); ;
Таким образом, если третьим параметром метода «Add» будет задано True, то TVCLScriptControl установит обработчики событий OnClick, OnChange, OnEnter, OnExit и OnTimer на свои методы, реализованные следующим образом:
TVCLScriptControl.OnClickHandler(Sender: TObject); RunProc((Sender TComponent).Name + '_' + 'OnClick'); ;
Примером использования данной функциональности может служить следующий код:
Sub Main() Self.Add "Timer1", "TTimer", TRUE With Timer1 .Interval = 1000 .Enabled = True End With End Sub Sub Timer1_OnTimer() Self.Caption = CStr(Time) End Sub
Если требуется назначить обработчики событий, имеющихся на форме компонентов, это может быть сделано в коде
Button1.OnClick := ScriptControl1.OnClickHandler;
или путем реализации соответствующего метода в GetIdsOfNames и Invoke.
Получение свойств
Для получения свойств классов VCL служит метод GetVCLProperty. В нем осуществляется трансляция типов данных Object Pascal в типы данных OLE.
TVCLProxy.GetVCLProperty(PropInfo: PPropInfo; dps: TDispParams; PDispIds: PDispIdList; Value: OleVariant ): HResult; I, J, K: Integer; S: String; P, P1: TPersistent; Data: PTypeData; DT: TDateTime; TypeInfo: PTypeInfo; Result := S_OK; PropInfo^.PropType^.Kind
Для данных строкового и целого типа Delphi осуществляет автоматическую трансляцию.
tkString, tkLString, tkWChar, tkWString: // Символьная строка Value := GetStrProp(FOwner, PropInfo);
tkChar, tkInteger: // Целое число Value := GetOrdProp(FOwner, PropInfo);
Для перечисленных типов OLE не имеет прямых аналогов. Поэтому для всех типов, кроме Boolean, будем передавать символьную строку с именем соответствующей константы. Для Boolean имеется подходящий тип данных, и этот случай необходимо обрабатывать отдельно.
tkEnumeration: // Проверяем, не Boolean ли это CompareText(PropInfo^.PropType^.Name, 'BOOLEAN') = 0 // Передаем как Boolean Value := Boolean(GetOrdProp(FOwner, PropInfo)); // Остальные - передаем как строку I := GetOrdProp(FOwner, PropInfo); Value := GetEnumName(PropInfo^.PropType^, I); ; ;
Самым сложным случаем является свойство объектного типа. Нормальным поведением будет возврат интерфейса IDispatch, позволяющего OLE обращаться к методам класса, на который ссылается свойство. Однако для некоторых классов, имеющих свойства «по умолчанию», таких как TStrings и TСollection, свойство может быть запрошено с индексом. В этом случае следует выдать соответствующий индексу элемент. В то же время, будучи запрошенным без индекса, свойство должно выдать интерфейс IDispatch для работы с экземпляром TCollection или TStrings.
tkClass: // Получаем значение свойства P := TPersistent(GetOrdProp(FOwner, PropInfo)); Assigned(P) (P TCollection) (dps.cArgs = 1) // Запрошен элемент коллекции с индексом (есть параметр) ValidType(dps.rgvarg^[pDispIds^[0]], VT_BSTR, FALSE) // Параметр строковый, ищем элемент по свойству // DisplayName S := dps.rgvarg^[pDispIds^[0]].bstrVal; P1 := ; I := 0 Pred(TCollection(P).Count) CompareText(S, TCollection(P).Items[I].DisplayName) = 0 P1 := TCollection(P).Items[I]; Break; ; Assigned(P1) // Найден - возвращаем интерфейс IDispatch Value := FScriptControl.GetProxy(P1) // Не найден Result := DISP_E_MEMBERNOTFOUND; // Параметр целый, возвращаем элемент по индексу I := IntValue(dps.rgvarg^[pDispIds^[0]]); (I >= 0) and (I < TCollection(P).Count) P := TCollection(P).Items[I]; Value := FScriptControl.GetProxy(P); Result := DISP_E_MEMBERNOTFOUND; ;
Для класса TStrings результатом будет не интерфейс, а строка, выбранная по имени или по индексу.
Assigned(P) (P TStrings) (dps.cArgs = 1) // Запрошен элемент из Strings с индексом (есть параметр) ValidType(dps.rgvarg^[pDispIds^[0]], VT_BSTR, FALSE) // Параметр строковый - возвращаем значение свойства // Values S := dps.rgvarg^[pDispIds^[0]].bstrVal; Value := TStrings(P).Values[S]; // Параметр целый, возвращаем строку по индексу I := IntValue(dps.rgvarg^[pDispIds^[0]]); (I >= 0) (I < TStrings(P).Count) Value := TStrings(P)[I] Result := DISP_E_MEMBERNOTFOUND; ; // Общий случай, возвращаем интерфейс IDispatch свойства Assigned(P) Value := FScriptControl.GetProxy(P) // Или Unassigned, если оно = NIL Value := Unassigned; ;
У чисел с плавающей точкой также есть особенный тип данных – TDateTime. Его необходимо обрабатывать иначе, чем остальные числа с плавающей точкой, поскольку у него в OLE есть отдельный тип данных — OleDate.
tkFloat: (PropInfo^.PropType^ = System.TypeInfo(TDateTime)) (PropInfo^.PropType^ = System.TypeInfo(TDate)) //Помещаем значение свойства в промежуточную // переменную типа TDateTime DT := GetFloatProp(FOwner, PropInfo); Value := DT; Value := GetFloatProp(FOwner, PropInfo); ;
В случае свойства типа «набор» (Set), не имеющего аналогов в OLE, будем возвращать строку с установленными значениями набора, перечисленными через запятую.
tkSet: // Получаем значение свойства (битовая маска) I := GetOrdProp(FOwner, PropInfo); // Получаем информацию RTTI Data := GetTypeData(PropInfo^.PropType^); TypeInfo := Data^.CompType^; // Формируем строку с набором значений S := ''; I <> 0 K := 0 31 J := 1 K; (J I) = J S := S + GetEnumName(TypeInfo, K) + ','; ; // Удаляем запятую в конце System.Delete(S, Length(S), 1); ; Value := S; ;
И наконец, с типом Variant не возникает никаких сложностей.
tkVariant: Value := GetVariantProp(FOwner, PropInfo); // Остальные типы не поддерживаются Result := DISP_E_MEMBERNOTFOUND; ; ;
Установка свойств
Для установки свойств классов VCL служит метод SetVCLProperty. В нем осуществляется обратная трансляция типов данных OLE в типы данных Object Pascal.
TVCLProxy.SetVCLProperty(PropInfo: PPropInfo; Argument: TVariantArg): HResult; I, J, K, CommaPos: Integer; GoodToken: Boolean; S, S1: ; DT: TDateTime; ST: TSystemTime; IP: IQueryPersistent; Data, TypeData: PTypeData; TypeInfo: PTypeInfo; Result := S_OK; PropInfo^.PropType^.Kind
Главным отличием этого метода от SetVCLProperty является необходимость проверки типа данных передаваемого параметра.
tkChar, tkString, tkLString, tkWChar, tkWString: // Проверяем тип параметра ValidType(Argument, VT_BSTR, TRUE); // И устанавливаем свойство SetStrProp(FOwner, PropInfo, Argument.bstrVal); ;
Для целочисленных свойств добавим еще один сервис (если свойство имеет тип TCursor или Tcolor) — обеспечим трансляцию символьной строки с соответствующим названием константы в целочисленный идентификатор.
tkInteger: // Проверяем тип свойства на TCursor, TColor, // если он совпадает и передано символьное значение, // пытаемся получить его идентификатор (CompareText(PropInfo^.PropType^.Name, 'TCURSOR') = 0) (Argument.vt = VT_BSTR) IdentToCursor(Argument.bstrVal, I) Result := DISP_E_BADVARTYPE; Exit; ; (CompareText(PropInfo^.PropType^.Name, 'TCOLOR') = 0) (Argument.vt = VT_BSTR) IdentToColor(Argument.bstrVal, I) Result := DISP_E_BADVARTYPE; Exit; ; // Просто цифра I := IntValue(Argument); // Устанавливаем свойство SetOrdProp(FOwner, PropInfo, I); ;
Для перечисленных типов, за исключением Boolean, значение передается в виде символьной строки, а Boolean, как и раньше, обрабатывается отдельно.
tkEnumeration: // Проверяем на тип Boolean - для него в VBScript есть // отдельный тип данных CompareText(PropInfo^.PropType^.Name, 'BOOLEAN') = 0 // Проверяем тип данных аргумента ValidType(Argument, VT_BOOL, TRUE); // Это свойство Boolean - получаем значение и значение SetOrdProp(FOwner, PropInfo, Integer(Argument.vBool)); // Перечисленный тип передается в виде символьной строки // Проверяем тип данных аргумента ValidType(Argument, VT_BSTR, TRUE); // Получаем значение S := Trim(Argument.bstrVal); // Переводим в Integer I := GetEnumValue(PropInfo^.PropType^, S); // Если успешно - устанавливаем свойство I >= 0 SetOrdProp(FOwner, PropInfo, I) EInvalidParamType.Create(''); ; ;
При установке объектного свойства необходимо получить ссылку на класс Delphi, представителем которого является переданный интерфейс IDispatch. Для этой цели служит ранее определенный нами интерфейс IQueryPersistent. Запросив его у объекта-представителя, мы можем получить ссылку на объект VCL и корректно установить свойство.
tkClass: // Проверяем тип данных - должен быть интерфейс IDispatch ValidType(Argument, VT_DISPATCH, TRUE); Assigned(Argument.dispVal) // Передано непустое значение // Получаем интерфейс IQueryPersistent IP := IDispatch(Argument.dispVal) IQueryPersistent; // Получаем ссылку на класс, представителем которого // является интерфейс I := Integer(IP.GetPersistent); // Иначе - очищаем свойство I := 0; // Устанавливаем значение SetOrdProp(FOwner, PropInfo, I); ;
Для чисел с плавающей точкой основной проблемой является отработка свойства типа TDateTime. Дополнительно обеспечим возможность установить это свойство в виде символьной строки. При установке свойства типа TDateTime необходимо обеспечить трансляцию его из формата TOleDate в TDateTime.
tkFloat: (PropInfo^.PropType^ = System.TypeInfo(TDateTime)) (PropInfo^.PropType^ = System.TypeInfo(TDate)) // Проверяем тип данных аргумента Argument.vt = VT_BSTR DT := StrToDate(Argument.bstrVal); ValidType(Argument, VT_DATE, TRUE); VariantTimeToSystemTime(Argument.date, ST) <> 0 DT := SystemTimeToDateTime(ST) Result := DISP_E_BADVARTYPE; Exit; ; ; SetFloatProp(FOwner, PropInfo, DT); // Проверяем тип данных аргумента ValidType(Argument, VT_R8, TRUE); // Устанавливаем значение SetFloatProp(FOwner, PropInfo, Argument.dblVal); ; ;
Наиболее сложным случаем является установка данных типа «набор» (Set). Необходимо выделить из переданной символьной строки разделенные запятыми элементы, для каждого из них – проверить, является ли он допустимым для устанавливаемого свойства, и установить соответствующий бит в числе, которое будет установлено в качестве свойства.
tkSet: // Проверяем тип данных, должна быть символьная строка ValidType(Argument, VT_BSTR, TRUE); // Получаем данные S := Trim(Argument.bstrVal); // Получаем информацию RTTI Data := GetTypeData(PropInfo^.PropType^); TypeInfo := Data^.CompType^; TypeData := GetTypeData(TypeInfo); I := 0; Length(S) > 0 // Проходим по строке, выбирая разделенные запятыми // значения идентификаторов CommaPos := Pos(',', S); CommaPos = 0 CommaPos := Length(S) + 1; S1 := Trim(System.Copy(S, 1, CommaPos - 1)); System.Delete(S, 1, CommaPos); Length(S1) > 0 // Поверяем, какому из допустимых значений соответствует // полученный идентификатор K := 1; GoodToken := FALSE; J := TypeData^.MinValue TypeData^.MaxValue CompareText(S1, GetEnumName(TypeInfo , J)) = 0 // Идентификатор найден, добавляем его в маску I := I K; GoodToken := TRUE; ; K := K 1; ; GoodToken // Идентификатор не найдет Result := DISP_E_BADVARTYPE; Exit; ; ; ; // Устанавливаем значение свойства SetOrdProp(FOwner, PropInfo, I); ;
Свойство типа Variant установить несложно.
tkVariant: // Проверяем тип данных аргумента ValidType(Argument, VT_VARIANT, TRUE); // Устанавливаем значение SetVariantProp(FOwner, PropInfo, Argument.pvarVal^); ; // Остальные типы данных OLE не поддерживаются Result := DISP_E_MEMBERNOTFOUND; ; ;
Таким образом, мы реализовали полную функциональность по трансляции вызовов OLE в обращения к свойствам VCL. Наш компонент может динамически создавать другие компоненты на форме, обращаться к их свойствам и даже обрабатывать возникающие в них события.