Mostrando entradas con la etiqueta Items. Mostrar todas las entradas
Mostrando entradas con la etiqueta Items. Mostrar todas las entradas

lunes, 9 de diciembre de 2019

TListView: evitar ítems duplicados.

El componente TListView carece de una propiedad para impedir ítems duplicados, seguramente porque a su vez, los ítems son una clase que además posee subítems.
Una forma para evitar esta situación, sería, antes de agregar el ítem, recorrer la lista y comparar el texto (Caption) del ítem con los ya existentes, una función con un ciclo for to alcanza y es una manera correcta, pero si la lista tiene 15.000 ítems ¿que pasa?, para empezar, una lista del tipo TListView no debería contener tal cantidad y para terminar, también.
Pero TListView tiene un método (función) que hereda de TCustomListView: FindCaption.

{------------------------------------------------------------------------------}
{   TListItems FindCaption                                                     }
{------------------------------------------------------------------------------}
function TListItems.FindCaption(StartIndex: Integer; Value: string;
  Partial, Inclusive, Wrap: Boolean; PartStart: Boolean): TListItem;
var
  I: Integer;
  CaptionFound, AllChecked: Boolean;
begin
  result := nil;
  if (Count = 0) or (StartIndex >= Count) or (not Inclusive and (count = 1)) then Exit;
  CaptionFound := False;
  AllChecked := False;
  if Inclusive then
    I := StartIndex
  else begin
    I := succ(StartIndex);
    if I >= Count then I := 0;
  end;
  if Wrap then Wrap := (StartIndex <> 0);
  repeat
    if Partial then begin
      if PartStart then
        CaptionFound := pos(Value, Item[I].Caption) = 1
      else
        CaptionFound := pos(Value, Item[I].Caption) <> 0;
    end else
      CaptionFound := Value = Item[I].Caption;
    if not CaptionFound then begin
      Inc(I);
      if Wrap then begin
        if I = Count then
          I := 0
        else
          if I = StartIndex then
            AllChecked := True;
      end else begin
        if I = Count then AllChecked := True;
      end;
    end;
  until CaptionFound or AllChecked;
  if CaptionFound then result := Item[I];
end; 

Las ventajas son las opciones y que ya está hecha.

¿Cómo utilizarla?

if Assigned(ListView1.FindCaption(0,'eltexto',False,True,False,True)) then Exit;

Este código debe ejecutarse antes de agregar un ítem, si el ítem ya existe, entonces FindCaption devuelve un ítem ("completo"), caso contrario, devuelve nil.

Para probarlo basta con este ejemplo:

unit principal;

{$mode objfpc}{$H+}

interface

uses
  Classes, SysUtils, FileUtil, Forms, Controls, Graphics, Dialogs, ComCtrls,
  ExtCtrls, ShellCtrls, StdCtrls, Buttons;

type

  { TForm1 }

  TForm1 = class(TForm)
    BAgregar: TBitBtn;
    lPath: TLabel;
    Lista: TListView;
    Panel1: TPanel;
    arbol: TShellTreeView;
    Splitter1: TSplitter;
    procedure arbolClick(Sender: TObject);
    procedure BAgregarClick(Sender: TObject);
    procedure FormCreate(Sender: TObject);
  private

  public

  end;

var
  Form1: TForm1;

implementation

{$R *.lfm}

{ TForm1 }

procedure TForm1.FormCreate(Sender: TObject);
begin
  lPath.Caption:=arbol.Path;
end;

procedure TForm1.BAgregarClick(Sender: TObject);
var
  li:TListItem;
begin
  if not Assigned(arbol.Selected) then Exit;
  if Assigned(Lista.FindCaption(0,arbol.Path,False,True,False,True)) then Exit;
  li:=Lista.Items.Add;
  li.Caption:=arbol.Path;
end;

procedure TForm1.arbolClick(Sender: TObject);
begin
  if not Assigned(arbol.Selected) then Exit;
  lPath.Caption:=arbol.Path;
end;

end. 

Este programa agrega los nombres de las carpetas mostradas en un TShellTreeView a un TListView nombrada "lista" cada vez que se presiona el botón agregar, evitando duplicados.

