Formularios Transparentes

 

Fecha: 05/Jun/99 (05/Jun/99)
Autor: Luis Sanz <[email protected] >

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)