domingo, 15 de septiembre de 2019

TTreeView básico, con ítems.

La aclaración "con ítems" en el título significa, sin punteros, eso queda para la próxima, nos limitaremos a dos elementos por nodo: texto e imagen. Si bien en sí casi carece de utilidad, es por donde se debe empezar, luego vendrá lo útil, con punteros a objetos. Veremos como agregar nodos raíz, hijos; eliminar, modificar, selección simple y múltiple, ordenar el árbol, ajustar la identación, guardar a un archivo de texto, cargar desde un archivo de texto, averiguar los niveles de las hojas, buscar, expandir y contraer todo el árbol.

En un formulario, colocar un TTreeView, 12 botones, un TImageList y un TTrackBar.


Vamos a empezar con un árbol vacío para agregar, modificar y borrar hojas.

Nuevo:

procedure TForm1.BNuevoClick(Sender: TObject);
var
  nombre:String;
begin
  nombre:=InputBox('Crear un nodo','Nombre: ','');
  if nombre<>'' then
  begin
    if TreeView1.Selected<>nil then
      begin
        TreeView1.Items.AddChild(TreeView1.Selected, nombre);
        TreeView1.Selected.Expanded:=True;
      end
    else
      TreeView1.Items.Add(nil, nombre);
  end;
  TreeView1.Selected:=nil; //opcional, para que no quede el nodo seleccionado.
end;


Siempre hay que saber si hay un nodo seleccionado, para eso if TreeView1.Selected<>nil
Si hay un nodo seleccionado, entonces se agrega un nodo hijo con el evento Items.AddChild y como primer parámetro se pasa el nodo seleccionado TreeView1.Selected y el texto. Luego, opcional pero recomendable, expandir el nodo para que se vea el nuevo nodo TreeView1.Selected.Expanded:=True;
Si no hay un nodo seleccionado, entonces el nuevo nodo será raíz y usamos el evento Items.Add y como primer parámetro nil para indicar que se trata de una raíz. Desde ya si queremos que el árbol pueda tener varias raíces, hay casos en que el árbol solo tiene una raíz.
Y la última sentencia TreeView1.Selected:=nil es justamente para eso, para permitir al usuario agregar más nodos raíces, caso contrario siempre hay activa una selección y siempre se crearían nodos hijos. Otra opción sería un botón para des-seleccionar.

Modificar:

procedure TForm1.BModificarClick(Sender: TObject);
var
  nombre:String;
begin
  if TreeView1.Selected<>nil then
  begin
    nombre:=InputBox('Modificar un nodo','Nombre: ',TreeView1.Selected.Text);
    if nombre<>'' then TreeView1.Selected.Text:=nombre;
  end;
  TreeView1.Selected:=nil;
//opcional, para que no quede el nodo seleccionado.
end;

Eliminar:

procedure TForm1.BEliminarClick(Sender: TObject);
begin
  if TreeView1.Selected<>nil then TreeView1.Selected.Delete;
end;


Hasta ahora siempre trabajamos sobre un ítem seleccionado.

Borrar todo el árbol:

procedure TForm1.BBorrarArbolClick(Sender: TObject);
begin
  TreeView1.Items.Clear;
end;


Niveles:

procedure TForm1.BNivelesClick(Sender: TObject);
var
  i:Integer;
begin
  for i:=0 to TreeView1.Items.Count-1 do
    TreeView1.Items[i].Text:=TreeView1.Items[i].Text+
    'Niv.'+IntToStr(TreeView1.Items[i].Level);
end;


La propiedad items es del tipo TTreeNode y es la colección de ítems del  árbol. Este procedimiento recorre todos los ítems, averigua el nivel del ítem con con la propiedad Level (entero) y lo agrega al texto del nodo. El nodo raíz es de nivel 0 (cero), un nodo hijo es de nivel 1, etc.

Ordernar:

procedure TForm1.BOrdenarClick(Sender: TObject);
begin
  TreeView1.AlphaSort;
end;


Esta es una opción, el método AlphaSort. En el inspector de objetos podemos encontrar la opción SortType. Volviendo a AlphaSort, hay una diferencia con Delphi donde este método tiene un parámetro del tipo boolean, AlphaSort(True) en FreePascal nos devolverá un error.

El evento OnCompare:

procedure TForm1.TreeView1Compare(Sender: TObject; Node1, Node2: TTreeNode; var Compare: Integer);
begin
  Compare:=CompareText(Node1.Text,Node2.Text);
end;


Si este evento está definido y se llama a AlphaSort, AlphaSort se ignora.
Compare:=CompareText(Node1.Text,Node2.Text); donde compare es una variable pasada por referencia en OnCompare:
Compare será menor que 0 si Node1 es menor que Node2, Compare es 0 si Node1 es equivalente a Node2 y Compare será mayor que 0 si Node1 es mayor que Node2. Si no se usa OnCompare, los nodos de vista de árbol se ordenan alfabéticamente.

Elementos seleccionados:

procedure TForm1.BSeleccionadosClick(Sender: TObject);
var
  i:Integer;
  s:TStringList;
begin
  s:=TStringList.Create;
  for i:=0 to TreeView1.Items.Count-1 do
    if TreeView1.Items[i].Selected then s.Add(TreeView1.Items[i].Text);
  ShowMessage(s.Text);
  FreeAndNil(s);
end;


Como en varias de las operaciones con árboles, deben recorrerse todos sus elementos, en este caso, debemos averiguar si la propiedad Selected es True.

Cargar imágenes a los nodos:


En este caso cargué dos imágenes de 32x32 pixeles en ImageList1.
Hay que asignar ImageList1 a la propiedad Images de TreeView1, desde el IO (Inspector de Objetos) o por código.

