Formularios Transparentes

 

Fecha: 05/Jun/99 (05/Jun/99)
Autor: Luis Sanz <hrst@ctv.es >

Revisado 26/Oct/2002


Estimado "Guille":

Como va a ser tu "cumple " dentro de poco, para festejarlo, te envío un regalillo, por si te interesa publicarlo. Se trata de un control OCX que permite hacer ventanas transparentes.

Es una cuestión que se repite bastante (por lo menos, en las News), y la solución que se da no es siempre la adecuada. Realmente, no sé para que lo pide la gente, pues una ventana transparente sirve para bastante poco. Pero, por petición popular...

Hay por lo menos cinco formas de resolverlo:

1. Hacerlo "bien": el estilo C:

Windows se basa, entre otras cosas, en ventanas. Que deben ser "informadas" de lo que ocurre: que se ha pulsado una tecla, o que se tiene que cerrar, o que se tiene que redibujar. Tras estos mensajes, la ventana ejecuta (o no) un código, y, posteriormente, desencadena (o no) los eventos de VB: KeyDown, QueryUnload, o Paint (en el ejemplo anterior).

Una forma de modificar el funcionamiento de una ventana es sustituir su código (la función de la ventana) por otro que escribamos nosotros. Entonces, lo que deberemos hacer será interceptar en mensaje correspondiente, ejecutar nuestro código, y luego devolver o modificar el mensaje, según nos convenga. Esto se llama "subclasificar" la ventana.

Desde VB5, hay una nueva palabra clave (AddressOf) que permite enviar la dirección de una función como parámetro, y que nos permite hacer lo antedicho. Pero el que pueda hacerse no quiere decir que VB sea la herramienta más adecuada para ello, por varios motivos. Por eso nos saltaremos esta forma de hacerlo, y se la dejaremos a los "gurús" del C.

Se puede hacer de una forma algo mas sencilla con un control subclasificador de terceras partes, pero sigue siendo una labor compleja. Por eso me la salto (aunque admito sugerencias).

2. "Pintar" el fondo en nuestra ventana:

Una forma de hacer una ventana que parezca transparente es que esta tenga, como fondo, lo que estaría por debajo. Básicamente, se trataría de poder hacer algo así como:

MiFormulario.PaintPicture Escritorio.Image, PosicionX, PosicionY

Pero, por desgracia, no se puede: ni existe el objeto Escritorio en VB (el objeto Screen no lo es), ni tiene propiedades Image o Picture. Por eso, habrá que intentar otro sistema.

La solución es la de siempre: usar el API de Windows, con el cual podremos acceder a la ventana de nivel superior (al "escritorio"), leer lo que está dibujado en ella, y "pintarlo" en nuestra ventana.

Un problema que tenemos es que hay que leerlo antes de presentar nuestra ventana (porque, una vez que esta esté visible, formará parte del "fondo" del escritorio). O sea que tendremos que leer el fondo, almacenarlo de alguna forma, y luego presentarlo cuando dibujemos la ventana.

Para eso precisaremos varias funciones del API de Windows:

Private Declare Function GetDC Lib "user32" (ByVal hwnd As Long) As Long
Private Declare Function CreateCompatibleDC Lib "gdi32" (ByVal hdc As Long) As Long
Private Declare Function DeleteDC Lib "gdi32" (ByVal hdc As Long) As Long

Con estas funciones manejamos "Contextos de Dispositivo" (DC) de ventanas: una especie de área donde van a parar las órdenes gráficas (textos, líneas, mapas de bits) para que luego Windows pueda presentarlos. Estos DC están preparados para uno u otro dispositivo. Muchos objetos tienen asociado un DC (por ejemplo, todas las ventanas), al que podemos acceder desde VB mediante su propiedad hDC. Pero en ocasiones hay ventanas con DC (como, por ejemplo, un TextBox) sin propiedad hDC; entonces podemos acceder a su DC mediante el API GetDC (no es igual que la propiedad hDC, pero para el caso, nos vale).

También podemos crear y destruir DCs, compatibles con un dispositivo de salida, que hagan de áreas de "almacenamiento intermedio": para almacenar temporalmente algo (como si fuese una propiedad Picture), o para agilizar la presentación. Para eso se usan la función (entre otras) CreateCompatibleDC. Para destruirlos (y almacenar recursos) se usa el API DeleteDC.

