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
D Ejemplo 36 win036.zip 2004-07-11 3892 bytes 367

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
D Ejemplo 37 win037.zip 2004-07-11 2987 bytes 422

Comentarios de los usuarios (4)

LordKupa
2012-05-21 17:08:02

hola soy nuevo por aqui alguien me puede decir si es posible modificar el teclado para que me reconosca mas de 4 teclas presionadas o si ya esta escrito por ahi que me digan ¿en donde? jejeje gracias por leer y un cordial saludo gente

Salvador Pozo
2012-05-23 15:18:12

Hola, LordKupa:

Hasta donde yo sé, no es posible detectar más de tres teclas simultáneamente pulsadas. El motivo parece que está relacionado con el hardware del teclado.

El teclado es una matriz, diseñado de tal manera que si se pulsan más de tres teclas, la cuarta y siguientes no pueden ser determinadas, porque intentan activar las mismas líneas que ya estaban activadas. Algunas combinaciones de cuatro o más teclas podrían ser detectadas, pero siempre que se cumplan algunas reglas, como que pertenezcan a filas y columnas diferentes de la matriz, lo cual limita mucho las posibilidades. Por ejemplo, se pueden detectar las teclas "asdf", o "1234".

Hasta pronto.

JoseDC
2012-10-09 10:10:53

Hola la pagina esta muy buena teno una duda con el control del teclado, en visual c++ 6.0 cuando creo un proyecto sencillo dialog base, borro todos los controles que el programa agrega por defecto y activo la funcione OnKeyDown para escribir caracteres sobre la pantalla el programa funciona, pero ni bien agrego cualquier elemento (botones, Edit, etc) la ventana padre pierde el foco y el progama no hace nada, intente devolverle el foco con esta funcion:

CWnd* pWnd = GetDlgItem(IDD_EJEMPLO_DIALOG);

pWnd->SetFocus();

pero no funciona, lo curioso es que el metodo si funciona para otros elementos, osea cambiar dinamicamente el foco entre botones por ejemplo. Ojala me pueda alguien ayudar aunque el visual c++ 6.0 parece que anda fuera de moda.

Saludos

L
2015-06-28 23:32:01

Muchas gracias por el grandioso curso, me ha servido mucho en mi formación como programador.