Está en la página 1de 157

CALLING DR.

MARTEENS
1998-2000
TABLA DE CONTENIDO
TRUCOS ___________________________________________________________________ 5

REFRESCAR LA FILA ACTIVA ___________________________________________________7


OPTIMIZAR LA OPERACIÓN LAST ________________________________________________9
EXTRAYENDO EL SQLCODE__________________________________________________11
OBTENIENDO LA LISTA DE USUARIOS DE PARADOX ________________________________13
PROBLEMAS COMUNES AL INSTALAR UN SERVIDOR SQL ___________________________15
INDICES ÚNICOS EN DBASE 7 __________________________________________________17
CREACIÓN AUTOMÁTICA DE COMPONENTES _____________________________________19
COLECCIONES Y PERSISTENCIA ________________________________________________23
ESCRIBIR EN LA BARRA DE TÍTULO _____________________________________________27
EL USO DE CONST CON PARÁMETROS STRING _____________________________________29
BITMAPS, VENTANAS MDI Y SUBCLASSING _______________________________________31
SECUENCIAS EN ORACLE _____________________________________________________35
COSAS QUE NUNCA DEBE HACER CON C++ BUILDER _______________________________37
AUMENTANDO LA SEGURIDAD DE LOCATE _______________________________________41
INTERBASE Y LA SEMÁNTICA DE UPDATE_______________________________________43
LA VCL Y EL AÑO 2000 ______________________________________________________45
CAMPOS LOOKUP MÁS RÁPIDOS ________________________________________________47
AÑADIENDO ESTABILIDAD A PARADOX __________________________________________49
MODIFICANDO PARÁMETROS DEL BDE _________________________________________51
EL IDENTIFICADOR DE LA TARJETA DE RED ______________________________________53
PRIMEROS PASOS CON LAS OPEN TOOLS_________________________________________55
AÑADIENDO PROPIEDADES A UN FORMULARIO ____________________________________61
LIMITANDO EL NÚMERO DE REGISTROS _________________________________________65
ESCOGIENDO UN SERVIDOR ___________________________________________________67
INTERBASE Y EL DISCO D: ____________________________________________________71
CHECK BOXES EN REJILLAS DE DATOS __________________________________________73
DECISION CUBE Y LAS FECHAS _________________________________________________75
VISTAS PRELIMINARES A LA MEDIDA ____________________________________________77
ETIQUETAS Y DIRECTORIOS ___________________________________________________79
EL OPERADOR DELETE Y EL MÉTODO FREE ______________________________________81
TABLAS EN MEMORIA ________________________________________________________83
EL MES SIGUIENTE Y EL ANTERIOR _____________________________________________87
EL BUEN USO DE LAS IDENTIDADES _____________________________________________89
FECHAS EN SQL SERVER 7____________________________________________________91
CÓMO ELIMINAR UN GENERADOR ______________________________________________93
MÁS TRUCOS CON GENERADORES ______________________________________________95
LOS TIPOS NUMÉRICOS DE INTERBASE __________________________________________97
VISTAS Y TRIGGERS__________________________________________________________99
EL BIT Y LA ESPADA ________________________________________________________105
AHORRANDO ESPACIOS CON LAS ETIQUETAS ____________________________________109
ACCIÓN INMEDIATA ________________________________________________________111
MÁS POTENTE QUE COPIAR Y PEGAR ___________________________________________113
¡LA DIRECCIÓN, IDIOTA, LA DIRECCIÓN...! ______________________________________115
LÍNEAS ___________________________________________________________________117
EL MISTERIO DE LA CABECERA PERDIDA_______________________________________123
NOMBRES DE DOMINIO FLEXIBLES ____________________________________________127
EL PRINCIPIO DE INCERTIDUMBRE DE HEISENBERG ______________________________129

3
4 Calling Dr. Marteens

COLUMNAS DE SÓLO LECTURA________________________________________________133


CARGANDO DATOS EN UN CONTROL DE LISTAS __________________________________135
¿DÓNDE ESTOY? ___________________________________________________________137
EL ALCANCE DE LAS ETIQUETAS ______________________________________________139

KOANS __________________________________________________________________ 141

CUANDO UNO SON DOS Y DOS SON UNO...________________________________________143


¿HAY VIDA DESPUÉS DE LA MUERTE? __________________________________________145
TODOS LOS ANIMALES SON IGUALES, PERO ALGUNOS SON MÁS IGUALES QUE OTROS ___147
ARMAGEDDON: EL DÍA DEL JUICIO FINAL ______________________________________149

ARTICULOS _____________________________________________________________ 151

RELIGION WARS IN LILLIPUT ____________________________________________153


TRUCOS
El día que la mataron, Lola estaba de suerte:
De diez tiros que le dieron, sólo dos eran de muerte...
Refrescar la fila activa

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?

• La tabla debe tener una clave primaria.


• La clave primaria debe ser, mientras más simple, mejor. Si es un entero, perfecto. Y si
es necesario, utilice una clave artificial.
• Asigne la clave primaria en el cliente, no en el servidor, para evitar los errores: "Record
or key deleted".

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:

select * from Tabla order by ClavePrimaria desc

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.

MORALEJA: Si es imprescindible trabajar con una de estas súper-tablas en una rejilla,


y no quiere eternizarse esperando por el último registro, añada un índice descendente
por la columna de la clave primaria. Por supuesto, esto va a ralentizar las operaciones
de actualización ... pero no se puede tener todo en esta vida.

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:

function GetSqlCode(E: EDatabaseError): Integer;


var
I: Integer;
begin
Result := -1;
if E is EDBEngineError then
with EDBEngineError(E) do
for I := 0 to ErrorCount - 1 do
if Errors[I].NativeError <> 0 then
begin
Result := Errors[I].NativeError;
Exit;
end;
end;

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:

procedure GetUsers(UserList: TStrings);


var
TmpCursor: hDbiCur;
rslt: dbiResult;
UsrDesc: USERDesc;
begin
UserList.Clear;
Check(DbiOpenUserList(TmpCursor));
try
repeat
Rslt:= DbiGetNextRecord(TmpCursor, dbiNOLOCK, @UsrDesc, nil);
if Rslt <> DBIERR_EOF then
UserList.Add(UsrDesc.szUserName);
until Rslt <> DBIERR_NONE;
finally
Check(DbiCloseCursor(TmpCursor));
end;
end;

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é.

La limitación principal de mi mini-red es el servidor: un viejo Hewlett-Packard a 75 MHz. Le


puse 64 MB de RAM (más no aguantaba la placa) y un disco duro UltraDMA de 2GB. Por su-
puesto, estoy desperdiciando la capacidad UltraDMA, pero ya no venden de otro tipo (y este
era muy barato). Y lo principal es que la velocidad de transferencia de datos es de todos modos
muy superior a la del disco original de 512 MB que traía.

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.

Mi primera pregunta fue:

• ¿Se podían ver ambas máquinas mediante un ping?

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:

1. Identifique cuáles propiedades de clase son propiedad del componente y cuáles


no.
Cuando me refiero a propiedades de clase, estoy hablando de propiedades cuyo tipo
es una clase, ya sea derivada de TComponent o no. Como sabemos, estos tipos de
propiedades almacenan punteros a otros objetos.

¿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:

property Items: TStrings read ... write ...

Cuando el cuadro de lista se construye, el cuadro es responsable de crear un objeto de


una clase derivada de TStrings (en este caso, un TListBoxStrings). Cuando el TListBox
se destruye, debe también liberar el objeto creado. Pero esto es también muy impor-
tante: cuando asignamos a la propiedad Items un objeto TStrings no estamos sencilla-
mente asignando punteros (que es lo que Delphi haría por omisión), sino que debemos
realizar una copia del objeto. Precisamente, nuestro primer koan trata acerca de esta
característica.

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:

property FocusControl: TWinControl read ... write ...

El constructor debe dejar simplemente a nil el valor de esta propiedad, y el destructor


no debe hacer nada especial al respecto. Una asignación a este tipo de propiedades se
implementa como una simple asignación de punteros. Existe, sin embargo, un pro-
blema: el objeto independiente apuntado por FocusControl puede "morir" antes que el
TWinControl que hace referencia a él. Entonces la propiedad FocusControl contendrá
un puntero inválido...

Para resolver este problema, el componente debe redefinir (override) el método


Notification, que hereda de la clase TComponent:

procedure TLabel.Notification(AComponent: TComponent;


Operation: TOperation);
begin
inherited Notification(AComponent, Operation);
if (Operation = opRemove) and (AComponent = FFocusControl) then
FFocusControl := nil;
end;

19
20 Calling Dr. Marteens

El método Notification existe desde Delphi 1. Cuando un componente muere, el pro-


pietario (owner) del componente recorre la lista de los restantes componentes y llama a
Notification para cada uno de ellos. Casi siempre, el propietario del componente es el
formulario donde se encuentra. Ahora bien, ¿qué pasaría si el TWinControl está en un
formulario y el TLabel en otro, como puede suceder a partir de Delphi 2? Para garanti-
zar que el aviso de destrucción se envíe incluso cuando los propietarios son diferentes,
debemos tomar precauciones especiales en el método de escritura de la propiedad
FocusControl:

procedure TLabel.SetFocusControl(Value: TWinControl);


begin
if Value <> FFocusControl then
begin
if Value <> nil then Value.FreeNotification(Self);
FFocusControl := Value;
end;
end;

El método FreeNotification añade la referencia a nuestro componente dentro de una


lista privada mantenida por el componente independiente al cual apuntamos (el
TWinControl). Cuando el componente independiente muere, su canto de cisne consiste
en llamar a Notification para cada elemento de la lista privada.

¿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:

• Si la propiedad es derivada de TComponent, probablemente deba ser una


propiedad independiente.
• Por el contrario, cualquier otro tipo de propiedad debe ser posesión de nuestro
componente. Esto se aplica a TFont, a los derivados de TStrings e incluso a las
colecciones, que son clases persistentes derivadas de TCollection.

Una vez que hemos realizado esta identificación, podemos pasar a programar mecáni-
camente los puntos que resumo a continuación:

• Para propiedades independientes: redefinir Notification, y realizar asignar el


puntero nulo cuando se detecta la destrucción del objeto referido. En el método
de acceso de escritura, llamar a FreeNotification cuando el valor a asignar no
es nulo.
• Para propiedades dependientes: incluir en el constructor y destructor de nues-
tro componente el código de construcción y destrucción del objeto depen-
diente. En el método de acceso de escritura, utilizar Assign para copiar el ob-
jeto, no el puntero al mismo.

2. Identifique los valores por omisión de las propiedades escalares "planas".


Cuando digo "plana", quiero decir que se trata de una propiedad que no es de tipo
clase: enteros, reales, valores lógicos, conjuntos, etc. Debemos utilizar la directiva
default siempre que sea posible, pues salva espacio posteriormente en los ficheros
dfm. Para cada directiva de este tipo, por supuesto, debe generarse una instrucción co-
rrespondiente en el constructor que inicialice la propiedad. Si el valor por omisión es 0,
False o el conjunto vacío, recuerde que esta inicialización es innecesaria, pues al pe-
dirse memoria para el nuevo objeto se limpia ésta con ceros. Recuerde también que la
directiva default no puede utilizarse para cadenas de caracteres.

3. Esté atento a los eventos de notificación de ciertos tipos de propiedades.


Clases como TFont y TStrings (aunque no son componentes) tienen eventos internos
que se disparan cuando ocurren cambios en sus propiedades. Supongamos que esta-
mos implementando un visor binario para ficheros: digamos que se llama THexView.
Este componente tiene una propiedad Font para indicar el tipo de letra que utilizaremos
al dibujar. Ahora bien, este tipo de letra puede variar en tiempo de ejecución. Por ejem-
plo, podemos tener instrucciones del siguiente tipo:
Creación automática de componentes 21

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:

procedure THewView1.TipoLetraCambiado(Sender: TObject);


begin
Invalidate; // Redibujar el control
end;

El enlace entre el objeto Font y este receptor se debe realizar durante la construcción
del objeto:

constructor THewView1.Create(AOwner: TComponent);


begin
inherited Create(AOwner);
FFont := TFont.Create;
FFont.OnChange := TipoLetraCambiado;
end;

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.

4. Preste atención a las colecciones.


Propiedades tales como Columns, de la rejilla de datos TDBGrid, o Panels, de la archi-
conocida TStatusBar se implementan a partir de clases derivadas de TCollection. De
este modo, las definiciones de columnas o paneles pueden almacenarse de forma au-
tomática en el fichero dfm del componente. Desgraciadamente, hay que teclear bas-
tante para poder utilizar propiedades derivadas de TCollection. Por ejemplo, para im-
plementar Columns Delphi define las clases auxiliares TDBGridColumns y TColumn, y
la mayor parte de la definición e implementación de estas clases es bastante mecánica.
No quiero dar más detalles en esta página, para mantenerla con un tamaño manejable,
pero tengo previsto escribir un artículo sobre este tema más adelante.

5. Muchas propiedades escalares llaman a Invalidate al ser asignadas.


Si su componente desciende de TControl, es decir, si es un componente visible, es
muy probable que tenga que llamar mecánicamente al método Invalidate del control
para redibujarlo cuando se cambie el valor de una propiedad. Esto se realiza mecáni-
camente en el método de acceso de escritura de la correspondiente propiedad. Por
ejemplo:

