martes, 10 de octubre de 2017

Lazarus: guardar la disposición de las ventanas

Prescindiendo de algún paquete tipo anchor docking porque simplemente en Linux con GTK2 al menos no me funciona y nunca le di mucha importancia al asunto. Hasta que descubrí, leyendo en el foro de Lazarus, donde se aprende muchísimo, dicho sea de paso, que podemos ubicar y dimensionar las ventanas como más nos guste y simplemente guardar el escritorio (desktop). No solo eso, podemos guardar todos los escritorio que queramos.

Primero abrimos todas las ventanas (Explorador del proyecto, Inspector del código, etc.) que queremos, las distribuimos convenientemente, fijamos su tamaño y cuando está todo como nos resulte más cómodo, guardamos el escritorio.


Ítem Desktops en el menú Herramientas.


Presionando el botón del diskette guardamos un nuevo escritorio, el famoso "guardar como" o "Save as...".

Diferencia entre Free y FreeAndNil

Básicamente Free libera el objeto, pero no el puntero, es decir, el puntero sigue apuntando a una dirección de memoria (inutilizable ya). Para evitar esto está FreeAndNil que además de liberar el objeto le asigna nil al puntero, por ende, si preguntamos si esta asignado nos devolverá False, de la otra forma, Assigned nos devolvería True.
Hay varias discusiones en torno a FreeAndNil, argumentos a favor y en contra. Depende la situación, a veces en un escenario particular conviene no utilizarlo.

Resumiendo, Free libera al objeto y FreeAndNil además de liberarlo le asigna Nil al puntero.

Ejemplo:

var
  objeto : TBitmap;
begin
  objeto := TBitmap.Create;
  //... Se hace algo con el objeto ...
  objeto.Free;
  if Assigned (objeto) then
    ShowMessage ('Sí, está asignado.')
  else ShowMessage ('No está asignado');
end;


Esto mostrará 'Sí, está asignado'.

Con FreeAndNil:

var
  objeto : TBitmap;
begin
  objeto := TBitmap.Create;
  //... Se hace algo con el objeto ...
  FreeAndNil (objeto);
  if Assigned (objeto) then
    ShowMessage ('Sí, está asignado.')
  else ShowMessage ('No está asignado');
end;


Esto mostrará 'No está asignado'.

Conclusión final: en la mayoría de los casos usar solo Free es correcto y también lo es usar FreeAndNil.

Este es el procedimiento FreeAndNil:

procedure FreeAndNil(var obj);
var
  temp: tobject;
begin
  temp:=tobject(obj);
  pointer(obj):=nil;
  temp.free;
end;
     

Como curiosidad aunque algo lógico, primero le asigna nil y luego hace el Free por razones más que obvias, digo curioso porque podría llamarse NilAndFree.

domingo, 8 de octubre de 2017

RecNo: obtener el registro actual de un DataSet

Un error muy común es intentar obtener y establecer el puntero de un DataSet desde la propiedad del mismo, ¿por qué? porque no está implementada y siempre retornará -1 o 0 (cero), se necesita acceder a ella a través de una clase descendiente que la implemente, por ejemplo, una query de SQL.

También es importante no confundir RecNo con RowID ni con la clave primaria ni con nada, es un puntero al registro actual y se manifiesta en forma de un número entero, un LongInt para ser más precisos, por ende puede almacenarse este valor en una variable del tipo Integer.

Ejemplo de forma incorrecta de leer la propiedad RecNo:

ZQuery.DataSource.DataSet.RecNo

La forma correcta es:

ZQuery.RecNo

Por ejemplo:

var
  RegistroActual:Integer;
begin
  RegistroActual:=ZQuery.RecNo;
  ZQuery.Close;
  //...Se hace algo....
  ZQuery.Open;
  ZQuery.RecNo:=RegistroActual;
end;

SQLiteStudio: mostrar más de 1.000 filas

