domingo, 30 de junio de 2019

Formularios MDI.

Mucho debate genera este tema, como que es obsoleto, antiguo, etc. Siempre que busqué información terminé abandonando, porque encontré paquetes abandonados, que use frames y miles de consejos de no usarlos. A mí me parece más ridículo programar todo con showmodal, si bien es más cómodo, fácil y, hasta si se quiere, seguro; pero no dejo de tener en mis programas un formulario principal eternamente vacío, en la mayoría de los casos. No me refiero a los formularios en cascada, sino a que cada vez que se abra un formulario, el mismo ocupe el espacio que hay en el formulario principal. También algunos usuarios prefieren este método a un formulario showmodal flotando. Probablemente esto pueda hacerse mejor usando frames, pero de momento lo desconozco.
La solución se me ocurrió simplemente pensando en la propiedad aling:=alClient pensé que si funciona con un TMemo, ¿por qué no con un Form? y así encontré en el foro, utilizando la búsqueda avanzada, la clave: FromStyle, uno debe ser fsMDIForm y el otro fsMDIChild sin olvidar Aling alClient.

En el video se muestra, con errores como el que se ve en el minuto 3:30 donde intento establecer el parentesco del Form1 respecto de Form2 sin notar que el Form2 se crea después que el Form1 obteniendo un hermoso SIGSEGV apreciable en el minuto 5.



Este error se soluciona cambiando en el archivo del proyecto el orden de creación de los formularios y estableciendo el parentesco en el evento Create del Form1.



Es solo un ejemplo, la mayoría de los formularios los creo, los muestro y los libero: Fomr2.Crate(nil); Form2.ShowModal; FreeAndNil(Form2); todavía no probé como hacerlo, pero ya sé que ShowModal no se utiliza con un Form de estilo fsMDIChild, y es lógico.

Código fuente Formularios MDI.

miércoles, 26 de junio de 2019

La ubicación del programa ejecutable.

Vamos a ver tres métodos de hallar o intentar encontrar el path del archivo ejecutable de nuestro programa (me resisto a llamarle aplicación, somos programadores, no aplicadores). Los resultados serán distintos en algunos casos, porque depende desde que ubicación se llame al programa, si es desde la carpeta o directorio donde se encuentra el mismo, entonces no hay problema, como diría ALF, pero si se lo hace a través de un acceso directo desde el escritorio o desde un navegador de archivos llegando al programa abriendo carpetas pero sin entrar en ellas, ahí cambia la cosa. No obstante, hay una forma que no falla nunca y es la correcta:

ExtractFileDir(Application.ExeName)

----> o más simple aún, que encontré después de escribir esta entrada:

Application.Location <---

Ejemplo:

unit Unit1;

{$mode objfpc}{$H+}

interface

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

type

{ TForm1 }

TForm1 = class(TForm)
  Memo1: TMemo;
  procedure FormCreate(Sender: TObject);
private
{ private declarations }
public
{ public declarations }
end;

var
  Form1: TForm1;

implementation

{$R *.lfm}

{ TForm1 }

procedure TForm1.FormCreate(Sender: TObject);
var
  elPath:String;
  elPrograma:String;
begin
  elPrograma:=Application.ExeName;
  elPath:=TrimFilename(GetCurrentDir+PathDelim);
  Memo1.Lines.Add('TrimFilename(GetCurrentDir+PathDelim+elPrograma) --> '+elPath);
  elPath:=ExtractFileDir(Application.ExeName);
  Memo1.Lines.Add('ExtractFileDir(Application.ExeName) --> '+elPath);
  elPath:=ExtractFilePath(Application.ExeName);
  Memo1.Lines.Add('ExtractFilePath(Application.ExeName) --> '+elPath);
end;
end.



El primero, es a lo bestia, y falla si no se ejecuta el programa desde su carpeta.
El segundo, ya utilizando Application.ExeName, no es que falle, sino que hace para lo que está programado, ExtractFileDir y no incluye la barra delimitadora o PathDelim.
Finalmente el tercero es el adecuado: ExtractFilePath que a diferencia del anterior, sí incuye la barra delimitadora.
Cabe destacar que ninguno en realidad falla, hablo de falla para lo que se busca, que es el path (completo).


Resultado ejecutando el programa desde la IDE Lazarus.

Resultado desde la carpeta del programa.


Resultado desde la carpeta programas.

Nota: la clase Application se ubica en la unidad Forms.

