Hola compañeros y amigos del blog de Delphi Básico.
Estamos tan cercanos a las vacaciones que tenemos que aprovechar los días que nos quedan para ir cerrando temas pendientes. Y entre esos temas pendientes, estaba acabar esta serie. De igual forma, quisiera escribir, antes de que entornemos las persianas del blog, alguna entrada sobre el día a día de la Comunidad y de cómo lo estamos viviendo.
🙂
Se podría decir que al tiempo de escribir estas notas ando también liado con los chismes y artilugios veraniegos propios del mes, bajo la sombra de una deseada sombrilla playera, afortunado de dejar las plantas sobre la arena humeda de la orilla. Con un ojo en el inmenso horizonte del mar mediterraneo y el otro en la plantilla de edición de wordpress de mi blog.
😀
¡ehhhh! ¡Despierta…!
¡Ya quisiera yo…! jajaja Lo único cierto de lo dicho es que estoy escribiendo la entrada. Lo otro es imaginación para hacer este momento mas agradable.
😉
Vamos a lo nuestro… Cierra este artículo, la serie que ha intentantado acercarte al conocimiento de algunas novedades de nuestro TTreeView en la nueva plataforma Firemonkey. El interés pienso que es justificado, dado que quienes hemos podido hacer uso de éste en nuestros desarrollos desde la VCL, nos gusta valorar a priori que dificultades vamos a encontrar en aquellos casos en que se nos pueda plantear una migración o simplemente su uso. El solo dar un vistazo a estas pinceladas ya nos ponen en alerta sobre qué problemas vamos a tener que ir sorteando y qué otras ganacias se suponen.
Ciertamente no lo hemos visto todo. Somos conscientes de las limitaciones que tenemos en cuanto a tiempo, y necesitariamos muchas entradas para verlo con detalle. 😦 Tras la andadura de estos tres artículos, tengo la intuición de que puede ser más provechoso para la Comunidad avanzar y profundizar en lo que atañe a otros temas más genéricos, como los estilos, de forma que podeamos tener unos conocimientos mas correctos de sus posibilidades. Así que es uno de los temas que tengo anotados, junto con otro tema, datasanp, que ha sido fruto de algunas consultas vuestras en correos privados. La verdad es que podría ampliar la lista porque también quise escribir alguna entrada sobre RadPhp. O sobre componentes web. O sobre FastReport.
¡Hay tanto de lo que podemos hablar y tan poco tiempo que tenemos que intentar aprovecharlo al máximo!.
Pero al menos, hemos podido ver algunas cosas esenciales en la serie: como la posibilidad de hacer uso del check en el item, las pautas mas básicas en la construcción de nuevos nodos, bien enlazados al propio árbol, bien a través de una relación de anidamiento a otro nodo. Y en relación a ésto, lo que nos ocupaba la última entrada, que nos servia de excusa para ver con un ejemplo básico la navegación, tanto global como local al nodo.
Además, podemos, si el día de mañana se hace necesario, ampliar algun detalle adicional añadiendo algún anexo a la serie, complementando, matizando o rectificando lo dicho en ella.
Y en la entrada de hoy…
Hoy tenemos dos puntos más que cubrir. Los elegí porque ambos los he tenido que usar desde el componente TTreeView en nuestra VCL, y al inicio de la serie ya me preguntaba a mi mismo, ¿cómo lo haría desde la nueva plataforma?
El primer punto o aspecto a cubrir será el uso de estructuras o clases que -valga la redundancia- hagan uso del árbol y se sirvan de él para interactuar con nuestro usuario. Esto no es algo extraño. Es más farragoso dicho así que en realidad el concepto último que encierra. Todos hemos utlizado alguna vez una estructura jerárquica como pueda ser el árbol para visualizar contenidos anidados, árboles de costes y estructuras o temas vinculados a la contabilidad o simplemente el desglose de un producto. Y en este caso, nos hemos inventado un pequeño supuesto donde buscamos crear una estructura que relaciona materias primas, operaciones y componentes con componentes, de forma similar a una estructura de coste que pueda devolver el computo del valor total de dicho anidamiento.
Lo mejor es que lo veamos desde uno de los diagramas que podemos obtener desde nuestro IDE. Este es un diagrama de clases y puede ser capturado si se activa el modelado en el proyecto (pulsando Model View den la ventana de nuestro Project Manager) y se añade un diagrama de clases sobre el módulo deseado. En este caso yo lo hicé sobre el módulo UEstructuras.pas, a posteriori de haberlo escrito, y como resultado pude visualizar las relaciones entre las distintas clases que especificaba su interfaz.
Esta es la imagen del diagrama:
.
Luego ampliaremos algunos detalles del ejemplo. Pero en si es bastante claro. Tenemos una clase base TClaseBase, de la cual descienden el resto de clases: TMateriaPrima, TOperación y TComponente. Y fijamos unos mínimos axiomas que deben cumplirse, para que tenga un sentido el ejemplo. A mi se me ocurrieron estos, pero podriamos ampliarlos o restringirlos tanto como desearamos:
- Un componente puede contener materias primas, operaciónes y un único componente. Su valor se calcula obteniendo la suma de los valores de su contenido.
- Una materia prima obtiene su valor multiplicando un precio por la cantidad unitaria.
- Una operación obtiene su valor multiplicando un precio por la cantidad unitaria.
Lógicamente se sobrentiende que un componente nunca podría componerse a si mismo y que de ser un ejemplo real deberiamos garantizar que no existen referencias circulares que nos hicieran entrar en cálculos que generasen desbordamientos de pila. No. Esto es tan solo un ejemplo y todos esos aspectos simplemente nos liarían mas. Al añadir la posibilidad de admitir que un componente pueda contener otro componente se busca simplemente visualizar el uso de la funcionalidad drag&drop de la clase TTreeView, cuando soltamos durante dicha operación de arrastre un nodo sobre otro.
Aquí tenéis una imagen del programa en ejecución
El segundo tema, que cerrará la entrada, será el tema de las imágenes a nivel de nodo. Esto también se me hacía algo básico a la hora de reflejar la naturaleza del nodo (es decir, del objeto que representa y que se esconde en alguna parte de el). Para ello, mostraremos una aplicación que segun el objeto representado, muestre un icono distinto al ser seleccionado por el usuario, lo cual nos muestra capacidad para poder, no ya incluir una imagen, sino modficarla en tiempo de ejecución.
Finalmente, ésta es una imagen de la aplicación test.
Calculando estructuras…
En el video expuesto en la primera parte de la serie Taller práctico – Arbol TTreeView en Firemonkey (I) se visualiza una aplicación que partía de los mismos supuestos que el ejemplo que vamos a ver. La pudisteis ver en funcionamiento en el mismo vídeo y se puede ver como a un arbol se añade una composición valorada de un supuesto despiece de un equipo informático. Habitualmente, llega a ser normal que mientras escribo el código de los ejemplos me pregunte si lo puedo hacer de una forma más sencilla y más clara, en vista a que sea más educativo y funcional. Y por esa razón, tras una larga semana volví a reescribir el código para hacer mas o menos lo mismo pero de una forma mas sencilla, de cara esta entrada.
En dicho proyecto, había añadido ventanas que permitían modificar las cantidades de cada compuesto, de forma que el enlace fuera también visto desde la perspectiva de livebindings, aplicando esta tecnología para guardar los datos. Esto es algo que finalmente he prescindido. Tampoco era idéntica la estructura de herencia. No es que fuera mas confusa pero no era exactamente igual.
Habia creado una clase descendiente del componente TTreeViewItem
TNewTreeViewItem = class(TTreeViewItem) private FHideObject: TObject; FImageItem: TBitmap; function GetHideObject: TObject; procedure SetHideObject(const Value: TObject); procedure SetImageItem(const Value: TBitmap); protected public constructor Create(AOwner: TComponent); override; destructor Destroy; override; procedure Paint; override; property ImageItem: TBitmap read FImageItem write SetImageItem; property HideObject: TObject read GetHideObject write SetHideObject; end;
La propiedad HideObject me permitía guardar una referencia al objeto real, que de una forma similar al ejemplo actual podía representar las clases objeto de negocio.
Tambien habia creado un descendiente de TTreeView (TSJTreeView) que permitía guardar una referencia a una clase TEstructuraCoste, que a su vez era contendedor de las clases TbaseMateriaPrima, TBaseOperacion y TBaseComponente. Esta era la especificación elegida.
TBaseMateriaPrima = class(TBase) public function GetValor: Double; override; constructor Create(AEstructura: TEstructuraCoste; AID: Integer; const ANombre: String; ACantidad, APrecio: Double; ANodo: TNewTreeViewItem); override; destructor Destroy; override; end; TBaseOperacion = class(TBase) public function GetValor: Double; override; constructor Create(AEstructura: TEstructuraCoste; AID: Integer; const ANombre: String; ACantidad, APrecio: Double; ANodo: TNewTreeViewItem); override; destructor Destroy; override; end; TBaseComponente = class(TBase) private FEstructuraOwner: TEstructuraCoste; FCambiosPendientes: Boolean; function GetEstructuraOwner: TEstructuraCoste; procedure SetEstructuraOwner(const Value: TEstructuraCoste); procedure SetCambiosPendientes(const Value: Boolean); public procedure Update(AEstructura: TEstructuraCoste; AValor: Double); virtual; function GetValor: Double; override; constructor CreateComponente(AEstructura, AEstructuraOwner: TEstructuraCoste; ACantidad: Double; ANodo: TNewTreeViewItem); virtual; destructor Destroy; override; property EstructuraOwner: TEstructuraCoste read GetEstructuraOwner write SetEstructuraOwner; property CambiosPendientes: Boolean read FCambiosPendientes write SetCambiosPendientes; end;
Y la clase TBase, servía como ancestro y como parámetro necesario al invocar genéricamente funciones y procedimientos mediante herencia operando con cualquiera de las estructuras finales descendiente de esta.
Apreciaréis por las lineas inferiores que TBase contenía básicamente lo que, tras el cambio, he venido a definir como TProducto, en esa nuevo diseño planteado.
TBase = class private FEstructura: TEstructuraCoste; FNodo: TNewTreeViewItem; FPrecio: Double; FID: Integer; FCantidad: Double; FNombre: String; FValor: Double; FPuedeValorar: Boolean; procedure SetCantidad(const Value: Double); procedure SetID(const Value: Integer); procedure SetNombre(const Value: String); procedure SetPrecio(const Value: Double); function GetCantidad: Double; function GetID: Integer; function GetNombre: String; function GetPrecio: Double; protected public function GetValor: Double; virtual; abstract; function GetNodo: TTreeViewItem; virtual; function GetEstructura: TEstructuraCoste; constructor Create(AEstructura: TEstructuraCoste; AID: Integer; const ANombre: String; ACantidad, APrecio: Double; ANodo: TNewTreeViewItem); virtual; destructor Destroy; override; property PuedeValorar: Boolean read FPuedeValorar; property ID: Integer read GetID write SetID; property Nombre: String read GetNombre write SetNombre; property Cantidad: Double read GetCantidad write SetCantidad; property Precio: Double read GetPrecio write SetPrecio; property Valor: Double read GetValor; end;
Bueno… todo eso fue historia tras los cambios, en aras de buscar claridad y sencillez. Y comento todo esto precisamente por los compañeros que en muchas ocasiones me han hecho referencia a que desean profundizar en la programación orientada a objetos y esperaban una receta mágica que sea capaz de expresar la lógica de su desarrollo, de forma infalible y perfecta. A ellos y por ellos les extiendo el comentario, haciendoles hincapié en que cualquier camino a menudo desvela una o más alternativas. A veces mas sencillas. Otras veces no tanto. Nos queda estar atentos a las señales y dejar que la experiencia, los patrones conocidos que han sido camino de otros programadores, nos enseñen el camino. A veces, este camino inicialmente sencillo puede convertirse con el tiempo en un tortuoso sendero a ninguna parte. El tiempo va a ser ese juez. No me queda duda.
🙂
Así que con el fin de eliminar código que no sea estrictamente necesario para nuestro ejemplo, también sustituí la introducción de datos para cantidades y precios por una rutina que introduzca una cantidad aleatoria. A efectos nuestros realmente nos da lo mismo y podemos dejar para mas adelante, el enlace de datos a través de livebindings en alguna entrada que lo aborde específicamente.
Un detalle más…: sobrescribir el metodo Paint en TNewTreeViewItem era factible. En el video de la entrada anterior se pueden ver iconos distintos en función de la naturaleza del objeto contenido en los nodos.
Al sobrescribir paint se podía hacer algo como:
procedure TNewTreeViewItem.Paint; var R: TRectF; begin if Assigned(FImageItem) then begin R:= LocalRect; Canvas.DrawBitmap(FImageItem, RectF(0,0, FImageItem.Width, FImageItem.Height), RectF(R.Left, R.Top, FImageItem.Width div 4, FImageItem.Height div 4), AbsoluteOpacity, True); //InflateRect(R, -FImageItem.Width-10, 0); //Canvas.Fill.Color:= claRed; //Canvas.FillText(R, Text, True, 255, [],TTextAlign.taLeading,TTextAlign.taCenter); end else inherited Paint; end;
lo cual, siendo funcional realmente no era la forma en la que la plataforma Firemonkey enfoccaba el tema, sino el modo que hemos usado hasta la llegada de Firemonkey, sobrescribiendo el procedimiento responsable de pintar y personalizar el control.
Tras la atenta lectura del artículo de Jeremy North, Extending TListBoxItem to customize list box behaviour, publicado en en numero 23 de la Revista Blaise Pascal Magazine descubrimos que los estilos permiten personalizar las clases y su representación visual. Este aspecto se verá en el punto dos de nuestra entrada. Es un artículo muy interesante y os lo recomiendo abiertamente. No tiene desperdicio.
La unidad Estructura.pas contiene las clases básicas que calculan la estructura de coste.
Esta es su especificación:
unit UEstructuras; interface uses System.SysUtils, System.Types, System.UITypes, System.Classes, System.Variants, FMX.TreeView, UInterfaces, FMX.Types; type TClaseBase = class(TFMXObject) private FNodo: TTreeViewItem; function GetNodo: TTreeViewItem; protected function HasNodo: Boolean; public constructor Create(AOwner: TComponent); override; destructor Destroy; override; property Nodo: TTreeViewItem read GetNodo; end; TComponente = class; AClaseBase = class of TClaseBase; TProducto = Class(TClaseBase, IValorable) private FID: Integer; FNombre: String; FCantidad: Double; FPrecio: Double; FValor: Double; FParent: TComponente; FExistenCambiosPendientes: Boolean; procedure SetParent(const Value: TComponente); procedure SetExistenCambiosPendientes(const Value: Boolean); protected //getters and setters function GetCantidad: Double; virtual; function GetID: Integer; virtual; function GetNombre: String; virtual; function GetPrecio: Double; virtual; function GetValor: Double; virtual; abstract; procedure SetCantidad(const Value: Double); virtual; procedure SetID(const Value: Integer); virtual; procedure SetNombre(const Value: String); virtual; procedure SetPrecio(const Value: Double); virtual; procedure ActualizaTextoNodo; virtual; procedure ActualizaValor; virtual; abstract; procedure DoDelete; virtual; abstract; property ExistenCambiosPendientes: Boolean read FExistenCambiosPendientes write SetExistenCambiosPendientes; public constructor Create(AOwner: TComponent); override; procedure Delete; //public properties property ID: Integer read GetID write SetID; property Nombre: String read GetNombre write SetNombre; property Cantidad: Double read GetCantidad write SetCantidad; property Precio: Double read GetPrecio write SetPrecio; property Valor: Double read GetValor; property Parent: TComponente read FParent write SetParent; end; TMateriaPrima = Class(TProducto) protected function GetValor: Double; override; procedure ActualizaTextoNodo; override; procedure ActualizaValor; override; procedure DoDelete; override; End; TOperacion = Class(TProducto) protected function GetValor: Double; override; procedure ActualizaTextoNodo; override; procedure ActualizaValor; override; procedure DoDelete; override; End; TComponente = Class(TProducto) private FItems: TList; FNotificaciones: TList; FSource: TComponente; FOwner: IUnknown; function GetItems(Index: Integer): TProducto; procedure SetItems(Index: Integer; const Value: TProducto); procedure SetSource(const Value: TComponente); function GetSource: TComponente; procedure CopiaContenido(const Value: TComponente); procedure SetOwner(const Value: IUnknown); protected function NuevoNodo: TTreeViewItem; function GetValor: Double; override; procedure SetPrecio(const Value: Double); override; procedure ActualizaTextoNodo; override; procedure ActualizaValor; override; procedure DoDelete; override; public constructor Create(AOwner: TComponent); override; destructor Destroy; override; function AddChildren(AClaseProducto: AClaseBase): Integer; function Count: Integer; procedure DeleteChildren(AProducto: TProducto); property Source: TComponente read GetSource write SetSource; property Items[Index: Integer]: TProducto read GetItems write SetItems; End;
Podemos comentar algunos puntos que pueden ser de vuestro interés respecto a la implementación.
La clase TClaseBase es el ascendente de nuestra jerarquía, responsable tanto de crear el nodo que permite su representación gráfica como de permitir que dicho nodo pueda almacenar la referencia al objeto que va a ser creado, que permitirá que cualquier nodo pueda tener una naturaleza propia en función del objeto guardado.
implementation uses UComunes; { TBase } constructor TClaseBase.Create(AOwner: TComponent); begin inherited Create(AOwner); FNodo:= TTreeViewItem(AOwner); FNodo.TagObject:= Self; end; destructor TClaseBase.Destroy; begin if FNodo <> Nil then begin FNodo.Parent:= Nil; end; inherited Destroy; end; function TClaseBase.HasNodo: Boolean; begin Result:= (FNodo <> Nil); end; function TClaseBase.GetNodo: TTreeViewItem; begin Result:= FNodo; end;
Existe un detalle interesante. Nuestra jerarquía pasa por descender de un componente (TFMXObjet lo es), lo cual permite aprovechar que exista una cadena de propietarios que elimine nuestra estructura creada, al momento de la destrucción. AOwner representará en el formulario principal uno de los nodos de nuestro arbol. Realmente podría ser cualquier nodo pero nosotros nos basamos sobre la selección del usuario y el nodo resultante de la propiedad Selected.
Respecto a la clase TProducto, es una clase «generica» (no en el sentido de las clases genericas) que nos permte parametrizar las llamadas de métodos sin saber a priori sobre quien recaen. TProducto no resuelve aquellos puntos que son responsabilidad de las clases descendientes, que sí conocen como deben implementar y responder.
Un ejemplo puede ser el método Delete.
procedure TProducto.Delete; begin DoDelete; end;
Nuestro método invoca a DoDelete, abstracto en TProducto. Cada uno de los posibles descendientes darán una respuesta especifica y la herencia hará el resto, permitiendo que podeamos diseñar un procedimiento generico apoyado en la clase ascendente. Esta es la invocación que hacemos desde el formulario principal, con independencia de que la llamada real la hagamos desde una materia prima, una operación o un componente.
procedure TMain.EliminarProducto; var FNodo: TTreeViewItem; FProd: TProducto; begin FNodo:= Arbol.Selected; if (FNodo <> Nil) and (FNodo.TagObject <> nil) then begin FProd:= FNodo.TagObject as TProducto; FProd.Delete; end; end;
Sucede algo similar con la función GetValor, definida en TProducto como abstracta. La clase TProducto realmente no conoce (ni le interesa) cómo valorarán sus descendientes (cómo calcularan su valor). Pero es definida a ese nivel porque el mecanismo de la herencia nos permite usarla en un momento posterior invocando el método en este ascendente común que acaba ejecutandose en el objeto que realmente hizo la invocación. Todo esto forma parte de los mecanismos que nos proporciona la herencia y el polimorfismo.
Si analizamos como implementan cada uno de ellos lo veremos mas claro. Tanto la materia prima como las operaciones dan una misma respuesta:
function TMateriaPrima.GetValor: Double; begin if ExistenCambiosPendientes then begin ActualizaValor; ActualizaTextoNodo; end; Result:= FValor; end; function TOperacion.GetValor: Double; begin if ExistenCambiosPendientes then begin ActualizaValor; ActualizaTextoNodo; end; Result:= FValor; end;
En cambio, cuando le pedimos a un componente que nos devuelva su valor, éste debe recorrer cada uno de los productos que contiene y acumular el valor de cada uno de ellos hasta alcanzar el último de los nodos en la cadena de componentes creados y anidados.
procedure TComponente.ActualizaValor; var i: Integer; begin if Source <> nil then FPrecio:= Source.GetValor else FPrecio:= 0; for i := 0 to FItems.Count - 1 do begin FPrecio:= FPrecio + TProducto(FItems[i]).GetValor; end; FValor:= FCantidad * FPrecio; ExistenCambiosPendientes:= False; //notificamos que se debe revisar el poseedor if (Parent <> Nil) then Parent.ExistenCambiosPendientes:= True; for i :=0 to FNotificaciones.Count - 1 do TComponente(FNotificaciones[i]).ExistenCambiosPendientes:= True; end; function TComponente.GetValor: Double; begin if ExistenCambiosPendientes then begin ActualizaValor; ActualizaTextoNodo; end; Result:= FValor; end;
El por qué tuve que definir un campo ExisteCambiosPendientes es simplemente para crear una cadena que pueda propagar los cambios y notificar que algo ha cambiado (el precio, la cantidad, el total) y que si existe un nodo de nivel superior debe ser recalculado para mostrar los nuevos datos. Cada vez que un componente recalcula su valor pone el campo ExistenCambiosPendientes a false, indicando que el valor que se almacena ya es correcto y no debe ser recalculado.
El resto de metodos que implementa TProducto no son demasiado interesantes, en el sentido de que contienen los campos que almacenan los datos básicos de nuestra lógica de negocio, como puedan ser el nombre del objeto, su identificador, su precio, su cantidad y cualquier otro dato que nos pudiera hacer falta y fuera común a sus descendientes. Algunos de los métodos de éste, pueden ser agrupados en una interfaz común: IValorable, permitiendo que desde el formulario principal se puedan utlilzar interfaces para manipular algunos de los métodos del objeto. La ventaja de tener una interfaz como IValorable es que nos brinda la posibilidad de manipular una instancia sin conocer realmente los detalles del objeto que hay detrás. A fin de cuentas, una interfaz es como un contrato. La clase que declara un interfaz asume dar una respuesta a los métodos que el interfaz expone públicamente.
La unidad Interfaces.pas contiene esa posible interfaz que representa un objeto que puede ser valorado.
IValorable = interface {setters and getters} procedure SetCantidad(const Value: Double); procedure SetID(const Value: Integer); procedure SetNombre(const Value: String); procedure SetPrecio(const Value: Double); function GetCantidad: Double; function GetID: Integer; function GetNombre: String; function GetPrecio: Double; function GetValor: Double; {acceso} property ID: Integer read GetID write SetID; property Nombre: String read GetNombre write SetNombre; property Cantidad: Double read GetCantidad write SetCantidad; property Precio: Double read GetPrecio write SetPrecio; property Valor: Double read GetValor; end;
Las clases TMateriaPrima y TOperacion tienen una implementación similar. Veamos el caso concreto de TMateriaPrima.
Contiene los métodos a los que da un respuesta específica en función de su naturaleza. Una materia prima actualiza el texto de su nodo, su valor, es borrado de acuerdo a los condicionantes o axiomas definidos al inicio.
{ TMateriaPrima } procedure TMateriaPrima.ActualizaTextoNodo; begin inherited; Nodo.Text:= Format('Materia Prima: ' + Nodo.Text + ', Valor %8.2f €', [FValor]); end; procedure TMateriaPrima.ActualizaValor; begin FValor:= Cantidad * Precio; ExistenCambiosPendientes:= False; //notificamos que se debe revisar el poseedor if (Parent <> Nil) then Parent.ExistenCambiosPendientes:= True; end; procedure TMateriaPrima.DoDelete; begin if Parent <> nil then begin Parent.DeleteChildren(Self); Parent.ExistenCambiosPendientes:= True; end; end; function TMateriaPrima.GetValor: Double; begin if ExistenCambiosPendientes then begin ActualizaValor; ActualizaTextoNodo; end; Result:= FValor; end;
Finalmente, podemos dar una ojeada a tres métodos propios de la clase TComponente, en mi opinión interesantes, sobretodo para quien se inicia y da sus primeros pasos en la orientación a objetos y en algunas de las técnicas propias de ella, apoyadas en la herencia y el polimorfismo.
AddChildren, por ejemplo, puede crear una MateriaPrima, una Operación u otro Componente, invocando un constructor genérico, a través del tipo AClaseBase, que representa cualquier descendiente de la clase TClaseBase.
type
AClaseBase = class of TClaseBase;
...
function TComponente.AddChildren(AClaseProducto: AClaseBase): Integer; begin if Assigned(AClaseProducto) then begin Result:= FItems.Add(AClaseProducto.Create(NuevoNodo)); TProducto(FItems[Result]).Parent:= Self; end else raise Exception.Create('Referencia a objeto no valida'); end;
DeleteChildren( ), nos permite en el lado opuesto, eliminiar un contenido del componente, con indepdencia de que éste sea cualquiera de los descendientes. Daros cuenta que la cadena de destrucciones iniciada en el nodo asociado al producto que deseamos eliminar transmite las notificaciones de destrucción a toda la cadena de objetos apropiados por este.
procedure TComponente.DeleteChildren(AProducto: TProducto); var i: Integer; begin i:= FItems.IndexOf(AProducto); if i > -1 then begin TProducto(FItems[i]).Nodo.Free; FItems.Delete(i); end; end;
Y ya para finalizar, el tercero de los métodos resaltados, es el que permite acoplar un componente dentro de otro, cuando desde el interfaz de la ventana de nuestro usuario, arrastre un nodo componente sobre otro, provocando que se elimine cualquier otro componente acoplado -para así seguir el axioma de inicio- y cambiar la cadena de notificaciones de los recalculos.
procedure TComponente.SetSource(const Value: TComponente); var i: Integer; btns: TMsgDlgButtons; begin if Value <> FSource then begin if FSource <> nil then begin if Value <> nil then begin Include(btns, System.UITypes.TMsgDlgBtn.mbYes); Include(btns, System.UITypes.TMsgDlgBtn.mbNo); if MessageDlg('Si sigues adelante eliminarás el actual contenido componente '+ FSource.Nodo.Text, System.UITypes.TMsgDlgType.mtWarning, btns, 0, System.UITypes.TMsgDlgBtn.mbNo ) <> mrYes then Exit; end; i:= FNotificaciones.IndexOf(FSource); if i >= 0 then FNotificaciones.Delete(i); FSource.Nodo.Free; end; FSource := Value; if FSource <> nil then begin CopiaContenido(Value); if Value.FNotificaciones.IndexOf(Self) = -1 then Value.FNotificaciones.Add(Self); end; ActualizaValor; ActualizaTextoNodo; end; end;
Yo creo que en este punto, podemos ver cual es la estructura del proyecto, que hemos venido a llamar Costes.Exe. Dado que hemos usado componentes y clases que son compatibles en las tres plataformas, windows 32 bits, windows 64 bits y OSX, sin hacer llamadas o invocaciones nativas de alguna de ellas, el resultado final va a ser poder compilar el proyecto en cualquiera de las tres, sin cambiar una línea de código.
El proyecto, va a contener las unidades donde hemos definido las clases citadas anteriormente y la unidad principal de proyecto, que representa el interfaz de nuestro usuario (UMain.fmx) o formulario principal.
Modulo Main.pas
En lo que respecta al formulario principal, pienso que podemos detenernos en algunos procedimientos y dejar una reseña que nos aclare que papel juegan.
ActualizarEstadoBotones permite que los distintos botones muestren una visualización de estado de acuerdo al nodo seleccionado por el usuario.
procedure TMain.ActualizarEstadoBotones(ATipoSel: TTipoProducto); begin bnInsertarComponente.Enabled:= True; bnEliminarComponente.Enabled:= ATipoSel = tpComponente; bnInsertarMateriaPrima.Enabled:= ATipoSel = tpComponente; bnEliminarMateriaPrima.Enabled:= ATipoSel = tpMateriaPrima; bnInsertarOperacion.Enabled:= ATipoSel = tpComponente; bnEliminarOperacion.Enabled:= ATipoSel = tpOperacion; end;
ArbolChange( ) es la respuesta al evento OnChange de la clase TTreeView. Nos hemos valido de él para mantener el estado coherente de nuestro interfaz. Cada vez que cambie la selección de nuestro usuario, los distintos botones se activarán o desactivarán en funcion de la clase que representa el nodo.
procedure TMain.ArbolChange(Sender: TObject); var FNodo: TTreeViewItem; FProd: TProducto; begin FNodo:= Arbol.Selected; if FNodo <> Nil then begin FProd:= FNodo.TagObject as TProducto; if FProd <> Nil then begin if FProd is TMateriaPrima then ActualizarEstadoBotones(tpMateriaPrima) else if FProd is TOperacion then ActualizarEstadoBotones(tpOperacion) else ActualizarEstadoBotones(tpComponente); end; end; end;
ArbolDragChange( ) es la respuesta al evento de arrastre de un nodo sobre el arbol. Utilizamos esta respuesta para saber si debemos permitir o no la operación y en caso de permitirlo, invocar los métodos adecuados a la operación de arrastre. En este caso concreto, un componente se anida dentro de otro.
procedure TMain.ArbolDragChange(SourceItem, DestItem: TTreeViewItem; var Allow: Boolean); var FComp, FNewComp: TComponente; FProd: TProducto; begin Allow:= (SourceItem <> Nil) and (DestItem <> Nil) and (SourceItem.TagObject <> Nil) and (DestItem.TagObject <> Nil) and (SourceItem.TagObject is TComponente) and (DestItem.TagObject is TComponente) and (SourceItem <> DestItem); if Allow then begin TComponente(DestItem.TagObject).Source:= TComponente(SourceItem.TagObject); end; end;
EliminarProducto ya fue comentado previamente. De acuerdo a la naturaleza del objeto referenciado por TagObject, se invocará correctamente la destrucción de la clase descendiente.
procedure TMain.EliminarProducto; var FNodo: TTreeViewItem; FProd: TProducto; begin FNodo:= Arbol.Selected; if (FNodo <> Nil) and (FNodo.TagObject <> nil) then begin FProd:= FNodo.TagObject as TProducto; FProd.Delete; end; end;
Los métodos imgInsertarComponenteClick( ) y InsertarProducto( ) se vinculan a la creación de los componentes (el primero) o bien, en en caso del segundo método, crear productos acoplados (contenidos) en el componente. Podemos insertar una materia prima, una operación u otro componente.
procedure TMain.imgInsertarComponenteClick(Sender: TObject); begin with TComponente.Create(NuevoNodo) do begin ID:= AsignarID(tpComponente); Nombre:= Format('Componente %d', [ID]); Cantidad:= 1; end; end; procedure TMain.InsertarProducto(ATipo: TTipoProducto); var FNodo: TTreeViewItem; FComp, FNewComp: TComponente; FProd: TProducto; begin FNodo:= Arbol.Selected; if (FNodo <> Nil) and (FNodo.TagObject <> nil) then begin if FNodo.TagObject is TComponente then FComp:= FNodo.TagObject as TComponente else raise Exception.Create('Es necesario seleccionar un componente'); case ATipo of tpComponente: begin FNewComp:= TComponente.Create(FNodo); with FNewComp do begin ID:= AsignarID(ATipo); Nombre:= Format('Componente %d', [ID]); Cantidad:= 1; end; FNewComp.Source:= FComp; Exit; end; tpMateriaPrima: FProd:= FComp.Items[FComp.AddChildren(TMateriaPrima)]; tpOperacion: FProd:= FComp.Items[FComp.AddChildren(TOperacion)]; end; with FProd do begin ID:= AsignarID(ATipo); Nombre:= Format('Operacion %d', [ID]);; Cantidad:= 1; Precio:= AsignarPrecio(ATipo); end; FNodo.IsExpanded:= True; end; end;
Podeis descargar el código fuente que he añadido al final de la entrada y compilarlo con el entorno de XE2. La imagen inferior nos muestra la aplicación corriendo sobre nuestro mac.
Me dejo algunas cosas en el tintero: una de ellas es ¿qué pasa con la propiedad Data de TTreeViewItem?. Habeis visto que en la especificación inicial usaba un campo definido a tal efecto, HideObjet. Era una posibilidad. En la actual me he valido de la propiedad TagObject del nodo.
Si buscais información a este respecto acabareis encontrando que en versiones anteriores y en la plataforma VCL, la clase TTreeViewItem contiene un puntero Data, capaz de almacenar una referencia a cualquier cosa. Podeis encontrar y ampliar informacion en el enlace ..store-more-custom-data-into-tree-node-tree-view-delphi.htm
En la definición propia de Firemonkey, Data representa un dato de tipo Variant. Ya no es un puntero sino un valor variant capaz de almacenar diversos tipos distintos.
Pero si deseais usarlo para almacenar la referencia al objeto real, previamente debeis sobrescribir el metodo que en uno de sus ascendientes asigna en función del valor contenido en el campo Text del nodo, convertido a texto en las asignaciones y visualizado como texto en las lecturas del campo.
function TTextControl.GetData: Variant; begin Result := Text; end; procedure TTextControl.SetData(const Value: Variant); begin if VarIsNull(Value) then Text := '' else if VarIsType(Value, varDate) then Text := DateTimeToStr(VarToDateTime(Value)) else Text := VarToWideStr(Value); end;
Quizás la unica pega que debéis de tener en cuenta al trabajar con el tipo variant son las posibles conversiones y la confusión que puede ofrecer los valores asignados a variants para representar el nulo. En la asignación a objetos parece mas sencillo que una referencia pueda contener un objeto o sea nil. Así que fue una de las razones al optar por el uso del campo TagObject, en aras simplemente de no complicarme demasiado la existencia.
En cualquier caso, la conversión entre un variant y una instancia puede hacerse haciendo un casting al tipo adecuado.
fobj := TMiObjeto.Create; fvar := NativeUInt(fobj); fobj := TMiObjeto(NativeUInt(fvar));
donde fobj es la variable que representa a la instancia incial que va a ser guardada en la variable variant, y desde esta podrá ser recuperada posteriormente, tal y como se puede ver. El peligro puede estar en la distinta naturaleza de los datos en la conversión a entero y que estos puedan verse afectados según compilemos en la plataforma 32 o 64 bit. Esto podría dar lugar a que el objeto recuperado contuviera una referencia no valida, fruto precisamente de las conversiones.
Decoramos el arbol…
El segundo punto a tratar en la entrada nos hablaba de añadir una imagen a los distintos nodos de forma que permita visualmente diferenciarlos. Para ello vamos a emplear esa caracteristica nueva que nos ha traido la plataforma Firemonkey: los estilos. Asi que para verlo, abriremos un nuevo proyecto que nos permita comentarlo sin arrastrar todo el código del ejemplo anterior.
El nuevo grupo de proyectos va a contener dos proyectos. Por un lado un proyecto de tipo package para añadir un par de componentes a nuestra paleta. Y por otro lado un proyecto con un formulario que los use. Este componente/componentes van a ser precisamente un descendiente de la clases TTreeView y TTreeViewItem, que representan al arbol y al nodo respectivamente.
Y utlizamos, para ello, como decíamos al principio, los estilos, definiendo un nuevo estilo para la nueva clase que define al nodo, que incluirá la imagen que podrá ser modificada esta en tiempo de ejecución.
Vamos a ir por partes.
Para definir el nuevo estilo podemos abrir un nuevo proyecto (solo a efectos de crear el estilo, luego no lo guardamos) y añadir al formulario un componente TTreeView. Añadimos un item en tiempo de diseño. Los seleccionamos y a continuación, llamando al menu contextual, podemos invocar el editor de estilos pulsando sobre la opción «Edit Custom Style». Lo que queremos es recuperar el estilo asociado a la clase TTreeViewItem.
También podemos añadir un cuaderno de estilos TStyleBook y llamar al editor al hacer click sobre la propiedad Resource del componente. Y a continuacion cargar mediante LoadDefault los estilos por defecto, localizando entre ellos el buscado.
De cualquier forma, lo que realmente nos interesa es que el editor cargue una copia del estilo TTreeViewItemStyle para poder guardarlo en un fichero de estilos y modificarlo con los cambios deseados. Una vez hemos guardado nuestro fichero, si observamos su interior: es semejante a algo a lo que ya estamos acostumbrados. Hablo del mecanismo de persistencia usado en los formularios para almacenar las propiedades en tiempo de diseño.
Este es su contenido, una vez eliminados los estilos que no deseo o no necesito (dejo unicamente los objetos contenidos bajo el objeto que fija el nombre de estilo (StileName) como treeviewitemstyle:
object TLayout Align = alClient Position.Point = '(0,33)' Width = 549.000000000000000000 Height = 702.000000000000000000 object TRectangle StyleName = 'treeviewitemstyle' Position.Point = '(231,331)' Width = 87.000000000000000000 Height = 40.000000000000000000 HitTest = False Fill.Kind = bkNone Stroke.Kind = bkNone object TButton StyleName = 'button' Align = alLeft Position.Point = '(3,3)' Width = 15.000000000000000000 Height = 34.000000000000000000 Padding.Rect = '(3,3,3,3)' StyleLookup = 'treeviewexpanderbuttonstyle' TabOrder = 0 CanFocus = False end object TLayout Align = alContents Position.Point = '(20,0)' Width = 67.000000000000000000 Height = 40.000000000000000000 Padding.Rect = '(20,0,0,0)' object TCheckBox StyleName = 'check' Align = alLeft Width = 20.000000000000000000 Height = 40.000000000000000000 TabOrder = 0 CanFocus = False DisableFocusEffect = True end object TText BindingName = 'text' StyleName = 'text' Align = alClient Position.Point = '(20,0)' Locked = True Width = 47.000000000000000000 Height = 40.000000000000000000 HitTest = False HorzTextAlign = taLeading WordWrap = False end end end end
El fichero de estilos todavía no contiene la imagen. Vamos a incorporarla. Para ello, recuperamos el contenido del nuevo fichero en el editor de estilos tras cargar el fichero mediante Load. Seleccionamos en el interfaz del editor el layout que contiene el check box y el texto del nodo y agregamos un componente TImage desde la paleta de componentes. Para que se ajuste correctamente, podemos desactivar momentaneamente el posicionamiento de align de los elementos citados y volver a activarlos de acuerdo a nuestro interés. Como veis en la imagen anterior, la imagen que muestra el icono del producto, queda entre el checkbox y la marca de colapsar/expandir. Una vez añadida la imagén, asignamos su propiedad StyleName con un nombre cualquiera, como por ejemplo, Image.
Esto último es importante para poder localizar el recurso una vez se cargue el estilo.
Igualmente, podemos cambiar el nombre del estilo del componente que pasaria a ser SJTreeViewItemStyle. Para ello cambiamos el valor de la propiedad StyleName del layout contenedor que ahora será SJTreeViewItemstyle, en lugar de TreeViewItemstyle.
Tenemos que pensar cómo trabaja internamente la definicion de los estilos. El sistema de busqueda permite a Firemonkey descubrir que estilo debe aplicar a cada componente de forma que asigne y encuentre el adecuado. En esta labor participa las propiedades StyleLookUp y StileName. Cuando no está definido un valor en dicha propiedad (StyleLookUp), Firemonkey intenta descubrir el nombre del estilo que por defecto le corresponde a la clase, y para ello ignora la T inicial del nombre de clase y añadiendo el sufijo style al nombre de la clase, incia la busqueda. Si este estilo no está definido, proseguirá en las clases ascendentes hasta encontrar el adecuado, siguiendo la misma mecánica.
Hecho esto, guardamos el fichero y lo dejamos a un lado para su uso posterior.
Esta es la estructura del grupo de proyecto, conteniendo la bpl y la aplicación Test.Exe.
Vamos a añadir un nuevo módulo al proyecto que va a generar la bpl que contiene los componentes. La unidad la hemos llamado USJTreeView.pas.
Contiene el siguiente código:
unit USJTreeView; interface uses System.SysUtils, System.Classes, FMX.Types, FMX.Layouts, FMX.TreeView; type TNotifyOnSelectEvent = procedure(Sender: TObject; ABase: TClass)of object; THelperTreeViewItem = class helper for TTreeViewItem public //Navegacion relativa a hijos de Nodo function First: TTreeViewItem; function Last: TTreeViewItem; function Next: TTreeViewItem; function Prior: TTreeViewItem; function Bof: Boolean; function Eof: Boolean; //Navegacion global function FirstGlobal: TTreeViewItem; function LastGlobal: TTreeViewItem; function NextGlobal: TTreeViewItem; function PriorGlobal: TTreeViewItem; function BofGlobal: Boolean; function EofGlobal: Boolean; end; TSJTreeView = class(TTreeView) private { Private declarations } FOnSelectEvent: TNotifyOnSelectEvent; procedure SetOnSelectEvent(const Value: TNotifyOnSelectEvent); protected { Protected declarations } function NuevoNodo: TTreeViewItem; procedure DoSelectEvent(const Value: TTreeViewItem); virtual; public { Public declarations } procedure SetSelected(const Value: TTreeViewItem); override; published { Published declarations } property OnSelectEvent: TNotifyOnSelectEvent read FOnSelectEvent write SetOnSelectEvent; end; TSJTreeViewItem = class(TTreeViewItem) procedure ApplyStyle; override; end; procedure Register; implementation uses FMX.Objects; procedure Register; begin RegisterComponents('Samples', [TSJTreeView]); end; { TSJTreeView } procedure TSJTreeView.DoSelectEvent(const Value: TTreeViewItem); begin if Assigned(OnSelectEvent) and (Value.TagObject <> Nil) then OnSelectEvent(Self, (Value.TagObject).ClassType); end; function TSJTreeView.NuevoNodo: TTreeViewItem; begin Result:= TTreeViewItem.Create(Self); Result.Parent:= Self; end; procedure TSJTreeView.SetOnSelectEvent(const Value: TNotifyOnSelectEvent); begin FOnSelectEvent := Value; end; procedure TSJTreeView.SetSelected(const Value: TTreeViewItem); begin inherited SetSelected(Value); DoSelectEvent(Value); end; { TSJTreeViewItem } procedure TSJTreeViewItem.ApplyStyle; var img: TFmxObject; begin inherited; img:= FindStyleResource('Image'); if img <> nil then begin if img is TImage then (img as TImage).Bitmap.LoadFromFile('NodoProducto24x24.gif'); end; end; end.
He eliminado de estas lineas la implementación que hicimos del helper, que conservaba de la entrada anterior para no liar con detalles que no son necesarios.
Voy a crear un descendiente de la clase TTreeView.
Añadimos un nuevo evento que nos notifique que nuestro usuario ha seleccionado un nodo. Esto es equivalente a la labor vista anteriormente por los eventos OnChange. Sin embargo, al hacerlo así podemos pasar un parametro adicional en dicha notificación (la naturleza del nodo) de forma que tenemos información adicional de una forma intuitiva y mas directa. También tenemos la ventaja, al hacerlo así, que dejamos libre el evento OnChange para otro uso propio del interfaz cliente.
El descendiente de nuestro nodo (TTreeViewItem) se nos muestra aun más sencillo.
Solo tenemos que sobrescribir el metodo ApplyStyle y buscar en los recursos cargados en memoria, aquel que identifica la imagen añadida al fichero de stilos. Esto lo hacemos mediante la función FindStyleResource( ), que recibe como parametro el nombre del estilo que buscamos. El resto consiste en chequear que la referencia es válida y que realmente contiene una imagen.
Nos resta cargar el contenido mediante cualquier rutina adecuada. Puede ser mediante un stream o como se ha hecho aquí, cargando el contenido de un fichero.
Resumiendo, al aplicar el estilo, el procedimiento cargará una imagen genérica asociada a cualquier producto (materia prima, operacion o componente). También crearemos un ejemplo que haga uso de este componente (que ya disponenmos en la paleta de componentes tras haberlo instalado).
Mas abajo podésis ver la imagen del interfaz. Es muy sencillo. Contiene cuatro botones que permiten crear respectivamente, un nodo no vinculado a un producto, un componente, una materia prima y una operación.
Al hacer click sobre los nodos creados, cambiará la imagen inicial que siempre es la misma (la del producto), en función de que el nodo represente cualquier da las clases citadas. Que es lo que realmente queriamos para ver que estamos modificando el estilo en tiempo de ejecución. Aquí podeis ver el programa en ejecución.
Nuestro programa en ejecución.
Finalmente, añadimos un componente TStyleBook a nuestro formulario. Este componente va a ser usado para guardar una colección de estilos. De hecho puede ser usado a nivel de formulario, permitiendo asignar la propiedad STyleBook del form, y recuperar de un fichero de estilos, aquellos definidos en el contenido, sobrescribiendo los recursos a traves del nombre de estilo. La manipulación del editor de estilos, cargando el fichero de estilos que definimos anteriormente, producirá que el formulario haga persistencia de la propiedad Resource del componente TStyleBook, permitiendo que sea sea recuperados los estilos durante la carga de la aplicación y encontrados por las rutinas de búsqueda a través del nombre.
Veamos el codigo de la unidad UTest.pas
unit UTest; interface uses System.SysUtils, System.Types, System.UITypes, System.Classes, System.Variants, FMX.Types, FMX.Controls, FMX.Forms, FMX.Dialogs, FMX.Layouts, FMX.TreeView, USJTreeView, FMX.Objects; type TMateriaPrima = class(TFMXObject) end; TComponente = class(TFMXObject) end; TOperacion = class(TFMXObject) end; TMain = class(TForm) Arbol: TSJTreeView; bnPulsar: TButton; StyleBook1: TStyleBook; bnMatPrim: TButton; bnOper: TButton; bnComp: TButton; procedure bnPulsarClick(Sender: TObject); procedure ArbolSelectEvent(Sender: TObject; ABase: TClass); procedure bnCompClick(Sender: TObject); procedure bnMatPrimClick(Sender: TObject); procedure bnOperClick(Sender: TObject); private { Private declarations } public { Public declarations } end; var Main: TMain; implementation {$R *.fmx} procedure TMain.bnCompClick(Sender: TObject); var nodo: TSJTreeViewItem; fComp: TComponente; begin nodo:= TSJTreeViewItem.Create(Arbol); nodo.StyleLookup:= 'SJTreeViewItemStyle'; fComp:= TComponente.Create(nodo); nodo.TagObject:= fComp; if Arbol.Selected = nil then nodo.Parent:= Arbol else nodo.Parent:= Arbol.Selected; nodo.Text:= 'Creado Componente'; nodo.ApplyStyleLookUp; end; procedure TMain.bnMatPrimClick(Sender: TObject); var nodo: TSJTreeViewItem; fMatPrim: TMateriaPrima; begin nodo:= TSJTreeViewItem.Create(Arbol); nodo.StyleLookup:= 'SJTreeViewItemStyle'; fMatPrim:= TMateriaPrima.Create(nodo); nodo.TagObject:= fMatPrim; if Arbol.Selected = nil then nodo.Parent:= Arbol else nodo.Parent:= Arbol.Selected; nodo.Text:= 'Creada Materia Prima'; end; procedure TMain.bnOperClick(Sender: TObject); var nodo: TSJTreeViewItem; fOper: TOperacion; begin nodo:= TSJTreeViewItem.Create(Arbol); nodo.StyleLookup:= 'SJTreeViewItemStyle'; fOper:= TOperacion.Create(nodo); nodo.TagObject:= fOper; if Arbol.Selected = nil then nodo.Parent:= Arbol else nodo.Parent:= Arbol.Selected; nodo.Text:= 'Creada Operacion'; end; procedure TMain.bnPulsarClick(Sender: TObject); var nodo: TSJTreeViewItem; begin nodo:= TSJTreeViewItem.Create(Arbol); nodo.StyleLookup:= 'SJTreeViewItemStyle'; if Arbol.Selected = nil then nodo.Parent:= Arbol else nodo.Parent:= Arbol.Selected; nodo.Text:= 'Prueba...'; end; procedure TMain.ArbolSelectEvent(Sender: TObject; ABase: TClass); var fNodo: TSJTreeViewItem; img: TFMXObject; begin fNodo:= TSJTreeViewItem(Arbol.Selected); img:= fNodo.FindStyleResource('Image'); if img <> nil then begin if (img is TImage) then begin if (fNodo.TagObject is TComponente) then (img as TImage).Bitmap.LoadFromFile('NodoComponente24x24gif.gif') else if (fNodo.TagObject is TMateriaPrima) then (img as TImage).Bitmap.LoadFromFile('NodoMateriaPrima24x24gif.gif') else if (fNodo.TagObject is TOperacion) then (img as TImage).Bitmap.LoadFromFile('NodoOperacion24x24gif.gif'); end; end; end; end.
A nosotros nos resultará especialmente valioso el nuevo evento, que nos valdrá para cambiar el contenido de las imagenes. Siempre chequeando la referencia al recurso.
Yo creo que lo mejor es que descargueis el código fuente del enlace que más abajo incluyo y hagais diversas pruebas ejecutando tanto ambas partes de la entrada.
He pensado que podía ayudar que preparara en esta ocasión, de forma especial, un video comentado, de forma que sea más claro el contenido del artículo. Lo del video es algo que me cuesta jajaja pero si puede ayudar como complemento de la entrada daré el esfuerzo por bien empleado. Siento si existen alguna incorrección fruto de la poca experiencia en este tema pero iremos corrigiendo los errores.
😀
Descargar el Código fuente.
En próximas entradas ampliaremos este acercamiento a los estilos. Compartiremos como enlazar estos recursos para no tener que distribuir los ficheros de estilos e imagenes con nuestra aplicación de forma que puedan ser compilados y distribuidos en el mismo ejecutable.
Me resta despedirme deseando que este rato que hemos compartido haya sido de provecho.
Woooooooo!!!!!! Como se suele decir…
Me ha gustado mucho la entrada Salvador, veo que todo el mundo descansa o está de vacaciones menos tú… 😉
Si las otras eran completas, esta sigue la línea.
Un saludo.
Me gustaMe gusta
Hola Germán.
Gracias por el comentario. Me alegra que te parezca útil y te guste.
Había que hacer un pequeño esfuerzo ya que vienen fechas donde, aunque exista mas tiempo libre, no siempre es posible escribir.
Un saludo
Me gustaMe gusta
Caramba Salvador, que pedazo de trabajo has desarrollado y yo que no termino con mi pequeño tutorial 🙂
excelente trabajo…..
Saludos
Me gustaMe gusta
Gracias Eliseo por el comentario.
Me alegra que sea bien valorada por vosotros.
Saludos.
Me gustaMe gusta