Рассылка закрыта
При закрытии подписчики были переданы в рассылку "Создание сайта. Уроки для начинающих и не только" на которую и рекомендуем вам подписаться.
Вы можете найти рассылки сходной тематики в Каталоге рассылок.
Плагины в Delphi.
Доброго времени суток, уважаемые подписчики!
Сегодня будет опубликована статья нашего почетного модератора DrPass про плагины.
Приложения с подключаемыми модулями (плагинами)… и Delphi
Судя по тому, с каким постоянством на форумах возникают просьбы помочь реализовать поддержку плагинов в Delphi, эта тема является очень актуальной. Действительно, можно найти массу ситуаций, где было бы оправдано применение модульных приложений. Впрочем, обзор возможных сфер применения плагинов уже выходит за рамки данной статьи, поэтому давайте займемся технической стороной этого вопроса. Тем более что она обещает быть намного интереснее.
Первой технологией создания подключаемых модулей, за которую обычно берутся новички в Delphi, являются динамические библиотеки. Казалось бы, что может быть проще: описать набор вызываемых функций, реализовать их в своих библиотеках, и вперед! И действительно, все будет работать, и работать замечательно… пока вам не захочется создать форму, загружаемую из плагина. А вам ведь уже хочется, я чувствую это J
«Так что же мешает?» – спросите вы. Мешает, как ни странно, VCL. Borland создала отличную объектно-ориентированную оболочку над Windows API. Но ее функциональность достигается за счет довольно сложной реализации. Вместе с формами VCL вам достанутся:
- Глобальные объекты вроде TScreen, TApplication, которые нужно синхронизировать с их «коллегами» в DLL.
- Сложная внутренняя система обмена сообщениями, которую тоже нужно… впрочем, даже и не пробуйте
- Секции инициализации и финализации модулей, которые не захотят работать в DLL, загружаемой через LoadLibrary.
- И еще много-много мелких неприятностей и подводных камней.
С этим можно мириться, если приложение простое и каких-либо существенных функций на форму в плагине просто не возлагается. Ну а если все-таки возлагается?
«Что же делать?» - опять спросите вы? Первый вариант – не использовать VCL. Традиционное создание окон и элементов управления через CreateWindowEx, оконная функция для обработки сообщений – все это будет работать замечательно. Но, я опять чувствую, вот этого вам совершенно не хочется J
Как обычно, есть и другой путь – это использование пакетов (packages). Если вам не нужна совместимость с другими средами разработки (пакеты можно создавать только в Borland Delphi / Borland C++ Builder), самое время обратить внимание именно на них.
Пакет – это
тоже динамическая библиотека, но ее секция экспорта автоматически генерируется
компоновщиком Delphi.
Что это означает? Давайте вспомним ограничения, которые налагают Dll: мы можем экспортировать только процедуры/функции. Если мы хотим, например, экспортировать класс, нам придется написать экспортируемые функции-оболочки для всех его методов. Если вы уже пробовали делать так, то знаете, насколько это неудобно. Кроме того, разрушается сама суть объектно-ориентированного программирования – от такого «экспортированного» класса нельзя что-либо унаследовать без полухакерских ухищрений.о программирования - от
Если же мы
поместим модули в пакет,
Кстати, о содержимом пакета. Оно может быть скомпоновано с программой тремя способами:
- Статически. В этом случае все модули пакета переносятся в исполняемый файл программы. Собственно, сам пакет в этом случае не используется, он служит только «поставщиком» модулей для компиляции программы.
- Связыванием на этапе компиляции. В этом случае пакеты автоматически компонуются с программой и загружатся при ее запуске. Чтобы воспользоваться этим способом, вам нужно поставить флажок Build with runtime packages на вкладке Project options – Packages. Конечно, модули из пакета должны использоваться в программе.
- Во время работы программы, загружая пакеты с помощью функции LoadPackage. Именно этот способ нас и интересует больше всего – он позволяет загружать произвольные наборы пакетов, и тем самым реализовать модульность нашего приложения.
Коротко о функции LoadPackage. Ее прототип достаточно прост:
function
LoadPackage(const Name: string): HMODULE;
В качестве параметра Name ей передается имя загружаемого пакета, а возвращает она его дескриптор в случае успешной загрузки или вызывает исключение EPackageError в случае неуспешной.
LoadPackage выполняет следующие действия:
- Загружает пакет с помощью LoalLibrary и в случае успеха использует полученный дескриптор как свой возвращаемый результат.
- Проверяет пакет на наличие модулей, у которых имена совпадают с уже загруженными, и вызывает исключение, если такой модуль найден.
- Выполняет секцию Initialization у модулей, содержащихся в пакете.
И вот тут мы приходим к Первому Очень Важному Правилу:
В загружаемых пакетах не должно быть двух
модулей с одинаковыми именами
На практике это означает, что если вы собираетесь писать приложение с плагинами-пакетами, вами придется выработать единый стиль «обзывания» модулей. Например, в качестве префикса имени каждого модуля в пакете использовать имя самого пакета. Впрочем, оставим это на ваше усмотрение.
Выгрузить пакет тоже несложно, для этого вам понадобится его дескриптор и соответствующая функция:
procedure UnloadPackage(Module: HMODULE);
Она выполняет обратные действия:
- Вызывает секцию Finalization у модулей, входящих в пакет
- Вызывает FreeLibrary с указанным дескриптором
Ну а теперь, имея некоторый запас теоретических знаний, перейдем непосредственно к плагинам.
В случае с подключением Dll программистом обычно разрабатывался некоторый набор функций, такой себе общий API для подключаемых библиотек. Но мы ведь не зря используем пакеты? Давайте реализуем механизм обращения к плагину в виде класса. Например, так:
type
TPlugin = class
//получить имя плагина
function
GetName: string; virtual; abstract;
//получить пиктограмму плагина
function
GetIcon: TBitmap; virtual; abstract;
//загрузить плагин и отобразить его форму на указанном контроле
function
Load(ASite: TWinControl): boolean; virtual; abstract;
//показать форму плагина
procedure
Show; virtual; abstract;
//скрыть форму плагина
procedure
Hide; virtual; abstract;
//выгрузить плагин
procedure
UnLoad; virtual; abstract;
end;
Данный набор методов не претендует на универсальность, и тем более на оптимальность. Его цель – всего лишь привести пример, что можно сделать. В конце-концов здесь есть где развернуться вашей фантазии, и я ни в коем случае не буду лишать вас этого удовольствия J.
Как вы наверное заметили, все методы объявлены как абстрактные. Все правильно, конкретная реализация будет сделана уже в потомках этого класса, которые мы опишем в пакетах.
И еще, нам понадобится какая-то общая структура данных, в которой плагины будут регистрировать себя при создании и убивать при выгрузке. Для этих целей нам будет достаточно стандартного объекта TList:
var
Plugins : TList;
Кстати, о пакетах: чтобы все плагины могли быть унаследованы от одного класса и смогли бы зарегистрировать себя в одном общем списке, они должны видеть один и тот же модуль, в котором описан базовый класс и список. Поэтому объявлять этот модуль (давайте назовем его громким именем Менеджер плагинов) в приложении нельзя, его тоже нужно вынести в отдельный пакет. А это уже Второе Очень Важное Правило:
Общие модули, используемые несколькими
пакетами, должны быть в отдельных пакетах. И эти «отдельные пакеты» должны быть
добавлены в список Requires тех
пакетов, которые их используют.
Если этого не сделать, то копии одного и того же модуля будут помещены компоновщиком во все пакеты (он, правда, честно вас предупредит: unit <такой-то> implicity imported into package <вот такой-то>). К чему это приведет, понятно – при попытке загрузить два таких плагина одновременно будет нарушено Первое Очень Важное Правило, и вы останетесь один на один с унизительной EPackageError.
«Почему бы не тогда оставить этот модуль в приложении?» – возможно, спросите вы. Нельзя по той же причине. Из приложения ничего не экспортируется, само собой. Поэтому при компиляции наших плагинов компоновщик будет вынужден помещать копию Менеджера плагинов и в пакеты… а дальше сценарий тот же.
А вот тут мы приходим к не совсем приятному Следствию Второго Очень Важного Правила:
Приложение, использующее плагины-пакеты,
должно быть скомпилировано с опцией Build with runtime packages - для того, чтобы модули не
статически компоновались с программой, а оставались в своих пакетах. Это
касается как Менеджера плагинов, так и модулей VCL/RTL, которые тоже должны совместно использоваться и приложением,
и плагинами (Forms, Classes, Controls и т.д.).
Что это означает, понятно – при распространении своего приложения вам предстоит «навесить» на него еще и солидный набор bpl-файлов. И, кстати, еще потратить время, чтобы определить, какие библиотеки нужны, а какие нет.
Пожалуй, это единственный существенный недостаток использования пакетов. При разработке корпоративных приложений его еще можно превратить в преимущество – обеспечив всех пользователей предприятия набором bpl-файлов, существенно уменьшить размер устанавливаемых приложений. Ведь при этом размеры самого приложения и плагином становятся крохотными, а если на предприятии ведется разработка нескольких приложений на Delphi, эта выгода коснется и их. Но совсем другое дело, если вы пишете массовое приложение и собираетесь распространять его через Интернет. В этом случае 8-10 Мб лишних библиотек могут оказаться непосильным (да и ненужным) бременем для нашего не слишком богатого на трафик пользователя.
Вот, теперь все карты выложены на стол, и вам пора принять окончательное решение, использовать или не использовать пакеты для реализации плагинов. Что? Вы уже приняли? Отлично, тогда идем дальше J.
Закроем тему
«Менеджер плагинов». Итак, как мы и договаривались, создайте новый пакет (File -> New -> Other -> Package), назовем его PluginManager. Теперь добавьте к нему
новый модуль (пусть будет uPluginManager).
В этом модуле мы и объявим класс TPlugin, а также переменную Plugins.
И да, нам понадобится еще один список:
Packages: TList;
В этом списке мы будем хранить дескрипторы загруженных пакетов. Ведь прямого соответствия плагин-пакет по сути нет, нам ничего не мешает хранить в одном пакете несколько разных классов-потомков TPlugin, и загружать/выгружать их вместе. Поэтому нужно две структуры – одна для индивдуального доступа к каждому экземпляру TPlugin, другая – для работы с пакетами.
Здесь же подходящее место для процедур загрузки и выгрузки пакетов. Как приложение будет узнавать, какие файлы считать плагинами, и откуда их загружать? О, это вопрос риторический. Мы для простоты будем загружать пакеты согласно списку в каком-нибудь текстовом файле.
Вот так:
//загрузка пакетов из файла
procedure
LoadPackagesFromFile(AFileName: string);
var
sl: TStringList;
i: integer;
H: THandle;
begin
sl:=TStringList.Create;
sl.LoadFromFile(AFileName);
for I := 0 to sl.Count - 1 do
begin
H:= LoadPackage(sl[i]);
Packages.Add(pointer(H));
end;
sl.Free;
end
;
//выгрузка пакетов
procedure
UnloadPackages;
var
i: Integer;
begin
for
I := Packages.Count - 1 downto 0 do
begin
UnloadPackage(cardinal(Packages[i]));
Packages.Delete(i);
end;
end
;
Пожалуй, это
все, что нам нужно от Менеджера плагинов. Полностью его код вы можете
посмотреть в прилагаемых к статье исходниках, а пока давайте перейдем к
рассмотрению устройства конкретного плагина.
Для успешной работы плагина нам, во-первых, нужно реализовать все шесть методов базового класса TPlugin. Начнем-с.
Создадим еще один пакет, в котором будет два модуля – один простой, с реализацией класса-потомка TPlugin, второй – с формой. Что поместить на форму, вы решите сами; в нашем примере я сделал форму с «текстовым редактором» TMemo:
Вот так выглядит класс плагина:
type
TEditorPlugin = class(TPlugin)
function GetName: string; override;
function Load(ASite: TWinControl): boolean; override;
procedure Show; override;
procedure Hide; override;
procedure UnLoad; override;
function GetIcon: TBitmap; override;
end;
var
Editor: TEditorPlugin;
Самые простые методы – GetName и GetIcon:
function
TEditorPlugin.GetName: string;
begin
Result:=
'Текстовый
редактор
'
;
end
;
function
TEditorPlugin.GetIcon: TBitmap;
var
bmp: TBitmap;
begin
bmp:=TBitmap.Create;
bmp.LoadFromResourceName(hInstance,
'ICON'
);
Result:=bmp;
end
;
Что касается метода GetIcon, он будет возвращать пиктограмму плагина, которую будет отображать главное приложение на своей панели выбора. В нашем примере я создал файл icon1.res в редакторе Image Editor с картинкой 32х32 пиксела, названной ICON. Чтобы подключить этот файл к пакету, мне понадобилось прописать в dpk-файле пакета строчку {$R icon1.res}.
Далее – два метода, один создает форму плагина и помещает ее на указанный элемент управления (это может быть как форма главного приложения, так и какая-либо панель на ней); второй соответственно разрушает:
function
TEditorPluginLoad(ASite: TWinControl): boolean;
begin
frmSample1:=TfrmSample1.Create(ASite);
try
frmSample1.Parent:=ASite;
frmSample1.Align:=alClient;
Result:=true;
except
Result:=false;
frmSample1.Free;
end
;
end
;
procedure
TEditorPlugin.Unload;
begin
if
Assigned(frmSample1) then frmSample1.Free;
end;
И, наконец, процедуры отображения и скрытия плагина. Здесь мы сделаем еще одну полезную функцию – «склеивание» главного меню и меню плагина.
procedure
TEditorPlugin.Show;
var
Menu: TMainMenu;
begin
frmSample1.Show;
Menu:= frmSample1.Parent.FindComponent(
'MainMenu'
) as TMainMenu;
if Menu<>nil then
Menu.Merge(frmSample1.MainMenu1);
end
;
procedure
TEditorPlugin.Hide;
var
Menu: TMainMenu;
begin
Menu:= frmSample1.Parent.FindComponent(
'MainMenu'
) as TMainMenu;
if Menu<>nil then
Menu.Unmerge(frmSample1.MainMenu1);
frmSample1.Hide;
end;
Имейте в виду,
что для правильного склеивания пунктов меню вам придется поиграться свойством TMenuItem.GroupIndex.
Последний маленький, но важный факт – нам нужно обеспечить автоматическое создание экземпляра класса плагина и добавление его в общий список при загрузке пакета. И наоборот, при выгрузке нужно убрать и разрушить плагин:
initialization
Editor:=TEditorPlugin.Create;
Plugins.Add(Editor);
finalization
Plugins.Remove(Editor);
Editor.Unload;
Editor.Free;
end.
Теперь не забудьте прописать пакет PluginManager в секцию Requires этого пакета. Вот вроде бы и все. В прилагаемом к статье примере я создал два таких плагина, один с «редактором», второй – с TImage на форме.
Давайте теперь рассмотрим главное приложение. В главном приложении будет одна форма, содержащая панель для отображения названия текущего плагина, и TListView для отображения пиктограмм и выбора плагинов:
Собственно, загрузка плагинов будет выполняться так: при выборе в OpenDialog файла программа выгрузит уже имеющиеся плагины (если такие имелись), потом загрузит пакеты в память, поместит пиктограммы плагинов в ListView и один из них отобразит на экране:
procedure
TfrmMain.N3Click(Sender: TObject);
begin
if
OpenDialog1.Execute then
begin
UnloadPackages;
LoadPackagesFromFile(OpenDialog1.FileName);
LoadPlugins;
end;
end
;
procedure
TfrmMain.LoadPlugins;
var
i:integer;
bmp: TBitmap;
li: TListItem;
begin
lvPlugins.Items.Clear;
ilIcons.Clear;
for i:=
0
to
Plugins.Count-
1
do begin
TPlugin(Plugins[i]).Load(Self);
bmp:=TPlugin(Plugins[i]).GetIcon;
li:=lvPlugins.Items.Add;
li.Caption:=TPlugin(Plugins[i]).GetName;
li.ImageIndex:=ilIcons.AddMasked(bmp, rgb(
255
,
0
,
255
));
li.Data:=Plugins[i];
bmp.Free;
end;
if Plugins.Count>
0
then begin
pnlTitle.Caption:=TPlugin(Plugins[
0
]).GetName;
TPlugin(Plugins[
0
]).Show;
end;
end
;
Как вы можете заметить, мы сохраняем указатели на плагины в свойстве Data соответствующих элементов TListView. Это сделано для того, чтобы упростить манипулирование плагинами, когда пользователь будет выбирать их в списке:
procedure
TfrmMain.lvPluginsChange(Sender: TObject; Item: TListItem;
Change: TItemChange);
begin
if
Change <> ctState then exit;
if Item = nil then exit;
if not Item.Selected then
begin
TPlugin(Item.Data).Hide;
pnlTitle.
Caption:= 'Модули не загружены';
end
else
begin
TPlugin(Item.Data).Show;
pnlTitle.Caption:= TPlugin(Item.Data).GetName;
end;
end;
Пожалуй, это был последний ключевой момент. Давайте теперь соберем и посмотрим, что у нас получилось:
Получилось, ИМХО, неплохо. Вы со мной согласны? То-то же J
Вам важно понимать, что приведенным примером возможности реализации плагинов в Delphi не ограничиваются. Они, собственно говоря, ограничиваются только вашей фантазией. Вы можете добавлять любые механизмы взаимодействия между плагинами – и передачу соединений с базами данных, и СОМ-технологии, и… что угодно, создавая таким образом гибко конфигурируемые приложения любой сложности.
На этом пока что все. Вопросы по этой теме я принимаю по электронной почте и в форуме http://forum.chertenok.ru. Вопросы про адрес/телефон девушки на фото не присылать – НЕ ДАМ!
Исходные тексты примера можно скачать отсюда>
Ждем Ваших откликов на емайл 5781-author@subscribe.ru или subscr@chertenok.ru
Приглашаем авторов в рассылку!
С уважением,
координатор рассылки Алексей aka Gelios.
Наши координаты:
сайт - www.delphi.chertenok.ru
форум - www.forum.chertenok.ru
контактный email - 5781-author@subscribe.ru
Другие проекты:
www.travel.chertenok.ru - сайт о путешествиях!
В избранное | ||