domingo, 29 de agosto de 2021

El bucle for in do.

La sentencia for in do (for in do loop) está disponible desde la versión 2.4.2 del compilador Free Pascal, es decir, es relativamente nueva y no existe ni en Pascal ni en Turbo Pascal, sí en Object Pascal de Delphi, aunque hay algunas "cosillas" que se pueden hacer con Free Pascal y no con Delphi, respecto de esta sentencia de iteración.

Seguramente para los nuevos (y no tan nuevos) programadores no hay ninguna novedad con este bucle, pero los que nos iniciamos mucho antes, dejamos la profesión unos años y retomamos en estos tiempos, hay cosas que cuestan un poco, no tenemos el concepto, nacimos con el for to do, while do, repeat until y de ser necesario, punteros, y con eso se hacía (y hace) de todo.

Las cosas cambian, los lenguajes evolucionan o mueren, aunque no siempre incorporar nuevas sentencias o lo que sea, signifique evolución, pero es preferible eso al estancamiento. Y la verdad que tanto Free Pascal y Delphi (ambos Object Pascal) conservan la belleza del lenguaje, esa sintaxis única.

Creo que lo mejor es empezar por responder ¿qué lo diferencia del for to do?:

En el for to se usa un índice, generalmente "i", entero, para recorrer los elemento de un vector (array) y acceder a ellos utilizando el valor de i, es decir obtenemos un valor de unarray[i]. En cambio en el for in para hacer lo mismo, usando una variable i y un array con índices enteros, la variable i va tomando los valores del array, los valores se obtienen de i. no de unarray[i].

Otra diferencia es que mediante el empleo de un for to se puede recorrer solo una parte, por ejemplo un array de 500 elementos con un for i:=1 to 10 do... solo accedemos a los 10 primeros, en cambio con el uso de for i in ... se recorre todo.

La variable de control debe ser del mismo tipo que los elementos del conjunto a recorrer.

El conjunto a iterar debe ser de un número fijo de elementos.

La variable es una copia temporal del elemento del bucle.

Ejemplos:

program pruebaforin;
{$mode objfpc}{$H+}
uses Classes;

type
  TSemana=(domingo, lunes, martes, miercoles, jueves, viernes, sabado);


var
  entero:Integer;
  arrEntero:array[1..12] of Integer=(1,2,4,8,5,6,7,8,9,27,11,12);
  cadena:String;
  arrCadena:array[1..5] of String =('Lunes','Martes','Miércoles','Jueves','Viernes');
  dia:TSemana;
  diaslaborables:set of TSemana =[lunes, martes, miercoles, jueves, viernes];
  caracter:Char;
  meses:TStringList;

begin
  WriteLn('Vector de eneteros');
  WriteLn('entero:Integer;');
  WriteLn('arrarrEntero:array[1..12] of Integer=(1,2,4,8,5,6,7,8,9,27,11,12);');
  WriteLn('for entero in arrEntero do WriteLn(''entero =  '',entero,''  arrEneter['',entero,'']='',arrEntero[entero]);');
  for entero in arrEntero do WriteLn('entero =  ',entero,'  arrEneter[',entero,']=',arrEntero[entero]);
  ReadLn;
  WriteLn('Vector de cadena de caracteres.');
  WriteLn('cadena:String;');
  WriteLn('arrCadena:array[1..5] of String =(''Lunes'',''Martes'',''Miércoles'',''Jueves'',''Viernes'');');
  WriteLn('for cadena in arrCadena do WriteLn(''Cadena = '',cadena);');
  for cadena in arrCadena do WriteLn('Cadena = ',cadena);
  ReadLn;
  WriteLn('Enumerados.');
  WriteLn('TSemana=(domingo, lunes, martes, miercoles, jueves, viernes, sabado); ');
  WriteLn('dia:TSemana;');
  WriteLn('for dia in Tsemana do WriteLn(''dia = '',dia);');
  for dia in Tsemana do WriteLn('dia = ',dia);
  ReadLn;
  WriteLn('Conjuntos.');
  WriteLn('TSemana=(domingo, lunes, martes, miercoles, jueves, viernes, sabado); ');
  WriteLn('diaslaborables:set of TSemana =[lunes, martes, miercoles, jueves, viernes];');
  WriteLn('dia:TSemana;');
  WriteLn('for dia in diaslaborables do WriteLn(''dia = '',dia);');
  for dia in diaslaborables do WriteLn('dia = ',dia);
  ReadLn;
  WriteLn('Caracteres.');
  WriteLn('caracter:Char;');
  WriteLn('for caracter in ''abcdefg'' do WriteLn(''caracter = '',caracter);');
  for caracter in 'abcdefg' do WriteLn('caracter = ',caracter);
  ReadLn;
  WriteLn('Classes.');
  WriteLn('cadena:String;');
  WriteLn('meses:TStringList;');
  WriteLn('for cadena in meses do WriteLn(''cadena = '',cadena);');
  meses:=TStringList.Create;
  meses.Add('Enero');
  meses.Add('Febrero');
  meses.Add('Marzo');
  meses.Add('Abril');
  meses.Add('Mayo');
  for cadena in meses do WriteLn('cadena = ',cadena);
  meses.Free;
  ReadLn;
end.      

Resultado del programa (salida por consola):