procedure TDBJPEG.SetStretch(Value: Boolean);


begin
if FStretch <> Value then
begin
FStretch := Value;
Invalidate;
end;
end;

6. La mayor parte del código de un control de datos es mecánica.


Las reglas del juego para los controles data-aware son bastante simples, pero requie-
ren que el programador teclee mucho código: creación del TDataLink, intercepción de
sus eventos y exportación de algunas de sus propiedades, manejos de mensajes inter-
nos de Delphi como CM_ENTER y CM_EXIT... Por los mismos motivos que he ex-
puesto al hablar de las colecciones, no voy a profundizar ahora en este tema.

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.

La primera peculiaridad que observamos cuando trabajamos con colecciones es la forma


en que se crean. Esta es la definición del constructor:

constructor TCollection.Create(ItemClass: TCollectionItemClass);

Recuerde que TCollection desciende de TPersistent, no de TComponent, por lo cual no


está obligada a tener un "propietario" (owner). El parámetro del constructor es una referen-
cia de clase, y debe contener el nombre de una clase derivada de TCollectionItem. Esto
quiere decir que la colección sabe de antemano qué tipo de objetos va a alojar. Ya en esto
se diferencia de TList, que permite guardar punteros arbitrarios.

El próximo paso es saber cómo está definida TCollectionItemClass:

type
TCollectionItem = class(TPersistent)
// Etcétera
end;

TCollectionItemClass = class of TCollectionItem;

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.

ELEMENTOS DE COLECCIONES Y PERSISTENCIA

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.

AYUDANDO AL INSPECTOR DE OBJETOS

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:

procedure TTableItem.Assign(Source: TPersistent);


begin
if Source is TTableItem then
// Una sola propiedad a copiar
Table := TTableItem(Source)
else
inherited Assign(Source);
end;

Es posible también que queramos redefinir el método GetDisplayName:

function TTableItem.GetDisplayName: string;


begin
if Assigned(FTable) then
Result := FTable.Name
else
Result := inherited GetDisplayName;
end;

¿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:

constructor TCollectionItem.Create(Collection: TCollection); virtual;


Colecciones y persistencia 25

¿Se da cuenta de que al constructor se le pasa la colección a la cuál va a pertenecer el


nuevo elemento? de este modo, al crear un elemento estaremos automáticamente inser-
tándolo en su colección.

RETOCANDO LA COLECCIÓN

¿Y qué hay de la colección en sí? ¿Podemos declarar directamente propiedades de tipo


TCollection? Pues casi, porque tenemos que ocuparnos solamente de un detalle: hay que
redefinir el método GetOwner. Este método se utiliza en dos subsistemas diferentes de
Delphi. En primer lugar, lo utiliza la función GetNamePath, que es a su vez utilizada por el
Inspector de Objetos para nombrar correctamente a los elementos durante su edición. Vol-
viendo al ejemplo del TDBGrid, cuando estamos modificando las propiedades de una co-
lumna, en el Inspector de Objetos aparece un nombre como el siguiente para la columna
seleccionada:

DBGrid1.Columns[1]

DBGrid1 es el objeto que contiene a la colección, su propietario. Este propietario es tam-


bién utilizado por el algoritmo que se utiliza en la grabación del componente en el DFM
(podéis encontrar más detalles acerca de este proceso, en general, en el libro Secrets of
Delphi 2, de Ray Lischner1).

De todos modos, si solamente tenemos que redefinir el propietario de la colección, basta


con utilizar la clase TOwnedCollection, cuyo constructor tiene el siguiente prototipo:

constructor TOwnerCollection.Create(AOwner: TComponent;


ItemClass: TCollectionItemClass);

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:

property TCollection.Items[I: Integer]: TCollectionItem;

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

function TTableCollection.GetItems(I: Integer): TTable;


begin
Result := TTableItem(inherited Items[I]).Table;
end;

procedure TTableCollection.SetItems(I: Integer; Value: TTable);


begin
TTableItems(inherited Items[I]).Table := Value;
end;

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.

INTEGRANDO LA COLECCIÓN EN EL COMPONENTE

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:

1. El componente debe construir una colección internamente en el constructor, y destruirla


en su destructor.
2. El método de acceso a la propiedad para escritura debe utilizar Assign para copiar la
colección que se asigna externamente a la colección interna mantenida por el
componente.

Puede encontrar más detalles en el truco "Creación automática de componentes".

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:

function CollectionsEqual(C1, C2: TCollection): Boolean;

La técnica utilizada por CollectionsEqual consiste en guardar las dos colecciones en un


flujo de datos en memoria (TMemoryStream). Luego obtiene los dos punteros a ambas zo-
nas de memoria, ¡y realiza una comparación binaria entre estas dos! Dicho de otra forma:
"aplana" los dos objetos complejos, y compara sus representaciones binarias.
Escribir en la barra de título
Hace poco me han preguntado cómo se puede dibujar en la barra de título (caption bar) de
una ventana. Personalmente, estas son técnicas que no recomiendo. Sí, demuestran habi-
lidad en el manejo del API de Windows, pero una barra de título es una barra de título, y si
uno se pone a maquillarla mucho, puede que no la reconozca ni su madre.

De todos modos, la técnica es sencilla. Hay que interceptar un par de mensajes de


Windows: WM_NCPAINT y WM_NCACTIVATE. Las iniciales NC en estos mensajes co-
rresponden a Non-Client, es decir, el área que precisamente no está dedicada al dibujo...
Ambos mensajes son similares a WM_PAINT y a WM_NCACTIVATE, con la diferencia de
que WM_NCACTIVATE no va seguido de un mensaje de dibujo. Por lo tanto, también hay
que dibujar en respuesta al mensaje de activació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:

procedure TForm1.Titulo(wParam: Integer);


var
DC: THandle;
R1, R2: TRect;
begin
DC := GetWindowDC(Handle);
try
SetWindowText(Handle, nil);
GetWindowRect(Handle, R2);
R1.Left := GetSystemMetrics(SM_CXSIZE)
+ GetSystemMetrics(SM_CXBORDER) + GetSystemMetrics(SM_CXFRAME);
R1.Top := GetSystemMetrics(SM_CYFRAME);
R1.Right := R2.Right - R2.Left - R1.Left
- 2 * GetSystemMetrics(SM_CXSIZE);
R1.Bottom := R1.Top + GetSystemMetrics(SM_CYSIZE);
if wParam = 1 then
SetBkColor(DC, GetSysColor(COLOR_ACTIVECAPTION))
else
SetBkColor(DC, GetSysColor(COLOR_INACTIVECAPTION));
SetTextColor(DC, GetSysColor(COLOR_CAPTIONTEXT));
DrawText(DC, 'A la izquierda', -1, R1, DT_LEFT or DT_VCENTER);
DrawText(DC, 'A la derecha', -1, R1, DT_RIGHT or DT_VCENTER);
finally
ReleaseDC(Handle, DC);
end;
end;

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;

procedure TForm1.WMNCActivate(var M: TMessage);


begin
DefWindowProc(Handle, M.Msg, M.wParam, M.lParam);
Titulo(M.wParam);
M.Result := 1;
end;

procedure TForm1.WMNCPaint(var M: TMessage);


begin
DefWindowProc(Handle, M.Msg, M.wParam, M.lParam);
Titulo(1);
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:

1. Comprueba que el implementador de la rutina no realice modificaciones directas sobre


el parámetro. Como corolario, asegura al cliente de la rutina que no se realizarán modi-
ficaciones sobre el parámetro, por lo que puede pasar sin problemas variables a dicho
parámetro.
2. El compilador puede optimizar el traspaso de parámetros cuya representación binaria
tenga más de 4 bytes (las variables reales se tratan de forma diferente).

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.

Retrocedamos a la época de Delphi 1, cuando el tipo string se representaba como un


array de 256 caracteres. Cada vez que se pasaba una cadena por valor a un procedimiento
o función, se producía la copia de 256 bytes desde la variable original hacia la memoria de
pila de la rutina. Sin embargo, si la cadena se hubiera pasado por referencia (utilizando
var), solamente se hubiera pasado a la rutina el puntero a la cadena original, y la copia no
se produciría. ¿El coste? Pues que la rutina podría potencialmente realizar modificaciones
sobre la variable original (todo un riesgo), y que solamente podríamos pasar variables a la
rutina, no constantes o expresiones en general.

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.

Por lo tanto, si declaramos un parámetro de cadena mediante el modificador const, nos


ahorraremos el incremento y el decremento del contador de referencias del valor que
pasemos, operación que normalmente realizaría la rutina implícitamente. Aunque esta acti-
vidad es menos costosa que una copia, si estamos llamando a la rutina en cuestión dentro
de un bucle puede ahorrarnos tiempo y, en cualquier caso, espacio de código.

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.

La técnica de subclassing consiste en modificar la referencia que contiene un objeto de


ventana a su procedimiento de ventana. Se hace que el objeto apunte a un procedimiento
nuestro. Dentro de este procedimiento trabajamos con los mensajes que nos interesa, y los
que no nos interesan se los devolvemos al procedimiento que estaba instalado antes de
que metiéramos nuestras pezuñas en las intimidades del objeto.

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;

El componente imMDIBkg (¡estoy estrenando el prefijo!) contiene una propiedad de tiempo


de diseño llamada Bitmap, que debe almacenar el mapa de bits que dibujaremos en el
fondo. Para simplificar, utilizaré una sola modalidad de dibujo, repitiendo el mapa de bits a
modo de mosaico. Si usted lo desea, puede ampliar las capacidades del componente, para
dibujar solamente en el centro de la ventana, para "estirar" el bitmap y adaptarlo al tamaño
del área cliente, para utilizar una "brocha" como fondo, etc.

Además, vemos dos variables FClientInstance y FPrevClientInstance, que apuntarán res-


pectivamente al nuevo y al anterior procedimiento de ventana. Examinemos el constructor:

constructor TimMDIBkg.Create(AOwner: TComponent);


begin
inherited Create(AOwner);
FForm := AOwner as TForm;
FBitmap := TBitmap.Create;
end;

El constructor asume que el propietario del componente es un formulario, y se queda con


su referencia y ... ¡un momento! ¿No habíamos quedado en redirigir el procedimiento de
ventana del cliente? Sí, pero el constructor del componente no se ejecuta en el momento
adecuado. Las inicializaciones que requieran que el propietario esté construido y que todas
las propiedades del componente hayan sido leídas deben realizarse redefiniendo el método
Loaded:

procedure TimMDIBkg.Loaded;
begin
inherited Loaded;
FClientInstance := MakeObjectInstance(InternalClientProc);
FPrevClientProc := Pointer(SetWindowLong(
Form.ClientHandle, GWL_WNDPROC, Integer(FClientInstance)));
end;

Aquí está el truco. El procedimiento MakeObjectInstance se encarga de generar un thunk.


¡Vaya nombre! Un thunk es un trozo de código que cuando se ejecuta, se limita a llamar a
otra rutina Thunk en inglés se pronuncia "zunk", dando la impresión de ser algo rápido. El
código del thunk se almacena en un área de memoria pedida dinámicamente, que después
hay que liberar. En nuestro caso el thunk redirige al procedimiento InternalClientProc, que
veremos en breve. ¿Por qué es necesario? Pues porque InternalClientProc es un método,
no un procedimiento tradicional de C o Pascal, que es lo que realmente necesita un objeto
para su procedimiento de ventana. Así que la dirección del procedimiento de ventana
nuevo estará en FClientInstance, y cuando éste se ejecute, llamará al método
InternalClientProc. La cirugía sobre la ventana cliente se realiza llamando a
SetWindowLong. Esta función del API de Windows "entra" dentro del formato interno de la
ventana y asigno un valor de 4 bytes. La constante GWL_WNDPROC indica la posición
donde el objeto almacena la dirección su procedimiento de ventana. Como efecto secunda-
rio, SetWindowLong devuelve lo que había antes en esa posición. En este ejemplo, es la
dirección del antiguo procedimiento de ventana, la cual almacenaremos en
FPrevClientProc.

Los pasos inversos se siguen en el destructor del componente:

destructor TimMDIBkg.Destroy;
begin
Bitmaps, ventanas MDI y subclassing 33

SetWindowLong(Form.ClientHandle, GWL_WNDPROC, Integer(FPrevClientProc));


FreeObjectInstance(FClientInstance);
FBitmap.Free;
inherited Destroy;
end;

Es muy sencillo: primero se restaura el anterior procedimiento de memoria. Luego se libera


la memoria reservada para el thunk. Finalmente se destruye el mapa de bits y el propio
componente.

El algoritmo que dibuja en la superficie de la ventana cliente se implementa en el método


InternalClientProc:

procedure TimMDIBkg.InternalClientProc(var M: TMessage);


var
SrcDC, DstDC: hDC;
R, C, H, W: Word;
begin
if not FBitmap.Empty then
case M.Msg of
WM_HSCROLL, WM_VSCROLL:
InvalidateRect(Form.ClientHandle, nil, False);
WM_ERASEBKGND:
begin
SrcDc := Bitmap.Canvas.Handle;
DstDc := TWMEraseBkGnd(M).DC;
H := Bitmap.Height;
W := Bitmap.Width;
for R := 0 to Form.ClientHeight div H do
for C := 0 to Form.ClientWidth div W do
BitBlt(DstDC, C * W, R * H, W, H, SrcDC, 0, 0, SRCCOPY);
M.Result := 1;
Exit;
end;
end;
M.Result := CallWindowProc(FPrevClientProc, Form.ClientHandle,
M.Msg, M.wParam, M.lParam);
end;

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.