Nunca debe destruirse un DC obtenido con la propiedad hDC, o con el API GetDC.

Private Declare Function GetDesktopWindow Lib "user32" () As Long

Esta función nos dará acceso al la ventana de nivel superior (la que contiene lo que se ve en pantalla), y a su DC usando luego GetDC.

Private Declare Function CreateCompatibleBitmap Lib "gdi32" _
	(ByVal hdc As Long, ByVal nWidth As Long, ByVal nHeight As Long) As Long
Private Declare Function DeleteObject Lib "gdi32" (ByVal hObject As Long) As Long
Private Declare Function SelectObject Lib "gdi32" _
	(ByVal hdc As Long, ByVal hObject As Long) As Long

No nos basta con tener un DC: para poder "imprimir" en él, debemos crear un área de memoria en la que se almacene el resultado de las operaciones: una especie de mapa de bits en memoria. Hay que crearlo (or ejemplo, con CreateCompatibleBitmap) y asociarlo a este (con SelectObject).

Private Declare Function BitBlt Lib "gdi32" (ByVal hDestDC As Long, ByVal x As Long, ByVal y As Long, _
	ByVal nWidth As Long, ByVal nHeight As Long, ByVal hSrcDC As Long, ByVal xSrc As Long, ByVal ySrc As Long, _
	ByVal dwRop As Long) As Long

El API BitBlt es una función muy útil (y muy usada) que realiza varias de las cosas que hace PaintPicture (no todas), a mayor velocidad.

Ahora lo que haremos será:

- Creamos un DC compatible con nuestra ventana, que será el "área de almacenamiento". Para ello, declaramos una variable Long en la sección Declaraciones:

Private hMFondoDC as Long
Private hMBMP as Long

Y luego lo creamos, "rellenándolo" del fondo

Private Sub Form_Load()
Dim hLDC As Long, iLAncho As Long, iLAlto As Long
'Vemos el tamaño de la pantalla (en Pixels, la unidad del
'API); podría hacerse con funciones del API, pero es más
'cómodo usar el objeto Screen
With Screen
	iLAncho = .Width / .TwipsPerPixelX
	iLAlto = .Height / .TwipsPerPixelY
End With
'Creamos el DC
hMFondoDC = CreateCompatibleDC(hdc)
'Le asociamos un Bitmap; para eso, primero lo creamos
hMBMP = CreateCompatibleBitmap(hdc, iLAncho, iLAlto)
'Y luego lo seleccionamos
SelectObject hMFondoDC, hMBMP
'Obtenemos el DC de la ventana de nivel superior
hLDC = GetDC(GetDesktopWindow)
'Pintamos en nuestro DC el contenido del fondo
BitBlt hMFondoDC, 0, 0, iLAncho, iLAlto, hLDC, 0, 0, vbSrcCopy
End Sub

No hemos de olvidar destruir el DC creado

Private Sub Form_Unload(Cancel As Integer)
DeleteObject hMBMP
DeleteDC hMFondoDC
End Sub

