Como limitar el ancho y alto de una ventana

 

Fecha: 21/Dic/1997 (recibido el 16/Dic/97)
Autor: Jordi Garcia Busquets


En esta colaboraci�n muestro c�mo limitar el ancho y alto de una ventana. Por el camino me parar� a explicar un par de cosas que es necesario saber para poder entender la soluci�n al problema.

Puede que la soluci�n que yo doy sea muy complicada, o que quiz�s hay alg�n m�todo m�s f�cil y r�pido. Puede que s�. (naturalmente, como t�rmino "f�cil" no incluyo el usar un control prefabricado que nos haga el trabajo. Eso lo sabe hacer cualquiera, siempre y cuando page el control en cuesti�n, claro est�. La soluci�n que yo doy la puede usar cualquiera que tenga un compilador de C++.)

Antes de todo, decir que parte del c�digo implementado en C++ se basa en la implementaci�n de la funci�n agCopyData de la librer�a apigid32.dll, escrita por Daniel Appelman con Visual C++ 4.0. Yo simplemente me he limitado a modificarla para adaptarla a mis necesidades y para ser creada con Borland C++ Builder.

Estar� encantado de recibir cualquier comentario referente a lo que explicar� a continuaci�n.

 

Nuestra primera tentativa

Empezemos por lo que todos har�amos. Nuestra primera tentativa ser�a programar el evento Resize del formulario, y detectar si el ancho y alto son superiores o inferiores a lo que queremos limitar, y si es as�, asignar a las propiedades height y width el ancho y alto l�mites.

Por ejemplo, cread un proyecto nuevo y en el form por defecto escrib�s en el evento Resize:


Private Sub Form_Resize()

If Form1.Height > 5000 Then Form1.Height = 5000
If Form1.Width > 5000 Then Form1.Width = 5000
If Form1.Height < 3000 Then Form1.Height = 3000
If Form1.Width < 3000 Then Form1.Width = 3000

End Sub


Pero si lo probais, enseguida se ve que con esto no vamos a ninguna parte, porque el efecto visual es muy pobre. Entonces, como hacerlo de forma que cuando nos pasemos de los l�mites, simplemente la ventana no crezca de tama�o, sin efectos visuales indeseados?

Recientemente me instal� el Borland C++ Builder, versi�n Trial. Comprobando los ejemplos que trae, encontr� uno llamado msgmap , que precisamente hace lo que se pretende, limitar el ancho y alto de la ventana. Y c�mo lo hace ? Pues interceptando el mensaje WM_GETMINMAXINFO. Yendo luego al fant�stico libro de D. Appelman, le� lo siguiente referente a este mensaje:



WM_GETMINMAXINFO

VB Declaration

Const WM_GETMINMAXINFO = &H24

Description

Sent to a window when Windows needs to determine the minimum or maximum sizes for the window.

Use with VB

None.

Use with Subclassing

Can be used to change the default minimum or maximum sizes for a window.

Parameter Description
wParam Not used. Set to zero
lParam A pointer to a MINMAXINFO data structure as defined in the comments section below.



Comments

The MINMAXINFO is defined as follows:

Type MINMAXINFO
    ptReserved As POINTAPI
    ptMaxSize As POINTAPI
    ptMaxPosition As POINTAPI
    ptMinTrackSize As POINTAPI
    ptMaxTrackSize As POINTAPI
End Type

ptMaxSize specifies the maximum width and height of the window. ptMaxPosition specifies the maximum position of the upper left corner of the window. ptMinTrackSize and ptMaxTrackSize specify the minimum and maximum width and height of the window when sized by dragging the borders.



Os habr�is dado cuenta tambi�n que los elementos de la estructura MINMAXINFO son de tipo POINTAPI. El tipo POINTAPI no es otra cosa que otra definici�n de una estructura compuesta de dos variables de tipo long: x e y. Estas dos variables indican las coordenadas de un punto en la pantalla. POINTAPI es una de las estructuras m�s usadas y t�picas, junto a la estructura RECT, de la programaci�n con API’s. La estructura POINTAPI tiene la siguiente forma:

Type POINTAPI
    x As Long
    y AS Long
End Type

Bueno. Todo esto es exactamente lo que est�bamos buscando, no? :-)