Puede descargar el código fuente de este truco desde la dirección:

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.

El ejemplo en cuestión utilizaba una secuencia definida del siguiente modo:

create sequence CodigoCliente increment by 1 start with 1;

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:

create or replace trigger BIClient


before insert on Clientes for each row
begin
if :new.Codigo is null then
:new.Codigo := CodigoCliente.NextVal; // ¡¡¡INCORRECTO!!!
end if;
end;
/

Aunque el código anterior tiene un aspecto bastante razonable y es bastante sencillo,


Oracle (por algún misterioso motivo) no lo acepta. La forma correcta de obtener el próximo
valor de una secuencia es como muestro a continuación:

create or replace trigger BIClient


before insert on Clientes for each row
begin
if :new.Codigo is null then
select CodigoCliente.NextVal // ¡¡¡AHORA SI!!!
into :new.Codigo
from Dual;
end if;
end;
/

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.

Por ejemplo, este método pertenece a la famosa MASTAPP, versión C++:

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
}

El código anterior tiene un error, y un mal aprovechamiento de recursos:

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.

En el siguiente método vemos en acción a LocalVCL en dos situaciones diferentes: con un


componente (el diálogo de configuración de la impresora) y con un objeto gráfico (un mapa
de bits). En el caso del mapa de bits tenemos que utilizar el primer constructor, y construir
explícitamente el objeto, ya que el constructor de la clase TBitmap no admite parámetros.
En cambio, la construcción del diálogo es más sencilla y compacta, por tratarse de un
componente:

void __fastcall TForm1::Button1Click(TObject *Sender)


{
LocalVCL<Graphics::TBitmap> B = new Graphics::TBitmap;
LocalVCL<TPrinterSetupDialog> PS;
PS->Execute();
B->Width = Screen->Width;
B->Height = Screen->Height;
}

En ambos casos, el objeto creado se destruye inexorablemente al finalizar el método, aun-


que se produzca una excepción durante su ejecución, pues de ello se encarga el compila-
dor. Si intentásemos utilizar el método correcto en C++ Builder que no utiliza smart pointers
ni __finally, esto sería lo que obtendríamos:

void __fastcall TForm1::Button1Click(TObject *Sender)


{
Graphics::TBitmap* B = new Graphics::TBitmap;
try
{
TPrinterSetupDialog *PS = new TPrinterSetupDialog(NULL);
try
{
PS->Execute();
}
catch(...)
{
delete PS;
throw;
}
delete PS;
B->Width = Screen->Width;
B->Height = Screen->Height;
}
catch(...)
{
delete B;
throw;
}
delete B;
}

De todos modos, me gustaría escuchar opiniones diferentes a la mía.

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:

void __fastcall TForm1::Button1Click(TObject *Sender)


{
// Observe que la sintaxis de la construcción es diferente
auto_ptr<Graphics::TBitmap> B(new Graphics::TBitmap);
auto_ptr<TPrinterSetupDialog> PS(NULL);
PS->Execute();
B->Width = Screen->Width;
B->Height = Screen->Height;
}
40 Calling Dr. Marteens

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:

Clientes.Locate('CODIGO', 1234, []);

Y esto es lo que le propongo que haga:

Clientes.Locate(ClientesCodigo.FieldName, 1234, []);

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

Por supuesto, según la semántica tradicional de un lenguaje de programación "normal", la


instrucción anterior solamente logra asignar el valor de B a A. ¡Y eso es precisamente lo
que pasa en InterBase! En la primera instrucción que he mostrado, al realizarse la segunda
asignación ya ha cambiado el valor de Cantidad, cuando no debería ser así. En el caso
anterior he solucionado el problema de este modo:

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:

create procedure Intercambiar as


declare variable MiA, MiB, MiClave int;
begin
for select Clave, A, B from Tabla into :MiClave, :MiA, :MiB do
update Tabla
set A = :MiB, B = :MiA
where Clave = :MiClave;
end

¿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:

Tabla1.Locate('FECHA', Edit1.Text, []);

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:

Tabla1.Locate('FECHA', StrToDate(Edit1.Text), []);

Así forzamos el uso de TwoDigitYearCenturyWindow.

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).

De las doscientas tablas mencionadas, la mayor parte corresponden habitualmente a ta-


blas de referencia. ¿Qué es una tabla de referencia, y cómo es posible que sean necesa-
rias tantas? Supongamos que estamos trabajando en un lenguaje de programación tradi-
cional, y que intentamos definir un tipo que represente a Personas. Uno de los atributos de
una persona sería su estado civil. El dominio de los distintos estados civiles se reduce a un
conjunto finito de valores, y casi siempre bastante reducido. Si nuestro lenguaje es
ObjectPascal podemos representarlo mediante un enumerativo:

type
TEstadoCivil = (ecSoltero, ecCasado, ecDivorciado);

Sin embargo, lo que es aceptable para un lenguaje de programación tradicional, no es vá-


lido para un diseño de bases de datos. ¿Qué pasa si queremos distinguir también a los
viudos y a los separados? En ObjectPascal modificaríamos la definición de EstadoCivil, re-
compilaríamos la aplicación y punto. Pero una base de datos plantea otras necesidades,
pues estos cambios dinámicos no pueden afectar a los datos ya existentes. Y nos interesa
además que el propio usuario de la aplicación pueda añadir estas opciones sin la interven-
ción nuestra.

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.

NOTA: No he analizado aquí explícitamente el uso de la propiedad LookupCache, pues


es similar al uso de consultas como conjuntos de referencia: la caché vale durante la
ejecución de la aplicación, pero durante la carga es necesario traer todo el conjunto de
datos.
Añadiendo estabilidad a
Paradox
Paradox falla. Este hecho es tan inevitable como el que siempre que se cae una tostada,
cae con la mantequilla hacia abajo. Hay que acostumbrarse a la idea de que el día menos
pensado encontramos en la pantalla de nuestro ordenador el temido mensaje: "Index is
corrupt". Ahora bien, existen toda una serie de medidas profilácticas que pueden retrasar el
deceso de nuestra base de datos. En esta página resumiré aquellas medidas que están
relacionadas con entradas del registro.

HKEY_LOCAL_MACHINE\System\CurrentControlSet\Services\VXD\VREDIR\
DiscardCacheOnOpen = 01

Esta entrada se modifica en los clientes. Mediante la misma, le pedimos al redirector de la


red Microsoft que cuando abra un fichero en red descarte cualquier página que haya que-
dado en la memoria del cliente. De este modo garantizamos que los datos de Paradox
sean siempre los actuales. Es preferible que tenga instalado Windows 95 OSR2 o superior,
pero en cualquier caso, asegúrese que la fecha del fichero VREDIR.VXD sea posterior al
11/Sep/97, y la de VNETSUP.VXD, posterior al 30/May/97.

HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\FileSystem\
DriveWriteBehind = 00 (DWORD)

Este cambio también se aplica al cliente, y es equivalente a entrar en MiPC, Rendimiento,


Sistema de archivos, Solución de problemas, y activar la opción "Desactivar la caché de
escritura en segundo plano".

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:

function GetBDEInfo(const Path, Param: string): string;


var
hCur: HDbiCur;
Desc: CFGDesc;
begin
Result := '<Not found>';
DbiOpenCfgInfoList(nil, dbiReadOnly, cfgPersistent, PChar(Path), hCur);
try
while DbiGetNextRecord(hCur, dbiNoLock, @Desc, nil) = DBIERR_NONE do
if StrIComp(Desc.szNodeName, PChar(Param)) = 0 then
begin
Result := StrPas(Desc.szValue);
Break;
end;
finally
DbiCloseCursor(hCur);
end;
end;

procedure SetBDEInfo(const Path, Param, Value: string);


var
hCur: HDbiCur;
Desc: CFGDesc;
begin
DbiOpenCfgInfoList(nil, dbiReadWrite, cfgPersistent, PChar(Path), hCur);
try
while DbiGetNextRecord(hCur, dbiNoLock, @Desc, nil) = DBIERR_NONE do
if StrIComp(Desc.szNodeName, PChar(Param)) = 0 then
begin
StrCopy(Desc.szValue, PChar(Value));
DbiModifyRecord(hCur, @Desc, True);
Break;
end;
finally
DbiCloseCursor(hCur);
end;
end;

¿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:

procedure SetLocalShare(const Value: string);


begin
SetBDEInfo('SYSTEM\INIT', 'LOCAL SHARE', Value);
end;

procedure SetNetDir(const Value: string);


begin
SetBDEInfo('DRIVERS\PARADOX\INIT', 'NET DIR', Value);
end;

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.

function GetNetworkID: string;


var
G: TGuid;
begin
OleCheck(CoCreateGuid(G));
Result := GuidToString(G);
Result := Copy(Result, Length(Result) - 12, 12);
end;

Para comprobar el resultado de la función, ejecute el programa winipcfg en su Windows y


compare el identificador de la tarjeta que este programa nos muestra. Puede también te-
clear en el editor de Delphi o C++ Builder la combinación Ctrl+May+G.

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).

CREAR UN PACKAGE PARA EL EXPERTO

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

Si se tratase de un runtime package, también modificaría DCP output directory y Unit


output directory.

CREAR EL OBJETO EXPERTO Y REGISTRARLO

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;

En la cláusula uses de la interfaz debemos añadir, obligatoriamente, la unidad ToolsAPI,


que define las interfaces IOTAWizard y IOTANotifier. No voy a entrar en detalles de cómo
se implementa una interfaz, ni qué es TInterfacedObject. Hay material suficiente para un li-
bro sobre estos asuntos. El nombre que se la da a la clase no tiene importancia, pero trate
por todos los medios que sea poco probable una colisión de nombres: en definitiva, esta
clase va a integrarse con las clases de la VCL y del propio entorno de desarrollo de Delphi.
Aquí yo he utilizado mi prefijo registrado: im.

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;

function TimExpertTemplate.GetIDString: string;


begin
Result := 'Su empresa.Nombre del experto';
end;

function TimExpertTemplate.GetName: string;


begin
Result := 'El nombre descriptivo de su experto';
end;

function TimExpertTemplate.GetState: TWizardState;


begin
Result := [wsEnabled];
end;
Primeros pasos con las Open Tools 57

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;

La implementación será así de fácil:

procedure Register;
begin
RegisterPackageWizard(TimExpertTemplate.Create as IOTAWizard);
end;

AÑADIR UN MÓDULO DE DATOS

Es el momento de añadir el módulo de datos que antes mencionábamos. Vaya a File|New,


y realice un doble clic en Data module. Guarde la unidad y su DFM con un nombre que
pueda posteriormente recordar. Y no olvide regresar a la unidad del paso anterior para in-
cluir la referencia a la nueva unidad. Finalmente, cambie la propiedad Name del módulo a
TimExpertModule.

¿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.

CREAR LA ESTRUCTURA DE MENÚ DE NUESTRO EXPERTO

¿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'.

Lo siguiente es ir a la página Standard y añadir un TPopupMenu al módulo. Es ahí donde


debemos crear los menu items que añadiremos al IDE. Si lo desea, puede utilizar separa-
dores, submenús o el truco que le de la gana. Para este ejemplo, crearé solamente dos
elementos de menú: el primero será un vulgar separador (Caption = '-'). El segundo co-
mando debe ir inmediatamente debajo del separador, y debemos asociarlo a la única ac-
ción que hemos creado. A este elemento lo llamaremos mi_im_ArrangeLabels.
Detalle importante: ¿quiere utilizar un dibujo en su opción de menú? Cree el dibujo en un
fichero BMP, a 16 colores y con 16x16 píxeles de área. Utilice la propiedad Bitmap del
menu item que acabamos de crear (mi_im_ArrangeLabels) para cargar la imagen.

Cuando el experto se instale, debemos añadir estos comandos a la estructura de menú de


Delphi.

procedure TimExpertModule.imExpertModuleCreate(Sender: TObject);


var
I, InsertPosition: Integer;
58 Calling Dr. Marteens

DataMenu, Item: TMenuItem;


begin
with BorlandIDEServices as INTAServices do
begin
DataMenu := MainMenu.Items[7]; // ¡¡Cuente con cuidado!
ac_im_ArrangeLabels.ImageIndex :=
AddMasked(mi_im_ArrangeLabels.Bitmap, clSilver);
end;
InsertPosition := DataMenu.Count;
for I := PopupMenu1.Items.Count - 1 downto 0 do
begin
Item := PopupMenu1.Items[I];
PopupMenu1.Items.Delete(I);
DataMenu.Insert(InsertPosition, Item);
end;
end;

¿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.

FUNCIONES AUXILIARES QUE UTILIZAN LA OTAPI

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:

function CurrentForm: TCustomForm;


var
CurrMod: IOTAModule;
FormEdt: IOTAFormEditor;
CurrCom: INTAComponent;
Comp: TComponent;
I: Integer;
begin
Result := nil;
CurrMod := (BorlandIDEServices as IOTAModuleServices).CurrentModule;
if CurrMod <> nil then
for I := 0 to CurrMod.GetModuleFileCount - 1 do
if CurrMod.GetModuleFileEditor(I).QueryInterface(IOTAFormEditor,
FormEdt) = S_OK then
if FormEdt.GetRootComponent <> nil then
if FormEdt.GetRootComponent.QueryInterface(INTAComponent,
CurrCom) = S_OK then
begin
Comp := CurrCom.GetComponent;
if Comp is TCustomForm then Result := TCustomForm(Comp);
Exit;
end;
end;

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