El valor predeterminado (por default) de filas o registros a mostrar en una consulta (query) en SQLiteStudio es 1.000 (mil). Por eso cuando hacemos una consulta que arroja un resultado de más de mil registros, solo se muestran los primeros mil únicamente, como si se hiciera un LIMIT 1000;

Para modificar esto y cambiar ese valor por otro, ya sea mayor o menor, solo debemos acceder a la configuración a través del menú Tools, ítem Open configuration dialog. O presionando F2.


Ahora en la barra izquierda seleccionamos Data browsing.


En Number of data rows per page establecemos el número deseado de filas a mostrar y finalmente presionamos OK.

viernes, 22 de septiembre de 2017

Traducir diálogos y botones

¿Cómo hacer que el programa creado con Lazarus muestre los diálogos y botones en español?

Utilizando la unidad Translations y lafunción TranslateUnitResourceStrings.

En nuestra unidad principal del proyecto debemos incluir la unidad Translations de la manera habitual:

uses Translations

Luego podemos definir un procedimiento privado dentro del formulario de la misma unidad principal de esta forma:

private
    procedure Traducir;


Lo implementamos:

procedure TForm1.Traducir;
var
  Lang, FallbackLang: string;
begin
  Lang := 'es';
  FallBackLang := '';
  TranslateUnitResourceStrings('LclStrConsts','lclstrconsts.es.po', Lang, FallbackLang);
  TranslateUnitResourceStrings('lr_const','lr_const.es.po', Lang, FallbackLang);
  TranslateUnitResourceStrings('printer4lazstrconst','printer4lazstrconst.es.po', Lang, FallbackLang);
end;


Aclaraciones:

lclstrconsts.es.po: ya viene incluído con Lazarus, traduce los diálogos y botones.

lr_const.es.po: Solo si se utiliza LazReports, el archivo está incluido en el paquete.

printer4lazstrconst.es.po: es para traducir los diálogos de impresión, que no están traducidos en Lazarus 1.6, por lo que lo traduje y lo posteé en el foro de Lazarus para que lo agreguen, desconozco si está incuído en versiones posteriores a la 1.6, por ende es probable que si no se posee el archivo printer4lazstrconst.es.po o bien puede tirar un error o simplemente no traducir.

Finalmente debemos hacer la llamada al procedimiento, sino, no pasa nada:

procedure TForm1.FormCreate(Sender: TObject);
begin
  Traducir;
end;


Si esto no funciona de la forma esperada hay dos opciones: una es estudiar a fondo el tema empezando por la wiki: http://wiki.freepascal.org/Translations_/_i18n_/_localizations_for_programs/es . La otra es copiando esos 3 archivos en la carpeta de nuestro proyecto y listo.

Los dos primeros archivos, como ya dije, vienen incluídos, por lo tanto solo hay que encontrarlos y copiarlos.

En esta carpeta están los 3 archivos, si se está usando la versión 1.6 de Lazarus se pueden usar sin problemas todos. Para el caso de los diálogos de impresora, pueden usarse de manera segura en versiones 1.6 y anteriores.

Exit, el procedimiento que devuelve un valor.

Caso curioso el del procedure exit que devuelve opcionalmente un valor, como si fuese una función. Claro que esto es muy práctico cuando se realizan funciones de validación de datos. Veamos la definición oficial de Exit:

procedure Exit(

const X: TAnyType

);


Exit sale de la subrutina y retorna el control a la rutina que lo llamó. Si se le llama desde la unidad principal, finaliza la ejecución del programa. El argumento opcional X permite especificar un valor de retorno, en el caso de que se invoque desde una función, siendo entonces result = X.

Ejemplo:

Function Validar ( a, b, c: integer ) : boolean;
begin
  if a=b then Exit (True);
  if b>c then Exit (True);
  Exit (False);
end;