Vector de eneteros
entero:Integer;
arrarrEntero:array[1..12] of Integer=(1,2,4,8,5,6,7,8,9,27,11,12);
for entero in arrEntero do WriteLn('entero =  ',entero,'  arrEneter[',entero,']=',arrEntero[entero]);
entero =  1  arrEneter[1]=1
entero =  2  arrEneter[2]=2
entero =  4  arrEneter[4]=8
entero =  8  arrEneter[8]=8
entero =  5  arrEneter[5]=5
entero =  6  arrEneter[6]=6
entero =  7  arrEneter[7]=7
entero =  8  arrEneter[8]=8
entero =  9  arrEneter[9]=9
entero =  27  arrEneter[27]=0
entero =  11  arrEneter[11]=11
entero =  12  arrEneter[12]=12

Vector de cadena de caracteres.
cadena:String;
arrCadena:array[1..5] of String =('Lunes','Martes','Miércoles','Jueves','Viernes');
for cadena in arrCadena do WriteLn('Cadena = ',cadena);
Cadena = Lunes
Cadena = Martes
Cadena = Miércoles
Cadena = Jueves
Cadena = Viernes

Enumerados.
TSemana=(domingo, lunes, martes, miercoles, jueves, viernes, sabado);
dia:TSemana;
for dia in Tsemana do WriteLn('dia = ',dia);
dia = domingo
dia = lunes
dia = martes
dia = miercoles
dia = jueves
dia = viernes
dia = sabado

Conjuntos.
TSemana=(domingo, lunes, martes, miercoles, jueves, viernes, sabado);
diaslaborables:set of TSemana =[lunes, martes, miercoles, jueves, viernes];
dia:TSemana;
for dia in diaslaborables do WriteLn('dia = ',dia);
dia = lunes
dia = martes
dia = miercoles
dia = jueves
dia = viernes

Caracteres.
caracter:Char;
for caracter in 'abcdefg' do WriteLn('caracter = ',caracter);
caracter = a
caracter = b
caracter = c
caracter = d
caracter = e
caracter = f
caracter = g

Classes.
cadena:String;
meses:TStringList;
for cadena in meses do WriteLn('cadena = ',cadena);
cadena = Enero
cadena = Febrero
cadena = Marzo
cadena = Abril
cadena = Mayo

Para ejemplos más avanzados y documentación oficial:

Documentación: https://www.freepascal.org/docs-html/ref/refsu59.html

Wiki (Siempre y cuando algún iluminado no haya eliminado o movido la página): https://wiki.lazarus.freepascal.org/for-in_loop

Código fuente del ejemplo.

domingo, 22 de agosto de 2021

SetFocus y un error muy común.

Casi todos los componentes visuales de Lazarus tienen un procedimiento llamado SetFocus, por lo que es normal que si tenemos 10 TEdit en un Form, queramos que el primero de ellos tenga el "focus", y luego el segundo y así sucesivamente. Y ahí el dicho "code first, think later" o "escribe el código primero, piensa después" se pone de manifiesto cuando intentamos un Edit1.SetFocus en el método FormCreate y tenemos un hermoso error en tiempo de ejecución. ¿Por qué?

[TCustomForm.SetFocus] Form1:TForm1 Can not focus.

Justamente porque pedimos poner el foco en algo que todavía no existe, el Edit1, en el evento FormCreate que hace lo que dice, crea el formulario, los TEdit todavía no existen.

Sí, todo muy lindo y suena hasta lógico, ¿pero entonces cómo lo consigo?

Opción 1: usando la opción "Orden de tabulación", click derecho sobre el formulario.

Opción 2: precisamente en el procedimiento FormCreate estableciendo el control activo de la siguiente forma: 

procedure TForm1.FormCreate(Sender: TObject);
begin
  ActiveControl:=Edit1;
end; 

Y la duda razonable, si no se puede un setfocus ¿por qué sí un ActiveControl?, la respuesta está en el código fuente de la unidad Forms (forms.pp), wincontrol.inc y customform.inc. La respuesta corta es que edit1.setfocus aún no está disponible porque estamos llamando a un evento de un TEdit en FormCreate, en cambio ActiveControl es una propiedad de TForm.

En el código de la unidad control.pp podemos encontrar:

    function CanFocus: Boolean; virtual;
    function CanSetFocus: Boolean; virtual;  

que son funciones públicas de la clase (class) TWinControl que hereda de TControl y también pueden ser usadas para evitar errores al utilizar el procedimiento SetFocus.

Otras opciones:

procedure TForm1.FormActivate(Sender: TObject);
begin
  Edit1.SetFocus;
end;    

procedure TForm1.FormShow(Sender: TObject);
begin
 Edit1.SetFocus;
end;    

Funcionan bien con formularios mostrados mediante ShowModal, no obstante recomiendo las dos primeras.

lunes, 16 de agosto de 2021

Registros de estructura variable.