LAS ACCIONES DEL MENÚ

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:

procedure TimExpertModule.ac_im_ArrangeLabelsUpdate(Sender: TObject);


begin
TAction(Sender).Enabled := CurrentForm <> nil;
end;

Igual de fácil es ejecutar la acción:

procedure TimExpertModule.ac_im_ArrangeLabelsExecute(Sender: TObject);


begin
OrganizarEtiquetas(CurrentForm);
end;

OrganizarEtiquetas es la función que contiene el meollo del experto. Este es el código de


ejemplo:

function CanFormat(C: TControl): Boolean;


begin
Result := (C is TCustomEdit) and not (C is TCustomMemo)
or (C is TCustomComboBox)
or (C is TDBLookupComboBox);
end;

procedure OrganizarEtiquetas(AForm: TCustomForm);


var
I: Integer;
ALabel: TLabel;
begin
for I := 0 to AForm.ComponentCount - 1 do
if AForm.Components[I] is TLabel then
begin
ALabel := TLabel(AForm.Components[I]);
if (ALabel.FocusControl <> nil)
and CanFormat(ALabel.FocusControl) then
begin
if not ALabel.AutoSize then ALabel.Alignment := taLeftJustify;
ALabel.Left := ALabel.FocusControl.Left;
ALabel.Top := ALabel.FocusControl.Top - ALabel.Height - 3;
end
end;
end;

Como podemos apreciar, OrganizarEtiquetas se limita a cambiar la posición de los compo-


nentes TLabel de un formulario que están asociados a cuadros de edición y combos, de
modo que aparezcan por encima del control al que están enlazadas. Por supuesto, se trata
de un algoritmo muy sencillo, pero constituye un buen punto de partida para que usted
pueda montarse asistentes más sofisticados.
Añadiendo propiedades a un
formulario
Vuelvo a estar en deuda con Ray Lischner4, pues este truco simplemente desarrolla la téc-
nica que describe en su propia página. De lo que se trata es de definir nuevas clases de
formularios (TForm) que incorporen nuevas propiedades y eventos, de modo tal que éstas
puedan ser editadas en el propio Inspector de Objetos.

DEFINIR UN NUEVO COMPONENTE BASADO EN TFORM

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.

El nuevo tipo de formulario debe crearse dentro de un package de tiempo de ejecución.


Podemos utilizar un package mixto, para diseño y ejecución, pero es preferible mover el
código de tiempo de diseño fuera, para evitar sobrecargar de código a los programas que
utilicen el formulario.

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;

Los procedimientos de clase se implementan del siguiente modo:

class function TDatabaseForm.Ejecutar: TModalResult;


begin
Result := Create(nil).ShowModal;
end;

class procedure TDatabaseForm.Mostrar;


var
I: Integer;
F: TForm;
begin
LockWindowUpdate(Application.MainForm.Handle);
try
for I := Screen.FormCount - 1 downto 0 do
begin

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.SetDataSet(Value: TDataSet);


begin
if Value <> FDataSet then
begin
FDataSet := Value;
if Value <> nil then Value.FreeNotification(Self);
end;
end;

procedure TDatabaseForm.Notification(C: TComponent; Op: TOperation);


begin
inherited Notification(C, Op);
if (C = FDataSet) and (Op = opRemove) then
FDataSet := nil;
end;

Durante la ejecución del nuevo Loaded se enganchan nuestros propios manejadores de


eventos:

procedure TDatabaseForm.Loaded;
begin
inherited Loaded;
FOldClose := OnClose;
FOldCloseQuery := OnCloseQuery;
OnClose := InternalClose;
OnCloseQuery := InternalCloseQuery;
end;

La respuesta interna a OnClose es muy sencilla:

procedure TDatabaseForm.InternalClose(Sender: TObject;


var Action: TCloseAction);
begin
if Assigned(FOldClose) then FOldClose(Sender, Action);
Action := caFree;
end;

La respuesta a OnCloseQuery es un poco más larga:

procedure TDatabaseForm.InternalCloseQuery(Sender: TObject;


var CanClose: Boolean);
begin
if Assigned(FOldCloseQuery) then
begin
FOldCloseQuery(Sender, CanClose);
if not CanClose then Exit;
end;
if Assigned(FDataSet) and FDataSet.Active then
if ModalResult in [mrOk, mrNone] then
FDataSet.CheckBrowseMode
Añadiendo propiedades a un formulario 63

else
FDataSet.Cancel;
end;

Con esto terminamos la programación del package de tiempo de ejecución.

CREAR UN EXPERTO

Para poder aprovechar un formulario con nuevas propiedades y eventos, necesitamos un


experto que cree formularios del nuevo tipo. El experto puede crearse en otro package,
esta vez de tiempo de diseño, en el cual debemos además registrar el nuevo tipo de for-
mulario a la medida. En la unidad DsgnIntf se define el siguiente método:

procedure RegisterCustomModule(BaseClass: TComponentClass;


CustomModule: TCustomModuleClass);

El tipo TCustomModuleClass es una referencia a clases derivadas de TCustomModule,


también definida en DsgnIntf. Para registrar un formulario personalizado debemos llamar a
este procedimiento dentro de un procedimiento denominado Register:

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:

function TDatabaseCustomModule.GetVerbCount: Integer;


begin
Result := 1;
end;

function TDatabaseCustomModule.GetVerb(Index: Integer): string;


begin
Result := '';
if Index = 0 then
Result := 'Alinear componentes...';
end;

procedure TDatabaseCustomModule.ExecuteVerb(Index: Integer);


begin
AlinearComponentes(Root);
// Esta se la debo, !
end;
Limitando el número de
registros
... El espía se introdujo sigilosamente en la sala de ordenadores. Con paso felino se dirigió a una de las
terminales de datos y la encendió. No tuvo problemas con la contraseña; el departamento de escuchas había
interceptado la palabra clave hacía una semana gracias a una indiscreción telefónica de uno de los empleados de
la Compañía. Una vez que la pantalla indicó que el sistema estaba listo, ordenó la ejecución del programa de
accesos a la base de datos. Tenía que averiguar el nombre de algunas de las personas que habían sido utilizadas,
sin su consentimiento, en los peligrosos experimentos biológicos. Tecleo una sencilla instrucción SQL y pulsó un
botón con el ratón. De repente, se dio cuenta de que algo andaba mal. La pantalla vomitaba página tras página
de datos, y no había forma de detenerla. Le quedaban dos minutos para apagar el ordenador, eliminar las
huellas y salir corriendo. Pero aquel trasto seguía con su verborrea, insistiendo en mostrar todos los nombres de
los afectados. Si aquello no se detenía, era hombre acabado. Desesperado, sacó su Magnum de la cartuchera y
disparó cuatro tiros a la maldita máquina ...

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

También podemos indicar un porcentaje del número total de registros:

select top 5 percent *


from Clientes

En DB2, se puede utilizar una cláusula equivalente al final de la consulta:

select *
from Clientes
fetch first 25 rows only

Oracle resuelve el problema gracias a una pseudo columna, de nombre rownum:

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:

create procedure NPrimeros(N integer)


returns (Nombre varchar(35), Direccion varchar(35)) as
begin
for select Nombre, Direccion from Clientes
into :Nombre, :Direccion do
begin
suspend;
N = N - 1;
if (N = 0) then exit;
end
end

Más adelante, podemos lanzar consultas como la siguiente:

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.

Otro de sus puntos fuertes es su lenguaje SQL y de extensiones procedimentales. Inter-


Base es uno de los pocos sistemas que implementan todas las acciones referenciales es-
tablecidas por el estándar ANSI. Dicho en cristiano: al definir una restricción de integridad
referencial (foreign key) podemos indicar que al eliminar una fila maestra se eliminen to-
das las filas de detalles asociadas (on delete cascade) o que, alternativamente, se prohiba
la operación si existen dichas filas (on delete no action); el objetivo de ambas opciones,
por supuesto, es evitar la aparición de referencias "huérfanas", a filas inexistentes. Usted
dirá: "sí, pero eso también lo hace Oracle (o el sistema que sea)". Es cierto, pero no todos
los sistemas permiten actualmente especificar on update cascade, para propagar en cas-
cada las modificaciones realizadas sobre la clave primaria de la tabla maestra. Además, se

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.

El gran defecto de InterBase, desde mi punto de vista, es la falta de un potente mecanismo


de particiones. Es cierto que una base de datos puede distribuirse en varios ficheros, que
pueden ubicarse en diferentes discos del mismo ordenador, pero no tenemos control al-
guno (al menos por el momento) de dónde se almacena cada tabla o índice. Tampoco
existen muchas posibilidades en la elección del almacenamiento de tablas, o en la imple-
mentación de índices.

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.

Y si seguimos mencionando cosas que faltan, llegamos a las opciones de replicación. No


hay replicación predefinida. Pero, ¿cuán importante es esta carencia? Algunos utilizan la
replicación para proteger físicamente los datos. En este caso, InterBase ofrece una va-
riante más sencilla: el uso de shadows que duplican los cambios en una base de datos en
otro disco del mismo ordenador. Otro uso de la replicación está relacionado con el soporte
para aplicaciones de tomas de decisión. Este tipo de aplicaciones necesita transacciones
serializables para mayor coherencia, y ya sabemos que este nivel de aislamiento se logra
en otros sistemas mediante bloqueos compartidos, afectando el rendimiento de las tran-
sacciones OLTP. La solución habitual es ejecutar estas aplicaciones sobre una réplica de
la base de datos. En InterBase no hay que llegar a estos extremos, pues la arquitectura
multigeneracional resuelve el conflicto entre lectores y escritores. De todos modos, no es
difícil implementar la replicación mediante programa, y es muy posible que próximas ver-
siones de InterBase ya traigan alguna solución preparada.

ORACLE

Agarre el nombre del producto como si se tratase de un calcetín y déle la vuelta:


Oracle==elcarO. Pero es tan bueno como costoso, a diferencia de otros productos que son
caros por puro vicio. Oracle es un producto con muchos años en el mercado, y eso se nota.
Si es justificable algún curso de administración de bases de datos, ese es el de Oracle,
pues las opciones de configuración abundan más que las pulgas sobre un perro flaco.
Yo no puedo hablarle acerca de qué tal va un Oracle multiservidor con opciones paralelas,
porque no tengo ese hardware. Pero sí he probado a optimizar una base de datos en un
servidor con varios discos ... y vaya si se nota la diferencia con respecto a una base de
datos con opciones por omisión. Ahí es donde está el problema: si usted no está dispuesto
a invertir tiempo y materia gris en estudiar cómo se configura correctamente un Oracle, no
malgaste su dinero y quédese con un InterBase. En caso contrario, resígnese a tener pe-
Escogiendo un servidor 69

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.

MICROSOFT SQL SERVER

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.

He aquí algunos de los pecados de Transact SQL:


70 Calling Dr. Marteens

1. Los triggers son siempre a nivel de instrucción, no de fila.


2. Los triggers, además se ejecutan siempre después de la instrucción, no antes.
3. Dentro de un trigger o de un procedimiento almacenado se puede ejecutar un
rollback de una transacción.
4. En realidad, el punto 3 se debe a que las acciones de los triggers no son
atómicas, que es lo que debería ser.

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 UNIVERSAL DATABASE

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.

La gran pifia de DB2: ¿necesita un procedimiento almacenado? ¡Escríbalo en C, o en cual-


quier otro lenguaje externo que pueda comunicarse con DB2! Vale, pero como el servidor
puede ejecutarse en OS/2, Windows NT, AIX, etc, olvídese de DB2 si quiere distribuir su
aplicación en empresas que tengan el producto instalado. En caso contrario, tendrá que
experimentar con los compiladores de C de todos los sistemas operativos posibles. No es
imposible, pero sí demasiado trabajoso. Además, C, Java, Visual Basic, o el lenguaje en
que se programe el procedimiento, son lenguajes de "bajo nivel" en comparación con SQL.
Lo que podemos hacer en tres líneas de SQL requiere 18 en C++.
InterBase y el disco D:

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

Observe que estamos refiriéndonos a una ruta de directorios, no a un recurso compartido. Es


muy posible y deseable que el usuario de red no tenga acceso al directorio C:\Datos\1999 me-
diante el servicio de compartir archivos del servidor; deseable porque, de este modo, no puede
realizar copias sin autorización del fichero, que pondrían en peligro la seguridad de los datos.

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:

procedure TForm1.DBGrid1DrawColumnCell(Sender: TObject; const Rect: TRect;


DataCol: Integer; Column: TColumn; State: TGridDrawState);
var
Check: Integer;
begin
if Column.FieldName = '' then
begin
DBGrid1.Canvas.FillRect(Rect);
Check := 0;
if Table1['PREFERRED'] then
Check := DFCS_CHECKED;
DrawFrameControl(DBGrid1.Canvas.Handle, Rect,
DFC_BUTTON, DFCS_BUTTONCHECK or Check);
end
else
DBGrid1.DefaultDrawColumnCell(Rect, DataCol, Column, State);
end;