Si a=b o b>c pudo haber ido en una sola sentencia pero lo separé para que quede más claro el ejemplo.
Si a=b retorna True y el resto no se ejecuta, lo mismo si b>c. Si no se cumplen las condiciones previas entonces retorna False.

domingo, 17 de septiembre de 2017

ZQuery y el bug en valores del tipo boolean en SQLite.

TZQuery es un componente de ZeosLib que tiene un error, al guardar un valor del tipo boolean, lo hace como Y/N en lugar 0/1.

Por ejemplo:

ZQuery1.FieldByName('puntual').asBoolean:=True;

Almacena en la columna puntual el valor Y en lugar de un 1.

Luego si se realiza una consulta ... WHERE puntual ... o WHERE NOT(puntual) no se obtendrán los resultados deseados, habrá que cambiar la consulta por WHERE puntual='Y' o WHERE puntual='N'.

Por qué SQLite lo acepta? Porque SQLite tiene la famosa particularidad de trabajar los tipos de datos por afinidad, para SQLite en definitiva es un caracter, ni siquiera le importa que no sea númerico, acepta Y/N y también una M. Error de SQLite? No, guste o no, con ventajas y desventajas, SQLite es así.

Error de ZeosLib? Sí, de hecho el mismo fue reportado (ver ticket) y solucionado para versiones 7.2 y obviamente, posteriores.

Soluciones, varias:

Conviviendo con el bug, podría usarse asInteger, por ejemplo:

ZQuery1.FieldByName('puntual').asInteger:=1;

No lo he chequeado pero debería funcionar.

Actualizar ZeosLib a una versión 7.2 o posterior.

Poner esta opción en el componente, comunmente ZQuery:

ZQuery.Properties.Values['BindOrdinalBoolValues'] := 'True';

Esto se puede hacer también desde el inspector de objetos, en Properties. 
Solución que obtuve desde el ticket del bug, aportada por uno de los desarrolladores de Zeos. Y teniendo en cuenta que hoy, la última versión estable es la 7.1.4 es una buena alternativa para quienes optamos siempre por versiones estables.

jueves, 7 de septiembre de 2017

SQLite – Triggers: explicación y ejemplos

Un trigger es un evento que se lanza cuando ocurre algo determinado en una base de datos y se ejecuta una sentencia SQL. Ese “algo” que ocurre puede ser que se borre un registro (fila) de una determinada tabla, que se agregue un registro o que se modifique uno o más campos de una tabla, es decir, un INSERT, DELETE o UPDATE.

¿Cuándo se declara el trigger? Puede declararse en cualquier momento y siempre de acuerdo a las necesidades del caso, siendo lo normal hacerlo al momento de crear las tablas, como los índices y constraints.

No es necesario que las tablas actuantes en el trigger estén relacionadas mediante una Foreign Key.

Existe una IDE para SQLite muy práctica para este tipo de cosas, especialmente hasta que le tomemos la mano, nos es de gran ayuda, que es SQLiteStudio (está en inglés) y es de código abierto.

Uno puede, no obstante, prescindir de los triggers de SQLite y hacerlo a mano, es decir, mediante programación, en mi caso, con Free Pascal desde Lazarus, de hecho lo estuve haciendo hasta ahora, por “seguridad”, por preferir tener el control total especialmente en cuanto a la validación de por ejemplo valores duplicados, todavía lo hago, no me llevo bien con el manejo de excepciones, además SQLite carece de Stored Procedures, de momento son excusas válidas. Pero para borrar un registro en otra tabla, actualizar un dato, o agregar, casos simples, empecé a utilizar triggers, más precisamente hace dos días y por suerte además de funcionar correctamente, no es algo difícil de implementar.

Los ejemplos son casos reales de un programa de control de proveedores e insumos.


INSERT:

Un trigger que cree un registro en otra tabla cada vez que el usuario da de alta un insumo.

CREATE TRIGGER agregoinsu
         AFTER INSERT
            ON prod