sábado, 22 de junio de 2019

TMemo: Leer y guardar en un archivo.

Varios componentes de Lazarus tienen los métodos LoadFromFile y SaveToFile y TMemo no es la excepción, pero lo hace, lógicamente, a través de TString.

Memo1.Lines.LoadFromFile('prueba.txt')

porque Lines es del tipo TStrings que es la clase que posee dichos métodos.

Código de ejemplo:

unit Unit1;

{$mode objfpc}{$H+}

interface

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

type

{ TForm1 }

TForm1 = class(TForm)
  Button1: TButton;
  Button2: TButton;
  Memo1: TMemo;
  procedure Button1Click(Sender: TObject);
  procedure Button2Click(Sender: TObject);
private
{ private declarations }
public
{ public declarations }
end;

var
  Form1: TForm1;

implementation

{$R *.lfm}

{ TForm1 }

procedure TForm1.Button1Click(Sender: TObject);
begin
  Memo1.Lines.LoadFromFile('tabla2019.txt');
end;

procedure TForm1.Button2Click(Sender: TObject);
begin
  Memo1.Lines.SaveToFile('tabla2019.txt');
end;

end.




Desde ya es el ejemplo más básico, generalmente se recurre a diálogos para que el usuario elija el archivo a leer y guardar, entre otras cosas.

viernes, 21 de junio de 2019

Formato de fecha y hora en SQLite.

En SQLite el formato de la fecha/hora es YYYY-MM-DD HH:MM.SS.MMM por ejemplo: 2019-06-22 01:00:27.123

Si bien Free Pascal tiene muchas funciones para el tratamiento de fecha y hora, a veces por la configuración regional hay que hacer conversiones.

Usando TFormatSettings:

aFormatSettings.LongDateFormat:='yyyy-mm-dd hh:nn:ss';
aFormatSettings.DateSeparator := '-';
aFormatSettings.TimeSeparator := ':';


Donde aFormatSettings es una variable del tipo TFormatSettings y luego utilizando la función FormatDateTime que devuelve un string:

function FormatDateTime(

const FormatStr: string;

DateTime: TDateTime;

Options: TFormatDateTimeOptions = []

):string;

function FormatDateTime(

const FormatStr: string;

DateTime: TDateTime;

const FormatSettings: TFormatSettings;

Options: TFormatDateTimeOptions = []

):string;


Por ejemplo en una sentencia SQL en WHERE:

sqlWhere:='WHERE regfecha BETWEEN '+QuotedStr(FormatDateTime('YYYY-MM-DD',edFechaDesde.Date))+
' AND '+QuotedStr(FormatDateTime('YYYY-MM-DD',edFechaHasta.Date))';


En este caso no fue necesario TFormatSettings y solo fue fecha, sin hora.

Con TZquery.FieldByName y utilizando componentes TDateTimePicker esto funciona:

ZQReg.FieldByName('regfechahora').AsDateTime:=dtpFecha.Date+dtpHora.Time;

En cuanto los campos del tipo TIME en SQLite; StrToTime('00:00:00') evita el valor nulo que muchas veces conviene evitar.

En SQLite la fecha debe ir entre comillas simples.

Y ante cualquier complicación siempre podemos hacer una función como la siguiente:

function FormatoFechaHoraSQLite(lafechahora: TDateTime): String;
var
  d,m,a,h,mi,s,ms:Word;
  dd,mm,hh,mmi,ss,mms:String;
  lfecha, lhora:String;
begin
  DecodeDate(lafechahora,a,m,d);
  if d < 10 then dd:='0'+IntToStr(d) else dd:=IntToStr(d);

  if m < 10 then mm:='0'+IntToStr(m) else mm:=IntToStr(m);
  lfecha:=IntToStr(a)+'-'+mm+'-'+dd;
  DecodeTime(lafechahora,h,mi,s,ms);
  if h < 10 then hh:='0'+IntToStr(h) else hh:=IntToStr(h);
  if mi < 10 then mmi:='0'+IntToStr(mi) else mmi:=IntToStr(mi);
  if s < 10 then ss:='0'+IntToStr(s) else ss:=IntToStr(s);
  if ms < 10 then mms:='00'+IntToStr(ms) else
    if ms < 100 then mms:='0'+IntToStr(ms) else
      mms:=IntToStr(ms);
  lhora:=hh+':'+mmi+':'+ss+'.'+mms;
  Result:=lfecha+' '+lhora;
