miércoles, 6 de septiembre de 2017

Temporizador TTimer

El componente TTimer es un temporizador con un intervalo mínimo de un milisegundo aproximado aunque es recomendable establecer un intervalo mínimo de 10 milisegundos para que se aproxime más a la realidad, si el intervalo se establece en 100 o 1000 entonces se obtendrá un mejor resultado. Free Pascal cuenta con varios temporizadores, siendo TTimer el más simple y limitado. Para medir con exactitud es recomendable crear un cronómetro propio en base a la hora provista por el sistema operativo y calcular en base a la diferencia entre finalización e inicio. Pero si lo que buscamos no requiere de mayor precisión que la de un segundo o décima de segundo, entonces Timer es el indicado.

Los ingredientes necesarios para seguir el ejemplo son, además del Form:
  • 1 TTimer
  • 1 TLabel
  • 3 TButton
  • 1 variable integer (privada o pública)

El componente TTimer se encuentra en la paleta System.

Por defecto el Timer viene activado, para este ejemplo, desde el inspector de objetos lo desactivamos.


De paso definimos el intervalo en 100 para obtener una medición lo más real posible, claro que después se puede jugar y poner en 1 a ver qué pasa. Pasará que en 10 segundos medirá 6 o como mucho 7; si lo ponemos en 10, en 10 segundos medirá 8-9 aprox. y en 1000 el margen de “error” será de aprox. 1% o incluso menos.


Sí, el contador en verde, bold y agrandado, porque todo en escala de grises es simplemente aburrido.

Pasemos al código:

unit Unit1;

{$mode objfpc}{$H+}

interface

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

type

  { TForm1 }

  TForm1 = class(TForm)
    btnComenzar: TButton;
    btnDetener: TButton;
    btnVolverACero: TButton;
    lblContador: TLabel;
    Timer1: TTimer;
    procedure btnComenzarClick(Sender: TObject);
    procedure btnDetenerClick(Sender: TObject);
    procedure btnVolverACeroClick(Sender: TObject);
    procedure Timer1Timer(Sender: TObject);
    private
      contador:Integer;
    { private declarations }
    public
    { public declarations }
 end;

var
  Form1: TForm1;

implementation

{$R *.lfm}

{ TForm1 }

procedure TForm1.Timer1Timer(Sender: TObject);
begin
  contador:=contador+1;
  lblContador.Caption:=IntToStr(contador);
end;

procedure TForm1.btnComenzarClick(Sender: TObject);
begin
  Timer1.Enabled:=True;
end;

procedure TForm1.btnDetenerClick(Sender: TObject);
begin
  Timer1.Enabled:=False;
end;

procedure TForm1.btnVolverACeroClick(Sender: TObject);
begin
  contador:=0;
  lblContador.Caption:=IntToStr(contador);
end;

end.


El resultado:

Obtener la lista de tablas de una base de datos SQL

Es algo muy simple desde consola o en tiempo de diseño, pero en tiempo de ejecución? También es sencillo, lo difícil fue encontrar cómo hacerlo. Resulta que Zeos (ZeosLib), más precisamente su principal componente, ZConnection, posee un procedimiento llamado GetTableNames que nos devuelve un parámetro del tipo TStrings pasado como referencia (lógico, si no fuese pasado como referencia no devolvería nada) con la lista de todas las tablas de la base de datos conectada. Se le puede pasar un TStringList o un ListBox.items por ejemplo.

 Primer ejemplo con TStringList y un TMemo:

 // Definir procedimiento o función o agregar el código donde sea necesario
var
  listatablas:TStringList;
  i:Integer;        
begin
  listatablas:=TStringList.Create; 
  ZConnection1.GetTableNames('',listatablas);   // La base de datos debe estar conectada...
  for i:=0 to listatablas.Count-1 do
    Memo1.Lines.Add(listatablas[i]);     // Memo1 debe estar en el Form
  ...
end;


