Seguimos nuestro pequeño taller explorando y comentando aspectos que pueden resultar de ayuda para vosotros (y para mi también).

😀

Esta es la segunda entrega de la serie.

En esta ocasión vamos a intentar profundizar en el aspecto de la navegación a través de los distintos nodos, que se que es algo que os puede llegar a preocupar (de hecho a mi me preocupaba antes de preparar la serie cuando recien abría el código fuente de la clase para darle un primer vistazo).

Así que nos ponemos manos a la obra y en este tercer ejemplo, vamos a implementar una posible solución a la navegación a nivel de nodo, recorriendo todos los nodos hermanos, y a nivel global, recorriendo todos los nodos (con la particularidad que ya conocíamos de la parte I).

Sería conveniente que si no habéis leído la parte I, lo pudierais hacer ahora y regresar a este punto una vez terminada la lectura, mas que nada para conocer los comentarios que han anticipado este ejemplo. 

Como una imagen vale mas que mis palabras y para que veáis cual es el objetivo, me he permitido hacer una captura de vídeo del ejemplo en ejecución. Por experiencia, y por los comentarios recibidos cada vez que ha acompañado al texto algún apunte visual, se acoge por los compañeros que siguen el blog y su contenido como algo bastante positivo.

Vamos al vídeo:

 

Creo que así queda mucho mas claro, ¿no os parece?.

 

¿Por donde empezamos…?

El proyecto consta de 3 unidades:  UTest.pas, UMain3.pasUHelperTreeView.pas. La segunda de estas unidades se corresponde con el formulario principal y contiene básicamente un componente de la clase TTreeView y unos botones para que su interfaz pueda simular una navegación, bien a través de los nodos o bien a través del indice global, como ya comentabamos.

Esta es la imagen de una de las capturas:

 

demo31

Imagen del formulario de la demo 3.

 

Para que el código de la unidad principal (e interfaz de la aplicación) no se mezclase con lo que es propiamente los datos de inicialización de nuestro árbol, pensé que era conveniente separar esas rutinas y aislarlas en lo que ha sido la primera unidad (UTest.pas) que contiene dos funciones

procedure FillAllNodes(ATreeView: TTreeView; AForm: TForm);
procedure ConfiguraArbol(ATreeView: TTreeView);

cuyo único objetivo es rellenar los distintos nodos del árbol y configurar alguna de sus propiedades (mostrar o no mostrar sus checks a nivel de nodo, pintarse con colores alternos, mostrar las barras de scroll mas estrechas, etc..). No he querido hacer la asignación desde el inspector de objetos para que lo pudierais observar y no pasara inadvertido.

De hecho, y comento esto como mera anecdota, en muchas ocasiones he asistido a discusiones sobre la conveniencia de  inicializar mediante código algunas de las propiedades que opcionalmente se pueden asignar en tiempo de diseño. Muchos compañeros hablan de esa conveniencia en función de una mayor claridad respecto a futuros cambios y migraciones. Es un debate que está condenado a usar el sentido común para resolverlo.   🙂

Y finalmente, en el ultimo de los módulos UHelperTreeView.pas agruparemos los métodos que nos permitan hacer la navegación.

El tema en principio se puede plantear como: ¿me conviene hacer un componente, descendiente de la clase X que los declare?

Aquí cada uno puede plantearse distintas alternativas. Desde el compañero que tome como referencia la clase TTreeView y crease un descendiente que expusiese públicamente estos métodos, hasta quien hiciese recaer el peso en la clase asociada a sus items (TTreeViewItem) para que hiciese lo propio también.

Yo había pensado añadir los siguientes métodos a la interfaz (sea cual fuera la opción final)

    //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;

Y respecto a las opciones, cabría también la posibilidad de haberlos definido a través de un método de clase, entregando como parámetro de la firma de las funciones: el nodo de interés para nuestro usuario (el nodo que interactua desde la interfaz y que en teoría se correspondería con el retorno de Selected, fruto de la selección del usuario).

Yo, en este caso, he optado por la creación de una clase helper (ayudante), que es un recurso en mi opinión poco conocido por nosotros pero valioso. Es algo similar a cuando hablabamos de interponer una clase, en aquel ejemplo de la serie de los mayores:

Un día con los mayores (5) Parte A

