Lister-плагин на Borland Delphi 7 для начинающих

Не боги горшки обжигают…

Наверное, данная статья является не учебным пособием, а попыткой обобщить опыт, полученный автором в процессе разработки плагина xBaseView на Delphi 7, когда пришлось столкнуться с проблемами, довольно неприятными программисту, привыкшему к мощной поддержке VCL среды.


Задача: создать плагин для просмотра RTF файлов

Воспользуемся пунктами меню "File/New/Other" и в окне New Items выбираем значок DLL Wizard и сохраняем проект под именем ListSimple в отдельной папке, т.е. плагин является DLL библиотекой. Через пункты меню "Project/Options" откроем диалоговое окно и в поле "Target file extension" введем расширение имени плагина: WLX.
В разделе USES DPR файла модуль Classes изменяем на модуль Windows.
Плагин должен экспортировать из библиотеки три функции:


ListGetDetectString,
ListLoad,
ListCloseWindow.


Их тексты находятся в файле ListSimple.dpr.
ListGetDetectString должна записать в параметр DetectString строку, которая содержит RTF.
ListLoad вызывает плагин для работы и передает параметры:
ListerWin - дескриптор окна Листера;
FileToLoad - полное имя RTF файла.
Плагин должен создать свое окно в качестве дочерней по отношению к окну Листера и вернуть дескриптор этого окна. Эту инициализацию мы будем осуществлять в функции ShowRTF:

function ShowRTF(ListerWin: HWND; FileToLoad: string): HWND;

ListCloseWindow требует от плагина завершения работы, передавая дескриптор окна плагина. Это освобождение ресурсов мы будем делать в процедуре HideRTF:

procedure HideRTF(PluginWin: HWND);

Надо добавить в проект форму, изменить ее имя на fmMain и сохранить модуль формы под именем unMain.pas. Эта форма и есть окно плагина.
В модуле unMain можно удалить бесполезную глобальную переменную:

var fmMain: TfmMain;

Замечание: ПЛАГИН НЕ ДОЛЖЕН ИСПОЛЬЗОВАТЬ ГЛОБАЛЬНЫЕ ПЕРЕМЕННЫЕ!

Бросив на форму компонент RichEdit, надо установить ему такие свойства:

Align = alClient,
ReadOnly = True,
ScrollBars = ssBoth.


Объект RichEdit1 обеспечивает всю работу с RTF файлом.
В плагинах часто используется контекстное меню, поэтому надо бросить на форму компонент PopupMenu и создать два пункта меню Help и About, назначив им клавиши F1 и F2, соответственно. Необходимо прикрепить контекстное меню объекту RichEdit1, установив свойство RichEdit1:

PopupMenu = PopupMenu1.

Окно плагина является дочерним, следовательно, оно не должно иметь рамку и заголовок. Для этого надо переопределить виртуальный метод CreateParams базового класса TCustomForm. Здесь, мы кроме настройки стилей окна:

Params.Style := WS_CHILD or WS_MAXIMIZE and not WS_CAPTION and not WS_BORDER;

зарезервируем память для хранения указателя на нашу форму:

Params.WindowClass.cbWndExtra := SizeOf(Pointer);

Указатель нужен процедуре HideRTF для корректного закрытия плагина. В функции ShowRTF мы сохраняем указатель в WinAPI структуре окна плагина следующим вызовом:

SetWindowLong(fmMain.Handle, GWL_USERDATA, Integer(p));

Позднее мы вытащим указатель из структуры, используя дескриптор нашего окна, который является параметром HideRTF, вызывая функцию GetWindowLong:

p := Pointer(GetWindowLong(PluginWin, GWL_USERDATA));