end;

O más sencillo usando la función AddChar que se encuentra en la unidad strutils.

function FormatoFechaHoraSQLite2(lafechahora: TDateTime): String;
var
  d,m,a,h,mi,s,ms:Word;
  dd,mm,hh,mmi,ss,mms:String;
  lfecha, lhora:String;
begin
  DecodeDate(lafechahora,a,m,d);
  dd:=AddChar('0',IntToStr(d),2);
  mm:=AddChar('0',IntToStr(m),2);
  lfecha:=IntToStr(a)+'-'+mm+'-'+dd;
  DecodeTime(lafechahora,h,mi,s,ms);
  hh:=AddChar('0',IntToStr(h),2);
  mmi:=AddChar('0',IntToStr(mi),2);
  ss:=AddChar('0',IntToStr(s),2);
  mms:=AddChar('0',IntToStr(ms),3);
  lhora:=hh+':'+mmi+':'+ss+'.'+mms;
  Result:=lfecha+' '+lhora;
end;


DecodeDate y DecodeTime son procedimientos y utiliza variables del tipo Word que son enteros sin signo entre 0 y 65535. Le enviamos un TDateTime y las variables del tipo Word donde se escribirán el año, mes y día (en DecodeDate). No es necesario inicializar las variables, el procedimiento utiliza out:

procedure DecodeDate(

Date: TDateTime;

out Year: Word;

out Month: Word;

out Day: Word

);

martes, 18 de junio de 2019

Número a texto con formato.

Pasar de número a string utilizando la función Format y el registro TFormatSettings. También se puede usar Format para cadenas de texto (strings), básicamente para alinearlas y/o recortarlas, no obstante su potencia radica en lo numérico, no solo enteros y decimales, también hexadecimales, exponenciales y hasta punteros. Ejemplos de todos ellos en: https://www.freepascal.org/docs-html/rtl/sysutils/format.html la cual recomiendo leer antes de continuar. Como también las referidas a TFormatSettings y DefaultFormatSettings.

Como lo indica su nombre, DefaultFormatSettings, es una variable global del tipo TFormatSettings que contiene los valores actuales de formato, si se altera, afectará a todo el proyecto, por ende debe usarse con algunos recaudos, como ser, por ejemplo, si necesito que en una función se utilice el separador decimal punto pero luego continuar utilizando la coma, entonces una opción es, antes de llamar a la función usar DefaultFormatSettings.DecimalSeparator:='.' e inmediatamente en la siguiente línea de código que sucede a la función DefaultFormatSettings.DecimalSeparator:=',' o, lo mismo pero dentro de la función.

Otra opción, y según las necesidades, es crear una variable del tipo TFormatSettings (que es del tipo record) y especificar en ella únicamente los formatos que vamos a utilizar.

Recordemos que Format devuelve un string así que puede usarse casi para cualquier cosa, pero, siempre hay un pero, a nivel IDE, más precisamente en el componente TMemo, no produce siempre los resultados deseados, el problema es con los espacios y el ancho del caracter, a no ser que exista alguna Font de tamaño fijo que desconozco.

En mi caso, necesitaba imprimir unas tablas (no tablas de bases de datos) y no tenía ganas de recurrir a LazReport, porque era algo muy simple, por eso mi primera opción fue TMemo. Pero por qué no aprovechar lo que ya está hecho, por ejemplo un editor de texto plano que cualquier sistema operativo posee.
Fue entonces que opté por TStringList que dispone de dos procedimientos elementales: LoadFromFile y SaveToFile. Finalmente utilizo OpenDocument y listo, de ahí en más se encarga el sistema operativo.

procedure TForm1.BTablaRetGcias2019Click(Sender: TObject);
var
  mes:Integer=0;
  ind:Integer=0;
  FS:TFormatSettings; //Creo la variable del tipo TFormatSettings
  sl:TStringList; //Creo la variable del tipo TStringList
