Página principal
Artículos y trucos
Catálogo de productos
Ejemplos y descargas
Mis libros
Cursos de formación
Investigación y desarrollo
Libros recomendados
Mis páginas favoritas
Acerca del autor
 
En colaboración con Amazon
 
Intuitive Sight

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 ciertas 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,               // Rectángulo interior
      ClientWidth - Canvas.TextWidth(S) - 3, // Just. a la derecha
      2,                                     // Posición vertical
      S);                                    // El texto a mostrar
end;

Precisamente, de lo que se trata ahora es de cómo dibujar eficientemente el rectángulo tridimensional 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 también existe DrawEdge, ¡que permite dibujar un rectángulo tridimensional con muchas opciones!

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 flecha de despliegue con una llamada a DrawFrameControl, la lista desplegable es una instancia 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 superficie cliente del control.

El primer paso, si queremos utilizar este mecanismo para trazar el borde exterior, es renunciar 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;

Observe que cautelosamente llamamos a la implementación heredada del método; en definitiva, 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.