A diferencia de los registros de estructura fija, los registros de estructura variable tienen campos disponibles según el resultado del selector CASE OF. No es algo simple de comprender a primera vista por ende lo más recomendable es practicar (y mucho) para poder entender como funciona.
Es usado en programación avanzada, en el código fuente de Turbo Pascal, por ejemplo, y calculo que en Free Pascal también. Vale aclarar que no se recomienda su uso, además de por lo complejo, por tratarse Pascal de un lenguaje fuertemente tipado. Si bien existen al menos desde Turbo Pascal, sospecho que su implementación fue para tener algo parecido a “union” de otro lenguaje. Con Object Pascal, hay muy buenas alternativas, advanced records, objects y class (registros avanzados, objetos y clases).

En mi caso me costaba entender como hacía el compilador, ya que según el CASE luego podrían venir 10 campos más, o 2 o ninguno, porque supuestamente es una estructura variable, pero no es tan variable, es una unión, ahí está la clave, lo variable es como se lo utiliza, pero el record se arma con todos lo campos, si hay un CASE con un selector que define 3 campos y otro define 5 campos, entonces el registro tendrá 8 campos, no 3 u 8 según el valor del selector. Claro que algunos campos compartirán la misma área de memoria. La longitud (máxima) de los campos de la parte variante debe ser conocida por el compilador, por ejemplo, Byte, Integer, string[20] está bien, en cambio string, no.

Por ejemplo:

type
  Reg=record
    campo1:string[5];
    case algo: boolean of:
    true : (campo2: integer; campo3:Byte);
    false: (campo4:word);
  end;

Vendría internamente a ser algo así:

type
  Reg=record
    campo1:string[5];
    campo2: integer;
    algo:boolean;
    campo3:Byte;
    campo4:word;
  end;

Pero hay algo más complicado aún, todos los campos son accesibles independientemente del selector, es decir, el programador es el responsable de por ej., si algo es false entonces no utilizar el campo2 y el campo3, solo el campo4.

type TRec=record
       int:Integer;
       case Boolean of
       True : (s1:String[10]);
       False: (b1:Byte);
     end;

En este caso, no es posible establecer el valor de Boolean.
¿Qué dice la documentación oficial al respecto?
La parte variante debe ser la última en el registro. El identificador opcional en la instrucción case sirve para acceder al valor del campo de etiqueta, que de otro modo sería invisible para el programador. Se puede utilizar para ver qué variante está activa en un momento determinado (sin embargo, depende del programador mantener este campo). En efecto, introduce un nuevo campo en el registro.

Pero hay más, en alguno casos, como veremos en un ejemplo de consola, la parte variante para determinados identificadores o campos, comparten la misma área de memoria, por ende el valor es el mismo independientemente de valor del selector. Según alguien escribió en la wiki de FP, esto es algo extremadamente útil y cita un ejemplo con una declaración de registro y una breve explicación.
El ejemplo es el siguiente:

type
  TSpecialWord = record
    case Byte of
      0: (Word_value: Word);                      // 7  type Word = 0..65535;
      1: (Byte_low, Byte_high: Byte);             // 7, 0 type Byte = 0..255
      2: (Bits: bitpacked array [0..15] of 0..1); // 1, 1, 1, 0, 0, ...
  end;

Este registro tiene solo una parte variable y permite el acceso al valor de la palabra, los bytes individuales e incluso a los bits. Un identificador no es necesariamente necesario en la cláusula case, por lo que no ocupa ninguna memoria. El tamaño de este registro es de dos bytes. En el caso de Bits, esto solo es posible si se utiliza bitpacked. Observe el orden de los bytes, con el byte menos significativo (LSB) primero.

Veamos que pasa con ese ejemplo.
Declaramos una variable sw:TSpecialWord. No inicializamos la variable e imprimimos con writeln. El resultado serán todos ceros. Lo mismo sucede si incicializamos con Deflaut (sw:=Default(TSpecialWord);).
Ahora agregamos:  

sw.Word_value:=4;

Resultado:

Word_value = 4  //Lo asignamos.
Byte_low = 4  //Comparte la misma dirección de memoria, es correcto.
Byte_high = 0  //Es el valor por default.
Bits = 0010000000000000 //Es 4 en binario.

Probemos con:

sw.Word_value:=40000;

Resultado:

Word_value = 40000
Byte_low = 64  //40000/625=64 o 40000/156,25*4=64
Byte_high = 156 //40000/256=156,25
Bits = 0000001000111001 //569 en decimal.

Más pruebas:

sw.Bits[0]:=1;
sw.Bits[1]:=1;
sw.Bits[2]:=1;

Resultado:

Word_value = 7
Byte_low = 7
Byte_high = 0
Bits = 1110000000000000 //7

sw.Byte_low:=5;

Resultado:

Word_value = 5
Byte_low = 5
Byte_high = 0
Bits = 1010000000000000 //5

sw.Byte_high:=128;

Resultado:

Word_value = 32768
Byte_low = 0
Byte_high = 128
Bits = 0000000000000001

sw.Byte_low:=128;

Word_value = 128
Byte_low = 128
Byte_high = 0
Bits = 0000000100000000 //256

sw.Byte_high:=8;

Resultado:

Word_value = 2048
Byte_low = 0
Byte_high = 8
Bits = 0000000000010000 //16

sw.Bits[0]:=1;
sw.Bits[1]:=1;
sw.Bits[2]:=1;
sw.Bits[3]:=1;
sw.Bits[4]:=1;
sw.Bits[5]:=1;
sw.Bits[6]:=1;
sw.Bits[7]:=1;
sw.Bits[8]:=1;  