[…] Es mas… antes de meternos de lleno en el código, permitirme por favor otro alto en este figurado camino. Permitid que os dirija a la siguiente página dentro de la web del escritor:
http://www.marteens.com/trick46.htm
En ella se habla de un concepto conocido como “interponer una clase”, que resulta extremadamente útil en este contexto como forma práctica y eficaz de extender la funcionalidad de los componentes sin tener que hacer instalaciones en el entorno. Leedla con tranquilidad porque es muy interesante y no tiene desperdicio. Marteens también se vale de esta técnica al abordar su curso y la aplica en varias ocasiones. En este caso concreto, es usada para cambiar el comportamiento del componente DBGrid contenido en la rejilla, de forma que todos los módulos que hagan uso de ella y declaran UGrids tras la unidad Grids de la VCL, redefinen su comportamiento. Así de práctico y de sencillo.
[…]

En mi opinión, parece mas natural el uso de una clase helper, siempre y cuando no se haga necesrio definir miembros nuevos que permitan almacenar estados, para lo cual parece mas propio usar la herencia y los mecanismos acostumbrados.

La ayuda del sistema nos habla acerca de las clases helper y el su motivación al usarlas (*)

A class or a record helper is a type that – when associated with another class or a record – introduces additional method names and properties that may be used in the context of the associated type (or its descendants). Helpers are a way to extend a class without using inheritance, which is also useful for records that do not allow inheritance at all. A helper simply introduces a wider scope for the compiler to use when resolving identifiers. When you declare a class or a record helper, you state the helper name, and the name of the type you are going to extend with the helper. You can use the helper any place where you can legally use the extended class or record. The compiler’s resolution scope then becomes the original type, plus the helper.

Class and record helpers provide a way to extend a type, but they should not be viewed as a design tool to be used when developing new code. For new code you should always rely on normal class inheritance and interface implementations.  […]

Podeis acceder a esta ayuda en el enlace ms-help://embarcadero.rs_xe2/rad/Class_and_Record_Helpers.html para tener mas información.

Es decir, resumiendo: una clase helper nos permitirá extenderla, de forma similar a como lo haría si hubieramos creado un descendiente, mostrándonos los nuevos métodos que publique la clase helper, visibles desde la clase que lo  usa. Esto hace factible también sobrescribir métodos virtuales o crear métodos de clase nuevos.

Podéis leer tambien la interesante entrada que escribió hace tiempo nuestro compañero Carlos Garcia en su blog y que nos hablaba acerca del uso de las clases helper:

http://cgarcia.blogspot.com.es/2006/05/extendiendo-la-vcl-sin-usar-herencia.html

Empezamos por definir la clase nueva:

type
  TCustomizeTreeViewItem = class helper for TTreeViewItem

con los métodos públicos citados mas arriba.

Podemos comentar uno cualquiera de ellos, y el resto van a ser similares: (tomemos como ejemplo ir al primer nodo hermano)

Esta es la implementación que he escogido:

function TCustomizeTreeViewItem.First: TTreeViewItem;
var
  FNode: TTreeViewItem;
  Idx: Integer;
begin
  Result:= Nil;
  FNode:= Self;
  if (FNode <> Nil) then
  begin
    FNode.IsSelected:= False;

    if ParentItem = Nil then
    begin
      Result:= TreeView.ItemByIndex(0);
    end
    else begin
      Result:= ParentItem.ItemByIndex(0);
    end;
    Result.IsSelected:= True;
    TreeView.Selected:= Result;
  end;
end;

La idea es retornar la instancia del nodo fruto del interés de la acción (en este caso el primer hermano del actual). Por tal motivo, si la función tiene éxito retornara este valor y en caso contrario nil. Lo mas resaltable es la distinción que debemos hacer en este caso de distinguir cuando nos situamos en un nodo hijo (inmediato) en el árbol (nivel 1) y cuando nos situamos en un nivel superior, ya que en el primer caso, el valor de ParentItem es siempre nil y la información en ese caso debemos obtenerla en el árbol. Si estuvieramos en un nivel mayor que 1, nuestro ParentItem seria el nodo padre del actual y sería una referencia válida para obtener cualquier información necesaria en ese contexto.

Esa idea se repite en todas las acciones definidas, salvando que en unos casos deseamos situarnos en una posición inicial, intermedia o final.

Os dejo el código de la unidad para que le deis un vistazo y descargueis el código fuente para probarlo.

Código fuente de la unidad UHelperTreeView;