Para empezar, est� claro que si queremos interceptar el mensaje WM_GETMINMAXINFO en Visual Basic tendremos que echar mano de algun control que nos permita trabajar con subclassing.

"Quietorl ! Qu� me acabas de decir? Sub-qu�? " Si a�n no sabes lo que es subclassing, a continuaci�n te lo explico, de forma f�cil de entender. Si ya sabes en que consiste, puedes pasar directamente a la continuaci�n del ejemplo.


Subclassing

Antes de poder entender lo que es el subclassing hay que saber ciertas cosas sobre las ventanas.

Toda ventana (form en VB) del Windows pertenece a una clase, de la cual hereda las propiedades que la diferencian de las otras (los m�s puristas dir�an, con raz�n, que cualquier cosa que sale por la pantalla es una ventana, pero ser� mejor no entrar en detalles, o nos perderemos). Una de estas propiedades o caracter�sticas heredadas es un puntero a una funci�n de gesti�n de mensajes. Esta funci�n recibe el c�digo del mensaje y unos par�metros asociados al mensaje y se encarga de, en respuesta a esos mensajes, ejecutar ciertas acciones.

Con Visual Basic nosotros podemos programar ciertos eventos b�sicos, como puede ser el form_click, form_load, etc. Pero hay muchos eventos, much�simos, que el Visual Basic no detecta, y que a veces puede ser �til detectarlos para realizar cierta tarea. Por ejemplo, cuando pasamos con el mouse sobre un elemento de un men� sin seleccionarlo se genera un mensaje WM_MENUSELECT. La funci�n de la ventana recibe el mensaje, y el identificador de la opci�n de men� seleccionada, pero por defecto no tiene nada que ejecutar cuando esto sucede. Y como intuireis, no hay un evento llamado form1_wm_menuselect que nos lo detecte y permita programarlo.

La t�cnica de subclassing se basa en crear una funci�n de gesti�n de estos mensajes y anteponerla a la funci�n por defecto de la ventana. En los casos que nos interese tambi�n podremos llamar desde nuestra funci�n a la funci�n por defecto de la ventana, pas�ndole el mensaje a tratar.

En la primera de las figuras que siguen se muestra la situaci�n normal: cuando una ventana recibe un mensaje se llama a la funci�n por defecto. En la segunda, cuando se recibe el mensaje este se pasa a la funci�n que nos hemos definido, pudiendo, si nos interesa (indicado con puntos), llamar a la funci�n por defecto.


Figuras 1 y 2

En el ejemplo anterior, en la funci�n definida por el usuario podr�amos capturar el mensaje WM_MENUSELECT y en respuesta a �l mostrar en una barra de estatus el texto de los diferentes men�s que estamos recorriendo.

Por tanto, con subclassing lo que estamos haciendo es modificar el contenido de la estructura de datos asociada a la ventana, sustituyendo el valor del puntero que apunta a la funci�n por defecto por la direcci�n de memoria de nuestra funci�n.

Subclassing es pues una t�cnica poderosa, no solo porque permite modificar el comportamiento estandard de los controles y formularios, sino tambi�n porqu� permite responder a mensajes que no estan contemplados en los eventos que el Visual Basic nos permite gestionar.

El Visual Basic no permite directamente el uso de subclassing, pero se puede hacer usando controles de terceras compa��as. (Por citar algunos, MSGBLAST; DWSBC32D.OCX, de Desaware; MSGHOOK, el que aqu� se usa; etc.).


Continuemos

Despu�s de haber visto en qu� consiste, continuamos con lo nuestro. Usando en control shareware llamado Msghook interceptaremos el mensaje en cuesti�n (WM_GETMINMAXINFO), y colocaremos el c�digo necesario para impedir que la ventana crezca del m�ximo y m�nimo que le indiquemos. El control Msghook lleva el siguiente evento Message asociado:

Private Sub Msghook1_Message(ByVal MSG As Long, ByVal wp As Long, ByVal lp As Long, result As Long)

End Sub

C�mo habeis podido leer en la documentaci�n del mensaje WM_GETMINMAXINFO, existe el argumento Lparam que nos proporciona la direcci�n de memoria donde se encuentra la estructura MINMAXINFO. Pues bien, el Msghook nos proporciona dicha direcci�n en la variable lp de la cabecera del evento. Ahora lo que hay que hacer es acceder a las variables ptMaxTrackSize.x, ptMaxTrackSize.y, ptMinTrackSize.x y ptMinTrackSize.y, y cambiar su valor por los m�ximos y m�nimos, respectivamente, que nosotros deseamos de alto y ancho.

