miércoles, 20 de junio de 2018

LazReport EVariantError

Un error muy común: Invalid variant  type cast, esta excepción puede ser lanzada por LazReport cuando utilizamos variables en el reporte que las enviamos mediante eventos como el típico GetValue.

Resulta que si bien Free Pascal no distingue entre mayúsculas y minúsculas, el componente LazReport sí lo hace con las variables del reporte, motivo que está fuera del alcance de esta entrada. En resumen, el string que utilizamos en dicho evento o cualquier otro evento de LazReport que utilice el par  de valores parname y parvalue, en parname debemos respetar las minúsculas.

Ejemplo del error:


Motivo del error:


En LazReport la variable está escrita toda en minúscula.


Mientras que en el evento está escrita combinando mayúsculas y minúsculas.

Solución: simplemente escribir correctamente la variable definida en el reporte, es decir todo en minúsculas, en este caso se cambia 'FechaReporte' por 'fechareporte'.

domingo, 17 de junio de 2018

TDBGrid: ordenar por columnas.

Ordenar los datos de una consulta SQL mostrados en un DBGrid al hacer click en la columna. En este caso además de ordenar, pondremos en bold (negrita) el título de la columna que está ordenada. (Para algo más avanzado aquí)

En el inspector de objetos, en DBGrid, eventos, buscar: OnTitleClick


y generar el evento (procedimiento):

procedure TFdCtas.DBGridCtasTitleClick(Column: TColumn);
var
  i:Integer;
begin
  for i:=0 to DBGridCtas.Columns.Count-1 do DBGridCtas.Columns.Items[i].Title.Font.Style:=[];
  ZQCtas.Close;
  ZQCtas.SQL.Text:='SELECT * FROM dcuentas ORDER BY '+Column.FieldName+';';
  ZQCtas.Open;
  Column.Title.Font.Style:=[fsBold];
end;


En el ciclo for quitamos "Bold" de todas las columnas y al final del código lo establecemos para la actual (que viene como parámetro).

Quedando así:



miércoles, 23 de mayo de 2018

TDataSource cuándo usarlo?

Lazarus, Free Pascal y muchos de sus componentes son sencillamente espectaculares, más aún teniendo en cuenta que es un proyecto de código abierto que comenzó hace muchos años (2001 creo), con pocos programadores para tamaño proyecto, actualmente entre Free Pascal y Lazarus, son unos 20.
El problema, lo dije desde un principio cuando retomé la programación, motivado por la existencia del IDE Lazarus, es la documentación, muchas veces inexistente y otras veces hay que dedicar mucho tiempo para dar con ella.
Los tutoriales para trabajar con bases de datos son pocos, algunos desactualizados y casi siempre los mismo ejemplos.
Es así como aprendí que para conectar y trabajar con una BD se necesitan 3 componentes: el conector (TZConnection), la consulta (TZQuery) y la fuente de datos (TDataSource). Pues no es así. Me di cuenta leyendo y participando del foro, cuando un usuario planteó una duda y detecté que en su código no utilizaba ningún TDataSource, le pregunté y me respondió si era necesario. Me sembró la duda y de hecho la respuesta es: No. Eso es cambia mucho mi panorama, para empezar no necesitaré tantos data modules si solo tengo un para de consultas, puedo declarar las conexiones y consultas sin necesidad de utilizar el data aware y que no molesten en el Form. Ahora entiendo por qué una vez me dijeron que cuando comienzas con esto, usas todo data aware y luego vas directamente por el código, y así es.

El data aware TDataSource solo es necesario cuando necesitamos enlazar los datos con un componente como puede ser un DBGrid, DBComboBox, etc. caso contrario no es necesario.

El ícono es muy claro, el componente envía los datos de un TDataSet hacia otros componentes, es indispensable para un TDBGrid por ejemplo, por eso es que el DataSource se debe vincular con un DataSet (y éste con un conector) y lo que alimenta al DBGrid es el DataSource. Ahora si no necesitamos alimentar ningún control, entonces no es necesario utilizar ningún DataSource.

jueves, 10 de mayo de 2018

SQL: Insertar registros en una tabla con campo auto increment.

¿Cómo ejecutar correctamente el comando SQL para insertar filas que contienen una columna auto incremental?

