Documentos de Académico
Documentos de Profesional
Documentos de Cultura
Dr. Marteens Trucos Delphi
Dr. Marteens Trucos Delphi
MARTEENS
1998-2000
TABLA DE CONTENIDO
TRUCOS ___________________________________________________________________ 5
3
4 Calling Dr. Marteens
Usted acaba de grabar una fila en una tabla SQL. Sabe, sin embargo, que existe un trigger en
el servidor que modifica ciertas columnas durante las grabaciones. Por lo tanto, le interesa
releer la columna después de la grabación. Puede que hasta el momento haya utilizado el mé-
todo Refresh para releer la columna ... sabiendo que éste método actualiza realmente todos los
registros que tiene en pantalla. ¿Alguna solución, Dr. Marteens? ¡Claro que sí! Utilice este par
de instrucciones para refrescar solamente el registro actual:
//...
Table1.Edit;
Table1.Cancel;
//...
El truco consiste en que el método Edit siempre relee el registro activo para asegurarnos de
que estamos trabajando con la última versión del mismo. Luego, el método Cancel devuelve la
tabla del estado dsEdit al modo dsBrowse. El truco consiste en que el método Edit siempre
relee el registro activo para asegurarnos de que estamos trabajando con la última versión del
mismo. Luego, el método Cancel devuelve la tabla del estado dsEdit al modo dsBrowse.
7
Optimizar la operación Last
Este truco está pensado para InterBase, pero es muy posible que también se aplique a su ser-
vidor SQL. Imagínese una tabla de un millón de registros. Eso no es nada para InterBase, por
supuesto. Pero a usted se le ocurre dale mantenimiento en una rejilla. Todo bien, excepto que
cuando pulsa el botón que lo lleva a la última fila, aquello parece nunca acabar. En primer lu-
gar, ¿está usted cumpliendo con las reglas del juego?
Bueno, esto lo saben hasta en las Universidades. Y ya usted había tomado medidas al res-
pecto, pero esa tabla sigue tardando demasiado en llegar a la última fila...
¿Sabe cómo el BDE responde al comando Last? Pues abriendo un cursor con la siguiente ins-
trucción:
Estoy asumiendo, para simplificar que la tabla no está ordenada; tendrá que adaptar el truco en
ese caso. El ciudadano que escribe estas líneas pensaba que InterBase aprovechaba el índice
primario en este caso (un cursor descendente), aún cuando el índice que crea InterBase es
ascendente. Y ... estaba equivocado. Esto puede comprobarlo mirando el plan de optimización
que crea InterBase para esta consulta.
9
Extrayendo el SQLCODE
Cada sistema de bases de datos define una serie de códigos de error para cuando las opera-
ciones que deben realizar fallan. Estos códigos son específicos de cada formato: InterBase
tiene los suyos, Oracle también, MS SQL Server no podía ser menos... En ocasiones, puede
que le interese recuperar este código (llamado sqlcode) para tomar alguna decisión. Cuando
una operación sobre una base de datos falla en Delphi, se genera una excepción del tipo gene-
ral EDatabaseError. Si el error ha sido provocado por el BDE, y no por la capa de la VCLDB
con la que Delphi encapsula las llamadas al Motor de Datos, la excepción pertenece al tipo
particular EDBEngineError. Y dentro de los objetos de esta clase puede encontrarse el
sqlcode, mezclado con la interpretación independiente de la plataforma que hace el BDE de la
situación de error. La siguiente función bucea dentro de una excepción e intenta encontrar un
error generado en el servidor para devolverlo como valor de retorno; si no lo encuentra, de-
vuelve el código -1:
11
Obteniendo la lista de usuarios
de Paradox
Si quiere obtener la lista de usuarios que están utilizando Paradox en su red, utilice la función
DbiOpenUserList, que abre un cursor virtual con registros del tipo USERDesc. Recuerde incluir
las unidades BDE y DBTables, y que el BDE debe estar inicializado para que esta función
pueda ejecutarse:
13
Problemas comunes al instalar
un servidor SQL
El otro día tuve que instalar una mini-red en casa: un servidor con Windows NT, mi superorde-
nador con Windows 95 (OSR 2.5) y un portátil con el mismo ¿sistema operativo? Digamos que
sí. Logré instalar Oracle 8, una beta de SQL Server 7 e InterBase 5.1. Me parece que puede
ser de interés que os cuente los principales problemas de configuración relacionados con la red
y los servidores SQL con los que tropecé.
Pude instalar Windows NT en esta máquina con muchos menos problemas que los que nor-
malmente tengo al instalar Windows 9X (tarjetas, drivers y todo eso). Como quería acelerar al
máximo el sistema, tomé la decisión de instalar solamente el protocolo TCP/IP: nada de
NetBEUI ni de IPX/SPX. Utilicé una dirección IP fija, tanto para el servidor como para los otros
dos equipos. Y nada tampoco de DHCP (consejo de mi colega Valentín) ni de WINS.
Tengo módem tanto en el portátil como en el superordenata. Así que estas dos máquinas tie-
nen, adicionalmente, activada la DNS (Domain Name Service), el servicio que permite traducir
nombres de dominios de la Internet a direcciones IP. Como sabéis, al configurar DNS para
Internet y correo electrónico hay que mencionar la dirección IP de un servidor remoto, que es el
ejecuta el servicio. Hasta aquí lo típico, ¿no?
Bien, el problema llegó cuando instalé Oracle 8 e InterBase en el servidor. La instalación fue
como una seda, y desde el propio servidor las conexiones se realizaban de maravillas. Pero
cuando trataba de conectar desde cualquiera de las otras dos máquinas ... ¡fracaso total! El
cliente se quedaba estupendamente colgado (lo mismo IB que Oracle). Aquellos que han ins-
talado InterBase (por ejemplo) saben que ésta es una instalación muy simple, y que casi todo
funciona bien a la primera.
Pues sí, en mi caso sí. Esto descartaba que hubiera algún problema físico en la red, o algún
conflicto de direcciones. Paso siguiente: fui al Explorador de Windows del portátil, a ver si podía
ver a los restantes equipos ... ¡y sí podía verlos sin problemas! Por supuesto, había instalado el
servicio de "compartir impresoras y archivos".
Aquí era donde estaba el razonamiento equivocado: si el cliente (el portátil, por ejemplo,
que se llama IAN) podía ver al servidor (os presento a CHRISTINE) en el Explorador de
Windows, suponía yo que contaba con algún mecanismo misterioso para convertir nombres de
máquinas en direcciones IP. ¡Esto solamente es cierto a medias! Si tienes instalado NetBEUI,
al parecer esto es así, pero si no, la resolución de nombres solamente funciona para los servi-
cios de compartir ficheros. No para un humilde ping. Así que si tecleaba desde el portátil: ping
MICHELLE, podía esperar sentado.
Lo curioso es que la conexión no fallaba. Yo esperaba que lo hiciera con algún mensaje del tipo
"Vale, chaval, pero ¿quién esa tía, la MICHELLE?". Y lo que al parecer pasa en estos casos es
que la máquina intenta resolver el nombre utilizando DNS. Por supuesto, si el servidor DNS
está al otro lado de la línea de teléfono y no tienes abierta la conexión, el cliente espera pa-
cientemente a recibir respuesta.
15
16 Calling Dr. Marteens
La solución: Reconozco que es una tontería, pero me hizo perder un par de horas, maldecir a
quien sabéis y pensar mal de mi parche de WinSock 2. ¡Claro que hay que configurar un fi-
chero HOSTS! Quítele la extensión al fichero HOSTS.SAM del directorio Windows (SAM quiere
decir sample, es decir, muestra o ejemplo). Siga las instrucciones y por cada ordenador que
esté presente en la red y del cual quiera utilizar algún servicio TCP/IP, introduzca una línea con
su dirección IP y su nombre lógico. Copie este fichero en el directorio Windows de cada má-
quina. En el servidor NT debe modificar LMHOSTS, que está en:
WinNT\System32\Drivers\Etc
(no estoy muy seguro de esto último, y pido perdón si estoy escribiendo alguna soberana tonte-
ría). Y por supuesto, con esto llegamos al final feliz. Ahora puedo conectarme en casa con
Oracle, InterBase y SQL Server ... y estoy pensando montar una copia de evaluación de DB2.
Indices únicos en dBase 7
¡Sorpresa! Tuve que impartir un curso hace poco utilizando dBase como "sistema de bases de
datos". Definí las tablas de mi ejemplo, les definí su índice primario como único, utilizando para
esto Database Desktop. Bien, ya sabemos que por algún despiste de nuestros amigos de
Inprise, seguimos sin poder crear tablas en el formato más moderno de Visual dBase 7, a pe-
sar de que basta tener un BDE con versión igual o superior a la 4.5. No obstante, para mí fue
totalmente inesperado que las tablas que creé permitían insertar varios registros con la misma
clave teóricamente única. Es más, ¡los registros anteriores desaparecían como si se los hu-
biera tragado la tierra!
Después de darle un par de vueltas al asunto, encontré que el tipo IDXDesc, del API de bajo
nivel del BDE, había cambiado de repente la interpretación de uno de sus campos, llamado
bUnique. Antes, este campo de tipo WordBool podía tener uno de los valores 1 ó 0: único o no.
Sin embargo, ahora permite también el valor 2. El problema es que 2 se utiliza para los índices
verdaderamente únicos: aquellos que protestan al intentar insertar un duplicado, mientras que
el antiguo 1 indica que en el índice aparece una sola entrada para cada clave. Al insertar una
clave repetida en este último tipo de índices, sencillamente se elimina la clave anterior y se
sobrescribe con la nueva (iba a escribir ¡cojonudo!, pero la decencia me impide utilizar palabras
tan groseras).
Conclusión: Si no quiere tener que crear sus índices con el API del BDE, es conveniente que
disponga de un Visual dBase 7 (es relativamente barata la versión Professional) para crear las
tablas con las que Delphi trabajará más adelante.
Más adelante, pondré en esta misma página código para corregir este problema. También he
encontrado cambios en la forma en que se crean restricciones de integridad referencial para
Paradox. Lo que antes funcionaba bien, ya no funciona...
17
Creación automática de
componentes
Admito que el título de este truco puede prestarse a confusión. Me explico: el propósito de este
mini-artículo es ofrecer consejos al desarrollador de componentes para que identifique eta-
pas y técnicas de la creación de componentes que son completamente mecánicas; tan mecáni-
cas que pueden diseñarse asistentes (¡de hecho, ya existen!) que realicen estos pasos por
usted. El asistente para la creación de componentes que trae Delphi ya automatiza la parte
inicial de la definición, creando una clase derivada del ancestro que usted elige, y declarando
sus secciones principales. Bien, mi teoría es que se puede ir mucho más allá. Espero que las
técnicas que presentaré no sean del todo triviales. Voy a enumerarlas a continuación:
¿Qué quiere decir que la propiedad sea "propiedad" del componente. Aquí me he liado
al intentar traducir al castellano. El segundo uso de "propiedad" se refiere a ownership:
¿debe el componente destruir el objeto apuntado al ser él mismo destruido? En tal
caso, se dice que el componente es el "propietario" (owner, o dueño) del objeto al que
apunta la propiedad. Por ejemplo, un cuadro de listas TListBox tiene una propiedad
Items, de tipo TStrings, que apunta a un objeto derivado de esta clase:
Por el contrario, existen propiedades de tipo clase que apuntan a objetos con vida
independiente a la del componente. Por ejemplo, tenemos la propiedad FocusControl
del componente TLabel:
19
20 Calling Dr. Marteens
¿Cómo sabemos si la propiedad que estamos creando debe ser independiente o no?
No existe una regla completamente mecánica, pero las siguiente pista ayuda:
Una vez que hemos realizado esta identificación, podemos pasar a programar mecáni-
camente los puntos que resumo a continuación:
HexView1.Font.Style := [fsBold];
El problema es que esta asignación "salta" por encima del método de acceso de escri-
tura SetFont, nunca llega a invocarlo. ¿Cómo nos damos cuenta de que ha cambiado
el tipo de letra? Resulta que TFont dispara un evento OnChange para estos casos.
Nuestro componente THexView debe crear un método, preferiblemente privado, en
este plan:
El enlace entre el objeto Font y este receptor se debe realizar durante la construcción
del objeto:
Recuerde que estas propiedades deben ser poseídas por el componente, así que su-
mamos estos cambios a la lista de cosas por programar que he presentado en el punto
número uno.
Voy a intentar crear un experto para la creación de componentes que automatice todas estas
tareas y algunas más. Cuando lo tenga, lo pondré en la página de ejemplos.
Colecciones y persistencia
A pesar de su sencillez, uno de los aspectos menos comprendidos de la Programación de
Componentes para Delphi es el uso de colecciones (collections). Hace poco descargué
desde Internet un componente freeware para mejorar el trabajo con rejillas, al estilo de
TStringGrid. El componente funcionaba bastante bien; de hecho, estaba inspirado en el
TDBGrid oficial de Delphi, como reconocía el autor. El problema sobrevino cuando intenté
configurar las columna de la nueva rejilla en tiempo de diseño: aunque el componente tenía
una propiedad Columns, no estaba publicada para tiempo de diseño, y había que configu-
rar toda esta información "a mano", en tiempo de ejecución. El autor se había cepillado
todo el código relacionado con el almacenamiento y edición de la información vectorial de
columnas. Claro, el código utilizaba esas "misteriosas colecciones"...
COLECCIONES Y ELEMENTOS
Si usted quiere crear una propiedad que almacene un número variable de objetos relacio-
nados, es muy probable que lo que necesite sea una colección. ¿Una colección? ¿Por qué
no una lista TList? Muy sencillo: TList desciende directamente de TObject, mientras que
TCollection desciende de TPersistent, por lo que ya trae incorporado las técnicas básicas
para que toda la colección o lista de elementos pueda almacenarse en disco, o en cual-
quier flujo de datos (stream) en general. Basta con que definamos una propiedad, cuyo tipo
esté basado en TCollection, en la sección published de un componente para que Delphi o
C++ Builder puedan almacenar sus datos en un DFM, y para que la propiedad aparezca en
el Inspector de Objetos, del mismo modo que si se tratase de un vulgar Integer o String.
type
TCollectionItem = class(TPersistent)
// Etcétera
end;
Esto quiere decir que los elementos que podemos almacenar dentro de una colección de-
ben pertenecer a alguna clase derivada de TCollectionItem. Por ejemplo, no podemos al-
macenar objetos TTable directamente en una colección. Por el contrario, debemos crear
una clase auxiliar TTableItem, que herede de TCollectionItem, y que apunte internamente a
una tabla.
Recuerde que el objetivo final de todo esto es poder almacenar listas de elementos más o
menos arbitrarios en los ficheros DFM. Usted puede directamente crear propiedades que
apunten a tablas (TTable) y, como esta clase deriva de TPersistent, Delphi puede automá-
23
24 Calling Dr. Marteens
ticamente almacenar en un DFM los datos necesarios para reconstruir un objeto de esta
clase. Más exactamente, lo que Delphi almacena en el DFM son los valores de las propie-
dades published del objeto. Se supone que estas son las propiedades "características" de
la instancia.
Por lo tanto, Delphi también sabe cómo guardar las propiedades que definamos como
published en una clase derivada de TCollectionItem. Si queremos guardar listas de punte-
ros a tablas, necesitamos una clase auxiliar definida de este modo:
type
TTableItem = class(TCollectionItem)
private
FTable: TTable; // Recuerde que esto es un puntero
procedure SetTable(Value: TTable);
// Etcétera ...
published
property Table: TTable read FTable write SetTable;
end;
Cuando Delphi vaya a guardar una colección de estos objetos, recorrerá uno a uno los
elementos de la colección, y los irá guardando individualmente. Para este objeto en parti-
cular, guardará el valor de la propiedad Table. Esta propiedad, que es concretamente un
puntero a un componente, se almacena en el DFM utilizando el nombre del componente
apuntado, junto al nombre del formulario o módulo en que se encuentra, si no está en el
mismo formulario o módulo que el componente que contiene a la colección.
Sin embargo, no terminan aquí nuestras responsabilidades. Es muy importante que redefi-
namos el método virtual Assign. De este modo, Delphi sabrá cómo copiar las propiedades
importantes desde un elemento a otro. El código que viene a continuación sirve de mues-
tra:
¿Se ha fijado en que cuando crea una columna para una rejilla de datos, la columna apa-
rece en el Editor de Columnas con el título TColumn, mientras que cuando a la columna le
asigna uno de los campos del conjunto de datos asociado en el Editor de Columnas apa-
rece el nombre del campo? Pues ése es el objetivo de GetDisplayName: indicar con qué
nombre aparecerá el elemento de la colección al editarlo con ayuda del Inspector de Obje-
tos.
Por último, si necesita un constructor para realizar alguna inicialización especial, tenga en
cuenta que el constructor de TCollectionItem es un constructor virtual, y que no puede mo-
dificar sus parámetros. Esta es su declaración:
RETOCANDO LA COLECCIÓN
DBGrid1.Columns[1]
Sin embargo, si vamos a programar bastante con la colección, es preferible derivar una
clase a partir de TCollection y, además de redefinir GetOwner, introducir una propiedad
vectorial por omisión, de nombre Items. TCollection ya tiene una propiedad con este nom-
bre:
Pero como notará, el tipo de objeto que devuelve es el tipo general TCollectionItem, lo que
nos obligará a utilizar constantemente la conversión de tipos. Además, esta propiedad no
es la propiedad vectorial por omisión de la clase. Para acceder a una de las tablas de la
lista de tablas de nuestro hipotético componente, tendríamos que teclear una y otra vez co-
sas como ésta:
TTableItem(MiComponente.Tablas.Items[1]).Table.Open;
Así que es frecuente que ocultemos la antigua propiedad Items en la nueva clase del si-
guiente modo:
type
TTableCollection = class(TCollection)
private
function GetItems(I: Integer): TTable;
procedure SetItems(I: Integer; Value: TTable);
// Etcétera ...
public
property Items[I: Integer]: TTable read GetItems write SetItems;
default;
end;
1
www.tempest-sw.com
26 Calling Dr. Marteens
Ahora podemos abreviar la instrucción que hemos mostrado antes de este modo:
MiComponente.Tablas[1].Open;
Como en nuestro ejemplo lo importante era la tabla, y no el TTableItem, hemos hecho que
Items devuelva la tabla asociada al elemento. Es más común, no obstante, que devuelva el
puntero a todo el elemento.
Por supuesto, falta incorporar a la nueva clase dentro de nuestro componente, pero esto es
muy sencillo. Sólo tiene que recordar que la colección será propiedad del componente, y
que esto implica:
Existen, claro está, muchas más técnicas que podemos aprovechar en relación con las co-
lecciones. Por ejemplo, podemos redefinir el método Update de la colección, para que ésta
notifique a su propietario cuando se produzcan cambios en las propiedades de algunos de
sus elementos.
COMPARACION DE COLECCIONES
Para terminar este artículo, quiero presentarle una técnica interesante: ¿sabe usted cómo
Delphi y C++ Builder comparan dos colecciones entre sí, para ver si son iguales? La unidad
Classes define con este propósito la siguiente función:
Para mayor claridad, voy a definir un método auxiliar, en el formulario que queremos "des-
figurar", que será llamado posteriormente desde los dos manejadores de mensajes:
El método Titulo se llamará con un parámetro de tipo entero. Cuando sea igual a 1, nos in-
dicará que tenemos que dibujar sobre una barra de título activa; en caso contrario, la barra
estará inactiva. Observe, como punto interesante, que llamamos al procedimiento
SetWindowText. Esto se hace para evitar que se muestre el texto del título en la ventana.
Usted se preguntará, ¿por qué no asignar sencillamente la cadena vacía en la propiedad
Caption del formulario? Bueno, puede comprobar que aunque hagamos eso mismo, Delphi
se encargará de utilizar el nombre del formulario en el título. Sospecho que se puede elimi-
nar el título también en algún sitio más conveniente, por ejemplo, redefiniendo
CreateWindow, pero son la una de la madrugada, hora local, y no tengo ganas de probarlo.
¿Dónde está el código que debe dibujar los botones de la barra, el borde de la ventana y el
resto de la parafernalia? ¡Ah!, voy a dejar que Windows los dibuje por mí. Esta es la imple-
mentación de los manejadores de mensajes antes mencionados.
type
TForm1 = class(TForm)
// ...
27
28 Calling Dr. Marteens
private
procedure WMNCActivate(var M: TMessage); message WM_NCACTIVATE;
procedure WMNCPaint(var M: TMessage); message WM_NCPAINT;
// ...
end;
Eso es todo. Sin embargo, le debo una advertencia: este algoritmo ha sido probado con
Windows 95. Si utiliza Windows 98, posiblemente tenga que variar la llamada a
SetBkColor. Lo más probable es que necesite jugar un poco con SetBkMode, para que los
mensajes que se dibujen lo hagan transparentemente, sobre el gradiente de fondo de la
nueva interfaz. También es conveniente que advierta que se ha asumido una ventana con
un icono a la izquierda y tres a la derecha. Si oculta alguno de los mismos, deberá recal-
cular el tamaño del rectángulo.
El uso de const con parámetros
string
Como todos sabemos, Delphi permite utilizar el modificador const en la declaración de un
parámetro. Esto tiene un doble efecto:
El último punto es muy importante para poder programar aplicaciones eficientes. Cuando
un parámetro cuya representación ocupa más de 4 bytes se pasa por valor (es decir, sin
especificar var o const), la rutina debe realizar una copia del mismo, para que si se reali-
zan modificaciones sobre el parámetro, no sean visibles por el código que llama a la rutina.
Y, por supuesto, esta copia es una operación potencialmente costosa.
Por eso Borland introdujo el modificador const, inspirado en C++. Este tipo de traspaso
ofrece lo mejor de ambos mundos: permite pasar un puntero a la variable, lo cual es más
eficiente. Si queremos pasar una expresión, el compilador genera una variable auxiliar
oculta, evalúa la expresión y la almacena allí, y pasa la dirección de la temporal. Y se evita
la copia de la cadena, pues el implementador tiene prohibido escribir sobre la cadena.
Hasta aquí bien, ¿no? El problema es que al aparecer Delphi 2 se cambió la representa-
ción de las cadenas de caracteres. En vez de representarlas como un array de caracteres
directo, las nuevas versiones de Delphi y C++ Builder utilizan un puntero a dicho array.
Tanto si utilizamos const como si no lo hacemos, Delphi pasa un puntero a la rutina, y no
saca copia local de la cadena. ¿Tiene sentido seguir utilizado const? En primer lugar, re-
cuerde que siguen existiendo tipos de datos en Delphi que pueden seguir beneficiándose
de este modificador: los arrays y los records, por ejemplo. Pero también es beneficioso
seguir utilizando const con parámetros de cadenas.
¿Por qué? No es evidente, pero intentaré explicarlo en pocas palabras. Delphi evita la co-
pia de la cadena manteniendo un contador de referencias para cada cadena. Al asignar
una cadena a un parámetro o a cualquier otra variable, se incrementa este contador. Si al-
guien intenta modificar la cadena, Delphi comprueba que no haya más de una referencia a
la misma. De haberlas, se produce lo que Borland denomina copy-on-demand (copia por
demanda): ahora sí se crea una nueva copia de la cadena, sobre la cual se realiza la mo-
dificación.
29
Bitmaps, ventanas MDI y
subclassing
¿Cómo podemos dibujar un mapa de bits como fondo de una ventana madre MDI
(FormStyle = fsMDIForm)? No es tan fácil como puede parecer a simple vista. La dificultad
consiste en que una ventana madre MDI tiene "dos capas": el marco de la ventana, que
es la zona donde está la barra de título, los iconos, las barras de herramientas, el menú... y
la ventana cliente. Esta ventana corresponde a la zona rectangular interior donde se si-
túan las ventanas hijas, y es allí donde queremos dibujar.
La clase TForm de Delphi administra tanto al marco como al cliente. La propiedad Handle
de esta clase se refiere al identificador de ventana de la ventana marco creada por
Windows. Cuando la propiedad FormStyle vale fsMDIForm, la clase TForm también crea la
ventana cliente, y almacena su identificador en la propiedad ClientHandle. Pero, como es
poco frecuente que alguien necesite modificar el comportamiento del cliente, los autores de
la VCL no preocuparon de transformar los mensajes que el cliente recibe de Windows en
eventos disponibles para el desarrollador de Delphi. Así, por ejemplo, cuando se dispara el
evento OnPaint de un TForm, nos está indicando que la ventana madre ha recibido desde
la cola de la aplicación el mensaje WM_PAINT. Peor aún: supongamos que definimos un
manejador de mensajes para el formulario de este modo:
type
TVentanaPrincipal = class(TForm)
// ...
private
procedure WMPaint(var M: TMessage); message WM_PAINT;
// ...
end;
En tal caso, este WM_PAINT se refiere al mensaje que recibe la ventana madre, no la
ventana cliente. Conclusión: no existe una forma directa de interceptar los mensajes que
llegan a una ventana cliente MDI. Aunque los mensajes del cliente pasan por el procedi-
miento ClientWndProc de la clase TCustomForm, Borland decidió que este procedimiento
perteneciera a la sección private de la clase.
Sin embargo, no todo está perdido, pues echaremos mano de una "sucia" técnica utilizada
durante muchos años por los pioneros de la programación con el API de Windows: la
creación de subclases, o subclassing. ¿Cómo viene determinado el comportamiento de
un objeto de ventana en Windows? Por una rutina denominada procedimiento de ventana,
a la cual cada objeto interno de ventana de Windows hace referencia. Este procedimiento
de ventana recibe todos los mensajes de la ventana, y en la heroica época en que se pro-
gramaba para Windows utilizando C, la parte central de un programa consistía en suminis-
trar un procedimiento de ventana adecuado para las ventanas que creaba la aplicación.
Para ilustrar la técnica, voy a desarrollar un componente que en tiempo de ejecución rea-
lice este acto de prestidigitación por nosotros. He aquí la declaración del componente:
type
TimMDIBkg = class(TComponent)
private
FBitmap: TBitmap;
FForm: TForm;
procedure SetBitmap(Value: TBitmap);
procedure InternalClientProc(var M: TMessage);
31
32 Calling Dr. Marteens
protected
FClientInstance: TFarProc;
FPrevClientProc: TFarProc;
procedure Loaded; override;
property Form: TForm read FForm;
public
constructor Create(AOwner: TComponent); override;
destructor Destroy; override;
published
property Bitmap: TBitmap read FBitmap write SetBitmap;
end;
procedure TimMDIBkg.Loaded;
begin
inherited Loaded;
FClientInstance := MakeObjectInstance(InternalClientProc);
FPrevClientProc := Pointer(SetWindowLong(
Form.ClientHandle, GWL_WNDPROC, Integer(FClientInstance)));
end;
destructor TimMDIBkg.Destroy;
begin
Bitmaps, ventanas MDI y subclassing 33
Los mensajes tratados son WM_ERASEBKGND, que se dispara cada vez que hay que di-
bujar el "fondo" de una ventana, y WM_HSCROLL y WM_VSCROLL, que se lanzan cada
vez que se toca una barra de desplazamiento. Recuerde que si una ventana hija sale fuera
del área cliente, automáticamente aparecen barras de desplazamientos en esa zona. Ob-
serve también cómo, al final del método, se tratan los restantes mensajes invocando indi-
rectamente al antiguo procedimiento de ventana mediante CallWindowProc.
http://www.marteens.com/imMDIBkg.zip
Secuencias en Oracle
Errar es de humanos; incluso yo me equivoco también de vez en cuando. En la impresión
final de La Cara Oculta de Delphi 4 se deslizó, a última hora, un error, al describir cómo se
puede obtener el valor de una secuencia de Oracle (página 527). Resulta que el error ya
había sido detectado durante la fase de revisión, pero la página que entregué para imprimir
fue finalmente la incorrecta. De todos modos, en el CD-ROM del libro el código, evidente-
mente, es el correcto.
La idea es que podemos obtener valores secuenciales de este objeto mediante el pseudo
atributo NextVal. En el libro se incluía el siguiente trigger para demostrar cómo podía utili-
zarse NextVal:
Dual es una tabla especial, predefinida por Oracle, que siempre tiene una sola fila, y se uti-
liza en trucos sucios y retorcidos como este que acabo de mostrar.
35
Cosas que nunca debe hacer
con C++ Builder
Sinceramente, estoy sorprendido. Los ejemplos que he estado mirando de C++ Builder en
estos días, tanto en los manuales de Inprise como en libros que hay por ahí, son como
para no poder dormir. ¿El motivo? La falta de atención que le prestan al tratamiento de ex-
cepciones.
void TMastData::DeleteItems()
{
DeletingItems = True; // Suppress recalc of totals during delete
Items->DisableControls(); // for faster table traversal.
try
{
Items->First();
while(!Items->Eof)
Items->Delete();
}
catch(...)
{
DeletingItems = False;
Items->EnableControls(); //always re-enable controls after disabling
return;
}
DeletingItems = False;
Items->EnableControls(); //always re-enable controls after disabling
}
1. El error consiste en llamar a return dentro de catch. ¿Sabe lo que logra con esto?
Pues que la excepción ha quedado "atrapada" y muere. El usuario nunca se entera de
que ha sucedido un error. Pero no es esto lo peor. ¿Sabe usted desde dónde se va a
llamar a DeleteItems? Yo no lo sé; puede que en algún caso particular usted lo sepa,
pero es mala programación asumir estas dependencias dentro de un programa contro-
lado por eventos. Un método manejador de eventos (no es éste el caso) tiene la si-
guiente particularidad: después de terminar la parte visible de su ejecución, el contador
de instrucciones sigue moviéndose por el código interno de la VCL, y ahí usted no tiene
control de todo lo que sucede. ¿Sabe el peligro a que se expone cuando deja que la
VCL asuma que no hubo errores dentro de la respuesta al evento? La Tercera Regla
de Marteens dice: "Nunca mates una excepción para la cual no tengas una solución".
2. El mal aprovechamiento de recursos se refiere a que las dos instrucciones finales se
repiten, tanto en el catch como después del catch. Esto sucede porque C++ puro y
duro no tiene la instrucción try/finally de Delphi y Java ... y de C sin los dos signos de
adición. ¿Qué por qué me molesta la repetición de código? Lo de menos es que el pro-
grama sea un poco más grande. Lo de más es que cuando tengamos que modificar
algo en esas dos instrucciones, tendremos que trabajar por duplicado ... y existe el
riesgo de que ambos bloques pierdan la sincronía.
¿Qué hubiera hecho yo? Si me interesara ajustarme estrictamente a lo que permite el es-
tándar de C++, hubiera sustituido, en primer lugar, el return por un throw (el raise de
Delphi) para relanzar la excepción:
catch(...)
{
DeletingItems = False;
Items->EnableControls();
throw; // Esto es diferente
}
37
38 Calling Dr. Marteens
DeletingItems = False;
Items->EnableControls();
Pero no hubiera podido resolver la duplicación de código. Mi colega Dave viene utilizando
desde hace tiempo la cláusula __finally de las excepciones estructuradas de C (no C++) y
le ha funcionado de maravillas, sin problema alguno:
void TMastData::DeleteItems()
{
DeletingItems = True; // Suppress recalc of totals during delete
Items->DisableControls(); // for faster table traversal.
try
{
Items->First();
while(!Items->Eof)
Items->Delete();
}
__finally
{
DeletingItems = False;
Items->EnableControls(); //always re-enable controls after disabling
}
}
Lo que me preocupa es que éste no es el estilo empleado por la propia Inprise en sus ma-
nuales, pero repito: funciona sin problema alguno.
De todos modos, una de las causas más extendidas en Delphi y C++ Builder que hacen
necesario el uso de try/finally es la creación de objetos dinámicos locales a un método.
Una idea que se me ocurre es utilizar una técnica bastante extendida entre los programa-
dores de C++: los punteros inteligentes, o smart pointers. He visto esta técnica en C++
Builder en relación con las interfaces para la programación COM, pero no he encontrado la
correspondiente aplicación a la programación con la VCL día a día (si Borland la ha imple-
mentado o recomendado, debe haber escondido bastante bien la recomendación). Observe
la siguiente plantilla de clase:
template<class T>
class LocalVCL
{
private:
T* Instance;
public:
LocalVCL(T* t) :
Instance(t) {}
LocalVCL()
{ Instance = new T(NULL); }
~LocalVCL()
{ delete Instance; }
T* operator->() { return Instance; }
operator T* () { return Instance; }
};
Esta plantilla define dos constructores: en uno se le pasa un puntero a un objeto arbitrario
recién creado, y el otro no recibe nada, pero crea internamente un objeto. La clase con la
que se instancia la plantilla debe permitir en este caso el uso de constructores con un solo
parámetro: este el caso de cualquier componente de la VCL, que necesitan un owner o
propietario. A este constructor se le pasa el puntero NULL, pues si se trata de un objeto de
vida limitada, el propietario no tiene importancia alguna. En cualquier caso, el destructor
destruye la instancia asignada durante la construcción. Recuerde que cuando definimos
una variable de clase local a una rutina en C++, este lenguaje garantiza siempre su des-
trucción, aunque se produzcan excepciones. Es como si tuviéramos una instrucción
try/finally implícita.
Para redondear la clase, definimos el operador ->, para que cuando lo apliquemos a una
variable de tipo LocalVCL devuelva el objeto al cual apunta la instancia que contiene. El
otro operador se encarga de las conversiones de tipo, desde un LocalVCL al componente
Cosas que nunca debe hacer con C++ Builder 39
de la VCL asociado. En todos los casos, las definiciones de los métodos son inline, para
evitar código innecesario.
POST SCRIPTUM: C++ Builder sigue manteniendo la definición de una "vieja conocida", la
plantilla auto_ptr, que está definida en <memory>. A grandes rasgos, realiza la misma tarea
que mi LocalVCL, pero soporta más operadores. El siguiente ejemplo muestra cómo puede
utilizarse auto_ptr:
De todos modos, la diatriba anterior sigue estando justificada. Los ejemplos de programas con
la VCL en C++ Builder de Inprise, y de la mayoría de los libros que andan por ahí no sólo care-
cen de la más mínima elegancia, sino que muchos de ellos son además incorrectos.
Aumentando la seguridad de
Locate
¿Cuántas veces se ha equivocado en el nombre de un campo al utilizar Locate o Lookup?
Como tenemos que pasar el nombre de la columna sobre la que se realiza la búsqueda
como una cadena de caracteres, es muy fácil cometer errores tipográficos. Lo que es peor,
el compilador no se quejará y el fallo se producirá en tiempo de ejecución ... posiblemente
cuando estemos intentado convencer al cliente de que compre nuestra estupenda aplica-
ción.
Supongamos que queremos localizar un registro de una tabla dado su código. Esto es lo
que hacemos casi siempre:
Por supuesto, es necesario haber creado campos persistentes para el conjunto de datos.
También es cierto que la instrucción anterior genera un poco más de código y es ligera-
mente más lenta (por un factor constante despreciable). Pero merece la pena la seguridad
añadida de que no nos hemos equivocado en el nombre del campo.
41
InterBase y la semántica de
UPDATE
Menuda sorpresa me llevé hace poco con InterBase. Estaba mostrando un ejemplo de
trigger en un curso, y utilicé una instrucción parecida a la siguiente:
update Inventario
set Cantidad = Cantidad + new.Cantidad,
Coste = (Coste * Cantidad + new.Coste * new.Cantidad) /
(Cantidad + new.Cantidad)
where Codigo = new.RefInventario
Puede que a usted le parezca normal lo que le voy a contar ahora, pero a mí no me lo pa-
rece. La semántica que establece el estándar ANSI de SQL consiste en que cualquier valor
que aparezca a la derecha de una asignación en una cláusula set se refiere al valor que
tenía la columna antes de que se produzca cualquier otra asignación. Supongamos que
una tabla tiene dos columnas del mismo tipo, A y B. Entonces la siguiente instrucción debe
intercambiar el valor de ambas columnas:
update Tabla
set A = B, B = A
update Inventario
set Coste = (Coste * Cantidad + new.Coste * new.Cantidad) /
(Cantidad + new.Cantidad),
Cantidad = Cantidad + new.Cantidad
where Codigo = new.RefInventario
Pero en el caso más general, las cosas pueden complicarse. Por ejemplo, para intercam-
biar el valor de dos columnas sería necesario ejecutar un procedimiento almacenado ba-
sado en un bucle for/select:
¿Qué hacen los demás sistemas de bases de datos? Resulta que Oracle y SQL Server
hacen precisamente lo que exige el estándar: intercambiar las columnas. No pongo en
duda de que exista algún nivel del estándar que permita el comportamiento de InterBase,
pues el estándar SQL es demasiado permisivo. Pero es bueno que estemos alerta acerca
de estas pequeñas incompatibilidades entre las distintas implementaciones de un lenguaje
"estándar".
43
La VCL y el año 2000
Este "truco" es muy sencillo, pues aparece en las instrucciones de la instalación del Update
Pack 2 de Delphi, que casi todos los programadores deben tener. Sin embargo, he notado
que muchas personas ignoran la existencia de una nueva variable, llamada
TwoDigitYearCenturyWindow, que hay que ajustar para evitar algunos problemas relacio-
nados con el año 2000.
Esta variable está declarada en la unidad SysUtils, y tiene un valor inicial de 0. Cuando
tiene un valor distinto de 0, se utiliza durante la conversión de cadenas a fechas si el año
se expresa mediante dos dígitos. En ese caso, TwoDigitYearCenturyWindow define el ori-
gen de una ventana de 100 años, a partir del año actual, dentro de la cuál se interpreta el
año de dos dígitos. Supongamos que asignamos 50 a la variable y que estamos en 1999.
Entonces, cuando tecleamos un año con dos dígitos se interpretará dentro del siguiente
rango de fechas:
1/1/1949 - 31/12/2048
Si asignamos 80, el rango será el siguiente (más adecuado para aplicaciones que trabajan,
por ejemplo, con fechas de nacimiento):
1/1/1919 - 31/12/2018
¿Dónde se puede inicializar esta variable? Quizás sea más conveniente utilizar el fichero
de proyecto, el dpr, antes de llamar a Application.Initialize, pero puede ser también en la
cláusula initialization de cualquier unidad. También puede modificarse temporalmente
dentro de un módulo cuya ejecución sea modal, y restaurarse posteriormente.
¿Cómo funciona? Fácil: afecta a la función ScanDate, que es llamada internamente por
StrToDate, que a su vez es llamada por las conversiones que realizan los componentes
data-aware cuando encuentran una fecha. Es importante este dato, pues podemos encon-
trar problemas al pasar cadenas de caracteres a Locate para realizar búsquedas sobre co-
lumnas de tipo fecha. Como el segundo parámetro de Locate es un Variant, y se traga lo
que le echen, muchos programadores pueden hacer cosas como esta:
Sin embargo, la conversión a fecha la realizará en tal caso las rutinas de manejo de va-
riantes del sistema operativo. Es preferible utilizar este otro código:
45
Campos lookup más rápidos
"Mi aplicación utiliza 200 tablas" - me dice con cierto orgullo un desarrollador. Y me mues-
tra el código fuente. Realmente, se trata de una buena aplicación, con un código excepcio-
nalmente claro. Funciona estupendamente con tablas Paradox, y ahora desea portarla a
cliente/servidor. Y es aquí donde comienzan los problemas. Aunque la velocidad de la
nueva versión es "aceptable", incluso con un solo usuario conectado se nota que el sistema
se toma su tiempo para devolver y modificar datos. Evidentemente, es el momento de op-
timizar algunas operaciones, y una de las muchas operaciones a mejorar es el uso de
campos de búsqueda (lookup fields).
type
TEstadoCivil = (ecSoltero, ecCasado, ecDivorciado);
Sí, sé que usted ya sabe la solución: utilizar tablas de referencia, que pueden ser manteni-
das por los usuarios. Pero ver las cosas desde un punto de vista "filosófico" a veces ayuda;
siempre es bienvenido un cambio del punto de vista. En este caso, le propongo que vea a
las tablas de referencia como los tipos "enumerativos" de su modelo de datos. ¿Caracterís-
ticas generales? La que más nos interesa en estos momentos es que estas tablas casi
siempre contienen una cantidad finita y bastante pequeña de valores. Para mí, por ejemplo,
la tabla de clientes no entra dentro de esta categoría, debido a su tamaño potencial.
Al editar objetos de tipo Persona en la base de datos, específicamente atributos del tipo
EstadoCivil, el programador casi siempre recurre a controles que le permitan seleccionar
uno de los posibles valores del dominio correspondiente. En Delphi y C++ Builder: el com-
ponente TDBLookupComboBox. En la mayoría de los casos, a partir de Delphi 2, se defi-
nen campos de búsqueda en la tabla de Personas, y se le suministran directamente al
combo. El problema consiste en que cada vez que se selecciona una persona de la tabla,
es necesario recalcular los valores de los campos de búsqueda asociados.
Si utilizamos un componente TTable para las referencias, cada vez que cambia la fila ac-
tiva de Personas, se lanza una operación Lookup contra la base de datos, que se imple-
menta mediante un select de SQL. Como beneficio, solamente se traen desde el servidor
las filas estrictamente necesarias.
Si utilizamos un componente TQuery para las referencias, desde el principio se realiza una
operación FetchAll, y se traen todas las filas. La parte positiva consiste en que, en lo ade-
lante, la búsqueda se realiza directamente desde la parte cliente de la aplicación.
Es evidente que, para el caso que hemos analizado (conjuntos de datos pequeños utiliza-
dos como referencia) es preferible el uso de una TQuery como conjunto de datos de con-
47
48 Calling Dr. Marteens
sulta (recuerde que estoy hablando de programación cliente/servidor). Ahora bien, existe
otra posibilidad: utilizar un conjunto de datos cliente (TClientDataSet) como tabla de refe-
rencia. En particular, podemos utilizar la técnica del maletín (briefcase): el contenido de la
tabla de referencia puede almacenarse en una caché local, en un fichero plano de exten-
sión CDS. La primera vez que se ejecute la aplicación, el TClientDataSet se alimenta a
partir del contenido de una consulta. De ahí en adelante, los datos siempre salen del CDS
local. La ventaja con respecto a las TQuery es evidente: el uso de una TQuery obliga siem-
pre a la aplicación a recuperar los datos de búsqueda durante la carga de la misma. En
conclusión: si la aplicación tarda mucho en arrancar debido a estas consultas, considere el
uso del modelo del maletín para acelerar la carga.
"¡Qué listo el tío! ¿Y qué pasa si alguien modifica una tabla de referencia?" - Fácil: en pri-
mer lugar debemos tener una opción explícita en el menú para refrescar la caché de refe-
rencias sin salir de la aplicación. Pero la parte más importante es detectar durante la carga
si es necesario releer la caché o no. La forma más sencilla es utilizar triggers en las opera-
ciones de modificación de una tabla de referencia para almacenar en algún lugar (otra ta-
bla) la fecha de última modificación de dicha tabla. Durante la carga de la aplicación, se
realiza una consulta sobre dicha tabla. Esta consulta solamente necesita traer un registro
hasta el cliente. Después de analizar las fechas, la aplicación puede decidir si prescindir de
la caché o seguir con la misma.
HKEY_LOCAL_MACHINE\System\CurrentControlSet\Services\VXD\VREDIR\
DiscardCacheOnOpen = 01
HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\FileSystem\
DriveWriteBehind = 00 (DWORD)
HKEY_LOCAL_MACHINE\System\CurrentControlSet\Services\LanManServer\
Parameters\
EnableOpLocks = 00 (DWORD)
Esta vez la entrada corresponde al registro de un servidor NT, y lo que hace es desactivar
los bloqueos oportunistas en el servidor.
Están además las opciones de configuración del BDE, como LOCAL SHARE y NET DIR,
con las que espero que el lector esté familiarizado. En cualquier caso, puede consultar el
uso de las mismas en "La Cara Oculta de Delphi 4".
49
Modificando parámetros del
BDE
Este truco está explicado en "La Cara Oculta de Delphi 4", en el capítulo sobre
InstallShield. Pero me han preguntado recientemente cómo se pueden modificar desde
programa parámetros arbitrarios del Motor de Datos. Puede que exista un mecanismo más
directo, pero yo suelo utilizar las funciones que muestro a continuación:
¿Por qué dos parámetros, Path y Param, para estas rutinas? El BDE organiza sus pará-
metros de configuración en un árbol. Cada rama del árbol puede abrirse como si se tratase
de una tabla, y cuando se recorre la "tabla", se obtienen registros con el formato
(parámetro; valor). Por ejemplo, para modificar el NET DIR de Paradox, y el parámetro
LOCAL SHARE podemos utilizar estas rutinas auxiliares:
51
52 Calling Dr. Marteens
La mejor forma de averiguar todas las ramas del árbol de configuración es mediante un pe-
queño programa auxiliar que las recorra y las muestre en pantalla. Busque este programa
en la página de ejemplos2.
2
www.marteens.com/ejemplos
El identificador de la
tarjeta de red
Como todos sabemos, cada tarjeta de red se identifica por medio de un número de 48 bits
que es único. Para recuperar este valor (en una máquina que tenga tarjeta de red, por su-
puesto) podemos utilizar funciones de NetBios. Hay que tener en cuenta que una misma
máquina puede tener instalada más de un adaptador de red. Pero si quiere una función
sencilla que le dé el identificador de la tarjeta principal, podemos basarnos en que el algo-
ritmo de generación de identificadores únicos para OLE utiliza este valor para garantizar la
unicidad del resultado.
Un detalle final: he utilizado el tipo string para el valor de retorno. Pero también es posible
retornar el valor como un Int64.
53
Primeros pasos con las
Open Tools
Open Tools API es la nueva interfaz para la programación de extensiones de los IDE de
Delphi y C++ Builder, que acompaña a las últimas versiones de estos productos. Estas ex-
tensiones nunca han sido bien documentadas por Borland pero, gracias a Dios, el exce-
lente libro "Hidden Paths of Delphi", de Ray Lischner, es todavía la mejor introducción al
tema que conozco. Aunque este libro no trata la nueva OpenTools API, puede encontrar
ayuda sobre la misma en la página de Mr. Lischner3. De hecho, mi primera experiencia con
OTAPI comenzó por la lectura de la información de esa página. Este artículo no intenta ser
una exposición en profundidad de esta interfaz. Pero, siendo de nivel introductorio, puede
ayudar a dar los primeros pasos a quien desee aprovechar la OTAPI para desarrollar ex-
pertos sencillos para uso personal.
¿Qué vamos a hacer ahora? Voy a explicar, paso a paso, cómo crear un experto sencillo
que añada una opción de menú en el entorno de Delphi, y que permita realizar alguna ope-
ración sencilla sobre el formulario activo del proyecto activo (si es que hay alguno).
Limpie el espacio de trabajo del IDE con el comando File|Close all. Abra el depósito de ob-
jetos (File|New) y en la primera página realice un doble clic sobre Package. Guarde este fi-
chero en un directorio aparte y bautícelo con un nombre sonoro, a su elección.
¿Por qué un package? Es que también se pueden crear expertos que residen en DLLs.
Pero es más complicado instalarlos y desinstalarlos. Además, veremos que es mucho más
fácil acceder directamente a los recursos del propio IDE si utilizamos un package en vez de
una DLL.
Lo siguiente es modificar las opciones del package. Pulse el botón Options del editor del
package. La opción que debe cambiar obligatoriamente es Usage options, que debe cam-
biarse a Designtime only. Es conveniente asignar Description (ojo con los apóstrofos que a
veces dan problemas; yo los evito).
Ahora bien, para ahorrarme quebraderos de cabeza con toda la porquería que instalo y de-
sinstalo habitualmente, yo tengo un directorio AddOns que cuelga directamente del directo-
rio raíz de Delphi 4. Ahí es donde pruebo toda la bazofia que genero. Por lo tanto, para mí
es muy importante ir a la página de directorios del diálogo de opciones, y asignar a Output
directory la siguiente macro:
$(DELPHI)\AddOns
Ejecute nuevamente File|New y realice un doble clic sobre el objeto Unit. Esto creará una
nueva unidad vacía; guárdela con un bonito nombre. Tendrá que ir nuevamente al editor
del package para añadir la nueva unidad al proyecto. Utilice para esto el botón Add, y se-
leccione el fichero que contiene la unidad. Dentro de la unidad que acabamos de crear de-
finiremos el objeto que representará al experto a los ojos de Delphi. Lo haremos en la sec-
ción interface de la unidad:
3
www.tempest-sw.com
55
56 Calling Dr. Marteens
type
TimExpertTemplate = class(TInterfacedObject, IOTAWizard, IOTANotifier)
procedure AfterSave;
procedure BeforeSave;
procedure Destroyed;
procedure Modified;
function GetIDString: string;
function GetName: string;
function GetState: TWizardState;
procedure Execute;
constructor Create;
destructor Destroy; override;
end;
Hay que darle un cuerpo a los métodos anteriormente definidos. Mostraré solamente cómo
implementar el conjunto mínimo requerido:
procedure TimExpertTemplate.AfterSave;
begin
end;
procedure TimExpertTemplate.BeforeSave;
begin
end;
constructor TimExpertTemplate.Create;
begin
imExpertModule := TimExpertModule.Create(nil);
// Ver más adelante
end;
destructor TimExpertTemplate.Destroy;
begin
imExpertModule.Free;
// Ver más adelante
end;
procedure TimExpertTemplate.Destroyed;
begin
end;
procedure TimExpertTemplate.Execute;
begin
end;
procedure TimExpertTemplate.Modified;
begin
end;
Como ve, la mayor parte de los métodos tienen implementaciones triviales. Pero me he
adelantado un poco al implementar el constructor y el destructor. En esos procedimientos
estoy creando y destruyendo un objeto de la clase TimExpertModule. Bien, esta clase co-
rresponderá al módulo de datos que crearemos en el siguiente paso. Es decir, cada vez
que nuestro experto se instale, creará una instancia del módulo que definiremos en breve.
Falta aún registrar el objeto como un experto para que Delphi pueda utilizarlo. En la sec-
ción interface tenemos que declarar el siguiente método:
procedure Register;
procedure Register;
begin
RegisterPackageWizard(TimExpertTemplate.Create as IOTAWizard);
end;
¿Por qué necesitamos un módulo de datos? La respuesta es que nos servirá de contene-
dor a objetos componentes que necesitaremos añadir comandos de menú, definir imáge-
nes o lo que nos apetezca.
¿Cuántos comandos de menú añadirá nuestro experto? Por cada comando que añadamos,
necesitaremos una acción. Por lo tanto, traiga un componente TActionList sobre el módulo
de datos. Cree una acción por cada comando de menú que vaya a definir. No se preocupe
de las propiedades ImageIndex (luego veremos por qué) ni Hint (me parece que Delphi no
la utilizará). Tenga mucho cuidado con la propiedad ShortCut, pues puede provocar un
conflicto de teclado con las opciones nativas de Delphi. Lo mismo le digo respecto a utilizar
una letra subrayada en Caption. Para mi ejemplo, definiremos una sola acción, cuyo nom-
bre será ac_im_ArrangeLabels, y su Caption será 'Organizar etiquetas'.
¿De que manga me he sacado el número 7 anterior? Mi interés es añadir el menú al final
del submenú Database de Delphi. Y este submenú es el octavo elemento de la barra. Ob-
serve cómo podemos manipular directamente, sin necesidad de proxies, los propios obje-
tos internos del IDE de Delphi. Observe además como añado una imagen a la lista global
de imágenes de Delphi (AddMasked) y le asigno la posición en la que es añadida a la pro-
piedad ImageIndex de la acción.
¿Por qué utilizo el ImageIndex de la acción y no me conformo con la propiedad Bitmap del
propio comando de menú? Muy sencillo: si me limito a lo último, el bitmap no aparecerá
desactivado cuando la acción desactive al comando por no ser aplicable.
Voy a definir una función sencilla que devuelva el puntero directo al formulario activo del
proyecto activo, y que devuelva el puntero vacío nil cuando no exista tal objeto:
Si el lector conoce algo del modelo COM, se extrañará de que esté utilizando
QueryInterface para averiguar información sobre interfaces, en vez de utilizar los operado-
res de alto nivel de Delphi is y as. La explicación es que con QueryInterface mato dos pája-
ros de un tiro: sé si el objeto dado soporta la interfaz indicada y además obtengo el puntero
a dicha interfaz. En caso contrario, tendría primero que llamar a is, para evitar una excep-
ción, y luego a as. Esta función puede modificarse muy fácilmente si lo que necesitamos
saber es cuál es el proyecto activo.
Primeros pasos con las Open Tools 59
Una vez definida la función anterior, podemos indicar muy fácilmente cuándo es aplicable
la acción que ejecuta nuestro experto. Debemos interceptar el evento OnUpdate de la ac-
ción:
El primer paso consiste en definir el nuevo formulario que queremos utilizar, basándonos
en el tipo TForm. No es estrictamente necesario que se el ancestro sea precisamente
TForm; naturalmente, puede ser también un derivado de TDataModule, pero lo interesante
es que podemos utilizar con el mismo éxito cualquier derivado de TWinControl. En este
ejemplo, por simplificar, solamente utilizaré formularios.
He aquí una sencilla clase que define una propiedad, un par de métodos de clase, y que
intercepta eventos de modo transparente para el programador:
type
TDatabaseForm = class(TForm)
private
FOldCloseQuery: TCloseQueryEvent;
FOldClose: TCloseEvent;
FDataSet: TDataSet;
procedure SetDataSet(Value: TDataSet);
protected
procedure InternalClose(Sender: TObject; var Action: TCloseAction);
procedure InternalCloseQuery(Sender: TObject; var CanClose: Boolean);
procedure Loaded; override;
procedure Notification(C: TComponent; Op: TOperation); override;
public
class procedure Mostrar;
class function Ejecutar: TModalResult;
published
property DataSet: TDataSet read FDataSet write SetDataSet;
end;
4
www.tempest-sw.com
61
62 Calling Dr. Marteens
F := Screen.Forms[I];
if F.ClassType = Self then
begin
if F.WindowState = wsMinimized then
F.WindowState := wsNormal;
F.BringToFront;
Exit;
end;
Create(Application).Show;
end;
finally
LockWindowUpdate(0);
end;
end;
El funcionamiento de estos métodos está explicada en La Cara Oculta de Delphi 4 (un poco
de autobombo, ¿puedo?). Ahora los métodos relacionados con el mantenimiento del pun-
tero al conjunto de datos:
procedure TDatabaseForm.Loaded;
begin
inherited Loaded;
FOldClose := OnClose;
FOldCloseQuery := OnCloseQuery;
OnClose := InternalClose;
OnCloseQuery := InternalCloseQuery;
end;
else
FDataSet.Cancel;
end;
CREAR UN EXPERTO
procedure Register;
begin
RegisterCustomModule(TDatabaseForm, TCustomModule);
// Aquí podemos también registrar componentes y expertos
end;
Pero podemos también definir una clase derivada de TCustomModule, si deseamos, por
ejemplo, añadir comandos al menú de contexto del formulario:
type
TDatabaseCustomModule = class(TCustomModule)
function GetVerbCount: Integer; override;
function GetVerb(Index: Integer): string; override;
procedure ExecuteVerb(Index: Integer); override;
end;
GetVerbCount indica el número de comandos a añadir, GetVerb debe indicar el texto del
comando, y ExecuteVerb debe redefinirse para ejecutar dichos comandos:
Para que usted no tenga que llegar a esos extremos, he aquí algunos trucos para limitar el
número de filas que puede devolver una consulta SQL. Comencemos con SQL Server,
donde podemos indicar la limitación mediante una cláusula especial después de la palabra
select:
select top 25 *
from Clientes
select *
from Clientes
fetch first 25 rows only
select *
from Clientes
where rownum <= 25
¿Y qué pasa con nuestro viejo amigo InterBase? Que no tiene operadores especiales para
limitar el tamaño de una consulta. Para lograr un efecto semejante, debemos definir un
procedimiento almacenado:
select *
from NPrimeros(25)
65
66 Calling Dr. Marteens
Así que si busque otra justificación si lo que desea es pegarle un tiro a su ordenador, pis-
tolero ...
Escogiendo un servidor
Quiero dejar claro un punto importante: no me considero un "gurú" de los servidores de ba-
ses de datos. Tengo cierta experiencia trabajando con determinados servidores, sobre todo
desde el punto de vista del programador más que como administrador. Creo, no obstante,
que estoy en condiciones de evaluar los aspectos de los servidores SQL más comunes que
más afectan a los desarrolladores de aplicaciones. Quizás no conozca determinada ins-
trucción de determinado servidor que sirva para resolver aquel "gran problema" que a de-
terminado programador está empujando al manicomio. Pero de lo que se trata es de evitar
llegar hasta dichos extremos. Terminado el sermón, ahí van mis consejos.
Los servidores que voy a analizar son aquellos con los que tengo más experiencia. Por su-
puesto, también me referiré de pasada a versiones anteriores.
1. InterBase 5.5
2. Oracle 8
3. Microsoft SQL Server 7
4. DB2 Universal Database 5.2
Tenía dos alternativas para organizar este artículo: definir áreas de evaluación y ver cómo
se comportan los distintos servidores, o analizar cada servidor mencionando sus aspectos
positivos y negativos. Me he decidido por la última, pues al analizar por áreas puedes per-
der de vista el contexto en que se implementa o se deja de implementar alguna caracterís-
tica. Y la última advertencia: el orden de evaluación no implica preferencias personales (o
casi).
INTERBASE
Es el servidor más asequible de todos, sobre todo para los programadores de Delphi, C++
Builder y JBuilder. Es el más barato de los cuatro que analizo aquí. Además existe una ver-
sión gratuita para Linux. InterBase es multiplataforma: existen versiones para Windows 9x,
Windows NT, HP-UX, Solaris, Netware, SCO, Linux, etc, etc. Puede comprobar en
www.interbase.com la versión actual de cada una de las plataformas soportadas.
La principal baza de InterBase es la Arquitectura Multigeneracional. Casi todos los restan-
tes sistemas de bases de datos, cuando leen un registro durante una transacción, colocan
un bloqueo de lectura sobre el mismo. De este modo, cualquier otro proceso concurrente
puede también leer el registro, pero no modificarlo. Si se produce este conflicto, la aplica-
ción que llega más tarde tiene que esperar a que la otra termine. InterBase, sin embargo,
resuelve esta situación creando versiones del registro que se modifica. Así, las transaccio-
nes de lectura pueden alcanzar la máxima coherencia sin afectar a las transacciones de
escritura. En situaciones de mucho jaleo, esta característica es incluso más importante que
las cifras "crudas" de rapidez de localización, lectura y modificación.
67
68 Calling Dr. Marteens
pueden especificar las opciones adicionales on delete set default, on delete set null, y
las correspondientes para on update.
El lenguaje de restricciones check es muy potente. Las cláusulas check pueden incluir ex-
presiones con subconsultas, algo que solamente permite DB2 además de InterBase (hasta
donde conozco). En el resto de los sistemas, una restricción de este tipo nos forzaría a
programar un trigger.
¿Y qué tal los triggers? InterBase soporta triggers que se disparan para cada fila indepen-
dientemente, antes y después de la operación. Y se pueden especificar varios triggers por
evento. La ejecución de un trigger, además, se trata atómicamente. Si ocurre un error du-
rante la ejecución de un trigger se deshacen todos los cambios realizados por la operación
inicial que desencadenó el trigger. Lo mismo sucede con los procedimientos almacenados
que, dicho sea de paso, permiten programar procedimientos de selección para devolver
conjuntos de datos.
Otro defecto importante tiene que ver con el conjunto de funciones escalares predefinidas:
creo que en total sólo hay tres o cuatro de estas funciones. Claro, se pueden definir funcio-
nes en módulos dinámicos, y registrarlas para que el servidor puede utilizarlas como
funciones definidas por el usuario (UDF). Pero el uso de estas funciones nos obliga a elegir
"para siempre" un sistema operativo: si queremos pasar desde Windows NT a algún UNIX,
probablemente necesitemos recompilar o reprogramar completamente nuestras bibliotecas
de funciones. Y si pasamos a NetWare peor, pues en este sistema operativo InterBase no
permite el uso de funciones de usuario.
ORACLE
sadillas con particiones, espacios de índices y tablas, índices hash, índices por bits, índices
con claves invertidas, tablas organizadas por medio de índices, tablas relacionadas me-
diante clusters, y todo lo demás.
El lenguaje SQL soportado por Oracle, denominado PL/SQL, es muy completo. Permite la
expresión de consultas recursivas (cláusulas start with/connect by), y soporta un número
grande de funciones escalares. Los triggers son tanto a nivel de filas como de instruccio-
nes, y se disparan antes y después. Su ejecución es también atómica, lo cual facilita su
programación. La presencia de cláusulas when y update of los hacen incluso más fáciles
de escribir que en InterBase. Se pueden utilizar explícitamente cursores en los triggers y
procedimientos almacenados (InterBase sólo los permite implícitamente en la instrucción
for). Por último, podemos utilizar paquetes para definir nuevos tipos de datos, y para crear
variables globales para cada conexión cliente.
Y están las recientes extensiones de objetos. Se pueden definir clases, que encapsularán
datos y métodos, aunque todavía no puede utilizarse la herencia y no existe la posibilidad
de esconder información. Se pueden crear columnas de tipo objeto, se permite incluso
crear tablas basadas en una clase, y se admite el uso de referencias a objetos, como si se
tratase de punteros. A esto se le añaden los nuevos tipos como tablas anidadas y arrays.
Puede que en un par de años la forma de programar aplicaciones de bases de datos sea
radicalmente diferente gracias a la Programación Orientada a Objetos. Pero ese momento,
desgraciadamente, aún no ha llegado.
¿Alguna pega a Oracle? La implementación actual de las transacciones con lecturas repe-
tibles del BDE requiere que estas transacciones sean sólo lectura. No sé si se trata de una
limitación intrínseca de Oracle o una de las tonterías habituales del BDE. También hay pro-
blemas con el uso de campos LOB (imágenes, documentos, etc) y las transacciones implí-
citas.
En el Mundo Medio de Tolkien habitaban hombres, elfos, enanos, gigantes y hobbits. Los
hobbits eran los "medianos". De MS SQL Server se puede decir que es un hobbit. No es el
más rápido, no es el más barato, no es el más elegante ... pero es el más popular.
La arquitectura física de la versión 6.5 no era mala ... era pésima. Bloqueos a nivel de pá-
gina (para las actualizaciones), ficheros de datos y de transacciones que no crecían auto-
máticamente, páginas fijas de 2048 bytes... Todo esto ha mejorado en la versión 7. En vez
de seguir utilizando el concepto de device para el almacenamiento físico (herencia de
Sybase), en la nueva versión los datos se almacenan directamente en ficheros físicos. Es
muy fácil definir grupos de ficheros, asignar tablas e índices a estos grupos, e implementar
particiones. Antes había que definir segmentos y asignar las tablas a estos segmentos me-
diante procedimientos almacenados. Ya se permiten bloqueos a nivel de fila. Y las técnicas
de replicación implementadas son muy potentes. El mayor inconveniente de SQL Server es
que el servidor solamente se puede ejecutar en Windows (NT, 95, 98).
Un punto a favor de SQL Server es la posibilidad de activar la seguridad integrada con NT.
Cuando esta opción está activa, el usuario solamente tiene que autentificar su identidad al
entrar en el sistema operativo. De ahí en adelante, SQL Server identifica al usuario y lo
asocia con uno de sus logins internos.
Mis principales críticas a este sistema vienen del lado de la programación SQL. Por ejem-
plo, una importantísima limitación de SQL Server: no se soportan acciones referenciales
declarativas. Es decir, no se pueden especificar borrados o actualizaciones en cascada
para las relaciones de integridad referencial. Unamos esto a las restricciones que impone
su obsoleto sistema de triggers.
Hasta el momento, el BDE accede a SQL Server por medio de la interfaz de programación
DBLibrary. Se espera que Borland sustituya en breve este método de acceso por ADO, que
según comentan las malas lenguas, va mucho más rápido. Mientras tanto, sepa usted que
cada tabla o consulta abierta en un cliente, tiene que implementarse mediante una cone-
xión separada al servidor. Esta política trae como consecuencia toda una serie de proble-
mas que no voy a tratar aquí.
DB2 es un producto de IBM, y eso ya lo dice todo. Alta tecnología, elegancia en la imple-
mentación, precios elevados ...y pifias inexplicables. En lo que respecta a la arquitectura fí-
sica, DB2 es de esos sistemas que permiten configurar hasta el más mínimo detalle. Casi
todo lo que he dicho acerca de Oracle, es también aplicable a DB2. Existen versiones de
DB2 para muchas plataformas.
Este sistema incorpora extensiones curiosas a SQL. Por ejemplo, es interesante (y ele-
gante) la forma de plantear consultas recursivas. DB2 tiene triggers a nivel de fila y de ins-
trucción, de ejecución previa y posterior. Además son atómicos. Eso sí, las instrucciones
que permiten ejecutar están bastante limitadas, pues no existen condicionales, bucles y
tonterías similares. Esto nos fuerza a hacer actos de magia con la cláusula when de la ca-
becera del trigger, y a aprendernos trucos sucios y extensiones propietarias de update,
insert y demás. Nada grave o irresoluble, sin embargo.
El BDE utiliza el parámetro SERVER NAME de los alias de InterBase para determinar, además
de cuál base de datos queremos abrir, el protocolo mediante el cual queremos comunicarnos
con el servidor. Por ejemplo, si deseamos abrir una base de datos local, es necesario que indi-
quemos la ruta completa al fichero principal de la base de datos:
C:\Datos\1999\Empresas.GDB
Si la base de datos reside en un servidor remoto, al cual llamaremos SERV, y queremos co-
nectarnos mediante NetBEUI, la sintaxis es la siguiente:
\\SERV\Datos\1999\Empresas.GDB
Por último, si queremos utilizar TCP/IP con un servidor remoto, ésta es la sintaxis correcta:
SERV:/Datos/1999/Empresas.GDB
Los dos ejemplos anteriores de conexiones remotas asumen que el fichero principal de la base
de datos reside en el disco C: del servidor (pueden existir ficheros secundarios en otros dis-
cos). ¿Qué pasaría si nos vemos obligados a situar este fichero en un disco alternativo? Si
utilizamos NetBEUI es muy sencillo indicar en qué disco está el fichero:
\\SERV\D:\Datos\1999\Empresas.GDB
Lamentablemente, la sintaxis para TCP/IP no permite incluir, por algún motivo que se me es-
capa, la indicación del disco. Pero no se preocupe, porque existe un truco bastante probado.
En primer lugar, debe eliminar el protocolo NetBEUI5 de su sistema. Esto es una buena idea en
general, sobre todo si quiere una red que vaya a una buena velocidad. Si entonces pasa al
BDE una cadena con la sintaxis de NetBEUI, se verá obligado a utilizar TCP/IP de todos mo-
dos, pero colateralmente podrá indicar también el disco donde se encuentra la base de datos.
ACLARACION: Varias personas me han escrito para comunicarme que pueden co-
nectarse a una base de datos remota situada en el disco D: utilizando directamente la
sintaxis de TCP/IP. Lo he comprobado y, efectivamente, funciona. Bien: juro por lo que
haya que jurar, que hasta determinada versión del cliente de InterBase, este problema
existía; tenía una base de datos de consultas precisamente un segundo disco de un
servidor, y no había forma de conectarse utilizando dicho protocolo. En algún momento
se arregló el problema, pero yo no me enteré. Voy a intentar realizar un par de pruebas
con versiones anteriores, y después corregiré esta página, o la sustituiré con algún otro
truco.
5
Vea el truco "Problemas al instalar un servidor SQL"
71
Check boxes en rejillas de datos
¿Quiere mostrar en una rejilla (DBGrid) el valor de una columna lógica utilizando una casi-
lla de verificación (checkbox)? Es muy fácil lograrlo, si se trata de una rejilla sólo lectura.
Asegúrese en primer lugar de que la rejilla tenga la propiedad ReadOnly a True, y que la
opción dgEditing de Options esté inactiva. Utilizaremos como ejemplo la tabla vendors.db
de dbdemos, que contiene un campo Preferred, de tipo lógico. Una vez que conecte esta
tabla a una rejilla y haya creado columnas para todos los campos de la tabla, seleccione la
columna del campo Preferred y limpie su propiedad FieldName. Seleccione nuevamente la
rejilla y cree la siguiente respuesta para su evento OnDrawColumnCell:
73
Decision Cube y las fechas
Cuando en un Decision Cube se utiliza un campo de tipo fecha como dimensión para el
análisis, este componente agrupa los valores por años, por omisión. Este comportamiento
es lógico: una dimensión de un cubo de decisión debe contener un número pequeño de
valores posibles. ¿Cuántas celdas tendrá un cubo? Para cada dimensión hay que estimar
el número de valores de su dominio, y multiplicarlos a todos entre sí. Si implementamos un
cubo con dos dimensiones: formas de pago y provincia, y tenemos 10 formas de pago y 50
provincias, nuestro cubo contendrá finalmente 500 celdas.
He visto ejemplos de otros programadores que intentan mostrar una rejilla de decisión
siendo el nombre de cliente una de las dimensiones. Se trata, por supuesto, de un dispa-
rate total. El total de clientes puede crecer desorbitadamente, y TDecisionCube se prote-
gerá limitándose a almacenar un número predeterminado de celdas como máximo. En uno
de esos malos ejemplos, además, el nombre de cliente era la única dimensión. Este tipo de
información se muestra de forma más adecuada en un TDBGrid corriente, o en un
TDBChart si necesitamos una representación gráfica.
Volvamos a las fechas. ¿Qué pasa si necesitamos un mayor nivel de detalles en el análisis
por fechas? Como sabemos, podemos controlar el grado de agrupación de las fechas en
tiempo de diseño haciendo doble clic sobre el componente TDecisionCube, seleccionando
la dimensión correspondiente y asignando el valor deseado mediante el combo Grouping.
Pero, ¿cómo modificar este parámetro dinámicamente?
No se trata de algo evidente: mi primera intención fue modificar directamente el valor den-
tro del cubo. Un TDecisionCube tiene una propiedad DimensionMap, que a su vez contiene
una lista de componentes de tipo TCubeDim. Estos últimos son los que contienen datos de
cada dimensión o estadística. En particular, la propiedad BinType es la que determina
cómo se agrupan las fechas. Pues bien, si modificamos directamente dicha propiedad, no
pasará nada: el cubo ignorará nuestros intentos. La técnica correcta consiste en crear un
duplicado de DimensionMap, modificar la copia y llamar entonces al método Refresh del
cubo para que actualice su contenido.
El siguiente procedimiento muestra cómo aplicar la técnica explicada. Hay que incluir la
unidad MxCommon para que pueda ser compilado:
Los valores que puede asumir el parámetro Grouping son los siguientes:
75
76 Calling Dr. Marteens
type
TBinType = (binNone, binYear, binQuarter, binMonth,
binSet, binCustom);
Por supuesto, los que nos interesan son binYear, binQuarter (trimestre) y binMonth.
PERO ...
Siempre hay un "pero". En este caso, el procedimiento provoca un fallo general y catastró-
fico si existe un componente TDecisionGraph visible y acoplado al cubo. Este fallo viene
arrastrándose desde hace varias versiones; parece ser que a la gente de Borland (tan hu-
manas) les da lástima matar al viejo bicho, y esperan que muera de melancolía.
Vistas preliminares a la medida
Ya he perdido la cuenta de las veces que me han preguntado cómo se programa una ven-
tana de vista preliminar para QuickReport. No voy a mostrar las últimas técnicas disponi-
bles, que pasan por definir una clase de ventana de vista preliminar por omisión (falta de
documentación, como siempre), pero la técnica que mostraré vale para todas las versiones
de QuickReport.
En la barra de herramientas añadimos cuatro botones para navegar por las páginas. Estos
son los métodos que ejecutarán:
77
78 Calling Dr. Marteens
En el informe para el cual queremos esta vista preliminar debemos interceptar el evento
OnPreview:
Para tener acceso al tipo TQRPrinter necesitamos añadir la unidad QRPrntr a la cláusula
uses de la unidad del informe. Estoy mostrando la vista preliminar en forma modal, pero
también podía haber utilizado una ventana no modal o incluso MDI.
Finalmente, hay que tener cuidado con la forma en que se ejecuta el informe:
En La Cara Oculta de Delphi 4, el ejemplo de vista preliminar del CD-ROM llamaba direc-
tamente al método Preview. Pero a partir de la versión 3.0.3 de QuickReport, que apareció
mucho después de salir Delphi 4 al mercado, este método dejó de funcionar con formula-
rios de previsualización a la medida.
Etiquetas y directorios
¿Ha visto cómo algunas aplicaciones muestran a veces una versión abreviada de un nom-
bre de directorio demasiado largo? Estas aplicaciones sustituyen parte de la ruta mediante
puntos suspensivos, para que el texto quepa dentro de cierta área. ¿Cómo lo hacen? Re-
sulta que existen dos métodos diferentes, que detallaremos a continuación.
La técnica está basada en una opción de la función DrawText, del API de Windows:
type
TShortPathLabel = class(TCustomLabel)
protected
procedure Paint; override;
public
constructor Create(AnOwner: TComponent); override;
published
property Alignment;
property Transparent;
end;
79
80 Calling Dr. Marteens
Delphi ha arrastrado desde la versión de 16 bits una serie de clases y procedimientos auxi-
liares en la unidad FileCtrl. En concreto, ahí se encuentran los componentes de la página
Win3.1 relacionados con la selección de ficheros y directorios. La función que nos interesa
se llama MinimizeName, y éste es su prototipo:
Hay que suministrar un Canvas para que Delphi tenga conocimiento de las condiciones de
dibujo que afectan al tamaño del texto final. MaxLen es un valor en píxeles, para indicar la
anchura máxima del texto. He aquí un pequeño ejemplo que muestra cómo utilizar
MinimizeName:
La primera utiliza un cuadro de diálogo de la VCL que muestra, además del inevitable con-
trol de selección de directorios, un control TFileListBox para mostrar los ficheros existentes
dentro del directorio seleccionado. En cambio, la segunda versión encapsula una llamada a
SHBrowseForFolder, que es una función del Shell de Windows, y tiene un aspecto más
"estándar".
Si usted realiza un par de pruebas con el componente creado en el primer inciso y con la
función MinimizeName, encontrará que esta última ofrece resultados más estéticos que la
técnica basada en el API de Windows.
El operador delete y el
método Free
Como la memoria RAM tiene los precios por los suelos, ya nadie se preocupa de optimizar
sus programas para que no ocupen espacio innecesario ... menos ciertos idiotas (como el
que escribe estas líneas) que creen que no sólo es importante el resultado final del proceso
de programación (la aplicación) sino también el estilo con el cuál pasamos por el mismo.
No se trata de ir mirando con lupa cada instrucción, ni de dormir con un manual de profiling
como almohada. Pero muchas veces programamos descuidadamente, y utilizamos ciertas
instrucciones que son más lentas o voluminosas que algunas alternativas. Vale, si no co-
nocemos la existencia de estas alternativas no estamos cometiendo "pecado mortal"; pero
en cualquier otro caso estamos condenando nuestra alma al infierno. El infierno es una
sala de programación calurosa, con un servidor de Windows NT 3.51, ordenadores clónicos
que fallan cada dos por tres y un capataz de corbata azotando a los condenados para que
terminen los arreglos a una aplicación escrita en Visual Basic escrita por terceros...
Este truco está dirigido a los programadores de C++ Builder. Todos conocemos que en la
VCL los objetos de clases descendientes de TObject se destruyen mediante un destructor
virtual programado en Pascal cuyo nombre es Destroy. Los destructores de Object Pascal
no verifican si el puntero que reciben tiene algún objeto asociado, o si simplemente con-
tiene la constante nil. Pero la misma clase TObject implementa el método Free, que sí
comprueba el valor del puntero antes de pasárselo al destructor:
procedure TObject.Free;
begin
if Self <> nil then Destroy;
end;
Ahora vamos de vuelta a C++ Builder. Es bien sabido que la verificación de la nulidad de
un puntero es parte de la semántica del operador delete; en realidad, C++ nunca permite
llamar explícitamente a un destructor, como sucede en Object Pascal. El operador delete,
como se puede comprobar fácilmente, verifica que el puntero que se le pasa no sea nulo
antes de destruir el objeto asociado. Así que en C++ Builder parece que Free está conde-
nado a cubrirse de polvo en algún rincón perdido de nuestras neuronas.
¿Está seguro? Yo pensaba lo mismo, hasta que la curiosidad me dio por averiguar cómo
se implementaba una llamada a delete. Utilizando la ventana CPU del depurador de C++
Builder 4, comparé las implementaciones de una destrucción de un objeto dinámico (como
todos los de la VCL) en cuatro variantes diferentes:
Bien, dejemos que los números hablen por nosotros. Estos son los resultados, que se refie-
ren al tamaño en bytes de la operación que destruye al objeto:
81
82 Calling Dr. Marteens
Free delete
Variable global 13 bytes 51 bytes
Variable local 8 bytes 46 bytes
Como se puede ver, en cada llamada a delete sobre un objeto de la VCL que sustituyamos
por un Free estaremos ahorrando 38 bytes. Ya sé que 38 bytes no es nada, pero sume to-
das las veces que llama a delete desde su aplicación y saque cuentas. Y, como decía al
principio, ¿por qué utilizar inútilmente la peor alternativa, una vez que conocemos algo
mejor?
Tablas en memoria
A veces olvido que existen dos versiones de Delphi además de la cliente/servidor. Co-
mienzo a explicar técnicas para acelerar el trabajo con campos de búsquedas, o para hacer
más robusta la entrada de datos master/detail, y propongo el uso de conjuntos de datos
clientes (TClientDataSet). Pero Delphi Standard y Professional no tienen este componente.
Por fortuna, el BDE ofrece una técnica relativamente sencilla para suplir esta carencia: las
tablas en memoria. Una tabla en memoria se crea mediante la siguiente función del API del
BDE:
El primer parámetro es un handle a una base de datos; hace falta un alias para ubicar den-
tro del mismo a la tabla. pszName indica el nombre que le daremos a esta tabla; aunque,
en realidad, no sé para qué es necesario este nombre. iFields es la cantidad de columnas
que va a tener la tabla, y pfldDesc apunta a un array de valores del tipo FLDDesc, que con-
tiene la descripción de las columnas. Por último, hCursor recibirá el handle del cursor de la
recién nacida.
Hay varias limitaciones aplicables a este tipo de tablas. La más importante es que no se
pueden definir índices sobre las mismas. Además, tampoco el BDE soporta directamente
restricciones, ni valores por omisión, sobre sus columnas. En este sentido, TClientDataSet
es mucho más potente. Además, no existe un mecanismo directo de guardar su contenido
en un fichero plano, a no ser que utilicemos la función DbiBatchMove y un fichero ASCII
como destino.
ENCAPSULANDO LA TABLA
Conozco pocos kamikazes dispuestos a trabajar directa y exclusivamente con el API del
BDE. Si queremos aprovechar las tablas en memoria del BDE, debemos crear un compo-
nente que las haga "digeribles". Aquí explicaré los pasos básicos de esta técnica, y el có-
digo fuente completo puede encontrarse en la página de ejemplos.
Crearemos una nueva clase, TMemoryTable, cuyo ancestro será TDBDataSet. ¿Por qué
esta clase? Pues porque necesitamos un alias abierto para crear el cursor de la tabla, y
nos ahorraremos bastante código si aprovechamos la clase mencionada, que implementa
las propiedades DatabaseName, Database y DBHandle; esta última propiedad es la encar-
gada, además, de realizar la conexión con el alias cuando tratamos de recuperar su valor6.
Tenemos que redefinir la función CreateHandle, para obtener un cursor a la tabla en me-
moria; será en esta función donde llamaremos a DbiCreateInMemTable. Pero antes tene-
mos que decidir cómo configuraremos las columnas de la nueva tabla. Utilizaremos dos
técnicas:
6
¿Ha escuchado o leído, alguna vez, el vocablo procrastination? Consulte la página 252 del libro "Delphi
Component Design", de Danny Thorpe (ISBN 0-201-46136-6).
83
84 Calling Dr. Marteens
UN MUNDO DE POSIBILIDADES
Suponga que tiene un valor de tipo TDateTime representando a una fecha, y que quiere
obtener la misma fecha, pero del mes siguiente o del anterior. En tal caso, no hay que
liarse descomponiendo la fecha para adivinar el número de días del mes actual, ni nada
semejante. Una oportuna función de la unidad SysUtils nos puede resolver directamente el
problema:
El parámetro Meses puede ser indistintamente positivo o negativo, para obtener una fecha
anterior o posterior:
var
DentroDeUnMes: TDateTime;
HaceUnMes: TDateTime;
begin
DentroDeUnMes := IncMonth(Date, 1);
HaceUnMes := IncMonth(Date, -1);
// ...
end;
Recuerde, de todos modos, que para adicionar o substraer días de una fecha basta con la
suma y resta aritmética, la de toda la vida, pues las fechas se representan internamente en
la VCL en formato juliano.
87
El buen uso de las identidades
"En el Mundo Perfecto, Bach seguiría componiendo su Música "
Ian Marteens
• El BDE utiliza normalmente las claves primarias de los registros para identificar las co-
pias que guarda en la caché del cliente con el registro correspondiente del servidor. Si
la clave primaria se asigna en el lado del servidor, el BDE puede tener problemas para
identificar un registro.
• La principal ventaja, desde el punto de vista de la implementación del servidor, es que
estos recursos no se bloquean durante una transacción, al contrario de lo que sucede-
ría con una tabla de contadores. Pero esto se paga caro, pues matemáticamente es
imposible evitar saltos en la secuencia de valores asignados. Por supuesto, existen tru-
cos para que el servidor reasigne valores desperdiciados, especialmente si se trata de
SQL Server y las identidades. Sin embargo, estas técnicas no garantizan la secuencia-
lidad en cada instante.
MIRA GESTORUM...
Pero esto no quiere decir que debamos prohibir o desterrar a las identidades de nuestro
repertorio de recursos. ¿Sabéis algo de música? El intervalo que existe entre el Fa y el Si
natural se conocía en la Edad Media como diabolum, pues corresponde a un sonido apa-
rentemente inarmónico. En el famoso canto gregoriano que da nombre a las notas musi-
cales en Occidente gracias al monje Guido D'Arezzo, cada estrofa comienza con una nota
más alta que la anterior, secuencialmente siguiendo la escala. La primera sílaba de la es-
trofa le da nombre. Y curiosamente, la escala asciende solamente hasta el La natural. Hu-
biera sido pecado incluir el Si y su disonancia.
La moraleja de la historia anterior es que no hay recurso de programación malo, sino mal
aprovechado. Una buena aplicación de las identidades de SQL Server es con las tablas de
apoyo que abundan en todo diseño de base de datos: las tablas de referencia. La estruc-
tura típica de estas tablas es la siguiente:
Campo Tipo
Código INTEGER
Descripción VARCHAR
El código es un valor interno, que sirve para implementar las claves externas (foreign key).
Así que la verdadera clave de la tabla, en el sentido semántico, sería la Descripción. Es
89
90 Calling Dr. Marteens
posible que, en casos puntuales, existan atributos adicionales en cada fila, pero da lo
mismo para la explicación que sigue a continuación.
Supongamos que queremos definir una tabla para almacenar las diferentes formas de pago
existentes. La siguiente definición nos serviría:
Observe que, aunque Código se crea como clave primaria, también se crea una clave
única para la Descripción. La propiedad identity, que se asocia a la primera columna,
puede utilizarse con tipos numéricos. Estamos indicando que sus valores se asignen a par-
tir de uno, y que aumenten también de uno en uno.
La otra técnica que he aplicado es definir el índice sobre las descripciones mediante la
cláusula clustered. Así crearemos un índice agrupado: las filas de las formas de pago se
almacenarán de forma ordenada en la base de datos, en un formato similar al de las tablas
de Paradox. Con esta técnica, por ejemplo, la ordenación por descripciones se ejecutará
muy eficientemente. Como solamente puede haber un índice agrupado por tabla, explíci-
tamente indicamos que la clave primaria no irá agrupada.
Para que un mantenimiento sobre este tipo de tablas funcione, hay que tener en cuenta las
siguientes recomendaciones:
Truco breve y sencillo, pero muy importante: hasta hace un par de días, todas las
pruebas que estaba haciendo con SQL Server 7 las realizaba sobre una instalación de
Windows NT en inglés americano, aunque el cliente era un Windows 95/98 en caste-
llano. Cuando tuve que llevarme de viaje un portátil con la versión Desktop instalada di-
rectamente sobre un Windows 98 en castellano, me encontré con que era incapaz de
poder teclear una fecha si el mes pertenecía a un conjunto determinado; no recuerdo
todos, pero Abril era uno de ellos.
Esto sucedía desde programas de Delphi y C++ Builder 4, aunque también me ocurría
con el SQL Explorer. Pero, curiosamente, el problema no surgía cuando los cambios se
efectuaban con el Query Analyzer. Así que el problema estaba relacionado con la
DBLibrary: la interfaz que aún utiliza el BDE para el acceso directo a SQL Server.
Solución:
91
Cómo eliminar un generador
Todos sabemos cómo se puede crear un generador en InterBase:
¡Ah, pero lo complicado es cómo borrarlo! No existe una instrucción drop generator, ni nada
parecido. Hay que hacer trampas al sistema, y lanzar una instrucción de borrado contra una
tabla interna del sistema:
93
Más trucos con generadores
Ahora que ya sabe como crear y destruir un generador, nos queda conocer cómo podemos
obtener el valor actual del generador. ¡Elemental, Watson!, pensará usted. Hasta el alcalde de
mi pueblo sabe que existe la función gen_id, que recibe un nombre de generador y un incre-
mento, lo aplica al generador y devuelve el valor del mismo. Casi siempre el generador se uti-
liza en triggers o procedimientos como los siguientes:
Ante tanta sabiduría me siento obligado a preguntarle algo: ¿qué valor devuelve gen_id, el que
tenía el generador antes del incremento, o el valor posterior? Pues es el valor posterior al in-
cremento el que se retorno. Claro, no esperaba otra cosa de usted...
¿Y cómo puedo obtener el valor actual del generador, pero sin modificarlo? La primera vez que
me lo preguntaron, contesté algo que aún me avergüenza recordar:
/* ¡¡¡MUY MALO!!! */
create procedure ValorActual returns(Codigo integer) as
begin
Codigo = gen_id(OtroGenerador, 1);
Codigo = gen_id(OtroGenerador, -1);
end!
¿Funcionar? Creo que sí, pero funciona a lo bestia. Con más experiencia sobre mis espaldas,
ahora me doy cuenta de que el siguiente procedimiento es más racional:
De todos modos sigue existiendo un problema. Supongamos que estamos desarrollando una
herramienta de diseño, al estilo de SQL Explorer, o de Marathon7. En tal caso, no tendremos a
mano un procedimiento como el anterior para cada uno de los generadores de la base de da-
tos. Pero el problema no es grave: gen_id es una función como cualquier otra (aunque causa
un efecto secundario), y puede colocarse en una cláusula select. Inmediatamente me viene a
la mente la tabla Dual de Oracle: una tabla predefinida que siempre contiene una sóla fila. Si
tuviésemos esta tabla en InterBase, nos bastaría ejecutar la siguiente instrucción para conocer
el valor actual de un generador:
Bien, no existirá Dual en InterBase, pero sí tenemos la tabla interna del sistema
RDB$DATABASE, que hasta donde conozco, siempre tiene una sola fila. Entonces podemos
utilizar esta variante:
/* VARIANTE CORRECTA */
7
www.gimbal.com.au
95
96 Calling Dr. Marteens
Por supuesto, el resultado del select anterior ¡siempre tiene una sola fila!.
Los tipos numéricos de
InterBase
Mi objetivo es mostrarle al lector qué es lo que sucede realmente en InterBase cuando utiliza-
mos los tipos numeric y decimal de SQL estándar. Podemos utilizar estos tipos en la declara-
ción de columnas de tablas, en la creación de dominios, en variables locales y en parámetros
de procedimientos almacenados. La principal peculiaridad de ambos consiste en que podemos
adjudicarles una precisión y una escala. La precisión es el número de dígitos, tanto a la dere-
cha como a la izquierda de la coma decimal, que se pueden representar mediante el tipo. De
esa cantidad, la escala roba unos cuantos dígitos para que se coloquen a la derecha de la
coma. Por supuesto, la escala debe ser siempre menor o igual que la precisión:
Cuando un programador proveniente de xBase ve estos tipos los asocia inmediatamente con el
tipo numérico de dBase. En este sistema los valores numéricos se representan en formato
ASCII (todo un despilfarro), y está claro que es importante especificar un tamaño máximo para
ahorrar espacio. Por lo tanto, este programador hará un uso intensivo de numeric. ¿Hace bien
o mal? Le explico lo que hace InterBase, y ya usted decidirá.
Sorpresa número uno: Desde el punto de vista del espacio ocupado para su almacenamiento,
las dos columnas siguientes son iguales:
/* ... */
Altura numeric(3,2),
Peso numeric(4,2),
/* ... */
Lo que sucede es que InterBase elige siempre uno de los tres tipos siguientes para representar
físicamente el valor: smallint, integer o double precision. La elección está determinada por la
precisión especificada, y se ignora la escala:
¿Y qué pasa con la escala? Si definimos una columna como numeric(3,2), por ejemplo, sus
valores se almacenan multiplicados por 100, para obtener un número entero. La escala se
guarda en una tabla del sistema, y se utiliza para restaurar el número original durante la eva-
luación de expresiones por InterBase. Si, por el contrario, la precisión es superior a 9, el nú-
mero se almacena directamente con el formato de double precision, como ya he dicho. Este
tipo se representa con 8 bytes, y puede contener sin dificultad alguna los 15 dígitos de preci-
sión mencionados.
Sorpresa número 2: El BDE tiene problemas para trabajar con los tipos de campos anteriores,
especialmente cuando la precision es menor que 10. Cuando traemos un campo numérico a
Delphi y la configuración del BDE es la implícita, se crean objetos de tipo TIntegerField o
TFloatField, de acuerdo a la precisión. Por supuesto, en un campo entero se truncarán los de-
97
98 Calling Dr. Marteens
cimales o, en versiones anteriores del BDE, el número aparecerá multiplicado por el factor de
escala. Y los campos TFloatField ignorarán el número de dígitos decimales, mostrando cuantos
decimales estimen pertinentes.
Una solución consiste en activar el parámetro ENABLE BCD en la configuración del alias del
BDE. En tal caso, siempre se traen campos TBCDField, que utilizan el tipo Currency interna-
mente para almacenar los valores. Sin embargo, estos campos tienen una limitación: sola-
mente pueden representar hasta 4 dígitos decimales. Así que no abuse de ellos.
Finalmente, ¿está preparado para la Sorpresa número 3? Resulta que para InterBase los
siguientes tipos no solamente se representan del mismo modo, ¡sino que son indistinguibles
entre sí!
/* ... */
Columna1 numeric(5,2),
Columna2 numeric(8,2),
/* ... */
Esto sucede porque InterBase no almacena en las tablas de sistema la precisión, sino sola-
mente el factor de escala más el tipo base elegido para representarlos. Los dos tipos anteriores
son idénticos a numeric(9,2). El tipo numeric(5) es indistinguible respecto a un integer. Si
quiere comprobarlo, llame al SQL Explorer, abra un alias de InterBase y ejecute la siguiente
instrucción SQL:
Ahora sitúese en la rama del árbol de la izquierda correspondiente a los dominios, y pulse
Ctrl+R para actualizar el árbol. Seleccione el nodo DomPrueba y en el panel de la derecha
active la pestaña Text. Posiblemente le sorprenda ver que InterBase "cree" que la definición del
dominio es:
select *
from RDB$FIELDS
where RDB$FIELD_NAME = 'DOMPRUEBA'
Observe entonces que la columna rdb$field_length contiene sencillamente el valor 4, que co-
rresponde a un entero, y que no hay más información sobre la precisión utilizada en la defini-
ción.
• No merece la pena especificar una precisión y escala para columnas numéricas, pues
InterBase siempre utilizará uno de sus tipos binarios nativos para representarlas.
• Tampoco merece el esfuerzo utilizar precisión y escala si lo que necesitamos es res-
tringir el rango de valores admitidos por la columna. Como hemos visto, dentro de
cierto intervalo de valores de precisión, InterBase es incapaz de distinguir entre ellos.
Si quiere limitar un rango de valores utilice cláusulas check.
Vistas y triggers
... o triggers y vistas. ¿Y qué tiene que ver la gimnasia con la magnesia? Tiene que ver, y
mucho, con la única condición de que estemos trabajando con Oracle o InterBase. La téc-
nica que le voy a explicar es my sencilla, y pudiera resumirla en un párrafo y un diagrama
de sintaxis. Pero quiero también que comprenda todo el partido que se le puede sacar a
estos recursos. Tendremos que rebobinar la película y comenzar desde la primera escena.
Supongamos que estamos programando el mantenimiento de una tabla. Traemos una con-
sulta Query1 al formulario, le asignamos un "select * from Tabla" en su propiedad SQL,
asignamos True a la propiedad RequestLive y le enchufamos los correspondientes contro-
les visuales: rejillas, cuadros de edición o lo que se le antoje. ¿Estamos haciéndolo bien?
No: estamos haciéndolo fatal. Si la consulta no tuviese RequestLive activa, su apertura se-
ría realmente más rápida que la de un TTable. Pero al querer que su resultado sea actuali-
zable, hemos instruido al BDE para que pida información a la base de datos acerca de qué
columnas contiene el resultado, de qué tipos, cuál es la clave primaria de la tabla subya-
cente, etc, etc. La información se solicita a las tablas del catálogo de la base de datos, y es
lo que habitualmente hacen los componentes TTable. Puede encontrar más información en
mi artículo Religion Wars in Lilliput. Entonces, las consultas actualizables tardarán tanto
en abrirse como las tablas, y tendrán encima inconvenientes añadidos:
1. La navegación irá mal. Para llegar al registro 1.000 habrá que pasar por los 999
anteriores. Las operaciones Last, Locate y todas las que utilizan filtros serán len-
tas.
2. Se presentarán problemas con la cantidad de registros en el lado cliente. Usted
tiene una consulta con cinco registros, digamos por simplificar que en una rejilla.
Añade entonces un nuevo registro. ¿Se verán los seis registros en la pantalla? No:
se seguirán mostrando cinco registros. Puede que desaparezca el nuevo, o alguno
de los anteriores.
3. El método Refresh no funciona con las consultas. En caso contrario, el problema
anterior no sería tan grave. Si queremos "refrescar" una consulta, debemos cerrarla
y volverla a abrir. Como perdemos la posición original dentro del cursor, tenemos
que regresar a la fila activa mediante un bookmark o una llamada a Locate. Y ya
sabemos que ambas operaciones son costosas cuando se aplican a una consulta.
4. Una transacción confirmada mientras una consulta está abierta obligará a que to-
dos los registros de la consulta sean leídos por el cliente (FetchAll). Da lo mismo si
se trata de una transacción implícita o explícita. Hay un par de trucos para evitar
99
100 Calling Dr. Marteens
Si las consultas dan tantos dolores de cabeza, ¿cómo es posible que algún programador
siga utilizándolas? Es aquí donde comenzamos a aclarar las condiciones necesarias para
trabajar con estos componentes. En primer lugar, la mayoría de los problemas enumerados
dejan de serlo si la consulta devuelve pocos registros. Ahí va entonces la primera regla:
Pero seguimos sin resolver el problema de la lectura del catálogo para que el BDE genere
las actualizaciones, y el mal comportamiento del cursor cuando se inserta un registro. Para
evitar el largo protocolo inicial de consultas al catálogo podemos activar la opción ENABLE
SCHEMA CACHE del Motor de Datos. Dicho sea de paso: así también se resuelve el pro-
blema similar de las tablas. Sin embargo, se trata de una solución con sus propios límites,
pues por motivos para mí desconocidos, el BDE solamente puede manejar hasta 32 tablas
en la caché de esquemas. Un diseño típico de bases de datos tiene muchas más tablas
que esta cantidad. ¿Y ahora qué, listillo?
Hagamos una reflexión: ¿por qué el BDE hace preguntas a la base de datos sobre las ta-
blas, en vez de preguntarnos a nosotros? No es una idea descabellada. Puedo imaginar un
componente derivado de TDataSet, que tuviera propiedades en tiempo de diseño como
PrimaryKey, para especificar la clave primaria, InsertSQL, para indicar la instrucción SQL
que queremos lanzar cuando se realiza una inserción, ModifySQL, DeleteSQL, etc. De he-
cho, ¡existen componentes en este estilo! ¿Ha oído hablar sobre los componentes de ac-
ceso directo a InterBase FreeIBComponents? El componente que sustituye a los conjuntos
de datos confía en que el programador le indique cómo quiere actualizar cada registro leído
por una consulta. Así se ahorra la interrogación inicial de la base de datos.
ACERCA DE FreeIBComponents
Utilice FreeIBComponents, y otros componentes similares, solamente si su aplicación
está completamente basada en consultas, en vez de tablas. Los conjuntos de datos de
FIB implementan la navegación con el mismo sistema de TQuery. Así que no se le
ocurra abrir de golpe una tabla de 10.000 registros con este componente.
Para poder tener este grado de control con los conjuntos de datos del BDE es necesario
seguir estos dos pasos:
En "La Cara Oculta de Delphi 4" (capítulo 32) y en la de "... C++ Builder" (cap. 31), hay
unos cuantos ejemplos de esta técnica. Lo que nos interesa en este momento es que, si el
conjunto de datos manipulado de este modo es un TQuery, no se produce la larga consulta
inicial sobre el catálogo de la bases de datos. Desgraciadamente, esta optimización no se
produce con TTable, pues el BDE sigue necesitando la información de catálogo para im-
plementar eficientemente la navegación.
Vistas y triggers 101
Realmente, estamos usurpando en Delphi algunas atribuciones más apropiadas para que
ser desempeñadas por el servidor SQL.
set term !;
Observe que un intento de inserción debe generar una excepción. También observe que,
por simplicidad, los borrados y modificaciones utilizan todos los valores anteriores de los
campos, como si se tratase de un TDataSet con el valor upWhereAll en su propiedad
UpdateMode. Esta ha sido una decisión arbitraria, pues el programador tiene la libertad de
programar el trigger según considere pertinente. Ya puestos en el asunto, podíamos haber
programado el trigger de modificación así:
set term !;
update Pedidos
set Fecha = new.Fecha, Enviado = new.Enviado
where Numero = old.Numero;
if (old.Nombre <> new.Nombre) then
update Clientes
set Nombre = new.Nombre
where Nombre = old.Nombre;
end!
Con esto hemos permitido que el usuario pueda corregir el nombre del cliente si detecta,
por ejemplo, una falta de ortografía. Sin embargo, del mismo modo se puede establecer
que una modificación en el nombre del cliente constituya una asignación del pedido a otro
cliente diferente:
ADVERTENCIA IMPORTANTE
Esta técnica no nos evita, de todos modos, el uso de actualizaciones en caché y objetos
de actualización. El BDE trata a las vistas del mismo modo que a las consultas y ten-
dremos, por lo tanto, los mismos problemas de navegación y apertura de siempre. Sin
embargo, nos ahorraremos el tener que especificar un comportamiento tan complejo
como el anterior utilizando Delphi. Las reglas de empresa siguen su trayectoria habi-
tual: hacia el servidor, siempre que se pueda...
EN VEZ DE...
... es la traducción de instead of. Y éste es el nombre de la cláusula que necesita Oracle
para poder definir un trigger sobre una vista. Por ejemplo:
Sabemos que una pluma puede ser más fuerte que una espada. Sobre todo si la punta de
la primera ha sido convenientemente envenenada. Del mismo modo, un simple bit de más
o de menos en una vulgar estructura de Windows puede alterar radicalmente la apariencia
y el comportamiento de un control.
EL PROBLEMA
En realidad, mi intención era crear un control que mostrase estáticamente una cantidad de
dinero; nada de editarla, solamente mostrarla. ¿Por qué no un TLabel o un TStaticText? En
primer lugar, porque era necesario que el control tuviese la apariencia de un TEdit o
TDBEdit apagado. Como se comprueba fácilmente, el estilo de borde sbsSunken de los
textos estáticos no da la talla. En segundo lugar, porque el contenido del control no iba a
ser siempre el mismo; el programa debía cambiar la cantidad de dinero mostrada tras cier-
tas oscuras operaciones financieras. Y no estaba dispuesto a plagar el código fuente de
llamadas a Format y FormatFloat. Para más complicaciones, el control debía ser capaz de
cambiar en cualquier momento su formato de visualización de euros a pesetas, alterando
también el valor numérico almacenado.
type
TimEuroLabel = class
// ...
published
property Value: Currency;
property Euros: Boolean;
end;
Es muy fácil crear un control para Delphi que dibuje cualquier objeto que deseemos. Hay
alternativas: si el control no responde al teclado podemos derivarlo de TGraphicControl; si
el dibujo básico corresponde a un control existente en el sistema operativo, TWinControl es
más apropiado. Pero si el dibujo es de total responsabilidad nuestra, debemos crear un
descendiente de TCustomControl. Tras ciertas vacilaciones que no vienen a cuento, decidí
que mi "etiqueta de monedas" debía ser un TGraphicControl.
Es también muy sencillo escribir texto dentro de un control. La función más socorrida es
TextRect, en realidad un método de la clase TCanvas. La parte del dibujo del control que
solamente está relacionado con el texto podría ser similar al siguiente código:
procedure TimEuroLabel.Paint;
// Método virtual sobrescrito
var
S: string;
begin
// ... la parte que dibuja el marco ...
if FEuros then
S := FormatFloat('0,.00', FValue)
else
S := FormatFloat('0, Pts', FValue);
Canvas.Font := Font;
Canvas.Color := Color;
Canvas.TextRect(ClientRect, // El rectángulo interior
ClientWidth - Canvas.TextWidth(S) - 3, // Justificado a la derecha
2, // Posición vertical
S); // El texto a mostrar
end;
105
106 Calling Dr. Marteens
¿No habrá alguna ingeniosa función que nos eche una mano? En algún truco anterior he
mencionado a DrawFrameControl, que permite dibujar varios controles habituales de
Windows ... excepto los cuadros de edición, que son los que necesitamos ahora. Pero tam-
bién existe DrawEdge, ¡que permite dibujar un rectángulo tridimensional con muchas op-
ciones!
LA SOLUCION
Cuando me encuentro con este tipo de problemas, suelo visitar el código fuente de la VCL.
En este caso, inmediatamente recordé un hecho que a muchos quizás sorprenda: el control
TDBLookupComboBox no está basado en modo alguno en los combos nativos de
Windows, sino que es totalmente obra de la VCL. El texto se muestra con TextRect, la fle-
cha de despliegue con una llamada a DrawFrameControl, la lista desplegable es una ins-
tancia dinámica de TDBLookupListBox, ¡que también es dibujada por completo por la VCL!
Pero ... ¿y el borde?
function CreateWindowExA(
dwExStyle: DWORD; lpClassName: PAnsiChar;
lpWindowName: PAnsiChar; dwStyle: DWORD;
X, Y, nWidth, nHeight: Integer;
hWndParent: HWND; hMenu: HMENU; hInstance: HINST;
lpParam: Pointer): HWND; stdcall;
El primer paso, si queremos utilizar este mecanismo para trazar el borde exterior, es re-
nunciar a la derivación del control a partir de TGraphicControl, y utilizar como base la clase
TCustomControl. ¿Cómo podemos indicarle a la VCL las constantes de estilo que utiliza
nuestro control? La VCL, antes de llamar a CreateWindowEx, ejecuta el método virtual
CreateParams para darnos la oportunidad de modificar los atributos de creación. Queda
claro entonces que TimEuroLabel debe redefinir dicho método:
POSIBILIDADES...
Para que experimente con las múltiples posibilidades que permite el uso de CreateParams,
cree un componente derivado de TWinControl e implemente el siguiente método:
No le voy a estropear la sorpresa diciéndole para qué sirve el control anterior. Juegue un poco
con el mismo y disfrute de su asombro antes las cosas que puede lograr un humilde bit.
Ahorrando espacios con las
etiquetas
La ventana típica de una típica aplicación para bases de datos contiene montones de controles
de edición, quizás organizados por páginas, si el programador ha sido misericordioso con sus
usuarios ... y decenas de inertes etiquetas de texto que se asocian a dichos controles para
hacer mínimamente comprensible su significado.
Este truco puede ayudarle a ganar espacio y a hacer más legible el código fuente:
¿Qué ganamos con ésto? Vaya al código fuente del formulario después de realizar la transfor-
mación. Notará que, en la declaración de la clase del formulario ya no aparecen las declaracio-
nes correspondientes a las etiquetas. Esto implica que ya no podremos modificar en tiempo de
ejecución las características de estos componentes, pero ¿a quién le importa eso? En com-
pensación, un listado impreso del código fuente se acorta en un número de líneas idéntico al
número de TLabels que han perdido su honor en el proceso. Desde mi punto de vista ésta es
la principal ventaja del truco.
Pero hay más: también se gana espacio en el fichero DFM. Antes de aplicar el truco, cada eti-
queta comenzaba su declaración dentro del DFM más o menos de este modo:
object TLabel
Left = 164
Top = 108
Width = 32
Height = 13
Caption = 'Indice de Mala Leche Equivalente (%):'
end
Nos hemos ahorrado tantos bytes como caracteres tenía el nombre de la etiqueta. Por su-
puesto, da lo mismo que el fichero DFM esté en formato binario o en el formato de texto de
Delphi 5.
Sin embargo, hace poco me di cuenta que hay una pequeña ganancia adicional en espacio que
muchas veces ignoramos; este "descubrimiento" me ha impulsado a escribir este truco. Primero
una pregunta. Observe la siguiente declaración, y dígame a qué sección (private, protected o
public) cree usted que pertenece la declaración de la etiqueta:
type
109
110 Calling Dr. Marteens
TForm1 = class(TForm)
Label1: TLabel;
private
{ Private declarations }
public
{ Public declarations }
end;
Pues ni pública ni privada ni protegida: la sección que sigue inmediatamente a la palabra class
¡es published! Y las declaraciones en published generan estructuras de datos asociadas a la
clase que también ocupan espacio en tiempo de ejecución. No sólo ahorraremos este espacio,
sino que también aceleraremos algo la carga de la ventana y la asociación de objetos persis-
tentes a los campos de la clase del formulario.
Este punto es muy importante. Si todas las referencias a la clase TLabel desaparecen del có-
digo fuente de nuestro proyecto, el enlazador inteligente de Delphi llegará a la conclusión de
que no necesita incluir el código binario correspondiente en el ejecutable. La llamada a
RegisterClasses evita el problema.
Tome nota de que, aunque durante toda esta página me he referido a la clase TLabel, todo lo
dicho se aplica también a otras clases "pasivas" como TBevel, TPanel, e incluso a determina-
dos TDBEdit.
Acción inmediata
Había liquidado los últimos fallos de aquel monstruoso programa y lo estaba mostrando,
lleno de orgullo, a uno de sus futuros usuarios. Este dedicaba su atención a una ventana
mediante la cual podía contratar determinados servicios de la compañía. El registro del
producto contenía un campo lógico, que al ser activado debía añadir un seguro mensual al
contrato; el coste del seguro debía reflejarse en el importe total de la contratación. Natu-
ralmente, la edición de dicho campo se realizaba mediante un componente
TDBCheckBox.
La anécdota ficticia que acabo de relatar (¡ninguno de mis usuarios se llama Pepe!) de-
muestra un comportamiento anómalo de TDBCheckBox, pero que también es padecido por
los restantes controles de datos de Delphi. Digámoslo en pocas palabras:
Es decir: nos ponemos a teclear sobre un TDBEdit, pero mientras tecleamos el contenido
original del campo asociado sigue siendo el mismo. Tenemos que pasar al siguiente control
dentro del formulario para que las modificaciones surtan efecto. En ese momento es que se
disparan, además, los eventos OnValidate y OnChange del campo, si es que tienen código
asociado.
¿Por quéeeee? Aceptemos lo contrario: que cada vez que cambie el contenido del editor,
se modifique el campo simultáneamente. Esto es posible y sensato si el campo editado es
numérico, o si es un nombre, por mencionar un par de casos. Pero no es factible cuando el
campo representa una fecha; la cadena "12/", a pesar de nuestras mejores intenciones de
completarla en breve, no representa una fecha correcta.
Así que la culpa de todo la tiene el control TDBEdit. Es lógico que suceda lo mismo con un
TDBComboBox, pues en el fondo se trata simplemente de un TDBEdit con cuernos ...
quiero decir, con lista desplegable. Pero no es tan evidente cuando se trata de un
TDBCheckBox, o de un TDBLookupComboBox, pues estos controles siempre (o casi
siempre) contienen un valor correcto para el campo que representan. Irónicamente, el cul-
pable TDBEdit ofrece un mecanismo adicional para que el usuario diga "lo que he tecleado
hasta aquí vale", sin necesidad de pasar al control siguiente. Si tecleamos Intro sobre el
control, su texto pasa inmediatamente al campo.
111
112 Calling Dr. Marteens
procedure TimDBCheckBox.Toggle;
begin
if FDataLink.Edit then
begin
inherited Toggle;
FDataLink.Modified;
if FImmediate then // Nueva línea
FDataLink.UpdateRecord; // Nueva línea
end;
end;
Immediate es una propiedad de tipo Boolean, y sirve para activar el modo de asignación
inmediata a campos.
Más potente que copiar y pegar
Estoy seguro de que alguien me bendecirá por este truco. Usted tiene un conjunto de datos
(da lo mismo ahora si es una tabla, una consulta, o un client dataset) situado en un módulo
de datos o en un formulario. Inesperadamente, se le enciende la bombilla y se da cuenta
que el mejor lugar para dicho componente es otro módulo o formulario. Hay que organizar
la mudanza. Y dos verbos le vienen a la mente: copiar y pegar.
Pero la aventura no puede terminar tan fácilmente. Usted copia el objeto al Portapapeles, y
lo pega en su nuevo domicilio ... bien, también se copian los campos definidos para el
conjunto de datos; un aplauso para Delphi. Entonces comprende que algo falta: algunos de
aquellos campos tenían eventos asociados. No le queda más remedio que ir, campo por
campo, recreando los métodos de respuesta, y trasladando fatigosamente el código de
módulo a módulo...
Por supuesto, hay una forma más fácil de conseguirlo, aunque no sea tan evidente.
¿Quiere mover de módulo a varios objetos simultáneamente, y quiere conservar además
los eventos asociados? Seleccione dichos objetos, y ejecute el comando de menú
Component|Create component template. ¿Nombre para la plantilla de componentes? El
que más rabia le dé, siempre que lo recuerde. Luego vaya a la ubicación de destino y traiga
el "pseudo-componente". Comprobará que las respuestas a eventos se han respetado. Fi-
nalmente, pulse el botón derecho del ratón sobre la Paleta de Componentes, active el co-
mando de Propiedades y utilícelo para eliminar la plantilla de componentes temporal.
He presentado el ejemplo mediante un conjunto de datos, pero la técnica puede aplicarse a cualquier
otro tipo de componentes, o de grupo de componentes.
113
¡La dirección, idiota, la
dirección...!
El SQL Server de Microsoft no deja de sorprenderme día a día ... casi siempre de forma desa-
gradable. Cuando pensaba que ya no podía encontrar más pretextos para no utilizarlo, he aquí
que Microsoft saca un as de la manga y te das cuenta de cuánto puedes odiar a alguien o algo.
Qué tristeza, he perdido la poca fé que me quedaba en la naturaleza humana.
Al grano. Para producir el desastre vale lo mismo la versión 6, que la 6.5, la 7 y sospecho que
cuando haya una 8, igual. Cree un procedimiento idiota como el siguiente, utilizando SQL
Explorer o el SQL Query Tool (rebautizado como SQL Query Analizer en la 7; es como cuando
llegas a tu casa de madrugada tras una juerga, no enciendes la luz y los dedos de tus pies
descubren que alguien ha cambiado los muebles de lugar):
Se supone que esta joya de la programación recibe tres parámetros. Los dos primeros son
parámetros de entrada; el procedimiento los suma y coloca el resultado en el tercer parámetro,
que ha sido declarado como un parámetro de salida.
¡Pobre de mí! Yo pensaba que podía utilizar el procedimiento anterior desde otro procedimiento
de esta simple manera:
execute OtraIdiotez 1, 2
¿Sabe qué valor se muestra en pantalla? ¡Nulo, amigo mío, en vez de 3! La variable
@resultado no recibe el valor asignado al parámetro de salida @c, durante la llamada interna a
ExtremaImbecilidad.
Para aquellos que no están familiarizados con Transact SQL, es necesario saber que la instrucción
select puede utilizarse en dos contextos muy diferentes en este dialecto. Dentro de
ExtremaImbecilidad, cada valor mencionado en la cláusula se asigna a una variable local. Este es el
equivalente al select/into de Oracle e InterBase. El ejemplo utilizado en OtraIdiotez, sin embargo, no
tiene equivalente directo en estos sistemas. Si no hay variables a las que asignar los valores del
select, dichos valores se muestran en la propia ventana de SQL Query Analizer, o se ignoran en
caso contrario. Esta es una técnica bastante primitiva para depurar procedimientos almacenados.
Pero también se utilizan los select libres para programar procedimientos de selección en Transact
SQL: procedimientos que devuelven un conjunto de filas como resultado.
115
116 Calling Dr. Marteens
La palabra reservada output funciona en este caso como un operador de referencia, que toma
la dirección de la variable precedente y la introduce en la pila.
¿Por qué no había tropezado antes con este problema? Sencillamente porque (gracias a Dios)
nunca había tenido que desarrollar ningún programa realmente grande con SQL Server. Y
siempre que había programado un procedimiento almacenado con parámetros de salida, termi-
naba llamando al procedimiento desde Delphi o C++ Builder. De hecho, podía haberme imagi-
nado lo que estaba sucediendo. ¿Se ha dado cuenta de que Delphi es incapaz de adivinar si
los parámetros de un procedimiento almacenado de SQL Server son de entrada o de salida? Al
parecer, esta información no está disponible en el catálogo de la base de datos.
Por este motivo, desde ayer un post-it adorna mi monitor, con un gran mensaje en letras rojas
sobre fondo amarillo:
Si lo que deseamos es, sencillamente, dibujar una sola línea, la técnica adecuada consiste
en combinar los métodos MoveTo y LineTo, que pertenecen a la clase TCanvas. El proto-
tipo de los mismos es el siguiente:
Se supone que toda superficie de dibujo en Windows (device context, en jerga técnica) re-
cuerda una determinada posición en su interior, a la que se le llama "posición actual". Al-
gunas de las funciones de dibujo de Windows, no todas, utilizan esta posición como punto
de partida, ahorrándonos un par de parámetros en su ejecución.
Parece algo elemental, pero el siguiente hecho es ignorado por muchos programadores:
Podemos observar que ninguno de estos métodos indica el color de la línea, ni su grosor o
estilo. ¡Bien, esta es una característica de la Programación Orientada a Objetos! (a veces,
hasta Microsoft acierta). En vez de declarar una función con montones de parámetros inú-
tiles que cambian con muy poca frecuencia, un objeto puede almacenar estos valores más
o menos estables como parte de su estado interno. En la encapsulación del API de
Windows que hace la VCL de Delphi, por ejemplo, el aspecto de la línea dibujada depende
de la propiedad Pen de la superficie de dibujo. Aunque sería curioso investigar todas las
posibilidades, dejaremos esa tarea para un mejor día.
Los metodos presentados anteriormente (MoveTo y LineTo) encapsulan las siguientes fun-
ciones del API de Windows:
117
118 Calling Dr. Marteens
¿Puede averiguarse la posición del cursor gráfico desde Delphi? Sí, si utilizamos la propie-
dad PenPos, también perteneciente a TCanvas. Es interesante saber que dicha propiedad
permite también asignaciones, de modo que podemos llamar indirectamente a MoveToEx
modificando PenPos. Para recuperar la posición actual en la implementación de PenPos,
sin embargo, se utiliza la siguiente función del API:
¿Y si tenemos que dibujar más de un segmento de línea? Parece una pregunta tonta, pues
al parecer basta con llamar consecutivamente al método LineTo:
Canvas.MoveTo(X0, Y0);
Canvas.LineTo(X1, Y1);
Canvas.LineTo(X2, Y2);
Como cada ejecución de LineTo deja el cursor gráfico en el punto final, la llamada que si-
gue a LineTo toma como punto de partida esa misma posición, y los segmentos quedan
enlazados.
Aunque la técnica es correcta, tiene un grave problema: nos obliga a atravesar la "barrera"
del sistema operativo una y otra vez. Es ligeramente más rápido, por lo general, llamar a
una rutina de nuestra propia aplicación que llamar a una rutina equivalente perteneciente a
las DLLs del sistema. Es cierto que se trata de una demora pequeña, pero recuerde que las
funciones de dibujo suponen en muchas ocasiones un cuello de botella para nuestros pro-
gramas. Además, es posible que nuestro ordenador esté equipado con una tarjeta acelera-
dora por hardware que ofrezca una operación especial para optimizar el trazado de varias
líneas enlazadas.
A Polyline debemos pasarle un vector "abierto" de puntos, para que Windows trace seg-
mentos de recta entre cada par consecutivo de puntos. La ventaja del uso de esta función
se explica porque toda la operación se realiza con una sola llamada al sistema operativo, y
así podemos eliminar el coste asociado al paso por la barrera antes mencionada.
Podemos observar que la lista de puntos se pasa como un parámetro "sin tipo", pues se ha
utilizado únicamente la palabra clave var en su declaración formal. Esta técnica es suma-
mente peligrosa, pues permite suministrar "cualquier cosa" en el parámetro Points.
El ejemplo ha resultado muy sencillo: sabemos siempre que necesitamos 6 puntos, así que
la memoria para el array se ha reservado automáticamente, en una variable local. Si la
cantidad de puntos hubiese sido variable, la solución habría consistido en pedir memoria
dinámicamente:
var
Pts: array of TPoint;
begin
// ...
SetLength(Pts, CantidadPuntos);
// ... llenar la lista ...
Canvas.Polyline(Pts);
end;
Llevemos ahora la técnica anterior a su máximo desarrollo. Supongamos esta vez que sí,
efectivamente, queremos dibujar muchos segmentos de recta, pero esta vez los segmentos
no van a ser consecutivos. Piense en cualquier dibujo formado por rectas que no se pueda
reproducir sin levantar el lápiz del papel. Por ejemplo, las líneas de una rejilla. Claro que
podemos llamar varias veces a Polyline, pero queremos algo más potente. Y ese algo más
potente existe.
El primer parámetro es el handle del contexto de dispositivo: ahí debemos pasar el valor de
la propiedad Handle del objeto Canvas sobre el cual queremos dibujar. El segundo pará-
metro es ya un poco más complicado. Su declaración comienza con el modificador const, y
no utiliza ningún identificador de tipo después del nombre del parámetro. Esto quiere decir
que ahí podemos pasar la dirección de cualquier tipo de variable. Teóricamente. En la
práctica, debemos pasar la dirección inicial de un array con TODOS los puntos del dibujo
final concatenados. Queríamos trazar varias polilíneas, ¿no? Pues nos montamos un su-
pervector con todos esos puntos.
Naturalmente, hay que indicar dónde comienza y dónde termina la definición de cada polilí-
nea. Esa es la función del tercer parámetro, donde debemos pasar otro array, esta vez de
valores enteros. Cada elemento suyo debe corresponder a la cantidad de puntos de la poli-
línea correspondiente. Si, por ejemplo, queremos dibujar una polilínea de 5 puntos, otra de
10 y finalizar con una de 4, el tercer parámetro debe apuntar a un vector con 3 elementos:
5, 10 y 4 serán los valores de sus elementos.
120 Calling Dr. Marteens
Así que el tercer parámetro sirve para establecer las dimensiones del segundo. Entonces,
el cuarto parámetro es el que indica las dimensiones del tercero. En nuestro ejemplo te-
níamos 3 polilíneas: hay que pasar el valor 3 en el último parámetro.
SENO Y COSENO
La función SinCos está declarada en la unidad Math, y es otro claro ejemplo de la polí-
tica "mientras menos viajes, mejor". La instrucción de la FPU (floating point unit) que
calcula el seno, también calcula de paso el coseno. Si llamamos a las conocidas funcio-
nes Sin y Cos por separado, estamos desaprovechando esta oportunidad de ahorrar
tiempo.
VARIABLES "SEMIDINAMICAS"
Ahora ya puedo confesar por qué se me ocurrió escribir acerca de las líneas. La culpa la
tienen las rejillas de datos. Más exactamente: la técnica de dibujo que utilizan los compo-
nentes TCustomGrid y sus descendientes. ¿Cómo se dibuja una rejilla? Evidentemente,
hay muchas líneas por medio, y no puede utilizarse Polyline, sencillamente, para dibujarlas.
Hay que utilizar PolyPolyline.
En cualquier caso, hay que pasar una lista con cantidad variable de puntos a cualquiera de
estas dos funciones. La cantidad de puntos es variable porque depende principalmente del
tamaño de la rejilla, que puede variar en tiempo de ejecución, pero también de varias op-
ciones de dibujo. Así que no podemos reservar un array en la pila de un procedimiento,
como hemos hecho antes. ¿Qué podemos hacer?
Quizás la solución más obvia, a partir de Delphi 4, sería recurrir a vectores dinámicos para
almacenar los extremos de los segmentos. Supongamos que necesitamos un array con N
puntos:
Líneas 121
var
Pts: array of TPoint;
begin
// ... ya hemos visto este ejemplo ...
SetLength(Pts, N);
// ... llenar la lista y llamar a PolyPolyline ...
end;
Lo malo es que SetLength reserva memoria de la zona conocida como heap. Y la opera-
ción de asignación de memoria dinámica es un poco lenta. Para empeorar las cosas, la
llamada a SetLength debe hacerse dentro del procedimiento de dibujo de la rejilla; esto es,
dentro de una zona que puede ocasionar un cuello de botella.
Ahí está: la función StackAlloc "roba" un trozo de memoria del stack frame (pido perdón a
los puristas del lenguaje) del procedimiento activo. La forma en la que ocurre el robo está
diseñada para que sea compatible con el código generador por el compilador de Delphi.
StackAlloc, además, devuelve el puntero a la zona "robada". Trabajamos con el área reser-
vada y, finalmente, debemos liberarla con StackFree (aunque este paso, en realidad, no es
necesario según la propia Borland).
El Misterio de la Cabecera
Perdida
Esta es la historia de un bug, pero de un bug tan maldito que ha logrado sobrevivir a unas
cuantas versiones del BDE. El relato también podría titularse La Maldición de los Cached
Updates, pues su argumento trata acerca de este misterioso recurso del que todos hablan,
pero pocos comprenden.
¿Cuál es el problema? Pues que, a mitad de camino, el usuario puede decidir anular el pe-
dido. Hay programadores que, simplemente, eliminan la cabecera y todas las líneas de
detalles. Si la base de datos permite borrados en cascada, esta operación es relativamente
sencilla. En caso contrario, hay que implementarla a mano. Tengo muchos otros reparos
hacia esta opción elemental, pero no es éste el momento adecuado para exponerlos.
Otra opción es delimitar la operación de alta por medio de una transacción. Para anular el
pedido basta con hacer un Rollback sobre la base de datos, y se cancelan todos los cam-
bios realizados en Cabecera, Detalles e incluso las modificaciones de las existencias de
productos en la tabla de Inventario. Y para confirmar, basta ejecutar el método Commit.
¿Algo malo? Sí: cualquier registro modificado durante la transacción se queda bloqueado
hasta el final de la misma. Así que no se le ocurra programar las actualizaciones del in-
ventario como triggers disparados por la tabla de detalles, o no podrá vender el mismo pro-
ducto concurrentemente desde dos puestos.
procedure TDataMod.GrabarPedido;
begin
Database1.StartTransaction;
try
Cabecera.ApplyUpdates;
Detalles.ApplyUpdates;
Database1.Commit;
except
Database1.Rollback;
raise;
123
124 Calling Dr. Marteens
end;
Cabecera.CommitUpdates;
Detalles.CommitUpdates;
end;
• La fila de cabecera se graba sin problemas. Recuerde que se trata de un único registro.
• Cuando se están grabando las filas de detalles se produce un error en el servidor. Por
ejemplo, puede que no existan existencias suficientes para un producto determinado. O
que la fila correspondiente en la tabla de inventarios esté bloqueada.
• La llamada a ApplyUpdates sobre la tabla Detalles falla, en consecuencia. Se produce
una excepción.
• La excepción nos conduce a la cláusula except. Allí se cancela la transacción, y el
error se vuelve a lanzar.
• Las llamadas a CommitUpdates no se efectúan. Por lo tanto, cuando el usuario recibe
el mensaje de error, las cachés independientes de la cabecera y los detalles siguen
conteniendo los valores anteriores al intento de grabación. Además, la base de datos
continúa en su estado original. Por lo tanto, el usuario puede tomar alguna medida
(modificar alguna cantidad, eliminar algún producto, o simplemente esperar a que el
producto deje de estar bloqueado) y reintentar la operación.
• Pero cuando se repite la grabación, se produce inexorablemente una excepción con el
enigmático mensaje: “ Master record missing” .
• ApplyUpdates solamente debe grabar la caché de la tabla o consulta, pero no debe va-
ciarla prematuramente. Así contemplamos la posibilidad de que se produzca algún otro
fallo más adelante, antes de cerrar la transacción.
• CommitUpdates es el responsable de vaciar realmente la caché. El método de Delphi
llama directamente a una función del BDE, y no debe generar excepciones. Es por eso
que podemos situar las llamadas a este método fuera de la instrucción try/except.
De modo que al reintentar la grabación, la caché de la cabecera debe contener aún las
modificaciones realizadas al conjunto de datos; en este caso, el nuevo registro a insertar.
Al menos en teoría. Porque en la práctica, el BDE no vacía la caché, pero marca interna-
mente todos sus registros como “ aplicados” , aunque solamente hayamos ejecutado
ApplyUpdates y no CommitUpdates. Se trata de un error del BDE, no de la VCL. Si fuese
esto último, quizás podríamos realizar las correcciones en el código fuente, pero no es el
caso.
SE COMPLICA LA TRAMA
El diagnóstico ha sido fácil, pero la solución será complicada. Lo que primero se nos ocu-
rre: ¿cuál es el problema, que la tabla de cabecera se queda marcada como “ limpia” ? Pues
ensuciémosla. Para detectar los errores durante la grabación de la tabla de detalles debe-
mos interceptar el evento OnUpdateError de Detalles:
end;
Este código tan simple logra que la propiedad Modified de la tabla Cabecera cambie a
True. La próxima vez que se intente la grabación habrá un registro en la caché de la tabla
maestra marcado como modificado ... por lo cual el BDE generará una instrucción update,
como si ya existiese dicho registro. En otras palabras, estamos en las mismas.
¿Cómo lograr que una modificación pueda grabarse como una inserción? Existe una forma
muy sencilla, y consiste en tomar el control de la generación de instrucciones de actualiza-
ción mediante un objeto TUpdateSQL y el evento OnUpdateRecord de la tabla de cabe-
cera. Colocamos un componente TUpdateSQL en el módulo de datos, y lo asignamos en la
propiedad UpdateObject de la tabla de cabecera.
Está claro que he simplificado un poco el trabajo al asumir que Cabecera y Detalles se utili-
zan solamente para altas, y no para modificaciones. De todos modos, si queremos asegu-
rarnos que este “ desvío” se produce sólo en caso de error, podemos añadir una variable
privada al módulo de datos:
type
TDataMod = class(TDataModule)
// ...
private
FErrorPending: Boolean;
end;
Esta variable se pondrá a True cuando se produzca un error en la grabación de los deta-
lles:
Por supuesto, al iniciar el proceso de entrada de pedidos se debe asignar False a la varia-
ble. Entonces podemos dejar las tres instrucciones SQL originales del TUpdateSQL, y en
su lugar interceptaremos el evento OnUpdateRecord de la tabla de cabecera:
UpdateSQL1.Apply(ukInsert)
else
UpdateSQL1.Apply(UpdateKind);
UpdateAction := uaApplied;
end;
(...se enciende la luz, y el encargado de la limpieza sacude las palomitas de maíz de las
butacas...)
Nombres de dominio flexibles
Si usted desarrolla aplicaciones para Internet y tiene que utilizar varios ordenadores en el
proceso de desarrollo, como es mi caso, recibirá este sencillo truco como una bendición.
Para agravar el asunto, parte del texto HTML que sirve de semilla a las respuestas de la
aplicación puede residir dentro del propio código o en ficheros HTML externos. El primer
caso sería el de un componente como TPageProducer que tuviese asignada su propiedad
HTMLDoc. El segundo caso sería el del mismo componente cuando hacer referencia a un
fichero externo mediante la propiedad HTMLFile. Es decir, la referencia al nombre del do-
minio puede venir configurada en los más diversos formatos y ubicaciones.
Afortunadamente, existe un punto central dentro las aplicaciones Web escritas con Delphi
en el cual podemos resolver el problema planteado. Se trata de la respuesta al evento
AfterDispatch del módulo Web:
type
THTTPMethodEvent = procedure (Sender: TObject; Request: TWebRequest;
Response: TWebResponse; var Handled: Boolean) of object;
El evento se dispara cuando ya se han activado todas las acciones que tenían algo que
decir. La respuesta se encuentra dentro de la propiedad Content del parámetro Response.
El truco consiste en buscar dentro de la respuesta cierta cadena de caracteres, que utiliza-
remos como símbolo del nombre de dominio, y reemplazarla cuantas veces aparezca por el
nombre de dominio verdadero.
<html>
<body>
<p>Visite nuestra <a href="$#!+/moreinfo.htm">página de información</a>.</p>
</body>
</html>
const
SDomainSymbol = '$#¡+'; SDomain = 'http://naroa/scripts';
Debemos modificar el valor definido para SDomain cuando cambiamos de ordenador. Por
supuesto, un truco más completo consistiría en sustituir la constante por una función inteli-
gente que detecte o deduzca el nombre del dominio. Esto se lo dejo a usted.
127
128 Calling Dr. Marteens
begin
if CompareText(Response.ContentType, 'text/html') = 0 then
Response.Content := StringReplace(Response.Content, SDomainSymbol,
SDomain, [rfReplaceAll]);
end;
Bien, pues lo mío es la Física. Y las partes complicadas, nada menos: mecánica cuántica,
teoría general de la relatividad y cosas así de entretenidas. Incluso a veces imagino que el
Universo es un gran ordenador, que se va inventando las leyes a la vez que ejecuta las le-
yes antiguas. "It from bit", como diría John Wheeler, el famoso físico.
LA CAJA NEGRA
¿Por qué todo el preámbulo anterior? Resulta que he estado leyendo el famoso libro sobre
patterns, "Design Patterns", de E. Gamma et al. No voy a dar aquí mi opinión; todavía no
he recibido la divina iluminación que prometen los autores. Pero me ha llamado bastante la
atención la ausencia de cierto patrón muy frecuente: lo llamaré la "caja negra".
Pondré un ejemplo de una aplicación real para que comprendáis más fácilmente de qué se
trata. Me traigo entre manos un proyecto para una financiera, y me las estoy viendo con
préstamos, hipotecas, intereses variables y toda esa parafernalia. Muy importante para la
aplicación es el cálculo de intereses y tablas de amortización. Si se tratara únicamente de
los casos más sencillos, me valdría quizás una función para poder calcular las condiciones
de un préstamo. Pero, aunque quizás no me crea, existen decenas de variables que alteran
la fórmula que se emplea.
¿Qué hago, programo una gran e inmensa función con sepetecientos parámetros? ¡No,
odio las funciones con muchos parámetros! Casi nunca te acuerdas del orden de los mis-
mos, y casi siempre tienes que pasar sencillamente ciertos valores predeterminados. Ade-
más, el cálculo de intereses no ofrece como resultado un sencillo valor escalar, sino toda
una tribu de ellos, sin contar la tabla de amortización, que es una estructura vectorial.
SIMULANDO UN PRESTAMO
Ahí va una primera aproximación a lo que necesitamos (omitiré muchos atributos del prés-
tamo):
type
TisLoan = class(TComponent) // La "is" es por IntSight
// ...
public
property Financed: Currency; // La cantidad
property Term: Word; // El plazo
property IntRate: Double; // El interés
public
property FirstPmt: Currency; // La primera cuota
property RegularPmt: Currency; // Cuota regular
property COB: Currency; // Coste del préstamo
129
130 Calling Dr. Marteens
Se puede apreciar cómo se produce una clara división de las propiedades de la clase: hay
tres de ellas que posiblemente serán de lectura y escritura (las tres primeras). Las restan-
tes, en nuestro modelo simplificado, serán de sólo lectura. El problema consiste en cuándo
debemos activar el algoritmo de cálculo de intereses.
MAGNITUDES OBSERVABLES
Y aquí es dónde me voy a inspirar en la física y la filosofía. La forma de trabajo que le voy a
proponer es la siguiente: para realizar el cálculo de cuotas, tablas de amortización y el
resto de estos asuntos tan excitantes como un partido de petanca (habrá quien se ponga
cachondo con esto, digo yo), debemos asignar, en el orden que se nos antoje, las propie-
dades "de entrada" de nuestro modelo. Mientras asignamos estas propiedades, ¿qué valor
deben ir tomando las propiedades "de salida"? ¡Ah, amigo!, eso será un misterio.
Había cierto señor Berkeley que sostenía más o menos que el mundo era un invento nues-
tro: a eso se le llama idealismo, y desde el punto de vista lógico no es tan disparatado
como puede parecer. Para refutar su teoría le plantearon el conocido "experimento de la
moneda", que hacía muchos años habían ingeniado los griegos. Un viajero tiene una mo-
neda en el bolsillo. Según los materialistas, la moneda simplemente "existe", pero según
Berkeley la moneda existe dentro de la mente del viajero. Aceptémoslo. En un descuido la
moneda se cae en una encrucijada del camino (el viajero tiene agujeros en los bolsillos).
Un par de millas más adelante, (Berkeley era inglés, claro) el viajero tantea su ropa y nota
la ausencia de la moneda. La da por perdida.
Un año después, algún tonto (siempre tienen suerte) pasa por el cruce de caminos y en-
cuentra la moneda. Lo que hará con ella ya no es problema nuestro. La pregunta es: ¿se
trata de la misma moneda? Si es la misma moneda, ¿qué es lo que garantiza la continui-
dad de su existencia durante todo el tiempo que permanece perdida? Berkeley (se me ol-
vidó decir que era clérigo) respondió que durante ese intervalo la moneda había seguido
existiendo "...porque estaba a la vista de Dios". No le gustaba perder una discusión.
Anécdotas aparte, resulta que algo en cierto modo similar ocurre en la física cuántica.
¿Cuál es el estado de un electrón, digamos que su posición y velocidad, en un momento
dado? La teoría cuántica es incapaz de decirlo con exactitud. Nos consuela con una fun-
ción de densidad que adecuadamente tratada nos dice la probabilidad de que el electrón
tenga cierto estado u otro. Pero nos basta realizar una medición sobre el mismo para que
estas posibilidades "paralelas" tomen un rumbo bien definido. "El Jardín de los Senderos
que se Bifurcan", de Borges, es una elaborada metáfora de esta parte de la física cuántica.
¿Qué tienen que ver los electrones con los préstamos? Pues que las propiedades de salida
de un préstamo mantendrán un estado indeterminado hasta que a alguien se le ocurra pre-
guntar el valor de alguna de ellas. En lo que queda de artículo veremos cómo implementar
clases que actúen de este modo; "cajas negras", como me gusta llamarlas.
LA IMPLEMENTACION
El primer paso es sumamente sencillo: todas las clases tipo "caja negra" deben tener una
variable interna que indique si se han producido cambios en alguna de las variables de en-
trada:
type
TisLoan = class(TComponent) // La "is" es por IntSight
protected
FDirty: Boolean;
El Principio de Incertidumbre de Heisenberg 131
// ...
end;
El siguiente paso consiste en implementar las escrituras de todas las propiedades de en-
trada mediante métodos de la clase que marquen el atributo FDirty en las asignaciones.
Por ejemplo, la propiedad Term (el plazo del préstamo) se declara del siguiente modo:
type
TisLoan = class(TComponent)
private
FTerm: Integer;
procedure SetTerm(Value: Integer);
public
property Term: Integer read FTerm write SetTerm;
// ...
end;
Por su parte, el acceso en lectura a todas las propiedades de salida será implementado
mediante funciones. Por ejemplo, así se implementa la propiedad RegularPmt, que con-
tiene la cuota regular que debe pagarse:
type
TisLoan = class(TComponent)
private
FRegularPmt: Currency;
function GetRegularPmt: Currency;
public
property RegularPmt: Currency read GetRegularPmt;
end;
Observe el detalle fundamental: hay una llamada a un intrigante método Recalculate. Bien,
pues este Recalculate contiene el meollo de la clase:
procedure TisLoan.Recalculate;
begin
if FDirty then
begin
// Calcular los valores de salida y dejarlos en atributos privados
FDirty := False;
end;
end;
Queda clara ahora nuestra estrategia. Cada vez que modificamos una propiedad de en-
trada, la clase se marca a sí misma como "sucia": los valores de salida son potencialmente
incorrectos. Pero no toma ninguna acción para corregir su estado hasta que alguien pre-
gunta por alguno de los valores de salida. En ese momento, la clase "recuerda" que nece-
sita actualizarse y calcula frenéticamente el valor solicitado ... y todos los demás. Podemos
entonces pedir cualquier otra propiedad de salida, que ya no necesitaremos más cálculo; al
menos mientras no volvamos a modificar las condiciones iniciales.
132 Calling Dr. Marteens
CONCLUSIONES
Os podéis dar cuenta de que la parte verdaderamente interesante de esta clase es el algo-
ritmo implementado dentro de Recalculate. Esto nos puede servir de guía para identificar
los casos en que es conveniente una clase tipo "caja negra": cuando necesitamos encap-
sular una función con muchos parámetros de entrada y varios parámetros de salida. La
caja negra nos permitirá ser máx flexibles en el momento de pasar los datos iniciales, pues
no hace falta seguir un orden estricto en la asignación de ellos, e incluso podemos dar por
buenos, en ocasiones, aquellos valores de entrada que coincidan con los valores por omi-
sión de las propiedades correspondientes. También la consulta de los parámetros de salida
se convierte en algo sencillo, pues podemos preguntar por ellos de uno en uno, y eficiente,
pues no necesitamos recalcular en cada pregunta.
Por supuesto, hay que tener cuidado al implementar Recalculate y disparar excepciones
cuando no disponemos de algunos de los valores de entrada, o si tienen valores inconsis-
tentes.
Columnas de sólo lectura
Este es el típico truco tonto de la página de Marteens. Espero, no obstante, que le sea útil a
alguien como lo ha sido para mí.
Usted tienen un rejilla, un TDBGrid. Quiere que alguna de sus columnas sea intocable, que
no puedan modificarse los valores que muestra, pero a la vez desea que puedan editarse
otras columnas. Fácil, ¿no?. Porque nos basta con asignar True a la propiedad ReadOnly
de la columna correspondiente de la rejilla.
¿Y qué tal si, para que el usuario no se confunda más de lo que suele estar, cambiamos el
color de la columna utilizando su propiedad Color? Pues que obtenemos un estupendo
efecto visual ... excepto que si nos posicionamos sobre una celda de la columna y pulsa-
mos el ratón otra vez, o intentamos escribir, la rejilla hace como si fuese a permitir la edi-
ción, aunque realmente no lo permita. Entre otras cosas, la celda "manipulada" vuelve a te-
ner el mismo color blanco de siempre, y le damos una oportunidad al usuario de que se
queje como siempre.
¿Por qué pasa esto? Pues porque la rejilla tiene una opción dgEditing en su propiedad
Options, y cuando la opción está presente, la rejilla coloca un cuadro de edición sobre la
celda en que se intenta esta acción. Siempre. No nos vale apagar definitivamente
dgEditing, porque entonces no podríamos editar las otras columnas. Pero la solución es tan
sencilla que me da vergüenza contarla: debemos interceptar el evento OnColEnter de la re-
jilla como muestro a continuación:
Es casi seguro también que tendrá que hacer lo mismo en el evento OnEnter de la rejilla,
por lo que el método anterior posiblemente será compartido por ambos eventos.
133
Cargando datos en un control
de listas
En los últimos tiempos, cuando tengo que mostrar un conjunto de filas al usuario y no es
necesario que este conjunto sea editable, en vez de utilizar la archiconocida rejilla
(TDBGrid) echo mano de un control de listas TListView. No sólo por el cambio estético, que
puede ser discutible, sino también por la posibilidad de mostrar casillas de verificación
(checkboxes) asociadas a cada fila. Basta con asignar True a la propiedad Checkboxes del
control para que el usuario pueda seleccionar un conjunto de filas con la mayor tranquilidad
del mundo. Si es usted de los que no se resigna a perder las líneas horizontales y vertica-
les de las rejillas de Delphi, puede activar la propiedad GridLines y sus sueños se harán
realidad. Por último, puede incluso intercambiar la posición de las columnas si activa
FullDrag. Sólo por mencionar algunas posibilidades evidentes.
Claro, Delphi no trae entre sus componentes una TDBListView; hay que cargar los datos de
una tabla o consulta directamente en la propiedad Items del control. No es una tarea muy
complicada, pues en el número de Junio de Mundo Delphi8 muestro un ejemplo de cómo
ayudarnos con un frame para simular una rejilla de datos...
... lo malo es que si cargamos chapuceramente los datos, la operación se ralentizará inu-
tilmente, y el efecto visual será bastante desagradable de observar. Sin embargo, un par de
trucos sencillos nos evitarán este inconveniente:
La propiedad AllocBy informa al control sobre el número de elementos que se van a alma-
cenar en memoria. Si no ajustamos este número de antemano, cada vez que añadimos un
nuevo elemento, el control tiene que pedir memoria dinámica, lo que disminuye la velocidad
y aumenta considerablemente el riesgo de fragmentación.
8
www.mundoDelphi.com
135
¿Dónde estoy?
¿Ha necesitado alguna vez conocer en qué directorio se está ejecutando su aplicación? Es
muy fácil averiguarlo, sobre todo si la aplicación es un fichero ejecutable. Se trata de un
truco que se remonta a los tiempos del venerable Turbo Pascal: hay que preguntar por el
parámetro cero de la línea de comandos.
Suponiendo que lo que realmente deseo es el directorio desde donde se ejecuta el fichero,
he utilizado la función ExtractFilePath, que se encuentra en la unidad SysUtils. Esta función
deja la última barra de directorio al final del resultado.
Claro, los tiempos cambian, y las técnicas mejoran. En una aplicación escrita en Delphi
para la interfaz gráfica de Windows es preferible utilizar la propiedad ExeName del objeto
global Application:
Ahora bien, ¿qué pasa si lo que estamos escribiendo es una DLL, y queremos saber en
qué directorio la han instalado, para saber desde dónde se está ejecutando? Está claro que
ya no vale utilizar la lista de parámetros, y que el objeto global Application ya no nos sirve.
En una aplicación CGI/ISAPI para Internet, incluso, el objeto Application no tiene nada que
ver con el tradicionalmente definido en la unidad Forms.
Por suerte existe una función del API de Windows, GetModuleFileName, que nos va a sa-
car las castañas del fuego. ¿Qué tal si primero muestro una versión incorrecta del código
necesario?
Se trata de un error muy fácil de cometer, aunque difícil de detectar. ¿Se ha dado cuenta
de que he pasado un cero en el primer parámetro de la función? Es que ese parámetro es-
pera la instancia: un valor numérico que identifica unívocamente al programa...
...y ese es el problema. El valor cero corresponde al programa activo. Si estamos desarro-
llando un ejecutable, cero es perfectamente adecuado. Pero si la llamada se efectúa desde
una DLL nuestra, ¡el directorio devuelto es el del ejecutable que llama a la DLL! Pero basta
con utilizar la poco conocida variable global HInstance para que todo vuelva a la normali-
dad:
// Versión correcta
function TForm1.DirectorioAplicacion: string;
var
Buffer: array [0..255] of Char;
137
138 Calling Dr. Marteens
begin
GetModuleFileName(HInstance, Buffer, SizeOf(Buffer));
Result := ExtractFilePath(StrPas(Buffer));
end;
El alcance de las etiquetas
Este truco trata sobre las seudo etiquetas que utiliza el WebBroker de Delphi durante la
generación de código HTML por parte de aplicaciones CGI/ISAPI. Es sabido que los com-
ponentes productores de HTML pueden utilizar una "plantilla" de texto escrita en este len-
guaje de descripción como base del código que generan. A la plantilla HTML se le añaden
etiquetas especiales que no son utilizadas por los navegadores, con el fin de ser sustituidas
durante la ejecución de la aplicación por los componentes mencionados.
Por ejemplo, si queremos mostrar una página con el importe total de los artículos en la
cesta de la compra de algún ciudadano, es probable que utilicemos una plantilla similar a
ésta:
<html>
<body>
El importe total es <#IMPORTE>.
</body>
</html>
Como el carácter que sigue al paréntesis angular de apertura es una almohadilla (#), Delphi
reconoce que se trata de una especie de macro que tiene que expandir.
Nuestro primer reflejo es pensar que Delphi no permitirá tampoco situar una etiqueta
"transparente" dentro de una etiqueta regular de HTML, ¿no? Pues si piensa así, está
completamente equivocado. El siguiente ejemplo muestra una plantilla correcta:
La etiqueta <#DIRECTORIO> no sólo aparece dentro de una etiqueta de enlace, sino que
además se expande dentro de las dobles comillas del valor del parámetro HREF. Delphi lo-
caliza sus etiquetas especiales analizando secuencialmente el texto de la plantilla. Una vez
que ha encontrado una secuencia "<#" busca el siguiente paréntesis angular de cierre. Esto
quiere decir que realmente no importa dónde se encuentra la etiqueta, aunque no se per-
miten etiquetas especiales (de Delphi) anidadas.
139
KOANS
Desde lo alto de un poste de cien metros, ¿cómo dar un paso más?
¿Cuál es el sonido de una sola mano que aplaude?
Sin palabras, sin la falta de palabras, ¿me dirás la verdad?
Cuando uno son dos y
dos son uno...
Coloque un componente TMemo en un formulario; agregue un botón e intercepte el evento
OnClick de este último:
Estoy asignando a una variable temporal F el valor de la propiedad Font del memo. Como sa-
bemos, las variables de objetos representan realmente punteros a dichos objetos. Así que la
asignación que se realiza sobre el estilo del tipo de letra está afectando a su vez al tipo de letra
del memo, pues F y Memo1.Font apuntan al mismo objeto. Dos son uno...
Este código es el correcto. Pero, ¿qué está pasando aquí? Cuando asignamos el Font del
memo a la propiedad del diálogo, ¿no se está perdiendo el objeto de tipo de letra al cual apun-
taba el diálogo? Y ahora, ¿por qué necesitamos reasignar el tipo de letra del diálogo al memo?
¿No habíamos quedado en que uno eran dos...?
En este caso, la solución es muy sencilla: hay que darse cuenta de que Memo.Font (y también
FontDialog1.Font) no es una variable de clase, sino una propiedad de tipo clase ... y que las
propiedades, aunque intentan engañarnos haciéndose pasar por variables, realmente no lo
son. La propiedad Font se implementa generalmente del siguiente modo:
type
TLoQueSea = class(TOtraCosa)
// ...
private
FFont: TFont;
procedure SetFont(Value: TFont);
published
property Font: TFont read FFont write SetFont;
end;
El método Assign es introducido por la clase TPersistent (ancestro de TComponent) y debe ser
redefinido por aquellas clases que deseen implementar la asignación de objetos "propiedad por
143
144 Calling Dr. Marteens
propiedad". En la jerga informática, a este tipo de asignación se le llama asignación por valor,
en contraste con la asignación por referencia, que es la implementada por el operador de asig-
nación de Delphi para las variables de tipo clase.
De modo que en el primer ejemplo realmente estabamos asignando el puntero del objeto origi-
nal a la variable F, precisamente por tratarse de una variable. En el segundo ejemplo, la asig-
nación se realizaba a una propiedad de tipo Font, y Delphi implementa las asignaciones a este
tipo de propiedades como asignaciones por valor.
¿Hasta que punto podemos fiarnos de que Delphi implemente para todas sus propiedades de
tipo clase asignaciones por valor? Se trata de una recomendación metodológica, que Delphi
(hasta donde sé) cumple escrupulosamente. Cuando usted implemente una propiedad de tipo
clase debe tener cuidado y ajustarse a este comportamiento. Por ejemplo, es fácil perder de
vista el hecho de que la variable interna FFont debe ser construida a la vez que la clase que la
contiene (y destruida en Destroy):
También es muy fácil olvidar el requisito de que la clase para la cual se define la propiedad
debe contar con una implementación correcta del método Assign.
¿Hay vida después de la muerte?
Con frecuencia, utilizo el evento OnClose de los formularios para hacer que estos se destruyan
automáticamente al cerrarse:
// ...
TDialogo.Create(nil).ShowModal;
// ...
// ...
with TOtroDialogo.Create(nil) do
try
ShowModal;
finally
Free;
end;
// ...
Sin embargo, un buen día descubrí por accidente que si en la clase TOtroDialogo definía un
evento OnClose similar al de TDialogo, la última secuencia de creación/destrucción seguía
funcionando estupendamente bien. Esto me planteó una serie de problemas teológicos: ¿acaso
el truco con caFree no destruía realmente el objeto? ¿Cómo entonces, en caso contrario, la
llamada a Free no reventaba, si al parece tratábamos de destruir un objeto dos veces? ¿Existía
vida para los objetos después de la muerte? Hice pruebas exhaustivas y comprobé que:
1. Los dos métodos (el uso de caFree y el uso explícito de Free), efectivamente, funcio-
nan bien por separado.
2. Al mezclar las dos técnicas, el algoritmo sigue funcionando bien, y el objeto se destruye
una sola vez.
¿Cómo es posible?
Me costó trabajo comprender cómo era posible que la llamada a Free no diera problemas. Mi
primera reacción fue pensar que alguna de las variantes presentadas en el problema tenía
problemas de perdida de memoria (memory leaks). En consecuencia, dediqué casi una hora a
comprobar esto. Y no: no encontré problema alguno. Entonces, desde el fondo de mi incons-
ciente comenzó a aflorar la solución: un método de los formularios que había visto hacía mucho
tiempo, me había llamado la atención y lo había archivado en el sótano. Fui al código fuente a
confirmarlo y, efectivamente, la clave estaba en el injustamente olvidado método Release.
145
146 Calling Dr. Marteens
¿Qué hace el famoso método Release? Poca cosa: deposita en la cola de mensajes de la apli-
cación un mensaje definido por Delphi, denominado CM_RELEASE. Lo importante es que el
mensaje se envía con la función PostMessage del API de Windows, no con la más común
SendMessage. Esto quiere decir que el mensaje no se trata inmediatamente por el procedi-
miento de ventana (windows procedure) del formulario. El tratamiento ocurre asíncronamente,
en el próximo paso del bucle de mensajes.
¿Y qué hace la ventana cuando recibe un CM_RELEASE? Bueno, para decirlo con suavidad,
se suicida. Es decir, llama a Free. ¿Por qué Delphi necesita un método tan sádico y perverso
para liquidar a un pobre formulario? Suponga, con un poco de imaginación, que el siguiente
diagrama representa al bucle de mensajes, y al recorrido por las rutinas del programa que se
produce durante el procesamiento de determinado mensaje. Suponga también que en el nodo
señalado, el formulario que está tratando el mensaje decide poner fin a sus días, llamando a
Release. Bueno, colega, no es tan sencillo abandonar este mundo. Puede que todavía tengas
que hacer algo algunos nodos más abajo, pero lo más frecuente es que tengas que realizar
trámites burocráticos durante el camino de regreso al bucle de mensajes.
Está claro entonces que cuando asignamos caFree en el parámetro del evento OnClose, Del-
phi lo que hace es llamar a Release, en vez de Free, que es lo que casi todos esperamos.
Normalmente, la destrucción de formulario se efectúa en el próximo paso del bucle de mensa-
jes. Pero si nos adelantamos y destruimos la ventana antes de que llegue su momento, no
pasa nada. Sencillamente, el comando CM_RELEASE (la carta-bomba) nunca alcanza su des-
tino. Y esto es normal en Windows.
Todos los animales son iguales,
pero algunos son más iguales
que otros
Vaya título para un koan ... pero de algún modo contiene la respuesta. Intentaré explicarme, y
esta vez recurriré a su imaginación gráfica para plantearle el aparente problema. ¿Se ha
puesto a pensar alguna vez en la cantidad de componentes que publican propiedades de tipo
TStrings? El cuadro de listas (TListBox) tiene Items, al igual que el TComboBox; el TMemo y el
TRichEdit utilizan Lines...
- ¡Ah! Este Ian va de listo - dice una voz crítica dentro de mi caja craneal - y quiere hacerse
el iluminado contando la diferencia entre TStrings como clase abstracta, y TStringList como
clase concreta, que implementa todos los métodos abstractos de TStrings.
Pues no vamos por ahí: es posible que hablemos de clases abstractas y concretas, pero tam-
bién de cosas más interesantes. El problema que he detectado en muchos programadores de
Delphi, es que piensan en un TListBox (pongamos por caso) mediante una imagen similar a la
siguiente:
Es decir: el TListBox es un objeto independiente, que tiene una propiedad Items de tipo
TStrings. Como el tipo TStrings es una clase abstracta, "realmente" Delphi crea un TStringList y
lo asocia a Items; a fin de cuentas, ¡esto es polimorfismo! Cuando uno inserta en el TStringList
un nuevo elemento, de alguna forma el TListBox se entera y retoca el control de Windows aso-
ciado, para mostrar el nuevo elemento...
Pudiera ser pero ... ¿no es ésta una implementación muy ineficiente? Pues el TListBox ges-
tiona, como hemos dicho, un control nativo de Windows, y este control mantiene en memoria
privada una lista de los elementos a mostrar. De ser cierto la explicación anterior, ¡estaríamos
duplicando toda esta estructura interna dentro de Delphi (el supuesto TStringList sería una
copia fiel del contenido del control)! Y Delphi tendría que encargarse de mantener sincroniza-
das ambas copias: cada operación en cada una de las estructuras debe repetirse en la otra.
Cuando uno lo piensa, se da cuenta de que la solución es evidente y trivial, pero si no nos de-
tenemos a meditar, podemos formarnos imágenes mentales equivocadas como la del plantea-
miento. La solución, claro está, es que TListBox no almacena sus datos en un TStringList, sino
en una clase ad-hoc llamada TListBoxStrings, que por supuesto también desciende de
TStrings. Algo parecido sucede con otros componentes que "atacan" a controles nativos y que
tienen propiedades TStrings, como TMemo, TRichEdit, etc.
147
148 Calling Dr. Marteens
La clase TListBoxStrings, por ejemplo, no utiliza memoria de la aplicación para sus cadenas,
sino que gestiona directamente las cadenas situadas dentro del cuadro de listas asociado.
Muestro a continuación un fragmento de la declaración de esta clase:
TListBoxStrings = class(TStrings)
private
ListBox: TCustomListBox;
// ...
end;
Veamos, por ejemplo, como TListBoxStrings redefine e implementa el método Add, que añade
una nueva cadena al final de la lista:
Como vemos, la lista se añade a una estructura interna del control mediante un mensaje de
Windows, LB_ADDSTRING. Para buscar una cadena, por otra parte, se utiliza el siguiente
método:
Y así sucesivamente. Los Items de un cuadro de lista, las Lines de un memo, la SQL de una
consulta: sí, todos son TStrings, y comparten muchos métodos, pero la implementación de los
mismos es diferente en cada caso. Lo maravilloso es cómo todos estos animales de especies
diferentes pueden comunicarse entre sí, gracias al polimorfismo ("da igual gato blanco o gato
negro, con tal de que cace ratones"). Cuando realizamos una asignación como la siguiente:
Memo1.Lines := ListBox1.Items;
estamos ejecutando en realidad el método Assign que está definido en la clase base TStrings,
y en cuyo corazón encontraremos la siguiente instrucción (unidad Classes):
for I := 0 to Strings.Count - 1 do
AddObject(Strings[I], Strings.Objects[I]);
Ese AddObject se ejecuta sobre las líneas del memo: será responsabilidad del TMemoStrings
saber cómo almacenar una cadena. Ese otro valor Strings[I] se refiere al ListBox1: es respon-
sabilidad del TListBoxStrings el suministrarnos la i-ésima cadena que contiene, y no nos im-
porta a nosotros cómo las haya almacenado. Ventajas de la Programación Orientada a
Objetos.
Armageddon: el Día del
Juicio Final
¿En qué criterio me baso para decidir si un artículo encaja dentro de los trucos o debe expli-
carse como un koan? Para que se convierta en un koan es necesario que la técnica o situación
que trata me haya dejado, cuando menos, perplejo. Da lo mismo el nivel de dificultad. Y esto
fue lo que me pasó con la siguiente "característica" de C++ Builder.
Cuando los programadores de Delphi necesitan ejecutar algún código al comienzo o final de un
programa, y quieren encapsular este comportamiento dentro de una unit, pueden recurrir a las
cláusulas initialization y finalization de la unidad:
unit UnitXX;
interface
// ...
implementation
// ...
initialization
// ...
// Aquí puede escribir una lista de instrucciones
finalization
// ...
// No se puede utilizar finalization sin initialization
end.
En C++ no existe una construcción sintáctica similar a la unit, por lo que tampoco existe un
equivalente mimético de las cláusulas anteriores. Pero el programador puede recurrir a objetos
globales: la definición de C++ deja bien claro que el constructor de estos objetos se debe eje-
cutar al inicio del programa, y el destructor se debe activar automáticamente al terminar la apli-
cación. ¿Seguro?
class TObjetoExhibicionista
{
protected:
AnsiString fName;
public:
// Constructor inline
TObjetoExhibicionista(const AnsiString aName) :
fName(aName)
{
ShowMessage("¡Ha nacido una estrella!");
}
// Un sencillo y amargo adiós a la vida (también inline)
~TObjetoExhibicionista()
{
ShowMessage("¡Adiós, mundo cruel!\nFirmado: " + fName);
}
};
Ahora, en el mismo fichero, cree una variable global con la nueva clase:
TObjetoExhibicionista pepe("Pepe");
Se supone Pepe debe saludar al inicio del programa, y expresar su desencanto cuando se
termine el programa y sea destruido (el Día del Juicio Final).
Compile y ejecute entonces la aplicación ... y disfrute de esos breves momentos de asombro,
en los que pensará que algo anda "muy mal"...
149
150 Calling Dr. Marteens
TObjetoExhibicionista::~TObjetoExhibicionista()
{
char buffer[255];
Lo que Delphi ofrece es un "sustituto": un formulario o ventana "normal", no modal, pero que
ejecuta un ciclo modal de mensajes. El método que se encarga del ciclo de mensajes es, natu-
ralmente, el conocidísimo ShowModal. Y la implementación de ShowModal es esquemática-
mente similar a la siguiente:
int TCustomForm::ShowModal()
{
// ...
do {
// ... leer mensaje ...
// ... procesar mensaje ...
} while (! Application->FTerminate);
// ...
}
It's not wise to blindly trust in so called "common sense". A question has divided for long time the
community of Delphi programmers: do we have to use tables or queries, when accessing SQL database
servers? Opinions are divided, and I'm afraid that the latest polls would give queries as the current
winner. I will try to show here that, when it comes to navigation, queries are the loser's choice.
Big-endians and little-endians cannot both be right, and even they both can be wrong. What's the truth
behind the iron curtain of the BDE implementation for tables and queries?
Let me be more accurate: the real enemy for client/server developers is not the TTable component, but
TDBGrid. Traditionally, C/S applications have been provided with very simple UI. Take a look at an
ATM: do you see grids or navigators? I must admit that this is a very ugly interface, but it is the interface
needed for a kind of applications that impose strict demands on network traffic.
Grids give the users the illusion that they are moving on a local copy of the whole data file, though what
they really have is a narrow and moveable window of about 20-30 rows at a time. A very naive
implementation of browsing would be fetching all the records from the source. Indeed, I have heard some
programmers say that tables behaves this way; I'll prove later that they are wrong.
So, unrestricted navigation is very inefficient, specially when large data sets are involved. Can you
always avoid this operation in your applications? If so, you don't need to read the rest of this paper. But if
you are forced to use browsing on a large result set for some reason, it is better for you to learn the most
efficient way to implement navigation with Delphi tools.
SQL Monitor output will be the touchstone that will confirm the veracity of our assertions. As we all
know, this tool can be executed from inside the IDE, or as an independent program. We will execute it
using the Database|SQL Monitor menu command. SQL Monitor gives us a trace of all SQL statements
sent by the BDE to the database server. Moreover, it even shows the native interface calls to the client
library that directs these statements to the server. We can also watch data sent to and received from the
database.
We will need a very simple application in order to test the basic navigation facilities offered by Delphi.
This application will connect to a table located in a database server, and it will browse this table in a
record-by-record fashion. We won't use a database grid to avoid obscuring the SQL Monitor output with
all the commands needed to fill a whole page of records. I also suggest you to use InterBase to play the
role of the database server, for two major reasons. First of all, it is the least common denominator for all
Delphi & C++ Builder programmers, including those using the Professional version. And the second one
is that InterBase generates a cleaner output with SQL Monitor. A test performed with Oracle, by instance,
would have to adjust the ROWSET SIZE parameter from the BDE configuration. If we don't, we will see
Oracle sending 20 rows every time we request a single record. Nevertheless, I have experimented with
Oracle and MS SQL Server, and the guidelines of the BDE implementation are very similar. Trust me.
So, let's start a new application, and drop a TQuery component inside the main form. Connect its
DatabaseName property to some SQL alias, say, IBLOCAL. Then type the following statement inside the
SQL property:
Now, drop a TDataSource component, and link its DataSet property to the query. Add also a
DBNavigator, and set its DataSource. Double click the query, and add all fields to the application. Select
them all, drag these fields and drop them inside our form, in order to create DBEdit components
automatically. Finally, set Active to True for the query.
Let's go to the Database menu, from the IDE, and execute the SQL Monitor command. Then run the
application and see what's happening below the quiet surface of the BDE.
153
154 Calling Dr. Marteens
If you skip all the noise representing the way BDE instructs the InterBase API to communicate with the
server, you'll be left basically with those entries starting with SQL Prepare and SQL Execute. And, in this
case, you will only see an initial execution of the same statement we included in our query:
SQL preparation is slang for "query compiling". If a query contains parameters, you could compile it
once, and execute it several times with different parameters. Query execution, on the other hand, opens a
database cursor on the server side, representing the result set of the select statement. I won't discuss
whether this execution produces instantly the whole result set, or whether this result set is generated on
demand; I suspect that generation on demand is the policy adopted by most SQL servers.
Next, you'll find a very special instruction: Fetch. What is important to us now, is that BDE can only
fetch the next record from the result set. It cannot fetch backwards, or skip intermediate records. This is
true even for database servers that support server-side bi-directional cursors. So, you may move forward
with the navigator's Next button, and you'll see the corresponding Fetch lines on SQL Monitor form. At
this point, if you move the active row a few records backwards, you won't see any output. Sure, those
records are stored in a client-located cache, so the BDE does not need to fetch them again. Bi-directional
client-side cursors are currently implemented by the BDE, despite our server's capabilities.
Now, let's be prepared for the bad news: just click the Last button on the navigator, and see what's
happening. In order to retrieve the last record from the query, the BDE needs to fetch every intermediate
record! You will see as many Fetch instructions as records have the base table...
Let me propose a metaphor: a little man is on the right side of a river, and wants to reach the other side.
He does it by dropping stones as he advances across the stream. Once he arrives to the opposite shore, a
bridge has been built. Of course, he cannot jump straight to the middle of these troubled waters, as long as
he tries to keep himself dry. He will need some supernatural force to succeed.
In order to test the table component, remove Query1 from the form and add a table. Assign again
IBLOCAL in DatabaseName, and set TableName to Employee. Then, change its Active property to True,
and redirect the DataSet property from DataSource1 to point to Table1. That's all. Now, clean the SQL
Monitor log and execute the application.
What does all this madness mean? It is just that the table component needs to know its field and index
definitions. The first statement verifies the existence of the table on the server:
Of course, these instructions take a time to execute and increment network traffic. Imagine a typical
business application, with more than 100 tables on a data module. And think about what happens when
the module is loaded, and tries to open the functional set of tables needed to start the application...
However, this is a problem with a very easy solution. Just turn on the ENABLE SCHEMA CACHE
parameter, on your SQL alias definition. When this parameter is on, the information retrieved at the table
prologue is stored in local files the first time the application executes. The master file's name is
scache.ini, and BDE creates a .scf file for each table, on the application directory or on the path specified
with the SCHEMA CACHE DIR parameter. When the application is run again, BDE extracts table
definitions from these files, instead of querying the server. I will not discuss here all the implications of
enabling the schema cache, because it's a well documented feature.
Finally, you'll see the expected statement that opens a cursor on your table:
But, wait a minute ... we did not ask any index to the table component. Why BDE has added this order
by clause, on the primary key fields, to the cursor?
Here is the reason. You can have a big surprise when you click on the Last button from the navigator.
Suddenly, the BDE closes its initial select statement, and prepares and executes the following one:
The first record fetched with the aid of this cursor will be the last record from the table, isn't it? And if
you go backwards, the BDE continues fetching records from this cursor. I can't help thinking about
Michael Jackson doing his moon-walk.
One more hint. As you must know, InterBase support both ascending and descending indexes. The dark
side of this flexibility is that InterBase cannot optimise the previous SQL statement if it only counts with
an ascending index for Emp_No. You'll have to create a descending index on the primary key (or the
ordering column) if you want backwards navigation on an InterBase table. On the contrary, Oracle
indexes can be scanned in both directions.
But, what if the angel wants to have fun, and make us land on the middle of the river? Well, first of all,
could angels do this sort of things? Yes, they can. This is just what happens when we perform a Locate
operation on a table component. Drop an Edit control inside the form, add a Button, and handle its
OnClick event this way:
What the BDE does is to execute the following statement, without closing the active cursor:
The :1 token stands for a parameter, which is initialised with the value passed in the second parameter of
Locate. So, locating a record given its primary key is a matter of fetching just that record, in case of
success. But, what if the search is performed on a different column?
156 Calling Dr. Marteens
This time, things are a little more complicated, because Delphi plays a role in this operation, and you'll
see Delphi and BDE statements mixed on the SQL Monitor window. First, an exact search is attempted.
After all, if your surname begins with 'Mac', maybe it is just Mac!
If this search succeeds, you already have the record. However, you can see how BDE apparently issues
another Locate, this time on the primary key. What is really happening is that the first statement is
launched by Delphi; take a look at the source code of the DBTables unit, and watch how the
GetLookupCursor method is used by TTable. The second statement is executed once you have the
primary key for the desired record, and its goal is to reposition the BDE cursor.
But if your surname starts with 'Mac' and it's not 'Mac', then it must come after the prefix in dictionary
order. So, this is the statement that will appear on the log:
If this statement yields no record, the search fails. But if it does, the BDE cursor is repositioned again by
means of another Locate on the primary key, with the value found. So, we needed a maximum of two
fetches in order to get the record.
I did not tell how queries react to Locate, but I can do it now: they advanced their cursor, painfully
fetching every intermediate record until they find the key or have reached the end of the cursor in vain.
But you must be very careful about what options are included in the third Locate parameter. When a case
insensitive search is specified using the loCaseInsensitive option, the BDE/Delphi dream team fails, at
least with InterBase, Oracle & MS SQL Server. Why? It's because CreateLookupCursor, the
TBDEDataSet method we mentioned before, looks for a case insensitive index in order to ensure that a
search query could take advantage of it. But neither InterBase nor Oracle do support this kind of indexes.
And MS SQL Server does it, but depending on a decision taken when installing the software, so Delphi
cannot make a decision at runtime.
Filters
In most cases, filters are implemented by merging the filter expression in the where clause of the select
statement. If you are planning to use a query based on a single table with a simple where condition, you
can also consider using a table with a filter expression. You must also avoid case insensitive comparisons
(option foCaseInsensitive, property FilterOptions). Partial matches, unfortunately, are not implemented
well. We can set the filter expression to something like this:
Last_Name = 'Mac*'
In this case, filter expressions are evaluated at the client side. But if you force your neurones a little
harder, you'll find out this equivalent expression:
Another interesting technique is used when FindFirst, FindNext, FindPrior and FindLast methods are
applied and a filter expression is specified (Filter) but not active (Filtered). Delphi implements this by
launching a secondary query (yes, CreateLookupCursor again). The four previous operations navigate
through the lookup cursor, and every time a record is found, Locate is used internally to synchronise the
table.
Religion Wars in Lilliput 157
• I'm not recommending indiscriminate browsing for client/server applications. If you can avoid
grids, do it, as they consume too much bandwidth from your network.
But if you cannot exclude free navigation from your application, then follow these recommendations:
1. Do not ever use queries for browsing over medium to large result sets. The only justification
would be the need to sort on some columns in descending order, something that BDE tables
currently do not support. The definition for a "medium-sized result set" varies according to the
database server and network configuration. If I had to say a number, I'd say about 500 rows.
2. Live queries without cached updates have no advantages over tables, as long as they execute the
same prologue at its activation.
3. Use queries for small result sets that don't require insertions. I have had problems when adding
rows to InterBase queries, forcing me to close and reopen the query after insertions. The extreme
situation are singleton selects: queries are best suited for these purposes.
4. Table's implementation is far from perfection. Turn on schema caching. Avoid always case
insensitive searches for servers that have no case insensitive indexes (including MS SQL
Server). Be aware that some VCL components, like TDBLookupComboBox, may trigger these
searches inside their implementation.
I'm amazed on the fact that Borland/Inprise has never give a hint on these very important topics. I have
started to think on a plot devised by some political forces and ...