domingo, 8 de diciembre de 2019

TlistView con columnas e imágenes.

TListView puede contener varias columnas, las mismas se puede definir cómodamente desde el inspector de objetos "Columns" donde solo se especifica el nombre. Luego hay que valerse de items y subitems para cargar los datos en las respectivas columnas. Todo es texto (strings) tanto el nombre de las columnas, como el items y los subitems. Para agregar un ícono usamos una lista de imágenes TImageList y la asociamos a SmallImages en TListView. También el estilo (ViewStyle) de la lista debe ser vsReport.


Agregar columnas, como vemos, es simple.

En este ejemplo, la lista debe verse así, en la primer columna en nombre del archivo, la segunda el tamaño y la tercera la fecha y hora.

Hay que ajustar un poco la lista (TListView) desde el inspector de objetos:
TListView
AutoSort:=True
ColumnClick:=True
Columns:=3 items
ReadOnly:=True
ShowColumnHeader:=True
ScrollBars:=ssAutoBoth
SmallImages:=ImageList1
SortColumn:=0
ViewStyle:=vsReport

Y también la lista de imágenes (TImageList):
TImageList
Height:=24
Width:=24

Los imágenes para la lista:

La estructura de la lista:


Si trabajamos con columnas necesitamos una lista de ítems (TListItem) con la cual agregaremos los subítems:

Por ejemplo:

var
  li:TListItem;
begin
  li:=lista.Items.Add; //Crea el ítem y lo agrega a Lista (TListView)
  li.Caption:='archivo.pas';
  li.SubItems.Add('2772');
  li.SubItems.Add('20-10-2018 15:30:58');

Como vemos no necesitamos instanciar la clase TListItem porque lo hace la función Add, su resultado es precisamente la instanciación de la variable:

function TListItems.Add: TListItem;
begin
  if Assigned(Owner) then
    Result := Owner.CreateListItem
  else
    Result := TListItem.Create(Self);
  AddItem(Result);
end;  

Siempre es bueno revisar el código fuente.

El código para llenar la lista es el siguiente:

procedure TForm1.MostrarArchivos;
var
  Arch:TSearchRec;
  li:TListItem;
  den:String='';
begin
  lista.Clear;
  opciones:=GetOpciones;
  case opciones of
    0 : if FindFirst(arbol.Path+'*',faHidden or faDirectory,Arch)<>0 then Exit;  //Si encontró devuelve 0 cero.
    1 : if FindFirst(arbol.Path+'*',faDirectory,Arch)<>0 then Exit;
    2 : if FindFirst(arbol.Path+'*',not faHidden and not faDirectory,Arch)<>0 then Exit;
    3 : if FindFirst(arbol.Path+'*',faHidden and not faDirectory,Arch)<>0 then Exit;
  end;
  repeat
    if ((Arch.Name<>'.') and (Arch.Name<>'..')) then  //Evita los directorios ocultos en Linux
    begin
      li:=lista.Items.Add;
      li.Caption:=Arch.Name;
      if (Arch.Attr and  faDirectory)<>0 then
        begin
          li.ImageIndex:=0;
          li.SubItems.Add('--');
        end
      else
        begin
          li.ImageIndex:=1;
          li.SubItems.Add(FormatFloat('0.00',Tamanio(den,Arch.Size))+' '+den);
        end;
      li.SubItems.Add(DateTimeToStr(FileDateToDateTime(Arch.Time)));
    end;
  until FindNext(Arch)<>0;
  FindClose(Arch);
  lista.Sort;
end; 

La primer columna corresponde al texto del item, en este caso, li.Caption, las restantes son subitems y el texto se asigna mediante el métodos Add.
En cuanto a la imágenes que utilizamos como iconos las cargamos en li.ImageIndex siendo 0 para carpetas y 1 para archivos.

Para obtener los datos de una lista del tipo TListView también lo hacemos mediante la clase TListItem, por ejemplo:

li:=Lista.Items[i];
ShowMessage(li.Caption);  

Descargar código fuente : TListView_Columns.7z

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.