BEGIN
    INSERT INTO movinsu (
                            idcompra,
                            idprod,
                            fecha,
                            cantidad,
                            precio,
                            total,
                            saldo
                        )
                        VALUES (
                            0,
                            new.id,
                            '2016-01-01',
                            new.ini,
                            0,
                            0,
                            0
                        );
END;


Al trigger lo nombré “agregoinsu”, luego hay que indicar cuando se dispara el trigger, antes o después y antes o después de ¿qué?, en este caso después (AFTER) de que se produjo el INSERT. ¿en qué tabla?, ON prod (en la tabla prod). Nota: la tabla prod debería llamarse insumos, pero se llama prod por motivos que no vale la pena aclarar. Hasta acá definimos cuando se lanza el trigger, ahora debemos indicar que se hace y eso lo hacemos mediante sentencias SQL comprendidas entre BEGIN y END; En este caso se indica que se inserte un registro en la tabla “movinsu” que posee un campo autoincremental “id” que se omite porque se encarga SQLite, el resto de las columnas o campos los especifico. Ahora debo definir los valores de esa nueva fila en “movinsu” y acá aparece la palabra new. Para triggers de inserción, se utiliza new, para triggers de eliminación se utiliza old y para triggers de actualización se pueden utilizar ambos (new y old). “new.id” significa que el nuevo registro en movinsu, en la columna idprod, irá el valor del nuevo (new) id (prod.id) y en la columna cantidad ira el valor “new.ini” que viene de prod.ini. Resumiendo, cada vez que se agregue, o dé de alta, un insumo (prod) se insertará una fila en la tabla movinsu con los valores especificados.

Este trigger debe definirse sobre la tabla que lanza el evento, en este caso, la tabla prod.

DELETE:

Lo contrario al ejemplo anterior, un trigger que elimine un registro en la tabla movinsu cuando se elimina un insumo de la tabla prod.

CREATE TRIGGER borroinsu
         AFTER DELETE
            ON prod
BEGIN
    DELETE FROM movinsu
          WHERE (idprod = old.id) AND
                (idcompra = 0);
END;


Este trigger es muy simple, AFTER (después) de DELETE (borrar) un registro ON (en) la tabla prod se ejecuta la sentencia SQL comprendida entre BEGIN y END, borrar de la tabla movinsu donde “old.id” sea igual a idprod (de la tabla movinsu). La otra condición idcompra=0 es porque con ese ID identifico el stock inicial; en realidad podría obviar esto, ya que antes de permitir borrar un insumos chequeo primero que no tenga movimientos registrados, si los tiene no permito su eliminación, pero por las dudas prefiero comprobarlo dos veces y no dejar que un trigger se dispare y borre alegremente todos los movimientos de un insumo, no nos olvidemos que cualquiera con un poco de conocimientos puede abrir la tabla y borrar un insumo y el trigger se dispara igual, por ejemplo, un usuario intenta borrar un insumo y el programa no lo deja porque tiene 20 registraciones, el usuario debe primero borrar todas las registraciones y luego borrar el insumo, puede tentarse de meter mano a la base de datos y borrarlo, pues bien, en este caso, el trigger con el condicional (idcompra=0) solo borrará el registro correspondiente al stock inicial dejando “vivas” las otras registraciones. Claro que si lo que se pretende es que el trigger arrase con todo quedaría así:

CREATE TRIGGER borroinsu
         AFTER DELETE
            ON prod
BEGIN
    DELETE FROM movinsu
          WHERE (idprod = old.id);
END;



“old.id” es el campo “id” del registro que se borró. Como es un trigger del tipo delete, solo se puede utilizar old para referenciar un campo.

UPDATE:

En update se puede usar tanto old como new para referenciar campos. Este trigger se lanza cuando se modifica el stock inicial de un insumo (campo “ini”) en la tabla prod y modifica el campo “cantidad” en la tabla movinsu.

