sábado, 27 de enero de 2018

DBGrid: Anchors de las columnas

Si bien TDBGrid tiene la propiedad Anchors (Anclajes), cuando la definimos y agrandamos y achicamos el formulario, la grilla también lo hace, mas no sus columnas, la cuales carecen de la propiedad Anchors, entonces el ancho de las columnas permanece estático, no cambia.

¿Cómo hacer que las columnas cambien el ancho cuando se agranda el formulario que contiene la DBGrid?

Creando el evento FormResize y asignarlo a los eventos del Form OnResize y OnChangeBounds. Desde ya el grid debe estar anclado al formulario y tener definido al menos las constraints de valores mínimos. Luego, un poco de matemáticas y eso es todo.


Así tengo definido el anclaje del DBGrid, mucho no entiendo de este tema, mucho prueba y error hasta dar con el resultado que busco.


Las Constraints del DBGrid, aunque esto es relativo si están definidas las del Form.


En los eventos del DBGrid, desde el inspector de objetos, creamos el evento para OnResize y también lo asignamos a OnChangeBounds.

procedure TfrmBancos.FormResize(Sender: TObject);
var
  ndiv, nmod, ndist:Integer;
begin
  ndist:=(Width-Constraints.MinWidth);
  if ndist < 4 then exit;
  ndiv:=ndist div 4;
  nmod:=ndist mod 4;
  DBGrid1.Columns.Items[1].Width:=160+ndiv;
  DBGrid1.Columns.Items[2].Width:=120+ndiv;
  DBGrid1.Columns.Items[3].Width:=150+ndiv;
  DBGrid1.Columns.Items[4].Width:=160+ndiv;
  if nmod>0 then
    DBGrid1.Columns.Items[4].Width:=DBGrid1.Columns.Items[4].Width+nmod;
end;


En este caso, la columna 0 (cero) no la muestro, solo las 1,2,3 y 4 con un ancho definido en la propiedad width de cada columna de 160, 120, 150 y 160 respectivamente.
Lo que hago es calcular en cuántos píxeles se agranda el formulario y en base a ello, lo distribuyo en las columnas, como son 4, hago la división entera sobre 4 y el resto (mod) lógicamente también sobre 4. Luego elijo a que columna se asigno el sobrante si es que lo hay (mod 4).

jueves, 25 de enero de 2018

Enviar archivos a un servidor vía FTP


En este ejemplo vamos a enviar un archivo "listado.txt" que debe estar en la misma carpeta que el programa a un servidor remoto, es decir, necesitamos sí o sí un servidor al cual conectarnos vía FTP, y un usuario con privilegio de lectura y escritura. La conexión la hacemos por el puerto 21. Desde ya el archivo y el puerto se pueden cambiar desde el código fuente.
Necesitamos también el paquete synapse.

Descargar Synapse desde la web del autor: http://www.ararat.cz/synapse/doku.php/download

En la wiki de Free Pascal hay mucha información y ejemplos de Synapse (en inglés): http://wiki.freepascal.org/Synapse

Podemos utilizar este paquete sin necesidad de instalarlo ni reconstruir la IDE Lazarus.


Desde un proyecto nuevo o habiendo ya descargado es de este ejemplo (al final de esta entrada) vamos a Paquete y Abrir archivo de paquete .lpk.


El paquete se encuentra en la carpeta lib dentro de source donde se haya descomprimido synapse.


No tiene que aparecer esto.


Ahora tal cual se ve en la imagen, botón Usar y Agregar al proyecto, nada más, no se compila ni se instala.

Lo primero es incluir las unidades ftpsend y blcksock en la sección uses.

uses
Classes, SysUtils, FileUtil, Forms, Controls, Graphics, Dialogs, StdCtrls, ComCtrls, ftpsend, blcksock;


Al Form1 le vamos a definir dos variables privadas para que puedan ser accedidas desde los procedimientos (eventos/métodos).


private
  TotalBytes : longint;
  CurrentBytes : longint;


En la clase TForm vamos a incluir estos procedimientos:

procedure btnEnviarClick (Sender: TObject);
procedure btnSalirClick (Sender: TObject);
procedure FormCreate (Sender: TObject);
procedure SockCallBack (Sender: TObject; Reason: THookSocketReason; const Value: string);