"Bueno, ahora te has pasado. C�mo pretendes acceder a tal posici�n de memoria y escribir en ella lo que te venga en gana, con Visual Basic ?", os preguntareis. C�mo que el Visual Basic no sirve para hacer tal tipo de cosas, lo que tendremos que hacer ser� crearnos una librer�a din�mica conteniendo una funci�n que nos de acceso a la posici�n de memoria, y poder escribir en ella el valor deseado.

En mi caso lo que hice fue crearme una DLL con Borland C++ Builder que contiene un procedimiento que realiza una copia de n bytes a partir de una posici�n de memoria a otra. La sint�xis de llamada al procedimiento desde Visual Basic es:

JGCopiarMem direcci�n_origen, direccion_destino, tama�o

Desde el Visual Basic declaro una variable tipo MINMAXINFO, obtengo la direcci�n de memoria de la variable, y llamo a la funci�n pas�ndole el puntero a la estructura asociada al evento (lp), la direcci�n de memoria de la variable y el tama�o a copiar (en nuestro caso, 40 bytes, que es lo que ocupa la estructura MINMAXINFO).

"Para el carro, Jordi. Ahora te est�s quedando conmigo otra vez. C�mo pretender�s obtener la direcci�n de memoria de una variable cualquiera de Visual Basic?". Sabes? Me gusta que me hagas esta pregunta! (qu� bueno que soy... ;-))


Obtener la posici�n de memoria de una variable en Visual Basic

Para realizar tal haza�a echaremos mano de una funci�n API que en principio no sirve para eso, pero que convenientemente modificada nos ir� de perlas. La funci�n se llama lstrcpy. He aqu� su declaraci�n:

Declare Function lstrcpy& Lib "kernel32" Alias "lstrcpyA" (ByVal lpString1 As String, ByVal lpString2 As String)

Lstrcpy es una funci�n que en principio se encarga de copiar el valor de una cadena (lpstring2) en otra cadena (lpstring1) y el valor que devuelve es un long que indica la direcci�n de memoria de la primera cadena. En cambio, cuando le pasamos la misma variable en ambos par�metros, no hace otra cosa que devolvernos la direcci�n de memoria del primer par�metro. O sea, un puntero a la variable.

Un ejemplo:

Dim puntero as Long ' Puntero a v
Dim v as integer
Dim s as string

puntero = Lstrcpy(v,v) ' asignar a puntero la direcci�n de v
' puntero ahora contiene la direcci�n de memoria donde se
' encuentra v

puntero = Lstrcpy(Byval s, Byval s) 'Los strings hay que pasarlos por valor

Si os fijais, aqu� hay algo que no cuadra, y es la declaraci�n de la funci�n. En la declaraci�n puesta anteriormente los par�metros son cadenas. En cambio ahora le he pasado una variable tipo entero. Eso es porque, si jugamos con la declaraci�n de la funci�n, aceptar� cualquier tipo de variable que le pasemos. La declaraci�n final queda as�:

Declare Function lstrcpy& Lib "kernel32" Alias "lstrcpyA" (lpString1 As Any, lpString2 As Any)

Ahora, pasemos lo que le pasemos, nos devolver� la direcci�n de memoria de la variable. Funciona con cualquier tipo de datos b�sico, y tambi�n con tipos definidos por el usuario.

Nota: Este truco es obra de Arthur W.Green. Truco publicado en el Visual Basic Tips & Tricks.


Creaci�n de la librer�a con C++

Ahora que sabemos obtener la direcci�n de memoria de una variable en Visual Basic, solo nos queda realizar la librer�a din�mica. Avanzar�, antes de todo y de forma resumida, c�mo ser� la llamada desde Visual Basic:


Private Sub Msghook1_Message(ByVal MSG As Long, ByVal wp As Long, ByVal lp As Long, result As Long)

Dim destino as MINMAXINFO
Dim destino_ptr as long

'Obtenemos un puntero a la estructura de la variable 'destino'
destino_ptr = lstrcpy (destino, destino)

' destino_ptr->estructura = lp->estructura
JGCopiarMem lp, destino_ptr, 40