El primer parámetro pasado podría haber sido por ejemplo ‘c*’ en cuyo caso hubiésemos obtenido la lista de tablas cuyos nombres comiencen con c. Al pasarlo vacío le indicamos que llene el StringList con todas las tablas.

Este ejemplo, con muy poco código, alcanza y sobra para mostrar los nombres de las tablas, pero si queremos que además el usuario pueda elegir una (o varias) tablas podemos valernos de un ListBox. Para ello, no le pasaremos como parámetro el ListBox sino ListBox.Items que es del tipo TStringList.

ZConnection1.GetTableNames('',ListBox1.Items) // Requiere un ListBox en el Form
 

DBGrid: seleccionar una columna

El componente DBGrid carece de una opción para marcar una columna de la forma en que lo hacemos con una fila con dgRowSelect, es decir, no tenemos la opción dgColSelect. Lo cual no significa que no podamos hacerlo, de hecho el código es bastante simple:

procedure TForm1.DBGrid1TitleClick(Column: TColumn);
begin
  DBGrid1.Columns[Column.Index].Color := clHighlight;
  DBGrid1.Columns[column.Index].Title.Color := clHighlight;
end;


Basta con dos líneas de código para el evento TitleClick que nos envía parámetro Column del tipo TColumn, esto nos facilita todo, pues usamos la propiedad Index para saber sobre que columna (el itulo  de la columna) se hizo el click y le cambiamos el color tanto al título como a las celdas, según se desee.

Como simular un click







Por ejemplo, un click en un botón que tiene definido el evento OnClick que a su vez pertenece a una clase que generalmente es un formulario. No puede llamarse como si fuese un procedimiento o una función, si bien está declarado como un procedimiento, es un evento.

Si tengo declarado este evento en el típico Form1:

procedure btnCerrarDBClick (Sender: TObject); 

se puede invocar de la siguiente  manera:

btnCerrarDBClick (self);

Formas incorrectas y que por lo tanto arrojarán errores de compilación:

Form1.btnCerrarDBClick;

btnCerrarDBClick;

Pausar un programa con Delay y Sleep

Delay es un procedimiento de Free Pascal disponible en la unidad Crt que permite pausar el programa en x milisegundos.

Es sumamente útil cuando queremos mostrar un mensaje al usuario y que lo lea antes de hacer click casi por acto reflejo sin leer.

Para utilizarla simplemente escribimos

Delay (5000); //Pausa de 5 segundos

El parámetro que recibe el procedimiento es del tipo Word, por ende los valores deben estar dentro del rango 0 .. 65535. Esto nos da aproximadamente un minuto, ya que el tiempo es aproximado. Si se necesita más tiempo, no es necesario hacer varias llamadas a Delay dentro de un loop, sino que contamos con un procedimiento similar llamado Sleep que recibe como parámetro por valor del tipo Cardinal que es a su vez del tipo LongWord cuyo rango es 0 .. 4.294.967.295 lo cual es suficiente como para poner en coma el programa por casi 50 días.

Sleep (4294967000); // lo duerme por 50 días

Sin olvidarse de agregar Crt en uses para el caso de utilizar Delay o sysutils en caso de hacer uso de Sleep.

Documentación oficial de Delay.

Documentación oficial de Sleep.

martes, 5 de septiembre de 2017

Volver a cero el rowid de SQLite

Como reiniciar o “resetear” el rowid de una tabla en SQLite? Muchas veces definimos el famoso campo ID como integer, primary key, autoincrement, unique para que SQLite se encargue de él y lo hace, guardando el valor del último registro y cada vez que agregamos uno toma esa valor y lo incrementa en uno. Es por eso que si tenemos 10 registros con ID del 1 al 10 y borramos 5 y luego insertamos uno, el valor de este último será 11 y no 6. Si tememos una tabla con 500 filas y las borramos todas, el próximo registro, el ID será 501, si lo que buscamos es que sea 1 y no 501, hay una solución, luego de borrar toda la tabla ejecutamos el siguiente comando:

DELETE FROM SQLITE_SEQUENCE WHERE name='tutabla';