FormCreate solamente hace los TEdit tipo password por lo tanto no es necesario para "practicar", o puede dejarse como password solo el correspondiente al TEdit de la contraseña.

edContrasena.EchoMode:=emPassword;
edServidor.EchoMode:=emPassword;
edUsuario.EchoMode:=emPassword;


El método SockCallBack solo es necesario para mostrar una barra de progreso.

case Reason of
  HR_WriteCount:


El enumerado HR_WriteCount se utiliza porque se envían archivos, si en cambio se recibieran archivos, el enumerado es otro, lo veremos en otra entrada donde explicare como recibir archivos vía FTP que desde ya, es parecido a esto.

Variables del evento btnEnviarClick:

procedure TForm1.btnEnviarClick(Sender: TObject);
var
  ftp: TFTPSend;
  remotefile: string;
  localfile: String;
  i: Integer;



Lo primero es definir una variable (en este caso llamada ftp) del tipo TFTPSend que se encuentra en la unidad ftpsend.pas. Si bien el nombre de esta unidad puede llevar a pensar que exista otra llamada ftpreciebe.pas, pues no, en ftpsend está todo, ya sea para enviar como para recibir archivos.

Luego de instanciar la variable "ftp" ftp := TFTPSend.Create; debemos "envolver" todo el código de conexión y envío del archivo en un try ... finally, no es obligatorio pero si recomendable.

ftp.DSock.OnStatus := @SockCallBack;

Es para incrementar la progressbar.

ftp.Timeout:=4000;

Esto es muy importante y lamentablemente no se menciona en la mayoría de ejemplos de FTPSend que hay en Internet. Si no definimos un tiempo de espera, el programa puede colgarse o largar un error, de esta forma establecemos en 4 segundo el tiempo de espera de respuesta del servidor.

if ftp.StoreFile(remotefile,False)

La función StoreFile es la que manda el archivo al servidor, el primer parámetros es un string que contiene la carpeta del servidor y el nombre del archivo /prueba/listado.txt y el segundo parámetro, booleano, solo debe ser True si el servidor soporta reanudar subidas, si es así solo se sube la parte que resta y si el tamaño del archivo es el mismo que el del archivo que está en el servidor, entonces no se envía nada. Como tercera opción, en caso de que el archivo del servidor sea más grande que el archivo local, entonces se envía todo el archivo desde el comienzo.

Todo el código:

unit Unit1;

{$mode objfpc}{$H+}

interface

uses
  Classes, SysUtils, FileUtil, Forms, Controls, Graphics, Dialogs, StdCtrls, ComCtrls, ftpsend, blcksock;

type

{ TForm1 }

TForm1 = class(TForm)
  btnEnviar: TButton;
  btnSalir: TButton;
  edCarpetaServidor: TEdit;
  edServidor: TEdit;
  edContrasena: TEdit;
  edUsuario: TEdit;
  Label1: TLabel;
  Label2: TLabel;
  Label3: TLabel;
  Label4: TLabel;
  MemoLog: TMemo;
  ProgressBar1: TProgressBar;
  procedure btnEnviarClick(Sender: TObject);
  procedure btnSalirClick(Sender: TObject);
  procedure FormCreate(Sender: TObject);
  procedure SockCallBack(Sender: TObject; Reason: THookSocketReason; const Value: string);
private
  TotalBytes : longint;
  CurrentBytes : longint;
{ private declarations }
public
{ public declarations }
end;

var
  Form1: TForm1;

implementation

{$R *.lfm}

{ TForm1 }

procedure TForm1.btnEnviarClick(Sender: TObject);
var
  ftp : TFTPSend;
  remotefile:string;
  localfile:String;
  i:Integer;