'Modificamos ptMaxTrackSize y ptMinTrackSize
destino.ptMaxTrackSize.x = 500
destino.ptMaxTrackSize.y = 500
destino.ptMinTrackSize.x = 300
destino.ptMinTrackSize.y = 300
'Regravamos la variable MINMAXINFO de lp con los valores nuevos
JGCopiarMem destino_ptr, lp, 40

(.....)

Ahora que sabemos como tiene que ser usada, veamos el c�digo de la librer�a din�mica, hecha con Borland C++ Builder:

#include &ltmem.h>
//---------------------------------------------------------------------------
extern "C" __export void __stdcall JGCopiarMem(long , long , long);

void __stdcall JGCopiarMem(long source , long dest, long size)
{

long *l1, *l2;

l1=(long *)source;
l2=(long *)dest;
memmove( l2 , l1 , size);

}

Peque�o, pero mat�n. :-)

Fijaos como el procedimiento JGCopiarMem recibe tres longs, las dos direcciones y el tama�o. Declaramos dos punteros a long (l1 y l2) y asignamos las variables pasadas por valor a dichos punteros. Una vez esto, l1 y l2 apuntan a las direcciones de memoria de las dos estructuras MINMAXINFO que estamos tratando.

El procedimiento llama entonces a una instrucci�n de C++ llamada memmove que copia size posiciones de memoria desde la posici�n de memoria apuntada por l1 a la posicion de memoria apuntada por l2. Nada m�s y nada menos lo que queremos que el procedimiento haga.

M�s o menos, de manera gr�fica, se est� haciendo lo siguiente:

long *l1, *l2; //l1 i l2 son punteros a longs, pero adonde apuntan ahora ??






l1=(long *)source;    //l1 apunta a la variable MINMAXINFO origen
l2=(long *)dest;        //l2 apunta a la variable MINMAXINFO destino.
                                // Su contenido no nos interesa, pues ser� machacado






memmove( l2 , l1 , size); //Se realiza la copia de bytes






Cuando se vuelve al Visual Basic, la variable destino contiene los mismos datos que la origen.

Ahora ya s�lo queda por hacer una cosa: declarar el procedimiento en el proyecto en Visual Basic. La declaraci�n es:

Declare Sub JGCopiarMem Lib "c:\copymem.dll" (ByVal var1 As Long, ByVal var2 As Long, ByVal count As Long)

Fijaos que indico el paso de par�metros por valor con la palabra Byval, tal como el procedimiento de la librer�a los est� esperando. No hace falta que diga lo importante que es indicar bien el paso de parametros a las funciones en una DLL hechas con otros lenguajes. Si quereis m�s informaci�n al respecto, consultad el fichero VB4DLL.TXT que acompa�a al Visual Basic 4.0 32bits. En este ejemplo tambi�n hay que tener mucho cuidado con el n�mero de bytes que se copian, procurando que sea el tama�o exacto (o menor, en el peor de los casos) de la variable destino. Si no vamos con cuidado con esto �ltimo, podemos escribir en posiciones de memoria ocupadas por otros programas, y no veas la que se podr�a liar entonces (VB5 ha generado un error de protecci�n general en bla bla bla... te suena?).

Bueno, dejo que descanseis un rato. :-). Juntamente con este texto explicativo incluyo un ejemplo hecho con Visual Basic 5.0. El ejemplo contiene un form el cual tiene limitado el tama�o entre 300 y 200 p�xels. Os animo a que probeis la librer�a, copiando enteros, longs, o lo que se os ocurra.

Para que funcione tendreis que colocar la librer�a din�mica "copymem.dll" en el directorio raiz (o modificar la sentencia Declare) y instalar el control Msghook (ya sab�is, lo copi�is en el windows/system, y desde el Visual Basic 5.0 yendo a referencias lo registr�is).



**************************************************
Jordi Garcia Busquets
[email protected]
Estudiante de 3� de Inform�tica de Gesti�n
Universidad de Girona. Catalunya. Espa�a
**************************************************


ir al índice

Ejemplo con Visual Basic 5. (Bordesjg.zip 124 KB)

Nota: Este zip tiene una estructura de directorios, as� que si lo descomprimes con el PKZIP desde MS-DOS deber�s especificar -d para crear dichos directorios.