Ejemplo: borrar la tabla cuentas

DELETE FROM cuentas; DELETE FROM SQLITE_SEQUENCE WHERE name='cuentas';

Si devuelve el siguiente error: No such table ‘SQLITE_SEQUENCE’ se debe a que en el schema no está definido, sqlite_sequence se crea al definir una columna como integer, primary key, autoincremental, unique.

Es muy útil para tablas que usamos como temporales, no del tipo temporal que es distinto.

Otro ejemplo con código Free Pascal:

dmrc.ZQtemp.SQL.Text:='DELETE FROM tasiento; DELETE FROM SQLITE_SEQUENCE WHERE name=''tasiento'';';

Lazarus problema con acentos: solución.

A veces puede pasar que de un día para el otro y sin recordar haber hecho ningún cambio en nuestro IDE ni en el sistema operativo, Lazarus no acentúa, dicho de otra manera, se come los acentos o los duplica en el inspector de objetos. Y el resto de los programas que tenemos instalados no reproducen este error, es solo Lazarus. Nada grave, se puede convivir con ello y seguir programando sin ningún problema, por ejemplo, escribiendo el caracter acentuado en un editor de texto plano como Gedit y copiándolo y pegándolo en el editor de código fuente del RAD Lazarus. Nada cómodo pero sirve mientras buscamos “algo” que nos devuelva los acentos.

La solución que describo está comprobada exitosamente en Lazarus 1.6 FPC 3.0.0 sobre Linux Mint 17.2 x64 MATE Gtk-2.

Hay que hacer un script, con un editor de texto plano:

#!/bin/sh
export GTK_IM_MODULE=gtk-im-context-simple
export QT_IM_MODULE=simple
export XMODIFIERS=@im=none
startlazarus %f

Lo guardamos donde nos resulte cómodo con la extensión .sh y lo marcamos como ejecutable.

Y de ahora en más, lanzaremos Lazarus desde ese script. Este método está documentado en la wiki de Free Pascal.


Crear tablas en SQLite con código Free Pascal

Como casi todo en programación, hay varias formas de hacer una misma tarea y ésta no es la excepción. Crearemos una base de datos y una tabla. También le agregaremos un registro a la tabla, todo con código Free Pascal desde Lazarus y utilizando un solo componente de Zeos Lib que no facilita las tareas con las bases de datos.

Ya he escrito que para crear una base de datos en SQLite solo hay que establecer la conexión, si la base de datos no existe, entonces SQLite la crea.
Solo necesitamos un TZConnection que lo soltaremos en el Form o en el Data Module.

Nota: al ZConnection1 en la propiedad protocol desde el inspector de objetos le seleccionamos sqlite3.

Este ejemplo consiste en una función que crea una base de datos, una tabla y un registro en la misma. Si todo salió bien se devuelve True, caso contrario, False.

function CrearEmpresas: Boolean;
var
  ret:Boolean;
begin
  ret:=True;
  if not FileExists('/home/programa1/empresas.db') then
  begin
    if ZConnection1.Connected then ZConnection1.Disconnect;
    try
      ZConnection1.Database:='/home/programa1/empresas.db';
      ZConnection1.Connect;
      ZConnection1.ExecuteDirect('CREATE TABLE IF NOT EXISTS empre ( '+
      'idempre INTEGER      PRIMARY KEY AUTOINCREMENT UNIQUE NOT NULL, '+
      'nombre  VARCHAR (40) NOT NULL, '+
      'cuit    VARCHAR (13) NOT NULL, '+
      'db      VARCHAR (40) NOT NULL, '+
      'dir     VARCHAR (80) NOT NULL );');
      ZConnection1.ExecuteDirect('INSERT INTO empre VALUES '+
      '(1,''DEMO EMPRESA S.A.'',''20-12345678-9'',''demodb.db'','+''''+CurrentDirectory+''');');
      ZConnection1.Disconnect;
    Except
      ret:=False;
    end;
    ZConnection1.Disconnect;
  end;
  CrearEmpresas:=ret;
