- Интерфейсы и TComponent
- Использование интерфейсоввнутри программы
- Использование интерфейсов для реализации Plug-In
- COM-сервер, структура и использование
Интерфейсы и TComponent
В базовом классе VCL TComponent имеется полный набор методов, позволяющих реализовать интерфейс IUnknown, хотя сам класс данный интерфейс не реализует. Это позволяет наследникам TComponent реализовывать интерфейсы, не заботясь о реализации IUnknown. Однако методы TComponent._AddRef и TComponent._Release на этапе выполнения программы не реализуют механизм подсчета ссылок, и, следовательно, для классов-наследников TComponent, реализующих интерфейсы, не действует автоматическое управление памятью. Это позволяет запрашивать у них интерфейсы, не опасаясь, что объект будет удален из памяти при выходе переменной за область видимости. Таким образом, следующий код совершенно корректен и безопасен:
type IGetData = interface ['{B5266AE0-5E77-11D4-84DD-9153115ABFC3}'] function GetData: String; end; TForm1 = class(TForm, IGetData) private function GetData: String; end; var I: Integer; GD: IGetData; S: String; begin S := ''; for I := 0 to Pred(Screen.FormCount) do begin if Screen.Forms[I].GetInterface(IGetData, GD) then S := S + GD.GetData + #13; end; ShowMessage(S); end;
Этот код проверяет наличие у всех форм в приложении возможности реализации интерфейса IGetData и в случае, если форма реализует этот интерфейс, вызывает его метод.
Использование интерфейсов внутри программы
Рассмотренное выше поведение TComponent позволяет строить просто и без потери строгой типизации, связывать компоненты приложения, а также единообразно вызывать их методы, не требуя, чтобы компоненты были унаследованы от общего предка. Достаточно лишь реализовать в компоненте интерфейс, а в вызывающей программе проверить его наличие.
В качестве примера рассмотрим MDI-приложение, имеющее много различных форм и единую панель инструментов. Предположим, что на этой панели инструментов имеются команды «Сохранить», «Загрузить» и «Очистить», однако каждое из окон реагирует на эти команды по-разному.
Создадим модуль с объявлениями интерфейсов:
unit ToolbarInterface; interface type TCommandType = (ctSave, ctLoad, ctClear); TCommandTypes = set of TCommandType; TSaveType = (stSave, stSaveAS); IToolBarCommands = interface ['{B5266AE1-5E77-11D4-84DD-9153115ABFC3}'] function SupportedCommands: TCommandTypes; function Save(AType: TSaveType): Boolean; procedure Load; procedure Clear; end; implementation end.
Интерфейс IToolBarCommands описывает набор методов, которые должны реализовать формы, поддерживающие работу с панелью кнопок. Метод SupportedCommands возвращает список поддерживаемых формой команд.
Создадим три дочерние формы — Form2, Form3 и Form4 — и установим им свойство FormStyle = fsMDIChild.
Form2 умеет выполнять все три команды:
type TForm2 = class(TForm, IToolBarCommands) private function SupportedCommands: TCommandTypes; function Save(AType: TSaveType): Boolean; procedure Load; procedure Clear; end; { TForm2 } procedure TForm2.Clear; begin ShowMessage('TForm2.Clear'); end; procedure TForm2.Load; begin ShowMessage('TForm2.Load'); end; function TForm2.Save(AType: TSaveType): Boolean; begin ShowMessage('TForm2.Save'); Result := TRUE; end; function TForm2.SupportedCommands: TCommandTypes; begin Result := [ctSave, ctLoad, ctClear] end;
Form3 умеет выполнять только команду Clear:
type TForm3 = class(TForm, IToolBarCommands) private function SupportedCommands: TCommandTypes; function Save(AType: TSaveType): Boolean; procedure Load; procedure Clear; end; { TForm3 } procedure TForm3.Clear; begin ShowMessage('TForm3.Clear'); end; procedure TForm3.Load; begin // Метод ничего не делает, но должен присутствовать // для корректной реализации интерфейса end; function TForm3.Save(AType: TSaveType): Boolean; begin end; function TForm3.SupportedCommands: TCommandTypes; begin Result := [ctClear] end;
И наконец, Form4 вообще не реализует интерфейс IToolBarCommands и не откликается ни на одну команду.
На главной форме приложения поместим ActionList и создадим три компонента TAction. Кроме того, разместим на ней TToolBar и назначим ее кнопкам соответствующие TAction.
type TForm1 = class(TForm) ToolBar1: TToolBar; ImageList1: TImageList; ActionList1: TActionList; acLoad: TAction; acSave: TAction; acClear: TAction; tbSave: TToolButton; tbLoad: TToolButton; tbClear: TToolButton; procedure acLoadExecute(Sender: TObject); procedure ActionList1Update(Action: TBasicAction; var Handled: Boolean); procedure acSaveExecute(Sender: TObject); procedure acClearExecute(Sender: TObject); end;
Наиболее интересен метод ActionList1Update, в котором проверяются поддерживаемые активной формой команды и настраивается интерфейс главной формы. Если нет активной дочерней формы либо она не поддерживает интерфейс IToolBarCommands, все команды запрещаются, в противном случае — разрешаются только поддерживаемые формой команды.
procedure TForm1.ActionList1Update(Action: TBasicAction; var Handled: Boolean); var Supported: TCommandTypes; TC: IToolBarCommands; begin if Assigned(ActiveMDIChild) and ActiveMDIChild.GetInterface(IToolBarCommands, TC) then Supported := TC.SupportedCommands else Supported := []; acSave.Enabled := ctSave in Supported; acLoad.Enabled := ctLoad in Supported; acClear.Enabled := ctClear in Supported; end;
При активизации команд проверяется наличие активной дочерней формы, у нее запрашивается интерфейс IToolBarCommands и вызывается соответствующий ему метод:
procedure TForm1.acLoadExecute(Sender: TObject); var TC: IToolBarCommands; begin if Assigned(ActiveMDIChild) and ActiveMDIChild.GetInterface(IToolBarCommands, TC) then TC.Load; end; procedure TForm1.acSaveExecute(Sender: TObject); var TC: IToolBarCommands; begin if Assigned(ActiveMDIChild) and ActiveMDIChild.GetInterface(IToolBarCommands, TC) then if not TC.Save(stSaveAS) then ShowMessage(‘Not Saved !!!’); end; procedure TForm1.acClearExecute(Sender: TObject); var TC: IToolBarCommands; begin if Assigned(ActiveMDIChild) and ActiveMDIChild.GetInterface(IToolBarCommands, TC) then TC.Clear; end;
Того же эффекта можно добиться и другими методами (например, унаследовав все дочерние формы от единого предка либо обмениваясь с ними сообщениями), однако эти методы имеют ряд существенных недостатков.
Так, при обмене сообщениями мы теряем строгую типизацию и вынуждены передавать параметры через целые числа, а при визуальном наследовании мы привязываем себя к родительскому классу, что не всегда удобно. К тому же можно определить множество интерфейсов и реализовывать в каждой из дочерних форм лишь необходимые, а в случае наследования все формы должны будут реализовывать все общие методы.
Использование интерфейсов для реализации Plug-In
Еще более удобно использовать интерфейсы для реализации модулей расширения программы (Plug-In). Как правило, такой модуль экспортирует ряд известных главной программе методов, которые могут быть из него вызваны. В то же время зачастую ему необходимо обращаться к каким-либо функциям вызывающей программы. И то и другое легко реализуется при помощи интерфейсов.
В качестве примера реализуем несложную программу, использующую Plug-In для загрузки данных.
Объявим интерфейсы модуля расширения и внутреннего API программы.
unit PluginInterface; interface type IAPI = interface ['{64CFF1E0-61A3-11D4-84DD-B18D6F94141F}'] procedure ShowMessage(const S: String); end; ILoadFilter = interface ['{64CFF1E1-61A3-11D4-84DD-B18D6F94141F}'] procedure Init(const FileName: String; API: IAPI); function GetNextLine(var S: String): Boolean; end; implementation end.
Этот модуль должен использоваться как в Plug-In, так и в основной программе и гарантирует использование ими идентичных интерфейсов.
Plug-In представляет собой DLL, экспортирующую функцию CreateFilter, возвращающую ссылку на интерфейс ILoadFilter. Главный модуль сначала должен вызвать метод Init, передав в него имя файла и ссылку на интерфейс внутреннего API, а затем вызывать метод GetNextLine до тех пор, пока он не вернет FALSE.
Рассмотрим код модуля расширения:
library ImpTxt; uses ShareMem, SysUtils, Classes, PluginInterface; {$R *.RES} type TTextFilter = class(TInterfacedObject, ILoadFilter) private FAPI: IAPI; F: TextFile; Lines: Integer; InitSuccess: Boolean; procedure Init(const FileName: String; API: IAPI); function GetNextLine(var S: String): Boolean; public destructor Destroy; override; end; { TTextFilter } procedure TTextFilter.Init(const FileName: String; API: IAPI); begin FAPI := API; {$I-} AssignFile(F, FileName); Reset(F); {$I+} InitSuccess := IOResult = 0; if not InitSuccess then API.ShowMessage('Ошибка инициализации загрузки'); end;
Метод Init выполняет две задачи: сохраняет ссылку на интерфейс API главного модуля для дальнейшего использования и пытается открыть файл с данными. Если файл открыт успешно – выставляется внутренний флаг InitSuccess.
function TTextFilter.GetNextLine(var S: String): Boolean; begin if InitSuccess then begin Inc(Lines); Result := not Eof(F); if Result then begin Readln(F, S); FAPI.ShowMessage('Загружено ' + IntToStr(Lines) + ' строк.'); end; end else Result := FALSE; end;
Метод GetNextLine считывает следующую строку данных и возвращает либо TRUE, если это удалось, либо FALSE — в случае окончания файла. Кроме того, при помощи API, предоставляемого главным модулем, данный метод информирует пользователя о ходе загрузки.
destructor TTextFilter.Destroy; begin FAPI := NIL; if InitSuccess then CloseFile(F); inherited; end;
В деструкторе мы обнуляем ссылку на API главного модуля, уничтожая его, и закрываем файл.
function CreateFilter: ILoadFilter;
begin Result := TTextFilter.Create; end;
Эта функция создает экземпляр класса, реализующего интерфейс ILoadFilter. Ссылок на экземпляр сохранять не нужно, он будет освобожден автоматически.
exports CreateFilter; // Функция должна быть экспортирована из DLL begin end.
Теперь полученную DLL можно использовать из основной программы.
type TAPI = class(TInterfacedObject, IAPI) procedure ShowMessage(const S: String); end; { TAPI } procedure TAPI.ShowMessage(const S: String); begin with (Application.MainForm as TForm1).StatusBar1 do begin SimpleText := S; Update; end; end;
Класс TAPI реализует API, предоставляемый модулю расширения. Функция ShowMessage выводит сообщения модуля в Status Bar главной формы приложения.
type TCreateFilter = function: ILoadFilter; procedure TForm1.LoadData(FileName: String); var PluginName: String; Ext: String; hPlugIn: THandle; CreateFilter: TCreateFilter; Filter: ILoadFilter; S: String; begin //Подготавливаем TMemo к загрузке данных: Memo1.Lines.Clear; Memo1.Lines.BeginUpdate; //Получаем имя модуля с фильтром для выбранного расширения файла. Описания модулей хранятся в файле plugins.ini в секции Filters в виде строк формата: //<расширение> = <имя модуля>, например: [Filters] TXT=ImpTXT.DLL try Ext := ExtractFileExt(FileName); Delete(Ext, 1, 1); with TIniFile.Create(ExtractFilePath(ParamStr(0)) + 'plugins.ini') do try PlugInName := ReadString('Filters', Ext, ''); finally Free; //Теперь попытаемся загрузить модуль и найти в нем функцию CreateFilter: hPlugIn := LoadLibrary(PChar(PluginName)); try CreateFilter := GetProcAddress(hPlugIn, 'CreateFilter'); if Assigned(CreateFilter) then begin //Функция найдена, создаем экземпляр фильтра и инициализируем его. Поскольку внутренний API реализован тоже как интерфейс — нет необходимости сохранять ссылку на него. Filter := CreateFilter; try Filter.Init(FileName, TAPI.Create); //Загружаем данные при помощи созданного фильтра: while Filter.GetNextLine(S) do Memo1.Lines.Add(S); //Перед выгрузкой DLL из памяти необходимо обязательно освободить ссылку на интерфейс Plug-In, иначе это произойдет по выходе из функции и вызовет Access Violation. finally Filter := NIL; end; end else raise Exception.Create('Не могу загрузить фильтр'); //Выгружаем DLL и обновляем TMemo: finally FreeLibrary(hPlugIn); end; finally Memo1.Lines.EndUpdate; end; end;
Таким образом, реализовать модули расширения для загрузки данных из форматов, отличающихся от текстового, и единообразно работать с ними несложно.
Преимущества данного способа становятся особенно очевидными при реализации сложных модулей расширения, интерфейс с которыми состоит из многих методов.
COM-сервер, структура и использование
Модель COM предоставляет возможность создания многократно используемых компонентов, независимых от языка программирования. Такие компоненты называются COM-серверами и представляют собой исполняемые файлы (EXE) или динамические библиотеки (DLL), специальным образом оформленные для обеспечения возможности их универсального вызова из любой программы, написанной на поддерживающем COM языке программирования. При этом COM-сервер может выполняться как в адресном пространстве вызывающей программы (In-Process-сервер), так и в виде самостоятельного процесса (Out-Of-Process-сервер) или даже на другом компьютере (Distributed COM). COM автоматически разрешает вопросы, связанные с передачей параметров (Marshalling) и согласованием потоковых моделей клиента и сервера.
Далее будут рассмотрены некоторые архитектурные вопросы, знание которых необходимо для работы с COM.