Por ejemplo, una tabla (tabla1) con 4 campos: id, nombre, apellido y edad.

CREATE TABLE tabla1 (id INTEGER UNIQUE NOT NULL PRIMARY KEY AUTOINCREMENT, nombre VARCHAR(50), apellido VARCHAR(50), edad INTEGER);

Error común:

INSERT INTO tabla1 VALUES ('Juan', 'Pérez', 25);

Esto arrojará un error del tipo "la tabla tiene 4 columnas pero solo se proporcionan 3 valores" y es cierto, pero claro, no se puede pasar el valor id porque el mismo debe establecerlo SQL.
La solución es simplemente especificar los campos:

INSERT INTO tabla1 (nombre, apellido, edad) VALUES ('Juan', 'Pérez', 25);

En caso de utilizar Zeos esto se realiza mediante ZConnection1.ExecuteDirect o también puede hacerse mediante el dataset ZQuery que sería algo así: (con la tabla ya creada)

ZQuery1.SQL.Text('SELECT * FROM tabla1;');
ZQuery1.Insert;
ZQuery1.FieldByName('nombre').AsString:='Juan';
ZQuery1.FieldByName('apellido').AsString:='Pérez';
ZQuery1.FieldByName('age').AsInteger:=25;
ZQuery1.Post

miércoles, 2 de mayo de 2018

Abrir documentos y URLs.

Las funciones para abrir archivos y páginas web se encuentran en la unidad LCLIntf la cual debe incluirse en uses.
OpenDocument: se le debe enviar el string con el nombre del archivo, retornará True si tuvo éxito o False en caso contrario. Esta función deriva la tarea al sistema operativo, el cual intentará abrir el archivo con el programa asociado según el tipo de archivo que sea. Ejemplo: OpenDocument('ayuda.pdf') abrirá el archivo ayuda.pdf que debe ubicarse en el directorio donde se encuentra el ejecutable. También se le puede pasar el path completo.
OpenURL: función similar que intentará que el navegador predeterminado se ejecute y vaya a la URL especificada. El string debe comenzar con 'http://' o 'https://'. Ejemplo: OpenURL('http://duckduckgo.com')

viernes, 13 de abril de 2018

Publicar proyecto


¿Para qué sirve esto? Para varias cosas, por ejemplo, para duplicar un proyecto o precisamente para publicar un proyecto. Lo de duplicar un proyecto se entiende, en cuanto a publicar, un ejemplo simple es para subirlo a un foro o ponerlo como descarga (primero publicar, luego comprimir).

Nota: el proceso de publicar proyecto no afecta en lo más mínimo al proyecto original.

Si usted participa de un foro de programación y le piden que adjunte el proyecto, pues bien, esto es lo que debe hacer:

Primero prepare un directorio vacío donde Lazarus publicará el proyecto.

Luego acceda a la opción desde el menú Proyecto.


En directorio de destino debemos establecer el que creamos para tal fin. Presionando sobre el botón con los tres puntos es la forma más práctica.
Si el proyecto tiene por ejemplo archivo de LazReport y queremos que se publiquen, los agregamos en los filtros a incluir "|lfr" y listo, aceptar.


Una ventana de diálogo nos advierte que si el directorio no está vacío se vaciará. Luego no habrá ningún mensaje de proyecto publicado ni nada, pero en el directorio especificado estarán todos los archivos. Se comprimen a formato 7z o zip y se publica. Es importante utilizar siempre 7z o zip en los foros de programación, especialmente el zip.

También sirve esto como copia de seguridad

domingo, 11 de marzo de 2018

Guardar y leer registros en archivos binarios.

Hay dos maneras de hacer esto, con AssingFile o con TFileStream. Prefiero AssingFile, pues se pueden hacer cosas que no se pueden con stream, por ejemplo mover el puntero hacia un registro (aunque solo en modo lectura) mediante el procedimiento Seek y también podemos usar la función FilePos para averiguar sobre qué registro está el puntero. Agregar datos a un archivo binario no se puede ni con AssigFile ni con FileStream, siempre debemos cargar los datos en memoria y re-escribir todo el archivo, lo cual hoy no es la gran cosa debido a la cantidad de sobra de memoria RAM que tiene cualquier ordenar y la velocidad de procesamiento.