Ahora, hemos de "pintar" nuestro formulario con el fondo. El problema es que sólo lo vamos a poder hacer con su "ventana cliente" (el interior de la ventana, sin incluir bordes, barra de título, menús, barras de herramientas... Y debemos, además, encontrar dichas coordenadas. No nos vale con comparar Height y ScaleHeight, y restarle el borde (obtenido con (Width – ScaleWidth) / 2), ya que puede haber barras de herramientas abajo, por ejemplo. Para eso, usaremos más estructuras y funciones del API:

Private Type POINTAPI
	x As Long
	y As Long
End Type
Private Type RECT
	P1 As POINTAPI
	P2 As POINTAPI
End Type
Private Declare Function GetWindowRect Lib "user32" (ByVal hwnd As Long, lpRect As RECT) As Long
Private Declare Function ScreenToClient Lib "user32" (ByVal hwnd As Long, lpPoint As POINTAPI) As Long

La función GetWindowRect devuelve una estructura (un tipo definido por el usuario) con las coordenadas de las cuatro esquinas de la pantalla, en Pixels. ScreenToClient convierte dichas coordenadas en coordenadas de la ventana (de su área cliente). Ahora el valor de estas será el inverso del borde izquierdo y superior.

Private Function MiraBorde(ByRef Punto As POINTAPI)
Dim R As RECT
GetWindowRect Forma.hwnd, R
ScreenToClient Forma.hwnd, R.P1
Punto.x = -R.P1.x
Punto.y = -R.P1.y
End Function

Hay que destacar cómo se ha declarado la estructura RECT: si usamos el visor API, lo que veremos es:

Private Type 
Left As Long
Top As Long
Right As Long
Bottom As Long
End Type

Pero nos da igual declarar una estructura como cuatro variables Long, que como dos POINTAPI (dos Long): el tamaño de esta será igual. Y es mas conveniente para usar luego ScreenToClient, que precisa una estructura POINTAPI.

Ahora lo que haremos será "pintar" el fondo guardado en nuestra ventana, en la coordenada adecuada. Lo haremos en el evento Paint del formulario (con Autoredraw = False, claro):

Private Sub Form_Paint()
Dim P As POINTAPI, iLLeft As Long, iLTop As Long
MiraBorde P
ScaleMode = vbPixels
With Screen
iLLeft = Left / .TwipsPerPixelX
iLTop = Top / .TwipsPerPixelY
End With
BitBlt hdc, 0, 0, ScaleWidth, ScaleHeight, hMFondoDC, _
P.x + iLLeft, P.y + iLTop, vbSrcCopy
End Sub

Con todo esto, se consigue una ventana cuyo fondo es igual al que "tiene debajo".

Pero aún así no funciona bien: podemos probar a desplazar la ventana, y vemos como el fondo no se modifica: con VB no podemos detectar cuando se mueve una ventana (y, por tanto, hemos de volver al sistema anterior: subclasificar la ventana para interceptar el mensaje WM_MOVE, como en el primer sistema). No sólo eso: el fondo que hemos "leído" queda como una imagen "fija": si abrimos otra ventana, esta no se verá a través de la anterior.

De todas formas, se pueden hacer aplicaciones curiosas: por ejemplo, se podría hacer un salvapantallas, donde apareciese un gusano que se comiese las ventanas. Es también una buena posibilidad para hacer un control tipo PictureBox transparente. En este caso, no sería preciso usar GetDesktopWindow ni GetDC: bastaría con usar la propiedad hDC del control o formulario contenedor. Como es sencillo, lo dejo como ejercicio para el que le guste experimentar.

3. Cambiar el estilo de la ventana a transparente:

La función del API SetWindowLong permite modificar algunas de las características (del estilo) de la ventana. Entre otras cosas, permite indicar una nueva función de la ventana (para subclasificarla). Pero vamos a modificar una característica mas sencilla: vamos a modificar el estilo de la ventana a transparente:

Private Declare Function SetWindowLong Lib "user32" Alias "SetWindowLongA" _
	(ByVal hwnd As Long, ByVal nIndex As Long, ByVal dwNewLong As Long) As Long
Private Const GWL_EXSTYLE = (-20)
Private Const WS_EX_TRANSPARENT = &H20&
Private Sub Form_Load()
SetWindowLong Me.hWnd, GWL_EXSTYLE, WS_EX_TRANSPARENT ' decía GWL_EXESTYLE
Move 0, 0
End Sub

Aunque es muy sencillo, por desgracia, no funciona bien. Se consigue que la ventana parezca transparente, pero no se impide el redibujado de esta, por lo que se consiguen efectos "raros". En el caso anterior, si se ejecuta el código y luego se pulsa donde debiera estar la barra, puede desplazarse... pero el fondo no se regenera. Además, la caja de control aparece o no según le viene bien. En resumen: es un buen intento, pero por sí sólo no basta. Para que funcionase bien habría que combinarlo con alguno de los sistemas anteriores.

4. Impedir el redibujado de la pantalla tras modificar su región:

Algunas funciones del API incluyen un parámetro que indica si la ventana debe o no redibujarse. Por ejemplo, la función SetWindowRgn

Private Declare Function SetWindowRgn Lib "user32" _
(ByVal hwnd As Long, ByVal hRgn As Long, ByVal bRedraw As Boolean) As Long

Es una función que puede modificar la región de una ventana (más o menos, la zona incluída con ella). Podemos crear regiones de formas irregulares (redondas, poligonales, etc.) y asignarlas (y se consiguen efectos muy curiosos). Pero además, el último parámetro indica que la ventana si la ventana debe "repintarse" o no tras la modificación. Esto podemos aprovecharlo nosotros: podemos asignar primero una región muy pequeña, luego asignarle una muy grande (sin que se repinte) y, ahora, dibujar en ella: parecerá que tenemos texto, o imágenes, sobre un fondo transparente.

Este sistema es sencillo, pero limitado: si se redibuja la pantalla por cualquier otro motivo (por ejemplo, con una orden Cls, o porque se produzca el evento Paint)) se va todo al garete: aparece el fondo "real" de la ventana. Pero puede servir para hacer efectos gráficos: por ejemplo, en el ejemplo frmHelice podemos pintar un texto o iconos por toda la pantalla.