begin
  sl:=TStringList.Create;  //Creo la instancia porque TStringList es una clase
  FS.CurrencyFormat:=1;  //FS es un type record y no necesita instanciasión
  FS.CurrencyDecimals:=2;
  FS.DecimalSeparator:=',';
  FS.ThousandSeparator:='.';
  FS.CurrencyString:='';
  sl.Add('Año 2019');
  sl.Add('');
  sl.Add('Mes Fijo Alíc.% Exedente');
  for mes:=1 to 12 do
  begin
    sl.Add('--------------------------------------');
    for ind:=1 to 9 do
      sl.Add(
      Format('%2.0d',[mes])+
      Format('%14.2m',[TablaGcias19[mes,ind].fijo],FS)+
      Format('%6.0m',[TablaGcias19[mes,ind].alicuota],FS)+
      Format('%16.2m',[TablaGcias19[mes,ind].exedente],FS));
      //%16 cantidad de caracteres .2 es 2 decimales y m es que el argumento es currency
  end;
  sl.SaveToFile('SL_Tabla_2019.txt');
  OpenDocument('SL_Tabla_2019.txt');
  sl.Free;
end;


Algunas aclaraciones:

FS.CurrencyFormat: en este caso no importa el valor, porque lo anulo con FS.CurrencyString:=''. Pero ya que estamos, lo valores para este campo (CurrencyFormat) pueden ser:
0 : $1
1 : 1$
2 : $ 1
3 : 1 $

En este caso TablaGcias19 es una matriz del registro TTablaGcias

TTablaGcias = record
  fijo:Currency;
  exedente:Currency;
  alicuota:Currency;
end;


Y con muy poco código se obtiene esto:



Al final se libera el TStringList y de la variable FS que usamos para FormatSettings nos olvidamos ya que es local a la función y del tipo record.

domingo, 9 de junio de 2019

TZConnection.ExecuteDirect y TZQuery.

Estos dos componentes pertenecen a Zeos Lib.
TZConnection se utiliza primariamente para establecer una conexión a una base de datos. Entre sus métodos está ExecuteDirect, una función sobrecargada (overloaded). Nada mejor que ver el código fuente:

function ExecuteDirect(SQL:string):boolean;overload;
function ExecuteDirect(SQL:string; var RowsAffected:integer):boolean;overload;


Si únicamente enviamos un parámetro, el string con la sentencia SQL, entonces la primera será llamada. Si usamos los dos parámetros, entonces se llamará a la segunda. Breve aclaración de overload.

Generalmente ExecuteDirect se utiliza para todo lo referido a actualizar la base de datos, por ejemplo: UPDATE, CREATE, DELETE, INSERT, VACUUM.

Como vemos, esta función siempre retorna un boolean en ambas versiones, que será True si hubo éxito o False si hubo un error.
Si además usamos el segundo parámetro, que como observamos es por referencia, el mismo se actualizará con el número de filas afectadas.

Por ejemplo, si mandamos VACUUM, siempre devolverá 0 (cero); en cambio si utilizamos un UPDATE nos devolverá la cantidad de registros actualizados.

Una vez más, veamos el código fuente de la implementación de las funciones:

{**
Executes the SQL statement immediately without the need of a TZQuery component
@param SQL the statement to be executed.
Returns an indication if execution was succesfull.
}


function TZAbstractConnection.ExecuteDirect(SQL : String) : boolean;
var
  dummy : Integer;
begin
  result:= ExecuteDirect(SQL,dummy);
end;

{**
Executes the SQL statement immediately without the need of a TZQuery component
@param SQL the statement to be executed.
@param RowsAffected the number of rows that were affected by the statement.
Returns an indication if execution was succesfull.
}


function TZAbstractConnection.ExecuteDirect(SQL: string; var RowsAffected: integer):boolean;
var
  stmt : IZStatement;
begin
  try
    try
      CheckConnected;
      stmt := DbcConnection.CreateStatement;
      RowsAffected:= stmt.ExecuteUpdate(SQL);
      result := (RowsAffected <> -1);
    except
      RowsAffected := -1;
      result := False;
      raise; {------ added by Henk 09-10-2012 --------}
    end;
  finally
    stmt:=nil;
  end;
end;


Ahora veamos la siguiente línea de código:

if ZConnection1.ExecuteDirect(Memo1.Text, n) then Memo1.Lines.Add('OK! '+IntToStr(n)+' filas.');

Sí bien es casi rídiculo hacer esto, un SELECT con ExecuteDirect, se puede, claro que siempre retornará cero, aunque la tabla tenga 500 filas.
En realidad también se puede, por ejemplo usar VACUUM desde un consulta TZQuery. El tema es saber cual de los dos métodos utilizar según lo que necesitemos, desde ya, una consulta será con TZQuery. Un INSERT puede ser tanto con ExecuteDirect o con los métodos de TZQuery, nuevamente, según lo que necesitemos y el estilo propio de cada programador.


