Formularios Transparentes
Fecha: 05/Jun/99 (05/Jun/99)
|
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 LongPrivate Declare Function CreateCompatibleDC Lib "gdi32" (ByVal hdc As Long) As LongPrivate 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 LongPrivate Declare Function DeleteObject Lib "gdi32" (ByVal hObject As Long) As LongPrivate 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 LongPrivate 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 ScreenWith ScreeniLAncho = .Width / .TwipsPerPixelXiLAlto = .Height / .TwipsPerPixelYEnd With'Creamos el DChMFondoDC = CreateCompatibleDC(hdc)'Le asociamos un Bitmap; para eso, primero lo creamoshMBMP = CreateCompatibleBitmap(hdc, iLAncho, iLAlto)'Y luego lo seleccionamosSelectObject hMFondoDC, hMBMP'Obtenemos el DC de la ventana de nivel superiorhLDC = GetDC(GetDesktopWindow)'Pintamos en nuestro DC el contenido del fondoBitBlt hMFondoDC, 0, 0, iLAncho, iLAlto, hLDC, 0, 0, vbSrcCopyEnd Sub
No hemos de olvidar destruir el DC creado
Private Sub Form_Unload(Cancel As Integer)DeleteObject hMBMPDeleteDC hMFondoDCEnd 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 POINTAPIx As Longy As LongEnd TypePrivate Type RECTP1 As POINTAPIP2 As POINTAPIEnd TypePrivate Declare Function GetWindowRect Lib "user32" (ByVal hwnd As Long, lpRect As RECT) As LongPrivate 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 RECTGetWindowRect Forma.hwnd, RScreenToClient Forma.hwnd, R.P1Punto.x = -R.P1.xPunto.y = -R.P1.yEnd Function
Hay que destacar c�mo se ha declarado la estructura RECT: si usamos el visor API, lo que veremos es:
Private TypeLeft As LongTop As LongRight As LongBottom As LongEnd 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 LongMiraBorde PScaleMode = vbPixelsWith ScreeniLLeft = Left / .TwipsPerPixelXiLTop = Top / .TwipsPerPixelYEnd WithBitBlt hdc, 0, 0, ScaleWidth, ScaleHeight, hMFondoDC, _P.x + iLLeft, P.y + iLTop, vbSrcCopyEnd 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 LongPrivate 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_EXESTYLEMove 0, 0End 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 LongDim rLWin As RECT, rLClient As RECTDim iLAntScale As Long, iLBorde As LongDim P As POINTAPI'Primero mira la ventana del formulario completoWith Forma'Usa las propiedades del formulario y de ScreenrLWin.P2.x = .Width / Screen.TwipsPerPixelXrLWin.P2.y = .Height / Screen.TwipsPerPixelY'Crea la ventana clienteiLAntScale = .ScaleMode.ScaleMode = vbPixels'Ahora crea la regi�n de la ventanahLWin = CreateRectRgnIndirect(rLWin)'Mira el ancho y el altoMiraBorde P'PonBorde es una funci�n que devuelve 2 o 0'seg�n sea el valor de la propiedad Efecto3DiLBorde = PonBorde'Pone las dimensiones del �rea clienterLClient.P1.x = P.x + iLBorderLClient.P1.y = P.y + iLBorderLClient.P2.x = .ScaleWidth + rLClient.P1.x - iLBorde * 2rLClient.P2.y = .ScaleHeight + rLClient.P1.y - iLBorde * 2�Restaura la escala del formulario.ScaleMode = iLAntScale'Ahora crea la regi�n de la ventana clientehLClient = CreateRectRgnIndirect(rLClient)'Ahora combina las regionesCall CombineRgn(hLClient, hLWin, hLClient, RGN_XOR)'Revisa la colecci�n Controls para combinar regiones'de los controles contenidosPonControles hLClientCall SetWindowRgn(.hwnd, hLClient, True)End WithEnd SubPrivate Sub PonControles(ByRef hLRegion As Long)Dim C As ControlDim hLWnd As LongDim rLControl As RECTDim hLControl As LongDim P As POINTAPI'Mira el tama�o del borde (de la barra, etc.)MiraBorde POn Error Resume Next 'por controles sin propiedad hWndFor Each C In Parent.ControlshLWnd = C.hwndIf Err = 0 Then'Mira la ventanaGetWindowRect hLWnd, rLControl'Convierte la escalaWith rLControlScreenToClient Forma.hwnd, .P1ScreenToClient 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.yEnd With'Crea la regi�nhLControl = CreateRectRgnIndirect(rLControl)'La combina con la anteriorCombineRgn hLRegion, hLRegion, hLControl, RGN_OREnd IfErr.ClearNextEnd 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).
Pulsando este link puedes bajar el c�digo de ejemplo (Trcod.zip 18.4 KB)