Resultado:

Word_value = 511
Byte_low = 255
Byte_high = 1
Bits = 1111111110000000 //511

Como vemos es bastante complejo cuando no se utiliza un identificador en el selector CASE.

El identificador del selector debe ser del tipo ordinal.

Los Case se pueden anidar.

Las partes variantes no pueden ser del tipo variant, string largos (string no, string[120] sí),  vectores dinámicos, datos estructurados que contengan lo anteriormente citado o interfaces, pero sí pueden ser punteros a esos tipos (types).

La mejor documentación que encontré es de Embarcadero: http://docwiki.embarcadero.com/RADStudio/Tokyo/en/Structured_Types_(Delphi)#Variant_Parts_in_Records

Como no encontré ningún ejemplo desarrollado completo elaboré uno.
 
Código fuente: RegistrosVariantes.7z
unit Unit1;

{$mode objfpc}{$H+}

interface

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

type

  { TForm1 }

  TForm1 = class(TForm)
    BMostrarRegVariante: TButton;
    BCerrar: TButton;
    cbCasado: TCheckBox;
    DateTimePicker1: TDateTimePicker;
    Label1: TLabel;
    Label2: TLabel;
    Memo2: TMemo;
    procedure BMostrarRegVarianteClick(Sender: TObject);
    procedure BCerrarClick(Sender: TObject);
    procedure FormClose(Sender: TObject; var CloseAction: TCloseAction);
    procedure FormCreate(Sender: TObject);
  private

  public

  end;

  TVariantRec=record
    ID:Word;
    Nombre:String[50];
    case Casado : Boolean of
      True : (vrDate:TDate);
      False: (str2:String[10]);
  end;

var
  Form1: TForm1;
  VariantRec:TVariantRec;

implementation

{$R *.lfm}

{ TForm1 }

procedure TForm1.FormCreate(Sender: TObject);
begin
  VariantRec.ID:=27;
  VariantRec.Nombre:='Jorge';
  VariantRec.Casado:=True;
  VariantRec.vrDate:=EncodeDate(2010,12,25);
  VariantRec.str2:='Soltero';
end;

procedure TForm1.BMostrarRegVarianteClick(Sender: TObject);
begin
  VariantRec.Casado:=cbCasado.Checked;
  if cbCasado.Checked then
    VariantRec.vrDate:=DateTimePicker1.Date
  else
    VariantRec.str2:='Soltero';
  Memo2.Lines.Add(VariantRec.ID.ToString);
  Memo2.Lines.Add(VariantRec.Nombre);
  Memo2.Lines.Add(BoolToStr(VariantRec.Casado,'True','False'));
  Memo2.Lines.Add(DateToStr(VariantRec.vrDate));
  Memo2.Lines.Add(VariantRec.str2);
end;

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

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

end.  

Más ejemplos, en este caso por consola:

program project1;

{$mode objfpc}{$H+}

uses
  {$IFDEF UNIX}{$IFDEF UseCThreads}
  cthreads,
  {$ENDIF}{$ENDIF}
  Classes
  { you can add units after this };

type TRec=record
       int:Integer;
       case Boolean of
       True : (s1:String[10]);
       False: (b1:Byte);
     end;
var
  rec:TRec;

begin
  rec.int:=2;
  rec.s1:='abx';
  rec.b1:=4;

  WriteLn(rec.int);
  WriteLn(rec.b1);
  WriteLn(rec.s1);
  ReadLn;
end.

Resultado:                   

2
4
abx

Todo, como un registro fijo.
 
-------------------------------------------------------------------------------------------------------------------------- 

type TRec=record
       int:Integer;
       case cond:Boolean of
       True : (s1:String[10]);
       False: (b1:Byte);
     end;
var
  rec:TRec;

begin
  rec.int:=2;
  rec.cond:=False;
  rec.s1:='abx';
  rec.b1:=4;

  WriteLn(rec.int);
  WriteLn(rec.cond);
  WriteLn(rec.b1);
  WriteLn(rec.s1);
  ReadLn;
end.       

Resultado:                   

2
FALSE
4
abx

De nuevo, todo, al compartir la misma dirección de memoria, entiendo que no debería suceder, o dar en s1 otro resultado que sea abx. Tampoco debí pedir el campo s1.
 
--------------------------------------------------------------------------------------------------------------------------

type TRec=record
       int:Integer;
       case cond:Boolean of
       True : (s1:String[10]);
       False: (s2:String[10]; b1:Byte);
     end;
var
  rec:TRec;

begin
  rec.int:=2;
  rec.cond:=False;
  rec.s1:='abx';
  rec.s2:='fgh';
  rec.b1:=4;

  WriteLn(rec.int);
  WriteLn(rec.cond);
  WriteLn(rec.s1);
  WriteLn(rec.s2);
  WriteLn(rec.b1);
  ReadLn;
end.   

Resultado:                   

2
FALSE
fgh
fgh
4

En este caso si se muestra el mismo valor en s1 y s2, mismo tipo, imprime el valor de s2 porque cond=flase. 
 
Documentación:

viernes, 6 de agosto de 2021

La directiva {$include}