En este ejemplo guardaremos registros con nombres de empresas, número ID y nombre de la base de datos, algo simple. Como buffer usaremos un array (vector o matriz unidimensional) de registros.

Código:

unit Unit1;

{$mode objfpc}{$H+}

interface

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

type

  { TForm1 }

    TForm1 = class(TForm)
    BGuardar: TButton;
    BLeer: TButton;
    BAgregar: TButton;
    edID: TEdit;
    EdEmpresa: TEdit;
    EdBD: TEdit;
    Label1: TLabel;
    Label2: TLabel;
    Label3: TLabel;
    Memo1: TMemo;
    procedure BAgregarClick(Sender: TObject);
        procedure BGuardarClick(Sender: TObject);
      procedure BLeerClick(Sender: TObject);
        procedure FormCreate(Sender: TObject);
    private
        { private declarations }
    public
        { public declarations }
    end;
type
    TReg=record
      ID:Integer;
      Empresa:string[100];
      BD:string[100];
  end;

var
    Form1: TForm1;
    archivo:String;
    aReg:array[0..99] of TReg;
    cantReg:Integer;


implementation

{$R *.lfm}

{ TForm1 }

procedure TForm1.FormCreate(Sender: TObject);
begin
  archivo:=GetCurrentDir+PathDelim+'datareg.bin';
  cantReg:=0;
  if FileExists(archivo) then BLeerClick(Sender);
  //Si el archivo existe lo carga al array
end;

procedure TForm1.BGuardarClick(Sender: TObject);
var
  FReg:File of TReg; //Archivo que contendrá registros tipo TReg
  i:Integer;
begin
  AssignFile(FReg,archivo);  //Vinculamos el archivo
  Rewrite(FReg);             //Lo vamos a sobreescribir
  for i:=0 to cantReg-1 do //Recorremos el array y lo escribimos
                           //con Write
  begin
    Write(FReg,aReg[i]);
 end;
  CloseFile(FReg);   //Cerramos el archivo
end;

procedure TForm1.BLeerClick(Sender: TObject);
var
  FReg:File of TReg; //Archivo que contendrá registros tipo TReg
  i:Integer;
begin
  AssignFile(FReg,archivo); //Vinculamos el archivo
  Reset(FReg); //Lo abrimos en modo solo lectura
  i:=0;
  while not (EOF(FReg)) do      //Lo cargamos al array
  begin
    Read(FReg,aReg[i]);
    Memo1.Lines.Add(IntToStr(aReg[i].ID)+' '+aReg[i].Empresa+' '+
    aReg[i].BD);
    Inc(i);
    Inc(cantReg);
  end;
  CloseFile(FReg);  //Cerramos el archivo
end;

procedure TForm1.BAgregarClick(Sender: TObject);
begin
  aReg[cantReg].ID:=StrToInt(edID.Text);//Agregamos solo al array, 
                                        //no al archivo.
  aReg[cantReg].Empresa:=EdEmpresa.Text;
  aReg[cantReg].BD:=EdBD.Text;
  Inc(cantReg);
  Memo1.Lines.Add('aReg['+IntToStr(cantReg-1)+']: '+EdEmpresa.Text);
end;

end.


En el registro debemos definir la longitud de los strings.
El primer registro de un archivo binario está en la posición 0 (cero).
No hay forma de agregar un registro a un archivo binario, como sí podemos hacerlo con archivos de texto plano, siempre ha que sobreescribir todo el archivo, de ahí ReWrite.
Al utilizar AssignFile podemos acceder mediante Seek a un determinado registro conociendo su posición y leerlo.

Código fuente:  archivosbinarios.7z (incluye e archivo binario con 5 registros).

o en GitLab



viernes, 9 de marzo de 2018

Cifrar y guardar en archivo binario.