CREATE TRIGGER modificoinsu
         AFTER UPDATE OF ini
            ON prod
BEGIN
    UPDATE movinsu
       SET cantidad = new.ini
     WHERE (idprod = new.id) AND
           (idcompra = 0);
END;


Traduciendo un poco, se lanza después (AFTER) de una modificación (UPDATE) del campo “ini” de la tabla prod (ON prod) y se ejecuta la actualización (UPDATE) de la tabla movinsu, se establece (SET) el campo “cantidad” con el valor de “new.ini” (el nuevo stock inicial) y la claúsula WHERE es la misma que el ejemplo anterior. Si bien el campo “id” no cambia, se debe referenciarlo con new o con old.

Son tres ejemplos sencillos para iniciarse en el tema triggers.

Documentación consultada: (en inglés)

https://sqlite.org/lang_createtrigger.html

https://www.tutorialspoint.com/sqlite/sqlite_triggers.htm

Cambiar color de varios TEdit OnEnter OnExit

Si tenemos pocos (2 o 3) Tedit en un formulario, podemos rápidamente hacer uso de los eventos OnEnter y OnExit de cada edit y listo, total, son pocos. Pero si tenemos muchos TEdit cómo crear un evento para OnEnter y otro para OnExit que sirva para todos los Edits, sean 1, 2, 10 o 50. Simple, creamos dos procedimiento en la clase del formulario, no aparte del Form, sino como miembros de la clase Form.

Ejemplo:

procedure EntraEdit(Sender: TObject);
procedure SaleEdit(Sender: TObject);


Luego implementamos dichos eventos:

procedure TForm1.EntraEdit(Sender: TObject);
begin
 TDBEdit(Sender).Color:=clMoneyGreen;
end;

procedure TForm1.SaleEdit(Sender: TObject);
begin
  TDBEdit(Sender).Color:=clDefault;
end;


Ahora, desde el inspector de objetos, en la parte Eventos, en cada TEdit deberemos seleccionar en todos el evento OnEnter y OnExit asignando EntraEdit y SaleEdit respectivamente. Con esto logramos reducir la cantidad de código. Con esta optimización, otra ventaja es que si deseamos cambiar el color y tenemos 20 edits, solo necesitamos modificar una sola línea de código.

miércoles, 6 de septiembre de 2017

DBGrid: Formato de la columna según el tipo de campo

En tiempo de diseño y conociendo la tabla es una cosa, pero en tiempo de ejecución y sin saber nada de la tabla, ya es distinto. Para empezar nos encontramos con tipos de datos enumerados y por ende, debemos conocerlos. Luego hay que recorrer las columnas o campos y definir el formato según lo que necesitemos, siendo lo normal los tipos de dato numéricos y de fecha. Para el ejemplo se recorrerán las columnas de una tabla SQL y se cambiará el formato del DBGird si el campo es numérico decimal.

procedure TForm1.FormatoColumnasGrid;
var
  ind:Integer;
begin
  FormatSettings.DecimalSeparator:='.';
  for ind:=0 to ZQ.FieldCount-1 do
    if (ZQ.FieldDefs.Items[ind].DataType=ftFloat) or (ZQ.FieldDefs.Items[ind].DataType=ftCurrency)
    or (ZQ.FieldDefs.Items[ind].DataType=ftBCD) then
      DBGrid1.Columns[ind].DisplayFormat:='#0.00';
end;


Este procedimiento se llama luego de habilitarse el DBGrid. Se establece el separador decimal para establecer correctamente el formato aunque también se podría hacer de forma automática o preguntarle al usuario.

Se utiliza FieldCount – 1 porque la indexación del ZQuery (de ZeosLib) comienza por cero. DataType es del tipo enumerado, para SQL los valores de numéricos con decimales son: ftFloat, ftCurrency y ftBCD,

Listado de tipos de datos completo en español 

Documentación de TFieldType