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
jordi@hades.udg.es
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.