procedure TForm1.BCargarIconosClick(Sender: TObject);
var
  i:Integer;
begin
  for i:=0 to TreeView1.Items.Count-1 do
    if TreeView1.Items[i].Level=0 then
      begin
        TreeView1.Items[i].ImageIndex:=0;
        TreeView1.Items[i].SelectedIndex:=0;
      end
    else
      begin
        TreeView1.Items[i].ImageIndex:=1;
        TreeView1.Items[i].SelectedIndex:=1;
      end;
end;


A modo de ejemplo se le asigna la imagen 0 a los nodos raíz (level=0) y la imagen 1 a las hojas de nivel 1 con TreeView1.Items[i].ImageIndex:=0 y agregamos también TreeView1.Items[i].SelectedIndex:=0 porque caso contrario al seleccionar el nodo se borra la imagen, al menos usando el widget GTK. Es decir, para evitar que desaparezca la imagen al seleccionar un ítem o nodo se debe asignar la misma imagen a la propiedad SelectedIndex.

Guardar en archivo de texto:

procedure TForm1.BguardarClick(Sender: TObject);
begin
  TreeView1.SaveToFile('arbol1.txt');
end;


Leer desde archivo de texto:

procedure TForm1.BCargarClick(Sender: TObject);
begin
  TreeView1.LoadFromFile('arbol1.txt');
end;



Como vemos es un elemento por línea y los niveles se establecen mediante tabulación.

Expandir y contraer (todo el árbol):

TreeView1.FullExpand y TreeView1.FullCollapse.

Espacio horizontal entre nodos:

procedure TForm1.TrackBar1Change(Sender: TObject);
begin
  TreeView1.Indent:=TrackBar1.Position;
end;


En este ejemplo se utiliza un TTrackBar, la posición se asigna a la propiedad Indent del árbol.

Personalizar:

procedure TForm1.TreeView1CustomDrawItem(Sender: TCustomTreeView; Node: TTreeNode; State: TCustomDrawState; var DefaultDraw: Boolean);
begin
  if Node.Level=1 then
    begin
      Sender.Canvas.Font.Color:=clBlue; //tvoThemeDraw:=False
      Sender.Canvas.Font.Style:=[fsBold];
    end
  else
    begin
      Sender.Canvas.Font.Color:=clBlack;
      Sender.Canvas.Font.Style:=[];
    end;
  if cdsFocused in State then Sender.Canvas.Font.Color:=clWhite;
end;


Mediante el evento OnCustomDraw pondremos en color azul y negrita los nodos del nivel 1 y el resto color negro sin estilo. A su vez, si el nodo está seleccionado el color de la fuente será blanco. Para que esto funcione se deberá establecer la propiedad del TTreeView.Options.tvoThemeDraw en False, ya sea mediante el IO o por código.

Luego de haber practicado como agregar, modificar y borrar un nodo, se recomienda agregar elementos al árbol.

Click derecho sobre el árbol y Editar elementos para cagar algunos datos.


Notas acerca de TTreeView:

Acepta 2 listas de imágenes, de una obtiene el estado (StateImages), ideal para check box, y la otra el icono (Images).
Para crear un nodo raíz: TreeView1.Items.Add(nil, 'texto') el nombre/texto es lo básico, sino hay que usar .Data que permite apuntar a un objeto (Clase, Registro).
Los Items del TTreeView son de la clase TTreeNode.
State: TCustomDrawState enumerados, state es un conjunto.
type TCustomDrawState = set of (
  cdsSelected,
  cdsGrayed,
  cdsDisabled,
  cdsChecked,
  cdsFocused,
  cdsDefault,
  cdsHot,
  cdsMarked,
  cdsIndeterminate
);


Código fuente del proyecto: TTreeView1.7z

Actualización: Buscar por texto:

Antes de comenzar con la actualización, quiero destacar un opinión respecto de la pobre documentación de Lazarus y Free Pascal. No es posible que tenga que estar días y días buscando documentación detalla de TTreeView, el ejemplo que viene con Lazarus es la cuarta parte de lo que dice esta entrada, y esta entrada en sí es limitadísima, basta con ver todas las propiedades y eventos que tiene la clase TTreeView como TCustomTreeView de la que deriva. ¿A quién le reclamo esto? A nadie, simplemente lo destaco, así como destaco mil cosas buenas de Lazarus y Free Pascal, destaco esta mala, de paso practico un poco de imparcialidad y me descargo. Volvamos al tema de la búsqueda.

Colocar un botón "Buscar" y un TEdit donde el usuario ingresará el texto y dicho texto es el que buscaremos que coincida con algún nodo o ítem, en caso de hallarlo, lo haremos visible y lo seleccionaremos.

procedure TForm1.BBuscarClick(Sender: TObject);
var
  n:TTreeNode;
begin
  n:=tv.Items.FindNodeWithText(edBuscar.Text);
  if not Assigned(n) then exit;
  n.MakeVisible;
  n.Selected:=True;
end;


A TreeView1 le cambié el nombre por tv por comodidad.
Usamos la función FindNodeWithText que pertence a ítems y le pasamos como parámetro el texto a buscar. Pero como dicha función devuelve un nodo, debemos tener a mano una variable del tipo TTreeNode para asignarle el resultado.
Para evitar un error SIGSEGV es que preguntamos if not Assigned(n) para el caso de que no se halla encontrado un nodo con el texto buscado. Finalmente con las dos últimas sentencias mostramos y seleccionamos el nodo.

TTreeView con nodos con punteros a registros.

No hay comentarios:

Publicar un comentario