end;

Para evitar errores, primero verificamos que el archivo no exista y luego también chequeamos que ZConnection1 no esté conectado, pues si lo está no nos sirve, dado que para estar conectado lo está a una base de datos, y necesitamos crear la base de datos lo cual sucede al momento de efectuar la conexión. Por eso, si Connected devuelve True, lo desconectamos. Lo que sigue lo “envolvemos” en un try / Except y si salta algún error la función retornará False.

Ya asegurándonos de que ZConnection1 está desconectado, le establecemos la base de datos con path completo aunque sin path creará la DB en el directorio de ejecución, por lo tanto puede obviarse el path y poner ‘empresas.db’ por ejemplo. Conectamos y en ese momento es cuando la DB es creada. Acto seguido, valiéndonos del método ExecuteDirect incluiremos el mismo código que escribiríamos en una terminal o consola para SQLite con la diferencia de que necesitamos el uso de comillas dobles en la inserción de datos. Con el primer ExecuteDirect creamos la tabla empresas.db si no existe, algo que sobra porque acabamos de crear la DB. Con el segundo, agregamos un registro, ahora llamado fila, lo cual es correcto pero no me gusta. Y ahí debemos utilizar las comillas dobles, pulsando dos veces la comilla simple, no utilizar nunca la doble. El 1 al tratase de un entero, no necesita comillas, es un valor numérico, el resto de los campo (ahora columnas) al ser de texto (VARCHAR) si necesita estar entre comillas simples, pero al estar ya dentro de un texto entre comillas, para indicarle al compilador que la comilla no finaliza el texto, en lugar de una comilla simple escribimos dos comillas simples, el dato, y nuevamente dos comillas simples. También se puede usar la función QuotedStr para evitar tantos apóstrofes.


Ahora pasemos a explicar esto:

‘ ‘demodb.db’ ‘,  ‘+ ‘ ‘ ‘ ‘ + CurrentDirectory + ‘ ‘ ‘ ) ; ‘ ) ;

corto la cadena de caracteres luego de la coma, la finalizo con la comilla simple y le concateno el directorio actual:

‘ ‘ ‘ ‘  la primer comilla inicia el texto y la última lo cierra, en medio dos comillas imprimen una comilla en la cadena.

+ para agregar el string que me devuelve CurrentDirectory, nuevamente + ‘ ‘ ‘  tres comillas, la primera para iniciar nuevamente un texto, la segunda y tercera para imprimir una comilla, luego ) ; ‘ esa última comilla cierra.

Lo que sigue es un Disconnect para cerrar la conexión con la base de datos.

Unidad sin formulario en Lazarus

Es algo tan simple que no figura en los resultados de búsqueda de los pocos buscadores de Internet que hay en la actualidad a nivel global.

Desde la IDE Lazarus en el menú Archivo, Nueva unidad.





Y nos aparecerá algo así:

unit Unit1;

{$mode objfpc}{$H+}

interface

uses
    Classes, SysUtils;

implementation

end.


En definitiva lo único que falta es el Form y el resto es igual, como no podía ser de otra manera.

Luego de uses declaramos las funciones y procedimientos y debajo de implementation, las escribimos.

Ideal para tener todas las funciones de, por ejemplo, validación, en una unidad a la cual luego podremos acceder desde otras unidades. Otra utilidad práctica es para declarar variables globales.



Crear, leer y guardar un archivo de texto plano.

Caso: crear un archivo de texto donde se guarde el directorio o carpeta seleccionada por el usuario. Consultar dicho archivo. El método funciona tanto en GNU/Linux como en Windows.

Crear y guardar:

procedure TForm1.btnSeleccionarClick(Sender: TObject);
var
  f:TFileStream;
  s:String;
