Sistema de PlugIns en Delphi – Parte 2
Pues ha llovido mucho desde la primera parte de este «articulillo»; Por lo que he visto en los fuentes del ejemplo, lo empecé hace aproximadamente hace 2 años, así que eso es lo que lleva de retardo… ;-)
En ese primer artículo se daba una visión general de lo que podía ser un sistema de PlugIns. Unas ideas generales y algo de código para empezar. Quedaba en el tintero profundizar un poco más en el tema y ver un ejemplo un poco más «práctico» que pudiera servir en una aplicación real. Eso es lo que he intentado tratar en esta segunda parte, centrandome en un sistema de «PlugIns Homogéneos«, con carga bajo petición (por parte del usuario).
TIPOS DE PLUGINS
Como ya vimos en la primera entrega, podemos dividir los plugIns en dos tipos según las tareas que desempeñan en una aplicación. Así podemos hablar de plugIns o grupos de ellos y catalogarlos como homogéneos si la tarea que realizan es similar o catalogarlos como heterogéneos (no-agrupados) si las tareas que desarrollan son independientes y no “agrupables” según su funcionalidad.
- Grupos homogéneos de PlugIns
- PlugIns heterogéneos
Esta división no sólo es conceptual en función de las características y desempeños de cada uno, sino que afecta directamente a la estructura con que se diseñarán estos elementos. Así, los PlugIns que pertenezcan a un grupo homogéneo tendrán estructura similar y un punto de entrada común (formulario base o procedimiento de ejecución). Mientras que los heterogéneos posiblemente no tengan una estructura común y la forma se ejecutarlos sea más “tosca” y menos “integrada” que los anteriores.
PLUGINS HOMOGENEOS
En este artículo vamos a tratar más profundamente esta variante de plugIns. Como ya hemos comentado se trata de plugIns con una estructura similar, aunque con variaciones en su funcionalidad. Tal vez con un ejemplo se vea más claro.
Tomemos como grupo homogéneo de PlugIns; Los efectos aplicables a una imagen dentro de un programa de diseño. A partir de una imagen podemos desarrollar plugIns que efectúen un determinado “cambio” de forma que la imagen resultante sea diferente a la original. La estructura y los parámetros de todos ellos parece claro que serán similares o idénticos. Todos ellos toman una imagen inicial y a partir de unos parámetros la modifican, para devolver una imagen de salida.
ESTRUCTURA FÍSICA
Para trabajar con esta estructura de plugins, utilizaremos un sistema de carga dinámica. Los plugins de programarán utilizando packages (BPL) con una estructura común y dependiendo de un package principal que contiene la Clase Base. Todos los plugins derivarán (heredarán) de una clase base que estará programada en el package principal.
Al cargar la aplicación se carga (puesto que está linkado de forma estática -utilizando la clausula USES-) también el package correspondiente a la clase Base. Esto da acceso a todos los métodos que estén definidos en la clase base (y en los derivados) desde el programa principal.
El resto de packages se cargan de forma dinámica y todos deben derivar (sus clases) de la Clase Base programada en el package Base.
PROTOTIPO
El prototipo que vamos a realizar para ilustrar el artículo simula un programa para realizar gráficos y diagramas simples. El programa utilizará un sistema de plugIns para añadir bibliotecas de objetos que puedan añadirse a los gráficos. Cada pluging (BPL) añade una nueva categoría de elementos y cada categoría implementa uno o varios objetos.
Todos los objetos que implementa una categoría derivan de una Clase Base (TShapeExBase) y esta clase base se implementa en un package que está linkado estáticamente a la aplicación principal (se carga siempre al arrancar la aplicación) y es obligatorio que exista, de otra forma la aplicación fallaría al ejecutarse.
En la imagen que se ve ala derecha, vemos la ventana correspondiente al Plugin de «Arrows»; Aquí implementa la clase TShapeExArrow (que deriva de TShapeExBase) y en esta clase se han programado los objetos que se ven en la imagen.
En nuestro ejemplo para este artículo se cargan los plugIns bajo petición. Es decir, en una primera pasada la aplicación revisa la existencia de PlugIns y detecta todos los ficheros presentes. Muestra una ventana con los plugns disponibles y la descripción de cada uno de ellos y a medida que el usuario los selecciona se cargan de forma dinámica. Imagen de la derecha.
El código de la carga es el siguiente:
... // Comprobación if not FileExists(AName) then begin _mens(Format('No se ha podido cargar el package <%s>;' + 'No existe en disco.',[AName])); Exit; end; // Cargar hndl := LoadPackage(AName); desc := GetPackageDescription(PChar(AName)); Result := hndl; // Acceder a la clase del menu pName := ChangeFileExt(ExtractFileName(AName), ''); b := ExClassList.Find(pName, i); // Encontrada? if (b) then begin AClass := TPersistentClass(ExClassList.Objects[i]); end; |
CLASE BASE (TShapeExBase)
La clase base para nuestro sistema de plugins se llama TShapeExBase. Esta clase sirve como punto de partida para todas las demás. Además de contener los métodos comunes a todos los plugins nos permitirá acceder desde la aplicación principal a todas las funciones de los plugins. Para ello los métodos importantes estarán definidos en esta clase y luego sobreescritos (override) en las clases derivadas.
{: Clase base lapa las clases implementadas en los plugins.} TShapeExBase = class(TShape) private FShapeEx: string; // Marca el tipo de Shape procedure SetShapeEx(const Value: string); virtual; protected W, H, S: Integer; X, Y:Integer; XW, YH:Integer; W2, H2, W3, H3, H4, W4, H8, W8, W16, H16:Integer; // Método de pintado procedure Paint; override; procedure CalculateData(); // PROCEDIMIENTOS DE INFORMACION //············································ // Autor del package function Autor():string; virtual; abstract; // Versión del Package function Version():string; virtual; abstract; // Fecha de creación function FechaCreacion():TDate; virtual; abstract; public // constructor de la clase constructor Create(AOwner: TComponent); override; // destructor de la clase destructor Destroy; override; published // Tipo de Shape property ShapeEx: string read FShapeEx write SetShapeEx; end; |
En nuestro caso es una clase sencilla. La función implementa el dibujo de componentes derivados de un TShape en pantalla.
La propiedad ShapeEx es la más importante, e indica el tipo (identificador) de la figura. Equivalente a lo que en los TShape son los valores stRectangle, stEllipse, stSquare,…
En nuestra clase no puede ser un elemento tipificado como lo es en TShape, puesto que los nuevos plugins irán añadiendo elementos a esta propiedad que a priori no conocemos.
Se añaden también procedimientos de información acerca del plugin como pueden ser el Autor, la fecha de creación o la versión.
El método Paint, que para la clase base está vacío, en las clases derivadas será donde se implementen las instrucciones de pintado para cada uno de los elementos.
Finalmente la clase Base implementa el procedimiento CalculateData y al utiliza algunas variables en la parte protected, que precalculan datos y los ponen a disposición de las clases derivadas (protected), para facilitar la implementación del método Paint y dar acceso a medidas ya precalculadas.
En la clase Base además se definen dos Listas (TStringList) que nos servirán de apoyo a la hora de acceder a los diferentes objetos de los plugIns; Tanto para las clases, como para los Shapes definidos en cada clase.
//: Lista de clases registradas en los packages dinámicos ExClassList:TStringList; //: Lista de objetos registrados en una clase (tipos de Shapes) ExShapeList:TStringList; |
En la primera añadiremos la referencia a la Clase y el String correspondiente al nombre del package y en la segunda, para cada Shape implementado en la Clase, su valor de la propiedad ShapeEx (comentada anteriormente) y el apuntador a su clase.
De esta forma, por ejemplo, el PlugIn que implementa la clase TshapeExArrow que corresponde a la imagen que se ve más arriba, añadirá en las lista los siguientes valores:
// Registrar los tipos ExShapeList.AddObject('stArrorRight', Pointer(TShapeExArrow)); ExShapeList.AddObject('stArrorRightW', Pointer(TShapeExArrow)); ... // registrar la clase ExClassList.AddObject('PlugArrows', Pointer(TShapeExArrow)); |
En las líneas anteriores podemos ver que el plugIn PlugArrow (1) tiene implementada la clase TShapeExArrow (1), y que dentro de esta clase hay 6 objetos diferentes de tipo ShapeEx; Cuyos identificadores son: stArrorRight, stArrorRight, stArrorRightM, stArrorLeft, stArrorUp y stArrorDown.
CLASES DERIVADAS
Tal y como está diseñada la estructura, las clases derivadas de la clase Base (TShapeExBase) deben redefinir el método Paint para definir cómo se define cada uno de los objetos de esa clase.
SISTEMA DE CARGA/DESCARGA
El sistema de carga es simple y lo único que hace de especial en este caso es comprobar primero si el package ya ha sido cargado, y si no es así llama a la función CargarPackage del formulario principal, utilizando el nombre del fichero.
Podemos ver por pasos y comentar qué hace esta función:
// Cargar hndl := LoadPackage(AName); desc := GetPackageDescription(PChar(AName)); Result := hndl; |
En primer lugar (una vez hemos comprobado que el fichero existe) cargamos el package a partir de su nombre. Una vez cargado obtenemos la Descripción. Para ello se llama a la función GetPackageDescription que se encuentra en la Unidad SysUtils.pas y que develve el valor almacenado en el DPK junto a la directiva {$DESCRIPTION} o {$D} que permite almacenar hasta 255 caracteres.
Todos los packages cuentan con una sección de INITIALIZATION donde añaden a las lista de clases (ExClassList) y a la lista de Shapes (ExShapeList) los elementos que ese package implementa. Estas dos clases son importantes puesto que nos facilitan mucho el trabajo a la hora de realizar todo tipo de operaciones con los elementos de cada packages. Además se registra la clase utilizando el método RegisterClass de Delphi. Por ejemplo, el package de “Arrows” contiene esta sección de INITIALIZATION:
//=================================================================== // // I N I T I A L I Z A T I O N // //=================================================================== initialization // Registrar la clase del form RegisterClass(TShapeExArrow); // Registrar los tipos ExShapeList.AddObject('stArrorRight', Pointer(TShapeExArrow)); ExShapeList.AddObject('stArrorRightW', Pointer(TShapeExArrow)); ExShapeList.AddObject('stArrorRightM', Pointer(TShapeExArrow)); ExShapeList.AddObject('stArrorLeft', Pointer(TShapeExArrow)); ExShapeList.AddObject('stArrorUp', Pointer(TShapeExArrow)); ExShapeList.AddObject('stArrorDown', Pointer(TShapeExArrow)); // registrar la clase ExClassList.AddObject('PlugArrows', Pointer(TShapeExArrow)); //=================================================================== |
Lo siguiente que vamos necesitamos, una vez que tenemos cargado el package, es crear la clase que se implementa en el package; Una vez hecho esto ya tendremos total acceso a los métodos que necesitemos y realmente ya habremos conseguido nuestro objetivo.
Para crear la clase utilizamos la lista de clases (ExClassList) que hemos comentado en el párrafo anterior y que hemos rellenado en la sección de inicialización; Otra opción también viable es utilizar GetClass de Delphi mediante RTTI junto con el nombre de la clase registrada (TShapeExArrow). También funcionaría, aunque en este caso, por comodidad, hemos utilizado estas listas auxiliares.
// Crear la clase b := ExClassList.Find(pName, i); // encontrada? if (b) then begin AClass := TPersistentClass(ExClassList.Objects[i]); // OTRA OPCIÓN: BClass := GetClass('TShapeExArrow'); end; |
Para finalizar y después de haber realizado unas comprobaciones, llamamos al método CargarCategoria, que crea de forma dinámica la ventana asociada a esa categoría (con la descripción) y también crea el elemento individual asociada a cada Shape implementado en esa clase.
En este punto ya hemos hecho uso de todo lo implementado en ese package, puesto que ya hemos creado un objeto de todos los implementados.
// Cargar los objetos de ese plugIn
CargarCategoria(AClass, desc);
En este ejemplo, no descargamos los packages, puesto que los necesitamos para seguir trabajando con los objetos que tenemos en pantalla, lo que hacemos realmente es ocultar la ventana. Si la operación que desempeña el package no necesita que posteriormente esté cargado, bastaría con descargarlo utilizando UnloadPackage.
Hasta aquí las descripción de todo el proceso. Junto con el artículo os adjunto el ejemplo completo y bastante comentado. Es sencillo, pero muestra a la perfección el manejo práctico de este tipo de ficheros.
Espero que haya quedado claro y si hay comentarios o sugerencias, ya sabéis. ¡¡Disparad!! ;-D
Imagen del programa de ejemplo.
El código del ejemplo se puede descargar desde aquí y los binarios (EXE + BPL’s) desde aquí.
Tal como me comenta Salvador, en el proyecto no se incluyen las dos BPL’s de Dephi (de la VCL) que ne necesitan para ejecutar el proyecto. Si no tenéis Delphi 6 instalado, las necesitaréis para ejecutar. Os coloco los links, con el proyecto (binarios incluyendo las BPLs) y un fichero sólo con los dos ficheros (VCL60.BPL y RTL60.BPL).
<DESCARGAR BINARIOS (Incluyendo VCL60.BPL y RTL60.BPL)>
<FICHEROS VCL60.BPL y RTL60.BPL>
Embarcadero MVP.
Analista y Programador de Sistemas Informáticos.
Estudios de Informática (Ingeniería Técnica Superior) en la UPC (Universidad Politécnica de Barcelona).
Llevo utilizando Delphi desde su versión 3. Especialista en diseño de componentes, Bases de Datos, Frameworks de Persistencia, Integración Continua, Desarrollo móvil,…
Hola German:
He intentado probar el ejecutable (los binarios) pero genera el error:
«Error al iniciar la aplicacion porque no se encontró rtl60.bpl.»
Te solicita tanto esta bpl como la vcl60.bpl. No te has dado cuenta porque los tienes instalados (con la instalacion de tu Delphi) y los encuentra pero en el caso de que se ejecute en un equipo donde no tenga las librerías generaría el error.
A mi me ha pasado también en ocasiones que no me he dado cuenta hasta que he intentado ejecutar en un equipo distinto. :-)
Ahh… corrigeme por favor el enlace ya que estoy duplicado como «Delphi Básico» y «Santiago Jover». Elimina lo de Santiago Jover ya que es Salvador Jover. Yo dejaría el de Dephi básico.
Un saludo,
Salvador
@Salvador Jover
Hola Salvador.
Gracias por el comentario. Efectívamente, al ser un proyecto compilado com packages, se hace necesario distribuir los de la VCL. Mi suposición de que todo el mundo los tenga instalador puede no ser acertada. Ahora añado un link con los necesarios.
He eliminado el enlace duplicado y disculpa por haberte «rebautizado».
;-D
Un saludo.
Mas comentarios (sobre el binario):
Al ejecutarse muestra las categorias «Borland RunTime Library» y «Borland Visual Component Library» (ademas de las propias) y generan un error al cargarlas. En este caso es la tipica excepcion. Creo que es porque la matriz que utilizas para recorrer y cargar las ventanas de las distintas categorias se sale del rango y no encuentra asignado el puntero.
Me he dado cuenta de que en el articulo solo aparecen las creadas por ti por lo que no se con seguridad si tenias pensado que se pudieran añadir en el ejemplo componentes de la vcl como botones, etc… o simplemente que no se ha filtrado para que no buscara esas categorias.
Saludos,
Salva
Hola German, nuevamente
resumiendo un poco, me parece fantástico el articulo y el ejemplo que has utilizado ya que muchos, como yo mismo, estaremos pensando que utilidades podemos darle dentro de las habituales estructuras que usamos.
Es mas, mientras te estoy escribiendo estoy pensando que quizás se pudiera extender y que el vinculo de unión entre la clases fuera en lugar de la herencia, el que todas las clases compartieran una interfaz?????
Eso permitiria que objetos heterogeneos pudieran ser cargados por el mismo pluggin sin tener que tener un tipo ascendente comun????
:-)
Igual digo una tontería…
En fin… un acierto el articulo, Germán.
Hola Germán:
Espero que no te moleste que añada un comentario mas. Solo quería añadir que el error venía porque involuntariamente había copiado las bpl rtl60 y vcl60 en el mismo directorio y por eso generaba la excepción, ya que al cargar dinámicamente se busca las bpl del directorio indicado.
Meu error… :-(
Era por ese motivo la excepcion. No me hagas mucho caso pero creo que saltaba finalmente en la linea:
if (TPersistentClass(ExShapeList.Objects[i]) = BaseClass)
de la unidad FCategoriaElemens, al invocarse «CreateItems();»
Había leído el código mas o menos, desde el bloc de notas, pero no habia intentado compilar, ya que tengo en casa D2007 y sabia que daria algunos problemas. Estoy en ello. :-) De hecho, llevo un rato y todavia no lo he conseguido :-(
Torpeza de uno!!!!! :-)
Mas saludos,
Salvador
Hola,
sería posible poder obtener el artículo en un PDF (como el anterior) o algo así…. El bus y los portátiles abiertos no se llevan bien… jejjeje
Gracias… y felicidades por el artículo..
@Salvador Jover
Hola Salvador.
Acabo de ver los comentarios, que supongo en su día se me pasaron (tal vez algun problema del Blog), ya que no he recibido aviso de ellos.
Pues no creo que sea ninguna tontería lo que dices. La distribución que he utilizado en el artículo va bien para ver el funcionamiento, pero tal vez no sea la más práctica si se desea utilizar en algo más serio. Y el tema de los Interfaces tiene buena pinta.
Un saludo.
Hola Salvador.
Nunca molestan tus comentarios, al contrario, sabes que te estoy muy agradecido.
¿Sigues con problemas?
He vuelto a descargar los fuentes, he cargado el GroupAllD2010.groupproj y me ha compilado y funcionado sin problemas (en D2009). En D2007 también debería funcionar. ¿Lo has conseguido ya?
Teóricamente con el EXE y las BPL’s en el mismo directorio no debería dar problemas.
Un saludo.
A ver si puedo hacer algo al respecto… ;-)
Ya están publicados y añadidos a la entrada los dos ficheros. Tanto en PDF como en formato OpenOffice.
Un saludo.
Is there a way to use an application with BPL as plugins without use the need of embarcadero BPL (VCL60.BPL e RTL60.BPL for example)? Only my application and my BPL.
Thanks
@array81
Yes. It’s possible. Read this article at blog, and test the different possibilities:
http://neftali.clubdelphi.com/?p=803
You can compile your Main applicaction without «runtime packages» and charge a packages using LoadLibrary, but in this case you can’t use RTTI information. You must use the package like a DLL, but it’s possible.
In the article (http://neftali.clubdelphi.com/?p=803), this case is explained like «EXE + BPL con Carga dinámica sin RTTI(El EXE sin BWRP)»; Is in Spanish but you can try the automatic translation. You can also see the samples.
Regards.
The problem is: I need pass an object between my application and my plugin and I have problem if I use a DLL :(
@array81
You can use a BPL and can pass an object, but you can’t use RTTI.
Regards.
Excelente articulo como siempre, muchas gracias German, seguramente me será de mucha utilidad
Gracias por el artículo me ha servido de mucho
Saludos
@axesys
Gracias.
Me alegra de que haya sido útil.
Un saludo.
German, buen dia. se que el articulo ya tiene mucho tiempo pero me surge una duda a ver si me puedes ayudar. Hace muchos años use un programa que te permitia agregarle plugins, estaba hecho en delphi y ofrecia un SDK para que los usuarios pudieran programarlos y agregar los plugins, el asunto es que ofrecia las librerias para que los plugins se pudieran programar tanto en delphi 3 como en delphi 5 y se podian agregar de la misma manera al programa. Tu sabes como puede hacerse eso? como puedo agregar plugins por ejemplo hechos en delphi XE2 y otros realizados en delphi Berlin y que sean soportados por la aplicacion host?. Saludos.
German, buen dia. se que el articulo ya tiene mucho tiempo pero me surge una duda a ver si me puedes ayudar. Hace muchos años use un programa que te permitia agregarle plugins, estaba hecho en delphi y ofrecia un SDK para que los usuarios pudieran programarlos y agregar los plugins, el asunto es que ofrecia las librerias para que los plugins se pudieran programar tanto en delphi 3 como en delphi 5 y se podian agregar de la misma manera al programa. Tu sabes como puede hacerse eso? como puedo agregar plugins por ejemplo hechos en delphi XE2 y otros realizados en delphi Berlin y que sean soportados por la aplicacion host?. Saludos.
Hola Juan Carlos.
Todo depende de cómo estén los plugins realizados. Hay muchas formas de hacerlo, pero básicamente los dividiría en 2.
1) Realizar los plugins como si fueran DLLs (pueden ser ficheros BPL o DLL).
2) Realizar los plugins como BPLs (utilizando caracteristicas de RTTI).
En el primer caso pierdes funcionalidad, pero puedes usar los plugins entre diferentes verisones y desde otras programaciones incluso. Es usar BPLs como si fueran DLLs.
En el segundo caso (que es la forma en que yo los utilicé en el ejemplo) se usa RTTI, por lo tanto necesitas que sean BPLs. La forma de usarlos y la programación es más potente. Permite hacer muchas más cosas, utilizar RTTI, pero estás limitado a que estén hechos en Delphi.
Por otro lado (y creo que esto afectará a ambos casos -seguro al segundo-) es que a lo largo de las versiones de delphi han cambioado el tema de strings/Unicode, así que has de tener presente el cambio que hubo entre Strings/AnsiStrings entre versiones.
Cosa que imposibilita, por ejemplo que los plugins del ejemplo (creados en Delphi 6), carguen sin problemas en versiones posteriores. Si llegan a cargar, pero cuando haces operaciones con strings obtienes errores.
Espero que te ayude.
Un saludo.