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:

martes, 24 de noviembre de 2020

TActionList

Es una clase muy útil para manejar y asociar manejadores de eventos; de hecho, debería utilizarse siempre, aunque no tengamos dos componentes que ejecuten la misma acción y utilizar el famoso evento OnClik solo para referenciar a la acción de la lista e incluso que la acción de la lista sea llamar a un procedimiento o función que pueda ser miembro público o privado del Form o incluso de otra unidad.

Veamos el famoso ejemplo de cuando se tienen un menú, una barra y unos botones para realizar lo mismo.


Ingredientes:

  • TMainMenu
  • 3 TMenuItem
  • TToolBar
  • 3 TToolButton
  • 3 TButton
  • TActionList

Los ítems del menú Archivos serán: Abrir, Guardar y Cerrar, lo mismo para los botones de la barra y los botones del formulario.

Para abrir el editor de la lista de acciones TActionList1 se hace doble click sobre el mismo. Con [Insert] o haciendo click en "+" y en "Nueva acción", no en "Nueva acción estándar" que es otra cosa. Escribimos el nombre "Abrir" y lo mismo para las otras dos acciones: Guardar y Cerrar.


Debe de quedar así:


Ahora desde el inspector de objetos definimos los eventos de cada acción:


Haciendo click en los "..." se define solo.

Recordemos que estamos hablando de eventos, no podemos asignar a OnClick (por ejemplo) un procedimiento regular, debe ser un evento, por eso usamos la lista de acciones.

Además nuestro programa queda bien factorizado, mejor lectura y mantenimiento.

A cada TAction le definimos Caption y Name.



Todavía no escribimos nada de código, sigamos.

Desde el inspector de objetos, asociamos el evento OnClick de cada componente con la acción respectiva.


Otra opción es asignar la TAction justamente en el evento Action, o solo en OnClick o también en ambas. Para el ejemplo con OnClick solamente está bien.

En este caso la acción será solo un showmessage, pero en lugar de incluir el mismo en la acción, creamos un procedimiento privado en el formulario y en la acción llamamos al mismo. Siempre es mejor tener el menor código posible en los manejadores de eventos, además de esta forma podemos utilizar el procedimiento, llegado el caso, desde cualquier lado, incluso tenerlos en otra unidad.

El código:

unit Unit1;

{$mode objfpc}{$H+}

interface

uses
  Classes, SysUtils, Forms, Controls, Graphics, Dialogs, Menus, ComCtrls,
  StdCtrls, ActnList;

type

  { TForm1 }

  TForm1 = class(TForm)
    aAbrir: TAction;
    aCerrar: TAction;
    aGaurdar: TAction;
    ActionList1: TActionList;
    bAbrir: TButton;
    bGuardar: TButton;
    bCerrar: TButton;
    mnAbrir: TMenuItem;
    mnGuardar: TMenuItem;
    mnCerrar: TMenuItem;
    mnMenu: TMainMenu;
    mnArchivos: TMenuItem;
    tbBarrar: TToolBar;
    tbAbrir: TToolButton;
    tbGuardar: TToolButton;
    tbCerrar: TToolButton;
    procedure aAbrirExecute(Sender: TObject);
    procedure aCerrarExecute(Sender: TObject);
    procedure aGaurdarExecute(Sender: TObject);
  private
    procedure Abrir;
    procedure Guardar;
    procedure Cerrar;
  public

  end;

var
  Form1: TForm1;

implementation

{$R *.lfm}

{ TForm1 }

procedure TForm1.aAbrirExecute(Sender: TObject);
begin
  Abrir;
end;

procedure TForm1.aCerrarExecute(Sender: TObject);
begin
  Cerrar;
end;

procedure TForm1.aGaurdarExecute(Sender: TObject);
begin
  Guardar;
end;

procedure TForm1.Abrir;
begin
  ShowMessage('Procedimiento Abrir archivo');
end;

procedure TForm1.Guardar;
begin
  ShowMessage('Procedimiento Guardar archivo');
end;

procedure TForm1.Cerrar;
begin
  ShowMessage('Procedimiento Cerrar archivo');
end;

end. 

Este es el uso clásico de la clase TActionList, pero la misma posee muchas más opciones. Además nos ahorramos tener en el código los eventos OnClick de cada componente y nos pudimos ahorrar también los procedimientos colocando ese código en el evento Execute de cada TAction.

Descargar proyecto.

domingo, 8 de noviembre de 2020

TZQuery.ExecSQL

Actualización 26-12-2020: downgrade de Zeos 7.2.6 (también probé la 7.2.8) a Zeos 7.1.3.a stable del año del jopo.

