Интерфейс IDispatch
Объекты автоматизации представляют собой СОМ-объекты, которые используют интерфейс IDispatch. Данный интерфейс описан в модуле system следующим образом:
type
IDispatch = interface (IUnknown)
[' {00020400-0000-0000-СООО-000000000046} ' ]
function GetTypelnfoCount(out Count: Integer): Integer; stdcall;
function GetTypelnfo(Index, LocalelD: Integer; out Typelnfo):Integer; stdcall;
function GetlDsOfNames(const IID: TGUID; Names: Pointer; NameCount, LocalelD: Integer; "
DispIDs: Pointer): Integer; stdcall;
function Invoke(DispID: Integer; const IID: TGUID; LocalelD: Integer; Flags: Word;
var Params; VarResult, Exceptlnfo, ArgErr: Pointer): Integer;
end;
Основной функцией интерфейса IDispatch является метод invoke. Приложение-клиент может вызывать данный метод для выполнения определенных действий на сервере автоматизации. Для того чтобы указать, какой именно метод хочет вызвать клиент, он передает при вызове метода invoke параметр Displo. Этот параметр представляет собой число, которое называется идентификатор диспетчера (dispatch ID). Данное число указывает, какой именно метод должен исполняться на сервере. Следующий параметр метода invoke - параметр IID не используется. Параметр LocalelD содержит информацию о локализации. Параметр Flags указывает, как данный метод будет вызываться: для получения свойств, для установки свойств или обычным способом. Свойство Params содержит указатель на массив TDispparams. Данный массив хранит параметры, передаваемые методу. Параметр VarResult представляет собой указатель на olevariant, который содержит возвращаемое значение вызываемого метода (если такое имеется), а параметр Excepinfo - указатель на запись TExcepinfo, которая содержит информацию об ошибке, если метод invoke возвращает значение DISP_E_EXCEPTION. Последний параметр ArgError - это указатель на целое число, которое является индексом некорректного параметра в массиве Params. В данном случае метод invoke возвращает значение DISP_E_TYPEMISMATCH или DISP_E_PARAMNOTFOUND.
Следующий метод интерфейса invoke - метод GetlDsOfNames. Он применяется для получения идентификатора диспетчера, одного или нескольких методов по строкам имен этих методов. Параметр IID данного метода не используется. Параметр Names - это указатель на массив имен методов. Такой массив имеет тип PWideChar. Параметр NameCount содержит число, показывающее количество строк в массиве, на который указывает параметр Names. Параметр LocalelD содержит информацию о локализации. Параметр Displos - это указатель на массив целых чисел NameCount.
Метод GetTypelnfo применяется для получения информации о типе объектов автоматизации. Параметр index описывает получаемую информацию о типе и должен (обычно) быть равен нулю. Параметр LCID содержит ин формацию о локализации. Если метод выполнился успешно, то параметр Typeinfo будет содержать указатель iTypeinfo на информацию о типе объекта автоматизации.
Метод GetTypeinfoCount используется для получения числа интерфейсов информации о типе, поддерживаемых объектом автоматизации. Число возвращается в параметре count. Он может содержать два возможных значения: 0, если объект автоматизации не поддерживает информацию о типе, и 1, если поддерживает.
Обработка событий диспетчера автоматизации
Как только вы инсталлировали необходимый сервер автоматизации в палитру компонентов, вы можете использовать инспектор объектов для написания обработчиков событий. Обработка событий осуществляется так же, как и обработка событий других компонентов Delphi:
1. Поместите на форму сервер автоматизации из палитры компонентов.
2. Щелкните на компоненте, затем нажмите вкладку Events в окне инспектора объектов - вы увидите список событий данного компонента.
3. Дважды щелкните напротив нужного события, после чего Delphi сгенерирует заготовку для обработчика события, в который вы можете поместить свой код.
После того как вы напишете необходимые обработчики событий, вы можете приступать к подключению к серверу автоматизации.
Подключение к серверу автоматизации
Рассмотрим процесс подключения к серверу автоматизации на примере таких программ, как Microsoft Word, Microsoft Excel и Microsoft Outlook, входящих в состав популярного программного пакета Microsoft Office. Подключение к другим серверам автоматизации может немного отличаться, но принцип работы везде одинаков.
Прежде чем переходить непосредственно к подключению к серверу автоматизации, рассмотрим объектную модель Microsoft Office.
В Microsoft Office нет понятия наследование, вместо него используется так называемое встраивание. Встраивание помогает получать новые классы. Итак, в любом приложении Microsoft Office всегда имеется центральный базовый объект. Для Microsoft Word это word.Application, для Microsoft Excel - Excel.Application. Microsoft Outlook сам является базовым объектом и называется outLookAppiication, однако, несмотря на это, в объект Application встраиваются все остальные объекты, которые, в свою очередь, являются свойствами базового объекта.
Различные объекты приложений Microsoft Office имеют самые разнообразные методы, но некоторые из них одинаковы для разных приложений. К числу совпадающих методов относятся Run, Activate и др.
Примечание
Разница в этих методах все же есть. В различных приложениях Microsoft Office они имеют разные параметры. Действия методов тоже могут отличаться.
Рис. 3.14 показывает небольшую часть структуры объекта Microsoft Word Object.Более полную информацию об объектной архитектуре Microsoft Office можно найти в справочных материалах по Microsoft Office.
Рис. 3.14. Фрагмент структуры объекта Microsoft Word Object
Как только открывается новый документ, приложения Microsoft Office, автоматически создается каркас нового документа, который представляет собой набор библиотек с классами. Объекты этих классов будут доступны в данном документе. Задачей разработчика диспетчера автоматизации является получить доступ к корневому объекту сервера, выстроить цепочку доступа к встроенным объектам и правильно передать параметры.
Итак, базовым объектом любого приложения Microsoft Office является объект Application. Попробуем получить к нему доступ средствами Delphi.
Примечание
Все написанное ниже, справедливо для пятой (или выше) версии Delphi.
Итак, выполним следующие шаги:
1. Создадим новый проект Delphi с помощью пункта главного меню File/New Application (Файл/Новое приложение).
2. Поместим на форму компонент wordApplication с вкладки Servers палитры компонентов Delphi.
3. Установим свойстваа AutoConnect И AutoQuit компонента WordAppiication
в значение true (эти свойства отвечают за автоматическую загрузку и выгрузку из памяти сервера автоматизации после запуска приложения).
4. Запустим приложение с помощью пункта главного меню Run/Run (Запуск/Запуск).
В результате, хотя визуально ничего не наблюдается, кроме отображения формы, приложение проделало большую работу. Наше приложение запустило сервер автоматизации Microsoft Word. Чтобы убедиться в этом, достаточно посмотреть список задач, выполняемых Windows во время работы нашего приложения. Нажмите комбинацию клавиш <Ctrl>+<Alt>+<Del> во время работы приложения и убедитесь, что среди задач, выполняемых Windows, появилась задача Winword.exe.
В общих чертах, наше приложение выполнило следующее:
- при запуске приложения, в системном реестре Windows был найден сервер автоматизации WordApplication при помощи идентификатора (CLSID);
- запустилось приложение, находящееся по адресу, указанному в системном реестре Windows;
- сервер автоматизации предоставил нашему приложению (простейшему диспетчеру автоматизации) интерфейс, через который приложение может получить доступ к базовому объекту Microsoft Word.
Теперь закройте приложение и посмотрите еще раз на задачи, выполняемые Windows. Сервер автоматизации выгружен из памяти компьютера при помощи интерфейса IDispatch (данный интерфейс имеет метод _ADDRef, при помощи которого можно определить число клиентов, которые в настоящий момент пользуются услугами сервера).
Тот же результат можно получить, если использовать код, приведенный на листинге 3.1. Для этого разместите на форме две кнопки Start и Finish.
Листинг 3.1
unit Unitl;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics', Controls, Forms, Dialogs, StdCtrls, COMObj;
type
TForml = class(TForm)
Start: TButton;
Finish: TButton;
procedure StartClickfSender: TObject);
procedure FinishClick(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
end;
var
Forml: TForml;
wd:01eVariant;
fileName:string;
implementation {$R *.DFM}
procedure TForml.StartClick(Sender: TObject);
begin
try
fileName:=ExtractFilePath(Application.EXEName)+'report.DOC';
// Создаем объект интерфейса для доступа к серверу СОМ
wd:=Create01eObject('Word.Application');
// Проверка наличия методов и правильность передачи параметров будет осуществляться на стадии выполнения приложения
wd.application.documents.add;
wd.application.activedocument.range.insertAfter(now);
wd.application.activedocument.saveas(fileName);
except
end;
end;
procedure TForml.FinishClick(Sender: TObject);
begin
// Выгружаем сервер из памяти компьютера
wd.application.quit(true,0);
end;
end.
Обратите внимание на необходимость добавления модуля coMObj в раздел Uses.
Как вы можете видеть, вкладка Servers палитры компонентов содержит много компонентов. Кроме базовых объектов серверов автоматизации, данная вкладка содержит несколько вложенных объектов, таких как документ Microsoft Word (WordDocument), рабочая Книга Excel (ExcelWorkbook) И Др.
Отметим, что все компоненты, содержащиеся на вкладке Servers, являются наследниками класса TOLEServer, который, в свою очередь, происходит от класса TComponent. Класс TOLEServer - базовый класс для всех СОМ-серверов, которые можно получить путем импортирования библиотек типов серверов автоматизации. Данный класс имеет несколько свойств и методов, позволяющих управлять связью с СОМ-сервером. Например, свойство Autoconnect, которое, в случае, если оно имеет значение true, автоматически запускает СОМ-сервер и производит извлечение интерфейса для связи сервера и диспетчера автоматизации.
Рассмотрим очень важное свойство данного класса ConnectKind (тип подключаемого процесса). Это свойство используется методом Connect, который вызывается автоматически при AutoConnect=true. В табл. 3.6 представлены значения свойства ConnectKind.
Таблица 3.6. Значения, принимаемые свойством ConnectKind
Значения свойства
ConnectKind
CkRunningOrNew |
Описание
Диспетчер автоматизации подключается к уже существующему процессу. В случае, если процесс не запущен - запускает и подключается к нему. Этот вид взаимодействия между диспетчером и сервером автоматизации является наиболее часто применяемым. Данное значение свойства устанавливается по умолчанию |
||
CkNewInstance |
Диспетчер автоматизации в любом случае создает новый экземпляр сервера автоматизации |
||
CkRunninglnstance |
Соединение устанавливается только с уже запущенные сервером автоматизации, если такой, не обнаружен - генерируется ошибка |
||
CkRemote |
Применяется для установления соединения с сервером автоматизации, который располагается на удаленном компьютере в сети. При использовании данного значения свойства ConnectKind необходимо указать имя удаленного компьютера в свойстве RemoteMachineName |
||
CkAttachToInterface |
В данном случае соединение не создается, поэтому нельзя устанавливать значение true для свойства AutoConnect. Соединение с сервером в этом случае осуществляется при помощи метода ConnectTo |
||
Примечание
При помощи последнего значения свойства ConnectKind - CkAttachToInterface- можно последовательно подключать к уже существующему интерфейсу несколько компонентов, таких как wordDocument или WordFont. Установка данного значения свойства необходима, когда диспетчер автоматизации должен отслеживать события, которые происходят в сервере автоматизации.
Свойство ConnectKind можно устанавливать в окне инспектора объектов при помощи выпадающего списка, который появляется при щелчке мышью на данном свойстве.
Позднее и раннее связывание
Объекты автоматизации могут работать двумя способами, которые называются позднее связывание (late binding) и раннее связывание (early binding).
При позднем связывании необходимый метод сервера автоматизации вызывается клиентом внутри метода invoke интерфейса IDispatch. При использовании позднего связывания вызов метода не разрешен до момента его исполнения, а возможен только с помощью метода invoke. Во время компиляции вызов метода сервера автоматизации автоматически преобразуется в вызов метода IDispatch, invoke. Во время выполнения приложения метод invoke вызывает нужный метод сервера автоматизации.
Раннее связывание означает, что сервер автоматизации предоставляет свои методы при помощи пользовательского интерфейса, который является наследником интерфейса IDispatch. В данном случае диспетчер автоматизации может обращаться к методам сервера автоматизации напрямую, без вызова метода invoke интерфейса IDispatch. Использование раннего связывания позволяет ускорить работу объектов автоматизации.
Многие объекты автоматизации поддерживают так называемый двойной интерфейс (dual interface). Это означает, что такие объекты автоматизации позволяют вызывать методы как из метода invoke, так и из потомков интерфейса IDispatch. Серверы автоматизации, которые создаются при помощи Delphi, всегда поддерживают двойной интерфейс. Диспетчеры автоматизации, созданные при помощи Delphi, позволяют вызывать методы напрямую внутри интерфейса либо внутри метода invoke.
Создание диспетчера автоматизации
Одним из самых простых способов создания диспетчера автоматизации является способ импортирования библиотеки типов сервера автоматизации. При этом, вы можете использовать автоматически генерируемые классы для управления сервером автоматизации.
Примечаниe
Импортирование библиотеки типов сервера автоматизации особенно важно для разработчиков, которые используют старые версии Delphi. В Delphi 5 в палитру компонентов была добавлена вкладка Servers (Серверы), на которой располагаются значки, обеспечивающие доступ к основным серверам автоматизации, установленным на данном компьютере.
Для того чтобы импортировать библиотеку типов, нужно выполнить следующее:
1. Выбрать в главном меню Delphi пункт Project/Import Type Library (Проект/Импорт библиотеки типов).
2. В появившемся диалоговом окне (рис. 3.13) выбрать нужную библиотеку типов из представленного списка.
Примечание
В списке диалогового окна импортирования библиотеки типов содержатся все библиотеки типов, зарегистрированные в операционной системе на данном компьютере. Если нужная вам библиотека типов отсутствует в списке, вы можете ее туда добавить при помощи кнопки Add (Добавить). После того как вы нажмете кнопку Add (Добавить), найдите и добавьте нужную библиотеку типов с расширением TLB, OLB, DLL, OCX или EXE. Вы можете также удалить ненужную библиотеку типов из списка диалогового окна при помощи кнопки Remove (Удалить). Просто выделите ненужную библиотеку типов и нажмите кнопку Remove (Удалить).
Рис. 3.13. Диалоговое окно импортирования библиотеки типов
3. В выпадающем списке Palette page (Страница палитры компонентов) диалогового окна выберите страницу палитры компонентов Delphi, на которую будет размещен выбранный вами сервер автоматизации.
4. Убедитесь в том, что флажок Generate Component Wrapper (Создать суперобложку компонента) включен.
5. Нажмите кнопку Install (Установить).
Создание сервера автоматизации
Сервером автоматизации может являться либо приложение, либо DLL. Рассмотрим шаги, которые вам предстоит выполнить для создания сервера автоматизации.
1. Создайте приложение или DLL, которое должно выступать в роли сервера автоматизации. Можно использовать любое ранее созданное вами приложение и добавить к нему автоматизацию.
2. При помощи мастера объекта автоматизации создайте объект автоматизации и добавьте его к проекту.
3. Добавьте свойства и методы к вашему объекту автоматизации при помощи библиотеки типов. Данные свойства и методы нужны для того, чтобы диспетчеры автоматизации могли их использовать при обращении к вашему серверу автоматизации.
4. Зарегистрируйте приложение как сервер автоматизации. Теперь рассмотрим эти шаги более подробно.
Создадим самое простое приложение, состоящее из одной только формы. Для этого достаточно лишь запустить Delphi и выбрать пункт меню File/New Application (Файл/Новое приложение).
Теперь добавим к нашему проекту объект автоматизации для того, чтобы приложение выполняло функции сервера автоматизации. Чтобы сделать это, нужно выбрать пункт главного меню File/New (Файл/Новый). После чего перейдите в открывшемся окне New Items (Новые объекты) на вкладку ActiveX (рис. 3.15).
Выберите пиктограмму Automation Object (Объект автоматизации) двойным щелчком. После этого появится окно Automation Object Wizard (Мастер объекта автоматизации) (рис. 3.16).
Рис. 3.15. Вкладка ActiveX окна New Items
Рис. 3.16. Диалоговое окно Automation Object Wizard
Впишем имя Auto в поле CoClass Name для СОМ-класса объекта автоматизации. Мастер автоматически добавит букву т к имени класса, если создается класс Object Pascal для объекта автоматизации, или букву i - при создании основного интерфейса для объекта автоматизации.
Возможные -значения остальных полей ввода нами уже рассматривались в предыдущей главе.
После установки всех параметров в данном диалоговом окне Delphi создает новую библиотеку типа для проекта, и, кроме того, добавит интерфейс и класс компонентов к данной библиотеке типа. Более того, мастер создаст новый модуль в нашем проекте, который содержит реализацию интерфейса автоматизации, добавленного к библиотеке типа. На рис. 3.17 изображен редактор библиотеки типа сразу же после закрытия диалогового окна Automation Object Wizard (Мастер объекта автоматизации).
Рис. 3.17. Новый проект автоматизации в редакторе библиотеки типа
В листинге 3.4 приведен код нового модуля, который генерирует мастер объекта автоматизации Delphi.
Листинг 3.4.
unit Unit2; interface
uses
ComObj, ActiveX, Projectl_TLB, StdVcl;
type
TAuto = class(TAutoObject, lAuto)
protected
{ Protected declarations }
end;
implementation
uses ComServ;
initialization
TAutoObjectFactory.Create(ComServer, TAuto, Class_Auto,ciMultilnstance, tmApartment);
end.
Из вышеприведенного листинга ясно, что новый объект автоматизации TAuto является классом-наследником класса TAutoObject. Отметим, что класс TAutoobject является базовым классом всех объектов автоматизации.
Теперь, после добавления к нашему проекту нового объекта автоматизации TAuto мы можем дописать к основному интерфейсу lAuto необходимые свойства и методы.
Поставим перед собой простую задачу. Допустим, наш сервер автоматизации должен изменять цвет формы путем передачи необходимого цвета из диспетчера автоматизации. Для решения данной задачи добавим свойство color к интерфейсу lAuto. Это можно сделать в редакторе библиотеки типов разными способами, один из которых такой:
- щелкните левой кнопкой мыши на изображение интерфейса lAuto в левой части редактора библиотеки типа;
- т. к. нам нужно только получать цвет от диспетчера автоматизации, то нам понадобится свойство только для чтения, для этого щелкните на стрелочке справа от пиктограммы, изображающей свойство в верхней панели редактора, и выберите Write Only (Только для записи) (рис. 3.18);
- назовите свойство color и установите тип свойства OLE_COLOR в поле туре.
Рис. 3.18. Добавление свойства только для чтения в интерфейс lAuto
Все. Новое свойство добавлено, теперь нажмите в верхней панели редактора кнопку Refresh Implementation для обновления модуля реализации объекта автоматизации (рис. 3.19). В результате Delphi подкорректирует модуль реализации в соответствии с теми изменениями, которые были нами внесены в редакторе библиотеки. Код измененного модуля представлен на листинге 3.5. Кроме того, в данном листинге вручную дописано тело процедуры TAuto.Set_Color.
Листинг 3.5
unit Unit2;
interface
uses
ComObj, ActiveX, TestJTLB, StdVcl;
type
TAuto = class(TAutoObject, lAuto)
protected
procedure Set_Color(Value: OLE_COLOR); safecall;
{ Protected declarations }
end;
implementation
uses ComServ, Unitl;
procedure TAuto.Set_Color(Value: OLE_COLOR);
begin
Forml.color:=Value;
end;
initialization
TAutoObjectFactory.Create(ComServer, TAuto, Class_Auto, ciMultilnstance, tmApartment);
end.
После этого создайте на жестком диске новую папку. Назовем ее, например, Automation. Сохраним наше приложение в эту папку. При сохранении, дайте проекту имя Test. Затем откомпилируйте или запустите приложение для того, чтобы получить исполняемый файл Test.exe.
Итак, наш новый сервер автоматизации готов. Для того чтобы убедиться, что он работает, создадим диспетчер автоматизации. Для этого закроем текущий проект File/Close All (Файл/Закрыть все) и создадим новый File/New Application. Разместим на форме две кнопки: Соединение и Изменить цвет (рис. 3.20).
Рис. 3.19. Законченная библиотека типов
Рис. 3.20. Внешний вид формы диспетчера автоматизации
Приведенный листинг 3.6 иллюстрирует код для нашего диспетчера автоматизации.
Листинг 3.6
unit Unitl;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls, TestJTLB;
type
TForml = class(TForm)
Buttonl: TButton;
Button2: TButton;
procedure ButtonlClick(Sender: TObject);
procedure,Button2Click(Sender: TObject);
private
FIntf: lAuto;
{ Private declarations }
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation {$R *.DFM}
procedure TForml.ButtonlClick(Sender: TObject);
begin
FIntf:=CoAuto.create ;
end;
procedure TForml.Button2Click(Sender: TObject);
begin
FIntf.Set_Color(clRed);
end;
end.
Рассмотрим вышеприведенный листинг. Итак, для начала нужно добавить в раздел uses библиотеку типов Test_TLB. Далее в разделе private добавим поле FIntf типа lAuto. После чего для кнопки Соединение пишем обработчик события Onclick, которое создает экземпляр сервера автоматизации. При нажатии на эту кнопку будет запущено приложение Test.exe. Для события Onclick кнопки Изменить цвет пишем обработчик события, который вызывает метод set_color интерфейса lAuto. Установим, например, красный цвет clRed формы приложения-сервера. Запустим наш диспетчер автоматизации на исполнение. Нажмем последовательно кнопки Соединение и Изменить цвет. Вы можете видеть, как сначала запускается приложение Test.exe, а затем цвет формы приложения Test.exe изменяется на красный.
Наше простейшее приложение, использующее автоматизацию успешно работает (рис. 3.21).
Рис. 3.21. Результат работы приложения автоматизации
Управление сервером автоматизации
Рассмотрим в качестве примера применения свойства ConnectKind подключение и управление сервером автоматизации. Выполните последовательно следующие шаги:
1. При помощи пункта главного меню File/New Application (Файл/Новое приложение) создайте новый проект.
2. Поместите на форму компоненты wordApplication и wordDocument, расположенные на вкладке Servers.
3. Установите свойства AutoConnect и AutoQuit для компонента WordApplication В true.
4. Установите свойство ConnectKind для компонента WordDocument в CkAttachToInterface.
5. Выберите на форме компонент wordApplication и в окне инспектора объектов перейдите на вкладку Events (События).
6. Дважды щелкните на событии onDocumentchange и запишите в заготовке обработчика события, которую создаст Delphi, приведенный на листинге 3.2 код
Листинг З.2
procedure TForml.WordApplicationlDocumentChange(Sender: TObject);
begin
// Производим подключение к активному документу Microsoft Word
WordDocumentl.ConnectTo(WordApplicationl.ActiveDocument};
// Наш диспетчер автоматизации добавляет новую строку в текущий документ
WordDocumentl.Range.InsertAfter(#13+'Переход к документу'+#13+
WordApplicationl.ActiveDocument.Get_FullName+' произведен :'+
DateTimeToStr(Now));
end;
Рассмотрим, какие действия произведет код, представленный на листинге 3.2. Во-первых, данный код будет выполняться всякий раз при смене текущего документа Microsoft Word. После смены текущего документа (то есть, простого переключения между несколькими документами Microsoft Word) наш диспетчер автоматизации при помощи метода ConnectTo подключится к активному документу. Затем, при помощи метода InsertAfter производится вставка текстовой строки в текущий документ. Переход на новую строку в документе осуществляется при помощи вставки символа перевода строки (#13). Метод Get_FullName позволяет получить название текущего документа Microsoft Word.
7. Для события формы Create напишите код, представленный на листинге 3.3.
Листинг З.3
procedure TForml.FormCreate(Sender: TObject);
begin
// Отображение сервера автоматизации на экране
WordApplicationl.Visible:=true;
end;
Данный код отобразит Microsoft Word на экране, даже если он ранее не был загружен.
Запустите созданный вами диспетчер автоматизации при помощи пункта главного меню Run/Run (Запуск/Запуск). После старта приложения будет автоматически загружен Microsoft Word. С помощью пункта меню Microsoft Word File/New (Файл/Создать) создайте несколько новых документов. Теперь убедимся, что наш диспетчер автоматизации работает. Попробуйте переключаться между документами при помощи раздела меню Microsoft Word Window (Окно). Вы можете видеть, как диспетчер автоматизации добавляет новые строки в текущий документ.
Такой же принцип управления можно применять и в случае, когда необходимо контролировать действия Microsoft Excel. Когда в Microsoft Excel создается новая книга, возникает событие OnNewworkBook.