viernes, 20 de diciembre de 2019

El temporizador TIdleTimer

Según la documentación oficial es "Un temporizador para medir el tiempo de inactividad entre procesos." y hereda la mayoría de sus propiedades de la clase TCustomTimer, aunque la clase base es TCustomIdleTimer.

Es como un TTimer pero con más opciones.

Se encuentra en la paleta Systems de Lazarus.

En el siguiente ejemplo haremos que el formulario se oculte por 10 segundos cuando el usuario hace click en el botón, luego de trascurrido dicho lapso, el formulario se muestra nuevamente.

procedure TForm1.FormCreate(Sender: TObject);
begin
  IdleTimer1.Enabled:=False;
  IdleTimer1.Interval:=10000;
end;

Esto también se puede hacer directamente en el inspector de objetos.

procedure TForm1.BitBtn1Click(Sender: TObject);
begin
  IdleTimer1.Enabled:=True;
  Hide;
end;      

Activamos el temporizador y ocultamos el formulario.

Desde el inspector de objetos, en eventos de IdleTimer1 definimos el evento OnTimer que es cuando el tiempo estipulado ya transcurrió.

procedure TForm1.IdleTimer1Timer(Sender: TObject);
begin
  IdleTimer1.Enabled:=False;
  Show;
end;         

Es importante desactivar el temporizador, caso contrario seguirá ejecutándose.



El código completo:

unit Unit1;

{$mode objfpc}{$H+}

interface

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

type

  { TForm1 }

  TForm1 = class(TForm)
    BitBtn1: TBitBtn;
    IdleTimer1: TIdleTimer;
    procedure BitBtn1Click(Sender: TObject);
    procedure FormCreate(Sender: TObject);
    procedure IdleTimer1Timer(Sender: TObject);
  private

  public

  end;

var
  Form1: TForm1;

implementation

{$R *.lfm}

{ TForm1 }

procedure TForm1.FormCreate(Sender: TObject);
begin
  IdleTimer1.Enabled:=False;
  IdleTimer1.Interval:=10000;
end;

procedure TForm1.BitBtn1Click(Sender: TObject);
begin
  IdleTimer1.Enabled:=True;
  Hide;
end;

procedure TForm1.IdleTimer1Timer(Sender: TObject);
begin
  IdleTimer1.Enabled:=False;
  Show;
end;

end.    

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, 1 de diciembre de 2019

Listar archivos de un directorio: FindFirst

FindFirst, FindNext y FindClose: se utilizan para obtener el contenido de un path, carpeta o directorio (contenido = archivos y directorios).

La primera función, FindFirst, inicia la búsqueda, la segunda función, FindNext la continúa y finalmente el procedimiento FindClose, la finaliza.



function FindFirst(

  const Path: RawByteString;

  Attr: LongInt;

  out Rslt: TRawbyteSearchRec

):LongInt;

El tipo RawByteString es un string que no tiene asociado ningún código de página.

Attr: es un campo de bit y se maneja con operaciones bit a bit, "Bitwise operation", no obstante se puede utilizar perfectamente sin tener conocimientos al respecto pero sabiendo al menos de que se trata. Para ello debemos hacer uso de las constantes que comienzan por "fa" por "File attributes".

Const
  { File attributes }
  faReadOnly   = $00000001;
  faHidden     = $00000002 platform;
  faSysFile    = $00000004 platform;
  faVolumeId   = $00000008 platform deprecated;
  faDirectory  = $00000010;
  faArchive    = $00000020;
  faNormal     = $00000080;
  faTemporary  = $00000100 platform;
  faSymLink    = $00000400 platform;
  faCompressed = $00000800 platform;
  faEncrypted  = $00004000 platform;
  faVirtual    = $00010000 platform;
  faAnyFile    = $000001FF; 

El parámetro tipo out: es muy parecido a usar var, también es pasado por referencia, la diferencia es que el valor inicial (si es que lo tiene) se descarta. Si no está inicializado el valor, el compilador tampoco enviará una advertencia.

TRawbyteSearchRec es un registro:

type TRawbyteSearchRec = record
  Time: LongInt; // Last modification timestamp
  Size: Int64; // File size in bytes
  Attr: LongInt; // File attributes
  Name: RawByteString; // File name (single byte version)
  ExcludeAttr: LongInt; // For internal use only
  FindHandle: Pointer; // Native file search handle. For internal use only, treat as opaque
  Mode: TMode; // File permissions (mode, Unix only)
end;

El resultado de la función: 0 (cero) si encuentra un archivo o un valor distinto a cero en caso contrario.

function FindNext(

  var Rslt: TRawbyteSearchRec

):LongInt;

Como la búsqueda ya fue iniciada por FindFirst, no necesita ni el path ni attr y como vemos, el resultado en lugar de out es var. Generalmente se la utiliza como condicional para un ciclo repeat until:

until FindNext(Arch)<>0;  

Devuelve 0 (cero) en caso de haber encontrado una coincidencia y un valor distinto de cero en caso contrario, igual que FindFirst.

procedure FindClose(

  var F: TRawbyteSearchRec

);

Al igual que las funciones FindFirst y FindNext, el procedimiento FindClose recibe por referencia el registro TRawbyteSearchRec y finaliza la serie de llamadas FindFirst/FindNext. Siempre debe utilizarse para evitar fugas de memoria.

Como ejemplo tendremos un TShellTreeView cuyo evento OnClick mostrará los archivos de la carpeta en un TMemo. Además un par de TCheckBox para mostrar u ocultar en el listado (TMemo) los archivos ocultos y/o subdirectorios o subcarpetas. Unos TLabel para información de la cantidad de archivos mostrados y su tamaño.

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

La función Assigned:

function Assigned(

  P: Pointer

):Boolean;

Comprueba si el valor del puntero no es nil y devuelve TRUE, caso contrario devuelve FALSE. Sería lo mismo que:
if arbol.Selected=nil then Exit;

procedure TForm1.MostrarArchivos;
var
  Arch:TSearchRec;
  den:String='';
  linea:String='';