En este ejemplo, además, se indica como usar otras funciones del API: para poner una ventana en primer plano, para poner textos inclinados, para pintar iconos... El código es algo largo, por lo que no voy a ponerlo aquí, pero creo que es bastante interesante.

5. Delimitar la región de la ventana:

En el punto anterior hemos visto que a una ventana se le puede asignar una región que no coincida con su forma. Las partes de la ventana fuera de dicha región serán como si estuviesen fuera de la pantalla: ni se verán, ni se verán tampoco los controles, etc., que estén en esa zona. Tampoco se podrá actuar sobre ella: por ejemplo, no responderá al ratón. A todos los efectos, es como si hubiésemos "recortado" la ventana.

Podemos crear regiones de formas variadas (elípticas, rectangulares, redondeadas, irregulares), y podemos combinarlas con la función del API CombineRgn. Las funciones que usaremos (hay muchas más) serán:

Private Declare Function DrawEdge Lib "user32" (ByVal hdc As Long, qrc As RECT, ByVal edge As Long, ByVal grfFlags As Long) As Long

Con esta función dibujaremos un marco (para hacer efecto 3D), sin tener que usar controles Line, ni la orden Line.

Private Declare Function GetClientRect Lib "user32" (ByVal hwnd As Long, lpRect As RECT) As Long

Private Declare Function GetWindowRect Lib "user32" (ByVal hwnd As Long, lpRect As RECT) As Long

Private Declare Function SetWindowRgn Lib "user32" (ByVal hwnd As Long, ByVal hRgn As Long, ByVal bRedraw As Boolean) As Long

Con estas, obtendremos la posición y tamaño de las ventanas, y convertiremos las escalas (de pantalla a ventana). Para ello, declaramos la estructura RECT como antes (como dos POINTAPI).

Private Declare Function CreateRectRgnIndirect Lib "gdi32" (lpRect As RECT) As Long

Private Declare Function CombineRgn Lib "gdi32" (ByVal hDestRgn As Long, ByVal hSrcRgn1 As Long, ByVal hSrcRgn2 As Long, ByVal nCombineMode As Long) As Long

Private Declare Function SetWindowRgn Lib "user32" (ByVal hwnd As Long, ByVal hRgn As Long, ByVal bRedraw As Boolean) As Long