Mucho se dice que no hay que guardar datos cifrados porque de una u otra forma pueden llegar a descifrarlos, máxime si dejamos el valor de la key o llave en nuestro programa. Y es cierto, pero no menos cierto es que es mucho mejor que dejar los datos sin encriptar en un archivo de texto o ini (que también es texto plano) o si se quiere en una tabla en SQLite, o también en un binario sin cifrar. Resumiendo es mejor cifrar. Y si además no guardamos la llave en el programa, sino que la requerimos al usuario (tampoco guardamos su hash), la única manera de obtener los datos es mediante el método de fuerza bruta, que tardará bastante con una llave de 30 caracteres con mayúsculas, minúsculas, números, espacios y símbolos, actualmente tardaría años.

En este ejemplo, para que se entienda bien, porque de eso tratan los ejemplos, cargaremos la llave en una variable. También cabe destacar que los nombres de variables y funciones que se ven en este ejemplo, son para aprender, en la práctica hay que esconder los datos, no usar para cifrar una función llamada cifrar, etc. También hay que validar datos y utilizar try al leer y escribir archivos.

Veamos el código:

unit Unit1;

{$mode objfpc}{$H+}

interface

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

type

  { TForm1 }

    TForm1 = class(TForm)
        BGuardar: TButton;
 BLeer: TButton;
 Edit1: TEdit;
 Edit2: TEdit;
 Edit3: TEdit;
 Edit4: TEdit;
 Edit5: TEdit;
 Edit6: TEdit;
 Label1: TLabel;
 Label2: TLabel;
 Label3: TLabel;
 Label4: TLabel;
 Label5: TLabel;
 Label6: TLabel;
 procedure BGuardarClick(Sender: TObject);
 procedure BLeerClick(Sender: TObject);
        procedure FormCreate(Sender: TObject);
    private
       function Cifrar (const texto:String):RawByteString;
       function DesCifrar (const texto:String):RawByteString;
        { private declarations }
    public
        { public declarations }
    end;

type
    TRegistro=Record
      Servidor:String[100];
      Usuario:String[100];
      Clave:String[100];
    end;

var
    Form1: TForm1;
    archivo:String;
    llave:String;

implementation

{$R *.lfm}

{ TForm1 }

procedure TForm1.FormCreate(Sender: TObject);
begin
  archivo:=GetCurrentDir+PathDelim+'prueba.dat';
  llave:='La llave';
end;

function TForm1.Cifrar(const texto: String): RawByteString;
var
  str_Cifrar:TBlowFishEncryptStream;
  streamTexto:TStringStream;
begin
  streamTexto:=TStringStream.Create('');
  str_Cifrar:=TBlowFishEncryptStream.Create(llave,streamTexto);
  str_Cifrar.WriteAnsiString(texto);
  str_Cifrar.Free;
  Result:=streamTexto.DataString;
  streamTexto.Free;
end;

function TForm1.DesCifrar(const texto: String): RawByteString;
var
  str_DesCifrar:TBlowFishDeCryptStream;
  unstream:TStringStream;
  temp:RawByteString;
begin
  unstream:=TStringStream.Create(texto);
  unstream.Position:=0;
  str_DesCifrar:=TBlowFishDeCryptStream.Create(llave,unstream);
  temp:=str_DesCifrar.ReadAnsiString;
  str_DesCifrar.Free;
  unstream.Free;
  Result:=temp;
end;

procedure TForm1.BGuardarClick(Sender: TObject);
var
  Registro:TRegistro;
  FReg:File of TRegistro;
begin
  Registro.Servidor:=Cifrar(Edit1.text);
  Registro.Usuario:=Cifrar(Edit2.text);
  Registro.Clave:=Cifrar(Edit3.text);
  AssignFile(FReg,archivo);
  Rewrite(FReg);
  Write(FReg,Registro);
  CloseFile(FReg);
end;

procedure TForm1.BLeerClick(Sender: TObject);
var
  Registro:TRegistro;
  FReg:File of TRegistro;
begin
  AssignFile(FReg,archivo);
  Reset(FReg);
  Read(FReg,Registro);
  CloseFile(FReg);
  edit4.Text:=DesCifrar(Registro.Servidor);
  edit5.Text:=DesCifrar(Registro.Usuario);
  Edit6.Text:=DesCifrar(Registro.Clave);
end;

end.