begin
  if SelectDirectoryDialog1.Execute then
  begin
    if SelectDirectoryDialog1.FileName<>'' then
      EmpresasDBDir:=SelectDirectoryDialog1.FileName;
  end
  else
  begin
    ShowMessage('No se seleccionó ningún directorio. Se utilizará el actual: '+GetCurrentDir);
    EmpresasDBDir:=GetCurrentDir;
  end;
  f:=TFileStream.Create(GetCurrentDir+PathDelim+'empredir.txt',fmCreate);
  s:=EmpresasDBDir;
  f.Write(s[1],Length(s));
  f.Free;
end;

Este evento es lanzado cuando el usuario presionar el botón seleccionar directorio. Las variables utilizadas son:

f del tipo TFileStream, s del tipo string, ambas locales;

EmpresasDBDir del tipo string, global, definida en otra unidad del proyecto.

Además se utiliza el componente SelectDirectoryDialog disponible en la paleta Dialogs de Lazarus.

Si el usuario no seleccionó ningún directorio, es decir, se escapó haciendo click en Cancelar o cerró el cuadro de diálogo, entonces, en lugar de preguntarle para qué presionó el botón si no seleccionó nada, se le informa amablemente que se utilizará el directorio actual, se le muestra el mismo mediante GetCurrenteDir y utilizando la misma función, se asigna el directorio actual a la variable global EmpresasDBDir.

Luego creamos el objeto de la clase TFileStream y le pasamos dos parámetros, el primero de ellos, el nombre del archivo y el segundo la forma mediante la cual accedemos al archivo, en este caso fmCreate que significa que creamos un nuevo archivo, entendiendo que si el archivo ya existe, lo sobrescribe. Respecto del primer parámetro, es un string, el cual formamos con el directorio actual + el delimitador + el nombre del archivo, cuya extensión puede ser otra que no sea .txt. El uso de PathDelim es lo que permite que funcione tanto en Linux como en Windows y seguramente también en MacOS que al igual que GNU/Linux está basado en UNIX, ya que el separador o delimitador entre carpeta y archivo será / o \ según el sistema operativo.

La asignación del contenido de la variable EmpresasDBDir a la variable s no es necesario, se podría prescindir de la misma y utilizar directamente EmpresasDBDir, eso queda a gusto del programador teniendo en cuenta la legibilidad del código.

f.Write(s[1], Length(s)) se guardan desde el primero s[1] al último Length(s) caracter de la cadena de caracteres s y se finaliza liberando el objeto.

Si quisiéramos que el archivo conste de varias lineas, habría que agregar #13#10 a la variable s

s:=EmpresasDBDir+#13#10;

Pero para este caso luego al leer el archivo e intentar conectar una base de datos con ese path, nos dará error, porque el Enter (#13#10) no se ve pero está ahí.

Leer el archivo:

procedure TForm1.LeoDirEmpreDB;
var
  f:TFileStream;
  s:String;
begin
  f:=TFileStream.Create(GetCurrentDir+PathDelim+'empredir.txt', fmOpenRead );
  SetLength(s,f.Size);
  f.Read(s[1],F.Size);
  EmpresasDBDir:=s;
  f.Free;
end;

Para leer el archivo, en este caso no interviene el usuario de manera directa, lo realizo mediante un procedimiento y utlizando 2 variables locales f y s y la misma variable global EmpresasDBDir.

El método es parecido al de escritura, al crear el objeto, el primer parámetro es igual y el segundo no, con fmOpenRead, como su nombre lo indica, establecemos que solamente leeremos el archivo, en otras palabras: modo lectura. Paso siguiente, se debe establece el tamaño de s mediante SetLength le asignamos la misma cantidad de caracteres que contiene el archivo, si no hacemos, no funciona. Luego se realiza la lectura con el método Read del primer s[1] al último f.Size caracter. Lo leído lo asigno a la variable global y luego libero el objeto.

Se pueden hacer más cosas mediante el uso de TFileStream, como agregar datos a un fichero existente. También hay otros métodos para la lectura y escritura de archivos. Para más información recomiendo leer los siguiente enlaces:

Delphi al límite (en el cual me basé para este artículo).

FreePascal.org (Documentación oficial).

CuPas (Otro método, Pascal sin objetos).