La función DrawFrameControl pertenece al API de Windows. Puede consultar la ayuda en


línea para comprobar todas las posibilidades que ofrece dicha función.

73
Decision Cube y las fechas

USO Y ABUSO DE DECISION CUBE

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?

MODIFICACIÓN DINÁMICA DE PARÁMETROS DE DIMENSIONES

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:

procedure ModificarCubo(ACube: TDecisionCube; ItemNo: Integer;


Grouping: TBinType);
var
DM: TCubeDims;
begin
DM := TCubeDims.Create(nil, TCubeDim);
try
DM.Assign(ACube.DimensionMap);
DM.Items[ItemNo].BinType := Grouping;
ACube.Refresh(DM, False);
finally
DM.Free;
end;
end;

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.

Primero desarrollaremos la ventana de vista preliminar. Creamos un nuevo formulario, al


cual llamaremos wndPrev. En su interior dejamos caer un ToolBar y un componente
QRPreview, de la página QReport de la Paleta de Componentes. A este último control le
cambiamos su propiedad Align a alClient. Ahora interceptamos el método OnClose del for-
mulario, para garantizar la devolución de recursos:

procedure TwndPrev.FormClose(Sender: TObject; var Action: TCloseAction);


begin
Action := caFree;
QRPreview1.QRPrinter := nil;
end;

En la barra de herramientas añadimos cuatro botones para navegar por las páginas. Estos
son los métodos que ejecutarán:

procedure TwndPrev.bnPrimero(Sender: TObject);


begin
QRPreview1.PageNumber := 1;
end;

procedure TwndPrev.bnAnterior(Sender: TObject);


begin
QRPreview1.PageNumber := QRPreview1.PageNumber - 1;
end;

procedure TwndPrev.bnSiguiente(Sender: TObject);


begin
QRPreview1.PageNumber := QRPreview1.PageNumber + 1;
end;

procedure TwndPrev.bnUltimo(Sender: TObject);


begin
QRPreview1.PageNumber := QRPreview1.QRPrinter.PageCount;
end;

Traemos dos botones más, para el factor de escala de la muestra:

procedure TwndPrev.bnZoomIn(Sender: TObject);


begin
QRPreview1.Zoom := QRPreview1.Zoom + 20;
end;

procedure TwndPrev.bnZoomOut(Sender: TObject);


begin
QRPreview1.Zoom := QRPreview1.Zoom - 20;
end;

Existen también métodos para los valores especiales de acercamiento: ZoomToFit y


ZoomToWidth. Para terminar con la ventana, traemos un botón de impresión:

procedure TwndPrev.bnPrint(Sender: TObject);


begin
QRPreview1.QRPrinter.Print;
end;

77
78 Calling Dr. Marteens

En el informe para el cual queremos esta vista preliminar debemos interceptar el evento
OnPreview:

procedure TrptCustomer.QuickRep1Preview(Sender: TObject);


begin
with TwndPrev.Create(nil) do
begin
QRPreview1.QRPrinter := Sender as TQRPrinter;
ShowModal;
// El formulario se destruye automáticamente
end,
end;

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:

procedure TwndMain.Button1Click(Sender: TObject);


begin
rptCustomer.QuickRep1.PreviewModal;
end;

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.

CON LA AYUDA DE WINDOWS

La técnica está basada en una opción de la función DrawText, del API de Windows:

function DrawText(hDC: HDC; lpString: PChar; nCount: Integer;


var lpRect: TRect; uFormat: UINT): Integer;

Esta función, y su prima DrawTextEx, permiten el uso de la opción DT_PATH_ELLIPSIS en


su parámetro uFormat. Conociendo este detalle, es fácil llamar directamente a función en la
respuesta a algún evento OnPaint, o sencillamente crearnos un nuevo componente, ba-
sado en el viejo TLabel, para abreviar los nombres de directorios:

type
TShortPathLabel = class(TCustomLabel)
protected
procedure Paint; override;
public
constructor Create(AnOwner: TComponent); override;
published
property Alignment;
property Transparent;
end;

El constructor se redefine para asignar False a la propiedad WordWrap de la etiqueta, que


intencionalmente se ha quedado sin publicar:

constructor TShortPathLabel.Create(AnOwner: TComponent);


begin
inherited Create(AnOwner);
WordWrap := False;
AutoSize := False;
end;

El método de dibujo es muy sencillo, como se muestra a continuación:


procedure TShortPathLabel.Paint;
const
Alignments: array[TAlignment] of Word = (DT_LEFT, DT_RIGHT, DT_CENTER);
var
Rect: TRect;
begin
Rect := ClientRect;
if not Transparent then
begin
Canvas.Brush.Color := Self.Color;
Canvas.Brush.Style := bsSolid;
Canvas.FillRect(Rect);
end;
Canvas.Brush.Style := bsClear;
Canvas.DrawText(Canvas.Handle, PChar(Caption), -1, Rect,
DT_PATH_ELLIPSIS or Alignments[Alignment]);
end;

79
80 Calling Dr. Marteens

Se puede mejorar el componente añadiendo una propiedad para decidir si se abrevia o no


el nombre de directorio. Además, DrawText permite también utilizar la opción alternativa
DT_END_ELLIPSIS, para que los puntos suspensivos se coloquen al final de la cadena.
Esta opción quizá sea la más adecuada para los procesos que indican su progreso mos-
trando el nombre de un fichero.

CON LA AYUDA DE DELPHI

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:

function MinimizeName(const Filename: TFileName; Canvas: TCanvas;


MaxLen: Integer): TFileName;

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:

procedure TForm1.SelectClick(Sender: TObject);