Tal como lo indica su nombre, la directiva de compilación {$Include} o {$I} se utiliza para incluir el contenido de un archivo. Generalmente se utiliza la extensión de archivo .inc aunque es opcional, también se utiliza .pp y .pas o puede no utilizarse ninguna extensión.

Si nos fijamos en el código fuente de Lazarus o de Free Pascal, encontraremos miles de ejemplos.

El uso de archivos ".inc" mejora mucho la legibilidad de un proyecto y su mantenimiento. Muchas veces se utiliza para separar el código que "molesta", como gran cantidad de definiciones. Por ejemplo una unidad que utiliza muchas definiciones de registros que ocupan varias líneas (100, 200, lo que cada uno entienda por "varias"), esas definiciones se pueden separar a otro archivo .inc e invocar ese archivo mediante la directiva {$I} en una sola línea en el lugar donde antes estaban, es importante esto, porque el compilador traerá lo que contenga el archivo .inc y lo insertará donde se encuentre la directiva.

En este caso serán definiciones de registros pero puede ser cualquier cosa, procedimientos, clases, etc. 

No es necesario incluir el archivo al proyecto, el compilador lo buscará en el directorio del proyecto, en los directorios a buscar indicados en el proyecto o en el lugar que se haya especificado, por ejemplo: {$I /home/programas/prueba/archivo.inc"}.

Se puede invocar esta directiva en una unidad todas las veces que sea necesario y en cualquier parte de la unidad, es decir que podemos incluir varios archivos en distintas partes.

No solo se pueden incluir archivos, también información de compilación y variables de entorno, en este último caso se diferencia entre mayúsculas y minúsculas.

Documentación oficial (en inglés) de esta directiva:

$I or $INCLUDE : Include file 

$I or $INCLUDE : Include compiler info

Wiki de Free Pascal.

jueves, 11 de marzo de 2021

La función Assigned

Assigned es un función sencilla y muy útil, devuelve True si el parámetro pasado no es Nil y False si es Nil, pero cuidado que las variables del tipo puntero no reciben el valor nil por el solo hecho de declararlas, es decir por default o de manera predeterminada; así, en el siguiente ejemplo, Assigned devolverá true:

var
  aPtrChar:PChar;
begin
  if Assigned(aPtrChar) then
    ShowMessage('aPtrChar no es nil.')
  else
    ShowMessage('aPtrChar es nil');
end;

se mostrará el mensaje de que no es nil, aunque no apunte a ningún lado. Por eso es muy recomendable utilizar esta función en lugar de preguntar si es distinto de nil: 

if aPtrChar <> nil.

Algunos ejemplo de su uso:

Si por ejemplo en un árbol queremos mostrar información sobre un nodo con el evento Click, primero hay que asegurarse de que en efecto hay un nodo seleccionado para que no ocurran desastres.

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

Continuando con ejemplos TTreeView:

nuevamente se verifica que el nodo esté asignado, caso contrario, se sale sin hacer nada. En este caso también se averigua si el nodo es raíz.

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;

Un caso con TStringList:

procedure TFFiltrar.FormCreate(Sender: TObject);
begin
  if not(Assigned(filtros)) then filtros:=TStringList.Create;
  Memo1.Lines:=filtros;
end; 

la variable filtros está declarada en otra unidad y puede ser que ya haya sido creada (inicializada o instanceada) por eso, ante de intentar crearla dos veces, lo que generará un hermoso run time error, se verifica mediante Assigned.

Para finalizar, un ejemplo de TListView:

procedure TForm1.BQuitarClick(Sender: TObject);
begin
  if ((LView.Items.Count<1) or (not Assigned(LView.Selected))) then Exit;
  ListaCarpetas.Borrar(LView.Items[LView.ItemIndex].Caption);
  ListaCarpetas.ToListView(LView);
end;

se usa Assigned sobre TListView.Selected, de esta forma si no hay ningún elemento seleccionado, no se hace nada.

jueves, 4 de marzo de 2021

Cuadro de diálogo con casilla de verificación.

Desde la versión 1.8 de Lazarus, TTaskDialog está disponible en la pestaña o lengüeta de Dialogs. Se trata de un componente no visual.


En este caso veremos como incluir un par de botones comunes como si y no y un checkbox, porque no encontré en ningún lado como averiguar el estado del checkbox, pues lo habitual es que tenga una propiedad checked del tipo boolean, digo, si es un botón checkbox... pero no, es un enumerado y hay que buscarlo en Flags, si está ahí entonces sería True, caso contrario, False. Más rebuscado imposible, pero funcionar, funciona.
Este componente tiene varias opciones y se puede hacer bastante con él, en todos los ejemplos que vi (pocos, por cierto) se lo crea con código, lo que implica no usar el inspector de objetos; en este ejemplo, por lo tanto, se usa el inspector de objetos.

En un formulario poner 2 botones, 1 memo y un TTaskDialog. Un botón para comenzar y otro para cerrar o salir del programa.

En el inspector de objetos marcar las siguientes opciones e insertar los textos en las propiedades correspondientes.

Nota: no sé si es un bug o una nueva característica de Lazarus 2.X pero ahora para que el texto ingresado en una propiedad se refleje en el componte, una vez escrito el texto, hay que presionar Enter... obvio que es un pequeño y molesto bug que será reportado como corresponde para beneficio de todos.

 

Si dejamos la propiedad VerificationText vacía (o escrita pero sin presionar [Enter] entonces la casilla de verificación no se mostrará en el cuadro de diálogo. También se podría establecer mediante código: TaskDialog1.VerificationText:='No volver a pausar.".

Esta propiedad es un enumerado y al no estar vacía incorpora al conjunto de enumerados Flags el elemento tfVerificationFlagChecked. Por ende para saber si el usuario marco dicha casilla se debe preguntar si ese elemento está contenido en el conjunto Flags de esta forma:

tfVerificationFlagChecked in TaskDialog1.Flags

El código completo:

{$mode objfpc}{$H+}

interface

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

type

  { TForm1 }

  TForm1 = class(TForm)
    Button1: TButton;
    Button2: TButton;
    Memo1: TMemo;
    TaskDialog1: TTaskDialog;
    procedure Button1Click(Sender: TObject);
    procedure Button2Click(Sender: TObject);
  private

  public

  end;

var
  Form1: TForm1;

implementation

{$R *.lfm}

{ TForm1 }

procedure TForm1.Button1Click(Sender: TObject);
var
  i:integer;
  SeguirPreguntando:Boolean=True; //Asociaremos con el checkbox
begin
  for i:=1 to 200 do
  begin
    if (i mod 10) = 0 then
      if SeguirPreguntando then
        if TaskDialog1.Execute then
          // Si presiona Sí o si está marcado el checkbox y se presionó Sí: la segunda condición
          // es necesaria porque si no, si se marca el checkbox y se presiona no produce resultados
          // no deasedos.
          if (TaskDialog1.ModalResult=mrYes) or ((tfVerificationFlagChecked in TaskDialog1.Flags) and (TaskDialog1.ModalResult=mrYes) )then
            SeguirPreguntando:=not (tfVerificationFlagChecked in TaskDialog1.Flags)
          else
            Break; // Se aborta el ciclo, por ende es imposible continuarlo, ya que no se interrumpe sino que
                   // directamente se aborta el for do.
    Memo1.Lines.Add(i.ToString);
    Application.ProcessMessages;
    Sleep(50);
  end;
end;

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

end. 

Proyecto completo en GitLab

Documentación oficial de TTaskDialog

TTaskDialog en la Wiki


martes, 19 de enero de 2021

El selector Case.

El selector Case.

El poder de Case of en Pascal es muy grande, especialmente si se lo compara con cierto lenguaje. Por ejemplo permite rangos, valores separados por comas y una vez encontrada la coincidencia se sale del case, es decir, no se escribe un Break para finalizar cada sentencia.
Case trabaja con tipos de datos ordinales: enteros, enumerados, caracteres y cadenas. Los selectores deben ser todos del mismo tipo y literales (constantes) ya que se evalúan en tiempo de compilación.

Ejemplos:

const
  num1=10;
var
  num2:integer;
  i:integer;
...
i:=20;
Case i of
  num1 : writeLn('Es el número 10'); // Si i vale 10 se ejecuta esta sentencia y se sale del Case.
  num2 : writeLn('Es un entero'); // Error
  num1+10 : writeLn('Es el número 20');
else
   writeLn('Es otro número');
   writeLn('Pero no es el 10');
end;

Num2 es inválido porque no puede determinarse el valor de Num2 durante la compilación.
Num1 + 10 sí es válido ya que 10 + 10 = 20 es una expresión que se determina durante la compilación.
Else: también puede usarse Otherwise, es lo mismo, pero suele utilizarse Else. Nótese que no requiere begin .. end, no obstante se puede utilizar. Especificar Else no es obligatorio, si no encuentra el valor, simplemente se continúa con la siguiente sentencia del programa.

var
  c:Char;
...
case c of
  'a' : WriteLn('c es a');
  'b' : WriteLn('c es a');
  'c' : WriteLn('c es a');
  'a' : WriteLn('c es a'); // Error
end;

Error: no se pueden duplicar los selectores, aunque es algo más que obvio.

var
 s:String;
...
case s of
  'azul', 'rojo' : WriteLn('Son colores');
  'Debian', 'Linux Mint', 'Ubuntu' : WriteLn('Son sistemas operativos');
else
  WriteLn('Es otra cosa.');
end;

Este ejemplo no tiene errores.

var
  i,a:integer;
...
Case i of
  0 : begin
        WriteLn('El número es cero');
        a:=i+1;
      end;
  1..99 : WriteLn('Es un número de 2 dígitos');
  100, 101, 102..999 : WriteLn('Número de 3 dígitos');
else
  WriteLn('Mayor o igual a 1000');
end;

Desde ya, se pueden usar rangos y como vemos, en un mismo selector se pueden especificar varios valores. No es obligatorio que los valores estén ordenados, pero es una buena práctica al tratarse de enteros, pues facilita la lectura del código.

Otro ejemplo con Char:

var
  c:Char;
...
Case c of
  'A..Z', 'a..z' : WriteLn('Es una letra');
  '0..9' : WriteLn('Es un número');
  1..2 : WriteLn('E'); //Error
else
  WriteLn('No es ni letra ni número');
end;

Como vemos los rangos también se pueden utilizar con caracteres. El error se daría en 1..2 porque son enteros, no caracteres.

Para finalizar, un ejemplo con enumerados:

type
 TDia=(Lunes, Martes, Miercoles, Jueves, Viernes, Sabado, Domingo);
var
  Dia:TDia;
...
Case Dia of
  Lunes..Viernes : WriteLn('Es un día laborable');
  Sabado         : WriteLn('A veces los sábados se trabaja');
  Domingo        : WriteLn('Es feriado o no laborable');
end;

El siguiente código lo utilizo en el programa Marcadores:

type
  TBuscar=(Duplicados,Errores,Error0,Error400,Error500,Redirect,OK200,Todos);
...
var
  QueBuscar:TBuscar;
...
function TFSeleccionar.CargarGrid: Boolean;
var
  i,f:Integer;
begin
  f:=0;
  for i:=Low(aReg) to High(aReg) do
  begin
    case QueBuscar of
      Todos : begin
                if ((aReg[i].chequear) and (not(aReg[i].Eliminar))) then
                 begin
                   Inc(f);
                   SGrid.InsertRowWithValues(f,['','','','']);
                   if aReg[i].borrar then SGrid.Cells[0,f]:='1' else SGrid.Cells[0,f]:='0';
                   SGrid.Cells[1,f]:=IntToStr(aReg[i].indice);
                   SGrid.Cells[2,f]:=aReg[i].URL;
                   SGrid.Cells[3,f]:=IntToStr(aReg[i].statuscode);
                   SGrid.Cells[4,f]:=IntToStr(aReg[i].Redirect);
                   SGrid.Cells[5,f]:=IntToStr(i);
                 end;
              end;
      Errores : begin
                  if ((aReg[i].chequear)  and (not(aReg[i].Eliminar)) and ((aReg[i].statuscode=0) or (aReg[i].statuscode>=400))) then
                  begin
                    Inc(f);
                    SGrid.InsertRowWithValues(f,['','','','']);
                    if aReg[i].borrar then SGrid.Cells[0,f]:='1' else SGrid.Cells[0,f]:='0';
                    SGrid.Cells[1,f]:=IntToStr(aReg[i].indice);
                    SGrid.Cells[2,f]:=aReg[i].URL;
                    SGrid.Cells[3,f]:=IntToStr(aReg[i].statuscode);
                    SGrid.Cells[4,f]:=IntToStr(aReg[i].Redirect);
                    SGrid.Cells[5,f]:=IntToStr(i);
                  end;
                end;
      Duplicados : begin
                     if ((aReg[i].chequear) and (not(aReg[i].Eliminar))) then
                       begin
                         Inc(f);
                         SGrid.InsertRowWithValues(f,['','','','']);
                         SGrid.Cells[0,f]:='0';
                         SGrid.Cells[1,f]:=IntToStr(aReg[i].indice);
                         SGrid.Cells[2,f]:=aReg[i].URL;
                         SGrid.Cells[3,f]:=IntToStr(aReg[i].statuscode);
                         SGrid.Cells[4,f]:=IntToStr(aReg[i].Redirect);
                         SGrid.Cells[5,f]:=IntToStr(i);
                       end;
                     end;
    end;
  end;
  Result:=SGrid.RowCount>0;
end;

domingo, 17 de enero de 2021

Los procedimientos Break y Continue.

Break: sirve para salir de un bucle for, while o repeat y por ende solo deberá utilizarse únicamente dentro de estos bucles, caso contrario el compilador marcará el error. Actualmente, su uso, se considera una mala práctica de programación, al igual que, por ejemplo, while TRUE, sin embargo, podemos observar un hermoso ejemplar de while true en el código fuente del comando dd, pero usando el correspondiente break, desde ya. 


 

Siempre hay debates respecto de su uso, pienso que no hay ninguna problema en emplear Break para salir de un bucle infinito siempre y cuando estemos 100% seguros de que se llegará al Break "a salvo". El hecho de evitar esta práctica utilizando un condicional tampoco garantiza que no se salga nunca del bucle. Si utilizamos while x<z do y x siempre es menor z estamos en la misma situación.

Por ejemplo, esto no termina nunca:

var
  i:integer;
begin
  while TRUE do
  begin
    Inc(i);
    writeln(i);
  end;
  writeln('Fin.'); //Esta sentencia no se ejecutará nunca y el programa se colgará.
end;

En cambio

var
  i:integer;
begin
  i:=0;
  while TRUE do
  begin
    Inc(i);
    if i>100 then BREAK; // Se ejecuta la siguiente sentencia fuera del While do: writeln('Fin.');
    writeln(i); // Cuando i llegue a valer 101 esta sentencia no se ejecutará.
  end;
  writeln('Fin.');
end;

finaliza cuando i vale 101. Claro que optaría por:

var
  i:integer;
begin
  i:=0;
  while i<101 do
  begin
    Inc(i);
    writeln(i);
  end;
  writeln('Fin.');
end;

mejor legibilidad y no utilizo while TRUE, que solo lo implementaría en casos muy especiales y en lo posible, nunca.

Continue: con este procedimiento se logra que se procese la siguiente iteración sin finalizar la actual, ignorando todas las sentencias posteriores a Continue (siempre dentro del bucle). Al igual que Break, solo debe utilizarse en bucles for to, while do y repeat until. A diferencia de Break, no hay ningún riesgo extra de bucle infinito, es decir, todo bucle while y repeat a veces tiene ese riesgo, no solo el while True do.

var
  i:integer;
begin
  for i:=1 to 100 do
  begin
    if (i mod 2) = 0 then CONTINUE;
    writeln(i); // Cuando i es par esta sentencia no se ejecuta.
  end;
  writeln('Fin.');
end;

Debido a que no es muy habitual la utilización de estos procedimientos, opto por escribirlos en mayúscula para que destaquen.

jueves, 17 de diciembre de 2020

Los bloques Initialization y Finalization

Tanto initialization y finalization son palabras reservas y se utilizan como identificadores de los bloques de inicialización y finalización de una unidad. Si bien estas secciones de la unidad son opcionales y deben ubicarse al final de la misma, el bloque initialization es el primero en ejecutarse y en contraparte, finalization, es el último. Son los bloques olvidados de Pascal, la OOP o PPO han hecho que ya no se utilice casi nunca, pero su utilidad sigue vigente en algunos casos como veremos en unos ejemplos.

Estos bloques pueden utilizarse en conjunto o solo uno de ellos.

No se usa ni begin ni end para definir el comienzo y el fin de estos bloques, aunque puede utilizarse, es opcional. No confundir con en end. (end punto) que marca el final de la unidad.

Estos bloques se usan casi exclusivamente en unidades simples.

En el componente de Lazarus llamado Online Package Manager (OPM) o Gestor de Paquetes en Línea, en la unidad opkman_VTLogger veremos un ejemplo de su uso:

initialization
  Logger:=TLCLLogger.Create;
finalization
  Logger.Free;
end.

En la unidad DCConvertEncoding del populat programa Double Commander:

procedure Initialize;
begin
  //aquí hay código que es irrelevante para el ejemplo
end;

{$ENDIF}

initialization
  {$IF DEFINED(FPC_HAS_CPSTRING)}
  FileSystemCodePage:= WideStringManager.GetStandardCodePageProc(scpFileSystemSingleByte);
  {$ENDIF}
  Initialize;

end.

Declara un procedimiento llamado Initialize y lo llama al final del bloque Initialization.

Recordemos que el end seguido de un punto indica el fin de la unidad y nada tiene que ver con los bloques Initialization y Finalization.

El siguiente ejemplo lo utilizo en bastante en mis programas:

initialization
  ARCHIVO_OPCIONES:=Application.Location+'opciones.bin';
  ARCHIVO_CARPETAS:=Application.Location+'carpetas.txt';
  ARCHIVO_NOBORRAR:=Application.Location+'noborrar.txt';
  CARPETA_COPIAS:=Application.Location+'copias';

finalization; //Esto sobra pero si lo dejamos no pasa nada.

end.

Aunque están en mayúsculas son variables que utilizo como si fueran constantes, por eso las escribo así.

Para más información (en inglés) puede leerse la documentación oficial de Free Pascal de unit.

sábado, 28 de noviembre de 2020

Ejecutar programas externos con TProcess

Para empezar hay que tener claro algo: TProcess no es un emulador de terminal.

TProcess es una clase utilizada para ejecutar y controlar otros procesos ajenos a nuestro programa.

La mayoría de la veces se usa para ejecutar comandos (que son programas) que se ejecutan desde un emulador de terminal, pero se pueden ejecutar también programas con entorno gráfico, es decir, cualquier clase de programas, incluso aquellos que requieren permiso de administrador, como veremos en el ejemplo.

TProcess es un componente "no visual" que está en la pestaña o lengüeta "System" de la barra de componentes de Lazarus. Esta clase se halla definida en la unidad process y forma parte de la FCL (Free Component Library).

function TFmain.LoadList: Boolean;
var
  proc:TProcess;
begin
  if Assigned(dmiList) then dmiList.Free;
  dmiList:=TStringList.Create;
  proc:=TProcess.Create(nil);
  proc.CommandLine:='pkexec dmidecode';
  proc.Options:=proc.Options+[poWaitOnExit, poUsePipes];
  proc.Execute;
  dmiList.LoadFromStream(proc.Output);
  proc.Free;
  Result:=dmiList.Count>0;
end;

Esta función es del programa LazDMIDecode y carga una lista con el resultado del comando dmidecode, que requiere contraseña de administrador y es solicitada al usuario anteponiendo el comando pkexec al comando dmidecode. De esta manera el programa nunca sabrá la password y el cuadro de diálogo que la solicita es el nativo del sistema operativo.

Las opciones, desde ya, deben definirse antes de llamar al proceso Execute o seleccionarlas desde el inspector de objetos si se utilizó como componente en un formulario o similar.

La opción poWaitOnExit indica que debe esperar a que finalice el proceso antes de pasar a la instrucción siguiente a proc.Execute.

La opción poUsePipes es para capturar el resultado que se almacena en la propiedad Output de TProcess en un stream. La propiedad Output solo debe utilizarse conjuntamente con la opción poUsePipes y solo si el comando invocado devuelve un resultado, caso contrario ocurrirá un error.

Para finalizar se asigna el stream de proc.Output al stringlist y luego se libera la instancia de TProcess si utilizó como en este ejemplo creándola mediante el constructor de la clase.

Documentación de TProcess: