Curso de WinAPI
Curso de WinAPI
Requisitos previos
Para el
presente curso
supondré que
estás
familiarizado
con la
programación
en C y C++ y
también con las
aplicaciones y
el entorno Logo de Windows
Windows, al
menos al nivel de usuario. Sin embargo, no se requerirán muchos
más conocimientos.
El curso pretende ser una explicación de la forma en que se
realizan los programas en Windows usando el API. Las
explicaciones de las funciones y los mensajes del API son meras
traducciones del fichero de ayuda de WIN32 de Microsoft, y sólo se
incluyen como complemento.
Para empezar, vamos a ponernos en antecedentes. Veamos
primero algunas características especiales de la programación en
Windows.
Independencia de la máquina
Los programas Windows son independientes de la máquina en la
que se ejecutan (o al menos deberían serlo), el acceso a los
dispositivos físicos se hace a través de interfaces, y nunca se
accede directamente a ellos. Esta es una de las principales ventajas
para el programador, ya que no hay que preocuparse por el modelo
de tarjeta gráfica o de impresora, la aplicación funcionará con todas,
y será el sistema operativo el que se encargue de que así sea.
Recursos
Un concepto importante es el de recurso. Desde el punto de vista
de Windows, un recurso es todo aquello que puede ser usado por
una o varias aplicaciones. Existen recursos físicos, que son los
dispositivos que componen el ordenador, como la memoria, la
impresora, el teclado o el ratón y recursos virtuales o lógicos, como
los gráficos, los iconos o las cadenas de caracteres.
Por ejemplo, si nuestra aplicación requiere el uso de un puerto
serie, primero debe averiguar si está disponible, es decir, si existe y
si no lo está usando otra aplicación; y después lo reservará para su
uso. Esto es necesario porque este tipo de recurso no puede ser
compartido.
Lo mismo pasa con la memoria o con la tarjeta de sonido,
aunque son casos diferentes. Por ejemplo, la memoria puede ser
compartida, pero de una forma general, cada porción de memoria no
puede compartirse, (al menos en los casos normales, veremos que
es posible hacer aplicaciones con memoria compartida), y se trata
de un recurso finito. Las tarjetas de sonido, dependiendo del
modelo, podrán o no compartirse por varias aplicaciones. Otros
recursos como el ratón y el teclado también se comparten, pero se
asigna su uso automáticamente a la aplicación activa, a la que
normalmente nos referiremos como la que tiene el "foco", es decir, la
que mantiene contacto con el usuario.
Desde nuestro punto de vista, como programadores, también
consideramos recursos varios componentes como los menús, los
iconos, los cuadros de diálogo, las cadenas de caracteres, los
mapas de bits, los cursores, etc. En sus programas, el Windows
almacena separados el código y los recursos, dentro del mismo
fichero, y estos últimos pueden ser editados por separado,
permitiendo por ejemplo, hacer versiones de los programas en
distintos idiomas sin tener acceso a los ficheros fuente de la
aplicación.
Ventanas
La forma en que se presentan las aplicaciones Windows (al
menos las interactivas) ante el usuario, es la ventana. Supongo que
todos sabemos qué es una ventana: un área rectangular de la
pantalla que se usa de interfaz entre la aplicación y el usuario.
Cada aplicación tiene al menos una ventana, la ventana
principal, y todas las comunicaciones entre usuario y aplicación se
canalizan a través de una ventana. Cada ventana comparte el
espacio de la pantalla con otras ventanas, incluso de otras
aplicaciones, aunque sólo una puede estar activa, es decir, sólo una
puede recibir información del usuario.
Eventos
Los programas en Windows están orientados a eventos, esto
significa que normalmente los programas están esperando a que se
produzca un acontecimiento que les incumba, y mientras tanto
permanecen aletargados o dormidos.
Un evento puede ser por ejemplo, el movimiento del ratón, la
activación de un menú, la llegada de información desde el puerto
serie, una pulsación de una tecla...
Esto es así porque Windows es un sistema operativo multitarea,
y el tiempo del microprocesador ha de repartirse entre todos los
programas que se estén ejecutando. Si los programas fueran
secuenciales puros, esto no sería posible, ya que hasta que una
aplicación finalizara, el sistema no podría atender al resto.
Estructura de programa secuencial:
Estructura de programa por eventos:
Pro
yec
tos
D
ebido
a la
Programa secuencial compl Programa orientado a eventos
ejidad
de los programas Windows, normalmente los dividiremos en varios
ficheros fuente, que compilaremos por separado y enlazaremos
juntos.
Cada compilador puede tener diferencias, más o menos grandes,
a la hora de trabajar con proyectos. Sin embargo creo que no
deberías tener grandes dificultades para adaptarte a cada uno de
ellos.
En el presente curso trabajaremos con el compilador de
"Bloodshed", que es público y gratuito, y puede descargarse de
Internet en la siguiente URL: http://www.bloodshed.net/.
Para crear un proyecto Windows usando este compilador
elegiremos el menú "File/New Project". Se abrirá un cuadro de
diálogo donde podremos elegir el tipo de proyecto. Elegiremos
"Windows Application" y "C++ Project". A continuación pulsamos
"Aceptar".
El compilador crea un proyecto con un fichero C++, con el
esqueleto de una aplicación para una ventana, a partir de ahí
empieza nuestro trabajo.
Convenciones
En parte para que no te resulte muy difícil adaptarte a la
terminología de Windows, y a la documentación existente, y en parte
para seguir mi propia costumbre, en la mayoría de los casos me
referiré a componentes y propiedades de Windows con sus nombres
en inglés. Por ejemplo, hablaremos de "button", "check box", "radio
button", "list box", "combo box" o "property sheet", aunque algunas
veces traduzca sus nombre a español, por ejemplo, "dialog box" se
nombrará a menudo como "cuadro de diálogo".
Además hablaremos a menudo de ventanas "overlapped" o
superponibles, que son las ventanas corrientes. Para el término
"pop-up" he desistido de buscar una traducción.
También se usaran a menudo, con relación a "check boxes",
términos ingleses como checked, unchecked o grayed, en lugar de
marcado, no marcado o gris.
Owner-draw, es un estilo que indica que una ventana o control
no es la encargada de actualizarse en pantalla, esa responsabilidad
es transferida a la ventana dueña del control o ventana.
Para "bitmap" se usará a menudo la expresión "mapa de bits".
Controles
Los controles son la forma en que las aplicaciones Windows
intercambian datos con el usuario. Normalmente se usan dentro de
los cuadros de diálogo, pero en realidad pueden usarse en cualquier
ventana.
Existen bastantes, y los iremos viendo poco a poco, al mismo
tiempo que aprendemos a manejarlos.
Los más importantes y conocidos son:
El borde de la
ventana
Hay varios tipos,
dependiendo de que
estén o no activas las
opciones de cambiar el Partes de una ventana
tamaño de la ventana.
Se trata de un área estrecha alrededor de la ventana que permite
cambiar su tamaño (1).
Barra de título
Zona en la parte superior de la ventana que contiene el icono y el
título de la ventana, esta zona también se usa para mover la
ventana a través de la pantalla, y mediante doble clic, para cambiar
entre el modo maximizado y tamaño normal (2).
Caja de minimizar
Pequeña área cuadrada situada en la parte derecha de la barra
de título que sirve para disminuir el tamaño de la ventana. Antes de
la aparición del Windows 95 la ventana se convertía a su forma
icónica, pero desde la aparición del Windows 95 los iconos
desaparecieron, la ventana se oculta y sólo permanece un botón en
la barra de estado (3).
Caja de maximizar
Pequeña área cuadrada situada en la parte derecha de la barra
de título que sirve para agrandar la ventana para que ocupe toda la
pantalla. Cuando la ventana está maximizada, se sustituye por la
caja de restaurar (4).
Caja de cerrar
Pequeña área cuadrada situada en la parte derecha de la barra
de título que sirve para cerrar la ventana. (5)
Menú
O menú del sistema. Se trata de una ventana especial que
contiene las funciones comunes a todas las ventanas, también
accesibles desde las cajas y el borde, como minimizar, restaurar,
maximizar, mover, cambiar tamaño y cerrar. Este menú se despliega
al pulsar sobre la caja de control de menú.
Barra de menú
Zona situada debajo de la barra de título, contiene los menús de
la aplicación (7).
El área de cliente
Es la zona donde el programador sitúa los controles, y los datos
para el usuario. En general es toda la superficie de la ventana lo que
no está ocupada por las zonas anteriores (10).
Capítulo 2 Notación Húngara
La notación húngara es un sistema usado normalmente para
crear los nombres de variables, tipos y estructuras cuando se
programa en Windows. Es el sistema usado en la programación del
sistema operativo, y también por la mayoría de los programadores.
A veces también usaremos este sistema en algunos ejemplos de
este curso, pero sobre todo, nos ayudará a interpretar el tipo básico
al que pertenece cada estructura, miembro, o tipo definido.
Consiste en prefijos en minúsculas que se añaden a los nombres
de las variables, y que indican su tipo; en el caso de tipos definidos,
las letras del prefijo estarán en mayúscula. El resto del nombre
indica, lo más claramente posible, la función que realiza la variable o
tipo.
Prefijo Significado
b Booleano
c Carácter (un byte)
dw Entero largo de 32 bits sin signo (DOBLE WORD)
f Flags empaquetados en un entero de 16 bits
h Manipulador de 16 bits (HANDLE)
l Entero largo de 32 bits
lp Puntero a entero largo de 32 bits
lpfn Puntero largo a una función que devuelve un entero
lpsz Puntero largo a una cadena terminada con cero
n Entero de 16 bits
p Puntero a entero de 16 bits
pt Coordenadas (x, y) empaquetadas en un entero de 32 bits
rgb Valor de color RGB empaquetado en un entero de 32 bits
sz Cadena terminada en cero
u Sin signo (unsigned)
w Entero corto de 16 bits sin signo (WORD)
Ejemplos
nContador: la variable es un entero que se usará como contador.
szNombre: una cadena terminada con cero que almacena un
nombre.
bRespuesta: una variable booleana que almacena una
respuesta.
Ejemplos de tipos definidos por el API de Windows:
UINT: entero sin signo. Windows redefine los enteros para
asegurar que el tamaño en bits es siempre el mismo para todas las
variables del API.
LRESULT: entero largo usado como valor de retorno.
WPARAM: entero corto de 16 bits usado como parámetro.
LPARAM: entero largo de 32 bits usado como parámetro.
LPSTR: puntero largo a una cadena de caracteres. En el API de
32 bits no existen distinciones entre punteros largos y cortos, pero la
nomenclatura se conserva por compatibilidad.
LPCREATESTRUCT: puntero a una estructura
CREATESTRUCT.
Capítulo 3 Estructura de un
programa Windows GUI
Hay algunas diferencias entre la estructura de un programa
C/C++ normal, y la correspondiente a un programa Windows GUI.
Algunas de estas diferencias se deben a que los programas GUI
estás basados en mensajes, otros son sencillamente debidos a que
siempre hay un determinado número de tareas que hay que realizar.
// Ficheros include:
#include <windows.h>
// Prototipos:
LRESULT CALLBACK WindowProcedure(HWND, UINT, WPARAM,
LPARAM);
// Función de entrada:
int WINAPI WinMain(HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPSTR lpszCmdParam,
int nCmdShow)
{
// Declaración:
// Inicialización:
// Bucle de mensajes:
return Message.wParam;
}
// Definición de funciones:
Cabeceras
Lo primero es lo primero, para poder usar las funciones del API
de Windows hay que incluir al menos un fichero de cabecera, pero
generalmente no bastará con uno.
El fichero <windows.h> lo que hace es incluir la mayoría de los
ficheros de cabecera corrientes en aplicaciones GUI, pero podemos
incluir sólo los que necesitemos, siempre que sepamos cuales son.
Por ejemplo, la función WinMain está declarada en el fichero de
cabecera winbase.h.
Generalmente esto resultará incómodo, ya que para cada nueva
función, mensaje o estructura tendremos que comprobar, y si es
necesario, incluir nuevos ficheros. Es mejor usar windows.h
directamente.
Prototipos
Cada tipo (o clase) de ventana que usemos en nuestro programa
(normalmente sólo será una), o cada cuadro de diálogo (de estos
puede haber muchos), necesitará un procedimiento propio, que
deberemos declarar y definir. Siguiendo la estructura de un
programa C, esta es la zona normal de declaración de prototipos.
/* Inicialización: */
/* Estructura de la ventana */
wincl.hInstance = hInstance;
wincl.lpszClassName = "NUESTRA_CLASE";
wincl.lpfnWndProc = WindowProcedure;
wincl.style = CS_DBLCLKS;
wincl.cbSize = sizeof(WNDCLASSEX);
/* Usar icono y puntero por defecto */
wincl.hIcon = LoadIcon(NULL, IDI_APPLICATION);
wincl.hIconSm = LoadIcon(NULL, IDI_APPLICATION);
wincl.hCursor = LoadCursor(NULL, IDC_ARROW);
wincl.lpszMenuName = NULL;
wincl.cbClsExtra = 0;
wincl.cbWndExtra = 0;
wincl.hbrBackground = (HBRUSH)COLOR_BACKGROUND;
hwnd = CreateWindowEx(
0,
"NUESTRA_CLASE",
"Ejemplo 001",
WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT,
CW_USEDEFAULT,
544,
375,
HWND_DESKTOP,
NULL,
hThisInstance,
NULL
);
ShowWindow(hwnd, SW_SHOWDEFAULT);
/* Bucle de mensajes: */
while(TRUE == GetMessage(&mensaje, 0, 0, 0))
{
TranslateMessage(&mensaje);
DispatchMessage(&mensaje);
}
return mensaje.wParam;
}
Declaración
Inicialización
Bucle de mensajes
Este es el núcleo de la aplicación, como se ve en el ejemplo el
programa permanece en este bucle mientras la función GetMessage
retorne con un valor TRUE.
while(GetMessage(&mensajee, 0, 0, 0)) {
TranslateMessage(&mensaje);
DispatchMessage(&mensaje);
}
Nota:
Definición de funciones
En esta parte definiremos, entre otras cosas, los procedimientos
de ventana, que se encargan de procesar los mensajes que lleguen
a cada ventana.
Capítulo 4 El procedimiento de
ventana
Cada ventana tiene una función asociada, esta función se
conoce como procedimiento de ventana, y es la encargada de
procesar adecuadamente todos los mensajes enviados a una
determinada clase de ventana. Es la responsable de todo lo relativo
al aspecto y al comportamiento de una ventana.
Normalmente, estas funciones están basadas en una estructura
"switch" donde cada "case" corresponde aun determinado tipo de
mensaje.
Sintaxis
Implementación de procedimiento de
ventana simple
void InsertarMenu(HWND);
...
InsertarMenu(hWnd);
ShowWindow(hWnd, SW_SHOWDEFAULT);
...
Veamos cómo funciona "InsertarMenu".
La primera novedad son las variables del tipo HMENU. HMENU
es un tipo de manipulador especial para menús. Necesitamos dos
variables de este tipo, una para manipular la barra de menú,
hMenu1. La otra para manipular cada uno de los menús pop-up, en
este caso sólo uno, hMenu2.
De momento haremos una barra de menú con un único elemento
que será un menú pop-up. Después veremos como implementar
menús más complejos.
Para crear un menú usaremos la función CreateMenu, que crea
un menú vacío.
Para ir añadiendo ítems a cada menú usaremos la función
AppendMenu. Esta función tiene varios argumentos:
El primero es el menú donde queremos insertar el nuevo ítem.
El segundo son las opciones o atributos del nuevo ítem, por
ejemplo MF_STRING, indica que se trata de un ítem de tipo texto,
MF_SEPARATOR, es un ítem separador y MF_POPUP, indica que
se trata de un menú que desplegará un nuevo menú pop-up.
El siguiente parámetro puede tener distintos significados:
Sencillo, ¿no?.
Observa que hemos usado la macro LOWORD para extraer el
identificador del ítem del parámetro wParam. Después de eso, todo
es más fácil.
También se puede ver que hemos usado la misma función para
salir de la aplicación que para el mensaje WM_DESTROY: la
función PostQuitMessage.
Ejemplo 2
Este ejemplo contiene todo lo que hemos visto sobre los menús
hasta ahora.
Ficheros de recursos
Veamos ahora una forma más sencilla y más frecuente de
implementar menús.
Lo normal es implementar los menús desde un fichero de
recursos, el sistema que hemos visto sólo se usa en algunas
ocasiones, para crear o modificar menús durante la ejecución de la
aplicación.
Es importante adquirir algunas buenas costumbres cuando se
trabaja con ficheros de recursos.
#include "win003.h"
#include "win003.h"
Y a continuación escribimos:
Menu MENU
BEGIN
POPUP "&Principal"
BEGIN
MENUITEM "&Prueba", CM_PRUEBA
MENUITEM SEPARATOR
MENUITEM "&Salir", CM_SALIR
END
END
En un fichero de recursos podemos crear toda la estructura de
un menú fácilmente. Este ejemplo crea una barra de menú con una
columna "Principal", con dos opciones: "Prueba" y "Salir", y con un
separador entre ellas.
La sintaxis es sencilla, definimos el menú mediante una cadena
identificadora, sin comillas, seguida de la palabra MENU. Entre las
palabras BEGIN y END podemos incluir items, separadores u otras
columnas. Para incluir columas usamos una sentencia del tipo
POPUP seguida de la cadena que se mostrará como texto en el
menú. Cada POPUP se comporta del mismo modo que un MENU.
Los ítems se crean usado la palabra MENUITEM seguida de la
cadena que se mostrará en el menú, una coma, y el comando
asignado a ese ítem, que puede ser un número entero, o, como en
este caso, una macro definida.
Los separadores se crean usando MENUITEM seguido de la
palabra SEPARATOR.
Observarás que las cadenas que se muestran en el menú
contienen un símbolo & en su interior, por ejemplo "&Prueba". Este
símbolo indica que la siguiente letra puede usarse para activar la
opción del menú desde el teclado, usando la tecla [ALT] más la letra
que sigue al símbolo &. Para indicar eso, en pantalla, esa letra se
muestra subrayada, en este ejemplo "Prueba".
Ya podemos cerrar el cuadro de edición del fichero de recursos.
Para ver más detalles sobre el uso de este recurso puedes
consultar las claves: MENU, POPUP y MENUITEM.
HMENU hMenu;
...
hMenu = LoadMenu(hInstance, "Menu");
SetMenu(hWnd, hMenu);
O simplemente:
WNDCLASSEX wincl;
...
wincl.lpszMenuName = "Menu";
hwnd = CreateWindowEx(
0,
"NUESTRA_CLASE",
"Ejemplo 003",
WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT,
CW_USEDEFAULT,
544,
375,
HWND_DESKTOP,
LoadMenu(hInstance, "Menu"), /* Carga y
asignación de menú */
hInstance,
NULL
);
Ejemplo 3
Capítulo 6 Diálogo básico
Los cuadros de diálogo son la forma de ventana más habitual de
comunicación entre una aplicación Windows y el usuario. Para
facilitar la tarea del usuario a la hora de introducir datos, existen
varios tipos de controles, cada uno de ellos diseñado para un tipo
específico de información. Los más comunes son los "static", "edit",
"button", "listbox", "scroll", "combobox", "group", "checkbutton" y
"ratiobutton". A partir de Windows 95 se indrodujeron varios
controles nuevos: "updown", "listview", "treeview", "gauge", "tab" y
"trackbar".
En realidad, un cuadro de diálogo es una ventana normal,
aunque con algunas peculiaridades. También tiene su procedimiento
de ventana (procedimiento de diálogo), pero puede devolver un
valor a la ventana que lo invoque.
Igual que los menús, los cuadros de diálogo se pueden construir
durante la ejecución o a partir de un fichero de recursos.
Ficheros de recursos
La mayoría de los compiladores de C/C++ que incluyen soporte
para Windows poseen herramientas para la edición de recursos:
menús, diálogos, bitmaps, etc. Sin embargo considero que es
interesante que aprendamos a construir nuestros recursos con un
editor de textos, cada compilador tiene sus propios editores de
recursos, y no tendría sentido explicar cada uno de ellos. El
compilador que usamos "Dev C++", en su versión 4, tiene un editor
muy limitado y no aconsejo su uso. De hecho, en la versión actual
ya no se incluye, y los ficheros de recursos se editan en modo texto.
De modo que aprenderemos a hacer cuadros de diálogo igual
que hemos aprendido a hacer menús: usando el editor de texto.
Para el primer programa de ejemplo de programa con diálogos,
que será el ejemplo 4, partiremos de nuevo del programa del
ejemplo 1. Nuestro primer diálogo será muy sencillo: un simple
cuadro con un texto y un botón de "Aceptar".
Este es el código del fichero de recursos:
#include <windows.h>
#include "win004.h"
Menu MENU
BEGIN
POPUP "&Principal"
BEGIN
MENUITEM "&Diálogo", CM_DIALOGO
END
END
/* Identificadores */
/* Identificadores de comandos */
#define CM_DIALOGO 101
#define TEXTO 100
Procedimiento de diálogo
Como ya hemos dicho, un diálogo es básicamente una ventana,
y al igual que aquella, necesita un procedimiento asociado que
procese los mensajes que le sean enviados, en este caso, un
procedimiento de diálogo.
Sintaxis
BOOL CALLBACK DialogProc(
HWND hwndDlg, // manipulador del cuadro de diálogo
UINT uMsg, // mensaje
WPARAM wParam, // primer parámetro del mensaje
LPARAM lParam // segundo parámetro del mensaje
);
Nota:
Ejemplo 4
Capítulo 7 Control básico Edit
Tal como hemos definido nuestro diálogo en el capítulo 6, no
tiene mucha utilidad. Los diálogos se usan para intercambiar
información entre la aplicación y el usuario, en ambas direcciones.
El ejemplo 4 sólo lo hace en una de ellas.
En el capítulo anterior hemos usado dos controles (un texto
estático y un botón), aunque sin saber exactamente cómo
funcionan. En este capítulo veremos el uso del control de edición.
Un control edit es una ventana de control rectangular que
permite al usuario introducir y editar texto desde el teclado.
Cuando está seleccionado muestra el texto que contiene y un
cursor intermitente que indica el punto de inserción de texto. Para
seleccionarlo el usuario puede hacer un click con el ratón en su
interior o usar la tecla [TAB]. El usuario podrá entonces introducir
texto, cambiar el punto de inserción, o seleccionar texto para ser
borrado o movido usando el teclado o el ratón.
Un control de este tipo puede enviar mensajes a su ventana
padre mediante WM_COMMAND, y la ventana padre puede enviar
mensajes a un control edit en un cuadro de diálogo llamando a la
función SendDlgItemMessage. Veremos algunos de estos mensajes
en este capítulo, y el resto el capítulos más avanzados.
Fichero de recursos
Empezaremos definiendo el control edit en el fichero de
recursos, y lo añadiremos a nuestro dialogo de prueba.
#include <windows.h>
#include "win005.h"
Menu MENU
BEGIN
POPUP "&Principal"
BEGIN
MENUITEM "&Diálogo", CM_DIALOGO
END
END
case WM_INITDIALOG:
SetFocus(GetDlgItem(hDlg, ID_TEXTO));
return FALSE;
case WM_CREATE:
/* Inicialización de los datos de la aplicación
*/
strcpy(Datos.Texto, "Inicial");
case WM_INITDIALOG:
SendDlgItemMessage(hDlg, ID_TEXTO, EM_LIMITTEXT,
80, 0L);
Datos = (DATOS *)lParam;
SetDlgItemText(hDlg, ID_TEXTO, Datos->Texto);
SetFocus(GetDlgItem(hDlg, ID_TEXTO));
return FALSE;
Hemos añadido dos llamadas a dos nuevas funciones del API.
La primera es a SendDlgItemMessage, que envía un mensaje a un
control. En este caso se trata de un mensaje EM_LIMITTEXT, que
sirve para limitar la longitud del texto que se puede almacenar y
editar en el control edit. Es necesario que hagamos esto, ya que el
texto que puede almacenar nuestra estructura de datos está limitado
a 80 caracteres.
También hemos añadido una llamada a la función
SetDlgItemText, que hace exactamente lo que pretendemos:
cambiar el contenido del texto en el interior de un control edit.
case WM_COMMAND:
if(LOWORD(wParam) == IDOK)
{
GetDlgItemText(hDlg, ID_TEXTO, Datos->Texto,
80);
EndDialog(hDlg, FALSE);
}
return TRUE;
case WM_COMMAND:
switch(LOWORD(wParam)) {
case IDOK:
GetDlgItemText(hDlg, ID_TEXTO, Datos-
>Texto, 80);
EndDialog(hDlg, FALSE);
break;
case IDCANCEL:
EndDialog(hDlg, FALSE);
break;
}
return TRUE;
Ejemplo 5
Editar números
En muchas ocasiones necesitaremos editar valores de números
enteros en nuestros diálogos.
Para eso, el API tiene previstas algunas constantes y funciones,
(aunque no es así para números en coma flotante, para los que
tendremos que crear nuestros propios controles).
Bien, vamos a modificar nuestro ejemplo para editar valores
numéricos en lugar de cadenas de texto.
Fichero de recursos para editar enteros
Empezaremos añadiendo una constante al fichero de
identificadores: "win006.h":
case WM_CREATE:
/* Inicialización de los datos de la aplicación
*/
Datos.Numero = 123;
BOOL NumeroOk;
int numero;
...
case WM_COMMAND:
switch(LOWORD(wParam)) {
case IDOK:
numero = GetDlgItemInt(hDlg, ID_NUMERO,
&NumeroOk, FALSE);
if(NumeroOk) {
datos->Numero = numero;
EndDialog(hDlg, FALSE);
}
else
MessageBox(hDlg, "Número no válido",
"Error",
MB_ICONEXCLAMATION | MB_OK);
break;
Ejemplo 6
Capítulo 8 Control básico
ListBox
Los controles edit son muy útiles cuando la información a
introducir por el usuario es imprevisible o existen muchas opciones.
Pero cuando el número de opciones no es muy grande y son todas
conocidas, es preferible usar un control ListBox.
Ese es el siguiente control básico que veremos. Un ListBox
consiste en una ventana rectangular con una lista de cadenas entre
las cuales el usuario puede escoger una o varias.
El usuario puede seleccionar una cadena apuntándola y
haciendo clic con el botón del ratón. Cuando una cadena se
selecciona, se resalta y se envía un mensaje de notificación a la
ventana padre. También se puede usar una barra de scroll con los
listbox para desplazar listas muy largas o demasiado anchas para la
ventana.
Ficheros de recursos
Empezaremos definiendo el control listbox en el fichero de
recursos, y lo añadiremos a nuestro diálogo de prueba:
#include <windows.h>;
#include "win007.h"
Menu MENU
BEGIN
POPUP "&Principal"
BEGIN
MENUITEM "&Diálogo", CM_DIALOGO
END
END
DialogoPrueba DIALOG 0, 0, 118, 135
STYLE DS_MODALFRAME | WS_POPUP | WS_VISIBLE | WS_CAPTION
CAPTION "Diálogo de prueba"
FONT 8, "Helv"
BEGIN
CONTROL "Lista:", -1, "static",
SS_LEFT | WS_CHILD | WS_VISIBLE,
8, 9, 28, 8
CONTROL "", ID_LISTA, "listbox",
LBS_STANDARD | WS_CHILD | WS_VISIBLE | WS_TABSTOP,
9, 19, 104, 99
CONTROL "Aceptar", IDOK, "BUTTON",
BS_DEFPUSHBUTTON | BS_CENTER | WS_CHILD | WS_VISIBLE |
WS_TABSTOP,
8, 116, 45, 14
CONTROL "Cancelar", IDCANCEL, "BUTTON",
BS_PUSHBUTTON | BS_CENTER | WS_CHILD | WS_VISIBLE |
WS_TABSTOP,
61, 116, 45, 14
END
// Datos de la aplicación
typedef struct stDatos {
char Item[80];
} DATOS;
DialogBoxParam(hInstance, "DialogoPrueba",
hwnd,
DlgProc, (LPARAM)&Datos);
UINT indice;
...
case WM_COMMAND:
switch(LOWORD(wParam)) {
case IDOK:
indice = SendDlgItemMessage(hDlg, ID_LISTA,
LB_GETCURSEL, 0, 0);
SendDlgItemMessage(hDlg, ID_LISTA,
LB_GETTEXT, indice, (LPARAM)Datos->Item);
EndDialog(hDlg, FALSE);
break;
case IDCANCEL:
EndDialog(hDlg, FALSE);
break;
}
return TRUE;
Ejemplo 7
Capítulo 9 Control básico
Button
Los controles button simulan el comportamiento de un pulsador o
un interruptor. Pero sólo cuando se comportan como un pulsador los
llamaremos botones, cuando emulen interruptores nos referiremos a
ellos como checkbox o radiobutton.
Los botones se usan para que el usuario pueda ejecutar ciertas
acciones o para dar órdenes a una aplicación. En muchos aspectos,
funcionan igual que los menús, y de hecho, ambos generan
mensajes de tipo WM_COMMAND.
Se componen normalmente de una pequeña área rectangular
con un texto en su interior que identifica la acción que tienen
asociada.
En realidad ya hemos usado controles button en todos los
ejemplos anteriores, pero los explicaremos ahora con algo más de
detalle.
Ficheros de recursos
Empezaremos definiendo el control button en el fichero de
recursos, y lo añadiremos a nuestro dialogo de prueba:
#include <windows.h>
#include "win008.h"
Menu MENU
BEGIN
POPUP "&Principal"
BEGIN
MENUITEM "&Diálogo", CM_DIALOGO
END
END
case WM_INITDIALOG:
SetFocus(GetDlgItem(hDlg, ID_BOTON));
return FALSE;
case WM_COMMAND:
switch(LOWORD(wParam)) {
case ID_BOTON:
MessageBox(hDlg, "Se pulsó 'Nuestro
botón'", "Acción", MB_ICONINFORMATION|MB_OK);
break;
case IDOK:
EndDialog(hDlg, FALSE);
break;
case IDCANCEL:
EndDialog(hDlg, FALSE);
break;
}
return TRUE;
Ejemplo 8
Capítulo 10 Control básico
Static
Los static son el tipo de control menos interactivo, normalmente
se usan como información o decoración. Pero son muy importantes.
Windows es un entorno gráfico, y la apariencia forma una parte muy
importante de él.
Existen varios tipos de controles static, o mejor dicho, varios
estilos de controles static.
Dependiendo del estilo que elijamos para cada control static, su
aspecto será radicalmente distinto, desde una simple línea o cuadro
hasta un bitmap, un icono o un texto.
Cuando hablemos de controles static de tipo texto,
frecuentemente nos referiremos a ellos como etiquetas. Las
etiquetas pueden tener también una función añadida, como veremos
más adelante: nos servirán para acceder a otros controles usando el
teclado.
En realidad ya hemos usado controles static del tipo etiqueta
cuando vimos los controles edit, lisbox y button, pero de nuevo los
explicaremos ahora con más detalle.
Ficheros de recursos
Empezaremos definiendo varios controles static en el fichero de
recursos, y los añadiremos a nuestro dialogo de prueba, para
obtener un muestrario:
#include <windows.h>
#include "win009.h"
Menu MENU
BEGIN
POPUP "&Principal"
BEGIN
MENUITEM "&Diálogo", CM_DIALOGO
END
END
Ejemplo 9
Capítulo 11 Control básico
ComboBox
Los ComboBoxes son una combinación de un control Edit y un
Lisbox. Son los controles que suelen recordar las entradas que
hemos introducido antes, para que podamos seleccionarlas sin tener
que escribirlas de nuevo, en ese sentido funcionan igual que un
Listbox, pero también permiten introducir nuevas entradas.
Hay modalidades de ComboBox en las que el control Edit está
inhibido, y no permite introducir nuevos valores. En esos casos, el
control se comportará de un modo muy parecido al que lo hace un
Listbox, pero, como veremos más adelante, tienen ciertas ventajas.
Exiten tres tipos de ComboBoxes:
Ficheros de recursos
Para nuestro ejemplo incluiremos un control ComboBox de cada
tipo, así veremos las peculiaridades de cada uno:
#include <windows.h>
#include "win010.h"
Menu MENU
BEGIN
POPUP "&Principal"
BEGIN
MENUITEM "&Diálogo", CM_DIALOGO
END
END
/* Datos de la aplicación */
typedef struct stDatos {
char Lista[6][80]; // Valores de los comboboxes
char Item[3][80]; // Opciones elegidas
} DATOS;
int i;
static DATOS *Datos;
...
case WM_INITDIALOG:
Datos = (DATOS*)lParam;
// Añadir cadenas. Mensaje: LB_ADDSTRING
for(i = 0; i < 6; i++) {
SendDlgItemMessage(hDlg, ID_COMBOBOX1,
CB_ADDSTRING, 0, (LPARAM})Datos->Lista[i]);
SendDlgItemMessage(hDlg, ID_COMBOBOX2,
CB_ADDSTRING, 0, (LPARAM)Datos->Lista[i]);
SendDlgItemMessage(hDlg, ID_COMBOBOX3,
CB_ADDSTRING, 0, (LPARAM)Datos->Lista[i]);
}
SendDlgItemMessage(hDlg, ID_COMBOBOX1,
CB_SELECTSTRING,
(WPARAM)-1, (LPARAM)Datos->Item[0]);
SendDlgItemMessage(hDlg, ID_COMBOBOX2,
CB_SELECTSTRING,
(WPARAM)-1, (LPARAM)Datos->Item[1]);
SendDlgItemMessage(hDlg, ID_COMBOBOX3,
CB_SELECTSTRING,
(WPARAM)-1, (LPARAM)Datos->Item[2]);
SetFocus(GetDlgItem(hDlg, ID_COMBOBOX1));
return FALSE;
case WM_COMMAND:
switch(LOWORD(wParam)) {
case IDOK:
// En el ComboList Simple usaremos:
GetDlgItemText(hDlg, ID_COMBOBOX1, Datos-
>Item[0], 80);
// En el ComboList DropDown usaremos:
SendDlgItemMessage(hDlg, ID_COMBOBOX2,
WM_GETTEXT,
80, (LPARAM)Datos->Item[1]);
// En el ComboList DropDownList usaremos:
indice = SendDlgItemMessage(hDlg,
ID_COMBOBOX3,
CB_GETCURSEL, 0, 0);
SendDlgItemMessage(hDlg, ID_COMBOBOX3,
CB_GETLBTEXT, indice, (LPARAM)Datos-
>Item[2]);
wsprintf(resultado, "%s\n%s\n%s",
Datos-Item[0], Datos-Item[1], Datos-
>Item[2]);
MessageBox(hDlg, resultado, "Leido",
MB_OK);
EndDialog(hDlg, FALSE);
return TRUE;
case IDCANCEL:
EndDialog(hDlg, FALSE);
return FALSE;
}
break;
}
/* Datos de la aplicación */
typedef struct stDatos {
int nCadenas;
char Lista[MAX_CADENAS][80];
char Item[3][80];
} DATOS;
Datos.nCadenas = 6;
Ejemplo 10
Capítulo 12 Control básico
Scrollbar
Veremos ahora el siguiente control básico: la barra de
desplazamiento o Scrollbar.
Las ventanas pueden mostrar contenidos que ocupan más
espacio del que cabe en su interior, cuando eso sucede se suelen
agregar unos controles en forma de barra que permiten desplazar el
contenido a través del área de la ventana de modo que el usuario
pueda ver las partes ocultas del documento.
Pero las barras de scroll pueden usarse para introducir otros
tipos de datos en nuestras aplicaciones, en general, cualquier
magnitud de la que sepamos el máximo y el mínimo, y que tenga un
rango valores finito. Por ejemplo un control de volumen, de 0 a 10, o
un termostato de -15º a 60º.
Las barras de desplazamiento tienen varias partes o zonas
diferenciadas, cada una con su función particular. Me imagino que
ya las conoces, pero las veremos desde el punto de vista de un
programador.
Una barra de desplazamiento consiste en un rectángulo
sombreado con un botón de flecha en cada extremo, y una caja en
el interior del rectángulo (llamado normalmente thumb). La barra de
desplazamiento representa la longitud o anchura completa del
documento, y la caja interior la porción visible del documento dentro
de la ventana. La posición de la caja cambia cada vez que el usuario
desplaza el documento para ver diferentes partes de él. También se
modifica el tamaño de la caja para adaptarlo a la proporción del
documento que es visible. Cuanta más porción del documento
resulte visible, mayor será el tamaño de la caja, y viceversa.
Hay dos modalidades de ScrollBars: horizontales y verticales.
El usuario puede desplazar el contenido de la ventana pulsando
uno de los botones de flecha, pulsando en la zona sombreada no
ocupada por el thumb, o desplazando el propio thumb. En el primer
caso se desplazará el equivalente a una unidad (si es texto, una
línea o columna). En el segundo, el contenido se desplazará en la
porción equivalente al contenido de una ventana. En el tercer caso,
la cantidad de documento desplazado dependerá de la distancia que
se desplace el thumb.
Hay que distinguir los controles ScrollBar de las barras de
desplazamiento estándar. Aparentemente son iguales, y se
comportan igual, los primeros están en el área de cliente de la
ventana, pero las segundas no, éstas se crean y se muestran junto
con la ventana. Para añadir estas barras a tu ventana, basta con
crearla con los estilos WS_HSCROLL, WS_VSCROLL o ambos.
WS_HSCROLL añade una barra horizontal y WS_VSCROLL una
vertical.
Un control scroll bar es una ventana de contol de la clase
SCROLLBAR. Se pueden crear tantas barras de scroll como se
quiera, pero el programador es el encargado de especidicar el
tamaño y la posición de la barra.
Ficheros de recursos
Para nuestro ejemplo incluiremos un control ScrollBar de cada
tipo, aunque en realidad son casi idénticos en comportamiento:
#include <windows.h>
#include "win011.h"
Menu MENU
BEGIN
POPUP "&Principal"
BEGIN
MENUITEM "&Diálogo", CM_DIALOGO
END
END
DialogoPrueba DIALOG 0, 0, 189, 106
STYLE DS_MODALFRAME | WS_POPUP | WS_VISIBLE | WS_CAPTION |
WS_SYSMENU
CAPTION "Scroll bars"
FONT 8, "Helv"
BEGIN
CONTROL "ScrollBar1", ID_SCROLLH, "SCROLLBAR",
SBS_HORZ | WS_CHILD | WS_VISIBLE,
7, 3, 172, 9
CONTROL "Scroll 1:", -1, "STATIC",
SS_LEFT | WS_CHILD | WS_VISIBLE,
24, 18, 32, 8
CONTROL "Edit1", ID_EDITH, "EDIT",
ES_LEFT | WS_CHILD | WS_VISIBLE | WS_BORDER |
WS_TABSTOP,
57, 15, 32, 12
CONTROL "ScrollBar2", ID_SCROLLV, "SCROLLBAR",
SBS_VERT | WS_CHILD | WS_VISIBLE,
7, 15, 9, 86
CONTROL "Scroll 2:", -1, "STATIC",
SS_LEFT | WS_CHILD | WS_VISIBLE,
23, 41, 32, 8
CONTROL "Edit2", ID_EDITV, "EDIT",
ES_LEFT | WS_CHILD | WS_VISIBLE | WS_BORDER |
WS_TABSTOP,
23, 51, 32, 12
CONTROL "Aceptar", IDOK, "BUTTON",
BS_PUSHBUTTON | BS_CENTER | WS_CHILD | WS_VISIBLE |
WS_TABSTOP,
40, 87, 50, 14
END
Para ver cómo funcionan las barras de scroll hemos añadido dos
controles Edit, que mostrarán los valores seleccionados en cada
control ScrollBar. Para más detalles acerca de estos controles ver
control scrollbar.
Ahora veremos más cosas sobre los estilos de los controles
ScrollBar:
...
case WM_INITDIALOG:
Datos = (DATOS*)lParam;
sih.nPos = Datos->ValorH;
siv.nPos = Datos->ValorV;
SetScrollInfo(GetDlgItem(hDlg, ID_SCROLLH),
SB_CTL, &sih, TRUE);
SetDlgItemInt(hDlg, ID_EDITH, (UINT)Datos-
>ValorH, FALSE);
SendDlgItemMessage(hDlg, ID_SCROLLV,
SBM_SETSCROLLINFO,
(WPARAM)TRUE, (LPARAM)&siv);
SetDlgItemInt(hDlg, ID_EDITV, (UINT)Datos-
>ValorV, FALSE);
return FALSE;
switch(Codigo) {
case SB_BOTTOM:
Pos = 0;
break;
case SB_TOP:
Pos = 100;
break;
case SB_LINERIGHT:
Pos++;
break;
case SB_LINELEFT:
Pos--;
break;
case SB_PAGERIGHT:
Pos += 5;
break;
case SB_PAGELEFT:
Pos -= 5;
break;
case SB_THUMBPOSITION:
case SB_THUMBTRACK:
Pos = Posicion;
case SB_ENDSCROLL:
break;
}
if(Pos < 0) Pos = 0;
if(Pos > 100) Pos = 100;
SetDlgItemInt(hDlg, ID_EDITH, (UINT)Pos, FALSE);
SetScrollPos(Control, SB_CTL, Pos, TRUE);
}
Como puede observarse, actualizamos el valor de la posición
dependiendo del código recibido. Nos aseguramos de que está
dentro de los márgenes permitidos y finalmente actualizamos el
contenido del control edit. La función para el scrollbar vertical es
análoga, pero cambiando los identificadores de los códigos y los
valores de los límites.
Hemos usado una función nueva, GetScrollPos para leer la
posición actual del thumb.
switch(Codigo) {
case SB_BOTTOM:
si.nPos = si.nMin;
break;
case SB_TOP:
si.nPos = si.nMax;
break;
case SB_LINEDOWN:
si.nPos++;
break;
case SB_LINEUP:
si.nPos--;
break;
case SB_PAGEDOWN:
si.nPos += si.nPage;
break;
case SB_PAGEUP:
si.nPos -= si.nPage;
break;
case SB_THUMBPOSITION:
case SB_THUMBTRACK:
si.nPos = Posicion;
case SB_ENDSCROLL:
break;
}
if(si.nPos < si.nMin) si.nPos = si.nMin;
if(si.nPos > si.nMax-si.nPage+1) si.nPos = si.nMax-
si.nPage+1;
SendDlgItemMessage(hDlg, ID_SCROLLH,
SBM_GETSCROLLINFO, 0, (LPARAM)&siv);
Devolver valores a la aplicación
Cuando el usuario ha decidido que los valores son los
adecuados pulsará el botón de Aceptar. En ese momento
deberemos capturar los valores de los controles scroll y actualizar la
estructura de parámetros.
También en este caso podemos usar un mensaje o una función
para leer la posición del thumb del scrollbar. La función ya la hemos
visto un poco más arriba, se trata de GetScrollPos. El mensaje es
SBM_GETPOS, ambos devuelven el valor de la posición actual del
thumb del control.
Usaremos los dos métodos, uno con cada control. El lugar
adecuado para leer esos valores sigue siendo el tratamiento del
mensaje WM_COMMAND:
case WM_COMMAND:
switch(LOWORD(wParam)) {
case IDOK:
Datos->ValorH =
GetScrollPos(GetDlgItem(hDlg, ID_SCROLLH), SB_CTL);
Datos->ValorV = SendDlgItemMessage(hDlg,
ID_SCROLLV,
SBM_GETPOS, 0, 0);
EndDialog(hDlg, FALSE);
return TRUE;
case IDCANCEL:
EndDialog(hDlg, FALSE);
return FALSE;
Ejemplos 11 y 12
Capítulo 13 Control básico
Groupbox
Los GroupBoxes son un estilo de botón que se usa para agrupar
controles. Generalmente se usan con controles RadioButton, pero
se pueden agrupar controles de cualquier tipo.
El comportamiento es puramente estático, es decir, actúan sólo
como marcas y facilitan al usuario el acceso a distintos grupos de
controles asociados en función de alguna propiedad común.
Ficheros de recursos
Para comprobar cómo funcionan los groupboxes agruparemos
dos conjuntos de controles edit:
#include <windows.h>
#include "win013.h"
Menu MENU
BEGIN
POPUP "&Principal"
BEGIN
MENUITEM "&Diálogo", CM_DIALOGO
END
END
Ejemplo 13
Capítulo 14 Control básico
Checkbox
En realidad no se trata más que de otro estilo de botón.
Normalmente, los CheckBoxes pueden tomar dos valores,
encendido y apagado. Aunque también exiten Checkbox de tres
estados, en ese caso, el tercer estado corresponde al de inhibido.
Los CheckBoxes se usan típicamente para leer opciones que sólo
tienen dos posibilidades, del tipo cuya respuesta es sí o no,
encendido o apagado, verdadero o falso, etc.
Aunque a menudo se agrupan, en realidad los checkboxes son
independientes, cada uno suele tomar un valor de tipo booleano,
independientemente de los valores del resto del grupo, si es que
pertenece a uno.
El aspecto normal es el de una pequeña caja cuadrada con un
texto a uno de los lados, normalmente a la derecha. Cuando está
activo se muestra una marca en el interior de la caja, cuando no lo
está, la caja aparece vacía. También es posible mostrar el checkbox
como un botón corriente, en ese caso, al activarse se quedará
pulsado.
Ficheros de recursos
Vamos a mostrar algunos de los posibles aspectos de los
CheckBoxes:
#include <windows.h>
#include "win014.h"
Menu MENU
BEGIN
POPUP "&Principal"
BEGIN
MENUITEM "&Diálogo", CM_DIALOGO
END
END
// Datos de la aplicación
typedef struct stDatos {
BOOL Normal;
BOOL Auto;
int TriState;
int AutoTriState;
BOOL AutoPush;
int AutoTriPush;
BOOL Derecha;
BOOL Plano;
} DATOS;
Creamos una variable estática en nuestro procedimiento de
ventana para almacenar los datos, y le daremos valores iniciales a
las variables de la aplicación, al procesar el mensaje WM_CREATE:
DialogBoxParam(hInstance, "DialogoPrueba",
hwnd, DlgProc, (LPARAM)&Datos);
#define DESHABILITADO -1
...
static DATOS *Datos;
...
case WM_INITDIALOG:
Datos = (DATOS*)lParam;
// Estado inicial de los checkbox
CheckDlgButton(hDlg, ID_NORMAL,
Datos->Normal ? BST_CHECKED : BST_UNCHECKED);
CheckDlgButton(hDlg, ID_AUTO,
Datos->Auto ? BST_CHECKED : BST_UNCHECKED);
if(Datos->TriState != DESHABILITADO)
CheckDlgButton(hDlg, ID_TRISTATE,
Datos->TriState ? BST_CHECKED :
BST_UNCHECKED);
else
CheckDlgButton(hDlg, ID_TRISTATE,
BST_INDETERMINATE);
if(Datos->AutoTriState != DESHABILITADO)
CheckDlgButton(hDlg, ID_AUTOTRISTATE,
Datos->AutoTriState ? BST_CHECKED :
BST_UNCHECKED);
else
CheckDlgButton(hDlg, ID_AUTOTRISTATE,
BST_INDETERMINATE);
CheckDlgButton(hDlg, ID_AUTOPUSH,
Datos->AutoPush ? BST_CHECKED :
BST_UNCHECKED);
// Usando mensajes:
if(Datos->AutoTriPush != DESHABILITADO)
SendDlgItemMessage(hDlg, ID_AUTOTRIPUSH,
BM_SETCHECK,
Datos->AutoTriPush ? (WPARAM)BST_CHECKED :
(WPARAM)BST_UNCHECKED, 0);
else
SendDlgItemMessage(hDlg, ID_AUTOTRIPUSH,
BM_SETCHECK,
(WPARAM)BST_INDETERMINATE, 0);
SendDlgItemMessage(hDlg, ID_DERECHA, BM_SETCHECK,
Datos->Derecha ? (WPARAM)BST_CHECKED :
(WPARAM)BST_UNCHECKED, 0);
SendDlgItemMessage(hDlg, ID_PLANO, BM_SETCHECK,
Datos->Plano ? (WPARAM)BST_CHECKED :
(WPARAM)BST_UNCHECKED, 0);
SetFocus(GetDlgItem(hDlg, ID_NORMAL));
return FALSE;
Con esto, el estado inicial de los CheckBoxes será correcto.
case WM_COMMAND:
switch(LOWORD(wParam)) {
case ID_NORMAL:
if(SendDlgItemMessage(hDlg, ID_NORMAL,
BM_GETCHECK, 0, 0) == BST_CHECKED)
SendDlgItemMessage(hDlg, ID_NORMAL,
BM_SETCHECK,
(WPARAM)BST_UNCHECKED, 0);
else
SendDlgItemMessage(hDlg, ID_NORMAL,
BM_SETCHECK,
(WPARAM)BST_CHECKED, 0);
return TRUE;
case ID_TRISTATE:
if(IsDlgButtonChecked(hDlg, ID_TRISTATE) ==
BST_INDETERMINATE)
CheckDlgButton(hDlg, ID_TRISTATE,
BST_UNCHECKED);
else if(IsDlgButtonChecked(hDlg,
ID_TRISTATE) == BST_CHECKED)
CheckDlgButton(hDlg, ID_TRISTATE,
BST_INDETERMINATE);
else
CheckDlgButton(hDlg, ID_TRISTATE,
BST_CHECKED);
return TRUE;
}
case IDOK:
Datos->Normal = (IsDlgButtonChecked(hDlg,
ID_NORMAL) == BST_CHECKED);
Datos->Auto = (IsDlgButtonChecked(hDlg,
ID_AUTO) == BST_CHECKED);
if(IsDlgButtonChecked(hDlg, ID_TRISTATE) ==
BST_INDETERMINATE)
Datos->TriState = DESHABILITADO;
else
Datos->TriState =
(IsDlgButtonChecked(hDlg, ID_TRISTATE) == BST_CHECKED);
if(IsDlgButtonChecked(hDlg,
ID_AUTOTRISTATE) == BST_INDETERMINATE)
Datos->AutoTriState = DESHABILITADO;
else
Datos->AutoTriState =
(IsDlgButtonChecked(hDlg, ID_AUTOTRISTATE) == BST_CHECKED);
Datos->AutoPush = (IsDlgButtonChecked(hDlg,
ID_AUTOPUSH) == BST_CHECKED);
if(IsDlgButtonChecked(hDlg, ID_AUTOTRIPUSH)
== BST_INDETERMINATE)
Datos->AutoTriPush = DESHABILITADO;
else
Datos->AutoTriPush =
(IsDlgButtonChecked(hDlg, ID_AUTOTRIPUSH) == BST_CHECKED);
Datos->Derecha = (IsDlgButtonChecked(hDlg,
ID_DERECHA) == BST_CHECKED);
Datos->Plano = (IsDlgButtonChecked(hDlg,
ID_PLANO) == BST_CHECKED);
EndDialog(hDlg, FALSE);
return TRUE;
case IDCANCEL:
EndDialog(hDlg, FALSE);
return FALSE;
Ejemplo 14
Capítulo 15 Control básico
RadioButton
De nuevo estamos hablando de un estilo de botón.
Los RadioButtons sólo pueden tomar dos valores, encendido y
apagado. Se usan típicamente para leer opciones que sólo tienen un
número limitado y pequeño de posibilidades y sólo un valor posible,
como por ejemplo: sexo (hombre/mujer), estado civil
(soltero/casado/viudo/divorciado), etc.
Es necesario agrupar usando un GroupBox, al menos dos, y con
frecuencia tres o más controles de este tipo. No tiene sentido
colocar un solo control RadioButton, ya que al menos uno de cada
grupo debe estar activo. Tampoco es frecuente agrupar dos, ya que
para eso se puede usar un único control CheckBox. Tampoco se
agrupan demasiados, ya que ocupan mucho espacio, en esos casos
es mejor usar un ComboBox o un ListBox.
El aspecto normal es el de un pequeño círculo con un texto a
uno de los lados, normalmente a la derecha. Cuando está activo se
muestra el círculo relleno, cuando no lo está, aparece vacío.
También es posible mostrar el RadioButton como un botón corriente,
en ese caso, al activarse se quedará pulsado.
Ficheros de recursos
Vamos a mostrar algunos de los posibles aspectos de los
RadioButtons:
#include <windows.h>
#include "win015.h"
Menu MENU
BEGIN
POPUP "&Principal"
BEGIN
MENUITEM "&Diálogo", CM_DIALOGO
END
END
// Datos de la aplicación
typedef struct stDatos {
int Grupo1;
int Grupo2;
} DATOS;
DialogBoxParam(hInstance, "DialogoPrueba",
hwnd, DlgProc, (LPARAM)&Datos);
switch(LOWORD(wParam)) {
case ID_RADIOBUTTON4:
case ID_RADIOBUTTON5:
case ID_RADIOBUTTON6:
CheckRadioButton(hDlg, ID_RADIOBUTTON4,
ID_RADIOBUTTON6,
LOWORD(wParam));
return TRUE;
Con mensajes:
switch(LOWORD(wParam)) {
case ID_RADIOBUTTON4:
case ID_RADIOBUTTON5:
case ID_RADIOBUTTON6:
SendDlgItemMessage(hDlg, ID_RADIOBUTTON4,
BM_SETCHECK,
(WPARAM)BST_UNCHECKED, 0);
SendDlgItemMessage(hDlg, ID_RADIOBUTTON5,
BM_SETCHECK,
(WPARAM)BST_UNCHECKED, 0);
SendDlgItemMessage(hDlg, ID_RADIOBUTTON6,
BM_SETCHECK,
(WPARAM)BST_UNCHECKED, 0);
SendDlgItemMessage(hDlg, LOWORD(wParam),
BM_SETCHECK,
(WPARAM)BST_CHECKED, 0);
return TRUE;
En este ejemplo intentamos simular el comportamiento de los
RadioButtons automáticos, pero lo normal es que para eso se usen
RadioButtons automáticos. Los no automáticos pueden tener el
comportamiento que nosotros prefiramos, y en eso consiste su
utilidad. Los automáticos no requieren ninguna atención de nuestro
programa.
case IDOK:
if(IsDlgButtonChecked(hDlg,
ID_RADIOBUTTON1) == BST_CHECKED)
Datos->Grupo1 = 1;
else
if(IsDlgButtonChecked(hDlg,
ID_RADIOBUTTON2) == BST_CHECKED)
Datos->Grupo1 = 2;
else
Datos->Grupo1 = 3;
if(SendDlgItemMessage(hDlg,
ID_RADIOBUTTON4,
BM_GETCHECK, 0, 0) == BST_CHECKED)
Datos->Grupo2 = 1;
else
if(SendDlgItemMessage(hDlg,
ID_RADIOBUTTON5,
BM_GETCHECK, 0, 0) == BST_CHECKED)
Datos->Grupo2 = 2;
else
Datos->Grupo2 = 3;
EndDialog(hDlg, FALSE);
return TRUE;
case IDCANCEL:
EndDialog(hDlg, FALSE);
return FALSE;
Ejemplo 15
Capítulo 16 El GDI
Ahora ya dominamos los controles básicos, así que pasaremos a
un capítulo muy amplio, pero mucho más interesante.
El GDI (Graphics Device Interface), es el interfaz de dispositivo
gráfico, que contiene todas las funciones y estructuras necesarias
que nos permiten comunicar nuestras aplicaciones con cualquier
dispositivo gráfico de salida conectado a nuestro ordenador:
pantalla, impresora, plotter, etc.
Veremos que podremos trazar líneas, curvas, figuras cerradas,
polígonos y mapas de bits. Además, podremos controlar
características como colores, aspectos de líneas y tramas de
superficies rellenas, mediante objetos como plumas, pinceles y
fuentes de caracteres.
Las aplicaciones Windows dirigen las salidas gráficas a lo que se
conoce como un Contexto de Dispositivo, abreviado como DC. Cada
DC se crea para un dispositivo concreto.
Un DC es una de las estructuras del GDI, que contiene
información sobre el dispositivo, como las opciones seleccionadas, o
modos de funcionamiento.
La
aplicación crea
un DC
mediante una
función del
GDI, y éste
devuelve un
manipulador de
DC (hDC), que
DC se usará en las
siguientes
llamadas para identificar el dispositivo que recibirá la salida.
Mediante el DC, la aplicación también puede obtener ciertas
capacidades del dispositivo, como las dimensiones del área de
impresión, la resolución, etc.
La salida puede enviarse directamente al DC del dispositivo
físico, o bien a un dispositivo lógico, que se crea en memoria o en
un metafile. La ventaja de usar un dispositivo lógico es que se
almacena la salida completa y posteriormente puede enviarse al
cualquier dispositivo físico.
También existen funciones para seleccionar diferentes modos y
opciones del dispositivo. Eso incluye colores del texto y fondo,
plumas y pinceles de diferentes colores y texturas para trazar líneas
o rellenar superficies, y lo que se conoce como operaciones binarias
de patrones (Binary Raster Operations), que indican cómo se
combinan las nuevas salidas gráficas con las existentes
previamente. También existen varios sistemas de mapeo de
coordenadas, para traducir las coordenadas usadas en la aplicación
con las el sistema de coordenadas del dispositivo.
HDC hDC;
PAINTSTRUCT ps;
...
case WM_PAINT:
hDC = BeginPaint(hwnd, &ps);
Ellipse(hDC, 10, 10, 50, 50);
EndPaint(hwnd, &ps);
break;
Plumas de Stock
La única excepción a lo dicho antes es un pequeño almacén de
objetos que usa el sistema y que están disponibles para usar en
nuestros programas.
El ejemplo anterior funciona gracias a ello, ya que usa la pluma
por defecto del DC. En el caso de los objetos de stock, no es
necesario crearlos ni destruirlos (aunque esto último no está
prohibido), siempre podemos obtener un manipulador y
seleccionarlo para usarlo.
Concretamente, en el caso de las plumas, existen tres en el
stock:
Valor Significado
BLACK_PEN Pluma negra
NULL_PEN Pluma nula
WHITE_PEN Pluma blanca
Para obtener un manipulador para una de esos objetos de stock
se usa la función GetStockObject. Dependiendo del valor que
usemos obtendremos un tipo de objeto diferente.
Plumas cosméticas y geométricas
Existen dos categorías de plumas.
Las cosméticas se crean con un grosor expresado en unidades
de dispositivo, es decir, las líneas que se tracen con estas plumas
tendrán siempre el mismo grosor. Estas plumas sólo tienen tres
atributos: grosor, color y estilo. Las plumas de stock son de este
tipo.
Las geométricas se crean con un grosor expresado en unidades
lógicas. Esto significa que el grosor de las líneas puede ser
escalado, y depende de la transformaciones actuales del "mundo
actual" (veremos esto en siguientes capítulos). Además de los tres
atributos que también tienen las plumas cosméticas, las geométricas
poseen otros cuatro: plantilla, trama opcional, estilos para extremos
y para uniones.
Las líneas trazadas con plumas cosméticas son entre tres y diez
veces más rápidas que las de las geométricas.
Ejemplo 16
Capítulo 19 Funciones para el
trazado de líneas
El GDI dispone de un repertorio de funciones para el trazado de
líneas bastante completo
Función Tipo de línea
Actualiza la posición actual del cursor gráfico,
MoveToEx
opcionalmente obtiene la posición anterior.
Traza una línea desde la posición actual del cursor
LineTo
al punto indicado.
ArcTo Traza un arco de elipse.
PolylineTo Traza uno o más trazos de líneas rectas.
PolyBezierTo Traza una o más curvas Bézier.
AngleArc Traza un segmento de arco de circunferencia.
Arc Traza un arco de elipse.
Traza una serie de segmentos de recta que
Polyline
conectan los puntos de un array.
PolyBezier Traza una o más curvas Bézier.
GetArcDirection Devuelve la dirección de arco del DC actual.
Cambia la dirección del ángulo para el trazado de
SetArcDirection
arcos y rectángulos.
Traza una línea, pero permite a una función de
LineDDA
usuario decidir qué pixels se mostrarán.
Función callback de la aplicación que procesa las
LineDDAProc
coordenadas recibidas de LineDDA.
PolyDraw Traza una o varias series de líneas y curvas Bézier.
Traza una o varias series de segmentos de recta
PolyPolyLine
conectados.
La mayoría de éstas funciones no requieren mayor explicación,
sobre todo las que trazan arcos y líneas rectas.
Curvas Bézier
Arc
Las curvas Bézier son una
forma de definir curvas irregulares mediante métodos matemáticos.
Para definir una curva Bézier sólo se necesitan cuatro puntos: los
puntos de inicio y final, y dos puntos de control.
En el gráfico se representa una curva Bézier y las líneas
auxiliares, que sólo se muestran como ayuda. El punto de inicio y el
punto de control 1 refinen una recta que es tangente a la curva en el
punto de inicio. La curva tiende a aproximarse al punto de control 1.
Análogamente, el punto de final y el punto de control 2 refinen
otra recta, que es tangente a la curva en el punto de final. La curva
también tiende a acercarse al punto de control 2.
Las ecuaciones que definen la curva no son demasiado
complejas, pero escapan al objetivo de este curso, se estudiaran las
curvas de Bézier con detalle en un artículo separado.
Funciones Poly<tipo>
Todas las funciones con el
prefijo Poly trabajan de un modo
parecido. Se basan en un array
de puntos para definir un
conjunto de líneas que se trazan
una a continuación de otra.
Polyline o PolylineTo trazan
Bezier
un conjunto de segmentos
rectos, PolyBezier y PolyBezierTo, un conjunto de curvas Bézier.
PolyDraw, varios conjuntos de líneas y curvas Bézier, y PolyPolyline,
varios conjuntos de líneas rectas.
struct DatosDDA1 {
int cuenta;
HDC hdc;
};
...
struct DatosDDA1 datos = {0, hDC};
Ejemplo 17
Basándonos en el último
programa de ejemplo, vamos a
hacer otro que muestre cada una
de éstas funciones y cómo se
Línea trazada con LineDDA usan.
Añadiremos un menú para
poder ver cada función por separado.
Nota:
Pinceles lógicos
Existen cuatro tipos distintos de pinceles lógicos.
Pinceles sólidos
Pinceles de Stock
Pinceles de patrones
Crear un pincel
Además de las funciones mencionadas para crear pinceles de
los distintos tipos lógicos mencionados, existe una función más para
crear pinceles: CreateBrushIndirect.
Esta función sirve para crear pinceles lógicos, pero lo hace a
través de una estructura LOGBRUSH, que almacena en su interior
los parámetros necesarios para crear un pincel sólido, de trama o de
patrón.
Seleccionar un pincel
Con los pinceles sucede lo mismo que con las plumas, aunque
podemos tener de un repertorio de manipuladores de pincel, sólo
puede haber uno activo en cada momento, para seleccionar el
pincel activo se usa la función SelectObject.
Como ya hemos dicho el tipo de objeto seleccionado depende
del parámetro que se pase a la función. El nuevo objeto
seleccionado reemplaza al actual, y se devuelve el manipulador del
objeto seleccionado anteriormente.
Se debe guardar el manipulador del pincel por defecto
seleccionado antes de cambiarlo por primera vez, y restablecerlo
antes de terminar el procedimiento de pintar.
Destruir un pincel
Por último, cuando ya no necesitemos más los manipuladores de
pinceles, debemos destruirlos, con el fin de liberar la memoria usada
para almacenarlos. Esto se hace mediante la función DeleteObject.
Ejemplo 18
Capítulo 21 Funciones para el
trazado de figuras rellenas
Veremos ahora el repertorio de funciones para el trazado de
figuras cerradas rellenas. Para trazar estas figuras se usa una pluma
para el borde y un pincel para los interiores.
Función Tipo de figura
Traza una figura definida por el corte de
Chord una elipse y una recta secante y la
rellena.
Ellipse Traza una elipse rellena.
Rellena un rectángulo, sin trazar el
FillRect
borde.
Traza un borde alrededor de un
FrameRect
rectángulo usando el pincel actual.
Pie Traza un sector de elipse.
Polygon Traza un polígono relleno.
Traza una serie de polígonos cerrados y
PolyPolygon
rellenos.
Rectangle Traza un rectángulo relleno.
Traza un rectángulo relleno con las
RoundRect
esquinas redondeadas.
La mayoría de éstas funciones no requieren mayor explicación,
los nombres y descripciones dan suficiente información sobre su
cometido.
Fragmentos
El "centro" de la elipse es el punto medio entre los dos focos, en
el caso de la circunferencia, que en realidad es una elipse
"degenerada", en la que los dos focos coinciden en un punto, ese
punto es el centro.
Tortuoso (winding):
en este modo se
asigna un número
a cada región de la
pantalla
dependiendo del
número de veces
que se ha usado la
pluma para trazar
el polígono que la
Alternado de relleno define. Hay que
tener en cuenta la
dirección en que se recorre cada línea. Las regiones en que ese
número no sea nulo, se rellenarán.
Por ejemplo, supongamos la figura siguiente:
Al seguir las
líneas del
cuadrado externo
en el sentido de
las flechas, cada
uno de los dos
cuadrados es
rodeado una vez,
a cada uno de
ellos le asignamos Relleno tortuoso
un valor de
winding igual a uno.
Al seguir las líneas de cuadrado interno, en el caso de la
izquierda recorremos el cuadrado interno en el mismo sentido que la
primera vez, por lo tanto, le sumamos a esa figura una unidad a su
valor de winding. En el caso de la figura de la derecha, la recorremos
en sentido contrario, por lo tanto le restamos una unidad al cuadrado
interno, es decir, que en el caso de la derecha, el valor winding del
cuadrado interno es cero, y no se rellena.
Si seleccionamos el modo de llenado alternativo, el cuadrado
interno no se rellenará nunca, como de hecho sucede en la primera
imagen.
Ejemplo 19
Capítulo 22 Objetos básicos del
GDI: La paleta (Palette)
El color es muy importante en Windows, y como todo, es un
recurso que tiene sus limitaciones. Cada dispositivo tiene sus
capacidades de colores, y el API proporciona funciones para
conocer esas capacidades, así como manipularlos, elegirlos,
activarlos, etc.
Tal vez, al menos a nivel de pantalla, ya no tenga mucho sentido
un capítulo como el presente, las tarjetas gráficas actuales ya no
tienen las limitaciones en cuanto a color que tenían hace unos años.
Pero en el capítulo dedicado a los mapas de bits veremos que
cuando se almacenan en disco o se transmiten, usar paletas nos
ahorrará mucho espacio de almacenamiento y mucho tiempo en
transmisiones.
Nota:
Paletas de colores
Una paleta de colores es un conjunto que contiene valores de
colores que pueden ser mostrados en el dispositivo de salida.
En Windows existen dos tipos de paletas: paletas lógicas y
paletas de sistema. Dentro de las lógicas existe una especial, la
paleta por defecto, que es la que se usa si el usuario no crea una.
Las paletas de colores se suelen usar en dispositivos que,
aunque pueden generar muchos colores, sólo pueden mostrar o
dibujar con un subconjunto de ellos en un momento dado. Para
estos dispositivos, Windows mantiene una paleta de sistema que
permite almacenar y manejar los colores actuales del dispositivo.
Windows no permite acceder a la paleta de sistema
directamente, en vez de eso, los accesos se hacen mediante una
paleta lógica. Además, Windows crea una paleta por defecto para
cada contexto de dispositivo. Como programadores, podemos usar
los colores de la paleta por defecto o bien crear nuestra propia
paleta lógica y asociarla al contexto de dispositivo.
Para determinar si un dispositivo soporta paletas de colores de
puede comprobar el bit RC_PALETTE del valor RASTERCAPS
devuelto por la función GetDeviceCaps.
Paleta lógica
Una paleta lógica es una paleta que crea una aplicación y que se
asocia con un contexto de dispositivo dado.
Para crear una paleta lógica se usa la función CreatePalette.
Antes es necesario llenar la estructura LOGPALETTE, que
especifica el número de entradas en la paleta y el valor de cada una,
después se debe pasar esa estructura a la función CreatePalette.
La función devuelve un manipulador de paleta que puede usarse en
otras operaciones para identificar la paleta.
#define NUMCOLORES 18
...
PALETTEENTRY Color[NUMCOLORES] = {
//peRed, peGreen, peBlue, peFlags
{0,0,0,PC_NOCOLLAPSE,
{0,20,0,PC_NOCOLLAPSE,
...
;
LOGPALETTE *logPaleta;
...
// Crear una paleta nueva:
logPaleta =
(LOGPALETTE*)malloc(sizeof(LOGPALETTE) +
sizeof(PALETTEENTRY) * NUMCOLORES);
if(!logPaleta)
MessageBox(hwnd, "No pude bloquear la memoria
de la paleta",
"Error", MB_OK);
logPaleta->palVersion = 0x300;
logPaleta->palNumEntries = NUMCOLORES;
for(i = 0; i < NUMCOLORES; i++) {
logPaleta->palPalEntry[i].peBlue =
Color[i].peBlue;
logPaleta->palPalEntry[i].peGreen =
Color[i].peGreen;
logPaleta->palPalEntry[i].peRed =
Color[i].peRed;
logPaleta->palPalEntry[i].peFlags =
Color[i].peFlags;
hPaleta = CreatePalette(logPaleta);
if(!hPaleta)
MessageBox(hwnd, "No pude crear la paleta",
"Error", MB_OK);
free(logPaleta);
case WM_PAINT:
hDC = BeginPaint(hwnd, &ps);
hPaletaOld = SelectPalette(hDC, hPaleta, FALSE);
hBrush = CreateSolidBrush(PALETTEINDEX(6));
hOldBrush = (HBRUSH)SelectObject(hDC, hBrush);
Rectangle(hDC, 20, 20, 40, 40);
SelectObject(hDC, hOldBrush);
DeleteObject(hBrush);
SelectPalette(hDC, hPaletaOld, FALSE);
EndPaint(hwnd, &ps);
break;
case WM_DESTROY:
DeleteObject(hPaleta);
PostQuitMessage(0);
break;
Paleta de sistema
Windows mantiene una paleta de sistema para cada dispositivo
que use paletas. Esta paleta contiene los valores de todos los
colores que pueden ser mostrados o usados actualmente por el
dispositivo. Las aplicaciones no tienen acceso a la paleta de sistema
directamente, al contrario, Windows tiene control absoluto de la
paleta del sistema y permite el acceso sólo a través de las paletas
lógicas.
Para ver el contenido de la paleta del sistema se usa la función
GetSystemPaletteEntries. Esta función recupera el contenido de una
o más entradas, hasta el número total de entradas en la paleta de
sistema. El número total es siempre el mismo que el devuelto para
el valor SIZEPALETTE de la función GetDeviceCaps y es el mismo
que el tamaño máximo de cualquier paleta lógica dada.
Ya hemos dicho que no es posible modificar los colores de la
paleta de sistema directamente, sino sólo cuando se activan paletas
lógicas. Antes de activar una paleta, Windows examina cada color
requerido e intenta encontrar una entrada en la paleta del sistema
que coincida exactamente. Si Windows encuentra ese color, asigna
el índice de la paleta lógica para que corresponda con ese índice de
la paleta del sistema. Si no lo encuentra, se copia el color requerido
en una entrada no usada de la paleta de sistema antes de asignar el
índice. Si todas las entradas de la paleta de sistema están en uso,
Windows asigna al índice de la paleta lógica la entrada de la paleta
de sistema cuyo color sea lo más parecido posible al color
requerido. Una vez que los índices han sido asignados, no es
posible ignorarlos. Por ejemplo, no es posible usar índices de la
paleta de sistema para especificar colores, sólo se permite el uso de
índices de la paleta lógica.
Se puede modificar el modo en que los índices serán asignados
cuando se seleccionen los valores de la paleta lógica mediante el
miembro peFlags de la estructura PALETTEENTRY. Por ejemplo, el
banderín PC_NOCOLLAPSE indica a Windows que copie
inmediatamente el color requerido en una entrada no usada de la
paleta de sistema aunque la paleta de sistema ya contenga ese
color. Además, el flag PC_EXPLICIT indica a Windows que le asigne
al índice de paleta lógica un índice esplícito de la paleta de sistema.
(Para eso se debe dar el índice de la paleta de sistema en la palabra
de menor orden de la estructura PALETTEENTRY).
Las paletas pueden ser activadas como paleta de fondo o como
paleta de primer plano espeficando TRUE o FALSE para el
parámetro bForceBackground en la función SelectPalette,
respectivamente. Sólo puede existir una paleta de primer plano en el
sistema al mismo tiempo. Si una ventana o una descendiente suya
es la activa actualmente, puede activar una paleta de primer plano.
En caso contrario la paleta se activa como paleta de fondo,
independientemente del valor del parámetro bForceBackground. La
principal propiedad de una paleta de primer plano es que cuando se
activa, puede sobrescribir todas las entradas de la paleta de sistema
(excepto las estáticas). Windows lo permite marcando las entradas
de la paleta de sistema que no sean estáticas como no usadas
antes de activar una paleta de primer plano, pudiendo eliminarse
todas las entradas usadas. La paleta de primer plano usa todos los
colores no estáticos posibles. Las de fondo sólo pueden usar las
que permanezcan libres y que estén disponibles para el primero que
las solicite. Normalmente, se usan paletas de fondo con ventanas
hijas que activen sus propias paletas. Esto ayuda a minimizar el
número de cambios en la paleta de sistema.
Una entrada de paleta de sistema no usada es cualquiera que no
está reservada y que no contenga un color estático.
En estos tiempos de potentes tarjetas gráficas, con millones de
colores y enormes velocidades de proceso, este tipo de
preocupaciones ha pasado (felizmente) a la historia. No hace
muchos años, muchas tarjetas gráficas limitaban sus paletas a 16 ó
256 colores, aunque fueran capaces de mostrar muchos más. El
resultado es que frecuentemente distintas aplicaciones activaban
diferentes paletas. Sólo la aplicación que tiene el foco puede activar
una paleta de primer plano, de modo que las aplicaciones que
perdían el foco también podían ver modificados gran parte de sus
colores. El resultado es que los colores de las aplicaciones
cambiaban continuamente al cambiar de aplicación, creando un
efecto molesto y poco estético.
Las entradas reservadas están marcadas explícitamente con el
valor PC_RESERVED. Esas entradas se crean cuando se activan
paletas lógicas para animanciones de paleta. Las entradas de color
estáticas se crean por Windows y corresponden a los colores de la
paleta por defecto. Se puede usar la función GetDeviceCaps para
recuperar el valor NUMRESERVED, que especifica el número de
entradas de la paleta de sistema reservados para colores estáticos.
Ya que la paleta de sistema tiene un número de entradas
limitado, la selección y activación de una paleta lógica para un
dispositivo dado puede afectar a los colores asociados con otras
paletas lógicas del mismo dispositivo.
Esos cambios de color son especialmente dramáticos cuando se
dan en una pantalla. Para asegurarse de que se usan los colores de
una paleta lógica seleccionada del modo más fiel hay que resetear
la paleta antes de usarla. Esto se hace llamando a las funciones
UnrealizeObject y RealizePalette. Usando estas funciones se obliga
a Windows a reasignar los colores de la paleta lógica a colores
adecuados de la paleta de sistema.
Ejemplo 20
Nota:
Fichero de recursos
Esta es la forma de añadir mapas de bits que la aplicación pueda
usar como parte de controles, por ejemplo, mapas de bits en menús
o botones, pero esos mapas de bits pueden usarse por la aplicación
para cualquier otra cosa. Hay que tener en cuenta que esos mapas
de bits se incluyen en el ejecutable, por lo tanto no es muy
recomendable usar mapas de bits muy grandes, ya que eso
aumentará considerablemente el tamaño del fichero ".exe".
El modo de usar estos recursos es añadir una línea BITMAP en
nuestro fichero de recursos:
#include <windows.h>
#define MASCARA 1000
#define CM_RECURSO 100
#define CM_FICHERO 101
Menu MENU
BEGIN
POPUP "Opciones"
BEGIN
MENUITEM "&Bitmap de recurso", CM_RECURSO
MENUITEM "&Bitmap de fichero", CM_FICHERO
END
END
Fichero BMP
Otro modo de obtener mapas de bits para usarlos en nuestra
aplicación es directamente a partir de ficheros BMP, sin necesidad
de fichero de recursos. Para leer uno de esos ficheros se usa la
función LoadImage.
En realidad, la función LoadImage se puede usar en ambos
casos, pero considero que la función LoadBitmap es más cómoda
para obtener un manipulador de mapa de bits cuando se trata de
recursos.
Necesitamos indicar como manipulador de instancia el valor
NULL, si se usa un valor de instancia, generalmente se usará para
obtener un mapa de bits de recursos. El segundo parámetro es el
nombre del fichero, o el nombre del recurso. El tercer parámetro
indica el tipo de imagen, que puede ser un mapa de bits, un icono o
un cursor. Los dos siguientes indican un tamaño de imagen, pero no
se usan cuando se trata de mapas de bits. El último parámetro
permite ajustar algunas opciones, pero sobre todo nos interesa el
valor LR_LOADFROMFILE.
1. Crear un DC de memoria.
2. Seleccionar el mapa de bits en ese DC.
3. Usar una de las funciones disponibles para mostrar el mapa de
bits.
4. Borrar el DC de memoria.
memDC = CreateCompatibleDC(hDC);
SelectObject(memDC, hBitmap);
BitBlt(hDC, 135, 225, 40, 40, memDC, 135, 225, SRCCOPY);
DeleteDC(memDC);
}
StretchBlt
memDC = CreateCompatibleDC(hDC);
SelectObject(memDC, hBitmap);
GetObject(hBitmap, sizeof(BITMAP), (LPSTR)&bm);
GetClientRect(hWnd, &re);
StretchBlt(hDC, 0, 0, re.right, re.bottom, memDC,
135, 225, 40, 40, SRCCOPY);
DeleteDC(memDC);
}
PatBlt
ExtFloodFill
Estructuras de datos
Existen varias estructuras de datos relacionadas con los mapas
de bits, aunque la mayoría están relacionadas con paletas o con el
modo de almacenar los datos que contienen o son muy específicos
de mapas de bits independientes del dispositivo. Sin embargo hay
una estructura que nos resultará muy útil:
BITMAP
La estructura es:
Los datos que más nos interesan son bmWidth y bmHeight, que
indican las dimensiones de anchura y altura, respectivamente, del
mapa de bits. El resto de los datos, al menos de momento, no nos
interesan.
Para obtener los datos de esta estructura para un mapa de bits
concreto, se usa la función GetObject, por ejemplo:
HBITMAP hBitmap;
BITMAP bm;
HDC memDC;
BITMAP bm;
HBITMAP hBitmap;
RECT re;
memDC = CreateCompatibleDC(hDC);
hBitmap = LoadBitmap(NULL, MAKEINTRESOURCE(OBM_CLOSE));
SelectObject(memDC, hBitmap);
GetObject(hBitmap, sizeof(BITMAP), (LPSTR)&bm);
BitBlt(hDC, 10, 10, bm.bmWidth, bm.bmHeight, memDC, 0,
0, SRCCOPY);
DeleteObject(hBitmap);
DeleteDC(memDC);
Ejemplo 21
Nota:
case WM_PAINT:
hDC = BeginPaint(hwnd, &ps);
TextOut(hDC, 10, 10, "Hola, mundo!", 12);
TextOut(hDC, 20, 30, "Curso WinAPI con Clase.",
23);
EndPaint(hwnd, &ps);
break;
SetBkColor(hDC, RGB(40,40,240));
SetBkMode(hDC, TRANSPARENT);
SetTextColor(hDC, RGB(255,0,0));
HFONT fuente;
HFONT grafico;
LOGFONT lf= {80, 0, 450, 450, 300, FALSE, FALSE, FALSE,
DEFAULT_CHARSET, OUT_TT_PRECIS,
CLIP_DEFAULT_PRECIS,
PROOF_QUALITY, DEFAULT_PITCH | FF_ROMAN,
"Webdings"};
El ángulo de escape
El ángulo de orientación
Ángulo de escape Se refiere al ángulo formado
por la línea de base con el eje x
del dispositivo. Cuando creamos fuentes tenemos una precisión de
décimas de grados para precisar dicho ángulo, de modo que un
valor de 900 indica 90º.
Peso
El peso indica cómo de gruesos son los trazos que se usan para
mostrar el carácter, es lo que diferecia un carácter en negrita de uno
normal. Tenemos mil valores posibles para el peso, los valores más
bajos indican trazos finos, los más altos, trazos gruesos.
Como ayuda existen ciertas constantes predefinidas para asignar
a este parámetro.
Cursiva
Subrayado
Tachado
Conjunto de caracteres
Precisión de salida
Precisión de recorte
Calidad
Paso y familia
Nombre
Fuentes de stock
AL igual que con otros objetos del GDI que hemos usado antes,
en el caso de las fuentes también disponemos de seis fuentes de
stock, que podemos seleccionar mediante la función
GetStockObject.
En concreto se trata de las siguientes:
Valor Significado
ANSI_FIXED_FONT Especifica una fuente de
espacio no proporcional
basada en el conjunto de
caracteres de Windows.
Normalmente se usa una
fuente Courier.
Especifica una fuente de
espacio proporcional
basada en el conjunto de
ANSI_VAR_FONT
caracteres de Windows.
Normalmente se usa una
fuente MS Sans Serif.
Especifica la fuente por
defecto para el
dispositivo especificado.
Se trata, típicamente, de
la fuente System para
dispositivos de
visualización. Para
DEVICE_DEFAULT_FONT algunas impresoras
matriciales esta fuente
es una que reside en la
propia impresora.
(Imprimir con esa fuente
suele ser mucho más
rápido que hacerlo con
otra.).
Especifica una fuente de
espacio no proporcional
basada en un conjunto
de caracteres OEM. Para
OEM_FIXED_FONT ordenadores IBM® y
compatibles, la fuente
OEM está absada en el
conjunto de caracteres
del IBM PC.
SYSTEM_FONT Especifica la fuente
System. Es una fuente
de espacio proporcional
basada en el conjunto de
caracteres Windows, y
se usa por el sistema
operativo para mostrar
los títulos de las
ventanas, los nombres
de menú y el texto en los
cuadros de diálogo. La
fuente System siempre
está disponible. Otras
fuentes sólo están
disponibles si han sido
instaladas.
Especifica una fuente de
espacio no proporcional
compatible con la fuente
SYSTEM_FIXED_FONT
System en versiones de
Windows anteriores a la
3.0.
fuente = GetStockObject(ANSI_FIXED_FONT);
anterior = SelectObject(hDC, fuente);
TextOut(hDC, 10, 10, "ANSI_FIXED_FONT", 15);
SelectObject(hDC, anterior);
DeleteObject(fuente);
Alineamientos de texto
Cuando mostramos un texto usando la función TextOut,
ExtTextOut (que aún no hemos visto), indicamos unas coordenadas
para situar el texto en el dispositivo. Estas coordenadas pueden
referirse a diversos puntos dentro del texto. Podemos referirnos a la
línea de base, al centro del texto, a la esquina superior izquierda, a
la esquina superior derecha, etc.
En concreto, tenemos las siguientes opciones:
Separación de caracteres
Normalmente, cuando mostramos texto en un dispositivo, la
separación entre caracteres está predeterminada, y depende del
diseño de la fuente. Pero, con el fin de hacer que el texto sea más
ancho, podemos aumentar la separación entre caracteres, usando la
función SetTextCharacterExtra.
Del mismo modo,
podemos comprimir
el texto usando
valores de
Separación separación
negativos.
Por último, podemos recuperar el valor de separación para un
contexto de dispositivo determinado usando la función
GetTextCharacterExtra.
Medidas de cadenas
A menudo nos interesa conocer las medidas que van a tener las
cadenas en el dispositivo antes de mostrarlas, por ejemplo, para
situarlas correctamente, separar las líneas adecuadamente,
formatear párrafos, etc.
Disponemos de varias funciones en el API para esta tarea.
Empezaremos por la función GetTextMetrics, que nos
proporciona datos sobre la fuente actual de un contexto de
dispositivo en una estructura TEXTMETRIC.
Esta estructura nos informa principalmente sobre las medidas
verticales de las líneas de texto, y nos da alguna información sobre
medidas horizontales, aunque en este caso, algo menos precisas.
Justificar texto
El API también nos proporciona funciones para mostrar texto
justificado: que se ajusta a los márgenes derecho e izquierdo del
área de visualización.
Para ello
deberemos usar dos
funciones de forma
conjunta. Por una
parte
GetTextExtentPoint3
2, que nos
proporciona
información sobre el
Medidas de cadenas tamaño de una línea
de texto. Por otra, la
función SetTextJustification, que prepara las cosas para que la
siguiente función de salida de texto TextOut incluya la separación
apropiada entre palabras.
La función GetTextExtentPoint32 sirve para calcular el espacio
necesario de pantalla necesario para mostrar una cadena. Esta
información la podemos usar, por una parte, para averiguar si cierta
cadena cabe en el espacio en que queremos mostrarla, y por otra,
para saber cuanto espacio sobra, si es que cabe.
El espacio sobrante se debe repartir entre las palabras de la
cadena a justificar, eso se hace mediante la función
SetTextJustification, que necesita como parámetros el manipulador
del DC, el espacio extra, y el número de espacios entre palabras
que contiene la cadena.
Hay que usar la función SetTextJustification con cuidado, ya que
sus efectos permanecen hasta la siguiente llamada. Es decir, si
usamos esta función para justificar una línea, y no anulamos su
efecto, subsiguientes llamadas a GetTextExtentPoint32 podrán
falsear la medida de la cadena, ya que se usará más espacio entre
palabras para calcular la longitud de la cadena.
Para anular el efecto de la función se usa la misma función, pero
con un valor nulo en el segundo parámetro, y también en el tercero,
aunque en realidad ese parámetro se ignora si el segundo es nulo.
char *cadena = "Para ello deberemos usar dos funciones
de";
SIZE tam;
RECT re;
Ejemplo 23
Capítulo 25 Objetos básicos del
GDI: Rectángulos y Regiones
Rectángulos
Los rectángulos se usan en Windows para muchas cosas, y
disponen de estructuras y funciones dedicadas a manejarlos.
Ya los hemos usado, por ejemplo, para invalidar parte de la
ventana y obligar al Windows a actualizarla, para ello usamos la
función InvalidateRect, también los hemos usado para obtener las
dimensiones del área de cliente con GetClientRect.
Lo primero que tenemos que ver es la estructura RECT, aunque
ya la hemos usado muchas veces en este curso.
Esta estructura define un rectángulo mediante las coordenadas
de la parte izquierda, superior, derecha e inferior del rectángulo: left,
top, right y bottom. Podemos decir que estos valores definen dos
puntos: la esquina superior izquierda y la inferior derecha. Es
importante tener claro esto, ya que si la coordenada izquieda es
mayor que la derecha, o la superior mayor que la inferior, no
estaremos definiendo un rectángulo, al menos desde el punto de
vista del API.
Asignar rectángulos
Comparaciones de rectángulos
Modificar rectángulos
Ejemplo 24
Regiones
Las regiones son figuras como rectángulos, elipses o polígonos,
o combinaciones de ellos. Y sus aplicaciones son análogas a las de
los rectángulos, aunque como veremos, más completas.
Las regiones se manejan mediante manipuladores, y se puede
usar para invalidar partes del área de cliente, InvalidateRgn.
Crear regiones
Como en el caso de los rectángulos, disponemos de varias
funciones para crear regiones:
Función Utilidad
Crea una región
CreateRectRgn
rectángular.
Crea una región
CreateRectRgnIndirect rectángular a partir de una
estructura RECT.
Crear una región
CreateRoundRectRgn rectángulas con las
esquinas redondeadas.
CreateEllipticRgn Crea una región elíptica.
Crea una región elíptica a
partir de una estructura
CreateEllipticRgnIndirect RECT que define el
rectángulo que inscribe a la
elipse.
CreatePolygonRgn Crea una región poligonal.
Crea una región
CreatePolyPolygonRgn compuesta por varios
polígonos.
Cualquier región se puede seleccionar para un contexto de
dispositivo, usando la función SelectObject.
Disponemos de un amplio conjunto de operaciones que se
pueden realizar con regiones: combinarlas, compararlas, obtener
sus dimensiones, pintarlas, recuadrarlas, etc.
Combinar regiones
Comparar regiones
Rellenar regiones
Comprobar posiciones
Destruir regiones
Como otros objetos del GDI, hay que destruir las regiones
cuando ya no nos hagan falta, liberando de ese modo los recursos
usados. Para destruir una región se usa la función DeleteObject.
Ejemplo 25
Capítulo 26 Objetos básicos del
GDI: El camino (Path)
Se usan para crear figuras complejas, a base de unir segmentos
rectos con curvas y líneas Bézier. Estas figuras también pueden
contener zonas rellenas y texto.
Los caminos siempre están asociados a un contexto de
dispositivo, pero al contrario que otros objetos del GDI, no existe un
camino por defecto.
Crear un camino
Para crear un camino hay que definir los puntos que lo
componen, esto se hace usando funciones de trazado del GDI entre
las llamadas a las funciones BeginPath y EndPath.
Las funciones que se pueden usar dentro de un camino son:
AngleArc LineTo Polyline
Arc MoveToEx PolylineTo
ArcTo Pie PolyPolygon
Chord PolyBezier PolyPolyline
CloseFigure PolyBezierTo Rectangle
Ellipse PolyDraw RoundRect
ExtTextOut Polygon TextOut
BeginPath(hdc);
SetBkMode(hdc, TRANSPARENT);
TextOut(hdc, 10,10, "Con Clase", 9);
Rectangle(hdc, 0,0,10,10);
EndPath(hdc);
La función CloseFigure sirve para cerrar figuras irregulares
creadas a partir de segmentos rectos y/o curvas.
En cualquier momento, antes de cerrar un camino, podemos
eliminarlo usando AbortPath.
Ejemplo 26
Capítulo 27 Objetos básicos del
GDI: El recorte (Clipping)
El recorte nos permite limitar las salidas del GDI a una
determinada zona, definida por una región o por un camino.
La región de recorte es uno de los objetos del GDI que podemos
seleccionar en un contexto de dispositivo. Del mismo modo que
seleccionamos pinceles, brochas, fuentes, etc.
Caminos de recorte
Además de regiones, podemos usar caminos para definir áreas
de recorte. Los caminos nos proporcionan algo más de flexibilidad,
ya que podemos usar curvas Bézier para definir sus figuras.
Normalmente usaremos caminos para definir áreas de recorte
con el objetivo de crear efectos especiales, como hicimos en el
ejemplo del capítulo anterior, creando un camino a partir de un texto,
y dibujando puntos aleatoriamente dentro de ese camino.
En el ejemplo de este capítulo usamos un camino de recorte, y
también una región para crear efectos con puntos o líneas que se
adaptan a ciertas figuras.
Se usa la función SelectClipPath para crear un área de recorte a
partir de un camino, y la función SelectClipRgn para hacerlo a partir
de una región.
Ejemplo 27
Capítulo 28 Objetos básicos del
GDI: Espacios de coordenadas
y transformaciones
Definiciones
Un sistema de coordenadas es una representación del espacio
plano basado en un sistema Cartesiano. Es decir, dos ejes
perpendiculares.
En Windows, el espacio es limitado, es decir, el valor máximo de
las coordenadas está acotado.
En Windows se usan cuatro sistemas de coordenadas:
Espacios de coordenadas
Espacios de coordenadas
Transformaciones
Ya hemos mencionado que las transformaciones y el espacio de
coordenadas del mundo son elementos nuevos dentro del API de
Win32. De modo que estas características no funcionan con
versiones previas a Windosw NT, ni siquiera con Windows 95.
Las transformaciones usan un par de fórmulas sencillas para
realizar el cambio de coordenadas del espacio del mundo al espacio
de página. Si (x,y) son las coordenadas en el espacio del mundo, y
(x',y') son las coordenadas en el espacio de página, las fórmulas
para obtener estas coordenadas son:
| eM11 eM12 0 |
|x' y' 1| = |x y 1| · | eM21 eM22 0 |
| eDx eDy 1 |
| 1 0 0 |
| 0 1 0 |
| 0 0 1 |
Traslaciones
Cambio de escala
| 0.5 0 0 |
| 0 0.5 0 |
| 0 0 1 |
Figura original
Rotaciones
eM11 es el coseno de α
eM12 es el seno de α
eM21 es menos el seno de α
eM22 es el coseno de α
Nota:
Cambio de ejes
x' = x + y * eM21
y' = x * eM12 + y
Reflexiones
x' = x * eM11
y' = y * eM22
Aplicar
transformacio
nes
En el API32 hay Cambio de eje
Combinar transformaciones
No tenemos que limitarnos a hacer transformaciones simples,
podemos combinarlas para crear transformaciones complejas, de
modo que podemos rotar, trasladar, cambiar ejes, escalar y reflejar
mediante una única transformación.
Para ello disponemos de dos opciones diferentes:
Combinar dos transformaciones mediante la función
CombineTransform. Esta función obtiene una transformación a partir
de otras dos, el resultado de aplicar la transformación obtenida
equivale a aplicar las dos transformaciones, una a continuación de
la otra.
Modificar la transformación del mundo actual mediante la
función ModifyWorldTransform. Mediante esta función será posible
combinar la transformación actual con otra, o bien, asignar la matriz
de transformación identidad (si se usa el valor MWT_IDENTITY
como tercer parámetro. Este tercer parámetro admite otros dos
valores: MWT_LEFTMULTIPLY y MWT_RIGHTMULTIPLY, que
permite multiplicar la transformación indicada por la izquierda o por
la derecha. El resultado puede ser diferente, ya que la multiplicación
de matrices no posee la propiedad conmutativa.
Si necesitamos obtener la matriz de transformación actual,
podemos hacer uso de la función GetWorldTransform.
Ejemplo 28
Ventanas y viewports
La ventana define en el espacio de coordenadas de la página,
mediante dos parámetros: la extensión y el origen.
El viewport define el espacio de coordenadas del dispositivo,
mediante los mismos parámetros que la ventana: la extensión, y el
origen.
En el caso del viewport, al definir el espacio del dispositivo, los
valores se expresan en coordendas de dispositivo, es decir, en
pixels.
Los puntos en el espacio de página se expresan en coordenadas
lógicas, y por lo tanto, también se usan valores lógicos en la
ventana. Tanto la extensión como el origen de la ventana se expresa
en valores lógicos.
No debemos confundir el espacio de página con la ventana física
de la aplicación, aunque existe cierta analogía, la ventana de la que
hablamos ahora es un concepto de espacio gráfico, no siempre
ligado a la ventana que se muestra en el monitor, es más bien, una
ventana que nos permite ver parte del espacio gráfico total.
Extensiones
Orígenes
Donde:
Nota:
Siempre que podamos, usaremos el modo gráfico
avanzado, ya que no sólo nos permite usar transformaciones,
sino que además simplifica el trazado de arcos, al no tener que
preocuparse del modo de mapeo que se use en cada caso.
Otras funciones
Hay algunas funciones más relacionadas con espacios de
coordenadas:
Convierte coordenadas de
ClientToScreen cliente a coordenadas de
pantalla.
Convierte coordenadas de
ScreenToClient pantalla a coordenadas de
cliente.
Recupera la posición actual
GetCurrentPositionEx del cursor gráfico en
coordenadas lógicas.
Desplaza el origen de la
OffsetWindowOrgEx ventana en las cantidades
especificadas.
Desplaza el origen del
OffsetViewportOrgEx viewport en las cantidades
especificadas.
Modifica la extensión de la
ScaleWindowExtEx ventana en el factor
especificado.
Modifica la extensión del
ScaleViewportExtEx viewport en el factor
especificado.
Ejemplo 29
Capítulo 29 Objetos básicos del
GDI: Plumas geométricas
En el capítulo 18 vimos cómo crear y usar plumas cosméticas, y
hablamos un poco de plumas geométricas, aunque sin entrar en
detalles. En este capítulo veremos cómo crear, usar y destruir
plumas geométricas, así como sus características y propiedades.
Anchura
Estilo de línea
Color
Especifica el
color de la pluma. Se
puede usar una
Estilos estructura
COLORREF para
indicar el color.
Patrón
Rayado
Estilo de final
(tapón)
Los extremos de
las líneas pueden
ser de diferentes
tipos. Para las
Tramas plumas geométricas
tenemos tres
posibles valores:
Valor Significado
Las líneas terminan con un
Redondeado
semicírculo.
Las líneas terminan con medio
Cuadrado
cuadrado.
Plano Las líneas terminan de forma abrupta.
Por ejemplo, estas líneas fueron trazadas con estos tres estilos
de final.
Estilo de unión
Crear una
pluma
geométrica
Para crear
plumas geométricas
se usa la función
Uniones ExtCreatePen:
HPEN pluma;
LOGBRUSH lb = {BS_SOLID, RGB(240, 0, 0), 0};
pluma = ExtCreatePen(
PS_GEOMETRIC | PS_DASH | PS_ENDCAP_ROUND |
PS_JOIN_ROUND,
15, &lb, 0, NULL);
SelectObject(hdc, pluma);
MoveToEx(hdc, 30, 30, NULL);
LineTo(hdc, 400, 30);
DeleteObject(pluma);
Ejemplo 30
Capítulo 30 Objetos básicos de
usuario: El Caret
Los carets son las marcas intermitentes que nos indican dónde
se insertará el texto o los graficos cuando un usuario los introduzca.
Normalmente son pequeñas líneas verticales u horizontales, aunque
pueden ser rectángulos de distintos tonos o incluso mapas de bits.
Ya sabemos que sólo una ventana puede tener el foco en un
determinado momento, de modo que sólo puede mostrarse un caret
en un momento determinado, precisamente en la ventana que tiene
el foco.
Veremos en este capítulo como crear, modificar, ocultar o
mostrar carets.
case WM_SETFOCUS:
CreateCaret(hwnd, (HBITMAP)NULL, 0, 20);
case WM_SETFOCUS:
CreateCaret(hwnd, hBitmap, 0, 0);
case WM_SETFOCUS:
DestroyCaret();
break;
case WM_SETFOCUS:
CreateCaret(hwnd, (HBITMAP)NULL, 0, 20);
ShowCaret(hwnd);
break;
case WM_PAINT:
HideCaret(hwnd);
hdc = BeginPaint(hwnd, &ps);
ActualizarPantalla(hdc);
EndPaint(hwnd, &ps);
ShowCaret(hwnd);
break;
SetCaretPos(120,120);
ActualBlink = GetCaretBlinkTime();
SetCaretBlinkTime(50);
Ejemplo 31
Capítulo 31 Objetos básicos del
usuario: El icono
Le toca el turno a otro recurso muy familiar para el usuario de
Windows: el icono.
Un icono es un pequeño mapa de bits, combinado con una
máscara que permite que parte de él sea transparente. El resultado
es que las figuras representadas por iconos pueden tener diferentes
formas, y no tienen por qué ser rectangulares.
Los usamos como ayuda para representar objetos: directorios,
ficheros, aplicaciones, etc.
Punto activo
Los iconos tienen un punto activo (hot spot), como veremos que
sucede también con los cursores. En el caso de los iconos, ese
punto suele ser el centro, y se usa para tareas de alineación y
espaciado de iconos.
Tamaños
En Windows se usan iconos en dos entornos, el sistema y el
shell, y para cada uno de ellos usamos dos tamaños de icono, el
grande y el pequeño.
El icono de sistema pequeño se usa en las barras de título de las
ventanas. Para averiguar el tamaño de este icono hay que llamar a
GetSystemMetrics con SM_CXSMICON y SM_CYSMICON.
El icono de sistema grande es que se usa normalmente en las
aplicaciones, las funciones DrawIcon y LoadIcon usan por defecto,
estos iconos. Para averiguar el tamaño de este icono se puede
llamar a GetSystemMetrics con SM_CXICON y SM_CYICON.
El icono pequeño del shell se usa en el explorador de windows y
en los diálogos comunes.
El icono grande del shell se usa en el escritorio.
Cuando creemos iconos a medida en nuestras aplicaciones,
deberemos suministrar recursos de icono de los siguientes tamaños:
Tipos
Existen iconos estándar disponibles para cualquier aplicación, y
también es posible modificar las imágenes asociadas a esos iconos
estándar, personalizando el escritorio de Windows.
Los iconos estándar son:
Icono Identificador Descripción
Aplicación IDI_APPLICATION Icono de aplicación por defecto.
Asterisco (usado en mensajes de
Información IDI_ASTERISK
información).
Signo de exclamación (usado en
Exclamación IDI_EXCLAMATION
mensajes de aviso).
Icono de mano extentida (usado
Aviso
IDI_HAND en mensajes de aviso
importante
importantes).
Signo de interrogación (usado en
Interrogación IDI_QUESTION
mensajes de petición de datos).
Logo IDI_WINLOGO Icono de logo de Windows.
Cargar uno de estos iconos es sencillo, basta usar la función
LoadIcon, indicando como manipulador de instancia el valor NULL, y
como identificador de icono, el que queramos cargar:
Mostrar iconos
En futuros capítulos veremos cómo incluirlos en menús o
botones, de momento nos conformaremos con mostrarlos en
pantalla.
Se puede obtener información mediante GetIconInfo, y mostrar el
icono mediante DrawIconEx o DrawIcon. Si se usa el parámetro
DI_COMPAT se mostrará la imagen por defecto, si no, se mostrará
la imagen que indiquemos. La función DrawIconEx es más potente
en el sentido de que nos permite mostrar iconos de tamaños
diferentes de 32x32, y además nos permite escalarlos.
DrawIconEx(hdc, 10, 10,
LoadImage(hInstance, "tajmahal", IMAGE_ICON,
0, 0, LR_LOADREALSIZE),
0, 0, 0, NULL, DI_NORMAL);
DrawIcon(hdc, 60, 10, LoadIcon(hInstance,
"antena"));
DrawIconEx(hdc, 100, 10, LoadIcon(hInstance,
"lapiz"),
16, 16, 0, NULL, DI_NORMAL);
DrawIcon(hdc, 140, 10, LoadIcon(NULL,
IDI_APPLICATION));
Destrucción de iconos
DestroyIcon sólo se puede aplicar a iconos creados mediante
CreateIconIndirect. No recomiendo usar este tipo de iconos, ya que
son dependientes del dispositivo, y pueden producir resultados no
deseados en algunos equipos.
Ejemplo 32
Capítulo 32 Objetos básicos del
usuario: El cursor
El cursor o puntero, indica la posición del ratón en pantalla, y nos
permite acceder a los elementos de las ventanas, al tiempo que su
aspecto nos da pistas sobre la acción asociada al cursor, o sobre el
estado del sistema.
Es un recurso importante y único en el sistema, por lo que hay
que compartirlo entre las diferentes aplicaciones. Algunos de los
cambios que hagamos en el cursor se deben deshacer cuando el
control pase a otras aplicaciones.
Cursor de clase
Uno de los parámetros que asignamos al registrar una clase de
ventana, usando la estructura WNDCLASS o WNDCLASSEX y las
funciones RegisterClass o RegisterClassEx es, precisamente, el
cursor de la clase. Windows siempre muestra ese cursor mientras
esté dentro del área de cliente de la ventana.
/* Estructura de la ventana */
wincl.hInstance = hThisInstance;
wincl.lpszClassName = "NUESTRA_CLASE";
wincl.lpfnWndProc = WindowProcedure; /* Esta
función es invocada por Windows */
wincl.style = CS_DBLCLKS; /* Captura los
doble-clicks */
wincl.cbSize = sizeof (WNDCLASSEX);
Cursores de recursos
Se pueden incluir cursores diseñados por nosotros o
almacenados en ficheros ".cur" en el fichero de recursos mediante la
sentencia CURSOR, y obtener un manipulador para ellos usando las
funciones LoadCursor o LoadImage.
Cursores estándar
Podríamos llamarlos cursores de stock, aunque en realidad no lo
son, ya que los cursores estándar se pueden personalizar por el
usuario al cambiar las opciones del escritorio.
Existen los siguientes cursores estándar:
Cursores estándar
Valor Descripción
Flecha estándar y un
IDC_APPSTARTING
pequeño reloj de arena.
IDC_ARROW Flecha estándar.
IDC_CROSS Cruz.
IDC_IBEAM I para texto.
Sólo en Windows NT: icono
IDC_ICON
vacío
IDC_NO Círculo barrado.
Sólo en Windows NT: flecha
IDC_SIZE
de cuatro puntas.
IDC_SIZEALL Igual que IDC_SIZE.
Flecha de dos puntas
IDC_SIZENESW
noreste y sudoeste.
Flecha de dos puntas, norte
IDC_SIZENS
y sur.
IDC_SIZENWSE Flecha de dos puntas,
noroeste y sudeste.
Flecha de dos puntas, este y
IDC_SIZEWE
oeste.
IDC_UPARROW Flecha vertical.
IDC_WAIT Reloj de arena.
El aspecto gráfico de cada uno depende de la configuración del
escritorio de Windows, y se puede modificar usando el Panel de
Control. En nuestra aplicación podemos cargar cualquiera de ellos
usando la función LoadCursor, indicando NULL como manipulador
de instancia, y el identificador que queramos.
Crear cursores
Los cursores estándar no es necesario crearlos, ya que existen
como parte del sistema. Podemos usar las funciones LoadCursor o
LoadImage para obtener manipuladores de esos cursores. Las
mismas funciones se usan para obtener manipuladores de cursores
de recursos.
En el caso de cursores animados no es posible crearlos a partir
de recursos, de modo que hay que cargarlos directamente desde un
fichero ".ani" durante la ejecución, usando la función
LoadCursorFromFile, esta función también puede cargar ficheros de
cursores no animados, con extensión ".cur".
punto.x = 60;
punto.y = 60;
ClientToScreen(hwnd, &punto);
SetCursorPos(punto.x, punto.y);
GetCursorPos(&punto);
ScreenToClient(hwnd, &punto);
Apariencia
Para obtener un manipulador del cursor actual se usa la función
GetCursor.
Para cambiar el cursor actual se usa la función SetCursor, y sólo
después de mover el cursor se mostrará la nueva apariencia.
Recordemos que el sistema se encarga de mostrar siempre el
cursor de acuerdo para la zona sobre la que esté, y cuando se sitúa
en el área de cliente, se usa el cursor de la clase.
De modo que para que sea posible cambiar la apariencia del
cursor, el cursor de la clase debe ser NULL. Pero esto significa que
si movemos el cursor fuera del área de cliente, el cursor cambia
automáticamente, y al regresar al área de cliente, mantiene el
aspecto que tenía después de la última asignación de cursor. Por
ejemplo, si situamos el cursor sobre el borde derecho se mostrará el
cursor de doble flecha, este-oeste. Si volvemos al área de cliente, se
mantiene ese cursor.
SetCursor(LoadCursor(hInstance, "flecha3D"));
SetClassLong(hwnd, GCL_HCURSOR,
(LONG)LoadCursorFromFile("horse.ani"));
El mensaje WM_SETCURSOR
El mensaje WM_SETCURSOR nos permite un control mucho
mayor sobre el aspecto del cursor, incluso aunque éste abandone el
área de cliente. Se recibe cada vez que el cursor se mueve dentro
de la ventana, y de ese modo podemos cambiar la apariencia del
cursor a capricho.
Podemos, por ejemplo, cambiar el cursor dependiendo de la
zona de la ventana, verificando si se encuentra dentro de un
rectángulo o de una región determinada.
Si el cursor está fuera de cualquiera de las zonas de nuestro
interés, probablemente queramos que su aspecto sea el esperado,
por ejemplo, cuando esté sobre un borde o sobre la barra de menús.
En este caso, debemos dejar que el mensaje se procese por el
procedimiento por defecto:
case WM_SETCURSOR:
SetRect(&re, 20, 20, 80,80);
GetCursorPos(&punto);
ScreenToClient(hwnd, &punto);
if(PtInRect(&re, punto))
SetCursor(LoadCursorFromFile("horse.ani"));
else
return DefWindowProc(hwnd, msg, wParam,
lParam);
break;
Ocultar y mostrar
También podemos ocultar o mostrar el cursor en cualquier
momento, para ello usaremos la función ShowCursor. Esta función
admite un parámetro de tipo BOOL, si es TRUE, el valor del
contador se incrementa, si es FALSE, se decrementa. Si el valor del
contador es mayor o igual que cero, el cursor se muestra, en caso
contrario, se oculta.
La utilidad de ocultar el cursor es limitada, tal vez, evitar que el
usuario lleve a cabo ciertas tareas que impliquen el uso de ratón en
determinados momentos.
Confinar el cursor
En ocasiones nos puede interesar que el cursor no salga de
cierta zona de la pantalla hasta que se se cumplan ciertas
condiciones especiales, por ejemplo, podemos confinar el cursor a
una zona concreta de una ventana, y limitar de este modo las
posibilidades de mover el cursor, o de hacer clic en ciertas partes de
la pantalla.
Para esto disponemos de la función ClipCursor, que admite un
parámetro de tipo RECT que define el rectángulo del que el cursor
no puede salir. La función GetClipCursor permite recuperar ese
rectángulo.
El cursor está asociado al ratón, y ambos son recursos únicos en
el sistema, es decir, una aplicación no debería adueñarse de ellos
de forma exclusiva. Cuando una aplicación que ha confinado el
cursor pierde el foco, debe liberarlo para que pueda acceder a
cualquier punto de la pantalla.
Esto se puede hacer procesando los mensajes WM_SETFOCUS
y WM_KILLFOCUS:
case WM_SETFOCUS:
ClipCursor(NULL);
break;
case WM_SETFOCUS:
GetWindowRect(hwnd, &re);
ClipCursor(&re);
break;
case WM_SETFOCUS:
ClipCursor(NULL);
break;
case WM_MOVE:
case WM_SIZE:
case WM_SETFOCUS:
GetWindowRect(hwnd, &re);
ClipCursor(&re);
break;
Destrucción de cursores
Cursores creados con CreateIconIndirect se destruyen con
DestroyCursor, no es necesario destruir el resto de los cursores.
Ejemplo 33
Capítulo 33 El ratón
Aunque importante, se considera que el ratón no es
imprescindible en Windows, por lo tanto, debemos incluir todo lo
necesario para que nuestras aplicaciones se puedan manejar
exclusivamente con el teclado. Esta es la recomendación de
Windows, sin embargo, no todo el mundo la sigue, y a menudo
(cada vez más) encontramos aplicaciones que no pueden
manejarse sin ratón.
Todas las entradas procedentes del ratón se reciben mediante
mensajes. Así que si nuestra aplicación quiere procesar el ratón
como una entrada, debe procesar esos mensajes.
Como vimos en el capítulo anterior, el ratón está asociado al
cursor, cuando el primero se mueve, el segundo se desplaza en
pantalla para indicar dicho movimiento. Tenemos esto tan asumido
que frecuentemente decimos que movemos tanto el cursor como el
ratón indistintamente. La ventana que recibe los mensajes del ratón
es sobre la que se sitúa el punto activo del cursor (hotspot).
Capturar el ratón
Como si fuesemos un gato, podemos capturar el ratón y
mantenerlo cautivo para nuestra aplicación usando la función
SetCapture e indicando qué ventana es la que captura el ratón. Para
liberarlo se puede usar la función ReleaseCapture, pero también se
liberará si otra ventana captura el ratón o si el usuario hace clic en
otra ventana distinta de la que lo ha capturado.
Cada vez que el ratón es capturado, se envía el mensaje
WM_CAPTURECHANGED a la ventana que pierde la captura.
Un caso típico de captura de ratón es el del arrastre de objetos
de una ventana a otra, por ejemplo, pulsamos el botón izquierdo del
ratón sobre el icono correspondiente a un fichero, y manteniéndolo
pulsado movemos el cursor a otra ventana, sólo entonces soltamos
el botón. Si queremos que la primera ventana siga recibiendo los
mensajes del ratón aunque el cursor salga de sus límites, incluido el
mensaje de soltar el botón, deberemos capturar el ratón.
No todos los mensajes sobre eventos del ratón son enviados a la
ventana que lo ha capturado. Por ejemplo si el cursor se desplaza
sobre ventanas diferentes a la que ha capturado el ratón, los
mensajes sobre el movimiento del cursor se envían a esas
ventanas, salvo que uno de los botones del ratón permanezca
pulsado.
Otro efecto secundario destacable es que también perderemos
las funciones normales del ratón sobre las ventanas hijas de la que
ha capturado el ratón. Es decir, si capturamos el ratón, los clics
sobre controles o menús de la ventana no realizan sus acciones
habituales. De modo que no podremos acceder al menú, ni activar
controles mediante el ratón.
Configuración
Para saber si el ratón está presente se puede usar la función
GetSystemMetrics, con el parámetro SM_MOUSEPRESENT.
Además, podemos averiguar el número de botones del ratón,
usando la misma función, con el parámetro
SM_CMOUSEBUTTONS. Se puede trabajar con ratones de uno,
dos o tres botones. Los llamaremos izquierdo, derecho y central, y
frecuentemente apareceran con sus inicales en inglés: L, R y M,
respectivamente.
Las funciones de los botones izquierdo y derecho se pueden
intercambiar cuando el usuario lo maneja con la mano izquierda (o si
quiere hacerlo), mediante la función SwapMouseButton. Esto
significa que el botón izquierdo genera los mensajes del botón
derecho, y viceversa.
Hay que tener en cuenta que el ratón es un recurso compartido,
por lo tanto, esta modificación afectará a todas las ventanas.
Mensajes
Cuando ocurre un evento relacionado con el ratón: pulsaciones
de botones o movimientos, se envía un mensaje, y junto con él, las
coordenadas del punto activo del cursor. Además, las ventanas
siguen recibiendo estos mensajes aunque no tengan el foco del
teclado. También los recibirán si la ventana ha capturado el ratón,
aunque el cursor no esté sobre la ventana.
Los mensajes del ratón se envían del modo "post", es decir, son
mensajes "lentos". En realidad se colocan en una cola que se
procesa cuando el sistema tiene tiempo libre. Si se generan muchos
mensajes en poco tiempo, el sistema elimina automáticamente los
más antiguos, de modo que la aplicación sólo recibe los últimos.
Los mensajes "rápidos" se envían en modo "send", y el control
pasa directamente al procedimiento de ventana, es decir, todos los
mensajes de este tipo se procesan.
Nota:
/* Estructura de la ventana */
wincl.hInstance = hThisInstance;
wincl.lpszClassName = "NUESTRA_CLASE";
wincl.lpfnWndProc = WindowProcedure; /* Esta
función es invocada por Windows */
wincl.style = CS_DBLCLKS; /* Captura los
doble-clicks */
wincl.cbSize = sizeof (WNDCLASSEX);
case WM_NCMOUSEMOVE:
punto = MAKEPOINTS(lParam);
izq = cen = der = 0;
hdc = GetDC(hwnd);
Pintar(hdc, izq, cen, der, punto, FALSE);
ReleaseDC(hwnd, hdc);
break;
case WM_NCLBUTTONDOWN:
izq = 2;
punto = MAKEPOINTS(lParam);
hdc = GetDC(hwnd);
Pintar(hdc, izq, cen, der, punto, FALSE);
ReleaseDC(hwnd, hdc);
return DefWindowProc(hwnd, msg, wParam, lParam);
break;
case WM_NCLBUTTONUP:
izq = 3;
punto = MAKEPOINTS(lParam);
hdc = GetDC(hwnd);
Pintar(hdc, izq, cen, der, punto, FALSE);
ReleaseDC(hwnd, hdc);
return DefWindowProc(hwnd, msg, wParam, lParam);
break;
case WM_NCLBUTTONDBLCLK:
izq = 4;
punto = MAKEPOINTS(lParam);
hdc = GetDC(hwnd);
Pintar(hdc, izq, cen, der, punto, FALSE);
ReleaseDC(hwnd, hdc);
return DefWindowProc(hwnd, msg, wParam, lParam);
break;
Mensaje WM_NCHITTEST
Mensaje WM_MOUSEACTIVATE
case WM_MOUSEWHEEL:
punto = MAKEPOINTS(lParam);
izq = (LOWORD(wParam) & MK_LBUTTON) ? 1 : 0;
cen = (LOWORD(wParam) & MK_MBUTTON) ? 1 : 0;
der = (LOWORD(wParam) & MK_RBUTTON) ? 1 : 0;
rueda = HIWORD(wParam);
rueda /= WHEEL_DELTA;
hdc = GetDC(hwnd);
Pintar(hdc, izq, cen, der, punto, TRUE);
sprintf(cad, "rueda = %04d ", rueda);
TextOut(hdc, 10, 130, cad, strlen(cad));
ReleaseDC(hwnd, hdc);
break;
case WM_NCHITTEST:
hittest = DefWindowProc(hwnd, msg, wParam,
lParam);
if(HTCLIENT == hittest) {
if(!dentro) {
dentro = TRUE;
tme.cbSize = sizeof(TRACKMOUSEEVENT);
tme.dwFlags = TME_HOVER | TME_LEAVE;
tme.hwndTrack = hwnd;
tme.dwHoverTime = 1000;
TrackMouseEvent(&tme);
}
}
return hittest;
break;
case WM_MOUSEHOVER:
hdc = GetDC(hwnd);
TextOut(hdc, 10, 170, "Hover", 5);
ReleaseDC(hwnd, hdc);
break;
Ejemplo 34
Arrastrar objetos
Una de las operaciones más frecuentes que se realizan
mediante el ratón es la de arrastrar objetos. No entraremos en
muchos detalles por ahora, Windows dispone de formas especiales
de realizar arrastre de objetos entre distintas ventanas y
aplicaciones, pero de momento veremos un ejemplo sencillo sobre
cómo arrastrar objetos dentro de una misma ventana.
En este ejemplo usaremos iconos como objetos. A cada icono le
corresponde una imagen y una posición en pantalla:
typedef struct {
HICON icono;
POINT coordenada;
} Objeto;
case WM_CREATE:
hInstance = ((LPCREATESTRUCT)lParam)->hInstance;
objeto[0].icono = LoadIcon(hInstance, "ufo");
objeto[0].coordenada.x = 10;
objeto[0].coordenada.y = 10;
objeto[1].icono = LoadIcon(hInstance, "libro");
objeto[1].coordenada.x = 10;
objeto[1].coordenada.y = 50;
objeto[2].icono = LoadIcon(hInstance, "mundo");
objeto[2].coordenada.x = 10;
objeto[2].coordenada.y = 90;
objeto[3].icono = LoadIcon(hInstance,
"hamburguesa");
objeto[3].coordenada.x = 50;
objeto[3].coordenada.y = 10;
objeto[4].icono = LoadIcon(hInstance, "smile");
objeto[4].coordenada.x = 50;
objeto[4].coordenada.y = 50;
capturado = -1;
break;
case WM_LBUTTONDOWN:
punto = MAKEPOINTS(lParam);
for(i = 0; capturado == -1 && i < 5; i++) {
SetRect(&re,
objeto[i].coordenada.x,
objeto[i].coordenada.y,
objeto[i].coordenada.x+32,
objeto[i].coordenada.y+32);
POINTSTOPOINT(lpunto, punto);
if(PtInRect(&re, lpunto)) {
capturado = i;
ClientToScreen(hwnd,
&objeto[i].coordenada);
SetCursorPos(objeto[i].coordenada.x,
objeto[i].coordenada.y);
SetCapture(hwnd);
ShowCursor(FALSE);
}
}
break;
case WM_MOUSEMOVE:
punto = MAKEPOINTS(lParam);
if(capturado != -1) {
hdc = GetDC(hwnd);
//drag
// Borrar en posición actual:
SetRect(&re,
objeto[capturado].coordenada.x,
objeto[capturado].coordenada.y,
objeto[capturado].coordenada.x+32,
objeto[capturado].coordenada.y+32);
FillRect(hdc, &re,
GetSysColorBrush(COLOR_BACKGROUND));
// Actualizar coordenadas:
POINTSTOPOINT(objeto[capturado].coordenada,
punto);
// Pintar en nueva posición:
DrawIcon(hdc,
objeto[capturado].coordenada.x,
objeto[capturado].coordenada.y,
objeto[capturado].icono);
ReleaseDC(hwnd, hdc);
InvalidateRect(hwnd, &re, FALSE);
}
break;
case WM_LBUTTONUP:
if(capturado != -1) {
capturado = -1;
InvalidateRect(hwnd, NULL, FALSE);
ReleaseCapture();
ShowCursor(TRUE);
}
break;
Ejemplo 35
Capítulo 34 El Teclado
Al igual que el ratón, las entradas del teclado se reciben en
forma de mensajes. En este capítulo veremos el manejo básico del
teclado, y algunas características relacionadas con este dispositivo.
Como pasa con otros dispositivos del ordenador, en el teclado
distinguimos al menos dos capas: la del dispositivo físico y la del
dispositivo lógico.
En cuanto al dispositivo físico, el teclado no es más que un
conjunto de teclas. Cada una de ellas genera un código diferente,
cada vez que es pulsada o liberada, a esos códigos los llamaremos
códigos de escaneo (scan codes). Por supuesto, dado que estamos
en la capa física, estos códigos son dependientes del dispositivo, y
en principio, cambiarán dependiendo del fabricante del teclado.
Pero Windows nos permite hacer nuestros programas
independientes del dispositivo, de modo que no será frecuente que
tengamos que trabajar con códigos de escaneo, y aunque el API
nos informe de esos códigos, generalmente los ignoraremos.
En la capa lógica, el driver del teclado traduce los códigos de
escaneo a códigos de tecla virtual (virtual-key codes). Estos códigos
son independientes del dispositivo, e identifican el propósito de cada
tecla. Generalmente, serán esos los códigos que usemos en
nuestros programas. (Tabla al final).
Ventanas inhibidas
A veces es útil hacer que una ventana no pueda recibir el foco
del teclado, ya sea porque los datos que contiene no deban ser
modificados, o porque no tengan sentido en un contexto
determinado. En ese caso, podemos inhibir tal ventana usando la
función EnableWindow. La misma función se usa para desinhibirla.
Una ventana inhibida no puede recibir mensajes del teclado ni del
ratón.
Ejemplo 36
Mensajes de pulsación de teclas
La acción de pulsar una tecla implica dos eventos, uno cuando
se pulsa y otro cuando se libera. Cuando se pulsa una tecla se
envía un mensaje WM_KEYDOWN o WM_SYSKEYDOWN a la
ventana que tiene el foco del teclado, y cuando se libera, un
mensaje WM_KEYUP o WM_SYSKEYUP.
Los mensajes WM_SYSKEYDOWN y WM_SYSKEYUP se
refieren a teclas de sistema. Las teclas de sistema son las que se
pulsan manteniendo pulsada la tecla [Alt]. Los otros dos mensajes
se refieren a teclas que no sean de sistema.
En todos los casos, el parámetro wParam contiene el código de
tecla virtual, y el parámetro lParam varios datos asociados a la tecla,
como repeticiones, código de escaneo, si se trata de una tecla
extendida, el código de contexto, el estado previo de la tecla y el
estado de transición.
Podemos crear un campo de bits para tratar estos datos más
fácilmente:
typedef union {
struct {
unsigned int repeticion:16;
unsigned int scan:8;
unsigned int extendida:1;
unsigned int reservado:4;
unsigned int contexto:1;
unsigned int previo:1;
unsigned int transicion:1;
};
unsigned int lParam;
} keyData;
Cuando el usuario deja pulsada una tecla generalmente tiene la
intención de repetir varias veces esa pulsación. El sistema está
preparado para ello, y a partir de cierto momento, se generará una
repetición cada cierto intervalo de tiempo. Los dos tiempos se
pueden ajusta en el Panel de control.
Pero lo que nos interesa en este caso es que el sistema genera
nuevos mensajes WM_KEYDOWN o WM_SYSKEYDOWN, sin los
correspondientes mensajes de tecla liberada. Es más, cada uno de
los mensajes puede corresponder a una pulsación, si el sistema es
lo bastante rápido para procesar cada pulsación individual; o a
varias, si se acumulan repeticiones entre dos mensajes
consecutivos.
Para saber cuantas repeticiones de tecla están asociadas a un
mensaje de tecla pulsada hay que examinar el cámpo de repetición
del parámetro lParam.
El código de escaneo, como comentamos antes, es dependiente
del dispositivo, y por lo tanto, generalmente no tiene utilidad para
nosotros.
El bit de tecla extendida indica si se trata de una tecla específica
de un teclado extendido. Generalmente, los ordenadores actuales
siempre usan un teclado extendido.
El bit de contexto siempre es cero en los mensajes
WM_KEYDOWN y WM_KEYUP, en el caso de los mensajes
WM_SYSKEYDOWN y WM_SYSKEYUP será 1 si la tecla Alt está
pulsada.
El bit de estado previo indica si la tecla estaba pulsada antes de
enviar el mensaje, 1 si lo estaba, 0 si no lo estaba.
Y el bit de transición siempre es 0 en el caso de mensajes
WM_KEYDOWN y WM_SYSKEYDOWN, y 1 en el caso de
WM_KEYUP y WM_SYSKEYUP.
Cuando nuestra aplicación necesite procesar los mensajes de
pulsación de tecla de sistema, debemos tener cuidado de pasarlos a
la función DefWindowProc para que se procesen por el sistema. No
lo hacemos esto, nuestra aplicación no responderá al menú desde el
teclado, mediante combinaciones Alt+tecla.
Los mensajes de pulsación de tecla se usarán cuando queramos
tener un control bastante directo del teclado, generalmente no nos
interesa tanto control, y los mensajes de carácter serán suficientes.
case WM_PAINT:
hdc = BeginPaint(hwnd, &ps);
SetBkColor(hdc, GetSysColor(COLOR_BACKGROUND));
for(i = 0; i < nLineas; i++)
TextOut(hdc, 10, i*20, lista[i],
strlen(lista[i]));
EndPaint(hwnd, &ps);
break;
case WM_KEYDOWN:
for(i = nLineas; i > 0; i--)
strcpy(lista[i], lista[i-1]);
if(nLineas < 39) nLineas++;
kd.lParam = lParam;
sprintf(lista[0], "Tecla %d pulsada Rep=%d "
"[Ext:%d Ctx:%d Prv:%d Trn:%d]",
(int)wParam, kd.repeticion, kd.extendida,
kd.contexto, kd.previo, kd.transicion);
InvalidateRect(hwnd, NULL, TRUE);
break;
case WM_KEYUP:
for(i = nLineas; i > 0; i--)
strcpy(lista[i], lista[i-1]);
if(nLineas < 39) nLineas++;
kd.lParam = lParam;
sprintf(lista[0], "Tecla %d liberada "
"[Ext:%d Ctx:%d Prv:%d Trn:%d]",
(int)wParam, kd.extendida,
kd.contexto, kd.previo, kd.transicion);
InvalidateRect(hwnd, NULL, TRUE);
break;
Nombres de teclas
Una función que puede resultar útil en algunas circunstancias es
GetKeyNameText, que nos devuelve el nombre de una tecla. Como
parámetros sólo necesita el parámetro lParam entregado por un
mensaje de pulsación de tecla, un búffer para almacenar el nombre
y el tamaño del búffer:
El bucle de mensajes
Es el momento de comentar algo sobre el bucle de mensajes
que estamos usando desde el principio de este texto:
Ejemplo 37
Mensajes de carácter
Si usamos la función TranslateMessage, cada mensaje
WM_KEYDOWN se traduce en un mensaje WM_CHAR o
WM_DEADCHAR; y cada mensaje WM_SYSKEYDOWN a un
mensaje WM_SYSCHAR o WM_SYSDEADCHAR.
Generalmente ignoraremos todos estos mensajes, salvo
WM_CHAR. Los mensajes WM_SYSCHAR y WM_SYSDEADCHAR
se usan por Windows para acceder de forma rápida a menús, y no
necesitamos procesarlos. En cuanto al mensaje WM_DEADCHAR,
notifica sobre caracteres de teclas muertas, y generalmente,
tampoco resultará interesante procesarlos.
Teclas muertas
case WM_CHAR:
switch((TCHAR) wParam) {
case 13:
// Procesar retorno de línea
break;
case 0x08:
// Procesar carácter de retroceso (borrar)
break;
default:
// Cualquier otro carácter
break;
}
InvalidateRect(hwnd, NULL, TRUE);
break;
Estado de teclas
A veces nos interesa conocer el estado de alguna tecla concreto
en el momento en que estamos procesando un mensaje procedente
de otra pulsación de tecla. Por ejemplo, para tratar combinaciones
de teclas como ALT+Fin o ALT+Inicio. Tenemos dos funciones para
hacer esto.
Por una parte, la función GetAsyncKeyState nos dice el estado
de una tecla virtual en el mismo momento en que la llamamos. Y la
función GetKeyState nos da la misma información, pero en el
momento en que se generó el mensaje que estamos tratando.
Ejemplo 38
Hot keys
He preferido no traducir el término "hot key", ya que me parece
que es mucho más familiar que la traducción literal "tecla caliente".
Una hot key es una tecla, o combinación de teclas, que tiene
asignada una función especial y directa.
En Windows hay muchas hot keys predefinidas, por ejemplo,
Ctrl+Alt+Supr sirve para activar el administrador de tareas, o la tecla
de Windows izquierda en combinación con la tecla 'E', para abrir el
explorador de archivos. Dentro de cada ventana o aplicación exiten
más, por ejemplo, Alt+F4 cierra la ventana, etc.
Hay dos tipos de hot keys, uno es el de las asociadas a
ventanas. Es posible asociar una tecla o combinación de teclas a
una ventana, de modo que al pulsarla se activa esa ventana,
estemos donde estemos, estas son las hot keys globales.
El otro tipo, que es el que vamos a ver ahora, son las hot keys de
proceso, lo locales. Nuestra aplicación puede crear tantas de ellas
como creamos necesario, procesarlas y, si es necesario, destruirlas.
Crear, o mejor dicho, registrar una hot key es sencillo, basta con
usar la función RegisterHotKey. Esta función necesita cuatro
parámetros. El primero es la ventana a la que estárá asociada la hot
key. El segundo parámetro es el identificador. El tercero son los
modificadores de tecla, indica si deben estar presionadas las teclas
de Control, Alt, Mayúsculas o Windows. Y el cuarto es el código de
tecla virtual asociado a la hot key. Recordemos que los códigos de
teclas virtuales de teclas correspondientes a caracteres son los
propios caracteres, en el caso de letras, las mayúsculas.
case WM_CREATE:
hInstance = ((LPCREATESTRUCT)lParam)->hInstance;
color = GetSysColor(COLOR_BACKGROUND);
RegisterHotKey(hwnd, ID_VERDE, 0, 'V');
RegisterHotKey(hwnd, ID_ROJO, MOD_ALT, 'R');
RegisterHotKey(hwnd, ID_AZUL, MOD_CONTROL, 'A');
break;
case WM_HOTKEY:
switch((int)wParam) {
case ID_VERDE:
color = RGB(0,255,0);
break;
case ID_ROJO:
color = RGB(255,0,0);
break;
case ID_AZUL:
color = RGB(0,0,255);
break;
}
InvalidateRect(hwnd, NULL, FALSE);
break;
case WM_DESTROY:
UnregisterHotKey(hwnd, ID_VERDE);
UnregisterHotKey(hwnd, ID_ROJO);
UnregisterHotKey(hwnd, ID_AZUL);
PostQuitMessage(0); /* envía un mensaje
WM_QUIT a la cola de mensajes */
break;
Ejemplo 39
Códigos de teclas virtuales
Los códigos virtuales de las teclas que generan caracteres son
los códigos ASCII de esos caracteres, por ejemplo, el código virtual
de la tecla [A] es el 'A', o en número, el 65. Para el resto de las
teclas existen constantes definidas en el fichero "winuser.h". Las
constantes definidas son:
Constante Tecla Constante Tecla
Botón
Botón derecho
VK_LBUTTON izquierdo VK_RBUTTON
de ratón
de ratón
Botón central
VK_CANCEL VK_MBUTTON
de ratón
VK_BACK VK_TAB Tabulador
VK_CLEAR VK_RETURN Retorno
VK_KANA VK_SHIFT Mayúsculas
VK_CONTROL Control VK_MENU
Bloqueo
VK_PAUSE Pausa VK_CAPITAL
mayúsculas
VK_ESCAPE Escape VK_SPACE Espacio
Página Página
VK_PRIOR VK_NEXT
anterior siguiente
VK_END Fin VK_HOME Inicio
Flecha
VK_LEFT VK_UP Flecha arriba
izquierda
VK_RIGHT Flecha VK_DOWN Flecha abajo
derecha
Imprimir
VK_SELECT VK_PRINT
pantalla
VK_EXECUTE VK_SNAPSHOT
VK_INSERT Insertar VK_DELETE Suprimir
Windows
VK_HELP Ayuda VK_LWIN
izquierda
Windows Menú de
VK_RWIN VK_APPS
derecha aplicación
'0'
VK_NUMPAD0 VK_NUMPAD1 '1' numérico
numérico
'2'
VK_NUMPAD2 VK_NUMPAD3 '3' numérico
numérico
'4'
VK_NUMPAD4 VK_NUMPAD5 '5' numérico
numérico
'6'
VK_NUMPAD6 VK_NUMPAD7 '7' numérico
numérico
'8'
VK_NUMPAD8 VK_NUMPAD9 '9' numérico
numérico
VK_MULTIPLY Multiplicar VK_ADD Sumar
VK_SEPARATOR VK_SUBTRACT Restar
Punto
VK_DECIMAL VK_DIVIDE Dividir
decimal
VK_F1 F1 VK_F2 F2
VK_F3 F3 VK_F4 F4
VK_F5 F5 VK_F6 F6
VK_F7 F7 VK_F8 F8
VK_F9 F9 VK_F10 F10
VK_F11 F11 VK_F12 F12
VK_F13 F13 VK_F14 F14
VK_F15 F15 VK_F16 F16
VK_F17 F17 VK_F18 F18
VK_F19 F19 VK_F20 F20
VK_F21 F21 VK_F22 F22
VK_F23 F23 VK_F24 F24
Bloqueo Bloqueo
VK_NUMLOCK VK_SCROLL
numérico desplazamiento
Mayúsculas Mayúsculas
VK_LSHIFT VK_RSHIFT
izquierdo derecho
Control Control
VK_LCONTROL VK_RCONTROL
izquierdo derecho
VK_LMENU VK_RMENU
VK_PROCESSKEY VK_ATTN
VK_CRSEL VK_EXSEL
VK_EREOF VK_PLAY
VK_ZOOM VK_NONAME
VK_PA1 VK_OEM_CLEAR
Las teclas sin descripción no están en mi teclado, de modo que
no he podido averiguar a qué corresponden.
Capítulo 35 Cadenas de
caracteres
Windows trata las cadenas de caracteres de un modo algo
distinto a como lo hacen las funciones estándar C. Esto se debe a
que Windows maneja varios conjuntos de caracteres: ANSI, que son
los que ya conocemos, como caracteres de ocho bits y Unicode, que
son de dos bytes.
También puede manejar, diferentes formas de comparar y
ordenar cadenas, diferentes configuraciones de idioma, que afectan
a la forma de representar mayúsculas y minúsculas, o de comparar
caracteres, etc. Por ejemplo, en español, la letra 'ñ' es mayor que la
'n' y menor que la 'o'. En inglés ni siquiera existe esa letra. Otro
ejemplo, si intentamos obtener la mayúscula de la letra 'ñ' usando
funciones estándar, el resultado no será la 'Ñ'.
Recursos de cadenas
Al igual que podemos crear recursos para mapas de bits, menús,
iconos, etc, también podemos crearlos para almacenar cadenas y
leerlas desde la aplicación. Esto tiene varias ventajas y aplicaciones.
Los recursos de una aplicación pueden ser modificados por un
editor adecuado sin modificar la parte ejecutable de una aplicación.
Esto permite traducir una aplicación a distintos idiomas sin tener que
compilar la aplicación ni tener que compartir el código fuente.
Es más, podemos crear nuestras aplicaciones para que sean
multilenguaje, de modo que usen las cadenas adecuadas según la
configuración de la aplicación.
Fichero de recursos
Lo primero que debemos crear es una tabla de cadenas
(stringtable) dentro del fichero de recursos, esto se hace mediante la
sentencia STRINGTABLE:
STRINGTABLE
BEGIN
ID_TITULO, "Título de la aplicación"
ID_SALUDO, "Hola, estoy preparado para empezar."
ID_DESPEDIDA, "Gracias por usar esta aplicación."
END
// Comparar cadenas:
if(lstrcmp("Niño", "Ñape") < 0)
lstrcpy(mensaje, "Niño es menor que Ñape");
else
lstrcpy(mensaje, "Niño es mayor que Ñape");
TextOut(hdc, 10, 280, mensaje, strlen(mensaje));
if(lstrcmp("Ola", "Ñape") < 0)
lstrcpy(mensaje, "Ola es menor que Ñape");
else
lstrcpy(mensaje, "Ola es mayor que Ñape");
TextOut(hdc, 10, 300, mensaje, strlen(mensaje));
abcdefghijklmnñopqrstuvwxyz áéíóúëü ç
ABCDEFGHIJKLMNÑOPQRSTUVWXYZ ÁÉÍÓÚËÜ Ç
Ejemplo 40
Capítulo 36 Aceleradores
Los aceleradores son atajos para los menús. Lo normal y
deseable es que nuestras aplicaciones proporcionen aceleradores
para las opciones más frecuentes de los menús.
Un acelerador es una pulsación de tecla, o de teclas, que
producen el mismo efecto que una selección en un menú. Windows
detecta la pulsación y convierte el menaje de teclado a un mensaje
WM_COMMAND o WM_SYSCOMMAND.
Cuando el usuario se familiariza con los aceleradores de teclado
puede ahorrar mucho tiempo al activar comandos, ya que es mucho
más rápido pulsar una tecla que activar una opción de menú, ya sea
mediante el teclado o el ratón.
Recursos de aceleradores
Como ya hemos visto con los menús, cuadros de diálogo,
cadenas, etc, en el caso de los aceleradores también podemos
crearlos como un recurso, y cargarlos por la aplicación cuando se
necesiten.
Fichero de recursos
aceleradores ACCELERATORS
BEGIN
VK_F1, CM_OPCION1, VIRTKEY /* F1 */
"^C", CM_SALIR /* Control C */
"K", CM_OPCION2 /* K */
"k", CM_OPCION3, ALT /* Alt k */
0x34, CM_OPCION4, ASCII /* 4 */
VK_F2, CM_OPCION5, ALT, SHIFT, VIRTKEY /* Alt Mays F2
*/
"1", CM_OPCION6, ALT, CONTROL, VIRTKEY /* Alt Control 1
*/
END
menu MENU
BEGIN
POPUP "&Principal"
BEGIN
MENUITEM "Opción &1\aF1", CM_OPCION1
MENUITEM "Opción &2\aK", CM_OPCION2
MENUITEM "Opción &3\aAlt+k", CM_OPCION3
MENUITEM "Opción &4\a4", CM_OPCION4
MENUITEM "Opción &5\aAlt+Mays+F2", CM_OPCION5
MENUITEM "Opción &6\aAlt+Ctrl+1", CM_OPCION6
MENUITEM SEPARATOR
MENUITEM "&Salir\a^C", CM_SALIR
END
END
Aceleradores globales
Existen varios aceleradores definidos a nivel global de Windows,
nuestras aplicaciones deben intentar evitar definir esos
aceleradores, aunque en principio no es imposible hacerlo,
sencillamente no es aconsejable. Los aceleradores son:
Acelerador Descripción
ALT+ESC Cambia a la siguiente aplicación.
ALT+F4 Cierra una aplicación o ventana.
ALT+HYPHEN Abre el menú de sistema de una
ventana de documento.
ALT+PRINT Copia una imagen de la ventana
SCREEN activa al portapapeles.
Abre el menú de sistema para una
ALT+SPACEBAR
ventana de aplicación.
ALT+TAB Cambia a la siguiente aplicación.
Cambia a la lista de tareas de
CTRL+ESC
Windows (menú de Inicio).
Cierra el grupo activo o ventana
CTRL+F4
de documento.
Arranca la ayuda si la aplicación la
F1
tiene.
Copia una imagen de la pantalla al
PRINT SCREEN
portapapeles.
Cambia a la aplicación anterior. El
SHIFT+ALT+TAB usuario debe presionar Alt+Mays
mientras presiona TAB.
Ejemplo 41
Capítulo 37 Menús 2
En el capítulo 5 tratamos el tema de los menús, pero de una
manera superficial. La intención era dar unas nociones básicas para
poder usar menús en nuestros primeros ejemplos. Ahora los
veremos con más detalle, y estudiaremos muchas características
que hasta ahora habíamos pasado por alto.
Los menús pueden tener, por ejemplo, mapas de bits a la
izquierda del texto. También pueden comportarse como checkboxes
o radiobuttons. Se pueden inhibir ítems. Podemos crear menús
flotantes contextuales al pulsar con el ratón sobre determinadas
zonas de la aplicación, etc.
Marcas en menús
Seguro que estás familiarizado con las marcas de chequeo que
aparecen en algunos ítems de menú en casi todas las aplicaciones
Windows. Es frecuente que se puedan activar o desactivar
opciones, y que se pueda ver el valor actual de cada opción
consultando menú. El funcionamiento es exactamente el mismo que
el de los checkboxes y radiobuttons.
Hemos visto que los ítems de menú se comportan exactamente
igual que los botones, pero hasta ahora sólo hemos usado los
menús como un conjunto de "Pushbuttons", veremos qué otras
opciones tenemos.
MENUITEMINFO infoMenu;
...
infoMenu.cbSize = sizeof(MENUITEMINFO);
infoMenu.fMask = MIIM_STATE;
GetMenuItemInfo(GetMenu(hwnd), CM_OPCIONA, FALSE,
&infoMenu);
if(infoMenu.fState & MFS_CHECKED)
infoMenu.fState = MFS_UNCHECKED;
else
infoMenu.fState = MFS_CHECKED;
SetMenuItemInfo(GetMenu(hwnd), CM_OPCIONA, FALSE,
&infoMenu);
CheckMenuRadioItem(GetMenu(hwnd),CM_RADIO1, CM_RADIO6,
CM_RADIO3, MF_BYCOMMAND);
Ejemplo 42
Inhibir y oscurecer ítems
También frecuente que en determinadas circunstancias
queramos que algunas opciones no estén disponibles para el
usuario, ya sea porque no tienen sentido, o por otra razón. Por
ejemplo, esto es lo que pasa con la opción de "Maximizar" del menú
de sistema cuando la ventana está maximizada.
En
ese
sentido,
los ítems
pueden
tener tres
Inhibir menús estados
distintos: activo, inhibido y oscurecido. Hasta ahora sólo hemos
trabajado con ítems activos. Los inhibidos tienen el mismo aspecto
para el usuario, pero no se pueden seleccionar. Los oscurecidos
además de no poderse seleccionar, aparecen en gris o difuminados,
para indicar que están inactivos.
Podemos cambiar el estado de acceso de un ítem usando la
función EnableMenuItem, o mejor, con la función SetMenuItemInfo.
Aunque la documentación del API dice que la primera está obsoleta,
se puede seguir usando si no se necesitan otras características de
la segunda.
La función EnableMenuItem necesita tres parámetros, el primero
es el manipulador de menú, el segundo su identificador o posición, y
el tercero indica si el segundo es un identificador o una posición y el
estado que se va a asignar al ítem.
En este ejemplo se usa la función EnableMenuItem para inhibir y
oscurecer un ítem, y la función SetMenuItemInfo para activarlo, de
modo que se ilustran los dos modos de realizar esta tarea.
MENUITEMINFO infoMenu;
...
switch(LOWORD(wParam)) {
case CM_INHIBIR:
EnableMenuItem(GetMenu(hwnd),CM_OPCION, MF_DISABLED
| MF_BYCOMMAND);
CheckMenuRadioItem(GetMenu(hwnd),CM_INHIBIR,
CM_ACTIVAR, CM_INHIBIR, MF_BYCOMMAND);
break;
case CM_OSCURECER:
EnableMenuItem(GetMenu(hwnd),CM_OPCION, MF_GRAYED |
MF_BYCOMMAND);
CheckMenuRadioItem(GetMenu(hwnd),CM_INHIBIR,
CM_ACTIVAR, CM_OSCURECER, MF_BYCOMMAND);
break;
case CM_ACTIVAR:
infoMenu.cbSize = sizeof(MENUITEMINFO);
infoMenu.fMask = MIIM_STATE;
infoMenu.fState = MFS_ENABLED;
SetMenuItemInfo(GetMenu(hwnd), CM_OPCION, FALSE,
&infoMenu);
CheckMenuRadioItem(GetMenu(hwnd),CM_INHIBIR,
CM_ACTIVAR, CM_ACTIVAR, MF_BYCOMMAND);
break;
Ejemplo 43
Más sobre ficheros de recursos
También en lo que respecta a los ficheros de recursos hay más
cosas que contar. Para empezar, además de la sentencia MENU
existe otra sentencia para crear recursos de menús extendidos, que
incorporan características introducidas en Windows 95. Pero de
todos modos, aún no hemos visto todo sobre la sentencia
MENUITEM.
Y la de POPUP es:
menu MENU
BEGIN
POPUP "&Pruebas"
BEGIN
MENUITEM "&Ayuda", 500
MENUITEM "&Gris", 501, GRAYED
MENUITEM "&Inactivo", 502, INACTIVE
MENUITEM "Ba&r Break", 503, MENUBARBREAK
MENUITEM "Prueba &2", 504, CHECKED
MENUITEM "Prueba &3", 505
MENUITEM "&Break", 506, MENUBREAK
MENUITEM "Prueba &4", 507
MENUITEM "Prueba &5", 508
END
END
Sentencia MENUEX
menu MENUEX
BEGIN
POPUP "&Principal"
BEGIN
MENUITEM "&Inhibir", CM_INHIBIR, MFT_STRING
MENUITEM "&Oscurecer", CM_OSCURECER, MFT_STRING
MENUITEM "&Activar", CM_ACTIVAR, MFT_STRING
MENUITEM "" // MFT_SEPARATOR
MENUITEM "O&pción", CM_OPCION, MFT_STRING
END
POPUP "&Pruebas",0,MFT_STRING | MFT_RIGHTJUSTIFY
BEGIN
MENUITEM "&Ayuda", 500, MFT_STRING
MENUITEM "&Gris", 501, MFT_STRING, MFS_GRAYED
MENUITEM "&Inactivo", 502, MFT_STRING, MFS_DISABLED
MENUITEM "Ba&r Break", 503, MFT_STRING | MFT_MENUBARBREAK
MENUITEM "Prueba &2", 504, MFT_STRING, MFS_CHECKED
MENUITEM "Prueba &3", 505, MFT_STRING, MFS_UNCHECKED
MENUITEM "&Break", 506, MFT_STRING | MFT_MENUBREAK
MENUITEM "Prueba &4", 507, MFT_STRING
MENUITEM "Prueba &5", 508, MFT_STRING
END
END
Nota:
Resumamos un poco:
Nota:
Cargar recursos
wincl.lpszMenuName = "menu";
HMENU hMenu;
...
hMenu = LoadMenu(hThisInstance, "menu");
SetMenuDefaultItem(GetSubMenu(GetMenu(hwnd),0),
CM_OPCION, FALSE);
id = GetMenuDefaultItem(GetSubMenu(GetMenu(hwnd),0),
FALSE, GMDI_GOINTOPOPUPS);
infoMenu.cbSize = sizeof(MENUITEMINFO);
infoMenu.fMask = MIIM_STATE
infoMenu.fState = MFS_DEFAULT;
SetMenuItemInfo(GetMenu(hwnd), CM_OPCION, FALSE,
&infoMenu);
Ejemplo 44
Menús flotantes o contextuales
Otra posibilidad de los menús es
crear menús flotantes, también
llamados menús de atajo o menús
contextuales. Estos menús se
suelen mostrar cuando el usuario
hace clic con el ratón sobre distintas
Menús flotantes zonas de la ventana, y normalmente
se muestran distintos menús
dependiendo de la zona, o mejor dicho, del contexto.
Todo esto es responsabilidad del programador: procesar las
pulsaciones de botones de ratón, decidir qué menú mostrar, y
finalmente mostrarlo en pantalla.
La aplicación recibe el mensaje WM_CONTEXTMENU cuando el
usuario hace clic sobre la ventana, aunque también podemos
procesar los mensajes de ratón comunes.
Para mostrar el menú flotante en pantalla se usa la función
TrackPopupMenuEx:
HMENU hmenu;
...
case WM_CREATE:
hInstance = ((LPCREATESTRUCT)lParam)->hInstance;
hmenu = LoadMenu(hInstance, "menu2");
...
case WM_CONTEXTMENU:
TrackPopupMenuEx(GetSubMenu(hmenu, 0),
TPM_CENTERALIGN | TPM_HORIZONTAL | TPM_RIGHTBUTTON,
LOWORD(lParam), HIWORD(lParam),
hwnd, NULL);
break;
...
case WM_DESTROY:
DestroyMenu(hmenu);
...
Ejemplo 45
Acceso por teclado
Ya comentamos en el capítulo dedicado al ratón que ese
dispositivo no es imprescindible en Windows, de modo que la
comunicación entre la aplicación y el usuario se puede hacer
exclusivamente desde el teclado, y los menús no son una
excepción.
Mnemónicos
Aceleradores
Modificar menús
Siempre es posible modficar un menú durante la ejecución de la
aplicación, tan sólo necesitamos su manipulador y aplicar las
funciones que necesitemos entre las siguientes:
Función Descripción
Añade un nuevo ítem de menú al
final del menú especificado. Esta
función ha sido sustituida por
InsertMenuItem. Sin embargo, se
AppendMenu
puede seguir usando, si no se
necesitan las características
extendidas de la función
InsertMenuItem.
InsertMenu Inserta un nuevo ítem de menú
dentro de un menú, moviendo los
otros ítems hacia abajo. Esta
función también ha sido sustituida
por la función InsertMenuItem. De
todos modos, se puede seguir
usando, si no se necesitan las
características extendidas de
InsertMenuItem.
Inserta un nuevo ítem de menú en
la posición especificada de un
InsertMenuItem menú. Usa una estructura
MENUITEMINFO para crear el
ítem.
Modifica un ítem de menú
existente. Esta función ha sido
sustituida por SetMenuItemInfo.
ModifyMenu De todos modos, se puede seguir
usando si no se necesitan las
características extendidas de
SetMenuItemInfo.
Modifica la información sobre un
ítem de menú. Usa una estructura
SetMenuItemInfo
MENUITEMINFO para modificar el
ítem.
Borra un ítem del menú
especificado. Si el ítem de menú
abre un menú o submenú, esta
DeleteMenu
función destruye el manipulador
del menú o submenú y libera la
memoria usada por él.
Si se modifica un menú que actualmente se está visualizando, se
debe llamar a la función DrawMenuBar para actualizar la ventana y
reflejar los cambios.
El menú de sistema
El menú de sistema, también llamado menú de ventana o menú
de control, es el que se muestra cuando se hace clic sobre el icono
de la aplicación, o cuando se pulsa Alt+espacio.
Cuando se crea una aplicación, Windows asigna siempre el
menú de sistema por defecto, siempre que se especifique el estilo
WS_SYSMENU al crear la ventana.
A diferencia de los menús que hemos usado hasta ahora, el
menú de sistema genera mensajes WM_SYSCOMMAND, en lugar
de mensajes WM_COMMAND, y generalmente, salvo que
modifiquemos el menú de sistema, dejaremos que el proceso por
defecto se encargue de esos mensajes, mediante la función
DefWindowProc.
case WM_SYSCOMMAND:
switch(LOWORD(wParam)) {
case CM_OPCION1:
break;
case CM_OPCION2:
break;
case CM_OPCION3:
break;
case CM_OPCION4:
break;
default:
return DefWindowProc(hwnd, msg, wParam,
lParam);
}
break;
Ejemplo 46
Destrucción de menús
Los menús asociados a ventanas se destruyen automáticamente
cuando se destruyen las ventanas, de modo que sólo es necesario
destruir los menús que no estén asociados a ninguna ventana, en
general, serán los que usemos como menús flotantes, o como
menús alternativos, por ejemplo, si disponemos de diferentes
versiones de menús para nuestras ventanas, y los usamos
aternativamente, en función de las circunstancias.
Para destruir uno de esos menús se usa la función DestroyMenu.
Mensajes de menú
Ya hemos visto algunos de los mensajes que generan los
menús, en concreto los que generan cuando se seleccionan ítems:
Función Descripción
Es enviado cuando el usuario
WM_COMMAND selecciona un comando de un
ítem de un menú.
Cuando el usuario elige un
WM_SYSCOMMAND comando desde el menú de
ventana.
Pero hay más, algunos de los más interesantes pueden ser:
Función Descripción
Se envía cuando un menú
se va a activar. Eso ocurre
cuando el usuario hace clic
sobre un ítem de la barra de
WM_INITMENU
menú o cuando presiona la
tecla de menú. Nos permite
modificar el menú antes de
que se muestre.
Se envía cuando un menú
emergente o un submenú
va a ser activado. Esto
WM_INITMENUPOPUP permita a una aplicación
modificar el menú antes de
que sea mostrado, sin
modificar el menú completo.
Se envía a la ventana
cuando el usuario
WM_MENUSELECT
selecciona un ítem del
menú.
WM_CONTEXTMENU Notifica a una ventana que
el usuario ha hecho clic con
el botón derecho del ratón
en la ventana.
infoMenu.cbSize = sizeof(MENUITEMINFO);
infoMenu.fMask = MIIM_TYPE | MIIM_ID;
infoMenu.fType = MFT_BITMAP;
infoMenu.wID = CM_OPCION;
infoMenu.dwTypeData = (LPSTR) (hBitmap);
InsertMenuItem(hmenu, 0, TRUE, &infoMenu);
Ejemplo 47
Capítulo 38 La memoria
Windows tiene su manera particular de manejar la memoria del
sistema. Esto es lógico, y era de esperar, ya que como recurso que
es, la memoria también debe ser controlada y administrada por el
sistema operativo.
Memoria virtual
Para administrar la memoria, en el API32, Windows mantiene un
espacio virtual de memoria con direcciones de 32 bits para cada
proceso. Esto permite que cada uno de los procesos disponga de
hasta cuatro gigabytes de memoria. Dos de esos gigas están
disponibles para el usuario, los correspondientes a las primeras
direcciones de memoria; los otros dos están reservados para el
núcleo del sistema.
Se trata de un espacio virtual, esto quiere decir que las
aplicaciones no acceden directamente a memoria, y que las
direcciones que manejamos no corresponden a direcciones de
memoria física. El sistema se encarga de mapear esas direcciones
virtuales a direcciones físicas. En esto, como en todos los recursos,
el sistema operativo trabaja como intermediario entre el usuario y el
hardware.
Este modo de trabajar proporciona a cada aplicación gran
cantidad de memoria, de hecho, en la mayoría de los casos
proporciona más memoria de la disponible físicamente. Para
mantener toda esa memoria se trabaja con un fichero en disco como
almacén de memoria complementaria: el fichero de paginación o
fichero de intercambio.
El sistema de memoria virtual trabaja con unidades de memoria
llamadas páginas. El tamaño de cada página varía dependiendo del
tipo de ordenador, (en microprocesadores de la familia x86 suele ser
de 4 KB). Mientras es posible, las páginas de memoria se asignan a
cada proceso desde la memoria física, pero cuando es necesaria
más memoria, los procesos inactivos pueden copiar algunas de sus
páginas en el fichero de intercambio y liberar de ese modo parte de
la memoria física que se puede asignar al proceso activo.
Todo esto es transparente para el usuario, que sólo notará que
cuando es sistema está muy cargado aumenta la actividad del disco
y disminuye la velocidad del sistema.
Nos interesa saber, de todos modos, que cada página de
memoria virtual asociada a un proceso puede estar en uno de tres
posibles estados:
Nota:
Un poco de historia
Vamos a ver algunos conceptos que tal vez te suenen, pero que
en el API de Win32 han quedado obsoletos. En cualquier caso,
puede ser conveniente saber algo sobre ellos, recordar sus
aplicaciones y comprender por qué ya no son necesarios.
int *puntero;
...
/* Reservar memoria sin asignar almacenamiento físico */
puntero = VirtualAlloc(NULL, 100 * sizeof(int),
MEM_RESERVE, PAGE_NOACCESS);
/* Acomodar físicamente memoria previamente reservada */
VirtualAlloc(puntero, 100*sizeof(int), MEM_COMMIT,
PAGE_READWRITE);
/* Reservar y acomodar espacio para memoria de una vez */
puntero = VirtualAlloc(NULL, 100 * sizeof(int),
MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);
int *puntero;
/* Libera memoria */
VirtualFree(puntero, 0, MEM_RELEASE);
/* Desasigna memoria, la memoria sigue reservada */
VirtualFree(puntero, 100*sizeof(int), MEM_DECOMMIT);
Bloquear páginas de memoria asignada
int *puntero;
...
VirtualLock(puntero, 100*sizeof(int));
...
VirtualUnlock(puntero, 100*sizeof(int));
int *puntero;
DWORD protant; /* Valor anterior de la protección */
int *puntero;
MEMORY_BASIC_INFORMATION mbi;
VirtualQuery(puntero, &mbi,
sizeof(MEMORY_BASIC_INFORMATION));
Ejemplo 48
Capítulo 39 Control Edit
avanzado
En este capítulo, y en los siguientes vamos a comentar con más
detalles los controles básicos que ya hemos visto previamente.
Veremos algunas características más avanzadas de cada uno, los
mensajes de notificación, y los mensajes que aún no conocemos de
cada uno.
Empezaremos por el control edit, y veremos todo lo que no se
explicó en el capítulo 7.
HWND hctrl;
...
case WM_CREATE:
hInstance = ((LPCREATESTRUCT)lParam)->hInstance;
/* Insertar control Edit */
hctrl = CreateWindowEx(
0,
"EDIT", /* Nombre de la clase */
"", /* Texto del título, no tiene
*/
ES_LEFT | WS_CHILD | WS_VISIBLE | WS_BORDER |
WS_TABSTOP, /* Estilo */
36, 20, /* Posición */
120, 20, /* Tamaño */
hwnd, /* Ventana padre */
(HMENU)ID_TEXTO, /* Identificador del control
*/
hInstance, /* Instancia */
NULL); /* Sin datos de creación de
ventana */
/* Inicialización de los datos de la aplicación
*/
SetDlgItemText(hwnd, ID_TEXTO, "Inicial");
SetFocus(hctrl);
return 0;
Ejemplo 49
Contoles edit de sólo lectura
Uno de los estilos que se pueden aplicar a un control edit es el
de sólo lectura. ES_READONLY. Cuando se activa estilo el
contenido del control no podrá ser modificado por el usuario.
Esto es, aparentemente, una contradicción. Bien pensado, un
control edit cuyo contenido no puede ser modificado es un control
estático. Sin embargo, en determinadas circunstancias puede que
no sea tan absurdo, sobre todo si tenemos en cuenta que este estilo
se puede modificar durante la ejecución. Esto puede ser útil si en
ciertas situaciones, determinados valores están predefinidos. Por
ejemplo, dependiendo de la opción seleccionada en un conjunto de
RadioButtons, determinadas entradas de texto pueden ser
innecesarias, o tener valores predefinidos o predecibles, que no
necesitan ser editados. Otro ejemplo puede ser un programa en el
que, dependiendo del nivel de privilegios de un usuario,
determinados valores puedan o no ser modificados.
Además, si queremos ser precisos, un control edit de sólo lectura
no es en todo equivalente a un control estático. Por ejemplo, el texto
del control edit siempre puede ser marcado y copiado al
portapapeles, algo que no se puede hacer con los textos de los
controles estáticos.
Para modificar esta opción para un control edit se envía un
mensaje EM_SETREADONLY.
Para averiguar si un control edit tiene el estilo ES_READONLY
se debe usar la función GetWindowLong usando la constante
GWL_STYLE.
Para ilustar esto, modificaremos el ejemplo 5 para añadir un
checkbox que active y desactive el control edit. Empezaremos por
modificar la definición del diálogo en el fichero de recursos:
Ejemplo 50
Leer contraseñas
A veces no nos interesa que el texto que se introduce en un
control edit no se muestre en pantalla de forma que pueda ser
reconocido. El caso más frecuente es cuando se introducen
contraseñas. En esos casos se hace que el texto introducido se
sustituya por otros caracteres. El usuario que introduce la
contraseña sabe qué escribe, porque es el que maneja el teclado,
pero una persona que observe este proceso no podrá reconocer el
texto en pantalla, y le resultará complicado deducir el texto mirando
el teclado.
Para que un control edit se comporte de este modo bastará con
activar el estilo ES_PASSWORD al crear el control.
Las funciones para asignar valores iniciales o recuperarlos del
control funcionanrán igual que con los controles normales, el estilo
sólo afecta al modo en que se visualiza el texto, no a su contenido.
Por defecto, el carácter que se usa para sustituir los introducidos
es el asterisco, pero esto se puede modificar usando el mensaje
EM_SETPASSWORDCHAR. Si se utiliza un carácter nulo se
mostrará el texto que introduzca el usuario.
También podemos usar el mensaje EM_GETPASSWORDCHAR
para averiguar el carácter que se usa actualmente para sustituir lo
introducidos por el usuario.
Estos mensajes sólo están disponibles para controles edit de una
línea.
Crearemos otro programa de ejemplo basado en el ejemplo 5.
En este caso añadiremos tres RadioButtons con tres opciones
distintas de caracteres: el '*', el '·' y el nulo.
El primer paso es modificar el fichero de recursos para añadir los
tres botones:
Ejemplo 51
Mayúsculas y minúsculas
Disponemos de dos estilos para los controles edit que nos sirven
para limitar el tipo de caracteres que se pueden usar. El estilo
ES_LOWERCASE convierte cualquier carácter en mayúscula a
minúscula. El contenido del control edit será sólo de letras en
minúsculas, números y caracteres especiales. El estilo
ES_UPPERCASE convierte cualquier carácter en minúscula a
mayúsculas.
El estilo no afecta al contenido de los caracteres del control,
podemos asignar valores con mayúsculas o minúsculas con
cualquiera de los estilos. Estos estilos afectan sólo a los nuevos
caracteres introducidos por el usuario.
Los estilos se pueden asignar en el momento de la creación del
control, como hemos hecho hasta ahora mediante ficheros de
recursos o mediante las funciones CreateWindow y
CreateWindowEx, o se pueden modificar durante la ejecución,
usando la función SetWindowLong, y la función GetWindowLong
para obtener el estilo actual:
Ejemplo 52
Mensajes de notificación
Windows envía un tipo especial de mensajes, denominados
mensajes de notificación, a la ventana padre de un control edit.
Estos mensajes sirven para informar a la aplicación de
determinadas circunstancias relativas a un control.
Los mensajes de notificación se reciben a través de un mensaje
WM_COMMAND. En la palabra de menor peso del parámetro
wParam se envía el identificador del control. El manipulador del
control se envía en el parámetro lParam y el código del mensaje de
notificación en la palabra de mayor peso de wParam.
Nota:
Modificación
Actualización
Cada vez que el usuario modifica el texto del un control Edit, y
antes de que este nuevo texto se muestre en pantalla, Windows
envía un mensaje EN_UPDATE.
Este mensaje está pensado para permitir a la aplicación
redimensionar el tamaño del control en función de su contenido.
Falta espacio
Texto máximo
case WM_COMMAND:
switch(LOWORD(wParam)) {
case ID_TEXTO:
/* Mensajes de notificación */
switch(HIWORD(wParam)) {
case EN_MAXTEXT:
MessageBox(hwnd, "Imposible insertar
más caracteres", "Control edit", MB_OK);
break;
case EN_ERRSPACE:
...
case EN_HSCROLL:
...
case EN_VSCROLL:
...
case EN_KILLFOCUS:
...
case EN_SETFOCUS:
...
case EN_UPDATE:
...
case EN_CHANGE:
...
}
...
El buffer de texto
Hasta ahora no nos hemos preocupado nunca del espacio de
memoria necesario para almacenar y editar el contenido de un
control edit. Windows se encarga de crear un buffer, y de aumentar
su tamaño si es necesario, hasta cierto límite, dependiendo del tipo
de control edit.
En el capítulo 7 vimos que podíamos fijar el límite máximo que el
usuario podía editar mediante el mensaje EM_LIMITTEXT.
Sin embargo este mensaje no limita el tamaño del buffer. El
mensaje no tiene efecto si el control ya contiene más caracteres que
el límite establecido, y sigue siendo posible insertar más caracteres
usando el mensaje WM_SETTEXT. De hecho, este mensaje no
debería usarse, ya que ha sido sustituido por EM_SETLIMITTEXT.
Para limitar el tamaño del buffer se usa el mensaje
EM_SETLIMITTEXT, y para obtener el valor del tamaño del buffer
se usa el mensaje EM_GETLIMITTEXT.
En versiones de Windows de 16 bits es posible asumir, por parte
de nuestra aplicación, todas las operaciones de control del buffer de
memoria asociado a un control edit multilínea. Para ello, lo primero
que debemos hacer es crear el control edit en una ventana que use
el estilo DS_LOCALEDIT. Además disponemos de los mensajes
EM_GETHANDLE y EM_SETHANDLE, para obtener un
manipulador de memoria local del buffer del control, o asignar uno
nuevo, respectivamente.
El proceso consiste en:
Controles multilínea
Hasta ahora sólo hemos trabajado con controles edit de una
línea, pero también es posible crear controles edit multilínea. Para
ello bastará con crearlos con el estilo ES_MULTILINE. Pero estos
controles tienen algunas peculiaridades que los hace algo más
complicados de usar que los de una línea.
Para empezar, cuando se ejecuta un cuadro de diálogo, la tecla
ENTER tiene el efecto de activar el botón por defecto. Esto nos crea
un problema con los controles edit multilínea, ya que no podremos
usar la tecla de [ENTER] para insertar un retorno de línea. Para
evitar este comportamiento por defecto en los cuadros de diálogo se
usa el estilo ES_WANTRETURN. Este estilo hace que las
pulsaciones de la tecla ENTER, cuando el control tiene el foco del
teclado, se conviertan en retornos de línea, y no se active el botón
por defecto.
Otro detalle importante es que con frecuencia el texto no va a
caber en el área visible del control, por lo que tendremos que
desplazar el contenido tanto horizontal como verticalmente.
Para lograr esto disponemos, por una parte, de dos estilos
propios de los controles edit: ES_AUTOHSCROLL y
ES_AUTOVSCROLL. Cuando se activan estos estilos el texto se
desplaza de forma automática en sentido horizontal o vertical,
respectivamente, cada vez que el usuario llegue a un borde del área
del control mientras escribe texto.
Además de esta posibilidad tenemos una segunda que consiste
en añadir las barras de desplazamiento. Estas barras se añaden con
los estilos de ventana WS_HSCROLL y WS_VSCROLL,
respectivamente, y activan de forma automática los dos estilos
anteriores: ES_AUTOHSCROLL y ES_AUTOVSCROLL.
La diferencia es que de esta segunda forma se muestran las
barras, y de la primera no.
/* Crear buffer */
buffer = (char *)malloc(strlen(texto)+1);
buffer[0] = 0;
/* Hacer la lectura */
SustituirCambiosDeLinea(texto, buffer);
/* Hacer la lectura */
SustituirCambiosDeLinea(texto, buffer);
/* Desbloquear buffer */
LocalUnlock(hloc);
int longitud;
pos =
SendMessage(hctrl
,
EM_GETFIRSTVISIBL
ELINE, 0, 0);
sprintf(mensaje,
"Primera línea
visible = %d",
pos);
MessageBox(hwnd,
Selección múltiple mensaje, "Control
edit multilínea",
MB_OK);
char linea[512];
int longitud;
fs = fopen(fichero, "w");
if(fs) {
nLineas = SendMessage(hctrl, EM_GETLINECOUNT, 0, 0);
for(i = 0; i < nLineas; i++) {
*(WORD*)linea = 1024;
longitud = SendMessage(hctrl, EM_GETLINE,
(WPARAM)i, (LPARAM)linea);
linea[longitud] = 0;
fprintf(fs, "%s\n", linea);
}
fclose(fs);
}
}
int pos;
int pos;
Ejemplo 53
Operaciones sobre selecciones de texto
El usuario puede seleccionar una parte del texto incluido en un
control edit, bien usando el ratón (que será lo más frecuente), o bien
mediente el teclado, (manteniendo pulsada la tecla de mayúsculas y
desplazando el cursor mediante el las teclas de movimiento del
cursor).
Estamos acostumbrados ya a las operaciones frecuentes que se
pueden hacer sobre una selección: copiar, cortar, borrar o pegar.
Veamos ahora cómo podemos realizar estas operaciones en
nuestros controles edit.
Cualquier control edit procesará los mensajes WM_CUT,
WM_COPY, WM_CLEAR y WM_PASTE que reciba.
En el caso del mensaje WM_CUT, se copiará el texto
seleccionado en el portapapeles y después se eliminará.
En el caso del mensaje WM_COPY, el texto seleccionado se
copiará en el portapapeles.
En el caso del mensaje WM_CLEAR, el texto seleccionado se
eliminará, sin copiarse en el portapapeles.
Y en el caso del mensaje WM_PASTE, el texto que esté en el
portapapeles se copiará en la posición actual del caret en el control
edit o sustituyendo al texto seleccionado, si existe.
Estas acciones están ya implementadas en el procedimiento de
ventana de la clase "EDIT", por lo que bastará con enviar cualquiera
de estos mensajes a un control para que funcionen.
Es más, las versiones actuales de Windows desplegarán un
menú contextual al pulsar el botón derecho del ratón sobre cualquier
control edit. Ese menú contendrá estos cuatro comandos: cortar,
copiar, pegar y eliminar, y otros dos: deshacer y seleccionar todo.
También funcionan, cuando el control edit tiene el foco, las
combinaciones de teclas para estas seis acciones: [control]+x para
cortar, [control]+c para copiar, [control]+v para pegar y [supr] para
borrar y [control]+z para deshacer.
Por nuestra parte, podemos añadir estos mensajes a nuestros
menús, y enviarlos al control edit que tenga el foco en el momento
en que se seleccionen.
Además, disponemos de otros mensajes para controlar la
selección de texto.
El mensaje EM_GETSEL nos sirve para obtener los índices de
los caracteres correspondientes al inicio y final de la selección
actual. Más concretamente, obtendremos el índice del primer
carácter de la selección y el del primero no seleccionado a
continuación de la selección. Por ejemplo, si la selección incluye los
caracteres 10º al 15º de texto de un control, obtendremos los
valores 9 y 15:
Texto de ejemplo para ilustrar la selección.
00000000001111111111222222222233333333334444
01234567890123456789012345678901234567890123
char nuevotexto[64];
BOOL modif;
case WM_COMMAND:
switch(LOWORD(wParam)) {
case CM_GUARDAR:
Guardar(hctrl, "texto.txt");
SendMessage(hctrl, EM_SETMODIFY, FALSE, 0);
ActualizarMenu(hwnd, hctrl);
break;
case ID_TEXTO:
if(HIWORD(wParam) == EN_CHANGE)
if(EnableMenuItem(hctrl, EM_GETMODIFY, 0,
0))
EnableMenuItem(GetMenu(hwnd),
CM_GUARDAR, MF_BYCOMMAND | MF_ENABLED);
break;
...
Márgenes y tabuladores
Si no indicamos nada, el control edit usará toda la superficie de
su área de cliente para mostrar el texto. Pero podemos cambiar esto
de varias formas, definiendo los márgenes izquierdo y derecho, o
especificando un rectángulo, dentro del área de cliente, que se
usará para mostrar el texto.
El mensaje EM_SETMARGINS nos permite fijar los márgenes
izquierdo y/o derecho del texto dentro del control edit. En el
parámetro wParam indicamos qué márgenes vamos a definir, y en
qué unidades se expresan. Para ello podemos combinar los valores
EC_LEFTMARGIN, EC_RIGHTMARGIN y EC_USEFONTINFO. El
primero para definir el margen izquierdo, el segundo para definir el
derecho, y el tercero para indicar que usaremos la anchura del
carácter "A" de la fuente actual para el margen izquierdo, y el del
carácter "C" para el derecho. Si no usamos este valor, el parámetro
lParam indicará la anchura en pixels.
El parámetro lParam indica el márgen izquierdo en la palabra de
menor peso, y el derecho en la de mayor peso. Para combinar estos
valores se puede usar la macro MAKELONG.
SendMessage(hctrl, EM_SETMARGINS,
EC_LEFTMARGIN | EC_RIGHTMARGIN, MAKELONG(50, 30));
GetClientRect(hctrl, &re);
re.left += 60;
re.top += 20;
re.right -= 40;
re.bottom -= 40;
SendMessage(hctrl, EM_SETRECT, 0, (LPARAM)&re);
Desplazar texto
Ya vimos más arriba, cuando hablamos del mensaje para
seleccionar texto, que podemos desplazar el contenido de un control
edit hasta el punto donde se encuentre el caret, con el fin de hacerlo
visible. Para esto usamos el mensaje EM_SCROLLCARET:
...
SendMessage(hctrl, EM_SETSEL, 1500, 1559);
SendMessage(hctrl, EM_SCROLLCARET, 0, 0);
...
Ejemplo 54
Caracteres y posiciones
Por último, disponemos de dos mensajes que relacionan los
puntos físicos de la pantalla con los caracteres que ocupan esas
posiciones. El mensaje EM_CHARFROMPOS nos devuelve el
índice del carácter situado en las coordenadas especificadas.
Las coordenadas se proporcionan en el parámetro lParam, la x
en la palabra de menor peso, y la y en la de mayor peso. Para crear
un valor LPARAM a partir de las coordenadas podemos usar la
macro MAKELPARAM.
El valor obtenido contiene en la palabra de mayor peso el índice
de la línea, y en la de menor peso, el índice del carácter:
LRESULT punto;
char mensaje[128];
...
punto = SendMessage(hctrl, EM_POSFROMCHAR, 153, 0);
sprintf(mensaje, "Coordenadas del carácter 153: "
"(%d, %d)", LOWORD(punto), HIWORD(punto));
MessageBox(hwnd, mensaje, "EM_POSFROMCHAR", MB_OK);
Ejemplo 55
Capítulo 40 Control List box
avanzado
Insertar controles list box durante la
ejecución
Al igual que vimos con los controles edit, también es posible
insertar controles list box durante la ejecución. En el caso del control
list box tendremos que insertar una ventana de la clase "LISTBOX".
Para insertar el control también usaremos las funciones
CreateWindow y CreateWindowEx.
HWND hctrl;
...
case WM_CREATE:
hInstance = ((LPCREATESTRUCT)lParam)->hInstance;
/* Insertar control Edit */
hctrl = CreateWindowEx(
0,
"LISTBOX", /* Nombre de la clase */
"", /* Texto del título, no tiene
*/
LBS_STANDARD | WS_CHILD | WS_VISIBLE |
WS_BORDER | WS_TABSTOP, /* Estilo */
9, 19, /* Posición */
104, 99, /* Tamaño */
hwnd, /* Ventana padre */
(HMENU)ID_LISTA, /* Identificador del control
*/
hInstance, /* Instancia */
NULL); /* Sin datos de creación de
ventana */
/* Inicialización de los datos de la aplicación
*/
SendMessage(hctrl, LB_ADDSTRING, 0,
(LPARAM)"Cadena nº 1");
SendMessage(hctrl, LB_ADDSTRING, 0,
(LPARAM)"Cadena nº 4");
SendMessage(hctrl, LB_ADDSTRING, 0,
(LPARAM)"Cadena nº 3");
SendMessage(hctrl, LB_ADDSTRING, 0,
(LPARAM)"Cadena nº 2");
SendMessage(hctrl, LB_SELECTSTRING, (UINT)-1,
(LPARAM)Datos.Item);
SetFocus(hctrl);
return 0;
Ejemplo 56
Mensajes de notificación
Los list box también envían mensajes de notificación para
informar sobre determinados eventos.
Los mensajes de notificación se reciben a través de un mensaje
WM_COMMAND. En la palabra de menor peso del parámetro
wParam se envía el identificador del control. El manipulador del
control se envía en el parámetro lParam y el código del mensaje de
notificación en la palabra de mayor peso de wParam.
Nota:
Doble clic
Cada vez que el usuario hace doble clic sobre uno de los ítems
de un list box, se envía un mensaje de notificación LBN_DBLCLK a
la ventana padre.
Falta espacio
Selección y deselección
int i;
...
i = SendMessage(hctrl, LB_GETCURSEL, 0, 0);
Pero existen otros mensajes que suelen ser muy útiles a la hora
de usar list boxes:
Por ejemplo, podemos eliminar líneas mediante el mensaje
LB_DELETESTRING, en el que indicaremos en el parámetro
wParam el valor del índice a eliminar.
/* Eliminar cadena actualmente seleccionada */
int i;
...
i = SendMessage(hctrl, LB_GETCURSEL, 0, 0);
SendMessage(hctrl, LB_DELETESTRING, (WPARAM)i, 0);
Ejemplo 57
El dato del ítem
En todos los ejemplos que hemos visto siempre hemos
recuperado cadenas de un list box, pero también podemos trabajar
con índices. Por ejemplo, crearemos un programa que nos muestre
las capitales y superficies de varios países, que podremos
seleccionar de un list box. Para ello almacenaremos esos datos en
un array:
struct Pais {
char *Nombre;
char *Capital;
int Superficie;
} paises[22] =
{
"Argentina", "Buenos Aires", 2766890,
"Mexico", "Mexico DC", 1972550,
"Brasil", "Brasilia", 8514876,
"Peru", "Lima", 1285220,
"Colombia", "Bogotá", 1138910,
"Bolivia", "La Paz", 1098580,
"Venezuela", "Caracas", 912050,
"Chile", "Santiago", 756096,
"España", "Madrid", 504782,
"Paraguay", "Asunción", 406750,
"Ecuador", "Quito", 283560,
"Uruguay", "Montevideo", 176220,
"Nicaragua", "Managua", 129494,
"Honduras", "Tegucigalpa", 112090,
"Cuba", "La Habana", 110860,
"Guatemala", "Guatemala", 108890,
"Portugal", "Lisboa", 92391,
"Panamá", "Panamá", 78200,
"Costa Rica", "San José", 51100,
"República Dominicana", "Santo Domingo", 48730,
"El Salvador", "San Salvador", 21040,
"Puerto Rico", "San Juan", 9104
};
case WM_PAINT:
i = SendMessage(hctrl, LB_GETCURSEL, 0, 0);
i = SendMessage(hctrl, LB_GETITEMDATA, (WPARAM)i,
0);
hdc = BeginPaint(hwnd, &ps);
SetBkMode(hdc, TRANSPARENT);
sprintf(cad, "País: %s", paises[i].Nombre);
TextOut(hdc, 300, 20, cad, strlen(cad));
sprintf(cad, "Capital: %s", paises[i].Capital);
TextOut(hdc, 300, 40, cad, strlen(cad));
sprintf(cad, "Superficie: %d Km²",
paises[i].Superficie);
TextOut(hdc, 300, 60, cad, strlen(cad));
EndPaint(hwnd, &ps);
break;
Ejemplo 58
Funciones para ficheros y directorios
Una de las aplicaciones más frecuentes de los list box es la
elección de ficheros. Por ese motivo, el API proporciona algunas
funciones para iniciar y seleccionar ítems en un list box a partir de
los datos de directorios.
La función DlgDirList nos permite iniciar el contenido de un list
box a partir de los ficheros, carpetas, unidades de disco, etc.
Esta función necesita cinco parámetros. El primero es un
manipulador de la ventana o diálogo que contiene el list box que
vamos a inicializar. El segundo es un puntero a una cadena con el
camino del directorio a mostrar. Esta cadena tiene que tener espacio
suficiente, ya que la función puede modificar su contenido. El tercer
parámetro es el identificador del list box. El cuarto el identificador de
un control estático, que se usa para mostrar el camino actualmente
mostrado en el list box. El último parámetro nos permite seleccionar
el tipo de entradas que se mostrarán en el list box.
Mediante este último parámetro podemos restringir el tipo de
entradas, impidiendo o permitiendo que se muestren directorios o
unidades de almacenamiento, o limitando los atributos de los
ficheros y directorios a mostrar.
Ya hemos dicho que se necesita un control estático. Como aún
no hemos visto el modo de insertar estos controles directamente en
la ventana, para este ejemplo lo haremos sin más explicaciones,
aunque como se puede ver, no tiene nada de raro:
HWND hestatico;
...
hestatico = CreateWindowEx(
0,
"STATIC", /* Nombre de la clase */
"", /* Texto del título, no tiene
*/
WS_CHILD | WS_VISIBLE | WS_BORDER |
WS_TABSTOP, /* Estilo */
9, 4, /* Posición */
344, 18, /* Tamaño */
hwnd, /* Ventana padre */
(HMENU)ID_TITULO,/* Identificador del control
*/
hInstance, /* Instancia */
NULL); /* Sin datos de creación de
ventana */
SendMessage(hestatico, WM_SETFONT, (WPARAM)hfont,
MAKELPARAM(TRUE, 0));
Por supuesto, podemos usar los comodines '*' y '?' para los
nombres de fichero.
Veamos un ejemplo para uniciar un list box a partir del directorio
actual, mostrando los discos y directorios, y limitando los ficheros a
los que se ajusten a *.c:
...
IniciarLista(hwnd, "*.c");
...
strcpy(path, p);
DlgDirList(
hwnd, /* manipulador de cuadro de diálogo con
list box */
path, /* puntero a cadena de camino o nombre
de fichero */
ID_LISTA, /* identificador de list box
*/
ID_TITULO, /* identificador de control estático
*/
DDL_DIRECTORY | DDL_DRIVES /* atributos de ficheros a
mostrar */
);
}
case WM_COMMAND:
switch(LOWORD(wParam)) {
case ID_LISTA:
switch(HIWORD(wParam)) {
case LBN_DBLCLK:
if(DlgDirSelectEx(hwnd, cad, 512,
ID_LISTA)) {
strcat(cad, "*.c");
IniciarLista(hwnd, cad);
} else
else
MessageBox(hwnd, cad, "Fichero
seleccionado", MB_OK);
break;
...
strcpy(path, p);
strcpy(path, "*.h");
SendMessage(GetDlgItem(hwnd, ID_LISTA), LB_DIR,
(WPARAM)0, (LPARAM)path);
...
SendMessage(GetDlgItem(hwnd, ID_LISTA), LB_ADDFILE,
0, (LPARAM)"*.exe");
Ejemplo 59
Listbox de selección sencilla y múltiple
Bien, hasta ahora sólo hemos hablado de list boxes de selección
sencilla. En estos list boxes sólo se puede seleccionar un ítem (o
ninguno), y cada nueva selección anula a la anterior.
Pero también podemos crear list boxes de selección múltiple, en
los que será posible seleccionar rangos de ítems, o varios ítems,
aunque no estén seguidos.
Para ello, lo primero será crear el list box con el estilo
LBS_MULTIPLESEL o el estilo LBS_EXTENDEDSEL.
Ambos estilos definen listas de selección múltiple. La diferencia
está en el modo en que se seleccionan los ítems.
En un list box con el estilo LBS_MULTIPLESEL la selección se
hace de manera individual, cada vez que se pulsa sobre un ítem no
seleccionado, pasa a estado seleccionado, y viceversa.
En un list box con el estilo LBS_EXTENDEDSEL, las selecciones
se comportan de la misma forma que en un list box de selección
sencilla. Pero mediante las teclas de mayúculas y de control
podemos hacer selecciones múltiples. La tecla de mayúsculas nos
permite seleccionar rangos. El último ítem seleccionado se comporta
como un "ancla", de modo que si pulsamos la tecla de mayusculas y
pulsamos con el ratón sobre otro ítem se seleccionarán todos los
ítems entre en "ancla" y el actual.
Por otra parte, la tecla de control permite usar el list box del
mismo modo que si tuviese el estilo LBS_MULTIPLESEL. Con la
tecla de control pulsada, si pulsamos sobre el ratón en un ítem
seleccinado, se deseleccionará, y viceversa.
Selecciones
int nSeleccionados;
...
nSeleccionados = SendMessage(hlista, LB_GETSELCOUNT, 0,
0);
int nSeleccionados;
int *seleccionado;
...
nSeleccionados = SendMessage(hlista, LB_GETSELCOUNT, 0,
0);
/* Obtener memoria para nSeleccionados índices */
seleccionado = (int *)malloc(nSeleccionados*sizeof(int));
/* Obtener la lista */
SendMessage(hlista, LB_GETSELITEMS,
(WPARAM)nSeleccionados, (LPARAM)seleccionado);
/* Tratamiento */
for(i = nSeleccionados-1; i >= 0; i--) {
SendMessage(hlista, LB_GETTEXT,
(WPARAM)seleccionado[i], (LPARAM)cad);
}
/* Liberar buffer */
free(seleccionado);
int n, i;
char cad[64];
...
n = SendMessage(hlista, LB_GETCOUNT, 0, 0);
for(i = 0; i < n; i++) {
if(SendMessage(hlista, LB_GETSEL, (WPARAM)i, 0)) {
SendMessage(hlista, LB_GETTEXT, (WPARAM)i,
(LPARAM)cad);
MessageBox(hwnd, cad, "Ítem seleccionado", MB_OK);
}
}
int n, i;
char cad[64];
...
/* Seleccionar ítems de posiciones pares (con índice
impar) */
n = SendMessage(hlista1, LB_GETCOUNT, 0, 0);
for(i = 0; i < n; i++) {
SendMessage(hlista1, LB_SETSEL, (WPARAM)i%2,
(LPARAM)i);
}
int n;
...
n = SendMessage(hlista1, LB_GETCOUNT, 0, 0);
SendMessage(hlista1, LB_SELITEMRANGE, (WPARAM)TRUE,
MAKELPARAM(0, n));
int i;
char cad[64];
...
case CM_ANCLA:
i = SendMessage(hlista2, LB_GETANCHORINDEX, 0, 0);
SendMessage(hlista2, LB_GETTEXT, (WPARAM)i,
(LPARAM)cad);
MessageBox(hwnd, cad, "Ítem ancla", MB_OK);
break;
case CM_CARET:
i = SendMessage(hlista2, LB_GETCARETINDEX, 0, 0);
SendMessage(hlista2, LB_GETTEXT, (WPARAM)i,
(LPARAM)cad);
MessageBox(hwnd, cad, "Ítem caret", MB_OK);
break;
...
int n;
...
n = SendMessage(hlista2, LB_GETCOUNT, 0, 0);
SendMessage(hlista2, LB_SETANCHORINDEX, 0, 0);
SendMessage(hlista2, LB_SETCARETINDEX, (WPARAM)n, 0);
Ejemplo 60
List box sin selección
Además de los list box de selección sencilla y de selección
múltiple, también es posible crear list boxes sin selección. Para ello
bastará con crear el list box con el estilo LBS_NOSEL.
Podemos usar estos list boxes para mostrar listas de valores
para una consulta por parte del usuario, pero que no precisen una
selección.
Ejemplo 61
Paradas de tabulación
En principio no es posible crear list boxes en los que a cada fila
corresponda un ítem, y que para cada ítem se creen varias
columnas con informaciones diferentes. (Esto es algo que se hace
con otro tipo de control que veremos más adelante: los list view).
Sin embargo, existe una forma limitada de hacer algo parecido.
Consiste en usar el estilo LBS_USETABSTOPS, y en separar las
columnas dentro de cada ítem con caracteres de tabulación. Si no
se especifica este estilo, los caracteres de tabulación no se
expanden en espacios, y el list box no tendrá el aspecto de una
tabla.
Podemos definir la anchura de cada columna mediante el
mensaje LB_SETTABSTOPS, indicando en el parámetro wParam el
número de paradas de tabulación y en el parámetro lParam un array
con las separaciones de cada parada en pixels:
Ejemplo 62
Actualizaciones de gran número de ítems
Hay dos posibles situaciones de potencialmente peligrosas en
las las actualizaciones que afecten a muchos ítems en un list box.
Por una parte, el proceso puede requerir una cantidad importante
de memoria, cuando se añaden muchos ítems.
Por otra parte, el proceso puede requerir mucho tiempo, ya sea
porque se deben añadir muchos ítems o porque se deben hacer
muchas modificaciones que impliquen el borrado e inserción de
ítems.
Optimizar la memoria
Optimizar el tiempo
Ejemplo 63
Responder al teclado
Podemos hacer que nuestro control list box responda a
determinadas pulsaciones del teclado, cuando tenga el foco, usando
el estilo LBS_WANTKEYBOARDINPUT.
Mediante este estilo, la ventana padre del list box recibirá un
mensaje WM_VKEYTOITEM cada vez que el usuario pulse una
tecla.
Nuestro procedimiento de ventana o diálogo podrá procesar este
mensaje y actuar en consecuencia. En el parámetro wParam
recibiremos dos valores, en la palabra de menor peso el código de
tecla virtual de la tecla pulsada, y en la palabra de mayor peso, la
posición del caret. En el parámetro lParam recibiremos el
manipulador del control list box.
En el siguiente ejemplo podemos hacer que nuestro list box
responda a la tecla 'B' borrando el ítem actualmente seleccionado, y
a la tecla 'I' insertando de nuevo los valores iniciales:
case WM_VKEYTOITEM:
if((HWND)lParam == hctrl) { /* Asegurarse de que
el mensaje proviene de nuestra lista */
switch(LOWORD(wParam)) {
case 'B': /* Borrar actual */
i = SendMessage(hctrl, LB_GETCURSEL, 0, 0);
SendMessage(hctrl, LB_DELETESTRING,
(WPARAM)i, 0);
SetFocus(hctrl);
return -2; /* Tratamiento de tecla
terminado */
case 'I': /* Leer valores iniciales */
IniciarLista(hctrl);
SetFocus(hctrl);
return -2; /* Tratamiento de tecla
terminado */
}
}
return -1; /* Acción por defecto */
Ejemplo 64
Aspectos gráficos del list box
En cuanto al aspecto gráfico del list box tenemos otras opciones
que podemos controlar.
return tam.cx;
}
char item[300];
int x;
int eActual;
x = CalculaLongitud(hlista, cad);
if(x > eActual) eActual = x;
SendMessage(hlista, LB_ADDSTRING, 0, (LPARAM)cad);
SendMessage(hlista, LB_SETHORIZONTALEXTENT, eActual, 0);
Ajustar la altura de los ítems
SendMessage(hctrl, LB_SETITEMHEIGHT, 0,
MAKELPARAM(30,0));
InvalidateRect(hctrl, NULL, TRUE);
Items y coordenadas
int i;
...
i = SendMessage(hctrl, LB_ITEMFROMPOINT, 0,
MAKELPARAM(40, 123));
Ejemplo 65
Localizaciones
Ya hemos visto que en los controles list box los ítems se
muestran por orden alfabético, al menos en los que hemos usado
hasta ahora. Pero el orden alfabético no es algo universal, y puede
cambiar dependiendo del idioma.
Generalmente esto no nos preocupará, ya que el idioma usado
para elegir el orden se toma del propio sistema. Sin embargo, puede
haber casos en que nos interese modificar o conocer el idioma
usado en un list box.
Para obtener el valor de la localización actual se usa el mensaje
LB_GETLOCALE. El valor de retorno es un entero de 32 bits, en el
que la palabra de menor peso contiene el código de país, y el de
mayor peso el del lenguaje, este último a su vez, se compone de un
identificador de lenguaje primario y un identificador de sublenguaje.
Se pueden usar las macros PRIMARYLANGID y SUBLANGID
para obtener el identificador de lenguaje primario y el de
sublenguaje, respectivamente.
int i;
char cad[120];
...
i = SendMessage(hctrl, LB_GETLOCALE, 0, 0);
sprintf(cad, "País %d, id lenguaje primario %d, "
"id de sublenguaje %d",
HIWORD(i), PRIMARYLANGID(LOWORD(i)),
SUBLANGID(LOWORD(i)));
MessageBox(hwnd, cad, "Localización", MB_OK);
SendMessage(hctrl, LB_SETLOCALE,
MAKELCID(MAKELANGID(LANG_SPANISH,
SUBLANG_SPANISH),
SSORT_DEFAULT), 0);
Ejemplo 66
Otros estilos
Nos quedan algunas cosas que comentar sobre los estilos de los
controles list box.
Generalmente, hasta ahora, hemos basado nuestros list boxes
en el estilo LBS_STANDARD. En realidad, este estilo se define
como la combinación de dos estilos: LBS_SORT y LBS_NOTIFY.
El estilo LBS_SORT indica que los ítems en el list box deben
mostrarse ordenados alfabéticamente. El estilo LBS_NOTIFY indica
que se debe enviar un mensaje de notificación a la ventana padre
del control cada vez que el usuario haga clic o boble clic sobre un
ítem.
Si no se especifica el estilo LBS_SORT, los ítems se muestran
en el mismo orden en que se añaden, (excepto aquellos insertados
mediante el mensaje LB_INSERTSTRING).
Si no se especifica el estilo LBS_NOTIFY, la ventana padre no
recibirá los mensajes de notificación: LBN_DBLCLK,
LBN_SELCHANGE y LBN_SELCANCEL.
Además, aunque no se especifique, todos los controles list box
que no tengan un estilo owner-draw, se definen con el estilo
LBS_HASSTRINGS por defecto. Este estilo indica que los ítems
consisten en cadenas de texto, el sistema se encarga de mantener
la memoria para almacenar estas cadenas.
También podemos usar el estilo LBS_DISABLENOSCROLL, de
modo que la barra de desplazamiento vertical se muestre siempre,
aunque todos los ítems puedan ser visualizados. Este estilo no se
puede aplicar a los controles con el estilo LBS_MULTICOLUMN.
Por último, hay que comentar algo sobre las dimensiones
verticales de los controles list box. Cuando se crea uno de estos
controles, la altura del control no se respeta de forma exacta, sino
que se reduce lo necesario para que el list box contenga un número
exacto de ítems. Así, si por ejemplo, indicamos una altura de 184, y
cada ítem ocupa 18, la altura usada para crear el control será de
180, ya que en la distancia especificada caben 18 ítems, pero no 19.
De este modo no se mostrará una parte de un ítem, y el control se
dibujará más rápidamente.
Este comportamiento se puede evitar mediante el uso del estilo
LBS_NOINTEGRALHEIGHT. Los list boxes con este estilo se
crearán con la altura exacta indicada al definirlos.
hctrl = CreateWindowEx(
0,
"LISTBOX", /* Nombre de la clase */
"", /* Texto del título */
LBS_HASSTRINGS | LBS_STANDARD |
LBS_OWNERDRAWVARIABLE |
WS_CHILD | WS_VISIBLE | WS_BORDER |
WS_TABSTOP, /* Estilo */
9, 19, /* Posición */
320, 250, /* Tamaño */
hwnd, /* Ventana padre */
(HMENU)ID_LISTA, /* Identificador del control
*/
hInstance, /* Instancia */
NULL); /* Sin datos de creación de
ventana */
itemState indica el estado del ítem. El valor puede ser uno o una
combinación de los siguientes:
case WM_DRAWITEM:
lpdis = (LPDRAWITEMSTRUCT) lParam;
if(lpdis->itemID == -1) { /* Se trata de un
menú, no hacer nada */
break;
}
switch (lpdis->itemAction) {
case ODA_SELECT:
case ODA_DRAWENTIRE:
case ODA_FOCUS:
/* Borrar el contenido previo */
FillRect(lpdis->hDC, &lpdis->rcItem, (HBRUSH)
(COLOR_WINDOW+1));
/* Obtener datos de las medidas de la fuente
*/
GetTextMetrics(lpdis->hDC, &tm);
/* Calcular la coordenada y para escribir el
texto de ítem */
y = (lpdis->rcItem.bottom + lpdis->rcItem.top
- tm.tmHeight) / 2;
/* Los países cuya superficie sea mayor que
92391 km2 se muestran en verde,
el resto, en azul */
if(paises[lpdis->itemData].Superficie >
92391)
SetTextColor(lpdis->hDC, RGB(0,128,0));
else
SetTextColor(lpdis->hDC, RGB(0,0,255));
/* Mostrar el texto */
TextOut(lpdis->hDC, 6, y,
paises[lpdis->itemData].Nombre,
strlen(paises[lpdis->itemData].Nombre));
/* Si el ítem está seleccionado, trazar un
rectángulo negro alrededor */
if (lpdis->itemState & ODS_SELECTED) {
SetTextColor(lpdis->hDC, RGB(0,0,0));
DrawFocusRect(lpdis->hDC, &lpdis->rcItem);
}
}
break;
...
El mensaje WM_DELETEITEM
Ejemplo 67
Otros mensajes para list box con estilos
owner-draw
Disponemos de otros mensajes destinados a controles owner-
draw.
El mensaje LB_GETITEMHEIGHT se puede usar para obtener la
altura de los ítems en un list box owner-draw. Si el control tiene el
estilo LBS_OWNERDRAWFIXED tanto el parámetro lParam como
wParam deben ser cero. Si el control tiene el estilo
LBS_OWNERDRAWVARIABLE, el parámetro wParam debe
contener el índice del ítem cuya altura queramos recuperar.
De forma simétrica, disponemos del mensaje
LB_SETITEMHEIGHT para ajustar la altura de los ítems. Si se trata
de un control con el estilo LBS_OWNERDRAWFIXED debe
indicarse cero para el parámetro wParam, y la altura se especifica
en el parámetro lParam, para lo que será necesario usar la macro
MAKELPARAM:
SendMessage(hctrl, LB_SETITEMHEIGHT, 0, MAKELPARAM(23, 0));
HWND hctrl;
...
hctrl = CreateWindowEx(
0,
"BUTTON", /* Nombre de la clase */
"Botón 1", /* Texto del título */
BS_PUSHBUTTON | WS_CHILD | WS_VISIBLE | WS_BORDER |
WS_TABSTOP, /* Estilo */
9, 19, /* Posición */
95, 24, /* Tamaño */
hwnd, /* Ventana padre */
(HMENU)ID_BOTON, /* Identificador del control */
hInstance, /* Instancia */
NULL); /* Sin datos de creación de ventana
*/
SetFocus(hctrl);
Cambiar fuente
También es posible modificar la fuente de un control button
enviando un mensaje WM_SETFONT. El lugar apropiado es, por
supuesto, al procesar el mensaje WM_INITDIALOG al iniciiar un
cuadro de diálogo, o al procesar el mensaje WM_CREATE, al iniciar
una ventana.
En el parámetro wParam pasamos un manipulador de fuente, y
usaremos la macro MAKELPARAM para crear un valor LPARAM, en
el que especificaremos la opción de repintar el control, que se
almacena en la palabra de menor peso de LPARAM.
Esto nos permite modificar la fuente durante la ejecución,
reflejando los cambios en pantalla.
HWND hctrl;
HICON hIcono;
...
hctrl = CreateWindowEx(
0,
"BUTTON", /* Nombre de la clase */
"icono", /* Texto del título */
BS_ICON | BS_AUTOCHECKBOX | WS_CHILD | WS_VISIBLE |
WS_BORDER | WS_TABSTOP, /* Estilo */
9, 49, /* Posición */
95, 24, /* Tamaño */
hwnd, /* Ventana padre */
(HMENU)ID_BOTON2, /* Identificador del control */
hInstance, /* Instancia */
NULL); /* Sin datos de creación de ventana
*/
hIcono = LoadIcon(hInstance, "Icono");
SendMessage(hctrl, BM_SETIMAGE, (WPARAM)IMAGE_ICON,
(LPARAM)hIcono);
HWND hctrl;
HBITMAP hBitmap;
...
hctrl = CreateWindowEx(
0,
"BUTTON", /* Nombre de la clase */
"Bitmap", /* Texto del título */
BS_BITMAP | WS_CHILD | WS_VISIBLE | WS_BORDER |
WS_TABSTOP, /* Estilo */
9, 9, /* Posición */
87+4, 20+4, /* Tamaño */
hwnd, /* Ventana padre */
(HMENU)ID_BOTON1,/* Identificador del control */
hInstance, /* Instancia */
NULL); /* Sin datos de creación de ventana
*/
hBitmap = LoadBitmap(hInstance, "power");
SendMessage(hctrl, BM_SETIMAGE, (WPARAM)IMAGE_BITMAP,
(LPARAM)hBitmap);
HICON hIcono;
...
hIcono = (HICON)SendMessage(hctrl, BM_GETIMAGE,
IMAGE_ICON, 0);
Alineación de contenidos
Hasta ahora sólo hemos mencionado el estilo BS_CENTER, que
nos permitía situal el texto, icono o mapa de bits de un botón
centrado horizontalmente.
Existen otros estilos que nos permiten situar el contenido en
otros lugares. Tres de esos estilos permiten definir la alineación del
texto en el sentido horizontal: BS_LEFT a la izquierda, BS_CENTER
en el centro y BS_RIGHT a la derecha.
Otros tres estilos permiten definir la alineación en sentido
vertical: BS_TOP en la parte superior, BS_VCENTER centrado
verticalmente y BS_BOTTOM en la parte inferior.
Estos estilos se pueden combinar, eligiendo uno de cada grupo,
de modo que podemos elegir nueve posiciones diferentes para
situar el texto:
BS_CENTER |
BS_LEFT | BS_TOP BS_RIGHT | BS_TOP
BS_TOP
BS_LEFT | BS_CENTER | BS_RIGHT |
BS_VCENTER BS_VCENTER BS_VCENTER
BS_LEFT | BS_CENTER | BS_RIGHT |
BS_BOTTOM BS_BOTTOM BS_BOTTOM
Ejemplo 68
Mensajes de notificación
La clase Button dispone de muchos mensajes de notificación,
pero la mayor parte de ellos se mantienen sólo por compatibilidad
con versiones de Windows de 16 bits, y no deben usarse en
aplicaciones de 32 bits, por lo que no los veremos con detalle.
Nota:
Selección
Doble clic
Botones pulsables
Check boxes
Radio buttons
Cajas de grupo
Estados de un botón
Un botón puede tener tres niveles de estado diferentes.
Por una parte, puede tener o no el foco del teclado. Esto se
indica, generalmente, mediante un rectángulo de líneas punteadas
alrededor del texto.
Puede estar o no resaltado. El resaltado se produce cuando el
usuario coloca el ratón sobre el botón y pulsa, y mantiene, el botón
izquiedo del ratón. O también si el control tiene le foco y se pulsa la
tecla [Espacio].
Por último, en el caso de check boxes y radio buttons, el botón
puede o no estar marcado (checked).
Selección de un botón
Cambios de estado
Botones owner-draw
Ya lo hemos mencionado antes: los controles botón también
disponen de un estilo owner-draw.
Un control botón con el estilo owner-draw, BS_OWNERDRAW no
tiene un comportamiento definido, ni un aspecto gráfico concreto.
Será el procedimiento de ventana o diálogo de la ventana
propietaria del control el encargado de actualizar el aspecto en
pantalla y también de definir las respuestas a cada evento de
teclado o ratón.
De modo que un botón owner-draw puede ser un botón pulsable,
un radio button, un check box o cualquier otra cosa que inventemos.
Lo primero es crear el control con el estilo BS_OWNERDRAW:
HWND hctrl;
...
hctrl = CreateWindowEx(
0,
"BUTTON", /* Nombre de la clase */
"Botón 2", /* Texto del título */
BS_OWNERDRAW | WS_CHILD | WS_VISIBLE | WS_BORDER |
WS_TABSTOP, /* Estilo */
9, 49, /* Posición */
95, 24, /* Tamaño */
hwnd, /* Ventana padre */
(HMENU)ID_BOTON, /* Identificador del control */
hInstance, /* Instancia */
NULL); /* Sin datos de creación de ventana
*/
LPDRAWITEMSTRUCT lpdis;
HBRUSH pincel, pincel2;
...
case WM_DRAWITEM:
lpdis = (LPDRAWITEMSTRUCT)lParam;
GetClientRect(lpdis->hwndItem, &re);
SetBkMode(lpdis->hDC, TRANSPARENT);
pincel =
CreateSolidBrush(GetSysColor(COLOR_BACKGROUND));
pincel2 = CreateSolidBrush(RGB(240,120,0));
FillRect(lpdis->hDC, &re, pincel);
if(wParam == ID_BOTON2) {
if(lpdis->itemState & ODS_SELECTED) {
SelectObject(lpdis->hDC,
(HBRUSH)GetStockObject(GRAY_BRUSH));
SelectObject(lpdis->hDC,
(HPEN)GetStockObject(WHITE_PEN));
Ellipse(lpdis->hDC, re.left, re.top, re.right,
re.bottom);
TextOut(lpdis->hDC, re.left+14, re.top+4, "Botón
2", 7);
} else if(lpdis->itemState & ODS_DISABLED) {
MoveToEx(lpdis->hDC, 0, 0, NULL);
LineTo(lpdis->hDC, re.right, re.bottom);
MoveToEx(lpdis->hDC, 0, re.bottom, NULL);
LineTo(lpdis->hDC, re.right, 0);
} else {
SelectObject(lpdis->hDC, pincel2);
SelectObject(lpdis->hDC,
(HPEN)GetStockObject(WHITE_PEN));
Ellipse(lpdis->hDC, re.left, re.top, re.right,
re.bottom);
TextOut(lpdis->hDC, re.left+12, re.top+2, "Botón
2", 7);
}
}
DeleteObject(pincel);
DeleteObject(pincel2);
return 0;
Ejemplo 69
Capítulo 42 Control estático
avanzado
En el capítulo 10 ya vimos la mayor parte de lo que se puede
decir sobre los controles estáticos. En este capítulo veremos que
estos controles no son tan poco interactivos como parecen, y
aunque no pueden ser seleccionados, ni pueden recibir el foco, ni
responden al teclado; sí puede recibir algunos mensajes de
notificación, si se definen con el estilo adecuado.
HWND hctrl;
...
hctrl = CreateWindowEx(
0,
"STATIC", /* Nombre de la clase */
"Texto", /* Texto del título */
SS_SIMPLE |
WS_CHILD | WS_VISIBLE, /* Estilo */
5, 5, /* Posición */
55, 55, /* Tamaño */
hwnd, /* Ventana padre */
(HMENU)ID_ESTAT, /* Identificador del control */
hInstance, /* Instancia */
NULL); /* Sin datos de creación de ventana
*/
Cambiar fuente
También es posible modificar la fuente de un control estático
enviando un mensaje WM_SETFONT. El lugar apropiado es, por
supuesto, al procesar el mensaje WM_INITDIALOG en diálogos o al
iniciar la ventana, al procesar el mensaje WM_CREATE.
En el parámetro wParam pasamos un manipulador de fuente, y
usaremos la macro MAKELPARAM para crear un valor LPARAM, en
el que especificaremos la opción de repintar el control, que se
almacena en la palabra de menor peso de LPARAM.
Esto nos permite modificar la fuente durante la ejecución,
reflejando los cambios en pantalla.
Marcos
Ranurados
Ejemplos
El cuarto parámetro
sirve para indicar el
tipo de borde, y
también pueden ser
combinados para
Estilos de bordes obtener más tipos.
Para ver cada tipo
individual se puede usar el programa de ejemplo.
Algunos de los tipos se refieren a los lados del rectángulo que
se van a dibujar, por ejemplo, BF_RIGHT, BF_LEFT, BF_TOP y
BF_BOTTOM, o combinaciones de ellos, como
BF_BOTTOMLEFT, BF_BOTTONRIGHT, BF_TOPLEFT y
BF_TOPRIGHT. Estos estilos se pueden combinar entre si para
obtener las combinaciones que faltan, usando el operador de
bits |. También se puede usar BF_RECT para mostrar los cuatro
lados del rectángulo.
Otros tipos permiten trazar diagonales: BF_DIAGONAL,
BF_ENDTOPRIGHT, etc.
El resto permite definir diferentes aspectos, por ejemplo,
BF_FLAT crea ranurados planos, que no dan sensación de
relieve; BF_MIDDLE muestra la superficie interior del rectángulo
a un nivel intermedio de profundidad, usando un tono de gris
intermedio; MF_MONO elimina por completo el relieve 3D;
BF_SOFT da un acabado blando, redondeado; BF_ADJUST
crea el ranurado de modo que se deje libre el área interna del
rectángulo para que la use el cliente.
Se puede
modificar el texto
dentro de un
control estático
mediante la
función
SetWindowText o
mediante el
mensaje
WM_SETTEXT.
Recordemos
que podemos
Estilos de texto enviar mensajes a
un control
mediante las funciones SendDlgItemMessage, conociendo el
identificador del control, o usando SendMessage, si disponemos del
manipulador de ventana del control. Este manipulador se puede
conseguir también mediante la función GetDlgItem, conociendo el
identificador del control.
Estas tres líneas de código son, por lo tanto, equivalentes:
SetWindowText(GetDlgItem(hwnd, ID_EST), "Nuevo texto");
SendDlgItemMessage(hwnd, ID_EST, WM_SETTEXT, 0,
(LPARAM)"Nuevo texto");
SendMessage(GetDlgItem(hwnd, ID_EST), WM_SETTEXT, 0,
(LPARAM)"Nuevo texto");
Imágenes
Modificador SS_NOPREFIX
Existen tres estilos básicos
para crear controles estáticos con imágenes:
Modificadores de estilo
Modificador de hundido
Todos los estilos anteriores se pueden combinar con un
modificador, el estilo SS_SUNKEN, que crea la apariencia de que el
control está ligeramente hundido con respecto al resto del área de
cliente de la ventana:
Mensajes de
notificación
Modificador SS_SUNKEN
La clase Static dispone de varios
mensajes de notificación, pero para
que se envíen se debe activar el estilo SS_NOTIFY. En realidad los
controles static no están diseñados para ser interactivos, de modo
que esto es algo lógico.
Como en todos los casos, los mensajes de notificación se envían
a la ventana padre en la palabra de mayor peso del parámetro
wParam, mediante un mensaje WM_COMMAND. Los mensajes de
notificación son:
Por ejemplo:
case WM_COMMAND:
switch(LOWORD(wParam)) {
case ID_EST14:
switch(HIWORD(wParam)) {
case STN_CLICKED:
MessageBox(hwnd, "Clic", "Controles
estáticos", MB_OK);
break;
case STN_DBLCLK:
MessageBox(hwnd, "Doble clic",
"Controles estáticos", MB_OK);
break;
case STN_DISABLE:
MessageBox(hwnd, "Disable", "Controles
estáticos", MB_OK);
break;
case STN_ENABLE:
MessageBox(hwnd, "Enable", "Controles
estáticos", MB_OK);
break;
}
break;
}
break;
HWND hctrl;
...
hctrl = CreateWindowEx(
0,
"STATIC", /* Nombre de la clase */
"Texto", /* Texto del título */
SS_OWNERDRAW |
WS_CHILD | WS_VISIBLE, /* Estilo */
9, 49, /* Posición */
95, 24, /* Tamaño */
hwnd, /* Ventana padre */
(HMENU)ID_STATIC,/* Identificador del control */
hInstance, /* Instancia */
NULL); /* Sin datos de creación de ventana
*/
LPDRAWITEMSTRUCT lpdis;
case WM_DRAWITEM:
lpdis = (LPDRAWITEMSTRUCT)lParam;
SendDlgItemMessage(hwnd, (UINT)wParam,
WM_GETTEXT, 128, (LPARAM)cad);
GetClientRect(lpdis->hwndItem, &re);
SetBkMode(lpdis->hDC, TRANSPARENT);
FillRect(lpdis->hDC, &re,
(HBRUSH)GetStockObject(LTGRAY_BRUSH));
SelectObject(lpdis->hDC,
(HBRUSH)GetStockObject(GRAY_BRUSH));
SelectObject(lpdis->hDC,
(HPEN)GetStockObject(WHITE_PEN));
Ellipse(lpdis->hDC, re.left, re.top, re.right,
re.bottom);
TextOut(lpdis->hDC, re.left+12, re.top+2, cad,
strlen(cad));
return 0;
Ejemplo 70
Capítulo 43 Control combo box
avanzado
Seguimos con el repaso de los controles más comunes del API
de Window.
En el capítulo 11 vimos cómo usar de una forma básica los
combo boxes. También vimos que estos controles son la
combinación de un control edit o un control de texto estático y un list
box, así como el modo de crearlos a partir de un fichero de recursos,
de inicializarlos y de leer el valor de su selección.
En este capítulo veremos mucho más sobre ellos, cómo
personalizarlos, qué estilos pueden tener, sus mensajes de
notificación, etc.
Ya que son la combinación de un control edit (o static) y de un
control listbox, estos controles tendrán muchas de las características
de los controles de los que se derivan, y por lo tanto, disponen de
los equivalentes de algunos de sus estilos, mensajes de notificación
y mensajes de control.
Insertar
control
es ComboBox lista desplegable
combo
box durante la ejecución
No hay nada nuevo en este tema, del mismo modo que hemos
visto para los controles anteriormente tratados, también es posible
insertar controles combo box durante la ejecución. En este caso
tendremos que insertar una ventana de la clase "COMBOBOX".
Para insertar el control también usaremos las funciones
CreateWindow y CreateWindowEx.
HWND hctrl;
...
case WM_CREATE:
hInstance = ((LPCREATESTRUCT)lParam)->hInstance;
/* Insertar control Edit */
hctrl = CreateWindowEx(
0,
"COMBOBOX", /* Nombre de la clase */
NULL, /* Texto del título */
CBS_SIMPLE |
WS_VSCROLL | WS_CHILD | WS_VISIBLE |
WS_TABSTOP, /* Estilo */
5, 5, /* Posición */
120, 85 /* Tamaño */
hwnd, /* Ventana padre */
(HMENU)ID_COMBO1,/* Identificador del control
*/
hInstance, /* Instancia */
NULL); /* Sin datos de creación de
ventana */
Nota:
Validar selección
Despliegue de lista
Doble clic
Cada vez que el usuario hace doble clic sobre uno de los
elementos del list box de un combo box, se envía un mensaje de
notificación CBN_DBLCLK a su ventana padre.
Falta espacio
Modificación
Actualización
Ejemplo 71
Mensajes correspondientes a la lista
La lista del combo box normalmente se inicializa por la
aplicación, y el usuario puede elegir uno de los elementos de la lista,
y salvo en el estilo de lista desplegable, también se podrán introducir
valores que no estén en la lista.
Añadir ítems
Recuperar información
int i;
...
i = SendMessage(hctrl, CB_GETCURSEL, 0, 0);
Cambiar la selección
Borrar ítems
Otros mensajes
Ejemplo 72
El dato del ítem
Ya sabemos que cada ítem tiene asociado un índice y una
cadena. Pero también tiene asociado un dato entero de 32 bits: el
ítem data, o dato de ítem.
A cada ítem le podemos asignar un valor entero mediante el
mensaje CB_SETITEMDATA, y recuperarlo mediante
CB_GETITEMDATA.
Podemos aprovechar que el valor de retorno del mensaje
CB_ADDSTRING es el índice del ítem insertado, y usar ese valor en
el mensaje CB_SETITEMDATA, para asignar el valor del índice en el
array:
Interfaces de usuario
Existen dos comportamientos diferentes de los combo boxes con
listas desplegables (con el estilo CBS_DROPDOWN o
CBS_DROPDOWNLIST), con respecto al teclado. Cada uno de
estos comportamientos viene definido por un interfaz.
En el interfaz por defecto, la tecla F4 despliega u oculta la lista
desplegable, y las teclas de flecha arriba y abajo nos permiten
desplazarnos a través de las distintas opciones.
En el interfaz extendido la tecla F4 no tiene ninguna función, y la
lista se despliega tan pronto como se pulsa una de las teclas de
flecha arriba o abajo.
Las flechas funcionan tanto si la lista está desplegada como si
no.
Para cambiar el interfaz de un control combo box se usa el
mensaje CB_SETEXTENDEDUI, si se usa el valor FALSE en el
parámetro wParam, se activa el interfaz por defecto, y con el valor
TRUE se activa el interfaz extendido.
Mediante el mensaje CB_GETEXTENDEDUI se puede obtener
el valor actual del interfaz para un control combo box determinado.
Este mensaje no tiene parámetros, y el valor de retorno indica el
interfaz asociado al control. TRUE si es el extendido y FALSE si es
el interfaz por defecto.
HWND hestatico;
...
hestatico = CreateWindowEx(
0,
"STATIC", /* Nombre de la clase */
"", /* Texto del título, no tiene
*/
WS_CHILD | WS_VISIBLE | WS_BORDER |
WS_TABSTOP, /* Estilo */
9, 4, /* Posición */
344, 18, /* Tamaño */
hwnd, /* Ventana padre */
(HMENU)ID_TITULO,/* Identificador del control
*/
hInstance, /* Instancia */
NULL); /* Sin datos de creación de
ventana */
SendMessage(hestatico, WM_SETFONT, (WPARAM)hfont,
MAKELPARAM(TRUE, 0));
Por supuesto, podemos usar los comodines '*' y '?' para los
nombres de fichero:
...
IniciarCombo(hwnd, "*.c");
...
strcpy(path, p);
DlgDirListComboBox(
hwnd, /* manipulador de cuadro de diálogo con
list box */
path, /* puntero a cadena de camino o nombre
de fichero */
ID_COMBO, /* identificador de list box
*/
ID_TITULO, /* identificador de control estático
*/
DDL_DIRECTORY | DDL_DRIVES /* atributos de ficheros a
mostrar */
);
}
case WM_COMMAND:
switch(LOWORD(wParam)) {
case ID_COMBO:
switch(HIWORD(wParam)) {
case CBN_CLOSEUP:
if(DlgDirSelectComboBoxEx(hwnd, cad,
512, ID_COMBO)) {
strcat(cad, "*.c");
IniciarCombo(hwnd, cad);
} else
{MessageBox(hwnd, cad, "Fichero
seleccionado", MB_OK);
break;
...
strcpy(path, p);
strcpy(path, "*.h");
SendMessage(GetDlgItem(hwnd, ID_LISTA), CB_DIR,
(WPARAM)0, (LPARAM)path);
Juegos de caracteres
Selección actual
Es el ítem de la lista del combo box seleccionado por el usuario.
Su texto se copia en el campo de selección, tanto si se trata de un
control de edición o de uno estático. Salvo en el caso de la lista
desplegable, existe otra forma de introducir datos en un combo box:
teclearlos en el control edit.
Ya hemos visto que es posible recuperar el valor de la selección
actual mediante un mensaje CB_GETCURSEL, o cambiarlo
mediante un mensaje CB_SETCURSEL o CB_SELECTSTRING.
También hemos visto que existe un mensaje de notificación
CBN_SELCHANGE que se envía a la ventana padre del combo box
cada vez que el usuario cambia la selección actual. Hay que tener
en cuenta que este mensaje de notificación no se envía si la
selección actual se modifica directamente usando un mensaje
CB_SETCURSEL.
Ejemplo 73
En este ejemplo hemos usado algunas funciones y estructuras
del API, relacionadas con información sobre ficheros y manejo de
tiempos, que no hemos comentado con anterioridad:
GetFileAttributesEx sirve para obtener información sobre un
fichero, sus atributos, fechas de creación, modificación y último
acceso y tamaño.
WIN32_FILE_ATTRIBUTE_DATA es la estructura usada por la
función anterior para devolver los datos de un fichero.
FileTimeToSystemTime las fechas devueltas por la función
anterior están en formato de fecha de fichero, esta función
permite traducir ese formato a formato de fecha de sistema, que
es más manejable.
FILETIME estructura que almacena una fecha en forma de
número entero.
SYSTEMTIME estructura que almacena una fecha en forma de
campos individuales.
El control de edición
El campo de selección de un combo box puede ser un control de
edición o un control estático. El la parte que contiene el valor de la
selección actual o el texto introducido por el usuario.
Podemos usar el mensaje WM_GETTEXT para obtener el texto
que contiene actualmente, o el mensaje WM_SETTEXT para
modificarlo.
También podemos usar el mensaje CB_GETEDITSEL para
obtener las posiciones de inicio y final de la selección de texto
actual, si existe; o usar el mensaje CB_SETEDITSEL para
seleccionar parte del texto dentro del control de edición.
Si se usa el estilo CBS_AUTOHSCROLL es posible introducir
más texto del que se puede visualizar en la anchura del control, en
caso contrario la cantidad de texto que es posible introducir estará
limitada por esa anchura.
También se puede limitar el número de caracteres que el usuario
puede teclear mediante el mensaje CB_LIMITTEXT.
Hay dos mensajes de notificación relacionados con el control de
edición. Cuando el se modifica el contenido del control de edición, la
ventana padre recibe primero el mensaje de notificación
CBN_EDITUPDATE, para indicar que el texto se ha alterado.
Después de que el texto se ha mostrado, Windows envía el mensaje
CBN_EDITCHANGE.
Optimizar la memoria
Optimizar el tiempo
return tam.cx;
}
char item[300];
int x;
int eActual;
x = CalculaLongitud(hlista, cad);
if(x > eActual) eActual = x;
SendMessage(hlista, CB_ADDSTRING, 0, (LPARAM)cad);
SendMessage(hlista, CB_SETHORIZONTALEXTENT, eActual, 0);
SendMessage(hctrl, CB_SETITEMHEIGHT, 0,
MAKELPARAM(30,0));
InvalidateRect(hctrl, NULL, TRUE);
Localizaciones
Ya hemos visto que en los controles combo box los ítems se
muestran por orden alfabético, al menos en los que hemos usado
hasta ahora. Pero el orden alfabético no es algo universal, y puede
cambiar dependiendo del idioma.
Generalmente esto no nos preocupará, ya que el idioma usado
para elegir el orden se toma del propio sistema. Sin embargo, puede
haber casos en que nos interese modificar o conocer el idioma
usado en un combo box.
Para obtener el valor de la localización actual se usa el mensaje
CB_GETLOCALE. El valor de retorno es un entero de 32 bits, en el
que la palabra de menor peso contiene el código de país, y el de
mayor peso el del lenguaje, este último a su vez, se compone de un
identificador de lenguaje primario y un identificador de sublenguaje.
Se pueden usar las macros PRIMARYLANGID y SUBLANGID
para obtener el identificador de lenguaje primario y el de
sublenguaje, respectivamente.
int i;
char cad[120];
...
i = SendMessage(hctrl, CB_GETLOCALE, 0, 0);
sprintf(cad, "País %d, id lenguaje primario %d, "
"id de sublenguaje %d",
HIWORD(i), PRIMARYLANGID(LOWORD(i)),
SUBLANGID(LOWORD(i)));
MessageBox(hwnd, cad, "Localización", MB_OK);
SendMessage(hctrl, CB_SETLOCALE,
MAKELCID(MAKELANGID(LANG_SPANISH,
SUBLANG_SPANISH),
SSORT_DEFAULT), 0);
hctrl = CreateWindowEx(
0,
"COMBOBOX" /* Nombre de la clase */
"", /* Texto del título */
CBS_HASSTRINGS | CBS_OWNERDRAWVARIABLE |
CBS_DROPDOWNLIST | CBS_SORT |
WS_CHILD | WS_VISIBLE | WS_BORDER |
WS_TABSTOP, /* Estilo */
9, 19, /* Posición */
320, 250, /* Tamaño */
hwnd, /* Ventana padre */
(HMENU)ID_COMBO, /* Identificador del control
*/
hInstance, /* Instancia */
NULL); /* Sin datos de creación de
ventana */
Si no activamos el estilo CBS_HASSTRINGS, el valor que
usemos al insertar el ítem será almacenado en el dato del ítem de
32 bits.
itemState indica el estado del ítem. El valor puede ser uno o una
combinación de los siguientes:
case WM_DRAWITEM:
lpdis = (LPDRAWITEMSTRUCT) lParam;
if(lpdis->itemID == -1) { /* Se trata de un
menú, no hacer nada */
break;
}
switch (lpdis->itemAction) {
case ODA_SELECT:
case ODA_DRAWENTIRE:
case ODA_FOCUS:
/* Borrar el contenido previo */
FillRect(lpdis->hDC, &lpdis->rcItem, (HBRUSH)
(COLOR_WINDOW+1));
/* Obtener datos de las medidas de la fuente
*/
GetTextMetrics(lpdis->hDC, &tm);
/* Calcular la coordenada y para escribir el
texto de ítem */
y = (lpdis->rcItem.bottom + lpdis->rcItem.top
- tm.tmHeight) / 2;
/* Cada tipo de comida se muestra en un color
diferente */
if(comida[lpdis->itemData].tipo == 'p')
SetTextColor(lpdis->hDC, RGB(0,128,0));
else
if(comida[lpdis->itemData].tipo == 'c')
SetTextColor(lpdis->hDC, RGB(0,0,255));
else
if(comida[lpdis->itemData].tipo == 'b')
SetTextColor(lpdis->hDC, RGB(255,0,0));
/* Mostrar el icono */
icono = LoadIcon(hInstance,
MAKEINTRESOURCE(Icono+lpdis->itemData));
DrawIcon(lpdis->hDC, 4, lpdis->rcItem.top+2,
icono);
DeleteObject(icono);
/* Mostrar el texto */
TextOut(lpdis->hDC, 42, y,
comida[lpdis->itemData].nombre,
strlen(comida[lpdis->itemData].nombre));
/* Si el ítem está seleccionado, trazar un
rectángulo negro alrededor */
if (lpdis->itemState & ODS_SELECTED) {
SetTextColor(lpdis->hDC, RGB(0,0,0));
DrawFocusRect(lpdis->hDC, &lpdis->rcItem);
}
}
break;
...
El mensaje WM_DELETEITEM
case WM_COMPAREITEM:
lpcis = (LPCOMPAREITEMSTRUCT) lParam;
/* Establecer un orden:
1) Por tipos, primero comidas, después postres
y último bebidas
2) Dentro de cada tipo, usar el orden
alfabético */
if(comida[lpcis->itemData1].tipo == comida[lpcis-
>itemData2].tipo)
return strcmp(comida[lpcis->itemData1].nombre,
comida[lpcis->itemData2].nombre);
else if(comida[lpcis->itemData1].tipo == 'c')
return -1;
else if(comida[lpcis->itemData1].tipo == 'b')
return 1;
else if(comida[lpcis->itemData2].tipo == 'c')
return 1;
else return -1;
break;
Ejemplo 74
Capítulo 44 Control scrollbar
avanzado
Terminamos el repaso de los controles más comunes del API de
Windows hablando del control de barra de desplazamiento. Vamos a
completar el contenido del capítulo 12 con las explicaciones de
todos los mensajes y características que no se mencionaron
entonces.
HWND hctrl;
...
case WM_CREATE:
hInstance = ((LPCREATESTRUCT)lParam)->hInstance;
/* Insertar control Edit */
hctrl = CreateWindowEx(
0,
"SCROLLBAR", /* Nombre de la clase */
NULL, /* Texto del título */
SBS_HORZ |
WS_CHILD | WS_VISIBLE | WS_TABSTOP, /* Estilo
*/
5, 5, /* Posición */
120, 85 /* Tamaño */
hwnd, /* Ventana padre */
(HMENU)ID_SCROLL,/* Identificador del control
*/
hInstance, /* Instancia */
NULL); /* Sin datos de creación de
ventana */
Cambiar colores
No tenemos demasiado control sobre los colores de los controles
de barra de desplazamiento. Sólo podemos modificar el color de la
zona sobre la que se deplaza el cursor de la barra.
Para ello debemos procesar el mensaje
WM_CTLCOLORSCROLLBAR. Como en otros casos, podemos
cambiar el color del fondo, usando la función SetBkColor, y
debemos devolver un pincel del color usado para pintar ese fondo:
case WM_CTLCOLORSCROLLBAR:
SetBkColor((HDC)wParam, RGB(0,255,0));
return (LRESULT)pincel;
Estilos de scrollbar
En el capítulo 12 sólo mencionamos los estilos básicos
SBS_HORZ y SBS_VERT, pero existen algunos más que veremos a
continuación.
Estilos de orientación
Existen cuatro estilos para alinear el control con cada uno de los
cuatro bordes del rectángulo indicado en la función CreateWindow o
CreateWindowEx:
SBS_BOTTOMALIGN para alinear un control vertical con el
borde superior del rectángulo.
SBS_TOPALIGN para alinear un control vertical con el borde
superior del rectángulo.
SBS_LEFTALIGN para alinear un control horizontal con el borde
izquierdo del rectángulo.
SBS_RIGHTALIGN para alinear un control horizontal con el
borde derecho del rectángulo.
Si se usa cualquiera de estos estilos, el control de barra de
desplazamiento tendrá la anchura por defecto para las barras de
desplazamiento del sistema, independientemente del tamaño del
rectángulo usado en la función.
Usando funciones
Usando mensajes
Respuesta al
teclado
Los controles de
barra de
desplazamiento tienen
un interfaz de teclado
predefinido, de modo
que cuando tienen el
foco, ciertas
pulsaciones de teclas
Mensajes de barras de desplazamiento se convierten en
mensajes
WM_HSCROLL o WM_VSCROLL.
En la siguiente tabla vemos las teclas que se procesan, y qué
códigos de notificación generan:
Código de notificación
Tecla
Vertical Horizontal
Flecha
SB_LINEUP SB_LINELEFT
arriba
Flecha
SB_LINEDOWN SB_LINERIGHT
abajo
Flecha
SB_LINEDOWN SB_LINERIGHT
derecha
Flecha
SB_LINEUP SB_LINELEFT
izquierda
Página
SB_PAGEDOWN SB_PAGERIGHT
abajo
Página
SB_PAGEUP SB_PAGELEFT
arriba
Fin SB_BOTTOM
Inicio SB_TOP
Vemos que en esta tabla aparecen dos códigos de notificación
que no se comentaron en el punto anterior: SB_TOP y
SB_BOTTOM. Esto se debe a que estos dos códigos sólo pueden
ser generados mediante el teclado.
En un control de barra de desplazamiento vertical, SB_TOP
indica que el cursor se debe desplazar al tope superior, y
SB_BOTTOM al inferior. En controles horizontales indica los
extremos izquierdo y derecho del control, respectivamente.
Las barras de desplazamiento estándar no tienen este interfaz
de teclado definido, pero se puede hacer algo análogo procesando
mensajes WM_KEYDOWN y enviando correspondientes mensajes
WM_HSCROLL y WM_VSCROLL a la ventana.
Ejemplo 75
Desplazar contenido de ventanas
El uso más común de las barras de desplazamiento estándar es
permitir la posibilidad de mostrar distintas áreas de un documento
cuando éste no puede ser mostrado íntegramente en el área de
cliente.
Cuando se responde a códigos de notificación como
SB_PAGEUP, SB_PAGEDOWN, SB_TOP, etc, generalmente será
necesario generar el contenido completo del área de cliente. Sin
embargo, ante códigos como SB_LINEUP o SB_LINERIGHT, sólo
una pequeña parte del área de cliente queda invalidada, el resto
sigue siendo válida, después de desplazarla algunos pixels en la
dirección adecuada.
Estas operaciones de desplazamientos de mapas de bits son tan
frecuentes que, inevitablemente, el API dispone de funciones para
realizarlas. Se trata de las funciones ScrollWindow y
ScrollWindowEx, de las que es recomendable usar la segunda, ya
que la primera se mantiene sólo por compatibilidad con versiones
anteriores del API.
Esta función tiene bastantes parámetros, pero veremos que
algunos de ellos no tendrán mucho uso en la mayor parte de los
casos.
El primer parámetro es el manipulador de la ventana cuya área
de cliete queremos desplazar.
El segundo indica las únidades lógicas a desplazar
horizontalmente. Los valores negativos indican desplazamientos a la
izquierda.
El tercer parámetro indica las unidades lógicas a desplazar
verticalmente. Los valores negativos son desplazamientos hacia
arriba.
El cuarto parámetros es un puntero a una estructrua RECT que
indica la zona rectangular, dentro del área de cliete, que queremos
desplazar. Si usamos el valor NULL se tomará el rectángulo
correspondienta al área de cliente completa.
El quinto parámetro también es un puntero a una estructura
RECT que define un área rectangular de recorte. Sólo los puntos
dentro de ese área se verán afectados, de modo que los puntos
fuera de ella que después del desplazamiento estén dentro se
pintarán, pero los que estén dentro que posteriormente queden
fuera, no. De nuevo, si se especifica el valor NULL, el rectángulo de
recorte coincidirá con el del parámetro anterior.
El sexto parámetro es un manipulador de región. Antes de la
llamada, esa región puede contener la zona de actualización actual,
y la función la modificará para incluir las nuevas zonas que deben
ser actualizadas. Si no nos interesa usar esta región, de nuevo
podemos usar el valor NULL.
El séptimo parámetro es un puntero a una estructura RECT, la
función actualiza ese rectángulo con la región rectangular invalidada
después de realizado el desplazamiento. También podemos ignorar
este dato usando el valor NULL.
El octavo y último parámetro es una bandera que puede tomar
tres valores diferentes:
Colores y medidas
Generalmente, usaremos los colores y medidas estándar para
las barras de desplazamiento. Ya hemos visto que el único color que
podemos personalizar es el del fondo de la zona de desplazamiento
en la barra, el resto corresponde con los colores normales de los
botones, ya que las flechas y el cursor se comportan, en cieto modo,
como tales.
En cuanto a los colores, ya sabemos que podemos usar las
funciones GetSysColor y SetSysColors para obtener o modificar,
respectivamente, los valores de color del sistema. Concretamente,
para este caso, nos interesa el valor de COLOR_SCROLLBAR, que
es el color del fondo de la zona de desplazamiento.
int anchura;
anchura = GetSystemMetrics(SM_CXHSCROLL);
Otros mensajes
Además de todos los mensajes comentados en el capítulo 12 y
en este, hasta ahora, existen dos de los que no hemos hablado.
El primero de ellos es SBM_GETRANGE, que nos sirve para
recuperar los valores mínimo y máximo del rango de un control de
barra de desplazamiento.
En el parámetro wParam enviaremos un puntero a un entero, en
ese entero recibiremos el valor mínimo del rango. En el parámetro
lParam enviaremos otro puntero a un entero, en este recibiremos el
valor máximo del rango.
El segundo mensaje es SBM_SETRANGEREDRAW, que se
comporta de forma idéntica a SBM_SETRANGE, salvo que además
de asignar un rango al control de barra de desplazamiento, también
redibuja el control para mostrar su nuevo estado.
Como en el caso de SBM_SETRANGE, en el parámetro wParam
pasaremos el valor mínimo del rango, y en lParam, el máximo.
Ejemplo 76
Capítulo 45 La impresora
Cuando, en el capítulo 16, hablamos del GDI y de los Contextos
de Dispositivo (DCs), comentamos que nuestras aplicaciones
Windows no acceden directamente a los dispositivos de salida, sino
que lo hacen a través de un interfaz, el DC, de modo que todo lo
comentado entre los capítulos 16 y 29 es indiferente de si la salida
es una pantalla o una impresora.
Pero esto es en lo que respecta al modo de enviar datos a un
dispositivo. Cada impresora, como cada tarjeta gráfica, tiene sus
particularidades: resolución, colores, orientación de papel, etc.
Además, existen otras funcionalidades que podemos querer añadir a
nuestras aplicaciones, como las vistas previas o la selección de
páginas a imprimir o el número de copias.
Proceso de impresión
Desde el punto de vista de la aplicación, no existe diferencia
entre enviar datos a un DC de pantalla o de impresora. Sin
embargo, para el sistema si existen diferencias, ya que la impresora
no muestra los gráficos del mismo modo que una pantalla: es más
lenta, no permite borrar, no permite superponer unas salidas
gráficas a otras, o usan diferentes protocolos (por ejemplo,
PostScript).
Por todo ello, normalmente no se envían los datos a la impresora
hasta que la aplicación da por finalizada la creación de una página o
incluso de un documento completo. Esto hace que deban existir
varios procesos intermedios:
El monitor
Ejemplo 77
Para poder compilar este ejemplo hay que incluir entre las
librerías "winspool.lib".
Contexto de dispositivo
Ya hemos comentado que para enviar una salida a una
impresora necesitamos un DC, pero los DC de impresora no son
exactamente iguales que los de ventana que hemos usado hasta
ahora. Para empezar, no podemos usar la función GetDC, ya que
los DC de impresora no están asociados a una ventana.
Para obtener un DC de impresora podemos usar dos funciones:
CreateDC o PrintDlg.
No hay una forma mejor que la otra, se trata de dos opciones
que tenemos a nuestra disposición. Tal vez parezca más complicado
usar PrintDlg, pero eso sólo es porque ofrece muchas más
opciones.
Típicamente, usaremos la primera forma para imprimir una copia
completa de un documento. Es la respuesta a la opción "Imprimir"
de los menús de las aplicaciones. La segunda opción la usaremos
cuando queramos dejar al usuario seleccionar ciertas opciones:
páginas a imprimir, número de copias, intercalado, impresora, etc.
Usando CreateDC
char impresora[256];
int i;
DOCINFO di;
HBITMAP hBitmap;
HDC memDC;
BITMAP bm;
...
hBitmap = (HBITMAP)LoadImage(NULL, "meninas24.bmp",
IMAGE_BITMAP,
0, 0, LR_LOADFROMFILE);
GetObject(hBitmap, sizeof(BITMAP), (LPSTR)&bm);
i = SendDlgItemMessage(hwnd, ID_LISTA, LB_GETCURSEL, 0,
0);
SendDlgItemMessage(hwnd, ID_LISTA, LB_GETTEXT,
(WPARAM)i, (LPARAM)impresora);
hPrinterDC = CreateDC("WINSPOOL", impresora, NULL,
NULL);
di.cbSize = sizeof(DOCINFO);
di.lpszDocName = "Prueba";
di.lpszOutput = NULL;
di.lpszDatatype = NULL;
di.fwType = 0;
StartDoc(hPrinterDC, &di);
StartPage(hPrinterDC);
TextOut(hPrinterDC, 10, 10, "Hola, mundo!", 12);
Ellipse(hPrinterDC, 150, 180, 450, 670);
EndPage(hPrinterDC);
StartPage(hPrinterDC);
memDC = CreateCompatibleDC(hPrinterDC);
SelectObject(memDC, hBitmap);
BitBlt(hPrinterDC, 250, 450, bm.bmWidth, bm.bmHeight,
memDC, 0, 0, SRCCOPY);
DeleteDC(memDC);
EndPage(hPrinterDC);
EndDoc(hPrinterDC);
DeleteDC(hPrinterDC);
DeleteObject(hBitmap);
Ejemplo 78
Para poder compilar este ejemplo hay que incluir entre las
librerías "winspool.lib".
Usando PrintDlg
PRINTDLG pd
= {
sizeof(PRIN
TDLG),
hwnd, 0, 0,
0,
// hDC
PD_ALLPAGES
|
PD_RETURNDC
|
PD_USEDEVMO
DECOPIESAND
COLLATE,
1,
Diálogo de impresión 2, // Desde
la página 1
a la 2
1, 2, // La primera página es la 1, la última la 2
1, // Copias
NULL, 0, 0, 0, 0, 0, 0, 0};
case CM_IMPRIMIR:
if(PrintDlg(&pd)) {
ImprimirDocumento(&pd);
GlobalFree(pd.hDevMode);
GlobalFree(pd.hDevNames);
DeleteDC(pd.hDC);
}
break;
Es importante liberar el manipulador del DC creado por PrintDlg
y los manipuladores de memoria asignados para las estructiras
DEVNAMES y DEVMODE.
Ejemplo 79
Capítulo 46 Controles comunes
Los controles que hemos visto hasta ahora son los básicos, en
los próximos capítulos veremos el resto de los controles comunes
añadidos mediante una DLL. Estos controles son:
Control animation
Listas de imágenes
Barra de estado
Barra de progreso
Herramienta de sugerencia (Tooltip)
Control Arriba-abajo (Up-Down)
Control cabecera (Header)
ComboBoxEx
Selección de fecha y hora
Calendario mensual
Barra de guía (Trackbar)
Hot Key
Barra de desplazamiento plana
Control de dirección IP
List Box mejorada, draglist
List View
Paginador
Hojas de propiedades (Property Sheet)
Rebar
Editor de texto enriquecido (Rich Edit)
SysLink
Pestañas
Barras de herramientas
Tree View
Task Dialog *
Todos estos controles se han ido añadiendo al API a medida que
aparecían nuevas versiones de Windows o de Internet Explorer. Nos
facilitan mucho las cosas a la hora de obtener del usuario valores
dentro de dominios específicos (fechas, texto enriquecido, teclas,
direcciones IP...), o para crear barras de herramientas, barras de
estado, o para añadir funcionalidades a los cuadros de diálogo,
como animaciones, imágenes, barras de progreso, etc.
Empezaremos por estos últimos, ya que algunos de ellos los
encesitaremos para sacar todo su rendimiento al resto.
¿Hay algo nuevo en los controles básicos?
Veremos que sí, y daremos un repaso al final. Los estilos
visuales disponibles desde Windows XP afectan a aspecto gráfico
de todos los controles, pero puede que haya más novedades...
Button
ComboBox
Edit Control
Scroll Bar
Static Control
Capítulo 47 Control animación
El control
animación es un simple
control estático que
permite mostrar
animaciones en
formato AVI, sin
sonido. Esto es
Diálogo copia ficheros importante, estos
controles rechazarán
cualquier AVI que contenga sonido.
Seguramente has visto estos controles, por ejemplo al copiar,
mover o borrar ficheros desde el administrador de archivos de
Windows. Se suelen usar para indicar que se está realizando una
tarea, en lugar de mostrar un cuadro de diálogo estático. De este
modo, el usuario tiene la sensación de que su petición está siendo
atendida y no que ha quedado atascada, sobre todo cuando lleva
algún tiempo completarla.
En realidad sólo sirven como adorno, no aportan mejoras de
rendimiento, ni tienen correspondencia con los procesos que se
están ejecutando en el programa. Pero los programas tienen mejor
aspecto (siempre que las animaciones tengan cierta calidad), y se
ven más profesionales.
Para poder usar los controles animación en nuestros programas
es necesario incluir el fichero de cabecera "commctrl.h", enlazar con
la librería "comctl32" y asegurarse de que la dll ha sido cargada
invocando a la función InitCommonControlsEx.
Nota:
A menudo podemos olvidar invocar a
InitCommonControlsEx, y a pesar de ello, el programa
funciona correctamente. Esto se debe a que cualquier otro
programa que se esté ejecutando actualmente (por ejemplo el
propio IDE del compilador), ya ha cargado la dll en memoria.
Si no tenemos esto en cuenta es probable que nuestro
ejecutable no funcione correctamente cuando se ejecute en
solitario.
INITCOMMONCONTROLSEX iCCE;
...
iCCE.dwSize = sizeof(INITCOMMONCONTROLSEX);
iCCE.dwICC = ICC_ANIMATE_CLASS;
InitCommonControlsEx(&iCCE);
Ficheros de recursos
Como con los otros recursos que hemos visto con anterioridad,
podemos integrar los recursos de animaciones dentro de un fichero
de recursos.
En este caso tenemos dos recursos relacionados con los
controles de animación. Uno es el propio recurso AVI, que contiene
una animación en formato AVI sin sonido. El otro es el control que
es una ventana de la clase "ANIMATE_CLASS".
Para poder incluir este control en nuestros cuadros de diálogo
hay que incluir el fichero de cabecera "commctrl.h".
#include <windows.h>
#include <commctrl.h>
Animate_Close(GetDlgItem(hwnd, IDC_ANIMATE));
Manipular la animación
El control de animación reproduce una animación,
evidentemente. Pero esa animación puede estar incialmente parada,
si no se indica el estilo ACS_AUTOPLAY, de modo que tendremos
que reproducirla si queremos que no esté parada, o podemos
pararla, o mostrar uno de sus fotogramas.
Para estas acciones disponemos de un juego de mensajes o
macros (que lo único que hacen es enviar esos mensajes).
Abrir animación
Animate_Open(hctrl, "mundo");
Animate_Open(hctrl, "Gears.avi");
SendMessage(hctrl, ACM_OPEN, (WPARAM)0,
(LPARAM)MAKEINTRESOURCE(101,0));
Reproducir
Detener
Mostrar un fotograma
// Segundo fotograma:
Animate_Seek(GetDlgItem(hwnd, IDC_ANIMATE), 1);
// Sexto fotograma:
SendMessage(GetDlgItem(hwnd, IDC_ANIMATE), ACM_PLAY,
(WPARAM) (UINT) 0, (LPARAM) MAKELONG(5, 5));
Verificar reproducción
Cerrar animación
Animate_Close(hctrl);
SendDlgItemMessage(hwnd, IDC_ANIMATE2, ACM_OPEN, 0,
0);
Mensajes de notificación
Exiten dos mensajes de notificación para los controles de
animación. El mensaje de notificación ACN_START se envía a la
ventana padre del control cuando la animación empieza a
reproducirse. El mensaje ACN_STOP se envía cuando la animación
se detiene.
Estos mensajes de notificación se reciben en el parámetro
wParam de un mensaje WM_COMMAND. En la palabra de menor
peso se envía el identificador del control, en la de mayor peso el
mensaje de notificación.
En este ejemplo usamos los mensajes de notificación para
mostrar un mensaje en un control estático y para activar o
desactivar los botones de marcha y paro de la animación:
case WM_COMMAND:
switch(LOWORD(wParam)) {
case IDC_ANIMATE:
switch(HIWORD(wParam)) {
case ACN_START:
SetWindowText(GetDlgItem(hwnd,
IDC_MENSAJE), "Marcha");
EnableWindow(GetDlgItem(hwnd,
CM_PLAY), FALSE);
EnableWindow(GetDlgItem(hwnd,
CM_STOP), TRUE);
break;
case ACN_STOP:
SetWindowText(GetDlgItem(hwnd,
IDC_MENSAJE), "Parado");
EnableWindow(GetDlgItem(hwnd,
CM_PLAY), TRUE);
EnableWindow(GetDlgItem(hwnd,
CM_STOP), FALSE);
break;
}
break;
...
Ejemplo 80
Capítulo 48 Listas de imágenes
Haremos un interludio en este capítulo. Las listas de imágenes
no son extrictamente controles, al menos según mi opinión, aunque
la documentación de Microsoft los considera como tales. Pero sí los
usaremos en otros controles que veremos en próximos capítulos,
como en las barras de herramientas, los TreeViews o los ListViews,
por lo que será bueno conocerlos antes.
Las listas de imágenes se usan también para operaciones de
arrastrar y soltar.
Para poder usar las listas de imágenes en nuestros programas
es necesario incluir el fichero de cabecera "commctrl.h".
Una lista de imágenes es una colección de imágenes del mismo
tamaño, a las que podemos acceder mediante un índice. Todas las
imágenes se almacenan en un único mapa de bits, una a
continuación de otra. Existe la opción de añadir un segundo mapa
de bits monocromo con máscaras, de modo que las imágenes se
puedan mostrar con una zona transparente, igual que los iconos.
El API suministra funciones y macros para crear y destruir listas
de imágenes, añadir o eliminar imágenes de una lista, mostrarlas en
pantalla, extraer iconos, etc.
ImageList_Destroy(hIml);
ImageList_Destroy(hIml2);
ImageList_Remove(hIml2, 9);
Y:
hIml = ImageList_LoadBitmap(hInstance,
"mapabits", 32, 4, CLR_NONE);
HICON hicon;
...
hicon = ImageList_GetIcon(himl, 2, ILD_TRANSPARENT);
Mostrar imágenes
Todo lo anterior suele ser suficiente para muchas de las
aplicaciones de las listas de imágenes. Por ejemplo, los controles
ListView y TreeView se encargan de mostrar las imágenes para
cada ítem, según su estado de forma automática. Sin embargo,
también podemos mostrar imágenes de una lista de imágenes
desde nuestros programas, para ello disponemos de un par de
funciones.
La función ImageList_Draw permite trazar una imagen de una
lista de imágenes. Esta función requiere seis parámetros. El
primero, un manipulador de la lista de imágenes. El segundo, un
índice de la imagen a mostrar. El tercero, un manipulador de
contexto de dispositivo de destino. El cuarto y quinto, las
coordenadas donde se mostrará la imagen. Y el sexto el estilo de
trazado.
Hay que tener en cuenta que si la lista de imágenes ha sido
creada con la bandera ILC_COLORDDB, ILC_COLOR24 o
ILC_COLOR32, los estilos resaltados ILD_BLEND25 e
ILD_BLEND50 usarán una trama de puntos, y no se podrá distinguir
entre ellos. El resto de las banderas de color usan una mezcla para
hacer los resaltados, y en general, tendrán un aspecto diferente si
se usa ILD_BLEND25 o ILD_BLEND50.
Los estilo ILD_BLEND25 e ILD_BLEND50 se añaden a
ILD_NORMAL y ILD_TRANSPARENT para indicar que el usuario ha
seleccionado la imagen.
Además, el modo normal con imágenes de listas sin máscara es
equivalente al modo transparente, en ninguno de los dos casos hay
transparencias.
Cuando estos estilos se aplican a imágenes que provienen de
listas con máscaras, la diferencia es más sutil. Si se muestra en el
estilo normal la parte del fondo enmascarada se muestra con el
color de fondo de la lista de imágenes. Si se muestra en el estilo
transparente la parte del fondo enmascarada conserva el contenido
original.
Por último, el estilo ILD_MASK muestra la máscara, si existe.
El color de fondo
Para cada lista de imágenes podemos establecer un color de
fondo, que se usará para rellenar el espacio no enmascarado
cuando se tracen imágenes en el estilo normal. Este color no afecta
a listas de imágenes sin máscaras. Para asignar el color de fondo se
usa la función ImageList_SetBkColor, indicando como parámetros el
manipulador de la lista de imágenes y el color de fondo que se
quiere establecer. Para recuperar el color de fondo de una lista de
imágenes podemos usar la función ImageList_GetBkColor.
ImageList_SetBkColor(hIml, GetSysColor(COLOR_MENU));
Imágenes superpuestas
En cada lista de imágenes se pueden indicar
hasta cuatro imágenes que se podrán usar
posteriormente como imágenes superpuestas.
El efecto es similar al que usa Windows para
Acceso directo crear los iconos de acceso directo, cuando
añade una pequeña flecha en una de las esquinas al icono de la
aplicación.
Estas imágenes se conocen en el API como "overlay masks". Se
puede usar cualquier imagen de la lista como imagen superpuesta,
tan sólo hay que especificar cuales de ellas lo pueden ser usando la
función ImageList_SetOverlayImage. Hay que especificar tres
parámetros. El primero un manipulador de la lista de imágenes, el
segundo el índice de la imagen, y el tercero el número de la imagen
superpuesta, entre 1 y 4.
Para trazar una imagen superpuesta se usan las funciones
ImageList_Draw o ImageList_DrawEx, el índice de la imagen
superpuesta se indica mediante la macro INDEXTOOVERLAYMASK
añadida a las banderas de estilo.
ImageList_SetOverlayImage(hIml2, 6, 1);
...
ImageList_Draw(hIml2, 2, hDC, 10, 10,
ILD_TRANSPARENT | INDEXTOOVERLAYMASK(1));
Ejemplo 81
Arrastre de imágenes
Las operaciones de arrastrar y soltar siguen siempre un patrón
parecido. Empiezan con una pulsación de ratón sobre el objeto a
arrastrar. Mientras se mantiene pulsado, el objeto seleccionado se
mueve junto con el cursor del ratón. Cuando se suelta el botón del
ratón, el objeto se suelta en el punto actual del cursor.
Generalmente usaremos el botón izquierdo del ratón, eso quiere
decir que deberemos procesar tres mensajes:
WM_LBUTTONDOWN, WM_MOUSEMOVE y WM_LBUTTONUP.
case WM_LBUTTONDOWN:
ptCur.x = LOWORD(lParam);
ptCur.y = HIWORD(lParam);
if(!PtInRect(&re, ptCur)) return FALSE;
// Capturar el ratón
SetCapture(hwnd);
endrag = TRUE;
ShowCursor(FALSE);
// Borrar la imagen a arrastrar:
InvalidateRect(hwnd, &re, TRUE);
UpdateWindow(hwnd);
// Calcular el hotspot:
hotspot.x = ptCur.x-re.left;
hotspot.y = ptCur.y-re.top;
ImageList_BeginDrag(hIml, 2, hotspot.x,
hotspot.y);
ImageList_DragEnter(hwnd, ptCur.x + cxBorde,
ptCur.y + cyBorde);
break;
Arrastre
case WM_MOUSEMOVE:
if(!endrag) return TRUE;
ptCur.x = LOWORD(lParam);
ptCur.y = HIWORD(lParam);
ImageList_DragMove(ptCur.x + cxBorde, ptCur.y +
cyBorde);
break;
case WM_LBUTTONUP:
if(!endrag) return TRUE;
ptCur.x = LOWORD(lParam);
ptCur.y = HIWORD(lParam);
ImageList_EndDrag();
ImageList_DragLeave(hwnd);
ShowCursor(TRUE);
endrag = FALSE;
ReleaseCapture();
re.left = ptCur.x - hotspot.x;
re.top = ptCur.y - hotspot.y;
re.right = re.left+32;
re.bottom = re.top+32;
return TRUE;
Ejemplo 82
Información de imagen
Disponemos de dos funciones para obtener información sobre
listas de imágenes. La primera es ImageList_GetImageInfo, que
obtiene algunos datos sobre una imagen dentro de una lista de
imágenes. Se necesitan tres parámetros, el primero es un
manipulador de una lista de imágenes, el segundo el índice de la
imagen sobre la que queremos información, y el tercero un puntero
a una estructura IMAGEINFO en la que se almacenarán los datos
sobre la imagen. Esta estructura tiene cinco campos, aunque dos de
ellos no se usan:
CreateStatusWindow(WS_CHILD|WS_VISIBLE, "Texto de
prueba", hwnd, ID_STATUS);
INITCOMMONCONTROLSEX iCCE;
...
iCCE.dwSize = sizeof(INITCOMMONCONTROLSEX);
iCCE.dwICC = ICC_BAR_CLASSES;
InitCommonControlsEx(&iCCE);
Estilos
Por defecto, las ventanas de estado se colocan en la parte
inferior de la ventana padre, es decir, estas ventanas tienen activo
por defecto el estilo CCS_BOTTOM. También se puede especifiar el
estilo CCS_TOP, que la coloca en la parte superior, aunque no es
nada habitual.
También se puede usar el estilo SBARS_SIZEGRIP para incluir
un mapa de bits a la derecha de la barra que se usa para
redimensionar la ventana. Aunque ese estilo se puede combinar con
CCS_TOP, carece de sentido hacerlo, ya que no funcionará.
Cuando se usan barras de estado es importante que el
procedimiento de ventana de la ventana padre procese el mensaje
WM_SIZE, de modo que cada vez que la ventana padre cambie de
tamaño, se envíe el mensaje a la ventana de estilo para que se
adapte al nuevo tamaño de su ventana padre.
case WM_SIZE:
SendDlgItemMessage(hwnd, ID_STATUS, WM_SIZE, 0,
0);
break;
Nota:
Ejemplo 83
Tamaño y altura
Generalmente, las dimensiones de las barras de estado se
ajustan automáticamente por su procedimiento de ventana,
ignorando cualquier tamaño indicado por la aplicación. La anchura
es la misma que la del área de cliente de la ventana padre, y la
altura se ajusta en función del tamaño de fuente seleccionada por el
contexto de dispositivo de la ventana de estado.
Por eso es importante, como ya comentamos antes, que la
ventana padre procese el mensaje WM_SIZE, y lo reenvíe a la
ventana de estado.
Sin embargo, la aplicación puede asignar la altura mínima de la
ventana de estado, o más concretamente, del área útil de la ventana
de estado, descontando los bordes. Esto se hace con el mensaje
SB_SETMINHEIGHT, indicando en wParam la altura mínima, en
pixels. Generalmente esto sólo será necesario en barras de estado
owner-draw.
También podemos recuperar la dimensiones de los bordes de
una barra de estado mediante el mensaje SB_GETBORDERS. En el
parámetro lParam tendremos que pasar la dirección de un array de
tres enteros en los que recibiremos las anchuras del borde
horizontal, vertical y el la distancia entre los rectángulos interiores
que contienen el texto, respectivamente.
RECT re;
int anchura[4];
...
case WM_SIZE:
GetClientRect(hwnd, &re);
anchura[3] = re.right-20;
anchura[2] = anchura[3]-60;
anchura[1] = anchura[2]-60;
anchura[0] = anchura[1]-60;
SendDlgItemMessage(hwnd, ID_STATUS, SB_SETPARTS,
4, (LPARAM)anchura);
SendDlgItemMessage(hwnd, ID_STATUS, msg, wParam,
lParam);
int nPartes;
int *anchura;
anchura = (int*)malloc(sizeof(int)*10);
nPartes = SendDlgItemMessage(hwnd,
ID_STATUS, SB_GETPARTS, 10, anchura));
free(anchura);
Manejar texto
En cada parte de una ventana de estado se puede modificar el
texto o recuperarlo.
Para modificarlo se usa el mensaje SB_SETTEXT. En el
parámetro wParam se indica el índice, empezando en cero, de la
parte en que se quiere modificar el texto, combinado con el tipo de
texto a mostrar. Ese tipo puede ser 0, que indica que se trace un
borde hundido, SBT_POPOUT que indica un borde sobresaliente,
SBT_NOBORDERS que indica que no se tracen bordes,
SBT_OWNERDRAW para ventanas de estado owner-draw o
SBT_RTLREADING para lenguajes que se escriben de derecha a
izquierda.
En lParam se pasa un puntero a la cadena terminada con cero
que se debe mostrar, o un valor entero de 32 bits, en caso de
ventanas de estado owner-draw.
char *txt;
int len;
...
len = LOWORD(SendDlgItemMessage(hwnd, ID_STATUS,
SB_GETTEXTLENGTH, 3, 0));
txt = (char*)malloc(len+1);
SendDlgItemMessage(hwnd, ID_STATUS, SB_GETTEXT,
3, (LPARAM)txt);
...
free(txt);
HDC hdc;
RECT re;
hdc = GetDC(hwnd);
re.left = 20; re.top = 10;
re.right = 150; re.bottom = 30;
DrawStatusText(hdc, &re, "\tTexto estático",
SBT_POPOUT);
ReleaseDC(hwnd, hdc);
Ejemplo 84
Ventanas de estado owner-draw
Cada una de las partes de una ventana de estado se puede
definir como owner-draw. Esto proporciona un mayor control sobre
los contenidos de esas partes, ya que nos permite incluir mapas de
bits, o en general, cualquier gráfico GDI que queramos, en lugar de
sólo texto.
Para definir una parte como owner-draw basta con enviar un
mensaje SB_SETTEXT, añadiendo el tipo SBT_OWNERDRAW al
identificador de la parte, en el parámetro wParam, y usando el
parámetro lParam para un valor de 32 bits que posteriormente se
usará para dibujar el contenido de la parte. Ese parámetro puede
ser un puntero a una estructura, un mapa de bits, un entero, etc. El
significado del valor queda definido por la aplicación, es decir, por
nosotros.
SendDlgItemMessage(hwnd, ID_STATUS, SB_SETTEXT,
2|SBT_OWNERDRAW, lParam);
...
SendDlgItemMessage(hwnd, ID_STATUS, SB_SETTEXT,
3|SBT_OWNERDRAW, (LPARAM)hBitmapSi);
INITCOMMONCONTROLSEX iCCE;
...
iCCE.dwSize = sizeof(INITCOMMONCONTROLSEX);
iCCE.dwICC = CC_PROGRESS_CLASS;
InitCommonControlsEx(&iCCE);
Estilos visuales
A partir de la versión XP de
Windows es posible seleccionar
diferentes temas que definen
muchos aspectos visuales del
interfaz de Windows. Entre
ellos, algunos afectan a la
Modo clásico apariencia de las barras de
progreso.
En nuestros programas podemos optar por una apariencia
clásica (modo clásico) o por una apariencia más actual (estilos
visuales).
Por defecto se usará el
modo clásico, en el que las
barras se suelen mostrar en
color azul sobre fondo blanco, si
efectos 3D. En modo clásico los
mensajes para cambiar los
colores de fondo y de la barra Estilo visual
funcionan como se espera, de
modo que aunque visualmente pueden ser menos atractivos, nos
proporcionan mayor control sobre la apariencia de estos controles.
Si se activan los estilos visuales, la apariencia de los controles
es más atractiva: se añaden efectos 3D, que aparentan relieve, y
animaciones de color. Pero perdemos la posibilidad de modificar los
colores desde la aplicación, ya que esos parámetros dependen sólo
del estilo activo. Tampoco podemos diferenciar entre las barras de
progreso de bloques o suaves, como en el modo clásico.
Para activar los estilos visuales tenemos dos alternativas, y
algunas condiciones previas.
Las condiciones están relacionadas con la versión de sistema
operativo y de Internet Explorer instaladas, o más concretamente,
con la versión de la DLL de los controles comunes, que
generalmente se asocia con una versión de Internet Explorer.
El sistema operativo debe ser Windows XP o posterior, y la de la
DLL la 6.0 o posterior. Para que el compilador tenga esto en cuenta
hay que definir estas macros:
Fichero de manifiesto
Estilos
Hay dos estilos disponibles para las barras de progreso.
El primero, PBS_VERTICAL, afecta a la orientación. Si se
especifica, la barra se rellenará de abajo a arriba, si no se
especifica, se rellenará de izquierda a derecha.
El segundo, PBS_SMOOTH, afecta a la apariencia. Si se
especifica el relleno de la barra será contínuo y no habrá cortes ni
saltos. Si no se especifica, la barra se divide en rectángulos. Este
estilo no tiene efecto cuando usamos estilos visuales.
Rangos
Cada barra de progreso tiene ciertos parámetros que podemos
modificar a nuestra conveniencia. Los principales son los que
definen el rango: valor mínimo y máximo, y el valor actual.
Cuando el valor actual sea igual al valor mínimo del rango, la
barra se mostrará vacía. Cuando el valor actual sea igual al valor
máximo del rango, la barra se mostrará llena.
En nuestros programas haremos variar el valor actual desde el
mínimo al máximo para reflejar el progreso de alguna tarea cuando
nos interese informar al usuario a medida que esa tarea se va
completando.
Por defecto, el rango de las barras de progreso es (0, 100), pero
podemos variar ese rango, siempre que tengamos en cuenta dos
reglas sencillas y lógicas:
SendDlgItemMessage(hwnd, ID_PROGRESSBAR,
PBM_SETRANGE, 0, MAKELPARAM(0, 200));
SendDlgItemMessage(hwnd, ID_PROGRESSBAR,
PBM_SETRANGE32, 0, 200);
PBRANGE rango;
Posicion
Hay tres modos de modificar la posición actual de un control de
barra de progreso, podremos elegir la que más se ajuste a nuestro
problema particular.
La más sencilla es asignar la posición directamente al valor que
queramos, para ello podemos usar el mensaje PBM_SETPOS,
indicando en el parámetro wParam la nueva posición deseada.
Colores
Si usamos barras de progreso en modo clásico, podemos
modificar los colores para la barra y el fondo, usando los mensaje
PBM_SETBARCOLOR y PBM_SETBKCOLOR, respectivamente.
En controles con estilos visuales, tal como comentamos antes, estos
mensajes no tienen efecto.
SendDlgItemMessage(hwnd, ID_PROGRESSBAR,
PBM_SETBARCOLOR, 0, (LPARAM)RGB(255,120,90));
SendDlgItemMessage(hwnd, ID_PROGRESSBAR, PBM_SETBKCOLOR,
0, (LPARAM)RGB(0,0,0));
Ejemplo 86
Capítulo 51 Control Tooltip
Los controles Tooltip son
pequeñas ventanas que
aparecen sobre otros controles,
generalmente llamados
herramientas, y que
desaparecen al cabo de un
Tooltip tiempo corto. Normalmente se
usan para mostrar pistas o
ayudas al usuario sobre la tarea asignada al control, o para mostrar
el texto completo de un ítem de una lista o árbol cuando parte de
ese texto queda oculto tras el borde del control.
Cuando veamos las barras de herramientas veremos que se
trata de conjuntos de botones, cada uno de los cuales indica la
acción mediante un icono. Esto hace que a menudo no sea evidente
qué hace cada herramienta. En estos casos, los tooltips serán
especialmente útiles.
De nuevo estamos ante controles estáticos, en el sentido de que
no sirven para obtener datos por parte del usuario, sino para
mostrarle información.
De forma similar a lo que pasa con las listas de imágenes,
veremos que estos controles se usan conjuntamente con otros que
veremos en próximos capítulos, como list-views, tree-views, barras
de herramientas, etc.
Creación de tooltip
Los controles tooltip se crean usando la función
CreateWindowEx, indicando como tipo de ventana la constante
TOOLTIPS_CLASS, el estilo extendido WS_EX_TOOLWINDOW, al
menos el estilo WS_POPUP, más los estilos específicos de
controles tooltip que queramos.
En realidad, los controles tooltip siempre tienen los estilos
WS_EX_TOOLWINDOW y WS_POPUP, aunque no se especifiquen
de forma explícita.
Para las coordenadas y dimensiones usaremos la constante
CW_USEDEFAULT, puesto que la posición dependerá de la
posición del ratón y del control al que pertenezca el tooltip, y su
tamaño del contenido que le asignemos.
hwndTip = CreateWindowEx(WS_EX_TOOLWINDOW,
TOOLTIPS_CLASS, NULL,
WS_POPUP | TTS_ALWAYSTIP |
TTS_BALLOON,
CW_USEDEFAULT, CW_USEDEFAULT,
CW_USEDEFAULT, CW_USEDEFAULT,
hwnd, NULL,
hInstance, NULL);
SetWindowPos(hwndTip, HWND_TOPMOST, 0, 0, 0, 0,
SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE);
INITCOMMONCONTROLSEX iCCE;
...
iCCE.dwSize = sizeof(INITCOMMONCONTROLSEX);
iCCE.dwICC = ICC_TAB_CLASSES;
InitCommonControlsEx(&iCCE);
Estilos
Los controles tooltip tienen varios estilos propios que controlan
varios aspectos gráficos:
Cambios de color
Es posible modificar los colores del fondo y de los caracteres de
un control tooltip, usando los mensaje TTM_SETTIPBKCOLOR y
TTM_SETTIPTEXTCOLOR, respectivamente. En ambos casos, el
color deseado se especifica en el parámetro wParam.
SendMessage(hwndTip, TTM_SETTIPTEXTCOLOR,
(WPARAM)RGB(255,0,0), 0);
SendMessage(hwndTip, TTM_SETTIPBKCOLOR,
(WPARAM)RGB(240,255,255), 0);
Hay que tener en cuenta que, como pasaba con las barras de
progreso, si se activan los controles visuales, estos mensajes no
tendrán efecto, y se usarán siempre los colores del tema.
Nota:
#ifndef TTM_SETTILE
#ifdef UNICODE
#define TTM_SETTITLE TTM_SETTITLEW
#else
#define TTM_SETTITLE TTM_SETTITLEA
#endif
#endif
#ifndef TTI_NONE
// ToolTip Icons (Set with
TTM_SETTITLE)
#define TTI_NONE 0
#define TTI_INFO 1
#define TTI_WARNING 2
#define TTI_ERROR 3
#if (_WIN32_WINNT >= 0x0600)
#define TTI_INFO_LARGE 4
#define TTI_WARNING_LARGE 5
#define TTI_ERROR_LARGE 6
#endif // (_WIN32_WINNT >= 0x0600)
#endif
Limitar anchura
Podemos limitar la anchura máxima del control tooltip, de modo
que el texto que incluye se fragmente en distintas líneas si supera la
anchura establecida. Para limitar la anchura máxima se usa el
mensaje TTM_SETMAXTIPWIDTH, indicando el lParam la anchura
máxima en pixels, o -1 para permitir cualquier anchura.
TOOLINFO toolInfo = { 0 };
...
toolInfo.cbSize = sizeof(toolInfo);
toolInfo.hwnd = hwnd;
toolInfo.uFlags = TTF_IDISHWND | TTF_SUBCLASS;
toolInfo.uId = (UINT_PTR)GetDlgItem(hwnd,
CM_PRUEBA);
toolInfo.lpszText = "Este botón emite un 'beep'.";
SendMessage(hwndTip, TTM_ADDTOOL, 0,
(LPARAM)&toolInfo);
TOOLINFO toolInfo = { 0 };
...
toolInfo.cbSize = sizeof(toolInfo);
toolInfo.uFlags = TTF_SUBCLASS;
toolInfo.hwnd = hwnd;
GetClientRect(hwnd, &toolInfo.rect);
toolInfo.lpszText = "Esta es la ventana del ejemplo
87";
SendMessage(hwndTip, TTM_ADDTOOL, 0,
(LPARAM)&toolInfo);
TOOLINFO toolInfo = { 0 };
...
toolInfo.cbSize = sizeof(toolInfo);
toolInfo.hwnd = hwnd;
toolInfo.uId = (UINT_PTR)GetDlgItem(hwnd,
CM_PRUEBA);
SendMessage(hwndTip, TTM_DELTOOL, 0,
(LPARAM)&toolInfo);
TOOLINFO toolInfo = { 0 };
...
toolInfo.cbSize = sizeof(toolInfo);
toolInfo.hwnd = hwnd;
GetClientRect(hwnd, &toolInfo.rect);
SendMessage(hwndTip, TTM_DELTOOL, 0,
(LPARAM)&toolInfo);
STRINGTABLE
{
STR_HABILITAR "Este botón habilita los tooltips."
}
TOOLINFO toolInfo = { 0 };
toolInfo.cbSize = sizeof(toolInfo);
toolInfo.hwnd = hwnd;
toolInfo.uFlags = TTF_IDISHWND | TTF_SUBCLASS;
toolInfo.uId = (UINT_PTR)GetDlgItem(hwnd,
CM_HABILITAR);
toolInfo.hinst = hInstance;
toolInfo.lpszText = STR_HABILITAR;
SendMessage(hwndTip, TTM_ADDTOOL, 0,
(LPARAM)&toolInfo);
Ejemplo 87
Notificaciones
Los controles tooltip disponen de algunos mensajes de
notificación que nos permiten controlar de forma detallada algunos
aspectos, veremos ahora algunos de ellos.
Estos mensajes de notificación vienen el el formato de un
mensaje WM_NOTIFY, donde wParam contiene el identificador del
control que envía el mensaje, y lParam es un puntero a una
estructura NMHDR, o una estructura cuyo primer campo es una
estructura NMHDR y otros campos adicionales, que dependen del
control que envía el mensaje. Este es el caso de los mensajes de
notificación de los tooltips.
Generalmente, el valor de wParam no resulta útil, ya que varios
controles pueden tener el mismo identificador. En su lugar se usará
alguno de los campos de NMHDR, o de la estructura específica
usada por el control.
Nota:
TOOLINFO toolInfo = { 0 };
toolInfo.cbSize = sizeof(toolInfo);
toolInfo.hwnd = hwnd;
toolInfo.uFlags = TTF_IDISHWND | TTF_SUBCLASS;
toolInfo.uId = (UINT_PTR)GetDlgItem(hwnd,
CM_DESHABILITAR);
toolInfo.lpszText = LPSTR_TEXTCALLBACK; // <-
Asignar texto mediante notificación
SendMessage(hwndTip, TTM_ADDTOOL, 0,
(LPARAM)&toolInfo);
Cuando se vaya a mostrar un tooltip para un control para el que
se ha especificado la constante LPSTR_TEXTCALLBACK para el
campo lpszText, se envía un mensaje de notificación
TTN_GETDISPINFO, a travé de un mensaje WM_NOTIFY a la
ventana padre del tooltip, que deberemos procesar. Cuando
recibamos un mensaje de notificación WM_NOTIFY, generalmente
podremos ignorar el parámetro wParam, y en un primer lugar,
tomaremos el parámetro lParam como un puntero a una estructura
NMHDR, ya que no sabemos qué tipo de control ha enviado la
notificación, y por lo tanto, no sabemos que estructura viene
apuntada por ese parámetro.
LPNMHDR pnmhdr;
...
case WM_NOTIFY:
pnmhdr = (LPNMHDR)lParam;
switch(pnmhdr->code) {
...
LPNMHDR pnmhdr;
LPNMTTDISPINFO pnmttdispinfo;
...
case WM_NOTIFY:
pnmhdr = (LPNMHDR)lParam;
switch(pnmhdr->code) {
case TTN_GETDISPINFO:
pnmttdispinfo = (LPNMTTDISPINFO)pnmhdr;
A continuación tendremos que identificar el control concreto para
el que se ha generado la notificación del tooltip. Para ello tendremos
que consultar el miembro uFlags de la estructura NMTTDISPINFO,
para ver si contiene la bandera TTF_IDISHWND. En ese caso, el
campo idFrom de la estructura NMHDR que es el primer campo de
NMTTDISPINFO contendrá un manipulador de la ventana. Esto será
lo normal, ya que nosotros habremos especificado esa bandera al
añadir el control al tooltip, pero no está de más verificarlo.
Como ese campo es un manipulador de ventana, podremos
obtener el identificador del control mediante una llamada a la función
GetDlgCtrlID:
LPNMHDR pnmhdr;
LPNMTTDISPINFO pnmttdispinfo;
...
case WM_NOTIFY:
pnmhdr = (LPNMHDR)lParam;
switch(pnmhdr->code) {
case TTN_GETDISPINFO:
pnmttdispinfo = (LPNMTTDISPINFO)pnmhdr;
if (pnmttdispinfo->uFlags & TTF_IDISHWND) {
switch(GetDlgCtrlID((HWND)pnmttdispinfo-
>hdr.idFrom)) {
case CM_DESHABILITAR:
...
pnmttdispinfo->lpszText = STR_DESHABILITAR;
pnmttdispinfo->hinst = hInstance;
LPNMHDR pnmhdr;
LPNMTTDISPINFO pnmttdispinfo;
...
case WM_NOTIFY:
pnmhdr = (LPNMHDR)lParam;
switch(pnmhdr->code) {
case TTN_GETDISPINFO:
pnmttdispinfo = (LPNMTTDISPINFO)pnmhdr;
if (pnmttdispinfo->uFlags & TTF_IDISHWND) {
switch(GetDlgCtrlID((HWND)pnmttdispinfo-
>hdr.idFrom)) {
case CM_DESHABILITAR:
pnmttdispinfo->uFlags |=
TTF_DI_SETITEM;
strcpy(pnmttdispinfo->szText,
"Deshabilita los tooltips.");
break;
case CM_HABILITAR:
pnmttdispinfo->uFlags |=
TTF_DI_SETITEM;
pnmttdispinfo->lpszText =
"Habilita los tooltips.";
break;
}
}
break;
}
break;
Ejemplo 88
Notificaciones de mostrar y ocultar
Cada vez que una ventana tooltip vaya a ser mostrada se envía
un mensaje de notificación TTN_SHOW y cuando va a ser ocultada,
uno TTN_POP. Ambos se envían mediante un mensaje
WM_NOTIFY, y en ambos casos, el parámetro lParam contiene un
puntero a una estructura NMHDR.
No he encontrado ninguna utilidad a estos mensajes. El único
ejemplo que se me ha ocurrido es contar cada vez que se muestra u
oculta el tooltip, y mostrar esa cuenta en la ventana. También se
podría usar esta notificación para visualizar un texto en la barra de
estado.
case WM_NOTIFY:
pnmhdr = (LPNMHDR)lParam;
switch(pnmhdr->code) {
case TTN_SHOW:
mostrado++;
InvalidateRect(hwnd, NULL, TRUE);
break;
case TTN_POP:
ocultado++;
InvalidateRect(hwnd, NULL, TRUE);
break;
}
break;
...
case WM_PAINT:
hdc = BeginPaint(hwnd, &ps);
SetBkMode(hdc, TRANSPARENT);
sprintf(cad, "Mostrado=%d Ocultado=%d",
mostrado, ocultado);
TextOut(hdc, 10, 40, cad, strlen(cad));
EndPaint(hwnd, &ps);
break;
Personalización
Es posible personalizar un tooltip, cambiando la fuente, los
colores o encargando a la aplicación el dibujo de algunos detalles
del tooltip.
El mensaje de notificación NM_CUSTOMDRAW se envía para
muchos controles, incluídos algunos que ya hemos visto, como por
ejemplo, los botones.
En el caso de los tooltips, los cambios de fuente que hagamos
sólo tendrán enfecto en el estilo clásico. Si activamos los estilos
visuales, los cambios de fuente serán ignorados.
En el caso que nos ocupa recibiremos un mensaje de
notificación NM_CUSTOMDRAW justo antes de que se empiece a
pintar el control, con el código de etapa de dibujo
CDDS_PREPAINT. Esto nos permite tomar varias acciones:
LPNMHDR pnmhdr;
LPNMTTCUSTOMDRAW pnmttcustomdraw;
...
case WM_CREATE:
...
hFont = CreateFont(-30, 0, 0, 450, 300, FALSE,
FALSE, FALSE,
DEFAULT_CHARSET, OUT_TT_PRECIS,
CLIP_DEFAULT_PRECIS,
PROOF_QUALITY, DEFAULT_PITCH | FF_ROMAN,
"Times New Roman");
hPen = CreatePen(PS_SOLID, 4, RGB(255,0,0));
...
case WM_NOTIFY:
pnmhdr = (LPNMHDR)lParam;
switch(pnmhdr->code) {
case TTN_SHOW:
mostrado++;
InvalidateRect(hwnd, NULL, TRUE);
break;
case TTN_POP:
ocultado++;
InvalidateRect(hwnd, NULL, TRUE);
break;
case NM_CUSTOMDRAW:
pnmttcustomdraw = (LPNMTTCUSTOMDRAW)pnmhdr;
if(pnmttcustomdraw->nmcd.hdr.hwndFrom ==
hwndTip &&
GetDlgCtrlID((HWND)pnmttcustomdraw-
>nmcd.hdr.idFrom) == CM_BOTON3 &&
pnmttcustomdraw->nmcd.dwDrawStage ==
CDDS_PREPAINT) {
SelectObject(pnmttcustomdraw->nmcd.hdc,
hFont);
SetTextColor(pnmttcustomdraw->nmcd.hdc,
RGB(255,0,0));
return CDRF_NEWFONT |
CDRF_NOTIFYPOSTPAINT;
} else
if(pnmttcustomdraw->nmcd.hdr.hwndFrom ==
hwndTip &&
GetDlgCtrlID((HWND)pnmttcustomdraw-
>nmcd.hdr.idFrom) == CM_BOTON3 &&
pnmttcustomdraw->nmcd.dwDrawStage ==
CDDS_POSTPAINT) {
SelectObject(pnmttcustomdraw->nmcd.hdc,
GetStockObject(HOLLOW_BRUSH));
SelectObject(pnmttcustomdraw->nmcd.hdc,
hPen);
Rectangle(pnmttcustomdraw->nmcd.hdc,
pnmttcustomdraw->nmcd.rc.left,
pnmttcustomdraw->nmcd.rc.top,
pnmttcustomdraw-
>nmcd.rc.right,
pnmttcustomdraw-
>nmcd.rc.bottom);
return CDRF_SKIPDEFAULT;
} else
return CDRF_DODEFAULT;
break;
}
break;
...
case WM_DESTROY:
DeleteObject(hFont);
DeleteObject(hPen);
...
Ejemplo 89
Otros mensajes
Los tooltips disponen de muchos otros mensajes, algunos de los
cuales los explicaremos cuando veamos otros controles comunes,
como tree-views y list-views, y otros que problemente no veamos,
porque tienen poca utilidad.
Esta es una lista del resto de los mensajes que no hemos visto
en este capítulo:
TTM_ADJUSTRECT
TTM_ENUMTOOLS
TTM_GETBUBBLESIZE
TTM_GETCURRENTTOOL
TTM_GETDELAYTIME
TTM_GETMARGIN
TTM_GETMAXTIPWIDTH
TTM_GETTEXT
TTM_GETTIPBKCOLOR
TTM_GETTIPTEXTCOLOR
TTM_GETTITLE
TTM_GETTOOLCOUNT
TTM_GETTOOLINFO
TTM_HITTEST
TTM_NEWTOOLRECT
TTM_POP
TTM_POPUP
TTM_RELAYEVENT
TTM_SETDELAYTIME
TTM_SETMARGIN
TTM_SETTIPBKCOLOR
TTM_SETTIPTEXTCOLOR
TTM_SETTOOLINFO
TTM_SETWINDOWTHEME
TTM_TRACKACTIVATE
TTM_TRACKPOSITION
TTM_UPDATE
TTM_UPDATETIPTEXT
TTM_WINDOWFROMPOINT
Capítulo 52 Control UpDown
Un control UpDown está formado por
un par de botones con flechas. Esos
botones se usan para incrementar o
decrementar un valor, generalmente
Validación de datos
asociado a otro control. Cuando este
control asociado existe, se le denomina
ventana amiga (buddy window). El valor asociado se denomina
posición actual.
Desde el punto de vista del usuario, el control UpDown y su
ventana amiga se comportan como un único control, de modo que
las flechas constituyen una forma alternativa de modificar el valor
mostrado por el control.
INITCOMMONCONTROLSEX iCCE;
...
iCCE.dwSize = sizeof(INITCOMMONCONTROLSEX);
iCCE.dwICC = ICC_UPDOWN_CLASS;
InitCommonControlsEx(&iCCE);
Estilos
Hay varios estilos que se pueden aplicar a los controles
UpDown, además de UDS_AUTOBUDDY.
UDS_ALIGNLEFT y UDS_ALIGNRIGHT afectan a la posición del
control con respecto a la ventana amiga asociada. El primer estilo
alinéa el control a la izquierda y el segundo a la derecha de la
ventana amiga.
Si se especifica UDS_HORZ, las flechas del control apuntan a
derecha e izquierda, si se omite, apuntan arriba y abajo.
Si se especifica el estilo UDS_HOTTRACK, se modificará el
aspecto del control cuando el cursor del ratón está sobre él,
resaltando la flecha correspondiente.
El estilo UDS_SETBUDDYINT provoca que se modifique el texto
asociado a la ventana amiga, enviando un mensaje WM_SETTEXT
con el valor actual de control UpDown.
El estilo UDS_NOTHOUSANDS omite los puntos separadores de
miles en el texto asociado a la ventana amiga. Si no se especifica,
se insertarán puntos separadores.
UDS_ARROWKEYS hace que las flechas del cursor se
comporten como si se pulsarán las flechas del control.
Por último UDS_WRAP ajusta los valores del control dentro de
los márgenes indicados, de modo que si se sobrepasa el máximo,
se vuelve al valor mínimo, y si se baja por debajo del mínimo, se
vuelve al máximo. Si no se indica este estilo, al llegar al máximo se
mantiene el valor, y sólo es posible disminuirlo, y al llegar al mínimo,
sólo se puede incrementar el valor actual.
Ficheros de recursos
Para usar controles UpDown a un cuadro de diálogo dentro de
un fichero de recursos basta con añadir un control de la clase
UPDOWN_CLASS. Si además se especifica el estilo
UDS_AUTOBUDDY, hay que tener especial cuidado en el orden en
que se definen los controles, ya que el control UpDown se asociará
con el control especificado inmediantamente antes:
EDITTEXT ID_EDIT5, 10, 9, 93, 14, ES_AUTOHSCROLL
CONTROL "", ID_UPDOWN5, UPDOWN_CLASS,
UDS_ARROWKEYS | UDS_AUTOBUDDY | UDS_HOTTRACK |
UDS_SETBUDDYINT, 104, 9, 11, 14
Aceleradores
Es probable que ya lo hayas notado, pero los controles UpDown
no se comportan del mismo modo si se pulsa una vez sobre una de
sus flechas que si se mantiene pulsado durante un tiempo. Cuanto
más tiempo se mantiene pulsada una de las flechas, a mayor
velocidad cambia su valor actual. Este comportamiento es lo que se
llama "aceleradores", y se puede modificar a nuestra discrección
para adaptarlo a nuestro gusto o a nuestras necesidades.
Los aceleradores trabajan por tramos, y podemos añadir tantos
de esos tramos como queramos. Cada uno de ellos se define por
dos valores, el primero es un tiempo expresado en segundos, y el
segundo es el valor del incremento que se aplica a partir de que
transcurra ese tiempo.
Para cada tramo se usa una estructura UDACCEL. Si queremos
usar aceleradores en varios tramos, crearemos un array de estas
estructuras.
Supongamos que queremos que el primer tramo efectúe
incrementos de uno en uno, el segundo de diez en diez, a partir de
que trasncurran cinco segundos de pulsación, el tercero de
cincuenta en cincuenta, a partir de los diez segundos de pulsación
(contando desde el principio). El cuarto de cien en cien, a partir de
los quince segundos. El array quedaría de esta forma:
UDACCEL uda[] = {
{0, 1},
{5, 10},
{10, 50},
{15, 100}
};
int i;
i=SendDlgItemMessage(hwnd, ID_UPDOWN3, UDM_GETACCEL,
4, (LPARAM)uda);
Bases de numeración
Cuando se usa el estilo UDS_SETBUDDYINT, el texto de la
ventana amiga asociada al control se actualiza automáticamente
según el valor actual del control. En este caso, tenemos dos
posibilidades a la hora de elegir la base de numeración para
expresar esos valores: decimal o hexadecimal. Para establecer el
valor de la base de numeración a utilizar podemos usar el mensaje
UDM_SETBASE, indicando en wParam el valor de la base deseada,
que sólo puede ser 10 ó 16.
i = SendDlgItemMessage(hwnd, ID_UPDOWN1,
UDM_GETBASE, 0, 0);
Mensajes de notificación
Los controles UpDown pueden envíar tres mensajes diferentes a
sus ventanas padre. Dos de ellos dependen del estilo, si es
UDS_HORZ enviarán mensajes WM_HSCROLL cada vez que se
pulse una de las flechas. Si no se especifica ese estilo, el control
tendrá orientación vertical, y en su lugar enviará mensajes
WM_VSCROLL.
Podemos usar estos mensajes para actualizar la ventana amiga
asociada de forma manual, de modo que podamos personalizar el
valor mostrado. De ese modo podemos añadir caracteres de
formato, o trabajar con valores no enteros, o incluso con valores no
numéricos, siempre y cuando a cada posible valor del ámbito de
valores del control UpDown le hagamos corresponder un valor de
otro dominio.
Por ejemplo, podemos asociar un control edit de sólo lectura a
un control UpDown para elegir frutas dentro de los valores definidos
en un array de cadenas:
NM_UPDOWN *nmup;
LPNMHDR pnmhdr;
...
case WM_NOTIFY:
pnmhdr = (LPNMHDR)lParam;
switch(pnmhdr->code) {
case UDN_DELTAPOS:
if(pnmhdr->idFrom == ID_UPDOWN3) {
nmup = (NM_UPDOWN*)lParam;
if(!((nmup->iPos+nmup->iDelta) % 3))
if(nmup->iDelta > 0)
nmup>iDelta++; else nmup>iDelta--;
return FALSE;
}
break;
}
break;
Ejemplo 90
Capítulo 53 Control de
cabecera
Un control de cabecera es
una ventana estrecha, en
forma de barra, dividida en
zonas, a las que denominamos
Ejemplo de control de cabecera
ítems. Generalmente se usan
con listas formadas por varias
columnas, a cada una de las cuales le corresponde un ítem. El
usuario puede modificar la anchura de cada ítem arrastrando el
divisor que se encuentra entre los ítems.
Los controles de cabecera no están pensados para ser usados
como controles individuales, sino como controles hijos de otros
controles, como listas. Sin embargo, asociar un control de cabecera
a un control lista no funcionará como esperaríamos directamente, ya
que los controles lista no están preparados para mostrar información
en varias columnas. En esos casos deberemos crear una subclase
del control lista, y además, la lista deberá ser owner-draw.
Sin embargo, hay otros controles comunes que incluyen un
control de cabecera, como el ListView, que aún no hemos visto. Lo
que aprendamos sobre controles de cabecera nos servirá para
aplicarlo a estos controles, ya que podremos conseguir un
manipulador al control de cabecera asociado a ellos.
RECT rc;
HDLAYOUT hdl;
WINDOWPOS wp;
...
GetClientRect(hwnd, &rc);
hdl.prc = &rc;
hdl.pwpos = ℘
SendDlgItemMessage(hwnd, ID_HEADER1, HDM_LAYOUT, 0,
(LPARAM)&hdl);
/* O bien la macro:
Header_Layout(GetDlgItem(hwnd, ID_HEADER1), &hdl);
*/
Una vez tenemos las dimensiones y coordenadas del control,
sólo queda moverlo a esa posición y modificar su tamaño. Para ello
usaremos la función SetWindowPos, y aprovecharemos para
hacerlo visible, añadiendo la bandera SWP_SHOWWINDOW:
SetWindowPos(GetDlgItem(hwnd, ID_HEADER1),
wp.hwndInsertAfter, wp.x, wp.y,
wp.cx, wp.cy, wp.flags | SWP_SHOWWINDOW);
INITCOMMONCONTROLSEX iCCE;
...
iCCE.dwSize = sizeof(INITCOMMONCONTROLSEX);
iCCE.dwICC = ICC_LISTVIEW_CLASSES;
InitCommonControlsEx(&iCCE);
Añadir columnas
Para insertar columnas en un control de cabecera se usa el
mensaje HDM_INSERTITEM, indicando en el parámetro wParam el
índice de la columna a continuación de la cual se insertará la nueva,
y el lParam un puntero a una estructura HDITEM.
En la estructura HDITEM el parámetro mask indica qué
miembros opcionales de la estructura contienen valores válidos.
Cuando se trate de columnas con texto, deberemos indicar los
valores de pszText y cchTextMax, con los valores del texto y su
longitud máxima, respectivamente.
cxy indica la anchura o altura de la columna o fila, dependiendo
de si el control es horizontal o vertical.
También se puede usar un mapa de bits, mediante el miembro
hbm, o bien una imagen dentro de una lista de imágenes indicando
el índice en el miembro iImage.
El miembro lParam permite usar un dato asociado al ítem,
definido por la aplicación.
iOrder sirve para indicar el orden en que se mostrará la columna
dentro del control. Esto puede ser útil cuando se quieren conservar
las preferencias de orden del usuario, en lugar de creaer el control
siempre con el orden original por defecto.
El puntero pvFilter se usa para asignar un filtro a la columna. El
tipo de filtro se indica el en miembro type, que puede tomar los
valores HDFT_ISSTRING para cadena, HDFT_ISNUMBER para
valores enteros, HDFT_ISDATE para fechas y
HDFT_HASNOVALUE para ignorar el filtro.
Cuando se define un tipo concreto el sistema impide que se
introduzcan valores no permitidos, por ejemplo, no será posible
introducir texto en un filtro de tipo HDFT_ISNUMBER o
HDFT_ISDATE.
El miembro state indica el estado de la columna, el valor
HDIS_FOCUSED indica que la columna tiene el foco del teclado.
El miembro fmt sirve para determinar el formato: si se trata de
imágenes o texto, y en ese caso, si el texto se muestra centrado, a
la derecha, a la izquierda o de derecha a izquierda. Se puede añadir
una imagen de flecha arriba o flecha abajo, para indicar el orden en
que aparecen los elementos de la columna mediante los valores
HDF_SORTUP o HDF_SORTDOWN, respectivamente. El valor
HDF_CHECKBOX indica que se debe mostrar un checkbox, y si se
indica además el valor HDF_CHECKED se mostrará marcado.
HDF_FIXEDWIDTH impide que el usuario pueda modificar la
anchura de la columna, HDF_SPLITBUTTON muestra un botón de
despliegue.
return index;
}
case WM_SIZE:
GetClientRect(hwnd, &rc);
hdl.prc = &rc;
hdl.pwpos = ℘
SendDlgItemMessage(hwnd, ID_HEADER1, HDM_LAYOUT, 0,
(LPARAM)&hdl);
SetWindowPos(GetDlgItem(hwnd, ID_HEADER1),
wp.hwndInsertAfter, wp.x, wp.y,
wp.cx, wp.cy, wp.flags | SWP_SHOWWINDOW);
Estilos
Hay varios estilos que se pueden aplicar a los controles de
cabecera.
HDS_BUTTONS: cuando se requiera que la aplicación realice
altuna tarea (como seleccionar o copiar) cuando el usuario pulsa
sobre alguna de las columnas, este estilo hace que cada
encabezado se comporte como un botón. Si no se usa, cada ítem se
comporta como una etiqueta estática.
HDS_DRAGDROP: permite arrastrar los elementos del
encabezado.
HDS_FILTERBAR: añade una segunda barra con filtros.
HDS_FLAT: produce una apariencia plana.
HDS_FULLDRAG: para que se siga mostrando el contenido de
la columna mientras el usuario cambia su tamaño. Si no se indica
sólo se muestra el borde hasta que el usuario establece el nuevo
tamaño.
HDS_HIDDEN: Indica un control de encabezado que se va a
ocultar. Este estilo no oculta el control. En su lugar, cuando se envía
el mensaje de HDM_LAYOUT a un control de encabezado con el
estilo de HDS_HIDDEN, el control devuelve cero en el miembro CY
de la estructura windowpos ( . A continuación, ocultaría el control
estableciendo su alto en cero. Esto puede ser útil si desea usar el
control como contenedor de información en lugar de como un control
visual.
HDS_HORZ: Crea un control de encabezado con una orientación
horizontal. Hasta donde he podido ver, esta es la opción por defecto,
y no es posible crear controles de encabezado verticales.
HDS_HOTTRACK: Habilita el seguimiento activo. Este estilo no
parece tener ninguna funcionalidad en controles de cabecera, pero
sí se usa en controles ListView, que contienen en su composición un
control de cabecera.
HDS_CHECKBOXES: Permite colocar cajas de chequeo en los
elementos de encabezado.
HDS_NOSIZING: El usuario no puede modificar la anchura de
los items arrastrando el divisor.
HDS_OVERFLOW: Se muestra un botón cuando no se pueden
mostrar todos los elementos dentro del rectángulo del control de
encabezado. Al hacer clic en este botón, se envía una notificación
HDN_OVERFLOWCLICK.
int n;
n = SendDlgItemMessage(hwnd, ID_HEADER1, HDM_GETITEMCOUNT,
0, 0);
HDITEM hdi;
hdi.mask = HDI_FORMAT;
hdi.fmt = HDF_STRING | HDF_CHECKBOX | HDF_CENTER |
HDF_SORTDOWN | HDF_SPLITBUTTON;
HDITEM hdi;
hdi.mask = HDI_FILTER; // Recuperar información del filtro
SendDlgItemMessage(hwnd, ID_HEADER1, HDM_GETITEM,
(WPARAM)1, (LPARAM)&hdi);
int *orden;
int n;
n = SendDlgItemMessage(hwnd, ID_HEADER1,
HDM_GETITEMCOUNT, 0, 0);
if((orden = (int*)calloc(n, sizeof(int)))) {
SendDlgItemMessage(hwnd, ID_HEADER1,
HDM_GETORDERARRAY, (WPARAM)n, (LPARAM)orden);
free(orden);
}
int *orden;
int n;
n = SendDlgItemMessage(hwnd, ID_HEADER1,
HDM_GETITEMCOUNT, 0, 0);
if((orden = (int*)calloc(n, sizeof(int)))) {
orden[0] = 2;
orden[1] = 1;
orden[2] = 0;
SendDlgItemMessage(hwnd, ID_HEADER1,
HDM_SETORDERARRAY, (WPARAM)n, (LPARAM)orden);
free(orden);
}
hhi.pt.x=10;
hhi.pt.y=10;
int i;
i = SendDlgItemMessage(hwnd, ID_HEADER1, HDM_HITTEST,
(WPARAM)0, (LPARAM)&hhi);
Arrastre de divisores
Si el control de cabecera no tiene el estilo HDS_NOSIZING, el
usuario podrá modificar la anchura de un item arrastrando el divisor
de la derecha de ese item.
Windows gestiona automáticamente las operaciones de arrastre
de divisores, pero si una aplicación tiene que gestionar estas
operaciones tendrá que procesar algunos mensajes de notificación.
La operación de arrastre comienza cuando el usuario hace clic
con el botón izquierdo sobre la división entre dos items. El programa
indica que el cursor está en ese divisor cambiado el cursor
Cursor de divisor. La aplicación recibe un mensaje de
notificación HDN_BEGINTRACK. Se siguen enviando mensajes de
notificación HDN_TRACK mientras el usuario mantenga pulsado el
botón izquierdo y mueva el ratón. Cuando se libere el botón del
ratón se envía un mensaje de notificación HDN_ENDTRACK.
Además la aplicación recibirá un mensaje de notificación
HDN_DIVIDERDBLCLICK cuando el usuario haga doble clic sobre
un divisor. De nuevo, no hay un comportamiento automático como
respuesta a este mensaje y será la aplicación la responsable de
procesarlo. El comportamiento esperado en este caso es ajustar la
anchura del item a la mínima para mostrar el texto, pero en cada
caso esto puede ser diferente.
Mensajes de filtros
Si el control de cabecera tiene el estilo HDS_FILTERBAR se
mostrará una segunda línea en cada item del control que permite
editar textos, números o fechas que se usarán como filtro para la
columna.
Cuando se especifica como numérico para el tipo de filtro, sólo
se permiten valores enteros, no en coma flotante.
Disponemos de varios mensajes para actualizar filtros,
HDM_CLEARFILTER borra el valor del filtro para un ítem
determinado. El índice del item se especifica en wParam. lParam no
se usa.
El mensaje HDM_EDITFILTER sirve para situal el foco en el
cuadro de edición del filtro. En wParam se indica el índice del item, y
en lParam se indica qué hacer con el valor del texto si el filtro ya
estaba siendo editado por el usuario.
Desde que se modifica el valor de un filtro hasta que se envía a
la aplicación un mensaje de notificación HDN_FILTERCHANGE
transcurre cierto tiempo. Este tiempo se puede modificar mediante el
mensaje HDM_SETFILTERCHANGETIMEOUT indicando en lParam
el tiempo en milisegundos desde que se modifican los atributos del
filtro hasta que se envía la notificación.
Cada vez que el valor de un filtro se modifica se envía un
mensaje de notificación HDN_FILTERCHANGE a la aplicación. Esto
se hace incluso durante la edición del valor del filtro, siempre que no
haya transcurrido el tiempo de espera entre modificaciones
sucesivas.
Por ejemplo, el usuario está editando un filtro, y el tiempo de
espera (timeout) está establecido en un segundo (1000 ms),
mientras el usuario está escribiendo, si deja de hacerlo más de un
segundo, se enviará un mensaje de notificación
HDN_FILTERCHANGE, si no llega a pasar ese tiempo entre
actualizaciones del valor, no se enviarán mensajes de notificación.
Hay que tener en cuenta que aplicar un filtro puede ser una tarea
que requiera mucho tiempo de procesador y de actualización de
pantalla, por lo que especificar un tiempo de espera permite que el
usuario pueda modificar el valor del filtro sin que la aplicación reciba
la notificación de que el valor del filtro ha sido modificado. Estos
mensajes de notificación se envían durante la edición del filtro, y no
sólo cuando el usuario de por terminada la edición pulsado return, o
haciendo click en otra zona de la pantalla.
También se envían a la aplicación mensajes de notificación
cuando se inicia la edición de un filtro, HDN_BEGINFILTEREDIT, y
cuando termina HDN_ENDFILTEREDIT.
Por último, el mensaje de notificación HDN_FILTERBTNCLICK
se envía a la aplicación cuando se pulsa sobre el icono del filtro de
un item (el icono del embudo). También se envía esta notificación
como respuesta a un mensaje HDM_SETITEM.
De nuevo, el sistema no tiene un comportamiento definido para
este botón, y será la aplicación la encargada de responder
adecuadamente a éste mensaje, si lo considera necesario.
Indicativos de orden
Cuando un control de cabecera esté asociado a un listview, cada
columna contendrá una lista de valores. Ya hemos visto que
podemos filtrar esos valores, y además podremos necesitar
ordenarlos. Cuando los valores de una columna estén ordenados
pueden aparecer en orden ascendente o descendente. Cada item
permite mostrar una marca para indicar el orden. Estas marcas se
activan mediante el miembro fmt de la estructura HDITEM,
añadiendo los valores HDF_SORTUP o HDF_SORTDOWN,
respectivamente.
Podemos hace esto al insertar cada item, o bien modificándolo
mediante un mensaje HDM_SETITEM.
HDITEM hdi;
int iItem = 1;
...
hdi.mask = HDI_FORMAT;
SendDlgItemMessage(hwnd, ID_HEADER1, HDM_GETITEM,
(WPARAM)iItem, (LPARAM)&hdi);
hdi.fmt |= HDF_SORTUP;
SendDlgItemMessage(hwnd, ID_HEADER1, HDM_SETITEM,
(WPARAM)iItem, (LPARAM)&hdi);
Botón de desplegar
Cuando se crea un control
de cabecera con el estilo
HDS_BUTTONS y el ítem
incluya la bandera de formato Control de cabecera dropdown
HDF_SPLITBUTTON, la
aplicación recibirá un mensaje de notificación HDN_DROPDOWN
cuando el usuario haga clic con el ratón en el icono de despliegue.
El icono permanecerá oculto, y sólo se mostrará cuando el cursor
del ratón esté sobre el ítem.
No hay comportamiento por defecto definido para este mensaje,
será el programador el responsable de mostrar en pantalla el
resultado de pulsar ese botón. Por ejemplo, las hojas de cálculo
despliegan una ventana para definir filtros, pero cada aplicación
puede requerir un comportamiento diferente.
Existen dos mensajes que pueden resultar útiles para procesar
éste mensaje:
Cajas de chequeo
Para cada item se puede añadir una caja de chequeo. Para ello
el control debe tener el estilo HDS_CHECKBOXES, y además, el
item debe tener el flag HDF_CHECKBOX. Adicionalmente se puede
añadir el flag HDF_CHECKED para indicar que la caja esté
marcada.
Se enviará un mensaje de notificación
HDN_ITEMSTATEICONCLICK a la aplicación cuando el usuario
pulse sobre la caja de chequeo. Para que este mensaje se envíe, el
control también debe tener el estilo HDS_BUTTONS.
case HDN_ITEMSTATEICONCLICK:
iItem = pnmhdr->iItem;
hdi.mask = HDI_FORMAT;
SendDlgItemMessage(hwnd, ID_HEADER1,
HDM_GETITEM, (WPARAM)iItem, (LPARAM)&hdi);
if(hdi.fmt & HDF_CHECKED) hdi.fmt =
(UINT)hdi.fmt & ~HDF_CHECKED;
else hdi.fmt |= HDF_CHECKED;
hdi.mask = HDI_FORMAT;
SendDlgItemMessage(hwnd, ID_HEADER1,
HDM_SETITEM, (WPARAM)iItem, (LPARAM)&hdi);
break;
Overflow
Si se crea el control de cabecera con el estilo HDS_OVERFLOW,
si todos los items no pueden ser visualizados en pantalla porque el
ancho de la ventana es insuficiente, se mostrará un botón
Botón de overflow a la derecha del control.
Podemos recuperar el rectángulo delimitador de ese botón
mediante el mensaje HDM_GETOVERFLOWRECT, pasando en
lParam un puntero a una estructura RECT que recibirá la
información del rectángulo.
Si el usuario pulsa el botón de overflow se enviará un mensaje
de notificación HDN_OVERFLOWCLICK. Tampoco existe un
comportamiento predefinido para este mensaje, de modo que de
nuevo será la aplicación la encargada de procesarlo.
Pulsaciones de tecla
Se envían mensaje de notificación HDN_ITEMKEYDOWN
cuando el usuario pulsa alguna tecla estando activo el control. No se
envían notificaciones con todas las teclas.
NMHDDISPINFO* pdi;
char* szTextoItem = "Prueba";
...
case WM_NOTIFY:
pnmhdr = (LPNMHEADER)lParam;
switch(pnmhdr->hdr.code) {
case HDN_GETDISPINFO:
pdi = (NMHDDISPINFO*)lParam;
if(pdi->mask & HDI_IMAGE) {
pdi->iImage = 4;
}
if(pdi->mask & HDI_TEXT) {
strcpy(pdi->pszText, szTextoItem);
}
pdi->mask |= HDI_DI_SETITEM;
return 0;
}
...
Ejemplo 91
Ejemplo 92
Ejemplo 93
Capítulo 54 Control
ComboBoxEx
Un control ComboBoxEx
es, como su nombre indica,
un control ComboBox
extendido. La mayor
diferencia con los ComboBox
que hemos visto hasta ahora
es que permite mostrar
imágenes para cada item.
Por lo demás, siguen
siendo un controles
ComboBox, es decir,
contienen una caja de edición Ejemplo de control ComboBoxEx
y una lista desplegable con
las posibles opciones. Mantiene los estilos de control de los
ComboBoxes básicos:
INITCOMMONCONTROLSEX iCCE;
...
iCCE.dwSize = sizeof(INITCOMMONCONTROLSEX);
iCCE.dwICC = ICC_USEREX_CLASSES;
InitCommonControlsEx(&iCCE);
case WM_CREATE:
hInstance = ((LPCREATESTRUCT)lParam)->hInstance;
hCtrl = CreateWindowEx(0, WC_COMBOBOXEX, NULL,
WS_BORDER | WS_VISIBLE | WS_CHILD |
CBS_DROPDOWN,
0, 0, 200, 200,
hwnd, (HMENU)ID_COMBOBOXEX,
hInstance, NULL);
break;
Estilos
Siguen disponibles los estilos de los controles ComboBox, ver
capítulo 43 para más detalles.
Además, existen otros estilos extendidos, que básicamente
sirven para limitar algunas de las nuevas características.
CBES_EX_CASESENSITIVE: las
búsquedas de cadenas en la lista
distinguen mayúsculas de minúsculas.
CBES_EX_NOEDITIMAGE: no se
muestra imagen de item en la caja de
edición ni en la lista.
CBES_EX_NOEDITIMAGEINDENT:
equivale a CBES_EX_NOEDITIMAGE
CBES_EX_NOSIZELIMIT: Permite que el
tamaño vertical del control ComboBoxEx
sea más pequeño que el del control
ComboBox incluido.
ComboBoxEx Elipsis
CBES_EX_PATHWORDBREAKPROC: se
usarán los caracteres '/', '\', y '.' como
delimitadores de palabra. Esto ayuda a moverse por palabras
en nombres de fichero y URLs
CBES_EX_TEXTENDELLIPSIS: cuando el texto de un ítem no
quepa en el ancho del control, en lugar de cortarse se sustituye
el final por puntos suspensivos.
SendMessage(hCtrl, CBEM_SETEXTENDEDSTYLE,
(WPARAM)CBES_EX_CASESENSITIVE,
(LPARAM)CBES_EX_CASESENSITIVE |
CBES_EX_TEXTENDELLIPSIS);
Lista de imágenes
Cada item en un control ComboBoxEx tiene asociadas tres
imágenes, una para mostrar normalmente, otra para mostrar cuando
el item está activo y una tercera que se usa para superponer. El
mensaje CBEM_SETIMAGELIST sirve para asignar una lista de
imágenes a un control ComboBoxEx.
Para recuperar la lista de imágenes asociada a un control
ComboBoxEx se usa el mensje CBEM_GETIMAGELIST, sin
parámetros.
Es importante eliminar la lista de imagenes antes de terminar la
aplicación.
HIMAGELIST hIml;
HWND hCtrl;
HBITMAP hbmp;
...
case WM_CREATE:
hInstance = ((LPCREATESTRUCT)lParam)->hInstance;
hIml = ImageList_Create(24, 24,
ILC_COLOR16|ILC_MASK, 19, 4);
hbmp = LoadBitmap(hInstance, "imagenes");
ImageList_AddMasked(hIml, hbmp,
RGB(255,255,255));
DeleteObject(hbmp);
hCtrl = CreateWindowEx(0, WC_COMBOBOXEX, NULL,
WS_BORDER | WS_VISIBLE | WS_CHILD |
CBS_DROPDOWN | CBS_SORT,
0, 0, 0, 200,
hwnd, (HMENU)ID_COMBOBOXEX,
hInstance, NULL);
SendMessage(hCtrl, CBEM_SETIMAGELIST,0,
(LPARAM)hIml);
...
case WM_DESTROY:
hIml = (HIMAGELIST)SendMessage(hCtrl,
CBEM_GETIMAGELIST, 0, 0);
ImageList_Destroy(hIml);
...
Insertar items
Para insertar items en un control ComboBoxEx se usa el
mensaje CBEM_INSERTITEM. En lParam hay que pasar un puntero
a una estructura COMBOBOXEXITEM con la información
correspondiente al item.
En el miembro mask de la estructura se deben activar las
banderas que indican qué miembros de la estructura contienen
valores válidos. En iItem el índice del item.
En el miembro iItem se especifica la posición de inserción. El
valor debe estar entre 0 y el número de items en el control. Si se
especifica un valor mayor, la operación de inserción fallará.
Para insertar el item en la última posición se puede usar el valor
retornado por el mensaje CB_GETCOUNT.
Para asignar un texto al item hay que asignar valores a los
miembros pszText. El miembro cchTextMax se ignora cuando se
está insertando un item.
El miembro iImage es el índice de la imagen dentro de la lista de
imágenes asignada al control que se muestra cuando el item no esté
seleccionado.
El miembro iSelectedImage es el índice de la imagen dentro de
la lista de imágenes asignada al control que se muestra cuando el
item esté seleccionado.
El miembro iOverlay se supone que es para generar una imagen
superponiendola a otra existente (presumiblemente iImage), sin
embargo no parece que funcione y la documentación al respecto es
muy incompleta.
El miembro iIndent sirve para añadir espacios a la izquierda del
item. Cada espacio equivale a 10 pixels.
Finalmente, el miembro lParam permite almacenar un valor que
puede usar la aplicación para su funcionamiento.
Modificar un item
Es posible modificar los atributos de un item que ya esté en la
lista, para ello usaremos el mensaje CBEM_SETITEM. El
comportamiento es similar al de insertar un item, pasando en lParam
un puntero a una estructura COMBOBOXEXITEM con los miembros
asignados con los nuevos valores del item y el índice del item en el
miembro iItem.
COMBOBOXEXITEM cbeitem;
...
cbeitem.mask = CBEIF_TEXT;
cbeitem.iItem = 4;
cbeitem.pszText = "Modificado";
SendMessage(hCtrl, CBEM_SETITEM, 0,
(LPARAM)&cbeitem);
Eliminar un item
Para eliminar items se usa el mensaje CBEM_DELETEITEM. En
wParam se determina el número del iItem a borrar.
Edición de valores
Si el control no tiene el estilo CBS_DROPDOWNLIST, la caja de
edición estará activa.
Cada vez que el usuario pulsa sobre la caja de edición, o que se
active por otra causa (TAB o atajo de teclado o que se haya pulsado
el botón de despliegue de la lista), la aplicación recibirá un mensaje
de notificación CBEN_BEGINEDIT. A partir de ese momento
estaremos en proceso de edición. Cunado la edición termine se
enviará a la aplicación recibirá un mensaje de notificación
CBEN_ENDEDIT.
Con el mensaje de notificación CBEN_ENDEDIT, en lParam se
envía un puntero a una estructura NMCBEENDEDIT. En esta
estructura, el miembro fChanged nos indicará si el valor actual de la
caja de edición ha sido modificado. iNewSelection contendrá el
índice del item de la lista, siempre que el contenido actual de la caja
de edición esté en la lista. En caso contrario vadrá -1. szText
contiene el valor de la caja de edición y iWhy nos indica el motivo
por el que se ha dado por concluida la edición, ya sea pérdida de
foco, despliegue de la lista o pulsación de ESC o INTRO.
NMCBEENDEDIT* nmCEE;
...
case WM_NOTIFY:
pnmhdr = (LPNMHEADER)lParam;
switch(pnmhdr->hdr.code) {
case CBEN_BEGINEDIT:
return 0;
case CBEN_ENDEDIT:
nmCEE = (NMCBEENDEDIT*)lParam;
if(nmCEE->fChanged && nmCEE-
>iNewSelection == -1) {
InsertarItem(hCtrl, -1, 17, 8, 2,
nmCEE->szText);
c = SendMessage(hCtrl, CB_GETCOUNT,
0, 0);
SendMessage(hCtrl, CB_SETCURSEL,
(WPARAM)c, 0);
}
return 0;
}
break;
También disponemos de un mensaje para averiguar si el
contenido de la caja de edición CBEM_HASEDITCHANGED, pero
sólo funcina si se usa antes de que se envíe el mensaje de
notificación NMCBEENDEDIT, ya que si se envía después siempre
devuelve FALSE.
Ejemplo 94
Ficheros de recursos
No existe un tipo de control específico para crear ComboBoxEx
en un fichero de recursos, de modo que siempre deberemos insertar
estos controles en ejecución. Podemos, sin embargo, usar un
control ComboBox para situar el control si usamos un editor de
recursos como ResEdit, y convertir las coordenadas de diálogo a
coordenadas de pantalla usando la función MapDialogRect:
//
// Dialog resources
//
LANGUAGE LANG_NEUTRAL, SUBLANG_NEUTRAL
DialogoEx DIALOG 0, 0, 321, 57
STYLE DS_3DLOOK | DS_CENTER | DS_MODALFRAME | DS_SHELLFONT |
WS_CAPTION | WS_VISIBLE | WS_POPUP | WS_SYSMENU
CAPTION "ComboBoxEx"
FONT 8, "Helv"
{
/* COMBOBOX usado como plantilla para COMOBOXEX, comentado
posteriormente para ser insertado en ejecución
COMBOBOX ID_COMBOBOXEX, 8, 9, 221, 81,
CBS_DROPDOWN | CBS_HASSTRINGS | CBES_EX_CASESENSITIVE,
WS_EX_LEFT */
PUSHBUTTON "Cancel", IDCANCEL, 259, 27, 50, 14, 0,
WS_EX_LEFT
DEFPUSHBUTTON "OK", IDOK, 259, 10, 50, 14, 0,
WS_EX_LEFT
}
Ejemplo de procedimiento de diálogo:
switch(uMsg) {
case WM_INITDIALOG:
hIml = ImageList_Create(24, 24,
ILC_COLOR16|ILC_MASK, 19, 4);
hbmp = LoadBitmap((HINSTANCE)lParam,
"imagenes");
ImageList_AddMasked(hIml, hbmp,
RGB(255,255,255));
DeleteObject(hbmp);
hfont = CreateFont(-11, 0, 0, 0, 0, FALSE,
FALSE, FALSE, 1, 0, 0, 0, 0, ("Ms Shell Dlg"));
re.top = 8;
re.left = 9;
re.bottom = 221;
re.right = 81;
MapDialogRect(hwndDlg, &re);
// Insertar items:
InsertarItem(hwndDlg, ID_COMBOBOXEX, 0, 9, 0, 0,
"Reloj");
InsertarItem(hwndDlg, ID_COMBOBOXEX, 1, 10, 1,
1, "Bateria");
InsertarItem(hwndDlg, ID_COMBOBOXEX, 2, 11, 2,
2, "Usuario");
InsertarItem(hwndDlg, ID_COMBOBOXEX, 3, 12, 3,
0, "Libro");
InsertarItem(hwndDlg, ID_COMBOBOXEX, 4, 13, 4,
1, "Caja");
InsertarItem(hwndDlg, ID_COMBOBOXEX, 5, 14, 5,
2, "Café");
InsertarItem(hwndDlg, ID_COMBOBOXEX, 6, 15, 6,
0, "Puzzle");
InsertarItem(hwndDlg, ID_COMBOBOXEX, 7, 16, 7,
1, "Galletas");
InsertarItem(hwndDlg, ID_COMBOBOXEX, 8, 17, 8,
2, "Tierra");
SendDlgItemMessage(hwndDlg, ID_COMBOBOXEX,
CB_SETCURSEL, (WPARAM)0, 0);
return TRUE;
...
}
}
Controles base
Los controles ComboBoxEx constan de dos controles base: un
control de edición y un ComboBox. Es posible obtener un
manipulador de cada uno de ellos. Para el control ComboBox se usa
el mensaje CBEM_GETCOMBOCONTROL y para el control de
edición un mensaje CBEM_GETEDITCONTROL,
Operaciones de arrastre
Si la aplicación implementa operaciones de drag and drop al
recibir un mensaje de notificación CBEN_DRAGBEGIN debe iniciar
una de ellas.
Ya hablamos algo sobre este tipo de operaciones en el capítulo
48, pero probablemente volvamos a dedicar tiempo a este tipo de
funciones.
Temas de Windows
Por último, también disponemos de un mensaje para aplciar un
tema de Windows al control ComboBoxEx:
CBEM_SETWINDOWTHEME.
Para más información sobre los temas de Windows, visita este
enlace.
Ejemplo 95
Capítulo 55 Control de
selección de fecha y hora
Un control de selección de
fecha y hora sirve,
evidentemente, para que el
usuario pueda introducir en
una aplicación valores de
fechas u horas válidos. Estos
controles nos facilitan la vida,
ya que, por lo que respecta a
fechas, nos proporciona una
forma sencilla de validarlas y
acceder a calendarios. Y en lo Ejemplo de control Fecha y hora
que respecta a horas, facilita
la validación de datos.
Como en todos los controles comunes que estamos viendo, hay
que asegurarse de que la DLL ha sido cargada invocando a la
función InitCommonControlsEx indicando el valor de bandera
ICC_DATE_CLASSES en el miembro dwICC de la estructura
INITCOMMONCONTROLSEX que pasaremos como parámetro.
INITCOMMONCONTROLSEX iCCE;
...
iCCE.dwSize = sizeof(INITCOMMONCONTROLSEX);
iCCE.dwICC = ICC_DATE_CLASSES;
InitCommonControlsEx(&iCCE);
case WM_CREATE:
hInstance = ((LPCREATESTRUCT)lParam)->hInstance;
hCtrl = CreateWindowEx(0, DATETIMEPICK_CLASS,
NULL,
WS_BORDER | WS_CHILD | WS_VISIBLE |
WS_TABSTOP | DTS_LONGDATEFORMAT,
10,10,220,30,
hwnd, (HMENU)ID_DATETIME,
hInstance, NULL);
break;
Estilos
Podemos dividir los estilos específicos de estos controles en
varios tipos: los de formato, los que afectan a al aspecto, y los que
afectan al comportamiento.
Estilos de formato:
Estilo de comportamiento:
Asignar un valor
Por defecto, si no se asigna un valor inicial a un control de fecha
y hora, se asignará la fecha y hora actual en el momento en que se
crea el control. Esta información se almacena en una estructura
SYSTEMTIME, de modo que aunque el control sólo visualice una
fecha o una hora, en realidad contiene toda la información de tiempo
de sistema.
Para asignar un valor diferente podemos usar indistintamente el
mensaje DTM_SETSYSTEMTIME o la macro
DateTime_SetSystemtime.
Si optamos por el mensaje, en wParam indicaremos el valor
GDT_VALID, si vamos a inicializar el control con una fecha/hora
válida o GDT_NONE, si queremos que el control esté en un estado
"sin fecha". Para este segundo caso el control debe tener el estilo
DTS_SHOWNONE, y como consecuencia se eliminará la marca del
checkbox, quedanto el texto en gris.
En lParam pasaremos un puntero a una estructura
SYSTEMTIME con el valor de fecha y/o hora a asignar.
Si todo va bien, el mensaje retornará un valor distinto de cero.
Si optamos por la macro, el primer parámetro es el manipulador
de ventana del control, el segundo equivale al wParam del mensaje
y el tercero equivale al lParam del mensaje.
SYSTEMTIME st;
int v;
...
st.wYear = 2020;
st.wMonth = 1;
st.wDay = 1;
st.wHour = 12;
st.wMinute = 15;
st.wSecond = 0;
st.wMilliseconds = 0;
// Mensaje:
v = SendDlgItemMessage(hwnd, ID_DATETIME,
DTM_SETSYSTEMTIME, (WPARAM)GDT_VALID, (LPARAM)&st);
// Macro
v = DateTime_SetSystemtime(GetDlgItem(hwnd,
ID_DATETIME), GDT_VALID, &st);
Obtener un valor
Para obtener el valor actual de un control de fecha y hora,
siempre que no tenga el estilo DTS_SHOWNONE y el checkbox
esté sin marcar, podemos usar el mensaje DTM_GETSYSTEMTIME
o bien la macro DateTime_GetSystemtime.
En el caso del mensaje tan sólo hay que pasar en lParam un
puntero a una estructrura SYSTEMTIME que recibirá el valor actual
del control.
Con la macro el primer parámetro será el manipulador de
ventana del control, y el segundo un puntero a una estructrura
SYSTEMTIME donde se situará el valor recuperado del control.
En ambos casos el valor de retorno será GDT_VALID si tiene
éxito o GDT_NONE si el control tiene el estilo DTS_SHOWNONE y
el checkbox no está marcado, en ese caso el valor de retorno no
será válido.
SYSTEMTIME st;
int v;
...
v = SendDlgItemMessage(hwnd, ID_DATETIME,
DTM_GETSYSTEMTIME, 0, (LPARAM)&st);
v = DateTime_GetSystemtime(GetDlgItem(hwnd,
ID_DATETIME), (LPARAM)&st);
Establecer rangos
Es posible limitar el rango de fechas que el usuario puede
seleccionar en el control. Para ello disponemos del mensaje
DTM_SETRANGE y de la macro DateTime_SetRange, que
podemos usar indistintamente.
En el caso del mensaje tendremos que pasar en wParam una
combinación de los valores GDTR_MIN y GDTR_MAX, para indicar
si queremos establecer el margen mínimo, el máximo o ambos. En
lParam pasaremos un puntero a un array de dos estructuras
SYSTEMTIME, en el que el primer elemento será el margen mínimo
y el segundo el máximo del rango.
En el caso de la macro, el primer parámetro será un manipulador
de ventana del control, el segundo una combinación de los valores
GDTR_MIN y GDTR_MAX y el tercero un array de estructuras
SYSTEMTIME que definen el rango.
En ambos casos el valor de retorno será distinto de cero si la
asignación de rango es establecida, y cero si no es así.
Si no se especifica alguna de las constantes GDTR_MIN o
GDTR_MAX, ese estremo del rango no se establecerá, y no será
necesario que el valor de SYSTEMTIME correspondiente tenga un
valor válido.
En este ejemplo se establece un rango de fechas válidas entre el
15 de noviembre de 2020 y el 15 de diciembre de 2020.
SYSTEMTIME rango[2];
int v;
...
rango[0].wYear = 2020;
rango[0].wMonth = 11;
rango[0].wDay = 15;
rango[1].wYear = 2020;
rango[1].wMonth = 12;
rango[1].wDay = 15;
v = SendDlgItemMessage(hwnd, ID_DATETIME, DTM_SETRANGE,
(WPARAM)(GDTR_MIN | GDTR_MAX), (LPARAM)rango);
v = DateTime_SetSystemtime(GetDlgItem(hwnd,
ID_DATETIME), GDTR_MIN | GDTR_MAX, rango);
Obtener rangos
De manera similar podemos recuperar el rango establecido para
un control mediante un mensaje DTM_GETRANGE o usando la
macro DateTime_GetRange.
En el caso del mensaje pasaremos en lParam la dirección de un
array de dos estructuras SYSTEMTIME que recibirá los valores
mínimo y máximo del rango.
Para la macro, el primer parámetro es un manipulador de
ventana del control, y el segundo la dirección del array.
En ambos casos el valor de retorno será una combinación de los
valores GDTR_MIN y GDTR_MAX, que nos indicará qué valores del
array son válidos.
SYSTEMTIME rango[2];
int v;
...
v = SendDlgItemMessage(hwnd, ID_DATETIME, DTM_GETRANGE,
0, (LPARAM)rango);
v = DateTime_GetSystemtime(GetDlgItem(hwnd,
ID_DATETIME), rango);
<assemblyIdentity type="win32"
name="Microsoft.Windows.Common-Controls" version="6.0.0.0"
processorArchitecture="*"
publicKeyToken="6595b64144ccf1df" language="*"/>
NMHDR* pnmHdr;
HWND hCMwnd;
...
case WM_NOTIFY:
pnmHdr = (NMHDR*)lParam;
switch(pnmHdr->code) {
case DTN_DROPDOWN:
hCMwnd = SendMessage(pnmHdr->hwndFrom,
DTM_GETMONTHCAL, 0, 0);
hCMwnd = DateTime_GetMonthCal(pnmHdr-
>hwndFrom);
printf("Abierto %d\n", hCMwnd);
break;
case DTN_CLOSEUP:
printf("Cerrado\n");
break;
}
break;
Cambiar la fuente
Los mensajes DTP_*MC* hacen referencia al control hijo
calendario mensual, pero no se deben enviar a ese control, sino al
propio control de selección de fecha y hora. Es decir las opciones
que modifiquemos (colores, fuentes o estilos) para el control hijo de
calendario mensual tendrán efecto cada vez que se despliegue ese
control, ya que se almacenarán con las propiedades del control
padre.
Podemos modificar la fuente usada por el control de calendario
mensual hijo mediante el mensaje DTM_SETMCFONT o la macro
DateTime_SetMonthCalFont.
Cuando se envíe el mensaje el wParam pasaremos un
manipulador de la fuente a usar y en lParam un valor BOOL que
indicará si el control debe ser redibujado. Cuando usemos la macro,
el primer parámetro será el manipulador de ventana del control de
selección de fecha y hora, y los otros dos parámetros son los
mismos que para el mensaje.
Cambio de estilos
Cambio de colores
Sólo si no están
activos los estilos
visuales, podemos
modificar los colores del
control de calendario
mensual. Para ello
disponemos del mensaje
DTM_SETMCCOLOR o
de la macro
DateTime_SetMonthCalCo
lor. Colores calendario
Si se usa el mensaje,
en wParam indicaremos de qué parte del control queremos cambiar
el color, y en lParam el color que vamos a asignar, en formato
COLORREF.
En el caso de la macro, el primer parámetro es un manipulador
del control de selección de fecha y hora, y los dos siguientes son los
mismos que en el mensaje.
Para indicar qué parte del control queremos modificar exiten
varias constantes:
Cerrar calendario
Asignar formato
En este contexto, el formato se refiere a la cadena que se
muestra cuando se elige el estilo de formato
DTS_LONGDATEFORMAT. Si el sistema operativo está en español,
por defecto tiene la forma "[dia de semana], [día del mes] de
[nombre del mes] de [año]", pero esto puede cambiarse usando el
mensaje DTM_SETFORMAT o la macro DateTime_SetFormat.
Usando el mensaje indicaremos en lParam un puntero a la nueva
cadena de formato, o NULL si queremos restablecer la cadena por
defecto. Con la macro el primer parámetro será el manipulador de
ventana del control, y el segundo el puntero a la nueva cadena de
formato.
Los literales que se quieran insertar en el formato deberán
indicarse entre comillas simples. Para los campos de fecha hay
ciertas reglas:
HFONT hFont;
SIZE size;
...
hFont = CreateFont(-11, 0, 0, 0, 0, FALSE, FALSE,
FALSE, 1, 0, 0, 0, 0, ("Ms Shell Dlg"));
SendDlgItemMessage(hwnd, ID_DATETIME,
DTM_GETIDEALSIZE, 0, (LPARAM)&size);
MoveWindow( GetDlgItem(hwnd, ID_DATETIME), 10, 30,
size.cx, size.cy, TRUE);
Obtener información
Se puede obtener información adicional sobre el control usando
el mensaje DTM_GETDATETIMEPICKERINFO o la macro
DateTime_GetDateTimePickerInfo.
En el mensaje pasaremos un puntero a una estructura
DATETIMEPICKERINFO en lParam. En la macro pasaremos en el
primer parámetro un manipulador de ventana del control de fecha y
hora y como segundo parámetro un puntero a la estructura.
Antes de enviar el mensaje o usar la macro hay que inicializar el
miembro cbSize de la estructura con el valor
sizeof(DATETIMEPICKERINFO).
La estructura se devolverá con datos relativos al control, el área
en pantalla y el estado de la caja de checkbox, el área y estado del
botón de despliegue, y los manipuladores de ventana del los
controles hijo de edición, up-down y cuadrícula desplegable.
Códigos de notificación
Existen algunos códigos de notificación específicos para este
tipo de controles. Como todos los códigos de notificación, se envían
a través de un mensaje WM_NOTIFY.
Campos de retrollamada
}
printf("Format\n");
return 0;
case DTN_FORMATQUERY:
// Obtener tamaños de callback fields:
pnmdtfq = (NMDATETIMEFORMATQUERY*)lParam;
if(!strcmp(pnmdtfq->pszFormat, "XX")) { //
Valor fijo:
CalculaTamanoTexto(GetDlgItem(hwnd,
ID_DATETIME), "Fecha:", &pnmdtfq->szMax);
}
if(!strcmp(pnmdtfq->pszFormat, "XXX")) { //
Mañana, tarde o noche, en función de hora.
CalculaTamanoTexto(GetDlgItem(hwnd,
ID_DATETIME), "mañana", &pnmdtfq->szMax);
}
return 0;
case DTN_WMKEYDOWN:
pnmdtk = (NMDATETIMEWMKEYDOWN*)lParam;
if(!strcmp(pnmdtk->pszFormat, "XXX")) {
if(pnmdtk->nVirtKey == 'M') {
pnmdtk->st.wHour=6;
}
if(pnmdtk->nVirtKey == 'T') {
pnmdtk->st.wHour=15;
}
if(pnmdtk->nVirtKey == 'N') {
pnmdtk->st.wHour=23;
}
}
return 0;
...
void CalculaTamanoTexto(HWND hctrl, char* szcad, SIZE*
lpsize) {
HDC hdc;
hdc = GetDC(hctrl);
GetTextExtentPoint32(hdc, szcad, strlen(szcad), lpsize);
ReleaseDC(hctrl, hdc);
}
Cadenas de usuario
case DTN_USERSTRING:
pnmdts = (NMDATETIMESTRING*)lParam;
if(!strcmp(pnmdts->pszUserString, "hoy"))
GetLocalTime(&(pnmdts->st));
else if(!strcmp(pnmdts->pszUserString,
"mañana")) {
GetLocalTime(&(pnmdts->st));
pnmdts->st.wDay++;
} else if(!strcmp(pnmdts->pszUserString,
"ayer")) {
GetLocalTime(&(pnmdts->st));
pnmdts->st.wDay--;
}
return 0;
Ejemplo 96
Ejemplo 97
Capítulo 56 Control de calendario
El control de calendario
mensual permite elegir fechas
mediante la selección desde
un calendario que muestra uno
o más meses. Estos controles
proporcionan una forma
sencilla de seleccionar fechas
o intervalos de fechas.
Como en todos los
controles comunes que Ejemplo de control calendario mensual
estamos viendo, hay que
asegurarse de que la DLL ha sido cargada invocando a la función InitCommonControlsEx indicando el
valor de bandera ICC_DATE_CLASSES en el miembro dwICC de la estructura
INITCOMMONCONTROLSEX que pasaremos como parámetro.
INITCOMMONCONTROLSEX iCCE;
...
iCCE.dwSize = sizeof(INITCOMMONCONTROLSEX);
iCCE.dwICC = ICC_DATE_CLASSES;
InitCommonControlsEx(&iCCE);
case WM_CREATE:
hInstance = ((LPCREATESTRUCT)lParam)->hInstance;
CreateWindowEx(0, MONTHCAL_CLASS, NULL,
WS_BORDER | WS_CHILD | WS_VISIBLE | WS_TABSTOP,
10,10,200,200,
hwnd, (HMENU)ID_CALENDAR1,
hInstance, NULL);
break;
Un control de calendario mensual puede mostrar uno o más meses. El número de meses que se
mostrarán dependerá de las dimensiones del control, tanto en anchura como en altura.
En la imagen del ejemplo se muestran tres meses en sentido horizontal, pero si las dimensiones del
control lo permiten se podrían mostrar en vertical o en una cuadrícula con varias columnas y filas.
Disponemos de dos mensajes para ayudarnos a calcular las dimensiones del control:
En el primer caso, los miembros left y top serán cero, y en right y bottom se devolverá la anchura y
altura del control, respectivamente.
En el segundo caso la anchura disponible se calcula como right-left y la altura como bottom-top, y al
retornar, left y top serán cero, y right y bottom serán la anchura y altura, respecitivamente.
El mensaje MCM_SIZERECTTOMIN sólo funciona si están activos los estilos visuales, usando el
manifiesto adecuado.
En ambos casos deberemos usar la función MoveWindow para redimensionar el control.
Ajustar el tamaño del control para mostrar un mes:
Estilos
Disponemos de algunos estilos específicos para estos controles que permiten personalizar su
aspecto y comportamiento.
MCS_DAYSTATE: permite mostrar algunas fechas en negrita, para ello el control envíará códigos
de notificación MCN_GETDAYSTATE para solicitar a la aplicación la información necesaria.
MCS_MULTISELECT: permite que el usuario pueda seleccionar un rango de fechas.
MCS_WEEKNUMBERS: muestra a la izquierda de cada fila de cada calendario el número de la
semana dentro del año.
MCS_NOTODAYCIRCLE: oculta el resaltado del día actual en el calendario correspondiente.
MCS_NOTODAY: oculta la leyenda al pié del calendario con la fecha del día actual. Esta leyenda
actúa, cuando es visible, como un botón que selecciona la fecha actual en el control.
MCS_NOTRAILINGDATES: oculta las fechas de los primeros días de la primera semana
correspondientes al mes anterior al primero mostrado y las de los últimos días de la última semana
correspondientes a la mes siguiente al último mostrado.
MCS_SHORTDAYSOFWEEK: las leyendas para los días de la semana se muestran con una letra
(L, M, X, etc) en lugar de usar tres caracteres (lu., ma., mi., etc).
MCS_NOSELCHANGEONNAV: si el usuario ha seleccionado alguna fecha o un rango de fechas,
la selección no cambia si al navegar a otros meses desaparece del rango de meses mostrado en el
control. De este modo el usuario puede seleccionar más fechas de las que son visibles en el
control.
Seleccionar fecha
SYSTEMTIME fecha;
...
fecha.wDay = 23;
fecha.wMonth = 3;
fecha.wYear = 2023;
SendDlgItemMessage(hwnd, ID_CALENDAR1, MCM_SETCURSEL, 0, (LPARAM)&fecha);
SYSTEMTIME fecha;
...
SendDlgItemMessage(hwnd, ID_CALENDAR1, MCM_GETCURSEL, 0, (LPARAM)&fecha);
Selección múltiple
Si se especifica el estilo MCS_MULTISELECT, el usuario podrá seleccionar un rango de fechas. Si
no se modifica explícitamente, el rango máximo es una semana.
Si se selecciona más de una fecha, deben ser consecutivas. La selección se hace pulsando con el
botón izquierdo sobre una fecha y, manteniendo pulsado el botón, moverse a otra fecha. También se
puede seleccionar una fecha y, manteniendo pulsada la tecla SHIFT, seleccionar una segunda fecha.
Si entre la primera y la segunda fecha seleccionadas hay más días que el máximo rango permitido,
sólo se seleccionará el número máximo de días a partir de la primera selección.
SYSTEMTIME fecha[2];
...
fecha[0].wDay = 20;
fecha[0].wMonth = 8;
fecha[0].wYear = 2048;
fecha[1].wDay = 15;
fecha[1].wMonth = 8;
fecha[1].wYear = 2048;
SendDlgItemMessage(hwnd, ID_CALENDAR1, MCM_SETSELRANGE, 0, (LPARAM)&fecha);
Para modificar el rango máximo de fechas que se pueden seleccionar en un control de calendario
usaremos el mensaje MCM_SETMAXSELCOUNT, indicando en wParam el nuevo valor máximo del
rango.
Por defecto, si el ususario navega a través del calendario y la selección actual queda fuera de la
vista, automáticamente se seleccionará un nuevo rango del mismo tamaño en la vista actual. Esto
también se aplica a controles de calendario de selección simple.
Podemos evitar este comportamiento asignado el estilo MCS_NOSELCHANGEONNAV. De este
modo la selección actual se mantiene aunque no sea visible en el control.
Esta es la única forma de que un usuario pueda seleccionar rangos más grandes de los que permite
mostrar el control.
Fechas seleccionables
Si queremos limitar el rango de fechas que el usuario puede seleccionar disponemos del mensaje
MCM_SETRANGE. En wParam indicaremos una combinación de los valores GDTR_MAX y
GDTR_MIN, dependiendo de que límites queramos establecer, y en lParam pasaremos un puntero a un
array de dos elementos con las fechas mínima y máxima permitidas.
Si sólo queremos establecer el límite superior usaremos el valor GDTR_MAX en wParam, y si sólo
queremos establecer un límit inferior, el valor GDTR_MIN. En los dos casos hay que pasar el array con
dos elementos, pero sólo se tendrán en cuenta los valores en función del valor de wParam.
SYSTEMTIME fecha[2];
...
/* Sólo se permiten fechas entre el 1 de enero y el 31 de diciembre de 2022 */
fecha[0].wDay = 1;
fecha[0].wMonth = 1;
fecha[0].wYear = 2022;
fecha[1].wDay = 31;
fecha[1].wMonth = 12;
fecha[1].wYear = 2022;
SendDlgItemMessage(hwnd, ID_CALENDAR1, MCM_SETRANGE, (WPARAM)(GDTR_MAX|GDTR_MIN),
(LPARAM)&fecha);
Aspecto gráfico
Disponemos de varias opciones para modificar el aspecto en pantalla de los controles de
calendario, tamaños, colores, fuentes, etc.
Borde
Para recuperar el valor actual del tamaño del borde se usa el mensaje
MCM_GETCALENDARBORDER, o la macro equivalente MonthCal_GetCalendarBorder.
Colores
Podemos modificar los colores de un control de calendario, pero sólo si no están activos los estilos
visuales. Para ello disponemos del mensaje MCM_SETCOLOR.
En wParam indicaremos de qué parte del control queremos cambiar el color, y en lParam el color
que vamos a asignar, en formato COLORREF.
Para indicar qué parte del control queremos modificar exiten varias constantes:
Para recuperar el color actualmente asignado a una de estas zonas se puede usar el mensaje
MCM_GETCOLOR, indicando en wParam la constante correspondiente a la zona cuyo color queremos
recuperar.
Alternativamente, también se pueden usar las macros MonthCal_SetColor y MonthCal_GetColor,
respectivamente.
Estado de días
Si el estilo MCS_DAYSTATE está activo, podremos especificar un estado resaltado para cada día
de los meses mostrados en el control, que se indicará mostrando el texto del día correspondiente en
negrita.
Como cada mes tiene como máximo 31 días, y el estado de resalte es un valor binario, se usa un
valor de 32 bits para especificar el estado de todos los días de un mes. Más concretamente, se usa un
valor de tipo MONTHDAYSTATE. Los bits con valor 1 indicarán que el día correspondiente se deberá
mostrar resaltado.
Por otra parte, como un control de calendario puede mostrar varios meses, tendremos que
especificar tantas de estas estructuras como meses contenga el control.
Así, usando el mensaje MCM_SETDAYSTATE podemos establecer qué días se mostrarán
resaltados en un control de calendario, indicando en lParam la dirección de un array de elementos de
tipo MONTHDAYSTATE, uno por cada mes a asignar, y en wParam el número de elementos que
contiene el array.
También podemo usar la macro MonthCal_SetDayState.
Evidentemente, cada vez que los meses mostrados en el control cambien deberemos asignar los
nuevos estados a los meses mostrados.
Pero todo esto es mucho más sencillo si procesamoe el código de notificación
MCN_GETDAYSTATE.
Nota:
Hay un error en el fichero de cabecera "commctrl.h" que se incluye con MinGW en la definición
de este código de notificación.
Donde pone:
#define MCN_GETDAYSTATE (MNN_FIRST+3)
Debe poner:
#define MCN_GETDAYSTATE (MCN_FIRST-1)
Siempre que tengamos asignado el estilo MCS_DAYSTATE, cada vez que cambien los meses
mostrados en el control recibiremos un código de notificación MCN_GETDAYSTATE. En lParam
tendremos un puntero a una estructura NMDAYSTATE con toda la información necesaria para
actualizar el estado de los meses mostrados en el control.
Como toda las estructuras recibidas en códigos de notificación, esta también contiene en primer
lugar una estructura NMHDR con la información relativa a la notificación (manipulador de ventana,
identificador y código de notificación). Además contiene una estructura SYSTEMTIME, con la fecha de
comienzo del rango requerido, un puntero a un array de valores MONTHDAYSTATE, que deberemos
asignar al procesar el mensaje y un valor entero, con el número de elementos que debe contener ese
array.
Por ejemplo, el control de la imagen anterior está mostrando dos meses, enero y febrero de 2022.
Enero empieza en sábado, lo que implica que deberán mostrarse los últimos días del mes anterior de
esa semana. Esto es así aunque hayamos asignado el estilo MCS_NOTRAILINGDATES, que oculta
esos días.
El segundo mes termina en lunes, lo que implica que deberán mostrarse los primeros días del mes
siguiente. Por lo tanto el array deberá contener cuatro elementos y la fecha que se solicitará será la del
día uno del mes anterior al primero, es decir, el 27 de diciembre de 2021 (el último lunes de diciembre
de 2021).
/* En este ejemplo supondremos que sólo se pueden seleccionar fechas del año 2022 */
/* El array 'estados' contiene los estados resaltados para los sábados y domingos desde
diciembre de 2021 a enero de 2023 */
MONTHDAYSTATE estados[] =
{0x3060c18,
0x3060c183, 0x60c1830, 0x60c1830, 0x20c18306, 0x183060c1, 0x3060c18,
0x60c18306, 0xc183060, 0x183060c, 0x3060c183, 0x60c1830, 0x4183060c,
0x183060c1};
LPNMDAYSTATE lpnmDS;
...
case WM_NOTIFY:
lpnmDS = (LPNMDAYSTATE)lParam;
switch(lpnmDS->nmhdr.code) {
case MCN_GETDAYSTATE:
if(lpnmDS->stStart.wYear == 2021)
lpnmDS->prgDayState = &estados[0];
else
lpnmDS->prgDayState = &estados[lpnmDS->stStart.wMonth];
return 1;
Lo cierto es que de la fecha recibida sólo nos interesa el mes y el año, el resto de los datos son
irrelevantes.
El día será el correspondiente al primer día de la semana del primer mes, aunque ese día
corresponda al mes anterior. El valor de wDayOfWeek será válido y nos puede servir para generar los
valores de estados automáticamente.
Los bits en la estructura MONTHDAYSTATE se empiezan a contar a partir de la derecha, el bit
menos significativo corresponde al primer día del mes.
Las operaciones de rotación de bits son sencillas, para rotar un bit a la izquerda el valor contenido
en un entero basta multiplicar por dos, o podemos usar directamente el operador de rotación de bits.
Por ejemplo, si en el ejemplo anterior queremos resaltar el 6 de enero de 2022 podemos usar esta
sentencia:
Calendarios contenidos
Disponemos de un mensaje para obtener información sobre el número de calendarios. El mensaje
MCM_GETCALENDARCOUNT nos devuelve el número de calendarios actualmente mostrados en el
control, no es necesario indicar ningún parámetro.
La macro MonthCal_GetCalendarCount es equivalente.
Obtener información
Por otra parte, el mensaje MCM_GETCALENDARGRIDINFO sirve para obtener información sobre
cada una de las zonas que definen un calendario. En lParam pasaremos un puntero a una estructura
MCGRIDINFO en la que se nos devolverá la información requerida.
Antes de enviar el mensaje deberemos iniciar algunos miembros de la estructura. cbSize debe
contener el tamaño de la estructura:
mcGI.cbSize = sizeof(MCGRIDINFO);
El miembro dwPart debe contener el valor de la constante que indica qué información en concreto
queremos obtener. Puede ser uno de los siguientes valores:
También deberemos indicar un valor para el miembro dwFlags, dependiendo de qué información
queremos que nos sea retornada. Puede ser una combinación de uno o varios de los valores
siguientes:
Para determinar de qué parte del calendario estamos solicitando la información, habrá que asignar
otros campos como iCalendar, iRow, iCol, bSelected o pszName, dependiendo de cada caso.
En el caso de que se solicite información en forma de cadenas, en pszName asignaremos la
dirección de un buffer, y el cchName el tamaño de ese buffer.
Al retornar, los miembros bSelected, stStart, stEnd, rc y la cadena apuntada por pszName
contendrán la información solicitada, dependiendo en cada caso de los valores de entrada.
MCGRIDINFO mcGI;
WCHAR cad[100];
...
mcGI.cbSize = sizeof(MCGRIDINFO);
mcGI.dwPart = MCGIP_CALENDAR;
mcGI.iCalendar = 0;
mcGI.pszName = cad;
mcGI.cchName = 100;
mcGI.dwFlags = MCGIF_NAME;
SendDlgItemMessage(hwnd, ID_CALENDAR1, MCM_GETCALENDARGRIDINFO, 0, (LPARAM)&mcGI);
/* En mcGI.pszName estará la cadena con la fecha actualmente seleccionada, por
ejemplo
5 de enero de 2022 */
SYSTEMTIME fecha[2];
...
SendDlgItemMessage(hwnd, ID_CALENDAR1, MCM_GETMONTHRANGE, (WPARAM)GMR_VISIBLE,
(LPARAM)fecha);
Podemos obtener la medida de la anchura máxima para la cadena 'hoy', mostrada al pié del control
si no se ha especifciado el estilo MCS_NOTODAY, usando el mensaje MCM_GETMAXTODAYWIDTH,
sin parámetros.
INT x;
...
x = SendDlgItemMessage(hwnd, ID_CALENDAR1, MCM_GETMAXTODAYWIDTH, 0, 0);
Tipos de calendario
Exiten varios tipos de calendarios, además del calendario gregoriano que usamos generalmente en
occidente. En otras partes del mundo se usan calendario diferentes: Japón, Taiwan, Corea, etc.
El control de calendario dispone de un identificador que determina qué tipo de calendario se está
usando. Podemos asignar un nuevo identificador mediante el mensaje MCM_SETCALID, indicando en
wParam el valor del nuevo identificador. Este valor puede ser uno de los valores definidos para CALID.
Para obtener el identificador de un control de calendario se usa el mensaje MCM_GETCALID.
También se pueden usar las macros MonthCal_SetCALID y MonthCal_GetCALID, respectivamente.
Nota:
He intentado hacer algún ejemplo de cambio de ID, pero aparentemente no tiene ningún
efecto en la apariencia del control.
Otra opción de la que disponemos es elegir qué día empieza cada semana. En mi configuración de
Windows las semanas empiezan en lunes, como se vé en las imágenes de ejemplo, pero esto se
puede modificar usando el mensaje MCM_SETFIRSTDAYOFWEEK, indicando en lParam qué día de la
semana será el primero, empezando en 0 para el lunes.
Si no se modifica, el valor por defecto es LOCALE_IFIRSTDAYOFWEEK, que depende de la
configuración regional.
/* El primer día de la semana es el domingo */
SendDlgItemMessage(hwnd, ID_CALENDAR1, MCM_SETFIRSTDAYOFWEEK, 0, (LPARAM)6);
El valor de retorno es un DWORD, donde la palabra de mayor peso será TRUE si el valor previo no
era LOCALE_IFIRSTDAYOFWEEK, y la palabra de menor peso contendrá el valor previo para el primer
día de la semana.
La macro MonthCal_SetFirstDayOfWeek equivale a ese mensaje.
Para obtener el valor actual para el primer día de la semana de un control de calendario se usa el
mensaje MCM_GETFIRSTDAYOFWEEK, sin parámetros o la macro equivalente
MonthCal_GetFirstDayOfWeek.
Navegacion
Los controles de calendario disponen de dos pequeños botones con iconos en forma triangular que
apuntan a la izquierda y derecha, y que permiten navegar a través de los meses hacia atrás y hacia
delante. Por defecto, cada vez que se pulsa uno de esos botones el control retrocede o avanza un mes
(o dependiendo de la vista, un año, una década o un siglo).
Podemos modifiar ese comportamiento usando el mensaje MCM_SETMONTHDELTA que
modificará el valor del salto. Por ejemplo, si nuesto control muestra tres meses, podemo hace que con
cada desplazamiento se muestren tres meses anteriores o posteriores asignando el valor 3 al delta.
Hoy
Si no se ha especificado el estilo MCS_NOTODAY, el control mostrará una línea que si es pulsada
por el usuario seleccionará la fecha actual, actualizando el control para que esa fecha sea visible, si es
necesario.
Por defecto, la fecha para 'hoy' es la fecha local actual, pero podemos modificar esa fecha usando
el mensaje MCM_SETTODAY, indicando en lParam el nuevo valor para la fecha de 'hoy' en una
estructura SYSTEMTIME.
SYSTEMTIME hoy;
...
/* Hasta nueva orden, hoy siempre será 15 de marzo de 2022 */
hoy.wDay = 15;
hoy.wMonth = 3;
hoy.wYear = 2022;
SendDlgItemMessage(hwnd, ID_CALENDAR1, MCM_SETTODAY, 0, (LPARAM)hoy);
Vistas
Además de la vista por defecto, que muestra los días de cada mes y que es la que permite
seleccionar una fecha concreta, existen otras vistas que nos permiten hacer una navagación más
rápida.
El miembro pt, de tipo POINT contendrá las coordenadas del punto a probar.
MCHITTESTINFO mcTI;
...
mcTI.cbSize = sizeof(MCHITTESTINFO);
mcTI.pt.x = 14;
mcTI.pt.y = 43;
SendDlgItemMessage(hwnd, ID_CALENDAR1, MCM_HITTEST, 0, (LPARAM)&mcTI);
uHit: será una constante que indica la zona concreta en la que está el punto. (Ver
MCHITTESTINFO para una lista de los posibles valores).
st: es una estructura SYSTEMTIME que devolverá la fecha correspondiente a la zona donde está
el punto, si a esa zona le corresponde una fecha.
rc: es una estructura RECT que devuelve el área de la zona a la que pertenece el punto.
iOffset: cuando hay más de un calendario en el control, indica el desplazamiento de aquel al que
pertenece el punto.
iRow e iCol: fila y columna concreta en la que está el punto, si está en una de las casillas.
Todos los desplazamientos, iOffset, iRow e iCol empiezan a contar desde cero.
La macro MonthCal_HitTest es equivalente.
Notificaciones
Veremos ahora otros códigos de notificación que puede enviar el control de calendario a través de
un mensaje MM_NOTIFY.
Cambio de selección
Cada vez que el usuario cambie la selección de la fecha o la selección cambie automáticamente al
navegar por el control o como consecuencia de un mensaje MCM_SETCURSEL o
MCM_SETSELRANGE, se generará un código de notificación MCN_SELCHANGE.
En lParam recibiremos un puntero a una estructura NMSELCHANGE, en el miembro nmhdr, que es
una estructura NMHDR con información sobre la notificación: manipulador de ventana del control, su
identificador y el código de notificación.
También recibiremos dos miembros stSelStart y stSelEndde tipo SYSTEMTIME con las fechas de
inicio y final seleccionadas.
Selección
Cambio de vista
Operaciones de arrastre
Ejemplo 98
Capítulo 57 Control Trackbar
podemos considerar el
control de trackbar como una
variante del control scrollbar.
Su funcionamiento es muy
parecido, aunque en general
se orientan a otro tipo de entradas de datos por parte del usuario,
como asignar posiciones; también sirven para indicar el estado de
progreso de una tarea o proceso.
El ejemplo más claro es en la reproducción de archivos
multimedia, ya sean de sonido o de video. Este tipo de controles nos
indica el punto en que se encuentra la reproducción, pero también
permite elegir el punto en que queremos continuar la reproducción,
avanzar o retroceder ciertos valores, etc.
Como en todos los controles comunes que estamos viendo, hay
que asegurarse de que la DLL ha sido cargada invocando a la
función InitCommonControlsEx indicando el valor de bandera
ICC_BAR_CLASSES en el miembro dwICC de la estructura
INITCOMMONCONTROLSEX que pasaremos como parámetro.
INITCOMMONCONTROLSEX iCCE;
...
iCCE.dwSize = sizeof(INITCOMMONCONTROLSEX);
iCCE.dwICC = ICC_BAR_CLASSES;
InitCommonControlsEx(&iCCE);
Hay varios estilos específicos para este tipo de control, pero los
más básicos son los que afectan a la orientación: horizontal
(TBS_HORZ) o vertical (TBS_VERT), aunque si no se especifica
ninguno, el valor por defecto es horizontal.
En el parámetro hMenu, como siempre, indicaremos el
identificador del control.
En principio, no sería necesario modificar la fuente, ya que este
control no muestra ningún texto. (Ya veremos si esto es así). En
cualquier caso, si se usa una fuente hay que recordar liberar el
recurso antes de terminar el programa, usando DeleteObject.
Partes de un trackbar
DWORD pos;
...
pos = SendDlgItemMessage(hwnd, ID_TRACKBAR, TBM_GETPOS,
0, 0);
Estilos
Podemos agrupar los estilos propios de los controles trackbar en
varias categorías:
Colocar marcas
Salida:
5
0 - 25
1 - 50
2 - 75
3 - -1
4 - -1
5
0 - 1
1 - 2
2 - 3
3 - 4
4 - 5
DWORD* pos;
...
SendDlgItemMessage(hwnd, ID_TRACKBAR, TBM_SETTICFREQ,
(WPARAM)25, 0);
printf("%d\n", SendDlgItemMessage(hwnd, ID_TRACKBAR,
TBM_GETNUMTICS, 0, 0));
pos = SendDlgItemMessage(hwnd, ID_TRACKBAR,
TBM_GETPTICS, 0, 0);
for(i=0; i < SendDlgItemMessage(hwnd, ID_TRACKBAR,
TBM_GETNUMTICS, 0, 0); i++)
printf("%d - %d\n", i, pos[i]);
Dará la salida:
5
0 - 1
1 - 2
2 - 3
3 - 4
4 - 5
Eliminar marcas
Posiciones de marcas
80
150
219
-1
-1
Da la salida:
14
17
19
22
25
28
Tooltips
Si se usa el estilo TBS_TOOLTIPS, el control trackbar soportará
tooltips. Por defecto, se crea un control tooltip que muestra la
posición actual del deslizador.
Con el mensaje TBM_SETTIPSIDE podemos elegir la posición
en la que se mostrará el tooltip, indicándolo en wParam. Para los
horizontales podemos elegir encima o debajo, TBTS_TOP o
TBTS_BOTTOM, y para los verticales izquierda o derecha,
TBTS_LEFT o TBTS_RIGHT.
hToolNew = CreateWindowEx(WS_EX_TOOLWINDOW,
TOOLTIPS_CLASS, NULL,
WS_POPUP | TTS_ALWAYSTIP,
CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,
CW_USEDEFAULT,
hwnd, NULL, hInstance, NULL);
if(hToolNew) {
SetWindowPos(hToolNew, HWND_TOPMOST, 0, 0, 0, 0,
SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE);
SendMessage(hToolNew, TTM_SETMAXTIPWIDTH, 0, 150);
toolInfo.cbSize = sizeof(toolInfo);
toolInfo.hwnd = hCtrl;;
toolInfo.hinst = hInstance;
toolInfo.uFlags = TTF_IDISHWND | TTF_SUBCLASS;
toolInfo.uId = (UINT_PTR)GetDlgItem(hwnd,
ID_TRACKBAR);
toolInfo.lpszText = LPSTR_TEXTCALLBACK;
SendMessage(hToolNew, TTM_ADDTOOL, 0,
(LPARAM)&toolInfo);
case WM_NOTIFY:
pnmhdr = (LPNMHDR)lParam;
switch(pnmhdr->code) {
case TTN_GETDISPINFO:
pnmttdispinfo = (LPNMTTDISPINFO)pnmhdr;
if(GetDlgCtrlID((HWND)pnmttdispinfo->hdr.idFrom)
== ID_TRACKBAR) {
sprintf(cad, "Valor: %d", SendMessage(hCtrl,
TBM_GETPOS, 0, 0));
strcpy(pnmttdispinfo->lpszText, cad);
}
break;
}
break
Otros estilos
Hay otros estilos menos útiles, por ejemplo TBS_REVERSED se
usa para trackbars "invertidas", donde un número más pequeño
indica "mayor" y un número más grande indica "menor." Esto no
afecta al control para nada, es sólo una etiqueta que puede ser
consultada para determinar si un trackbar es normal o inverso.
Si no se indica el estilo
TBS_DOWNISLEFT, en un
control trackbar la tecla de
dirección 'abajo' equivale a
'derecha' y 'arriba' a Ejemplo WM_PRINTCLIENT
'izquierda'. Utilizar este estilo
invierte el comportamiento por defecto de modo que 'abajo' equivale
a 'izquierda' y 'arriba' a 'derecha'.
Con el estilo TBS_TRANSPARENTBKGND el responsable de
pintar el fondo es la ventana a través de un mensaje
WM_PRINTCLIENT.
static HBITMAP hBitmapRes;
static HWND hCtrl;
POINT punto;
RECT wre;
HDC memDC;
...
case WM_CREATE:
hCtrl = CreateWindowEx(0, TRACKBAR_CLASS, NULL,
WS_CHILD | WS_VISIBLE | WS_TABSTOP |
TBS_TOOLTIPS | TBS_TRANSPARENTBKGND,
10,10,300,50,
hwnd, (HMENU)ID_TRACKBAR,
hInstance, NULL);
hBitmapRes = LoadBitmap(hInstance, "rojo");
...
case WM_PRINTCLIENT:
// Obtener posición del control en coordenadas
de pantalla:
GetWindowRect(hCtrl, &wre);
// Calcular tamaño:
wre.right -= wre.left;
wre.bottom -= wre.top;
// Convertir posición a coordenadas de cliente:
punto.x = wre.left;
punto.y = wre.top;
ScreenToClient(hwnd, &punto);
// Mostrar mapa de bits:
hdc = (HDC)wParam;
memDC = CreateCompatibleDC(hdc);
SelectObject(memDC, hBitmapRes);
BitBlt(hdc, punto.x, punto.y, wre.right,
wre.bottom, memDC, 0, 0, SRCCOPY);
DeleteDC(memDC);
break;
...
case WM_DESTROY:
DeleteObject(hBitmapRes);
Nota:
Ventanas compañeras
Se pueden
añadir dos
ventanas
compañeras al
control Ventanas compañeras
trackbar.
Generalmente serán controles estáticos, pero en realidad puede ser
cualquier tipo de ventana.
Para insertar una ventana compañera se usa el mensaje
TBM_SETBUDDY. En wParam indicaremos un valor TRUE para
insertar la ventana a la izquierda, si es un trackbar horizontal, o
encima, si se trata de un trackbar vertical, o un valor FALSE para
insertarla a la derecha o debajo, respectivamente.
En este ejemplo insertamos un botón como ventana compañera
a la izquierda, y un texto estático a la derecha. Como siempre que
insertamos controles en ejecución, si queremos modificar las
fuentes deberemos enviar un mensaje WM_SETFONT.
HWND hctrl;
HFONT hFont;
...
Delimitadores de áreas
Disponemos de un par de mensajes para obtener las áreas
delimitadoras de ciertas partes de los controles trackbar.
El mensaje TBM_GETCHANNELRECT recupera un puntero con
el rectángulo delimitador del canal de control en lParam.
El mensaje TBM_GETTHUMBRECT recupera un puntero con el
rectángulo delimitador del deslizador en lParam.
Formato de caracteres
Se puede modificar durante la ejecución el juego de caracteres
usado por un control trackbar mediante el mensaje
TBM_SETUNICODEFORMAT, indicando en wParam un valor
distinto de cero para usar caractere Unicode, o cero para usar
caracteres ANSI.
Para recuperar el juego de caracteres Unicode usado por un
control trackbar se usa el mensaje TBM_GETUNICODEFORMAT.
Ignoro en qué influye usar uno u otro juego de caracteres en un
control que no muestra texto, pero las opciones existen.
Notificaciones
Si el control tiene el estilo TBS_NOTIFYBEFOREMOVE se
enviará al procedimiento de ventana de la ventana padre un código
de notificación TRBN_THUMBPOSCHANGING cada vez que el
usuario modifique la posición de deslizador, y antes de que el control
se actualice. Esto permite a la aplicación evitar que los cambios de
posición se consoliden según nos convenga.
En lParam recibiremos un puntero a una estructura
NMTRBTHUMBPOSCHANGING con la información necesaria sobre
la nueva posición del deslizador, en el miembro dwPos y el modo en
que se ha modificado en el miembro nReason.
El miembro nReason puede ser uno de los siguientes valores,
dependiendo de cómo se haya modificado la posición del deslizador:
LPNMHDR pnmhdr;
NMTRBTHUMBPOSCHANGING* pnmtpc;
char cad[100];
char* szReason[] = {
"TB_LINEUP",
"TB_LINEDOWN",
"TB_PAGEUP",
"TB_PAGEDOWN",
"TB_THUMBPOSITION",
"TB_THUMBTRACK",
"TB_TOP",
"TB_BOTTOM",
"TB_ENDTRACK"
};
...
case WM_CREATE:
CreateWindowEx(0, "STATIC", "", WS_CHILD |
WS_VISIBLE | SS_SIMPLE, 10,70,220,20,
hwnd, (HMENU)(ET_TRACKBAR), hInstance, NULL);
...
case WM_NOTIFY:
pnmhdr = (LPNMHDR)lParam;
switch(pnmhdr->code) {
case TRBN_THUMBPOSCHANGING:
pnmtpc = (NMTRBTHUMBPOSCHANGING*)pnmhdr;
sprintf(cad, "%100s", " ");
SetWindowText(GetDlgItem(hwnd, ET_TRACKBAR),
cad);
sprintf(cad, "%-25s -> %8d\n",
szReason[pnmtpc->nReason], pnmtpc->dwPos);
SetWindowText(GetDlgItem(hwnd, ET_TRACKBAR),
cad);
break;
Nota:
Ejemplo 99
Glosario
Agruparemos en ésta página las palabras y siglas que se usan a
menudo cuando se programa en Windows:
Callback (retrollamada).
Las funciones callback son funciones creadas por el
programador. Windows utiliza estas funciones para que el
programador pueda personalizar la respuesta a ciertos eventos o
funciones del API. Es decir, son funciones de usuario que serán
llamadas por el sistema.
Manipulador (Handle)
En general se trata de números enteros que facilitan la
manipulación de objetos Windows en llamadas a funciones.
Windows puede obtener o modificar los datos del objeto a través de
su manipulador, y resulta mucho más útil trabajar con números
enteros que con punteros o estructuras.
Owner-draw
Entre los términos de difícil traducción habituales en el API, uno
de los más frecuentes, (y que no hemos querido ni intentado
traducir) es el de owner-draw.
Literalmente significa "dibujado por el dueño". Se trata de un
estilo que se aplica a los controles de Windows: controles edit, list
box, botones, etc, que inhibe el tratamiento automático del control,
en lo que se refiere a su representación gráfica, y le deja esa
responsabilidad a la ventana padre o propietaria del control.
Esto permite personalizar el aspecto gráfico del control, por una
parte, y el comportamiento general, en muchos casos.
OWL y MFC
También existen bibliotecas de clases para programar en
Windows, las más conocidas son OWL (Object Windows Library) de
Borland y MFC (Microsoft Foundation Class) de Microsoft. Aquí no
las usaremos, pero para algunos elementos de uso frecuente
probablemente diseñaremos nuestras propias clases, y
probablemente construyamos una biblioteca con ellas.