var
Dir: string;
begin
if SelectDirectory('Seleccione un directorio', 'C:\', Dir) then
begin
Dir := MinimizeName(Dir, Label1.Canvas, Label1.Width);
Label1.Caption := Dir;
end;
end;

He aprovechado también para mostrar otra función de Delphi relativamente desconocida:


esta vez se trata de SelectDirectory, que permite al usuario seleccionar un directorio me-
diante un cuadro de diálogo. Existen dos versiones de esta rutina en Delphi 4, ambas mar-
cadas con la directiva overload:

function SelectDirectory(var Directory: string;


Options: TSelectDirOpts; HelpCtx: Longint): Boolean; overload;
function SelectDirectory(const Caption: string; const Root: WideString;
out Directory: string): Boolean; overload;

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:

1. La variable de puntero era global. Destrucción con delete.


2. Variable de puntero global. Destrucción con Free.
3. Variable de puntero local. Destrucción con delete.
4. Variable de puntero local. Destrucción con Free.

Por ejemplo, para el primer caso utilicé el siguiente código:

void __fastcall TForm2::Button1Click(TObject *Sender)


{
// Form2 es una variable global
Form2 = new TForm2(0);
Form2->ShowModal();
delete Form2;
// La alternativa sería: Form2->Free();
}

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.

LAS TABLAS EN MEMORIA DEL BDE

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:

function DbiCreateInMemTable(hDb: hDBIDb; pszName: PChar; iFields: Word;


pfldDesc: pFLDDesc; var hCursor: hDBICu): DBIResult;

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.

Como consecuencia indeseada de la elección de este ancestro, la clase


TMemoryTable publicará las propiedades Filter, Filtered y FilterOptions, aunque
realmente no estén correctamente implementadas.

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

1. La nueva propiedad CloneDataSet puede apuntar a una tabla o consulta, de la cual


se extraerá las definiciones pertinentes.
2. Aunque el tipo TDataSet ya contiene la propiedad FieldDefs, no la publica en el
Inspector de Objetos. De esto nos encargaremos nosotros, además de implemen-
tar la función IsFieldDefsStored que utilizaremos en una cláusula stored para al-
macenar el valor de FieldDefs en el fichero DFM cuando el número de definiciones
de campos sea distinto de cero.

Entre estos dos mecanismos, CloneDataSet tendrá más prioridad.

El núcleo de todo el componente es, como he mencionado, la función CreateHandle, cuya


implementación incluyo a continuación:

function TMemoryTable.CreateHandle: HDBICur;


var
OldActive: Boolean;
DSProps: CurProps;
FieldDesc: TFieldDescList;
I, FieldCount: Word;
begin
FieldCount := FieldDefs.Count;
if FCloneDataSet <> nil then
begin
OldActive := FCloneDataSet.Active;
try
FCloneDataSet.Open;
Check(DbiGetCursorProps(FCloneDataSet.Handle, DSProps));
FieldCount := DSProps.iFields;
SetLength(FieldDesc, FieldCount);
Check(DbiGetFieldDescs(FCloneDataSet.Handle, pFLDDesc(FieldDesc)));
finally
FCloneDataSet.Active := OldActive;
end;
end
else if FieldCount > 0 then
begin
SetLength(FieldDesc, FieldCount);
for I := 0 to FieldCount - 1 do
with FieldDefs[I] do
EncodeFieldDesc(FieldDesc[I], Name, DataType, Size, Precision);
end
else
DatabaseError('Dataset required');
Check(DbiCreateInMemTable(DBHandle, PChar(FTableName), FieldCount,
pFLDDesc(FieldDesc), Result));
end;

El método auxiliar EncodeFieldDesc ha sido copiado de la clase TTable, y transforma un


TFieldDef en un valor de tipo FLDDesc, del API del BDE, haciendo uso de algunas cons-
tantes globales de la unidad DBCommon:

procedure TMemoryTable.EncodeFieldDesc(var FieldDesc: FLDDesc;


const Name: string; DataType: TFieldType; Size, Precision: Word);
begin
with FieldDesc do
begin
StrPLCopy(szName, Name, SizeOf(szName) - 1);
iFldType := FldTypeMap[DataType];
iSubType := FldSubTypeMap[DataType];
case DataType of
ftString, ftFixedChar, ftBytes, ftVarBytes, ftBlob..ftTypedBinary:
iUnits1 := Size;
ftBCD:
begin
{ Default precision is 32, Size = Scale }
if (Precision > 0) and (Precision <= 32) then
iUnits1 := Precision
else
iUnits1 := 32;
Tablas en memoria 85

iUnits2 := Size; {Scale}


end;
end;
end;
end;

UN MUNDO DE POSIBILIDADES

Esta misma técnica (crear descendientes de TBDEDataSet redefiniendo simplemente


CreateHandle) puede aplicarse a muchos otros tipos de cursores especiales del BDE. Por
ejemplo, existe una larga lista de funciones, que puede buscar en la ayuda del BDE te-
cleando "DbiOpenList functions", y que devuelven información sobre tablas disponibles, la
estructura de las mismas, los alias y controladores registrados, etc. Lo más interesante
consiste en que, una vez que redefinimos CreateHandle, el resto de los métodos hereda-
dos desde TBDEDataSet y TDataSet se encargan de la configuración del buffer del con-
junto de datos, de la creación de componentes TFields y todo lo demás. Quizás más ade-
lante muestre ejemplos de clases que utilicen estas otras funciones.

Esta misma técnica (crear descendientes de TBDEDataSet redefiniendo simplemente


CreateHandle) puede aplicarse a muchos otros tipos de cursores especiales del BDE. Por
ejemplo, existe una larga lista de funciones, que puede buscar en la ayuda del BDE te-
cleando "DbiOpenList functions", y que devuelven información sobre tablas disponibles, la
estructura de las mismas, los alias y controladores registrados, etc. Lo más interesante
consiste en que, una vez que redefinimos CreateHandle, el resto de los métodos hereda-
dos desde TBDEDataSet y TDataSet se encargan de la configuración del buffer del con-
junto de datos, de la creación de componentes TFields y todo lo demás. Quizás más ade-
lante muestre ejemplos de clases que utilicen estas otras funciones.
El mes siguiente y el anterior

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:

function IncMonth(Fecha: TDateTime; Meses: Integer): TDateTime;

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

Millones de veces he tronado contra el uso insensato de recursos como generadores


(InterBase), secuencias (Oracle) y campos de identidad (SQL Server). Repito los motivos:

• 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.

Hay un curioso acorde, o conjunto de notas que se pulsan simultáneamente, denominado


de séptima disminuida. Puede corresponder a la siguiente combinación: Do, Mi bemol, Fa
sostenido, La, Do octava. La particularidad de este acorde es que permite enlazar temas
musicales de los más diversos, y sirve de puente entre las más disímiles tonalidades. El
gran genio musical de Johann Sebastian Bach fue el primero que aprovechó todas sus po-
sibilidades. Y lo más interesante es que contiene dos intervalos diabólicos en su interior:
del Do al Fa sostenido, y del Mi bemol al La natural. Si crees en la Luz, es gracias a la Os-
curidad...

ESCALA ASCENDENTE, IDENTIDADES CAMBIANTES

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:

create table FormasDePago (


Codigo int not null identity(1,1),
Descripcion varchar(30) not null,

primary key nonclustered (Codigo),


unique clustered (Descripcion)
);

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.

COMO HACER QUE TODO FUNCIONE

Para que un mantenimiento sobre este tipo de tablas funcione, hay que tener en cuenta las
siguientes recomendaciones:

• La definición de la tabla no puede contener especificaciones default, ni pueden existir


triggers asociados a las operaciones de modificación. Si existen tales definiciones, el
BDE intentará releer los registros recién insertados, y tendremos problemas cuando no
pueda encontrarlos por culpa del cambio dinámico de la clave primaria. Es evidente
que FormasDePago cumple este requisito.
• En el lado de la aplicación, cuando abrimos un TTable o un TQuery, el conjunto resul-
tado debe estar ordenado por alguna clave alternativa única o casi única. Y también
estamos cumpliendo este requisito
• Por último, es sumamente conveniente, aunque no necesario, esconder el código de
operación de la vista del usuario durante los mantenimientos. La causa es que, a pesar
de las precauciones anteriores, cuando insertemos una nueva forma de pago el BDE
no se quejará, pero no actualizará el código asignado. Hay programadores que refres-
can entonces la tabla, pero esa técnica me parece un desperdicio de las capacidades
de la red. Además, ¿no habíamos quedado en que el código era un valor interno? Pues
seamos consecuentes.

Si se siguen las recomendaciones anteriores, la ventana de mantenimiento de la tabla de


formas de pago contendrá una rejilla con una sola columna. Se pueden arreglar las cosas
visualmente para que el usuario tenga la impresión de que está trabajando con una lista de
cadenas en un cuadro de listas. Por ejemplo, podemos eliminar los títulos de la rejilla, la
columna del indicador y las líneas de separación jugando un poco con la propiedad Options
del DBGrid.
Fechas en SQL Server 7

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:

1. Ejecute en el cliente la utilidad Client Network Utility. Esta aplicación puede


ejecutarse desde el grupo de programas que instala SQL Server.

2. Seleccione la última página de la utilidad: DBLibrary Options. Luego, desactive


la casilla Use International Settings.

3. Ejecute el Administrador del BDE. En la página Configuration seleccione el


nodo Drivers|Native|MSSQL y cambie el parámetro DATE MODE a 1 (DMY).

91
Cómo eliminar un generador
Todos sabemos cómo se puede crear un generador en InterBase:

create generator UnGenerador;

También sabemos cómo darle un determinado valor inicial:

set generator UnGenerador to 100;

¡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:

delete from RDB$GENERATORS


where RDB$GENERATOR_NAME = 'UNGENERADOR';

Observe que el nombre del generador debe especificarse con mayúsculas.

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:

create trigger AsignarCodigoFormaPago for FormasPago


active before insert position 0 as
begin
if (new.Codigo is null) then
new.Codigo = gen_id(FormaPagoGen);
end!

create procedure DameUnNumero returns(Codigo integer) as


begin
Codigo = gen_id(OtroGenerador, 1);
end!

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:

create procedure ValorActual returns(Codigo integer) as


begin
Codigo = gen_id(OtroGenerador, 0);
end!

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:

/* ¡¡¡NO FUNCIONA, LA TABLA Dual ES DE ORACLE, NO DE INTERBASE!!! */


select gen_id(OtroGenerador, 0) from Dual

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

select gen_id(OtroGenerador, 0) from RDB$DATABASE

En realidad, SQL Explorer utiliza este otro truco:

/* VARIANTE DE SQL EXPLORER */


select distinct gen_id(OtroGenerador, 0) from RDB$GENERATORS

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:

create tabla Empleados (


Codigo numeric(9) not null, /* Lo mismo que numeric(9,0) */
Peso numeric(6,3), /* Peso en kilogramos y gramos */
Patrimonio numeric(15,2),
/* ... */
);

En la actual implementación de InterBase, numeric y decimal son totalmente equivalentes. La


precisión máxima permitida actualmente es de 15 dígitos, pero probablemente cambie en
futuras versiones.

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:

Precisión Tipo base Espacio necesario


1..4 SMALLINT 2 bytes
5..9 INTEGER 4 bytes
10..15 DOUBLE PRECISION 8 bytes

¿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:

create domain DomPrueba as numeric(5,2)

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:

CREATE DOMAIN DomPrueba AS


NUMERIC(9,2)

Si lo desea, puede ejecutar también la siguiente consulta:

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.

¿Conclusiones? Estas son las mías:

• 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.

TODO COMENZO POR...

...las actualizaciones en caché. Muchos programadores piensan que la principal utilidad de


las actualizaciones en caché es agrupar determinado número de actualizaciones y enviar-
las de golpe al servidor. Los manuales de Borland nos explican que de este modo ahorra-
mos espacio en la transmisión de datos al servidor, y que así podemos disminuir el tráfico
en red. Todo esto es verdad, pero no toda la verdad; ni siquiera es la parte más importante
de la verdad.

Probablemente, el mayor atractivo de las actualizaciones en caché para el programador


experto sea el uso del componente TUpdateSQL y del evento OnUpdateRecord. Estoy se-
guro de que alguna vez se habrá tropezado con uno de esos profetas que predican: "No
hay más Conjunto de Datos que TQuery". Casi siempre, sin embargo, a estos personajes
se les olvida aclarar qué condiciones son necesarias para que un componente TQuery se
comporte mejor que un TTable.

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

este problema, pero solamente funcionan para algunos controladores y en algunos


casos.

ACTUALIZACIONES EN CACHE, ¡AL RESCATE!

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:

REGLA NUMERO 1 PARA CONSULTAS


Las consultas que utilicemos para navegación y mantenimiento deben limitarse a pocos
registros.

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:

1. Activar las actualizaciones en caché (CachedUpdates := True).


2. Para quitarle el control al BDE sobre las instrucciones de actualización generadas,
hay que asociar un componente TUpdateSQL a la propiedad UpdateObject del
conjunto de datos; esto, si basta con una sola instrucción SQL para las actualiza-
ciones. Si el algoritmo de actualización es más complejo, debemos interceptar el
evento OnUpdateRecord.

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

REGLA NUMERO 2 PARA CONSULTAS


Los tipos duros no dejan a una consulta que genere sus propias instrucciones de actua-
lización. En vez de eso, activan las actualizaciones en caché y suministran componentes
TUpdateSQL, o interceptan el evento OnUpdateRecord.

CONTROL EN EL LADO SQL DE LA VIDA

Realmente, estamos usurpando en Delphi algunas atribuciones más apropiadas para que
ser desempeñadas por el servidor SQL.

create view PedidoCliente as


select P.Numero, P.Fecha, P.Enviado, C.Nombre
from Pedidos P inner join Clientes C
on (P.Cliente = C.Codigo)

Esta consulta no es actualizable, al menos en InterBase. Sin embargo, la columna Nombre


solamente cumple una función decorativa y podemos exigir que no se pueda modificar su
valor. En tal caso, existe una correspondencia biunívoca entre las filas de PedidoCliente y
de la tabla base Pedidos, considerando que todo pedido tiene un cliente asociado. Enton-
ces, una modificación sobre un registro de PedidoCliente puede traducirse automática-
mente en una modificación sobre Pedidos. ¿Cómo le indicamos a InterBase nuestras
"ideas" acerca de las actualizaciones sobre PedidoCliente? Sencillamente creando triggers
para la vista en cuestión:

set term !;

create trigger UpdPedidoCliente for PedidoCliente


active before update as
begin
update Pedidos
set Numero = new.Numero, Fecha = new.Fecha, Enviado = new.Enviado
where Numero = old.Numero and Fecha = old.Fecha and Enviado = old.Enviado;
end!

create trigger UpdPedidoCliente for PedidoCliente


active before delete as
begin
delete from Pedidos
where Numero = old.Numero and Fecha = old.Fecha and Enviado = old.Enviado;
end!

create exception InsercionPedidoCliente "No se puede insertar en esta vista"!

create trigger UpdPedidoCliente for PedidoCliente


active before insert as
begin
exception InsercionPedidoCliente;
end!

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 !;

create exception NoPermitida "Operación no permitida"!

create trigger UpdPedidoCliente for PedidoCliente


active before update as
begin
if (old.Numero <> new.Numero) then
exception NoPermitida;
102 Calling Dr. Marteens

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:

create trigger UpdPedidoCliente for PedidoCliente


active before update as
declare variable NuevoCliente integer;
begin
if (old.Numero <> new.Numero) then
exception NoPermitida;
update Pedidos
set Fecha = new.Fecha, Enviado = new.Enviado
where Numero = old.Numero;
if (old.Nombre <> new.Nombre) then
begin
select Codigo
from Clientes
where Nombre = new.Nombre
into :NuevoCliente;
if (NuevoCliente is null) then
exception ClienteNoExiste;
update Pedidos
set Cliente = :NuevoCliente
where Numero = old.Numero;
end
end!

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:

create trigger UpdPedidoCliente


instead of update on PedidoCliente
for each row
declare
NuevoCliente integer;
begin
if :old.Numero <> :new.Numero then
raise_application_error(-20001, 'Operación no permitida');
end if;
update Pedidos
set Fecha = :new.Fecha, Enviado = :new.Enviado
where Numero = :old.Numero;
if :old.Nombre <> :new.Nombre then
select Codigo
into NuevoCliente
from Clientes
where Nombre = :new.Nombre;
Vistas y triggers 103

if NuevoCliente is null then


raise_application_error(-20001, 'El cliente no existe');
end if;
update Pedidos
set Cliente = NuevoCliente
where Numero = :old.Numero;
end if;
end;
El bit y la espada

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.

Para no aburrir, necesitaba un componente con estas propiedades:

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

Precisamente, de lo que se trata ahora es de cómo dibujar eficientemente el rectángulo tri-


dimensional hundido que sirve de marco al control. ¿Fácil? Siempre se pueden dibujar una
a una las líneas que dan la sensación de profundidad: dos bordes en negro, otros dos en
gris oscuro, dos líneas internas en blanco, dos más en gris 25%... El problema no es sólo el
tedio del cálculo de las coordenadas. Cada vez que llamemos a una función del API de
Windows para trazar una línea o una polilínea, estaremos "cruzando" la frontera del GDI. Y
esto es algo costoso, que todo programador decente debe intentar evitar.

¿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!

Lamentablemente, el rectángulo que más se acerca a lo que deseamos se dibuja mediante


la siguiente llamada, pero su aspecto es diferente al de un TEdit desactivado:

R := Rect(100, 100, 221, 121);


DrawEdge(Canvas.Handle, R, BDR_SUNKENOUTER, BF_RECT);

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?

Para no prolongar el suspense, resulta que el borde es dibujado automáticamente por


Windows. ¿Cómo? Pues incluyendo un bit dentro de las opciones de estilo de la ventana.
Una ventana se crea en el API de Windows mediante una llamada a la función
CreateWindowEx:

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 y el cuarto parámetro controlan en gran medida la apariencia visual de la ventana.


En nuestro caso necesitamos pasar la constante WS_EX_CLIENTEDGE, que dibuja el
borde que deseamos y, adicionalmente, resta el espacio ocupado por el mismo a la super-
ficie cliente del control.

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:

procedure TimEuroLabel.CreateParams(var Params: TCreateParams);


begin
inherited CreateParams(Params);
Params.ExStyle := Params.ExStyle or WS_EX_CLIENTEDGE;
end;
El bit y la espada 107

Observe que cautelosamente llamamos a la implementación heredada del método; en defi-


nitiva, solamente queremos alterar un bit de la estructura de parámetros de creación. Y
esto me bastó para terminar el componente.

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:

procedure TWindowControl.CreateParams(var Params: TCreateParams);


begin
inherited CreateParams(Params);
Params.Style := WS_OVERLAPPED or WS_CHILD or WS_CAPTION or WS_THICKFRAME;
Params.ExStyle := Params.ExStyle or WS_EX_TOOLWINDOW;
end;

Luego, puede también interceptar el mensaje WM_NCPAINT del siguiente modo:

procedure TWindowControl.WMNCPaint(var Msg: TMessage);


var
DC: THandle;
R1, R2: TRect;
begin
inherited;
DC := GetWindowDC(Handle);
try
GetWindowRect(Handle, R2);
R1.Left := GetSystemMetrics(SM_CXBORDER) + GetSystemMetrics(SM_CXFRAME);
R1.Top := GetSystemMetrics(SM_CYFRAME);
R1.Right := R2.Right - R2.Left - R1.Left;
R1.Bottom := R1.Top + GetSystemMetrics(SM_CYSMSIZE);
SetBkColor(DC, GetSysColor(COLOR_ACTIVECAPTION));
SetTextColor(DC, GetSysColor(COLOR_CAPTIONTEXT));
FillRect(DC, R1, COLOR_ACTIVECAPTION + 1);
DrawText(DC, PChar(Caption), -1, R1, DT_CENTER or DT_VCENTER);
finally
ReleaseDC(Handle, DC);
end;
end;

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:

EL GRAN TRUCO DE LAS ETIQUETAS


Elimine el nombre de todos sus componentes TLabel. Una por una, seleccione las etiquetas
existentes y, con la ayuda del Inspector de Objetos, borre el contenido de la propiedad Name.

¿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 Label1: TLabel


Left = 164
Top = 108
Width = 32
Height = 13
Caption = 'Indice de Mala Leche Equivalente (%):'
end

Ahora la declaración se queda así:

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.

Cuando creemos la ventana en tiempo de ejecución también estaremos ahorrando espacio.


Aunque muchos programadores prefieren ignorarlo, lo cierto es que el contenido de las propie-
dades Name de sus componentes existe también en tiempo de ejecución; he ahí la importancia
de evitar componentes con nombres como ElCabronDeFulano ó QueBuenaEstaMengana.
¿Una etiqueta sin nombre? Espacio ahorrado en tiempo de ejecución.

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.

ADVERTENCIA PARA EL GRAN TRUCO DE LAS ETIQUETAS


En algún punto del programa, preferiblemente en el fichero de proyecto, antes de comenzar la
carga de ventanas, llame a la función RegisterClasses, mencionando todas las clases de com-
ponentes que han perdido su nombre y su alma.

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.

Pepe, llamémosle así piadosamente, decidido a hacer estallar mi aplicación, seleccionó


precisamente la casilla mencionada, después de bregar fatigosamente con el insumiso ra-
tón. Pulsó sobre el control y me miró con insolencia: "Oye, esto no calcula el seguro".
Contuve mis deseos de arrancarle la lengua y arrojarla a la perra del portero, y me concen-
tré en el monitor ... ¡oops! ... ahí pasaba algo muy raro. Efectivamente, la opción estaba
marcada, ¡pero el importe seguía siendo el mismo! Le arrebaté el ratón y cambié el estado
del control unas cuantas veces, pero aquello seguía más tieso que la momia de Lenin. Tras
unos insoportables segundos de sudores fríos, comprendí lo que pasaba. "Pepe, macho" -
le dije, dándole a su nombre la misma entonación que la usual en el adjetivo 'gilipollas' -
"tienes que pulsar la tabulación para pasar al siguiente control". Me miró con sorna, res-
pondiendo: "tú te confundiste también". Y en lo más íntimo de mi encéfalo tuve que recono-
cer que, por una vez en la vida, aquel especimen de usuario tenía razón...

¿CUANDO SE ACTUALIZA UN CAMPO?

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:

DEL CONTROL DATA-AWARE AL CAMPO


Los cambios realizados en un control de datos de la VCL tienen lugar, por lo general,
sólo cuando abandonamos el control.

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

¿COMO SE ACTUALIZA UN CAMPO?

Ya puestos, mostremos el código que realiza la asignación al campo en los componentes


mencionados. El siguiente método, por ejemplo, corresponde a un TDBEdit:

procedure TDBEdit.CMExit(var Message: TCMExit);


begin
try
FDataLink.UpdateRecord;
except
SelectAll;
SetFocus;
raise;
end;
SetFocused(False);
CheckCursor;
DoExit;
end;

La llamada al método UpdateRecord del FDataLink desencadena el proceso de asignación


al campo. Esta llamada provoca que el componente de clase TFieldDataLink enganchado
en FDataLink envíe una notificación a todos los controles asociados al mismo conjunto de
datos, para que todos ellos vuelquen sus contenidos en la fila activa. Los controles escu-
chan la notificación interceptando el evento interno OnUpdateData del FDataLink. El código
correspondiente en el editor es:

constructor TDBEdit.Create(AOwner: TComponent);


begin
inherited Create(AOwner);
// ... más instrucciones ...
FDataLink := TFieldDataLink.Create;
// ... muchas más instrucciones ...
FDataLink.OnUpdateData := UpdateData;
end;

procedure TDBEdit.UpdateData(Sender: TObject);


begin
ValidateEdit;
FDataLink.Field.Text := Text; // ¡Esto es lo que buscábamos!
end;

UNA SOLUCION PARA LOS CHECK BOXES

Mi solución preferida es crear un componente; cualquier otra implicaría añadir chapuzas


para simular el cambio del foco del teclado, al menos hasta donde tengo noticia. En el si-
guiente componente (un derivado de TCustomCheckBox) que simula parte del comporta-
miento de TDBCheckBox y añade algunas características adicionales, hay que redefinir el
método Toggle, que se ejecuta cada vez que cambia el estado del control:

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):