unit UHelperTreeView;

interface

uses FMX.TreeView;

type
  TCustomizeTreeViewItem = 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;

implementation

{ TCustomizeTreeView }

function TCustomizeTreeViewItem.Bof: Boolean;
begin
   Result:= (0 = Index);
end;

function TCustomizeTreeViewItem.BofGlobal: Boolean;
begin
   Result:= (0 = GlobalIndex);
end;

function TCustomizeTreeViewItem.Eof: Boolean;
begin
   if ParentItem = Nil then
   begin
     Result:= (TreeView.Count - 1 = Index);
   end
   else begin
     Result:= (ParentItem.Count - 1 = Index);
   end;
end;

function TCustomizeTreeViewItem.EofGlobal: Boolean;
begin
   Result:= (TreeView.GlobalCount - 1 = GlobalIndex);
end;

function TCustomizeTreeViewItem.First: TTreeViewItem;
var
  FNode: TTreeViewItem;
  Idx: Integer;
begin
  Result:= Nil;
  FNode:= Self;
  if (FNode <> Nil) then
  begin
    FNode.IsSelected:= False;

    if ParentItem = Nil then
    begin
      Result:= TreeView.ItemByIndex(0);
    end
    else begin
      Result:= ParentItem.ItemByIndex(0);
    end;
    Result.IsSelected:= True;
    TreeView.Selected:= Result;
  end;
end;

function TCustomizeTreeViewItem.FirstGlobal: TTreeViewItem;
var
  FNode: TTreeViewItem;
  Idx: Integer;
begin
  Result:= Nil;
  FNode:= Self;
  if (FNode <> Nil) then
  begin
    FNode.IsSelected:= False;
    Result:= TreeView.ItemByGlobalIndex(0);
    Result.IsSelected:= True;
    TreeView.Selected:= Result;
  end;
end;

function TCustomizeTreeViewItem.Last: TTreeViewItem;
var
  FNode: TTreeViewItem;
  Idx: Integer;
  FCount: Integer;
begin
  Result:= Nil;
  FNode:= Self;
  if (FNode <> Nil) then
  begin
    FNode.IsSelected:= False;

    if ParentItem = Nil then
    begin
      FCount:= TreeView.Count;
      Result:= TreeView.ItemByIndex(FCount-1);
    end
    else begin
      FCount:= ParentItem.Count;
      Result:= ParentItem.ItemByIndex(FCount-1);
    end;
    Result.IsSelected:= True;
    TreeView.Selected:= Result;
  end;
end;

function TCustomizeTreeViewItem.LastGlobal: TTreeViewItem;
var
  FNode: TTreeViewItem;
  Idx: Integer;
  FCount: Integer;
begin
  Result:= Nil;
  FNode:= Self;
  if (FNode <> Nil) then
  begin
    FNode.IsSelected:= False;
    Result:= TreeView.ItemByGlobalIndex(TreeView.GlobalCount-1);
    Result.IsSelected:= True;
    TreeView.Selected:= Result;
  end;
end;

function TCustomizeTreeViewItem.Next: TTreeViewItem;
var
  FNode: TTreeViewItem;
  Idx: Integer;
  FCount: Integer;

begin
  Result:= Nil;
  FNode:= Self;
  if (FNode <> Nil) then
  begin
    Idx:= FNode.Index;

    if ParentItem = Nil then FCount:= TreeView.Count
    else FCount:= ParentItem.Count;

    if Idx < FCount - 1 then
    begin
       FNode.IsSelected:= False;
       Inc(Idx);

       if ParentItem = Nil then Result:= TreeView.ItemByIndex(Idx)
       else Result:= ParentItem.ItemByIndex(Idx);
       Result.IsSelected:= True;
       TreeView.Selected:= Result;
    end;
  end;
end;

function TCustomizeTreeViewItem.NextGlobal: TTreeViewItem;
var
  FNode: TTreeViewItem;
  Idx: Integer;
  FCount: Integer;

begin
  Result:= Nil;
  FNode:= Self;
  if (FNode <> Nil) then
  begin
    Idx:= FNode.GlobalIndex;

    if Idx < TreeView.GlobalCount - 1 then
    begin
       FNode.IsSelected:= False;
       Inc(Idx);
       Result:= TreeView.ItemByGlobalIndex(Idx);
       Result.IsSelected:= True;
       TreeView.Selected:= Result;
    end;
  end;