Estas funciones son para crear regiones, para combinarlas, y para asignarlas a una ventana. Las regiones pueden combinarse de varias formas (como si usásemos operadores lógicos): AND (lo contenido en ambas regiones), OR (en una u otra, XOR (en ninguna de las dos), etc.

Conseguimos este resultado:

Tenemos una ventana "hueca", que contiene varios controles; pero entre ellos vemos lo que está debajo, y podemos "pincharlo", con lo que cambiará el foco y se ocultará nuestra ventana supuestamente transparente, salvo que establezcamos una posición "en lo alto" con el API SetWindowPos, como en el ejemplo anterior. Para hacer el código mas modular, se ha hecho un control de usuario, con sólo dos propiedades: Enabled (que indica si la ventana será "transparente" o no, y Efecto3D, que indica si el borde tendrá o no ese efecto.

Lo que haremos para ello será:

- Para recibir los eventos del formulario contenedor, tenemos una variable Form, que declaramos con WithEvents:

Private WithEvents Forma As Form

- Establecemos la referencia en el evento ReadProperties del control:

If Ambient.UserMode Then Set Forma = Parent

- El código "responsable" está en el procedimiento QuitaFondo, al que se le llama cuando se producen los eventos Load, Paint o Resize del formulario:

Private Sub QuitaFondo()
Dim hLWin As Long, hLClient As Long
Dim rLWin As RECT, rLClient As RECT
Dim iLAntScale As Long, iLBorde As Long
Dim P As POINTAPI
'Primero mira la ventana del formulario completo
With Forma
'Usa las propiedades del formulario y de Screen
rLWin.P2.x = .Width / Screen.TwipsPerPixelX
rLWin.P2.y = .Height / Screen.TwipsPerPixelY
'Crea la ventana cliente
iLAntScale = .ScaleMode
.ScaleMode = vbPixels
'Ahora crea la región de la ventana
hLWin = CreateRectRgnIndirect(rLWin)
'Mira el ancho y el alto
MiraBorde P
'PonBorde es una función que devuelve 2 o 0
'según sea el valor de la propiedad Efecto3D
iLBorde = PonBorde
'Pone las dimensiones del área cliente
rLClient.P1.x = P.x + iLBorde
rLClient.P1.y = P.y + iLBorde
rLClient.P2.x = .ScaleWidth + rLClient.P1.x - iLBorde * 2
rLClient.P2.y = .ScaleHeight + rLClient.P1.y - iLBorde * 2
‘Restaura la escala del formulario
.ScaleMode = iLAntScale
'Ahora crea la región de la ventana cliente
hLClient = CreateRectRgnIndirect(rLClient)
'Ahora combina las regiones
Call CombineRgn(hLClient, hLWin, hLClient, RGN_XOR)
'Revisa la colección Controls para combinar regiones
'de los controles contenidos
PonControles hLClient
Call SetWindowRgn(.hwnd, hLClient, True)
End With
End Sub
Private Sub PonControles(ByRef hLRegion As Long)
Dim C As Control
Dim hLWnd As Long
Dim rLControl As RECT
Dim hLControl As Long
Dim P As POINTAPI
'Mira el tamaño del borde (de la barra, etc.)
MiraBorde P
On Error Resume Next 'por controles sin propiedad hWnd
For Each C In Parent.Controls
hLWnd = C.hwnd
If Err = 0 Then
'Mira la ventana
GetWindowRect hLWnd, rLControl
'Convierte la escala
With rLControl
ScreenToClient Forma.hwnd, .P1
ScreenToClient Forma.hwnd, .P2
'Modifica las posiciones
.P1.x = .P1.x + P.x
.P1.y = .P1.y + P.y
.P2.x = .P2.x + P.x
.P2.y = .P2.y + P.y
End With
'Crea la región
hLControl = CreateRectRgnIndirect(rLControl)
'La combina con la anterior
CombineRgn hLRegion, hLRegion, hLControl, RGN_OR
End If
Err.Clear
Next
End Sub

- En resumen. Lo que se hace es crear una región que abarque toda la ventana, y otra para el "área cliente". Se combinan con XOR. Posteriormente se recorre la colección Parent.Controls, y se combinan con el resultado de la operación anterior, con OR.

Así conseguimos tener una ventana con un "agujero" que abarca todo el espacio no ocupado por controles (fijaros que los controles sin propiedad hWnd no son "respetados", aunque eso es sencillo de modificar).

Hemos visto pues cinco formas de hacer un formulario transparente. Cada una de ellas tendrá un uso:

- El primer sistema es el mas complejo, y lo reservaría para efectos complejos (y, a ser posible, usando un control subclasificador de terceras partes).

- El segundo sistema es mas limitado, pero sirve para "capturar" lo que está debajo de una ventana (y puede ser la mejor forma de conseguir un PictureBox o un UserControl

que parezca transparente).

- El tercer sistema (el estilo transparente), el mas sencillo, es el que consigue los resultados más variables, por lo que lo reservaría para ventanas ocultas que tuviesen que recibir eventos.

- El cuarto sistema, aunque limitado, es útil para efectos visuales (como un salvapantallas o una pantalla de presentación.

- El quinto sistema es el que tal vez consiga el mejor resultado visual, pero con la limitación de que las zonas "transparentes" no pueden recibir eventos (tal vez podría combinarse con un segundo formulario de estilo transparente).


ir al índice

Pulsando este link puedes bajar el código de ejemplo (Trcod.zip 18.4 KB)