create procedure ExtremaImbecilidad


@a integer, @b integer, @c integer output as
select @c = @a + @b

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:

create procedure OtraIdiotez @v1 integer, @v2 integer as


begin
/* Declaración de variables locales */
declare @resultado integer
/* Llamamos al procedimiento */
execute ExtremaImbecilidad @v1, @v2, @resultado
/* Mostrar el resultado en la consola de SQL Server */
select 'El resultado es: ', @resultado
end

Ahora llame al último procedimiento directamente:

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.

La explicación: el tonto compilador de TransactSQL no verifica los prototipos de los procedi-


mientos que llamamos (muy en el estilo de una gente capaz de alumbrar un Visual Basic).
Cuando pasamos @resultado, el compilador lo acepta alegremente y mete en la pila el valor de
la variable, cuando lo que hace falte meter es su dirección:

execute ExtremaImbecilidad @v1, @v2, @resultado output

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:

¡La dirección, idiota, no te olvides de pasar la dirección!


Líneas
Algo tan sencillo como trazar una línea puede dar motivo para un artículo. Sobre todo
cuando lo intentamos en Windows. No pretendo contar todas las posibles combinaciones
de funciones y constantes que se pueden utilizar para este propósito. Mi objetivo es, sim-
plemente, destacar un par de curiosidades que me han venido a la mente hace poco más
de quince minutos.

¿PASARSE O QUEDARSE CORTO?

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:

procedure TCanvas.MoveTo(X, Y: Integer);


procedure TCanvas.LineTo(X, Y: Integer);

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:

WINDOWS SE QUEDA CORTO


Las líneas dibujadas con LineTo no incluyen el punto terminal pasado como parámetro.

El fragmento de código que muestro a continuación traza un segmento de línea desde el


punto (0, 0) hasta el punto (99, 99), en vez de llegar al (100, 100):

procedure TForm1.FormPaint(Sender: TObject);


begin
Canvas.MoveTo(0, 0);
Canvas.LineTo(100, 100);
end;

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.

MAS SOBRE LA POSICION ACTUAL

Los metodos presentados anteriormente (MoveTo y LineTo) encapsulan las siguientes fun-
ciones del API de Windows:

function MoveToEx(DC: HDC; X, Y: Integer; P: PPoint): Bool;


function LineTo(DC: HDC; X, Y: Integer): Bool;

Es interesante comprobar que el equivalente a MoveTo en el API es más potente que su


traducción a Delphi. En particular, MoveToEx permite pasar un puntero a una variable de
tipo TPoint, para recuperar la posición del cursor gráfico antes del movimiento.

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:

function GetCurrentPositionEx(DC: HDC; Point: PPoint): BOOL; stdcall;

LINEAS MAS COMPLEJAS

¿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.

Entonces es el momento de llamar a Polyline.

procedure TCanvas.Polyline(const Points: array of TPoint);

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.

La versión original de Polyline, en el API de Windows, es la siguiente:

function Polyline(DC: HDC; var Points; Count: Integer): Bool;

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.

PASANDO UNA LISTA DE PUNTOS

Si no ha trabajado antes con vectores de longitud dinámica en parámetros, puede que le


cueste un poco acostumbrarse a funciones como Polyline. Le muestro a continuación un
pequeño ejemplo que dibuja un hexágono en el área interior de una ventana:

procedure TForm1.FormPaint(Sender: TObject);


var
Pts: array [0..6] of TPoint;
R, PiDiv3, Sin, Cos: Extended;
I: Integer;
begin
PiDiv3 := Pi / 3;
Líneas 119

R := Min(ClientWidth, ClientHeight) div 2;


for I := 0 to 5 do
with Pts[I] do
begin
SinCos(I * PiDiv3, Sin, Cos);
X := Round(R * Cos) + ClientWidth div 2;
Y := Round(R * Sin) + ClientHeight div 2;
end;
Pts[6] := Pts[0];
Canvas.Polyline(Pts);
end;

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;

¿POR QUE POLYLINE, Y NO POLYGON?


Podíamos haber utilizado el método Polygon, y habernos ahorrado un punto. Pero
Polygon rellena el interior de la figura, mientras que Polyline no.

MAS DE UNA LINEA A LA VEZ

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.

La función en cuestión se llama (cacofónicamente) PolyPolyline. Lamentablemente, la


clase TCanvas no soporta directamente esta función, y tenemos que "bajar" al nivel del API
para poder emplearla. He aquí su declaración:

function PolyPolyline(DC: HDC; const PointStructs; const Points; p4: DWORD):


Bool;

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.

El siguiente ejemplo dibuja dos hexágonos en la superficie de un formulario. El segundo


está rotado 30 grados en relación con el primero:

procedure TForm1.FormPaint(Sender: TObject);


var
Pts: array [0..13] of TPoint;
Longitudes: array [0..1] of Cardinal;
R, PiDiv3, Sin, Cos: Extended;
I: Integer;
begin
PiDiv3 := Pi / 3;
R := Min(ClientWidth, ClientHeight) div 2;
// Preparar el primer polígono, de 7 puntos
Longitudes[0] := 7;
for I := 0 to 5 do
with Pts[I] do
begin
SinCos(I * PiDiv3, Sin, Cos);
X := Round(R * Cos) + ClientWidth div 2;
Y := Round(R * Sin) + ClientHeight div 2;
end;
Pts[6] := Pts[0];
// El segundo polígono, también de 7 puntos
Longitudes[1] := 7;
for I := 7 to 12 do
with Pts[I] do
begin
SinCos(I * PiDiv3 + PiDiv3 / 2, Sin, Cos);
X := Round(R * Cos) + ClientWidth div 2;
Y := Round(R * Sin) + ClientHeight div 2;
end;
Pts[13] := Pts[7];
// Dibujar el doble hexágono
PolyPolyline(Canvas.Handle, Pts[0], Longitudes, 2);
end;

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.

De modo que necesitamos memoria dinámica, pero el comportamiento de la misma se pa-


recerá al de la memoria automática (o local, o de pila, como prefiera denominarla): dentro
de un mismo procedimiento se reservará y se liberará. ¿Por qué no pedir memoria diná-
mica dentro de la propia pila del programa? Pues eso es lo que hace Delphi. Abra el fichero
grids.pas, del código fuente de la VCL, y busque las siguientes dos funciones:

function StackAlloc(Size: Integer): Pointer; register;


asm
{ Guarrerías en ensamblador }
end;

procedure StackFree(P: Pointer); register;


asm
{ Cochinadas misceláneas, que no vienen a cuento }
end;

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.

LA ESCENA DEL CRIMEN

Suponga que queremos desarrollar un simple y vulgar sistema de entradas de pedidos.


Tenemos un par de tablas: Cabecera y Detalles; y ya puede imaginar sus papeles. El gran
problema de la entrada de pedidos con Delphi y C++ Builder es que, si utilizamos controles
data-aware como TDBGrid y TDBEdit, estamos introduciendo los datos directamente den-
tro de la base de datos. Es decir: cada vez que usted o el usuario meta una línea dentro de
la rejilla y pase a la siguiente, se producirá una grabación.

Para simplificar la explicación, no entraré en la polémica acerca de si debemos utilizar


una tabla o una consulta para la entrada de datos. A lo largo del artículo utilizaré la
palabra tabla, pero todo lo que diga será también aplicable a las consultas. Realmente,
en este caso en que solamente se insertan registros, utilizaría consultas con las actuali-
zaciones en caché activadas, con objetos TUpdateSQL asociados, y con una cláusula
where imposible de satisfacer, para que devolviesen conjuntos de resultado vacíos.

¿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.

La mejor solución es utilizar actualizaciones en caché. A las dos tablas protagonistas de la


historia se les cambia su propiedad CachedUpdates a True. De este modo, cualquier cam-
bio en las mismas se refleja en una caché en el lado cliente, y los cambios se aplican pos-
teriormente, al confirmarse el pedido. Si se decide cancelar, basta con vaciar la caché, y no
habremos tocado la base de datos en ningún momento.

Casi siempre, el algoritmo de grabación de la caché tiene un aspecto similar al siguiente:

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;

El algoritmo de grabación de la caché es un protocolo en dos fases, que viene explicado en


casi cualquier libro sobre Delphi o C++ Builder. Aquí solamente analizaremos la siguiente
secuencia de operaciones, que produce un error que a muchos programadores toma por
sorpresa:

• 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” .

LOS MÓVILES DEL ASESINO

¿Cuál es la dificultad? El BDE obliga precisamente a un protocolo de aplicación en dos fa-


ses para que este tipo de problemas no se produzca:

• 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:

procedure TDataMod.DetallesUpdateError(DataSet: TDataSet;


E: EDatabaseError; UpdateKind: TUpdateKind;
var UpdateAction: TUpdateAction);
begin
Cabecera.Edit;
Cabecera['UnCampo'] := Cabecera['UnCampo'];
El Misterio de la Cabecera Perdida 125

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.

HOLMES LO ACLARA TODO

¿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.

Como sabemos, este componente tiene tres propiedades InsertSQL, ModifySQL y


DeleteSQL, que indican las instrucciones que tienen que ejecutarse sobre el servidor
cuando se graba la caché. En nuestro caso, solamente estamos considerando las altas de
pedidos, por lo que vamos únicamente a llenar la primera de las tres propiedades. Aunque
lo podemos hacer a mano, es preferible hacer doble clic sobre el TUpdateSQL y dejar que
el editor del componente genere las tres instrucciones por nosotros. No importa que se ge-
neren dos instrucciones adicionales que no utilizaremos. La sentencia SQL que queda en
InsertSQL es similar a la siguiente:

insert into Cabecera(Campo1, Campo2, ...)


values (:Campo1, :Campo2, ...)

Ahora, simplemente copie la instrucción de inserción dentro de la propiedad ModifySQL. De


esta forma, la actualización que erróneamente se genera tras la corrección efectuada en la
sección anterior, se traduce de todos modos en una inserción.

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:

procedure TDataMod.DetallesUpdateError(DataSet: TDataSet;


E: EDatabaseError; UpdateKind: TUpdateKind;
var UpdateAction: TUpdateAction);
begin
Cabecera.Edit;
Cabecera['UnCampo'] := Cabecera['UnCampo'];
FErrorPending := True;
end;

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:

procedure TDataMod.Table1UpdateRecord(DataSet: TDataSet;


UpdateKind: TUpdateKind; var UpdateAction: TUpdateAction);
begin
if (UpdateKind = ukModify) and FErrorPending then
126 Calling Dr. Marteens

UpdateSQL1.Apply(ukInsert)
else
UpdateSQL1.Apply(UpdateKind);
UpdateAction := uaApplied;
end;

EL CINE QUEDA VACÍO MIENTRAS PASAN LOS CRÉDITOS...

Visto en retrospectiva, parece fácil resolver el problema planteado. Pero personalmente me


costó su tiempo confirmar que, en efecto, se trataba de un bug interno del BDE y no un fa-
llo de la VCL. Debo también agradecer a Philip Cain, por haberme indicado la solución an-
terior en el grupo de noticias de Borland, y a José Luis Freire, que publicó inicialmente este
artículo.

(...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.

El principal problema que me produce el cambio constante de ordenador es que, teniendo


cada uno de ellos un nombre diferente, el Personal Web Server o el Internet Information
Server les asigna nombres de dominio distintos. En el texto HTML generado, en muchas
ocasiones, es necesario incluir el nombre completo del dominio. Es cierto también que las
rutas relativas dentro del dominio inicial pueden ser de utilidad. Pero a veces no funcionan
bien.

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.

Supongamos que el texto de cierta página que queremos generar es el siguiente:

<html>
<body>
<p>Visite nuestra <a href="$#!+/moreinfo.htm">página de información</a>.</p>
</body>
</html>

Observe que he utilizado la cadena $#!+ (una secuencia improbable, en circunstancias


normales) como sustituto del nombre de dominio. Dentro de la unidad del módulo de datos
defino dos constantes de alcance global:

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.

El siguiente y último paso es interceptar el evento AfterDispatch:

procedure TmodData.WebModuleAfterDispatch(Sender: TObject;


Request: TWebRequest; Response: TWebResponse; var Handled: Boolean);

127
128 Calling Dr. Marteens

begin
if CompareText(Response.ContentType, 'text/html') = 0 then
Response.Content := StringReplace(Response.Content, SDomainSymbol,
SDomain, [rfReplaceAll]);
end;

Tome nota de la comprobación del valor de la propiedad ContentType; nuestra aplicación


puede en ocasiones devolver un gráfico como resultado. La función StringReplace está de-
finida en la unidad SysUtils.
El Principio de Incertidumbre de
Heisenberg
Para aquellos que todavía dudan entre aceptar mi cordura o llegar a la conclusión de que
estoy como una coladera, ahí van mis últimas reflexiones sobre las leyes físicas del Uni-
verso y la Programación Orientada a Objetos. Tengo que confesar, en primer lugar, que
soy un físico frustrado. Un profesional frustrado es algo peligroso, sobre todo en el caso de
los informáticos; ahí está el ejemplo de B.G. ¡Cuántos horrores nos habríamos ahorrado si
una mano piadosa hubiera apartado en su infancia a este señor de los ordenadores (aun-
que fuese a collejas)!

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

Evidentemente, de lo que se trata es de diseñar una clase de objetos para representar la


parte de "cálculo" de un préstamo (un préstamo real tiene muchas otras interfaces que im-
plementar). No me alcanzaría el espacio para presentar también la implementación, pero
de lo que se trata, precisamente, es del diseño de la interfaz.

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

// Tabla de amortización (saldo, capital pagado, intereses)


property Balance[PmtNo: Word]: Currency;
property Principal[PmtNo: Word]: Currency;
property Interest[PmtNo: Word]: Currency;
end;

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;

y su método de escritura se implementa así:

procedure TisLoan.SetTerm(Value: Integer);


begin
if Value <> FTerm then
begin
FTerm := Value;
FDirty := True;
end;
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;

function TisLoan.RegularPmt: Currency;


begin
Recalculate;
Result := FRegularPmt;
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:

procedure TForm1.DBGrid1ColEnter(Sender: TObject);


begin
if DBGrid1.SelectedField = ElCampoSoloLectura then
DBGrid1.Options := DBGrid1.Options - [dgEditing]
else
DBGrid1.Options := DBGrid1.Options + [dgEditing];
end;

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:

• Antes de comenzar la carga, llame al método BeginUpdates de la propiedad Items del


control, y cuando haya terminado llame a su pareja: EndUpdates. La primera llamada
evita que se redibuje el control cada vez que se añade una nueva línea.
• El segundo truco es más importante, por ser menos conocido. Antes de empezar a
añadir filas, asigne a la propiedad AllocBy del TListView el número de registros que
tiene pensado cargar. Normalmente el conjunto de datos que se va a explorar contiene
un puñado de filas. ¡De no ser así no tendría mucho sentido utilizar TListView! De
modo que no debe ser traumático para Delphi el que utilicemos la función RecordCount
para saber con cuántos registros contamos.

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.

function TForm1.DirectorioAplicacion: string;


begin
Result := ExtractFilePath(ParamStr(0));
end;

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:

function TForm1.DirectorioAplicacion: string;


begin
Result := ExtractFilePath(Application.ExeName);
end;

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?

// Potencialmente incorrecta ...


function TForm1.DirectorioAplicacion: string;
var
Buffer: array [0..255] of Char;
begin
GetModuleFileName(0, Buffer, SizeOf(Buffer));
Result := ExtractFilePath(StrPas(Buffer));
end;

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.

Ahora lo importante es saber: ¿dónde pueden utilizarse estas etiquetas especiales? La


pregunta no es tan trivial como a primera vista aparenta ser. HTML no permite el uso de
etiquetas anidadas. No se permiten construcciones como la siguiente, porque la etiqueta
ETIQ2 aparece dentro de la etiqueta ETIQ1:

<!-- ¡¡¡ESTO NO LO PERMITE HTML!!! -->


<ETIQ1 <ETIQ2> PARAM1="XX" PARAM2="YY">

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:

<!-- ¡¡¡PERO ESTO SI LO PERMITE DELPHI!!! -->


<A HREF="<#DIRECTORIO>ayuda.htm">

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:

procedure TForm1.Button1Click(Sender: TObject);


var
F: TFont;
begin
F := Memo1.Font;
F.Style := [fsBold];
end;

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...

Ahora bien, traiga un componente TFontDialog, y sustituya el manejador de eventos anterior


por el siguiente:

procedure TForm1.Button1Click(Sender: TObject);


begin
FontDialog1.Font := Memo1.Font;
if FontDialog1.Execute then
Memo1.Font := FontDialog1.Font;
end;

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;

La implementación del método de escritura SetFont es la siguiente:

procedure TLoQueSea.SetFont(Value: TFont);


begin
FFont.Assign(Value);
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):

constructor TLoQueSea.Create(AOwner: TComponent);


begin
inherited Create(AOwner);
FFont := TFont.Create;
// ...
end;

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:

procedure TDialogo.FormClose(Sender: TObject;


var Action: TCloseAction);
begin
Action := caFree;
end;

Si se trata de un formulario que debe ejecutarse modalmente (como un cuadro de diálogo), el


código que utilizo habitualmente para crear el objeto, ejecutar el diálogo y permitir que se auto-
destruya es como el siguiente:

// ...
TDialogo.Create(nil).ShowModal;
// ...

Se supone que después de terminar la ejecución de la función ShowModal (porque el usuario


cierra el diálogo), el objeto recién creado se destruye. ¿Hasta aquí de acuerdo? Antes de des-
cubrir el truco con el evento OnClose, yo utilizaba esta otra variante, en la cual la destrucción
es explícita:

// ...
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.

¿Es realmente así? ¿Dónde nos hemos equivocado?

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:

function TListBoxStrings.Add(const S: string): Integer;


begin
Result := SendMessage(ListBox.Handle,
LB_ADDSTRING, 0, LongInt(PChar(S)));
if Result < 0 then
raise EOutOfResources.Create(SInsertLineError);
end;

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:

function TListBoxStrings.IndexOf(const S: string): Integer;


begin
Result := SendMessage(ListBox.Handle,
LB_FINDSTRINGEXACT, -1, LongInt(PChar(S)));
end;

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?

Defina una clase como la siguiente:

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

Que no cunda el pánico: el destructor sí se ejecuta. ¿Por qué no se muestra el mensaje?


Bueno, ¿quién ha dicho que no se muestra? El mensaje aparece en pantalla, pero desaparece
tan rápidamente que no nos percatamos de su existencia. Hagamos una prueba: sustituya la
llamada a ShowMessage por una llamada al método MessageBox de Application:

TObjetoExhibicionista::~TObjetoExhibicionista()
{
char buffer[255];

strcpy(buffer, "¡Adiós, mundo cruel!\nFirmado: ");


strcat(buffer, fName.c_str());
Application->MessageBox(buffer, "Last Famous Words",
MB_OK | MB_ICONEXCLAMATION);
}

¡Ahora sí debe aparecer el mensaje! La explicación es sencilla, y se basa en una afirmación


chocante, a primera vista:

DELPHI NO TIENE CUADROS DE DIALOGO


... si se entiende por "cuadro de diálogo" aquellos objetos de ventana cuya definición
visual se realiza mediante un fichero de recursos, y que se ejecutan mediante la función
del API CreateDialog.

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);
// ...
}

¿Puede ver ya la solución? ShowMessage crea un formulario y lo ejecuta llamando a


ShowModal. Pero cuando se ejecuta el destructor de la clase exhibicionista, el bucle de men-
sajes principal de la aplicación ya ha terminado. Lo que implica que el atributo FTerminate de
Application ya es verdadero. Por supuesto, el bucle modal termina apenas ha comenzado.
Aunque la ventana se muestra en pantalla, desaparece tan rápidamente que no nos damos
cuenta.

Por el contrario, TApplication::MessageBox llama a la función MessageBox del API de


Windows, que sí se implementa mediante un verdadero cuadro de diálogo.
ARTICULOS
Los individuos que faltan a la verdad pueden dividirse en tres grandes grupos:
los mentirosos, los grandes mentirosos y, finalmente, los matemáticos...
RELIGION WARS IN LILLIPUT
When you have to decide between tables and queries...

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?

The pros and cons of navigation

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.

The lethal weapon

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:

select * from Employee

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

The bad behaviour of queries

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 Prepare: select * from Employee


SQL Execute: select * from Employee

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.

Initially, tables do wrong...

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:

select rdb$owner_name, rdb$relation_name, rdb$system_flag,


rdb$view_blr, rdb$relation_id
from rdb$relations
where rdb$relation_name = 'employee'

Next instructions retrieve information about fields, indexes and validations:

select r.rdb$field_name, f.rdb$field_type, f.rdb$field_sub_type,


f.rdb$dimensions, f.rdb$field_length, f.rdb$field_scale,
f.rdb$validation_blr, f.rdb$computed_blr,
r.rdb$default_value, f.rdb$default_value, r.rdb$null_flag
from rdb$relation_fields r, rdb$fields f
where r.rdb$field_source = f.rdb$field_name and
r.rdb$relation_name = 'employee'
order by r.rdb$field_position asc

select i.rdb$index_name, i.rdb$unique_flag, i.rdb$index_type,


f.rdb$field_name
from rdb$indices i, rdb$index_segments f
where i.rdb$relation_name = 'employee' and
i.rdb$index_name = f.rdb$index_name
Religion Wars in Lilliput 155

order by i.rdb$index_id, f.rdb$field_position asc

select r.rdb$field_name, f.rdb$validation_blr, f.rdb$computed_blr,


r.rdb$default_value, f.rdb$default_value, r.rdb$null_flag
from rdb$relation_fields r, rdb$fields f
where r.rdb$field_source = f.rdb$field_name and
r.rdb$relation_name = 'employee'
order by r.rdb$field_position asc

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:

select * from Employee order by Emp_No asc

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?

... but you must admit it's getting better

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:

select * from Employee order by Emp_No desc

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.

How Locate is implemented

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:

procedure TForm1.Button1Click(Sender: TObject);


begin
Table1.Locate('EMP_NO', Edit1.Text, []);
end;

What the BDE does is to execute the following statement, without closing the active cursor:

select * from Employee where Emp_No = :1

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

procedure TForm1.Button1Click(Sender: TObject);


begin
Table1.Locate('LAST_NAME', Edit1.Text, [loPartialKey]);
end;

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!

select * from Employee where Last_Name = :1

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:

select * from Employee where Last_Name > :1 order by Last_Name asc

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.

Case insensitive search

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:

Last_Name >= 'Mac' and Last_Name < 'Mad'

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

If you can't say it with two words...

First of all, you need to understand this point:

• 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 ...

About the author


Ian Marteens is a guy with a serious schizophrenic behaviour. In his spare time, he likes to read books
like "The Golden Bough" and "Hamlet's Mill" (I've told you his problem was very serious). He is the
author of a book centred on database programming: "The Dark Side Of Delphi", currently published in
Spanish. Ian can be reached at ian@marteens.com.

También podría gustarte