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).
El Foco del teclado
Los mensajes del teclado se envían al proceso de primer plano que haya creado la ventana que actualmente tiene el foco del teclado. El teclado se comparte entre todas las ventanas abiertas, pero sólo una de ellas puede poseer el foco del teclado, los mensajes del teclado llegan a esa ventana a través del bucle de mensajes del proceso que las creó.
Sólo una ventana, o ninguna, puede poseer el foco del teclado, para averiguar cual es la que lo posee podemos usar la función GetFocus, siempre que esa ventana pertenezca al proceso actual. Para asignar el foco a la ventana que queramos se usa la función SetFocus. Ya hemos usado esta función anteriormente, para cambiar el control que recibe la entrada del usuario al iniciar un cuadro de diálogo.
Cuando una ventana pierde el foco, recibe un mensaje WM_KILLFOCUS y a la ventana que lo recibe, se le envía un mensaje WM_SETFOCUS. En ocasiones se puede usar el mensaie WM_KILLFOCUS para realizar la validación de los datos de un control, o cualquier tarea, antes de perder definitivamente la atención del usuario. Del mismo modo, el mensaje WM_SETFOCUS se puede usar para preparar una ventana o control antes de que el usuario pueda modificar los datos que contiene. Normalmente, si una ventana acepta entradas desde el teclado, sabremos si tiene el foco porque se muestra un caret en su interior.
Una propiedad relacionada con el foco del teclado es el de ventana activa. La ventana activa es con la que el usuario está trabajando. La ventana con el foco del teclado es, o bien la ventana activa, o bien una de sus ventanas hija. La ventana activa se distingue porque su barra de título cambia de color, y porque suele tener el foco del teclado y del ratón (aunque esto no siempre es cierto).
El usuario puede cambiar de ventana activa, usando el teclado, haciendo clic sobre ella, etc. También es posible hacerlo usando la función SetActiveWindow y con GetActiveWindow, un proceso puede obtener el manipulador de la ventana activa asociada, si es que existe. Cada vez que una ventana deja de ser la activa, recibe un mensaje WM_ACTIVATE, y después se envía el mismo mensaje a la que pasa a ser activa.
case WM_ACTIVATE: if((HWND)lParam == NULL) strcpy(nombre, "NULL"); else GetWindowText((HWND)lParam, nombre, 128); if(LOWORD(wParam) == WA_INACTIVE) sprintf(cad, "Ventana desactivada hacia %s", nombre); else sprintf(cad, "Ventana activada desde %s", nombre); break; case WM_SETFOCUS: if((HWND)wParam == NULL) strcpy(nombre, "NULL"); else GetWindowText((HWND)wParam, nombre, 128); sprintf(cad, "Pérdida de foco en favor de %s", nombre); break; case WM_SETFOCUS: if((HWND)wParam == NULL) strcpy(nombre, "NULL"); else GetWindowText((HWND)wParam, nombre, 128); sprintf(cad, "Foco recuperado desde %s", nombre); break;
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.
static BOOL cambio; ... cambio = FALSE; EnableWindow(GetDlgItem(hDlg, ID_CONTROL1), !cambio); EnableWindow(GetDlgItem(hDlg, ID_CONTROL2), cambio); SetFocus(GetDlgItem(hDlg, ID_CONTROL1));
Ejemplo 36
Nombre | Fichero | Fecha | Tamaño | Contador | Descarga |
---|---|---|---|---|---|
Ejemplo 36 | win036.zip | 2004-07-11 | 3892 bytes | 728 |
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:
char texto[128], cad[64]; ... case WM_KEYDOWN: GetKeyNameText(lParam, cad, 64); sprintf(texto, "Tecla %s (%d) pulsada", cad, (int)wParam); break;
El bucle de mensajes
Es el momento de comentar algo sobre el bucle de mensajes que estamos usando desde el principio de este texto:
while(TRUE == GetMessage(&mensaje, NULL, 0, 0)) { /* Traducir mensajes de teclas virtuales a mensajes de caracteres */ TranslateMessage(&mensaje); /* Enviar mensaje al procedimiento de ventana */ DispatchMessage(&mensaje); }
Me refiero a la función TranslateMessage, que como dice el comentario, traduce los mensajes de pulsaciones de teclas a mensajes de carácter. Si nuestra aplicación debe procesar los mensajes de pulsaciones de teclas no debería llamar a esta función en el bucle de mensajes. De todos modos, los mensajes de pulsación de teclas parecen llegar en los dos casos, pero no es mala idea seguir la recomendación del API en este caso.
Ejemplo 37
Nombre | Fichero | Fecha | Tamaño | Contador | Descarga |
---|---|---|---|---|---|
Ejemplo 37 | win037.zip | 2004-07-11 | 2987 bytes | 717 |
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
Veamos qué es este curioso concepto de tecla muerta. Las teclas muertas son aquellas que no generan un carácter por sí mismas, y necesitan combinarse con otras para formarlos. Por ejemplo, la tecla del acento (´), cuando se pulsa, no crea un carácter, es necesario pulsar otra tecla después para que eso ocurra. Si la tecla que se pulsa en segundo lugar genera un carácter que se puede combinar con la tecla muerta, se generará un único carácter, por ejemplo 'á'. Si no es así, se generan dos caracteres, el primero combinando la tecla muerta con un espacio, y el segundo con el carácter, por ejemplo "´b".
Cuando se pulse una tecla muerta, el mensaje que se genera por TranslateMessage puede ser WM_DEADCHAR o WM_SYSDEADCHAR, pero en cualquier caso, estos mensajes se puede ignorar, ya que el sistema los almacena internamente para generar los caracteres imprimibles.
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.
case WM_KEYDOWN: // CONTROL+Inicio = Borra todo if(VK_HOME == (int)wParam) { // Tecla de inicio if(GetKeyState(VK_CONTROL) && 0x1000) { nLinea=0; nColumna=0; lista[0][0] = 0; InvalidateRect(hwnd, NULL, TRUE); } } break;
En este ejemplo, usamos la combinación CTRL+Inicio para borrar el texto que estamos escribiendo. Procesamos el mensaje WM_KEYDOWN, para detectar la tecla de [Inicio], y si cuando eso sucede, verificamos si también está pulsada la tecla de [CTRL], para ello usamos la función GetKeyState y comprobamos si el valor de retorno tiene el bit de mayor peso activo, comparando con 0x1000.
Ejemplo 38
Nombre | Fichero | Fecha | Tamaño | Contador | Descarga |
---|---|---|---|---|---|
Ejemplo 38 | win038.zip | 2004-07-11 | 2952 bytes | 696 |
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;
Cada vez que se pulse la tecla o combinación de teclas correspondiente a una hot key, el sistema la busca entre las registradas, y envía un mensaje WM_HOTKEY a la ventana que la registró. Aunque esa ventana no esté activa, el mensaje será enviado, siempre que sea la única ventana que ha registrar esa combinación de teclas. En el parámetro wParam se recibe el identificador de la hot key.
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;
Finalmente, se puede desregistrar una hot key usando la función UnregisterHotKey, indicando la ventana para la que se registró, y el identificador.
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
Nombre | Fichero | Fecha | Tamaño | Contador | Descarga |
---|---|---|---|---|---|
Ejemplo 39 | win039.zip | 2004-07-11 | 2959 bytes | 695 |
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 |
---|---|---|---|
VK_LBUTTON | Botón izquierdo de ratón | VK_RBUTTON | Botón derecho de ratón |
VK_CANCEL | VK_MBUTTON | Botón central de ratón | |
VK_BACK | VK_TAB | Tabulador | |
VK_CLEAR | VK_RETURN | Retorno | |
VK_KANA | VK_SHIFT | Mayúsculas | |
VK_CONTROL | Control | VK_MENU | |
VK_PAUSE | Pausa | VK_CAPITAL | Bloqueo mayúsculas |
VK_ESCAPE | Escape | VK_SPACE | Espacio |
VK_PRIOR | Página anterior | VK_NEXT | Página siguiente |
VK_END | Fin | VK_HOME | Inicio |
VK_LEFT | Flecha izquierda | VK_UP | Flecha arriba |
VK_RIGHT | Flecha derecha | VK_DOWN | Flecha abajo |
VK_SELECT | VK_PRINT | Imprimir pantalla | |
VK_EXECUTE | VK_SNAPSHOT | ||
VK_INSERT | Insertar | VK_DELETE | Suprimir |
VK_HELP | Ayuda | VK_LWIN | Windows izquierda |
VK_RWIN | Windows derecha | VK_APPS | Menú de aplicación |
VK_NUMPAD0 | '0' numérico | VK_NUMPAD1 | '1' numérico |
VK_NUMPAD2 | '2' numérico | VK_NUMPAD3 | '3' numérico |
VK_NUMPAD4 | '4' numérico | VK_NUMPAD5 | '5' numérico |
VK_NUMPAD6 | '6' numérico | VK_NUMPAD7 | '7' numérico |
VK_NUMPAD8 | '8' numérico | VK_NUMPAD9 | '9' numérico |
VK_MULTIPLY | Multiplicar | VK_ADD | Sumar |
VK_SEPARATOR | VK_SUBTRACT | Restar | |
VK_DECIMAL | Punto decimal | VK_DIVIDE | Dividir |
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 |
VK_NUMLOCK | Bloqueo numérico | VK_SCROLL | Bloqueo desplazamiento |
VK_LSHIFT | Mayúsculas izquierdo | VK_RSHIFT | Mayúsculas derecho |
VK_LCONTROL | Control izquierdo | VK_RCONTROL | Control 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.