Lo primero que debemos hacer es incluir la unidad BlowFish en uses.
Si trabajamos con archivos binarios, definimos un registro para guardar y leer los datos.
En el método Create simplemente definimos el archivo y la llave, que como ven, puede contener espacios.
Luego 2 funciones para encriptar y desencriptar y 2 procedimientos para guardar y leer, para un ejemplo, alcanza y sobra.

Veamos la función Cifrar:

function TForm1.Cifrar(const texto: String): RawByteString;
var
  str_Cifrar:TBlowFishEncryptStream;
  streamTexto:TStringStream;
begin
  streamTexto:=TStringStream.Create('');
  str_Cifrar:=TBlowFishEncryptStream.Create(llave,streamTexto);
  str_Cifrar.WriteAnsiString(texto);
  str_Cifrar.Free;
  Result:=streamTexto.DataString;
  streamTexto.Free;
end;

Nótese que no devuelve un string, sino un RawByteString que es una cadena de caracteres (string) sin ningún CodePage asociado, ideal e indispensable para que esto funcione.
Necesitamos dos variables, una para el stream de cifrado de Blow Fish y otro un stream común, donde volcaremos el texto cifrado.
Creamos el stream común (streamTexto) vacío.
Creamos el stream de cifrado de BF y le pasamos como parámetros la llave (establecida en FormCreate) y el stream de texto vacío. Es importante hacer todo en este orden.
Ahora ciframos con WriteAnsiString, como parámetro le pasamos la constante de la función llamada texto.
Liberamos el stream de cifrado, el texto cifrado está en el stream de texto (streamtexto).
Finalmente asignamos al resultado de la función el DataString del stream de texto y liberamos el mismo.

Ahora la función DesCifrar:

function TForm1.DesCifrar(const texto: String): RawByteString;
var
  str_DesCifrar:TBlowFishDeCryptStream;
  unstream:TStringStream;
  temp:RawByteString;
begin
  unstream:=TStringStream.Create(texto);
  unstream.Position:=0;
  str_DesCifrar:=TBlowFishDeCryptStream.Create(llave,unstream);
  temp:=str_DesCifrar.ReadAnsiString;
  str_DesCifrar.Free;
  unstream.Free;
  Result:=temp;
end;


También usamos dos variables para los streams y una tercera del tipo RawByteString que contendrá el valor que retornará la función. Podría obviarse esta variable supuestamente, pero por algo está ahí, la verdad no me acuerdo, algún error me habrá hecho intentar con una variable temporal, funcionó y ahí está.
Aquí cuando creamos el stream de texto, no lo hacemos vacío sino con el valor de la constante texto y a su vez, volvemos a cero su posición para que BF la lea desde el comienzo.
Creamos el stream de descifrado de BF y le pasamos también la llave y el stream de texto.
Ahora sí desciframos usando el método ReadAnsiString perteciente a TBlowFishDeCryptStream y lo asignamos a la variable temp; liberamos los streams y como resultado enviamos el valor de temp.

El resto del código no lo voy a explicar porque simplemente es usar estas dos funciones, escribir y leer el archivo binario de la manera habitual.

Descargar el código fuente: BlowFish.7z

o en GitLab


viernes, 9 de febrero de 2018

Guardar y leer imágenes en bases de datos.

Además de ver como se guarda una imagen de cualquier formato (JGP, PNG, etc.) en una base de datos SQL (SQLite en este caso), también veremos como leerla y mostrarla en un TImage en un formulario.
Logotipo: TImage;
Al TImage del Form1 lo llamamos Logotipo.
Para leer la imagen y mostrarla en un TImage en un Form:
procedure TFom1.CargoDatos;
var
  unstream:TMemoryStream;
begin
  //Se cargan la campos "normales"...
  if ZQuery1.FieldByName('logo').IsNull then
  begin
    Logotipo.Picture.Clear;
    Exit;
  end;
  unstream:=TMemoryStream.Create;
  unstream.Position:=0;
  TBlobField(ZQuery1.FieldByName('logo')).SaveToStream(unstream);
  unstream.Position:=0;
  Logotipo.Picture.LoadFromStream(unstream);
  unstream.Free;