end;

function TCustomizeTreeViewItem.Prior: TTreeViewItem;
var
  FNode: TTreeViewItem;
  Idx: Integer;
  FCount: Integer;
begin
  Result:= Nil;

  FNode:= Self;
  if FNode <> Nil then
  begin
    Idx:= FNode.Index;

    if Idx > 0 then
    begin
       FNode.IsSelected:= False;
       Dec(Idx);
       if ParentItem = Nil then Result:= TreeView.ItemByIndex(Idx)
       else Result:= ParentItem.ItemByIndex(Idx);
       Result.IsSelected:= True;
       TreeView.Selected:= Result;
    end;
  end;
end;

function TCustomizeTreeViewItem.PriorGlobal: TTreeViewItem;
var
  FNode: TTreeViewItem;
  Idx: Integer;
  FCount: Integer;
begin
  Result:= Nil;

  FNode:= Self;
  if FNode <> Nil then
  begin
    Idx:= FNode.GlobalIndex;

    if Idx > 0 then
    begin
       FNode.IsSelected:= False;
       Dec(Idx);
       Result:= TreeView.ItemByGlobalIndex(Idx);
       Result.IsSelected:= True;
       TreeView.Selected:= Result;
    end;
  end;
end;

end.

También comentar que incialmente el interfaz era mas sencillo pero gracias a los comentarios de mi compañero Juan Antonio  me animó a añadirle algo que simulara el desplazamiento progresivo en los nodos del árbol mientras se mantuviera la pulsación de los botones Next y Prior. Así que, posteriormente, añadí la idea al ejemplo, y es por eso que aparezca un conmutador adicional para activar esta posibilidad (que podéis ver en funcionamiento también desde el video que he adjuntado).

Respecto al interfaz principal, hay algo que quizás pueda ser de interés para quien se inicia: la separación de lo que es el propio interfaz de lo que es el mundo de objetos que va a manipular en respuesta a la acción del usuario. Siempre intentamos que no conozca nuestra clase TTreeView detalles que son propios del interfaz y en sentido contrario, evitamos también que nuestro interfaz conozca detalles concretos de la implementación que hace el árbol.

Veamolos tomando como ejemplo una cualquiera de las respuestas que dan los botones:

procedure TMain.cbtNextClick(Sender: TObject);
var
  FNodo: TTreeViewItem;
begin
  Arbol.SetFocus;
  FNodo:= Arbol.Selected;
  if Assigned(FNodo) then
  begin
     FNodo.Next;
     ActualizaEtiqueta;
  end;
end;

El peso de la acción recae tras varias comprobaciones que aseguran la validez de la referencia, en el método Next de nuestro nodo. Una mala práctica de programación hubiera obviado la creación de los métodos de navegación en la clase TTreeViewItem y se hubiera lanzado a implentar los botones con el código necesario para ejecutar la acción, creando una dependencia absoluta de nuestro interfaz sobre la logica del negocio. Quizás sea en puntos como ese donde mejor se aprecia la filosofia de la metodologia orientada a objetos.

 

Dercargar el código fuente de la entrada: demo3

Espero que esta segunda parte de la serie os haya sido de ayuda.

En la tercera parte la idea es ver como abordamos el tema de la imagen y como añadimos un evento desde nuestra clase helper, que nos ayude en la simulación de estado de nuestra interfaz.

(*) Ver traducción en el primer comentario de la entrada. 

 