begin
  TotArchivos:=0;
  TotCarpetas:=0;
  TotTamanio:=0;
  Memo1.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
    begin
      linea:=Format('%0:-40s',[' '+Arch.Name]);
      if (Arch.Attr and  faDirectory)<>0 then
        begin
          linea:=linea+Format('%0:12s',['[Dir]']);
          Inc(TotCarpetas);
        end
      else
        begin
          linea:=linea+Format('%8.2n',[Tamanio(den,Arch.Size)])+Format('%0:4s',[den]);
          Inc(TotArchivos);
          TotTamanio:=TotTamanio+Arch.Size;
        end;
      linea:=linea+Format('%0:20s',[DateTimeToStr(FileDateToDateTime(Arch.Time))]);
      Memo1.Lines.Add(linea);
    end;
    lTotArch.Caption:=IntToStr(TotArchivos);
    lTam.Caption:=FormatFloat('0.00',(Tamanio(den,TotTamanio)))+' '+den;
    lTotSubDir.Caption:=IntToStr(TotCarpetas);
  until FindNext(Arch)<>0;
  FindClose(Arch);
end; 

function TForm1.GetOpciones: Byte;
begin
  if (cbDirectorios.Checked and cbOcultos.Checked) then Result:=0;
  if (cbDirectorios.Checked and not(cbOcultos.Checked)) then Result:=1;
  if ((not(cbDirectorios.Checked)) and (not(cbOcultos.Checked))) then Result:=2;
  if ((not(cbDirectorios.Checked)) and cbOcultos.Checked) then Result:=3;
end;    

Lo primero importante que hace el procedimiento MostrarArchivos es llamar a la función GetOpciones para saber la selección del usuario en cuanto a qué mostrar. Con el resultado obtenido se continúa con el CASE OF y la primera (y única) llamada a FindFirst, esto es importante, si debemos utilizar nuevamente FindFirst, antes debemos utilizar FindClose.

FindFirst(arbol.Path+'*',faHidden or faDirectory,Arch)<>0 then Exit;