Este UPDATE .. SET en realidad no hace nada, pero es válido como ejemplo, ExecuteDirect devuelve 51 en la variable n pasada por referencia, que desde ya, coincide con la cantidad total de filas de la tabla.

TZQuery:

De entrada conviene aclarar una especie de mito que hay de que siempre debe estar asociada con componente TDataSource, esto es falso, así de simple. Solo necesitaremos un TDataSource si los datos de la consulta deben ser mostrados en otros componentes, como ser un TDBGrid, TDBEdit, TDBLookUpComboBox, etc. Muchas veces veo ejemplos de LazReport donde se incluye innecesariamente un TDataSource.

Otra cosa elemental que no se debe intentar hacer, es editar una consulta que contiene JOIN, el error será inevitable. Es un error muy común tener una consulta con JOIN en un TDBGrid y querer actualizar una fila.

Para insertar un registro en una consulta del tipo SELECT campos FROM tabla (y puede también contener WHERE pero nunca JOIN), primero se debe invocar al método INSERT de TZQUery:

ZQuery1.Insert;

Luego, lo más común es utlizar el método FieldByName:

ZQuery1.FieldByName('nombre').asString:=edNombre.Text;
ZQuery1.FieldByName('edad').asInteger:=nEdad;


Y finalmente se usa el método Post para concretar la transacción. Si la propiedad Autocommit de TZConnection es True, entonces la transacción será inmediata:

ZQuery1.Post;

Un campo INTEGER con AUTOINCREMENT: al hacer un INSERT debe ignorarse siempre, ya sea utilizando ExecuteDirect o TZquery.

También puede utilizarse un TZQuery para un UPDATE, SET, etc.:


DataM.ZQa.Close;
DataM.ZQa.SQL.Text:='UPDATE reg SET saldo=saldo-:importe WHERE banco=:elbanco AND fecha>:regfecha;';
DataM.ZQa.Params.ParamByName('elbanco').AsInteger:=elbanco;
DataM.ZQa.Params.ParamByName('regfecha').AsString:=regfecha;
DataM.ZQa.Params.ParamByName('importe').AsCurrency:=importe;
DataM.ZQa.Open;
DataM.ZQa.Close;

Ventaja: se puede usar Params, algo que no se puede con ExecuteDirect.

sábado, 8 de junio de 2019

Mezclar un string.

Es decir, un random de una cadena de caracteres. Lo publico porque luego de hacer esta función se me ocurrió buscar en Internet cómo lo habían resuelto otros colegas o sí ya existía una función para tal fin. Pues resulta que no, sí lo hay para stringlist, pero eso es otra cosa. Si bien podría optimizarse un poco más, por tratarse de una función chica, creo que así está bien. Podría evitarse la variable q por ejemplo y el SetLength del array hacerlo en base Length(cl).
En definitiva esta función recibe un string y devuelve un string con los mismos caracteres pero cambiados de lugar, por ejemplo puede recibir "casa" y retornar "aacs".

function Mezclar(const cl: string): String;
var
  i,j,rand,indarray,q:Integer;
  res:String;
  aInd:array of Integer;
  yaesta:Boolean;
begin
  q:=Length(cl);
  yaesta:=False;
  SetLength(aInd,q);
  res:='';
  i:=Length(cl);
  indarray:=0;
  repeat
    rand:=Random(q)+1;
    for j:=Low(aInd) to High(aInd) do
      if aInd[j]=rand then yaesta:=True;
    if not(yaesta) then
      begin
        res:=res+cl[rand];
        aInd[indarray]:=rand;
        Inc(indarray);
        Dec(i);
      end;
    yaesta:=False;
  until i=0;
  Result:=res;
end;


Para los novatos o quienes están medios oxidados o retornando a la programación, puede ser un interesante ejercicio.

¿Qué me llevó a hacer esta función? Un programa de generación de contraseñas, del tipo que le permite al usuario especificar la cantidad de mayúsculas, minúsculas, números y caracteres especiales (símbolos), en una parte tenía los randoms de esta forma "GYEBkfuenlow4982($*!" entonces eso se lo paso a esta función y listo, me altera las posiciones, sería un random de un random "ordenado" o algo así.

Aclaración: Randomize lo llamo en el Form.Create por eso no está en la función. Se recomienda utilizar Randomize solo una vez.