7 comentarios sobre “Taller práctico – Arbol TTreeView en Firemonkey (II)

  1. (*)

    Una clase o un tipo record helper es un tipo que -al ser asociado a otra clase u otro tipo record – introduce métodos y propiedades adicionales que pueden ser usadas en el contexto del tipo asociado (o de sus descendientes). Helpers son un camino para extender ima clase sin el uso de la herencia, lo cual es tambien util para el tipo record que no permite el uso de la herencia. Un helper simplemente introduce una ambito mas amplio para que el compilador lo use cuando resuelve los conflictos de indentificadores. Cuando tu declaras una clase helper o un registro, estableces el nombre del helper, y el nombre del tipo que será extendido con el helper. Tu puedes usar el helper en cualquier lugar donde tu puedes legalmente usar la clase extendida o el registro. La resolución de ámbito del compilador convierte el tipo original añadiendo el helper.

    Las clases y registros helper proveen un camino para extender un tipo, pero no se deberia ver como una herramienta de diseño para ser usada cuando se desarrolla nuevo códitgo. Para nuevo código tu deberias siempre confiar en la habitual herencia de clases y la implementación del interface. […]

  2. Hola,

    ocupo ayuda para llenar un treeview, tengo una tabla sql con 4 campos (ID,OFICINA,EJERCICIO,PERIODO) y lo que ocupo hacer es que el treeview se llene con los campos como el ejemplo siguiente:

    -Oficina
    —Ejercicio
    —–Periodo

    y que la momento de dar clic en period me arroje en mensaje la ruta a la que pertenece ese submenu

  3. Hola Fabian:

    Gracias por el comentario.

    Te pido que traslades esta pregunta al grupo de facebook Delphi Solidario que es mas apropiado para resolver dudas como la que me comentas y además va a permitir que mas compañeros te puedan aconsejar.

    No obstante, te anticipo que cada nodo de un TTreeView contiene un puntero a su nodo padre, la propiedad Parent, de forma que puedes recorrer el camino hacia la raiz de forma ascendente, evaluando el valor de misma, valiendote de cualquier bucle (while, repeat) que se permita ascender en la estructura.

    No estaría de mas si pudieras dar algunos detalles mas sobre la duda especifica porque en el ejemplo de la entrada ya puedes ver como se llena un arbol (en la primera entrada ya se ve como se crea un item del arbol). Los distintos niveles solo van a depender de que el parent sea el propio arbol u otro nodo.

    Un saludo,

    Salvador

  4. Buen artículo, Salvador.
    Un comentario y una propuesta.
    El comentario: La clase TTreeView de Firemonkey también anda necesitada de un Helper. Aquí te dejo la primera versión del mismo.
    ……….
    type
    TTreeViewHelper = class helper for TTreeView
    public
    function IsEmpty :Boolean;
    end;

    implementation

    { TTreeViewHelper }

    function TTreeViewHelper.IsEmpty :Boolean;
    begin
    Result := Count = 0;
    end;

    end.
    …….

    La propuesta:
    Tengo una aplicación en la que es muy importante el orden que se encuentran los items dentro del TreeView. Por lo que mi propuesta (si quieres lo desarrollamos juntos) es añadir un par de métodos al Helper del TTreeViewItem.

    Podrían ser function PushToUp :Boolean;
    y por supuesto function PustToDown:Boolean;

    Moverían el item seleccionado hacia arriba o hacia abajo y devolverían True si se movieron. Serían False en caso de que estando en la posición superior llamásemos a PushToUp. Todo lo contrario con PushToDown.

    ¿Para cuando una serie similar sobre los TListBox de Firemonkey?

    Saludos.

    1. Hola Juan Carlos,

      gracias por el comentario.

      Llevo unos días preparando un complemento al articulo que escribió mi amigo German (Neftali) sobre TTethering. Va a ser dificil complementar mas el tema ya que fue muy bueno su articulo 😉 ¡Fue genial! pero la idea es compartir como siempre el aprendizaje e intentar que seas divertidos. Cuando lo tenga listo y pueda pasar a otro tema, si te parece y vemos el comentario que has dejado en esta entrada.

      Un saludo,
      Salvador

  5. ¡Hola Sr. Salvador!

    Soy algo nuevo en todo esto y específicamente este taller ha sido excelente, he aprendido mucho, solo que en el caso del número 2, ya la descarga no está disponible y bueno, quería saber si existe la posibilidad de que reparen el link, porque me interesa muchísimo poder ver las fuentes para comprender mejor lo relacionado con TreeView en Firemonkey.

    De antemano, mi más sincero agradecimiento por tan excelentes aportes.

    Atte,

    Luis Torres

    1. Hola Luis:

      No consigo ver cual es el enlace roto. He probado el que aparece en esta entrada y he descargado el fichero asociado al link que indica demo3. No se exactamente a cual te refieres. Ten en cuenta, que en la primera parte de la entrada, aparecen dentro del código fuente 2 proyectos, que también son descargables. Por eso, imagino que puse el rotulo demo3.
      Saludos.

      Me alegro mucho de que te haya servido de algo. Recuerda que puedes participar en Facebook en el grupo delphisolidario, donde puedes encontrar muchos enlaces de interés.

      https://www.facebook.com/groups/delphisolidario

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s