No hace mucho actualicé tanto el IDE como el compilador, algunos componentes que forman parte de Lazarus se actualizan y otros, como ZeosLib no. Años usando la versión estable 7.1.3 (si no me equivoco) ya que las primeras versiones de la 7.2 me tiraba errores por todos lados. Finalmente decidí ir por la 7.2.6 y al principio iba todo bien, pero un sistema de los primeros que hice, hace ya más de 3 años, y sin las mejores técnicas de programación precisamente, me pidieron una modificación, nada del otro mundo y ahí empezaron los problemas con el querido Zeos. La documentación de los cambios de Zeos no es la mejor del mundo.

En dicho programa, tenía mucho código que actualizaba la base de datos con TZQuery en lugar de TZConnection.ExecuteDirect. Motivo: se pueden utilizar parámetros, me resulta más cómodo. Por ejemplo tenía códigos de este tipo:

ZQTHab.SQL.Text:='DELETE FROM tashaber;';
ZQTHab.Open;
ZQTHab.Close; 

Funcionaba sin problemas.

Ahora arroja un error: "Can not open a Resultset." 

 



Se soluciona con TZQuery.ExecSQL que la verdad no sé si es un método nuevo o si siempre existió.

ZQTHab.SQL.Text:='DELETE FROM tashaber;';
ZQTHab.ExecSQL; 

Lo bueno es que no se necesita ni abrir ni cerrar la consulta, más legible.

También me saltaron errores de base de datos bloqueada y tuve problemas con la edición e inserción de registros. Pues bien, hoy salió una nueva versión de ZeosLib, la 7.2.8 que no corrige esos bugs, pero anuncia que los mismos serán corregidos en la versión 8.0.

"When using Cached Updates, it is not possible to add a row and then edit
that row before posting to the database. This bug cannot be fixed in Zeos 7.2.
It has been fixed in the upcoming Zeos 8.0. Please use database transactions
instead.".

Respecto de la base de datos bloqueada, era cuando usaba una segunda conexión a la base de datos, tuve que eliminar esa segunda conexión. El tema sin solución es que el programa se usa en red y cuando dos o más usuarios se conectan a la base de datos se bloquea. Además era una de las gracias de utilizar el conector de Zeos con la propiedad Autocommit en true y funcionaba de maravilla.

Deberé, otra vez, regresar a la versión 7.1.3 donde todo funcionaba bien.

sábado, 8 de agosto de 2020

Guardar y leer un array of integer en un archivo.

Un vector de números enteros lo podemos también guardar en un archivo de texto, al fin y al cabo no hay que complicarse con la separación decimal y con un IntToStr para guardar y un StrToInt para leer, no debería de haber ninguna complicación, en más, dependiendo de lo que se busque, hasta tiene la ventaja de poder leerse y editar con un editor de texto plano, claro que también esto puede se una desventaja.

En Pascal podemos manejar archivos de varias formas, a su vez los archivos pueden ser de texto o binarios. Dentro del tipo de los binario tenemos binario a secas o de algún tipo, como por ejemplo, de enteros, de registros, etc.

En el ejemplo se usa el vector1 del tipo dinámico (también se puede usar uno estático) al cual se le establece un rango de 100 elementos, al ser dinámico se basa en cero, por lo tanto su índice inicial es el 0 y el final el 99; a este array se le cargan número enteros aleatorios hasta el 5.000 (se pude cambiar por cualquier otro valor). Luego se lo guarda en un archivo, se muestra el contenido en Memo1.

El vector2 se usa para leer el archivo de enteros. ¿Era necesario usar 2 vectores? No, se podría usar uno solo, pero pienso que se visualiza mejor el ejemplo. El vector2 se muestra en el Memo2 para comparar el resultado obtenido.

Lo principal:

  • Para crear/cargar el archivo usamos: file of Integer.
  • Para grabar el array abrimos/creamos el archivo con Rewrite que si el archivo existe lo sobre escribe. Recorremos el vector de la forma clásica y grabamos cada elemento del mismo en el archivo mediante Write.
  • Para leer el archivo lógicamente también necesitamos definir una variable del tipo File Of Integer, abrimos el archivo mediante Reset que es de solo lectura y con un ciclo for to lo leemos y cargamos en vector2 mediante Read.
  • Siempre debe cerrarse el archivo usando CloseFile.

unit Unit1;

{$mode objfpc}{$H+}

interface

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

type

  { TForm1 }

  TForm1 = class(TForm)
    BCargarArray: TBitBtn;
    BGuardarArray: TBitBtn;
    BLeerArray: TButton;
    Edit1: TEdit;
    Memo1: TMemo;
    Memo2: TMemo;
    procedure BCargarArrayClick(Sender: TObject);
    procedure BGuardarArrayClick(Sender: TObject);
    procedure BLeerArrayClick(Sender: TObject);
    procedure FormClose(Sender: TObject; var CloseAction: TCloseAction);
    procedure FormCreate(Sender: TObject);
  private
    procedure ArrayToFile(aArray:array of integer; aNameFile:String);
    procedure ArrayFromFile(aArray:array of integer; aNameFile:String);
  public

  end;