end;
Declaramos una variable del tipo TMemoryStream donde almacenaremos el contenido de la imagen que se encuentra en el campo "logo" de una tabla.
Para no tener problemas, averiguamos si existe tal imagen, lo hacemos con FieldByName('logo').IsNull. Si esto devuelve True entonces borramos la imagen de LogoTipo, si no hacemos esto quedará la imagen cargada anteriormente si la hubiere. Si IsNull devuelve False quiere decir que hay una imagen (o debería haberla), entonces creamos el stream, lo posicionamos en 0 (cero) y lo cargamos con TBlobField(el campo).SaveToStream y cabe aclarar que no es necesario definir ninguna variable del tipo TBlobField, este procedimiento se encarga de todo. Ahora ya tenemos la imagen del campo logo de una tabla de una base de datos cargada en un stream de memoria, el paso final es mostrarla en el formulario, para eso posicionamos nuevamente a 0 (cero) el stream, y lo mandamos al TImage nombrado Logotipo y no olvidarse de liberar el stream utilizando el método Free.

Ahora lo inverso, leer la imagen y guardarla en la base de datos. Se omite la carga desde archivo en este ejemplo.
procedure TForm1.GuardarDatos(Sender: TObject);
var
  ms:TMemoryStream;
begin
  //Se pone el dataset en modo edit o insert y se graban los 
  //campos "normales"...
  if (logotipo.Picture.Width>0) then
    begin
      ms:=TMemoryStream.Create;
      Logotipo.Picture.SaveToStream(ms);
      ms.Position:=0;
      TBlobField(ZQuery1.FieldByName('logo')).LoadFromStream(ms);
      ms.Free;
    end
  else
    begin
      ZQuery1.FieldByName('logo').AsString:='';
    end;
  ZQuery1.Post;   
end;
Como en el código anterior, necesitamos una variable para el stream en memoria. Y nuevamente para no tener problemas, mediante Logotipo.Picture.Width>0 determinamos si hay alguna imagen que guardar, caso contrario se guarda NULL, AsString:='' hace eso.
Si hay imagen, creamos el stream y le asignamos la imagen que está contenida en la propiedad Picture de Logotipo (TImage). Posicionamos en 0 (cero) el stream que ya contiene la imagen y nuevamente nos valemos de TBlobField que asignará el stream al campo "logo" y liberamos con Free el stream.

Desde ya es un código orientativo, pero testeado que este método funciona, al menos para SQlite utilizando ZeosLib.

Hay muchas formas de hacer esto, pero esta forma es la que menos problemas me trajo y es bastante simple y "limpia". He probado antes con DBImage, pero a veces ejecutando el código desde la IDE me tiraba un error EReadError que podía ignorar y todo seguía bien, de hecho ejecutando el programa (fuera de la IDE) esos errores no se mostraban, hasta que detecté que si la imagen que leía DBImage no era JPG entonces largaba ese error, eso me motivo a deshacerme de TDBImage y hacerlo como lo muestro, básicamente con un stream, el procedimiento TBlobField y un TImage.

sábado, 3 de febrero de 2018

TDBLookUpComboBox validar OnExit

Cuando damos al usuario la posibilidad del autocompletado en un ComboBox y no validamos, existe la posibilidad de que salte un error y es correcto, porque siempre hay que validar. Por ejemplo puede pasar esto:


O esto otro:


que de paso, no puede pasar en el combo de bancos, porque es del tipo lista y de manera predeterminada ya seleccionamos el primer elemento, ahí no hay que validar nada, el usuario no puede hacer de las suyas. Pues bien, en cualquiera de los dos casos, si presiona imprimir y no se valida, el programa mostrará un mensaje de error. En cambio con una simple validación, obligamos al usuario a seleccionar un ítem del combo, mediante el evento OnExit:

procedure TFRepCuentas.cmbCtaExit(Sender: TObject);
begin
   if cmbCta.ItemIndex=-1 then
   begin
     ShowMessage('Debe seleccionar una cuenta.');
     cmbCta.SetFocus;
   end;
end;   

Entonces si el usuario lo deja en blanco o escribe xx (no coincidiendo xx con ningún elemento del combo) le aparecerá el siguiente mensaje y le enviará el cursor nuevamente al combo y de ahí no sale hasta que seleccione un ítem.


Y tendrá que seleccionar una cuenta sí o sí o cerrar la ventana (lo cual también podríamos evitar si quisiéramos).