Для работы плагина нам понадобятся следующие данные: дескрипторы окон Тотал Командира и Листера, и режим работы плагина: в обычном окне или на одной из двух панелей Тотал Командира (так называемый Quick View - режим быстрого просмотра). Дескрипторы нужны, чтобы избежать краха по клавишам выхода Alt+X и для реагирования на быстрые клавиши Листера. Эти данные будем хранить, соответственно, в переменных TotCmdWin, ParentWin и QuickView.
Напишем нестандартный конструктор CreateParented для инициализации этих трех переменных формы и для загрузки RTF файла в наше окно (в RichEdit1). Достаточно иметь два параметра конструктора: ParentWindow - дескриптор окна Листера и FileToView - имя RTF файла. Дескриптор окна Тотал Командира мы можем найти по имени класса этого окна (это не VCL класс!) с помощью WinAPI функции FindWindow.
Однако, тут самое главное то, что этот конструктор перекрывает и вызывает конструктор CreateParented базового класса TWinControl, чтобы создать наше окно в качестве дочернего. Таким образом, инициализационная функция ShowRTF может создать наше окно простым вызовом нестандартного конструктора CreateParented.
Немного теорий. Особенностью библиотеки визуальных компонентов Borland VCL является использование глобального объекта "приложение" - Application. Этот Application имеет скрытое окно, которое должно быть владельцем всех окон приложения, что обеспечивает корректное поведение всех окон программы. И DLL библиотека плагина, и исполняемый EXE файл Тотал Командира имеют собственный глобальный объект Application, следовательно, требуется их синхронизация. Для этого надо присвоить дескриптор скрытого окна объекта Application исполняемого файла дескриптору объекта Application библиотеки. К сожалению, Тотал Командир, хотя он написан на Delphi и использует все удобства и прелести VCL, не передает нам дескриптор данного скрытого окна (может, причиной этого является проблема версии компиляторов Delphi?). За неимением оного, будем использовать для синхронизации дескриптор окна Листера, в противном случае наше модальное окно (например, окно About) поведет себя неестественно, оно появится на панели задач Windows. Эта синхронизация является задачей для функции ShowRTF.
Чтобы защитить, Тотал Командир от сбоев плагина, будем использовать событие OnException объекта Application, которое перехватывает необработанное исключение. Кинем на форму компонент ApplicationEvents, переименуем его на App и создадим обработчик события OnException, куда введём вызов функции MessageBox для выдачи сообщения о сбое. Сохранив проект и модуль, можно удалить из формы ненужный компонент ApplicationEvents. Установка адреса обработчика события в Application.OnException также является задачей для функции ShowRTF.
Все готово для ShowRTF? Увы, "маленькая неточность" в Plugin API Тотал Командира (включая v.6.02) значительно усложняет нашу работу:
Если пользователь откроет окно плагина, затем переключится на Тотал Командир и закроет Тотал Командир, то мы не получим вызов через ListCloseWindow для завершения своей работы. Вместо этого Тотал Командир или Листер (кто его знает?) уничтожает наше окно вызовом WinAPI функции DestroyWindow, а потом то же самое попытается сделать наш объект Application, и в конечном счёте плагин вылетит с нарушением защиты памяти (Access Violation)!
Если бы мы не стали делать вышеописанную синхронизацию, этой проблемы удалось бы избежать. Что ж, будем расплачиваться за это и поставим ловушку для перехвата сообщений, которое получает наше же окно, подобно змее, которая пожирает себя за свой собственный хвост! Не будем вдаваться в подробности Windows API (кому же он нравится?), вкратце суть дела такова.
Объявляем тип записи TPlugInfo, где будем хранить данные, которые нужны для закрытия плагина. При инициализации выделяем память под эту запись и заполняем ее. Определяем функцию HookDestroy, который будет перехватывать оконные сообщения, чтобы среагировать на сообщение WM_DESTROY (уничтожение окна). Вызовом SetWindowLong(…, GWL_WNDPROC, …) подменяем стандартный обработчик оконных сообщений на функцию HookDestroy. Похожим вызовом SetWindowLong в процедуре завершения HideRTF обратно восстанавливаем прежний обработчик. Важно то, что когда функция HookDestroy поймает сообщение WM_DESTROY, она вызывает процедуру завершения HideRTF точно так же, как это делает процедура ListCloseWindow. В результате мы всегда успеваем нормально закрыть окно и убрать за собой, что, к примеру, демонстрирует Dispose(p) из HideRTF, освобождая память, выделенную в ShowRTF.
Остается создать диалоговую форму About, обработчики контекстного меню и обработчик RichEdit1KeyDown, который ловит нажатие специальных клавиш и передает их с помощью PostMessage в соответствующее окно. Если здесь не перехватывать клавиши Alt+X, это приведет к краху плагина. Причем, здесь придется удерживать фокус ввода в RichEdit1, проверяя режим работы плагина, т.е. значение переменной QuickView.

Об отладке и тестировании плагина.

Листер имеет встроенную возможность просмотра RTF файлов, поэтому перед запуском плагина надо отключить ее, сняв пометку у флага RTF в окне Configure Lister.
Чтобы использовать интегрированный отладчик IDE Delphi, закройте Total Commander, а в Delphi через меню "Run/Parameters" откройте диалог и в поле Host Application введите путь к Total Commander с помощью кнопки [Browse]. Теперь можно запускать плагин из-под Delphi.
Ссылка для скачивания архива с примером здесь
PS. Уважаемый Читатель! Программирование - многовариантно и я никак не претендую на абсолютную истину. В качестве дополнительного материала предлагаю Вам ознакомиться с исходными текстами плагина xBaseView - "Просмотр DBF, DB, MDB, ADO и ODBC баз данных", который предоставляется с исходными текстами, и где применяется аналогичный подход.
PPS. Прошу Читателя-Полиглота помочь мне перевести статью на английский язык, чтобы послать ее Кристиану Гислеру, автору знаменитого файл менеджера Тотал Командир, с призрачной надеждой, что он предпримет какие-то шаги навстречу нам - VCL программистам.

Е. Савич
19.03.04
© Mutex Ltd.

mutex@nm.ru