var
  Form1: TForm1;
  vector1, vector2:array of integer;

implementation

{$R *.lfm}

{ TForm1 }

procedure TForm1.FormCreate(Sender: TObject);
begin
  Randomize;
  SetLength(vector1,100);
  SetLength(vector2,100);
end;

procedure TForm1.BGuardarArrayClick(Sender: TObject);
begin
  ArrayToFile(vector1,Edit1.Text);
end;

procedure TForm1.BLeerArrayClick(Sender: TObject);
begin
  ArrayFromFile(vector2,Edit1.Text);
end;

procedure TForm1.ArrayToFile(aArray: array of integer; aNameFile: String);
var
  i:Integer;
  aFile:file of Integer;
begin
  AssignFile(aFile,aNameFile);
  Rewrite(aFile);
  for i:=Low(aArray) to High(aArray) do
    Write(aFile,aArray[i]);
  CloseFile(aFile);
end;

procedure TForm1.ArrayFromFile(aArray: array of integer; aNameFile: String);
var
  i:Integer;
  aFile:file of Integer;
begin
  Memo2.Clear;
  AssignFile(aFile,aNameFile);
  Reset(aFile);
  for i:=1 to 100 do
    begin
      Read(aFile,aArray[i-1]);
      Memo2.Lines.Add((IntToStr(aArray[i-1])));
    end;
  CloseFile(aFile);
end;

procedure TForm1.BCargarArrayClick(Sender: TObject);
var
  i:Integer;
begin
  Memo1.Clear;
  for i:=Low(vector1) to High(vector1) do
    begin
      vector1[i]:=i+Random(5000);
      Memo1.Lines.Add(IntToStr(vector1[i]));
    end;
end;

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

end.

Descargar el proyecto.

jueves, 20 de febrero de 2020

TBitButton: cambiar imagen.

Se trata de cambiar la imagen de un botón TBitButton en tiempo de ejecución (o mediante código). En este caso las imágenes las obtenemos de TImageList y, como ejemplo, algo básico como un botón para ocultar o mostrar.


Al ejecutar el programa se tiene que ver algo así, un TMemo, que será el elemento del formulario a mostrar y ocultar, y un TBitButton que cambiará la imagen y el texto cada vez que se presione.

unit Unit1;

{$mode objfpc}{$H+}

interface

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

type

{ TForm1 }

TForm1 = class(TForm)
  BitBtn1: TBitBtn;
  ImageList1: TImageList;
  Memo1: TMemo;
  procedure BitBtn1Click(Sender: TObject);
  procedure FormCreate(Sender: TObject);
private
  FOcultar:Boolean;
end;

var
  Form1: TForm1;

implementation

{$R *.lfm}

{ TForm1 }

procedure TForm1.FormCreate(Sender: TObject);
begin
  FOcultar:=True;
  ImageList1.GetBitmap(0,BitBtn1.Glyph);
end;

procedure TForm1.BitBtn1Click(Sender: TObject);
begin
  if FOcultar then
  begin
    ImageList1.GetBitmap(1,BitBtn1.Glyph);
    BitBtn1.Caption:='Mostrar';
    Memo1.Visible:=False;
  end
 else
 begin
   ImageList1.GetBitmap(0,BitBtn1.Glyph);
   BitBtn1.Caption:='Ocultar';
   Memo1.Visible:=True;
  end;
  FOcultar:=not(FOcultar);
end;

end.


En la variable FOcultar almacenamos el estado del botón.
En el evento FormCreate le asignamos el valor True a FOCultar y le asignamos la imagen correspondiente, en este caso la misma es la primera de TImageList que por cierto, también debemos incluir en el Form.
ImageList1.GetBitmap(0,BitBtn1.Glyph);
Para asignar la primera imagen de la lista, utilizamos el procedimiento GetBitmap de TImageList: el primer parámetro corresponde al índice de la imagen, el segundo, a la imagen destino, en este caso BitBtn1.Glyph.
Porque TBitBtn.Glyph:TBitmap = class(TFPImageBitmap).
El método (procedimiento) GetBitmap pertenece a la clase TCustomImageList:

TCustomImageList.GetBitmap(Index: Integer; Image: TCustomBitmap);

En este ejemplo también cambiamos el texto del botón.
Finalmente cambiamos el valor de FOcultar, sino, no pasa nada.

Si bien es algo sencillo, dejo el código fuente del proyecto: descargar.