He visto varios ejemplos así: If FindFirst('*',faAnyFile and faDirectory,... etc.
faAnyFile no es necesario.
Otra aclaración: es común pensar que FindFirst se fija en que se cumpla lo especificado en attr para devolver un resultado, pues esto no es tan así, attr son valores adicionales, significa que el resultado será un archivo normal o que tenga los atributos especificados en attr. Por eso, al llamar a FindFirst(arbol.Path+'*',faHidden or faDirectory,Arch) si encuentra un archivo normal, devolverá 0 (cero) y el registro Arch con la información del mismo.

repeat
  ...
until FindNext(Arch)<>0;
FindClose(Arch);

Al primer ciclo de repeat until entramos con el registro que completó FindFirst, en las siguientes iteraciones lo hacemos con el registro que devolvió FindNext y finalmente cerramos la búsqueda. Todo esto se ejecuta cada vez que se cambia de directorio en el árbol (que solo muestra directorios).

Todo el código:

unit principal;

{$mode objfpc}{$H+}

interface

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

type

  { TForm1 }

  TForm1 = class(TForm)
    BSalir: TBitBtn;
    cbOcultos: TCheckBox;
    cbDirectorios: TCheckBox;
    Label1: TLabel;
    Label2: TLabel;
    lTotArch: TLabel;
    Label4: TLabel;
    lTotSubDir: TLabel;
    lTam: TLabel;
    arbol: TShellTreeView;
    Memo1: TMemo;
    Panel1: TPanel;
    Panel2: TPanel;
    Splitter1: TSplitter;
    procedure arbolClick(Sender: TObject);
    procedure BSalirClick(Sender: TObject);
    procedure cbDirectoriosChange(Sender: TObject);
    procedure cbOcultosChange(Sender: TObject);
    procedure FormClose(Sender: TObject; var CloseAction: TCloseAction);
    procedure FormCreate(Sender: TObject);
  private
    procedure SetPath;
    procedure MostrarArchivos;
    {//0 Dir y Oc - 1 Dir - 2 No Dir y No Oc - 3 No Dir y Oc}
    function GetOpciones:Byte;
    function Tamanio(var sDenominacion: String; nByte: Int64):Single;
  public

  end;


var
  Form1: TForm1;
  APath:String;
  ARoot:String;
  ind:Integer;
  opciones:Byte;
  TotArchivos:Integer=0;
  TotCarpetas:Integer=0;
  TotTamanio:Int64=0;

implementation

{$R *.lfm}

{ TForm1 }

procedure TForm1.FormCreate(Sender: TObject);
begin
  SetPath;
  arbol.Path:=APath;   //le agrega un pathdelimiter al final de aPath
  MostrarArchivos;
  Label1.Caption:=arbol.Path;
end;

procedure TForm1.MostrarArchivos;
var
  Arch:TSearchRec;
  den:String='';
  linea:String='';
begin
  TotArchivos:=0;
  TotCarpetas:=0;
  TotTamanio:=0;
  Memo1.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
    begin
      linea:=Format('%0:-40s',[' '+Arch.Name]);
      if (Arch.Attr and  faDirectory)<>0 then
        begin
          linea:=linea+Format('%0:12s',['[Dir]']);
          Inc(TotCarpetas);
        end
      else
        begin
          linea:=linea+Format('%8.2n',[Tamanio(den,Arch.Size)])+Format('%0:4s',[den]);
          Inc(TotArchivos);
          TotTamanio:=TotTamanio+Arch.Size;
        end;
      linea:=linea+Format('%0:20s',[DateTimeToStr(FileDateToDateTime(Arch.Time))]);
      Memo1.Lines.Add(linea);
    end;
    lTotArch.Caption:=IntToStr(TotArchivos);
    lTam.Caption:=FormatFloat('0.00',(Tamanio(den,TotTamanio)))+' '+den;
    lTotSubDir.Caption:=IntToStr(TotCarpetas);
  until FindNext(Arch)<>0;
  FindClose(Arch);
end;

function TForm1.GetOpciones: Byte;
begin
  if (cbDirectorios.Checked and cbOcultos.Checked) then Result:=0;
  if (cbDirectorios.Checked and not(cbOcultos.Checked)) then Result:=1;
  if ((not(cbDirectorios.Checked)) and (not(cbOcultos.Checked))) then Result:=2;
  if ((not(cbDirectorios.Checked)) and cbOcultos.Checked) then Result:=3;
end;

function TForm1.Tamanio(var sDenominacion: String; nByte: Int64): Single;
CONST
  KBytes=1024;
  MBytes=1024*1024;
  GBytes=1024*1024*1024;
  TBytes=1024*1024*1024*1024;
  PBytes=1024*1024*1024*1024*1024;
begin
  case nByte of
    0..KBytes-1 : begin
                    sDenominacion:='B';
                    Result:=nByte;
                  end;
    KBytes..MBytes-1 : begin
                         sDenominacion:='KB';
                         Result:=nByte/KBytes;
                       end;
    MBytes..GBytes-1 : begin
                         sDenominacion:='MB';
                         Result:=nByte/MBytes;
                       end;
    GBytes..TBytes-1 : begin
                         sDenominacion:='GB';
                         Result:=nByte/GBytes;
                       end;
    TBytes..PBytes-1 : begin
                         sDenominacion:='TB';
                         Result:=nByte/TBytes;
                       end;
  else
    begin
      sDenominacion:='??';
      Result:=1;
    end;
  end;
end;


procedure TForm1.SetPath;
begin
  {$IFDEF WINDOWS}
  APath:=ExtractFilePath(Application.Location);
  {$ELSE}
  APath:=GetEnvironmentVariable('HOME');
  ARoot:=APath;
  //arbol.Root:=ARoot; Si queremos que la raíz del árbol sea la home del usuario.
  {$ENDIF}
end;

procedure TForm1.BSalirClick(Sender: TObject);
begin
  Close;
end;

procedure TForm1.cbDirectoriosChange(Sender: TObject);
begin
  MostrarArchivos;
end;

procedure TForm1.cbOcultosChange(Sender: TObject);
begin
  MostrarArchivos;
end;

procedure TForm1.FormClose(Sender: TObject; var CloseAction: TCloseAction);
begin
  CloseAction:=caFree;
end;

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

end.

Descargar todo el proyecto: Mostrar archivos con FindFirst.7z

viernes, 8 de noviembre de 2019

Obtener los archivos de una carpeta con GetFilesInDir

GetFilesInDir es un procedimiento de clase que la clase TShellTreeView hereda de la clase TCustomShellTreeView.
TShellTreeView es un componente que se encuentra en la paleta Misc de Lazarus y que ha quedado estancado por el tema de la compatibilidad con versiones anteriores o retrocompatibilidad. No obstante es muy útil y lo veremos más en profundidad en otra entrada.
Vale esta aclaración, porque el procedimiento en cuestión, tiene una fuga de memoria que no se corrige ni se corregirá, según lo leído en un hilo del foro oficial de Lazarus y Free Pascal, justamente para mantener la compatibilidad, es decir, para solucionarlo habría que hacer cambios que afectarían la compatibilidad con versiones anteriores, es decir, con programas viejos o no tanto, que necesitan compilarse con la versión más reciente de FPC.
Lo bueno es que con una línea de código se soluciona el problemita del memory leaks.

Otra forma de obtener los archivos de un directorio es mediante FindFirst, FindNext y FindClose.

Este sencillo ejemplo consta de tres componentes, un TShellTreeView, un TMemo y un TBitBtn.
unit Unit1;

{$mode objfpc}{$H+}

interface

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

type

{ TForm1 }

TForm1 = class(TForm)
  BitBtn1: TBitBtn;
  Memo1: TMemo;
  ShellTreeView1: TShellTreeView;
  procedure BitBtn1Click(Sender: TObject);
private

public

end;

var
Form1: TForm1;

implementation

{$R *.lfm}

{ TForm1 }

procedure TForm1.BitBtn1Click(Sender: TObject);
var
  sl:TStringList;
begin
  Memo1.Clear;
  sl:=TStringList.Create;
  sl.OwnsObjects:=True; //evita memory leaks
  ShellTreeView1.GetFilesInDir(ShellTreeView1.Path,'*.*',[otNonFolders],sl,fstAlphabet);
  Memo1.Text:=sl.Text;
  FreeAndNil(sl);
end;

end.


Veamos el código fuente del procedimiento:

class procedure TCustomShellTreeView.GetFilesInDir(const ABaseDir: string;
AMask: string; AObjectTypes: TObjectTypes; AResult: TStrings; AFileSortType: TFileSortType);


Como vemos le pasamos varios parámetros, el primero el directorio en el cual debe buscar archivos, luego qué archivos (en este ejemplo simplemente *.* para todos los archivos), AObjectTypes es un conjunto de enumerados:

TObjectType = (otFolders, otNonFolders, otHidden);

TObjectTypes = set of TObjectType;


en este caso pedimos solo archivos que no sean carpetas, por lo tanto, archivos ocultos y carpetas no formarán parte del resultado. Nota: en Linux todo son archivos, también las carpetas.
El cuarto parámetro AResult: ahí le pasamos el TStringList, al cual antes de mandarlo, le establecimos la propiedad OwnObjects en True para el tema de evitar la fuga de memoria.
El último parámetro es un enumerado:

TFileSortType = (fstNone, fstAlphabet, fstFoldersFirst);

y se utiliza para indicar el orden de los archivos.
Para finalizar, se libera el TStringList.

miércoles, 23 de octubre de 2019

Editor simple de SQLite.

Son tan solo 200 líneas de código y desde ya, la librería de SQLite. Desde ya hay programas potentes y también portables como SQLite Studio, el cual uso y recomiendo. Volviendo al editor simple, lo hice para un uso muy específico y limitado, conecta a la base de datos que le indiquemos, muestra las tablas, permite ejecutar consultar cuyo resultado se muestra en un TDBGrid el cual cuenta con un par de opciones y ejecutar sentencias de actualizaciones que realiza mediante TZConnection.ExecuteDirect.
También tiene una opción para leer de un archivo un script SQL y luego ejecutarlo.
Creo que puede ser útil para quienes comiencen con SQLite, Lazarus y ZeosLib.


El código: descargar

unit principal;

{$mode objfpc}{$H+}

interface

uses
Classes, SysUtils, db, FileUtil, Forms, Controls, Graphics, Dialogs,
StdCtrls, Buttons, DBGrids, DbCtrls, ZConnection, ZDataset;

type
  TConexion=(Conectado, NoConectado);

type

{ TForm1 }

TForm1 = class(TForm)
  BBorrarMemo: TBitBtn;
  BCerrarConsulta: TBitBtn;
  btnSelBD: TBitBtn;
  btnConectar: TBitBtn;
  BCerrar: TBitBtn;
  btnDesconectar: TBitBtn;
  BExecute: TBitBtn;
  BConsulta: TBitBtn;
  btnLeerArchivo: TBitBtn;
  cbAutoCommit: TCheckBox;
  cbAutoSizeCol: TCheckBox;
  DataSource1: TDataSource;
  DBGrid1: TDBGrid;
  DBNavigator1: TDBNavigator;
  edBaseDeDatos: TEdit;
  edConexion: TEdit;
  Label1: TLabel;
  lbTablas: TListBox;
  Memo1: TMemo;
  OpenDialog1: TOpenDialog;
  ZConnection1: TZConnection;
  ZQuery1: TZQuery;
  procedure BBorrarMemoClick(Sender: TObject);
  procedure BCerrarConsultaClick(Sender: TObject);
  procedure BConsultaClick(Sender: TObject);
  procedure BExecuteClick(Sender: TObject);
  procedure btnLeerArchivoClick(Sender: TObject);
  procedure btnSelBDClick(Sender: TObject);
  procedure btnConectarClick(Sender: TObject);
  procedure BCerrarClick(Sender: TObject);
  procedure btnDesconectarClick(Sender: TObject);
  procedure cbAutoSizeColChange(Sender: TObject);
  procedure FormClose(Sender: TObject; var CloseAction: TCloseAction);
  procedure FormCreate(Sender: TObject);
  procedure lbTablasDblClick(Sender: TObject);
private
  Conexion:TConexion;
  procedure MuestroTablas;
  function HayConexion:Boolean;
{ private declarations }
public
{ public declarations }
end;

var
  Form1: TForm1;

implementation

{$R *.lfm}

{ TForm1 }

procedure TForm1.FormCreate(Sender: TObject);
begin
  Conexion:=NoConectado;
end;

procedure TForm1.btnSelBDClick(Sender: TObject);
begin
  if OpenDialog1.Execute then edBaseDeDatos.Text:= OpenDialog1.FileName;
end;

procedure TForm1.btnLeerArchivoClick(Sender: TObject);
begin
 if OpenDialog1.Execute then Memo1.Lines.LoadFromFile(OpenDialog1.FileName);
end;

procedure TForm1.BBorrarMemoClick(Sender: TObject);
begin
  Memo1.Clear;
end;

procedure TForm1.BCerrarConsultaClick(Sender: TObject);
begin
  ZQuery1.Close;
end;

procedure TForm1.BConsultaClick(Sender: TObject);
begin
  if not(HayConexion) then Exit;
  ZQuery1.Close;
  ZQuery1.SQL.Text:=Memo1.Text;
  ZQuery1.Open;
end;

procedure TForm1.BExecuteClick(Sender: TObject);
var
  n:Integer;
begin
  if not(HayConexion) then Exit;
  if ZConnection1.ExecuteDirect(Memo1.Text, n) then Memo1.Lines.Add('OK! '+IntToStr(n)+'      filas.');
end;

procedure TForm1.btnConectarClick(Sender: TObject);
begin
  if Conexion=Conectado then
  begin
    ShowMessage('Hay una conexión establecida, primero desconecte dicha conexión.');
    Exit;
  end;
  if not (FileExists(edBaseDeDatos.Text)) then exit;
  ZConnection1.Database:=edBaseDeDatos.Text;
  if not(cbAutoCommit.Checked) then ZConnection1.AutoCommit:=False;
  ZConnection1.Connect;
  if ZConnection1.Connected then
  begin
    edConexion.Text:='Conectado';
    edConexion.Font.Color:=clGreen;
    Conexion:=Conectado;
    MuestroTablas;
  end;
end;

procedure TForm1.BCerrarClick(Sender: TObject);
begin
  Close;
end;

procedure TForm1.btnDesconectarClick(Sender: TObject);
begin
  ZConnection1.Disconnect;
  edConexion.Text:='Desconectado';
  edConexion.Font.Color:=clRed;
  Conexion:=NoConectado;
  lbTablas.Clear;
end;

procedure TForm1.cbAutoSizeColChange(Sender: TObject);
begin
  if cbAutoSizeCol.Checked then DBGrid1.AutoFillColumns:=False else DBGrid1.AutoFillColumns:=True;
end;

procedure TForm1.FormClose(Sender: TObject; var CloseAction: TCloseAction);
begin
  ZConnection1.Disconnect;
  CloseAction:=caFree;
end;

procedure TForm1.lbTablasDblClick(Sender: TObject);
begin
  if not(HayConexion) then Exit;
  Memo1.Clear;
  Memo1.Lines.Add('SELECT * FROM '+lbTablas.Items[lbTablas.ItemIndex]+';');
  ZQuery1.Close;
  ZQuery1.SQL.Text:=Memo1.Text;
  ZQuery1.Open;
end;

procedure TForm1.MuestroTablas;
var
  ZQTablas:TZQuery;
begin
  lbTablas.Clear;
  ZQTablas:=TZQuery.Create(nil);
  ZQTablas.Connection:=ZConnection1;
  ZQTablas.SQL.Text:='SELECT name FROM sqlite_master WHERE type='+
  QuotedStr('table')+' and name <>'+QuotedStr('sqlite_sequence');
  ZQTablas.Open;
  if ZQTablas.RecordCount <1 then Exit;
  ZQTablas.First;
  while not(ZQTablas.EOF) do
  begin
    lbTablas.AddItem(ZQTablas.FieldByName('name').AsString,lbTablas);
    ZQTablas.Next;
  end;
  ZQTablas.Close;
  FreeAndNil(ZQTablas);
end;

function TForm1.HayConexion: Boolean;
begin
  if not(ZConnection1.Connected) then
  begin
    Memo1.Lines.Add('No hay establecida ninguna conexión.');
    Exit(False);
  end;
  Result:=True;
end;

end.


jueves, 26 de septiembre de 2019

TTreeView con nodos y punteros TTreeNode.Data

En la entrada anterior vimos las operaciones básicas de un TreeView solo con ítems, ahora veremos con nodos, que ya sirve de algo, con punteros a objetos, más precisamente a un registro, que es apuntado por TreeNode.Data siendo Data del tipo Pointer y, siendo Pointer un puntero sin tipo. En el programa de ejemplo se muestra como agregar un nodo raíz, un nodo hijo, el índice absoluto, modificar los datos de un registro de un nodo, borrar un nodo, mostrar los datos del registro apuntado por un nodo. Para el caso, los nodos raíz, no pueden apuntar a un objeto, solo a hojas (nodos), para el caso que se necesite que lo nodos de nivel cero tengan un registro (apunten a un registro), ya que no se puede, lo que se hace es se crea un único nodo raíz y se lo oculta, de esta forma todos los nodos parten del nivel 1 y son hijos de ese único nodo padre. En este ejemplo se parte de dos nodos raíces a los cuales el usuario le agrega nodos hijos, nietos, etc. y todos tienen el mismo tipo de registro, lo cual no es obligatorio; en este ejemplo un nodo raíz se llama "profesores" y el otro "alumnos", los nodos de ambos apuntan el mismo tipo de registro pero bien se puede crear un registro para profesores y otro para alumnos.


Como se ve en la imagen, se utilizan: un TTreeView, 5 TEdit, 4 TLabel y 5 botones.


Le agregamos 2 elementos al árbol, nodos de nivel 0 (cero), todas la operaciones que se hagan, serán sobre los nodos que "cuelguen" de estos dos.

type
PRegistro=^TRegistro;

TRegistro=record
  documento:string;
  nacionalidad:string;
  estadocivil:string;
end;


Definimos un registro y un puntero a dicho registro.

Agregar nodo:

procedure TForm1.BAgregarClick(Sender: TObject);
var
  RegPtr:PRegistro;
begin
  New(RegPtr);
  RegPtr^.documento:=edDocumento.Text;
  RegPtr^.nacionalidad:=edNacionalidad.Text;
  RegPtr^.estadocivil:=edEstadoCivil.Text;
  if (tv.Items.Count=0) or (tv.Selected=nil) then
    tv.Items.Add (nil, edNombre.Text)
  else
   tv.Items.AddChildObject(tv.Selected, edNombre.Text, RegPtr);
end;


Declaramos una variable local del tipo PRegistro (puntero al registro) y con New la instanciamos. Es decir, tenemos una nueva dirección de memoria y espacio para nuestro nuevo registro, luego le asignamos los datos. Ahora a ese registro que está "flotando" en la memoria RAM, hay que enlazarlo al árbol, y esto se hace asignando a la propiedad Data del nodo, la dirección de memoria donde se aloja el registro, eso lo hacemos mediante AddChildObject (nodopadre, textoamostrar, punteroalregistro). La parte True del if es para agregar un elemento raíz, si se agrega sin haber seleccionado un nodo, creará un ítem nivel 0.

Borrar un nodo:

procedure TForm1.BBorrarNodoClick(Sender: TObject);
begin
  if tv.Selected=nil then Exit;
  tv.Selected.Delete;
  BorroEdit; //Procedimiento que borrar los TEdit.
end;


Siempre averiguar si hay algún nodo seleccionado para evitar errores.

Modificar un nodo:

procedure TForm1.BModificarClick(Sender: TObject);
begin
  if tv.Selected=nil then Exit;
  edNombre.Text:=tv.Selected.Text;
  edDocumento.Text:=PRegistro(tv.Selected.Data)^.documento;
  edNacionalidad.Text:=PRegistro(tv.Selected.Data)^.nacionalidad;
  edEstadoCivil.Text:=PRegistro(tv.Selected.Data)^.estadocivil;
end;


En esta primera parte se carga el registro apuntado por el nodo en los TEdit. Como vemos no es necesario crear una variable local del tipo puntero a TRegsitro, se utiliza PRegistro(puntero)^.campo.

procedure TForm1.BActualizarClick(Sender: TObject);
begin
  if tv.Selected=nil then Exit;
  if tv.Selected.Level < 1 then Exit; 

  tv.Selected.Text:=edNombre.Text;      
  PRegistro(tv.Selected.Data)^.documento:=edDocumento.Text;  
  PRegistro(tv.Selected.Data)^.nacionalidad:=edNacionalidad.Text; 
  PRegistro(tv.Selected.Data)^.estadocivil:=edEstadoCivil.Text; 
end;

Cuando puslamos el botón Actualizar, lo que está en los TEdit, lo asignamos al registro; previamente hacemos las validaciones mínimas del caso, ya que solo es un ejemplo.

Ver el registro apuntado por un nodo:

procedure TForm1.BVerClick(Sender: TObject);
begin
  if tv.Items.Count < 1 then Exit;
  if tv.Selected=nil then exit;
  if (tv.Selected.Data < > nil) then
  begin
    lNombre2.Caption:=tv.Selected.Text;
    lDocumento.Caption:=PRegistro(tv.Selected.Data)^.documento;
    lNacionalidad.Caption:=PRegistro(tv.Selected.Data)^.nacionalidad;
    lEstadoCivil.Caption:=PRegistro(tv.Selected.Data)^.estadocivil;
  end;
end;


Mostramos el contenido del registro en los TLabel. Como siempre, validar antes de ejecutar la acción.

procedure TForm1.tvChange(Sender: TObject; Node: TTreeNode);
begin
  if (not(Assigned(Node)) or (Node.Level < 1)) then Exit;
  edNombre.Text:=tv.Selected.Text;
  edDocumento.Text:=PRegistro(tv.Selected.Data)^.documento;  

  edNacionalidad.Text:=PRegistro(tv.Selected.Data)^.nacionalidad;
  edEstadoCivil.Text:=PRegistro(tv.Selected.Data)^.estadocivil;
end;



En este caso cargamos los TEdit con el registro del nodo al cual se le ha hecho click y atención, no usamos el evento OnClick sino el evento OnChange que se lanza o dispara cuando cambia el nodo seleccionado. Es importante la validación previa, si el Node, que viene como parámetro, no está asignado o es de nivel 0, entonces "huímos".
Al implementar este método ya no sería necesario el botón modificar que carga los TEdit.

Ver el índice absoluto de un nodo:

procedure TForm1.BVerIndiceClick(Sender: TObject);
begin
  if tv.Selected=nil then exit;
  Edit4.Text := IntToStr(tv.Selected.AbsoluteIndex);
end;


Cada nodo tiene un índice absoluto dentro de un árbol, independientemente de su nivel (level), vendría a ser un identificador único.

Video:



Código fuente del proyecto: TTreeView_Nodos.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.

domingo, 25 de agosto de 2019

Evitar varias instancias de un programa.

La mayoría de la veces para que un programa funcione bien, es imprescindible que solo se ejecute una vez al mismo tiempo, es decir, permitir una única instancia de ejecución del programa.

El paquete UniqueInstance, nos permite agregar esta funcionalidad a nuestros programas, de manera sencilla.

Este paquete se puede instalar cómodamente desde OPM (On Line Package Manager) disponible en versiones de Lazarus 1.8.4 y posteriores.


Una vez instalado y reconstruida la IDE, el componente se encuentra en la paleta System, el círculo rojo con el número 1.


Se coloca en el formulario principal del programa.


En el inspector de objetos se lo activa y se le establece un identificador que, como se ve en la imagen, puede ser cualquier cadena de caracteres.


En eventos, solo hay uno, lo creamos presionando sobre los tres puntos.

procedure TForm1.UniqueInstance1OtherInstance(Sender: TObject;
ParamCount: Integer; const Parameters: array of String);
begin
  ShowMessage('El programa ya se está ejecutando.');
  BringToFront;
  FormStyle:=fsSystemStayOnTop;
  FormStyle:=fsNormal;
end;


Ahí podemos hacer algo simple, como un showmessage y luego traer al frente el programa, dado que el usuario seguramente lo tenga minimizado y no lo vea.

Simple programa de ejemplo:

unit Unit1;

{$mode objfpc}{$H+}

interface

uses
Classes, SysUtils, FileUtil, Forms, Controls, Graphics, Dialogs, StdCtrls,
UniqueInstance;

type

{ TForm1 }

TForm1 = class(TForm)
  Button1: TButton;
  UniqueInstance1: TUniqueInstance;
  procedure Button1Click(Sender: TObject);
  procedure UniqueInstance1OtherInstance(Sender: TObject;
  ParamCount: Integer; const Parameters: array of String);
private

public

end;

var
  Form1: TForm1;

implementation

{$R *.lfm}

{ TForm1 }

procedure TForm1.Button1Click(Sender: TObject);
begin
  Close;
end;

procedure TForm1.UniqueInstance1OtherInstance(Sender: TObject;
ParamCount: Integer; const Parameters: array of String);
begin
  ShowMessage('El programa ya se está ejecutando.');
  BringToFront;
  FormStyle:=fsSystemStayOnTop;
  FormStyle:=fsNormal;
end;

end.

jueves, 15 de agosto de 2019

Cambiar imágenes en TListView y TImageList.

Lo que parecía simple, lo era, el tema fue probar varias veces mil formas hasta no conseguirlo y preguntar en el foro, donde un "agradable sujeto" me guió con este código:

ListView1.SmallImages:=nil;
ListView1.SmallImages:=ImageList1;


El problema no eran mensajes de error de ningún tipo, sino que no actualizaba la imagen que quería cambiar ya sea mediante un TImage o TImageList y simplemente, no pasaba nada.


En la primera lista, utilizando el IDE, cargo 4 ítems; la lista 2 está vacía lo mismo la lista de imágenes 2 y la imagen que se ve arriba del primer botón. La imagen corresponde al programa en ejecución luego de haber presionado los dos botones.

Con Button1, si hay un elemento de la lista1 seleccionado, se permite cambiarlo por una imagen almacenada en el disco que tenga como máximo 32x32 píxeles.

Con Button2, cargo 10 imágenes ubicadas en la misma carpeta que el programa en ImageList2 y luego se asignan a ListView1.

El problema del espacio que hay entre las imágenes, especialmente notorio en ListView1 es un problema de GTK que reserva espacio para el texto (caption) y si no se utiliza, pues lo deja igual, con otros widgets las imágenes prácticamente se tocan.

Código:

unit Unit1;

{$mode objfpc}{$H+}

interface

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

type

  { TForm1 }

  TForm1 = class(TForm)
    Button1: TButton;
    Button2: TButton;
    Image1: TImage;
    ImageList1: TImageList;
    ImageList2: TImageList;
    ListView1: TListView;
    ListView2: TListView;
    OpenPictureDialog1: TOpenPictureDialog;
    procedure Button1Click(Sender: TObject);
    procedure Button2Click(Sender: TObject);
  private

  public

  end;

var
  Form1: TForm1;

implementation

{$R *.lfm}

{ TForm1 }

procedure TForm1.Button1Click(Sender: TObject);
begin
  if ListView1.ItemIndex<0 then Exit;
  if OpenPictureDialog1.Execute then
    begin
      try
        Image1.Picture.LoadFromFile(OpenPictureDialog1.FileName);
      Except
         begin
           ShowMessage('Invalid image.');
           Exit;
         end;
      end;
      if (Image1.Picture.Height>32) or (Image1.Picture.Width>32) then
      begin
        ShowMessage('The image > 32x32 pix.');
        Image1.Picture.Clear;
        Exit;
      end;
    end
    else Exit;
  ImageList1.Delete(ListView1.ItemIndex);
  ImageList1.Insert(ListView1.ItemIndex,Image1.Picture.Bitmap,nil);
  ListView1.SmallImages:=nil;
  ListView1.SmallImages:=ImageList1;
end;

procedure TForm1.Button2Click(Sender: TObject);
var
  i:Word;
  imagen:TImage;
  li:TListItem;
begin
  imagen:=TImage.Create(nil);
  for i:=0 to 9 do
    begin
      imagen.Picture.LoadFromFile(Application.Location+'00'+IntToStr(i)+'.png');
      ImageList2.Insert(i,imagen.Picture.Bitmap,nil);
      li:=ListView2.Items.Add;
      li.Caption:=IntToStr(i);
      li.ImageIndex:=i;
      imagen.Picture.Clear;
    end;
  FreeAndNil(imagen);
  ListView2.SmallImages:=nil;
  ListView2.SmallImages:=ImageList2;
end;

end.

Algunos comentarios acercar del código:
Respecto de la primera lista ListView1:

  if ListView1.ItemIndex<0 then Exit;
  if OpenPictureDialog1.Execute then
    begin
      try
        Image1.Picture.LoadFromFile(OpenPictureDialog1.FileName);
      Except
         begin
           ShowMessage('Invalid image.');
           Exit;
         end;
      end;
      if (Image1.Picture.Height>32) or (Image1.Picture.Width>32) then
      begin
        ShowMessage('The image > 32x32 pix.');
        Image1.Picture.Clear;
        Exit;
      end;
    end
    else Exit;

Lo primero a validar es que se haya seleccionado un ítem de la lista, sino, chau.
Éste es un claro ejemplo de cuándo utilizar try/Except justificadamente y no sólo por el hecho de que existe, porque el usuario, todo lo puede, por ejemplo intentar abrir un archivo que no es una imagen, entonces si la imagen está OK, continuamos, caso contrario, mensaje y salimos sin romper nada.
La siguiente validación es que la imagen no exceda los 32x32 pix. que es el tamaño definido mediante el inspector de objetos para las imágenes de ListView1. Si se supera el filtro, entonces:

  ImageList1.Delete(ListView1.ItemIndex);
  ImageList1.Insert(ListView1.ItemIndex,Image1.Picture.Bitmap,nil);
  ListView1.SmallImages:=nil;
  ListView1.SmallImages:=ImageList1;

Encontré fácil 10 métodos distintos para esto, finalmente usando los métodos Delete e Insert funciona y es simple. Primero se borra el ítem según ItemIndex y luego se agrega en la posición ItemIndex la imagen de Image1.
Finalmente, la clave, se establece en nil SmallImages y luego se le asigna ImageList1.

En cuanto a la otra lista, ListView2:

procedure TForm1.Button2Click(Sender: TObject);
var
  i:Word;
  imagen:TImage;
  li:TListItem;
begin
  imagen:=TImage.Create(nil);

La variable li del tipo TListItem es necesaria para agregar elementos (ítems) a la lista vacía y no se crea, porque se crea en TListItemS, así está explicado en el código fuente por los programadores de Lazarus.

Descargar código fuente: TListView_TImageList.7z

domingo, 30 de junio de 2019

Formularios MDI.

Mucho debate genera este tema, como que es obsoleto, antiguo, etc. Siempre que busqué información terminé abandonando, porque encontré paquetes abandonados, que use frames y miles de consejos de no usarlos. A mí me parece más ridículo programar todo con showmodal, si bien es más cómodo, fácil y, hasta si se quiere, seguro; pero no dejo de tener en mis programas un formulario principal eternamente vacío, en la mayoría de los casos. No me refiero a los formularios en cascada, sino a que cada vez que se abra un formulario, el mismo ocupe el espacio que hay en el formulario principal. También algunos usuarios prefieren este método a un formulario showmodal flotando. Probablemente esto pueda hacerse mejor usando frames, pero de momento lo desconozco.
La solución se me ocurrió simplemente pensando en la propiedad aling:=alClient pensé que si funciona con un TMemo, ¿por qué no con un Form? y así encontré en el foro, utilizando la búsqueda avanzada, la clave: FromStyle, uno debe ser fsMDIForm y el otro fsMDIChild sin olvidar Aling alClient.

En el video se muestra, con errores como el que se ve en el minuto 3:30 donde intento establecer el parentesco del Form1 respecto de Form2 sin notar que el Form2 se crea después que el Form1 obteniendo un hermoso SIGSEGV apreciable en el minuto 5.



Este error se soluciona cambiando en el archivo del proyecto el orden de creación de los formularios y estableciendo el parentesco en el evento Create del Form1.



Es solo un ejemplo, la mayoría de los formularios los creo, los muestro y los libero: Fomr2.Crate(nil); Form2.ShowModal; FreeAndNil(Form2); todavía no probé como hacerlo, pero ya sé que ShowModal no se utiliza con un Form de estilo fsMDIChild, y es lógico.

Código fuente Formularios MDI.

miércoles, 26 de junio de 2019

La ubicación del programa ejecutable.

Vamos a ver tres métodos de hallar o intentar encontrar el path del archivo ejecutable de nuestro programa (me resisto a llamarle aplicación, somos programadores, no aplicadores). Los resultados serán distintos en algunos casos, porque depende desde que ubicación se llame al programa, si es desde la carpeta o directorio donde se encuentra el mismo, entonces no hay problema, como diría ALF, pero si se lo hace a través de un acceso directo desde el escritorio o desde un navegador de archivos llegando al programa abriendo carpetas pero sin entrar en ellas, ahí cambia la cosa. No obstante, hay una forma que no falla nunca y es la correcta:

ExtractFileDir(Application.ExeName)

----> o más simple aún, que encontré después de escribir esta entrada:

Application.Location <---

Ejemplo:

unit Unit1;

{$mode objfpc}{$H+}

interface

uses
  Classes, SysUtils, FileUtil, Forms, Controls, Graphics, Dialogs, StdCtrls;

type

{ TForm1 }

TForm1 = class(TForm)
  Memo1: TMemo;
  procedure FormCreate(Sender: TObject);
private
{ private declarations }
public
{ public declarations }
end;

var
  Form1: TForm1;

implementation

{$R *.lfm}

{ TForm1 }

procedure TForm1.FormCreate(Sender: TObject);
var
  elPath:String;
  elPrograma:String;
begin
  elPrograma:=Application.ExeName;
  elPath:=TrimFilename(GetCurrentDir+PathDelim);
  Memo1.Lines.Add('TrimFilename(GetCurrentDir+PathDelim+elPrograma) --> '+elPath);
  elPath:=ExtractFileDir(Application.ExeName);
  Memo1.Lines.Add('ExtractFileDir(Application.ExeName) --> '+elPath);
  elPath:=ExtractFilePath(Application.ExeName);
  Memo1.Lines.Add('ExtractFilePath(Application.ExeName) --> '+elPath);
end;
end.



El primero, es a lo bestia, y falla si no se ejecuta el programa desde su carpeta.
El segundo, ya utilizando Application.ExeName, no es que falle, sino que hace para lo que está programado, ExtractFileDir y no incluye la barra delimitadora o PathDelim.
Finalmente el tercero es el adecuado: ExtractFilePath que a diferencia del anterior, sí incuye la barra delimitadora.
Cabe destacar que ninguno en realidad falla, hablo de falla para lo que se busca, que es el path (completo).


Resultado ejecutando el programa desde la IDE Lazarus.

Resultado desde la carpeta del programa.


Resultado desde la carpeta programas.

Nota: la clase Application se ubica en la unidad Forms.

sábado, 22 de junio de 2019

TMemo: Leer y guardar en un archivo.

Varios componentes de Lazarus tienen los métodos LoadFromFile y SaveToFile y TMemo no es la excepción, pero lo hace, lógicamente, a través de TString.

Memo1.Lines.LoadFromFile('prueba.txt')

porque Lines es del tipo TStrings que es la clase que posee dichos métodos.

Código de ejemplo:

unit Unit1;

{$mode objfpc}{$H+}

interface

uses
Classes, SysUtils, FileUtil, Forms, Controls, Graphics, Dialogs, StdCtrls;

type

{ TForm1 }

TForm1 = class(TForm)
  Button1: TButton;
  Button2: TButton;
  Memo1: TMemo;
  procedure Button1Click(Sender: TObject);
  procedure Button2Click(Sender: TObject);
private
{ private declarations }
public
{ public declarations }
end;

var
  Form1: TForm1;

implementation

{$R *.lfm}

{ TForm1 }

procedure TForm1.Button1Click(Sender: TObject);
begin
  Memo1.Lines.LoadFromFile('tabla2019.txt');
end;

procedure TForm1.Button2Click(Sender: TObject);
begin
  Memo1.Lines.SaveToFile('tabla2019.txt');
end;

end.




Desde ya es el ejemplo más básico, generalmente se recurre a diálogos para que el usuario elija el archivo a leer y guardar, entre otras cosas.

viernes, 21 de junio de 2019

Formato de fecha y hora en SQLite.

En SQLite el formato de la fecha/hora es YYYY-MM-DD HH:MM.SS.MMM por ejemplo: 2019-06-22 01:00:27.123

Si bien Free Pascal tiene muchas funciones para el tratamiento de fecha y hora, a veces por la configuración regional hay que hacer conversiones.

Usando TFormatSettings:

aFormatSettings.LongDateFormat:='yyyy-mm-dd hh:nn:ss';
aFormatSettings.DateSeparator := '-';
aFormatSettings.TimeSeparator := ':';


Donde aFormatSettings es una variable del tipo TFormatSettings y luego utilizando la función FormatDateTime que devuelve un string:

function FormatDateTime(

const FormatStr: string;

DateTime: TDateTime;

Options: TFormatDateTimeOptions = []

):string;

function FormatDateTime(

const FormatStr: string;

DateTime: TDateTime;

const FormatSettings: TFormatSettings;

Options: TFormatDateTimeOptions = []

):string;


Por ejemplo en una sentencia SQL en WHERE:

sqlWhere:='WHERE regfecha BETWEEN '+QuotedStr(FormatDateTime('YYYY-MM-DD',edFechaDesde.Date))+
' AND '+QuotedStr(FormatDateTime('YYYY-MM-DD',edFechaHasta.Date))';


En este caso no fue necesario TFormatSettings y solo fue fecha, sin hora.

Con TZquery.FieldByName y utilizando componentes TDateTimePicker esto funciona:

ZQReg.FieldByName('regfechahora').AsDateTime:=dtpFecha.Date+dtpHora.Time;

En cuanto los campos del tipo TIME en SQLite; StrToTime('00:00:00') evita el valor nulo que muchas veces conviene evitar.

En SQLite la fecha debe ir entre comillas simples.

Y ante cualquier complicación siempre podemos hacer una función como la siguiente:

function FormatoFechaHoraSQLite(lafechahora: TDateTime): String;
var
  d,m,a,h,mi,s,ms:Word;
  dd,mm,hh,mmi,ss,mms:String;
  lfecha, lhora:String;
begin
  DecodeDate(lafechahora,a,m,d);
  if d < 10 then dd:='0'+IntToStr(d) else dd:=IntToStr(d);

  if m < 10 then mm:='0'+IntToStr(m) else mm:=IntToStr(m);
  lfecha:=IntToStr(a)+'-'+mm+'-'+dd;
  DecodeTime(lafechahora,h,mi,s,ms);
  if h < 10 then hh:='0'+IntToStr(h) else hh:=IntToStr(h);
  if mi < 10 then mmi:='0'+IntToStr(mi) else mmi:=IntToStr(mi);
  if s < 10 then ss:='0'+IntToStr(s) else ss:=IntToStr(s);
  if ms < 10 then mms:='00'+IntToStr(ms) else
    if ms < 100 then mms:='0'+IntToStr(ms) else
      mms:=IntToStr(ms);
  lhora:=hh+':'+mmi+':'+ss+'.'+mms;
  Result:=lfecha+' '+lhora;
end;

O más sencillo usando la función AddChar que se encuentra en la unidad strutils.

function FormatoFechaHoraSQLite2(lafechahora: TDateTime): String;
var
  d,m,a,h,mi,s,ms:Word;
  dd,mm,hh,mmi,ss,mms:String;
  lfecha, lhora:String;
begin
  DecodeDate(lafechahora,a,m,d);
  dd:=AddChar('0',IntToStr(d),2);
  mm:=AddChar('0',IntToStr(m),2);
  lfecha:=IntToStr(a)+'-'+mm+'-'+dd;
  DecodeTime(lafechahora,h,mi,s,ms);
  hh:=AddChar('0',IntToStr(h),2);
  mmi:=AddChar('0',IntToStr(mi),2);
  ss:=AddChar('0',IntToStr(s),2);
  mms:=AddChar('0',IntToStr(ms),3);
  lhora:=hh+':'+mmi+':'+ss+'.'+mms;
  Result:=lfecha+' '+lhora;
end;


DecodeDate y DecodeTime son procedimientos y utiliza variables del tipo Word que son enteros sin signo entre 0 y 65535. Le enviamos un TDateTime y las variables del tipo Word donde se escribirán el año, mes y día (en DecodeDate). No es necesario inicializar las variables, el procedimiento utiliza out:

procedure DecodeDate(

Date: TDateTime;

out Year: Word;

out Month: Word;

out Day: Word

);