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).

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 29

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 31

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 TanslateMessage 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 30

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 29

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.