begin
  btnEnviar.Enabled:=False;
  btnSalir.Enabled:=False;
  ftp := TFTPSend.Create;
  localfile:='listado.txt';
  MemoLog.Lines.Add('Conectando con el servidor. Por favor espere.');
  Application.ProcessMessages;
  try
    ftp.DSock.OnStatus := @SockCallBack;
    ftp.Username:=edUsuario.Text;
    ftp.Password:=edContrasena.Text;
    ftp.TargetHost:=edServidor.Text;
    ftp.TargetPort:='21';
    ftp.Timeout:=4000;
    if ftp.Login then
      MemoLog.Lines.Add('Login: ********* correcto')
    else
      begin
        MemoLog.Lines.Add('Login: ********* incorrecto');
        exit;
    end;
    Application.ProcessMessages;
    Sleep(1000);
    remotefile:=edCarpetaServidor.Text+'listado.txt';
    Progressbar1.Position:=0;
    ftp.DirectFileName:=localfile;
    ftp.DirectFile:=true;
    TotalBytes:=FileSize(localfile);
    MemoLog.Lines.Add('Enviando archivo: ' + localfile);
    MemoLog.Lines.Add('Total Bytes: ' + IntToStr(TotalBytes));
    if ftp.StoreFile(remotefile,False) then
      MemoLog.Lines.Add('Transferencia completa')
    else
      MemoLog.lines.add('Transferencia fallida');
    Application.ProcessMessages;
    Sleep(1000);
  finally
    ftp.Logout;
    ftp.free;
  end;
  MemoLog.Lines.Add(#13#10+'Envío de archivos finalizado'+#13#10+'Puede cerrar (salir) esta ventana');
  btnEnviar.Enabled:=True;
  btnSalir.Enabled:=True;
end;

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

procedure TForm1.FormCreate(Sender: TObject);
begin
  edContrasena.EchoMode:=emPassword;
  edServidor.EchoMode:=emPassword;
  edUsuario.EchoMode:=emPassword;
end;

procedure TForm1.SockCallBack(Sender: TObject; Reason: THookSocketReason;
const Value: string);
begin
  Application.ProcessMessages;
  case Reason of
    HR_WriteCount:
    begin
      inc(CurrentBytes, StrToIntDef(Value, 0));
      ProgressBar1.Position := Round(100 * (CurrentBytes / TotalBytes));
    end;
  HR_Connect: CurrentBytes := 0;
end;
end;

end.


Descargar proyecto (incluye el archivo listado.txt): EnviaraFTP.7z



miércoles, 24 de enero de 2018

Medir el tiempo de ejecución de un proceso

Hacer el "benchmark" de una función, proceso o programa es algo relativamente simple en Lazarus/FreePascal utilizando la unidad dateutils.

En este ejemplo vamos a calcular cuanto se tarda en imprimir 10.000 líneas en un TMemo. También obtendremos la velocidad promedio de líneas por segundo.

unit Unit1;

{$mode objfpc}{$H+}

interface

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

type

{ TForm1 }

TForm1 = class(TForm)
  Button1: TButton;
  Button2: TButton;
  CheckBox1: TCheckBox;
  dtpEmpieza: TDateTimePicker;
  dtpFinaliza: TDateTimePicker;
  dtpTranscurrido: TDateTimePicker;
  edImpps: TEdit;
  Label1: TLabel;
  Label2: TLabel;
  Label3: TLabel;
  Label4: TLabel;
  Memo1: TMemo;
  procedure Button1Click(Sender: TObject);
  procedure Button2Click(Sender: TObject);
private
  procedure Empezar;
  procedure Finalizar;
{ private declarations }
public
{ public declarations }
end;

var
  Form1: TForm1;

implementation

{$R *.lfm}

{ TForm1 }

procedure TForm1.Button1Click(Sender: TObject);
var
  i:Integer;
begin
  Empezar;
  if CheckBox1.Checked then
  begin
    for i:=1 to 10000 do
    begin
      Application.ProcessMessages;
      Memo1.Lines.Add(IntToStr(i));
    end;
  end
  else
    begin
      for i:=1 to 10000 do
        Memo1.Lines.Add(IntToStr(i));
    end;
  Finalizar;
end;

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

procedure TForm1.Empezar;
begin
  dtpEmpieza.Time:=Now;
end;

procedure TForm1.Finalizar;
begin
  dtpFinaliza.Time:=Now;
  dtpTranscurrido.Time:=dtpFinaliza.Time-dtpEmpieza.Time;
  edImpps.Text:=FloatToStr((10000/((SecondOf(dtpTranscurrido.Time)+((MilliSecondOf(dtpTranscurrido.Time)/1000))))));
end;

end.

Desde ya se pueden quitar los dos TDateTimePicker y reemplazarlos por dos variables del tipo TTime, o ocultar dichos componentes. También reemplazar el valor 10000 por una constante o variable. Se puede jugar un buen rato.

Código fuente: MedirProcesos.7z o en GitLab


martes, 23 de enero de 2018

TListBox: conceptos básicos.

TListBox es un componente que muestra una lista de cadenas (strings) y resalta la seleccionada por el usuario. Se encuentra en la pestaña Standard de la paleta de componentes. Permite la multiselección pero en este ejemplo se usará la selección única.

Esta lista se compone de Items que son del tipo TString que es una clase abstracta.

Es como la implementación gráfica de un vector de cadenas, con propiedades y métodos que permiten su manipulación.

En el ejemplo se crea un programa que carga al comienzo la lista con algunos elementos y permite agregar, eliminar, borrar la lista, ordenarla, copiarla a un TMemo y recorrerla mostrando el recorrido en el Memo. También se muestra en un TEdit el elemento seleccionado.



unit Unit1;

{$mode objfpc}{$H+}

interface

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

type

        { TForm1 }

    TForm1 = class(TForm)
                btnRecorrer: TBitBtn;
                btnListAMemo: TBitBtn;
                btnAgregar: TBitBtn;
                btnEliminar: TBitBtn;
                btnOrdenar: TBitBtn;
                btnBorrarTodo: TBitBtn;
                cbDuplicados: TCheckBox;
                edAgregar: TEdit;
                edSeleccionado: TEdit;
                lblSeleccionado: TLabel;
                ListBox1: TListBox;
                Memo1: TMemo;
                procedure btnRecorrerClick(Sender: TObject);
    procedure btnAgregarClick(Sender: TObject);
                procedure btnBorrarTodoClick(Sender: TObject);
                procedure btnEliminarClick(Sender: TObject);
                procedure btnListAMemoClick(Sender: TObject);
                procedure btnOrdenarClick(Sender: TObject);
                procedure FormCreate(Sender: TObject);
                procedure ListBox1Click(Sender: TObject);
    private
        function YaExiste (elemento: String): Boolean;
        { private declarations }
    public
        { public declarations }
    end;

var
    Form1: TForm1;

implementation

{$R *.lfm}

{ TForm1 }

procedure TForm1.btnAgregarClick(Sender: TObject);
begin
  if Length(Trim(edAgregar.Text))<1 then exit;
  //Trim elimina espacios en blanco, Length el tamaño.
  //Si ingresa un dato en blanco no hace nada.
  if not (cbDuplicados.Checked) then if YaExiste(edAgregar.Text) then exit;
  //Si no se marcó Permitir duplicados entonces se llama a la función YaExiste, si devuelve True no se agrega.
  ListBox1.AddItem(edAgregar.Text,ListBox1);
  //Además del string a agregar hay que especificar el objeto.
  edAgregar.Clear;
  //Limpia el TEdit. edAgregar.Text:='' también es válido.
end;

procedure TForm1.btnRecorrerClick(Sender: TObject);
var
  i:Integer;
begin
  Memo1.Clear;
  Memo1.Lines.Add('');
  Memo1.Lines.Add('for i:=0 to ListBox1.Count-1 do'+#13#10+'  Memo1.Lines.Add(ListBox1.Items.Strings[i]);'+#13#10);
  for i:=0 to ListBox1.Count-1 do
    //TListBox es base 0 (cero) por eso desde 0 hasta cantidad-1
    Memo1.Lines.Add('ListBox1.Items.Strings['+IntToStr(i)+']: '+ListBox1.Items.Strings[i]);
    //ListBox1.Items.String[i] así se accede al texto de cada ítem.
end;

procedure TForm1.btnBorrarTodoClick(Sender: TObject);
begin
  ListBox1.Clear;
  //Limpia la ListBox
end;

procedure TForm1.btnEliminarClick(Sender: TObject);
begin
  if ListBox1.ItemIndex>=0 then ListBox1.Items.Delete(ListBox1.ItemIndex);
  //Si hay algún ítem seleccionado, entonces ItemIndex tendrá un valor mayor o igual a cero, caso contrario será -1.
end;

procedure TForm1.btnListAMemoClick(Sender: TObject);
begin
  Memo1.Lines.Add('Memo1.Lines:=ListBox1.Items;');
  Memo1.Lines:=ListBox1.Items;
  //Ya la propiedades Lines e Items son del mismo tipo TStrings basta con asignar una a la otra.
end;

procedure TForm1.btnOrdenarClick(Sender: TObject);
begin
  ListBox1.Sorted:=True;
  ListBox1.Sorted:=False;
  //El False se agrega para que si se agrega un nuevo elemento lo agregue al final y se pueda volver a ordenar.
end;

procedure TForm1.FormCreate(Sender: TObject);
begin
  ListBox1.AddItem('Devuan',ListBox1);
  ListBox1.AddItem('Linux mint',ListBox1);
  ListBox1.AddItem('Gentoo',ListBox1);
  ListBox1.AddItem('Arch Linux',ListBox1);
  ListBox1.AddItem('Ubuntu',ListBox1);
  ListBox1.AddItem('Debian',ListBox1);
  ListBox1.AddItem('Mangaro',ListBox1);
end;

procedure TForm1.ListBox1Click(Sender: TObject);
begin
  edSeleccionado.Text:=ListBox1.Items.Strings[ListBox1.ItemIndex];
  //Se accede al texto Items.Strings y el índice lo tomamos de la propia lista ItemIndex que es el ítem seleccionado.
end;

function TForm1.YaExiste(elemento: String): Boolean;
//elemento es el texto de edAgregar
var
  i:Integer;
  ret:Boolean;
begin
  ret:=False;
  for i:=0 to listbox1.Count-1 do
    if elemento=listbox1.Items.Strings[i] then ret:=True;
  //Recorremos toda la lista buscando si ya existe.
  YaExiste:=ret;
end;

end. 


Como vemos, cuando ordenamos la lista, los elementos cambian el índice, no es solo un ordenamiento visual.

Para que la lista esté siempre ordenada basta con establecer la propiedad Sorted en True, de esta forma cuando se agregue un elemento, lo hará en la posición que le corresponda y no al final. Haciendo esto, el botón ordenar ya no tiene sentido y debe quitarse.

Código fuente: TListBox3.7z

Más entradas de ListBox.

domingo, 14 de enero de 2018

LazReport: incluir imagen de campo BLOB

Antes esto era una tarea un poco complicada según pude observar después de varias búsquedas que me mostraban hilos de foros de hace unos cuantos años. Por suerte esto ya no es así e incluir una imagen de cualquier formato (dentro de los más populares) almacenada en un campo o columna del tipo BLOB es tan sencillo que no requiere ni una línea de código.

Desde el diseñador LazReport debemos incluir un objeto del tipo imagen y nos aparecerá el siguiente diálogo:


La opción Cargar es para cargar una imagen contenida en un archivo, no es el caso. Debemos hacer click en Texto.


Y aquí tanto solo indicamos el campo que contiene la imagen. Si el dataset está conectado, podemos agregarlo desde el botón Campo de DB.
Eso es todo.


sábado, 13 de enero de 2018

Cambiar el ícono del programa y formularios

Esto que explicaré a continuación no cambia el icono del ejecutable, solo del programa en ejecución y los formularios (ventanas).

Para cambiar el icono principal, del programa, tenemos que ir a opciones del proyecto:


Es la primera opción así que encontrarla es bastante simple. Es importante respetar el tamaño y los bits por pulgada (bpp). No importa desde donde cargamos la imagen ya que pasará a ser parte del ejecutable. El formato debe ser .ico. Cualquier imagen se puede convertir a ICO usando GIMP se logra esto desde archivos--> Exportar como y escribir el nombrearchivo.ico y guardar.

Cuando se ejecute el programa en el panel se verá así:


Para cambiar o agregar un icono a un formulario (Form) desde el inspector de objetos ir a la propiedad icon y pulsar sobre los tres puntos (...):


para que se abra la siguiente ventana:


presionar "Cargar" el icono y aceptar.

viernes, 12 de enero de 2018

SQLite: Limpiar base de datos con VACUUM

El comando VACUUM copia todo el contenido de la base de datos a una base de datos temporal y luego sobre escribe la original, quedando de este modo, "limpia" u optimizada. Como cualquier comando SQLite lo podemos ejecutar ya sea desde consola, cómodamente utilizando la IDE SQLite Studio o desde código Free Pascal mediante una conexión Zeos con ExecuteDirect, por ejemplo:

ZConnection1.ExecuteDirect('VACUUM;');

Desde ya este comando necesita acceso exclusivo, no debe haber ninguna consulta activa ni transacción.

Desde la versión 3.15.0 (14 de octubre de 2016) se puede utilizar este comando en bases de datos adjuntas. Es importante saber que si se intentase ejecutar VACUUM a una base de datos adjunta desde una versión anterior, la adjunta (attached) será ignorada y VACUUM se ejecutará sobre la base de datos principal.

Tablas sin una clave primaria entera: VACUUM puede alterar los ROWIDs. Las tablas que tienen definida una INTEGER PRIMARY KEY no se modifican, solo aquellas que no lo posean. Es decir, si se utilizan los ROWIDs, algo poco recomendable, no se debe utilizar VACUUM ya que muy probablemente modifique sus valores.

Existe el pragma auto_vacuun que se puede habilitar, aunque según indican en el sitio oficial de SQLite "puede generar una fragmentación adicional de archivos de base de datos. Y auto_vacuum no compacta las páginas parcialmente rellenas de la base de datos como sí lo hace VACUUM.".

Este comando resulta extremadamente útil cuando se están realizando pruebas en la base de datos, generalmente en la etapa de diseño, una vez finalizas las mismas, usar VACUUM para una limpieza que además, reducirá el tamaño del archivo.

Como experiencia propia, estuve realizando pruebas con tipos de datos BLOB para almacenar imágenes, finalizada esta etapa, la base de datos estaba cerca de los 7 MB, luego de VACUUM su tamaño se redujo a 700 KB.

martes, 9 de enero de 2018

Formularios: establecer tamaños límites.

Si deseamos que un formulario pueda ser redimensionado por el usuario pero sobre un rango de valores mínimos y máximos, podemos valernos de las constraints (limitaciones) ya sea desde el inspector de objetos, previamente seleccionando el formulario, o por código.


Aquí vemos que MaxHeight y MaxWidth tiene valor cero, eso significa que puede agrandarse el formulario sin límites. En cambio en MinHeight y MinWidth le especificamos los valores mínimos del formulario, de esta forma si el usuario quiere achicarlo fuera de ese rango, le será imposible. Podrá achicarlo a por ejemplo 50x50 pero ni bien suelte el click del mouse, el formulario se redimensionará automáticamente a sus valores mínimos.
Lo mismo es válido para el tamaño máximo, solo hay que especificar los valores.

miércoles, 3 de enero de 2018

Libro para emprezar con Lazarus y Free Pascal en español

Es un libro libre distribuido bajo la licencia Creative Commons, del año 2012 y traducido al español. Son 150 páginas en formato PDF. Parte desde cero, motivo por el cual la mayor parte del libro son ejercicios o ejemplos por consola y recién los últimos capítulos con Lazarus. Como bien indicar el autor, el libro está dirigido a programadores y también a quienes no los son y quieren aprender a programar. Por mi parte también lo recomiendo para quienes dejaron de programar hace muchos años y en DOS, les ayudará mucho para dar ese salto a la programación con objetos, programación orientada a objetos y programación con interfaz gráfica. Y por si fuera poco, multiplataforma.

Y reitero que el año en que se hizo el libro no es un problema, ya que lo que se trata en el mismo, poco y nada a cambiado, es totalmente válido aún en 2018.

Dejo la versión original en inglés y la traducida al español.

Start programming using Object Pascal Free Pascal/Lazarus book

Introducción a la programación con Object Pascal Free Pascal / Lazarus

También puedes buscar más documentación en este sitio en:

Documentación y Enlaces.