INTRODUCCIÓN A LA PROGRAMACIÓN EN X WINDOW

Artículo 4: Eventos en X Window

Autor: (c) Santiago Romero.
Revista: Programación Actual (Prensa Técnica) nº 22, Enero-1999


Tras nuestra primera introducción a los eventos vamos por fin a ver las funciones y estructuras necesarias para su gestión, y, por tanto, del código necesario para añadir interactividad y funcionalidad a nuestros programas en X Window.


En la anterior entrega se explicó toda la teoría sobre los eventos y organización del código para la orientación a eventos de los programas, pero sin profundizar en las funciones y estructuras que se utilizarán para la gestión y detección de los mismos, aspecto que se va a completar en esta entrega y la siguiente mediante un amplio análisis de las variables, estructuras y funciones relacionadas.

En el anterior capítulo (como sencilla introducción) vimos que la estructura general de un programa en X Window se apoyaba en la selección de los eventos deseados por el cliente, y la detección y manipulación de los mismos en un bucle de eventos o mensajes:

Programa:
{
  Inicialización();
  SeleccionaEventos();

  REPETIR Espera_EVENTO
  {
    Si llega PULSACION_DE_TECLADO:
    {
       Código que gestiona teclado;
    }

    Si llega PULSACION_DE_RATON:
    {
       Código que gestiona ratón;
    }
    Etc...

    Resto_de_eventos: nada();
  }
}

Lo primero que se va a comentar es la manera de seleccionar eventos, pues como veremos a continuación, no todos los eventos son recibidos por el programa cliente, sino que se reciben tan sólo los que éste especifica, y esto es una gran ventaja en el ámbito de la velocidad de procesamiento de los programas.


XSELECTINPUT

Si los servidores enviaran a las aplicaciones todos los eventos que ocurren relacionados con las mismas, éstas se verían saturadas (en el sentido en que malgastarían tiempo en los no útiles) por la gran cantidad de los mismos, perdiendo tiempo en el procesado de aquellos que no nos son necesarios, e incluso se podría dar el caso de que la aplicación no supiese la manera de procesar un evento en concreto o que dicho procesado ralentice el funcionamiento de los eventos que sí son importantes para el programa. Para evitar esto, X-Window incluye la llamada de selección de eventos de Xlib XSelectInput.

Durante la inicialización de nuestra aplicación y creación de la ventana principal de la misma es necesario indicarle al servidor qué tipo de eventos se desean recibir, pues puede interesarnos no recibir información de algún tipo de evento ocurrido, mientras que para nuestra aplicación otro periférico o evento software puede ser vital. Para la indicación de eventos deseados (también llamada "selección de eventos") al servidor X se utiliza la función XSelectInput y las llamadas "máscaras de eventos".


XSelectInput(display, window, event_mask)
Display *display;
Window window;
long event_mask;

Sobre los parámetros que se le pasan a esta función, el argumento display es la variable que conecta nuestro cliente con el servidor X (ya habitual en prácticamente todas las llamadas Xlib), la variable window es el idenficador de la ventana sobre la que deseamos seleccionar los eventos que se recibirán, y event_mask es una variable máscara (es decir, se utilizarán los diferentes bits de la misma como indicadores o flags, lo cual indica que al ser una variable de tipo long dispondremos de 32 posibles eventos distintos). Cada bit de esta variable event_mask se refiere a un evento concreto, de tal modo que si este bit se encuentra activado se estará pidiendo al servidor que informe al cliente de dicho evento, mientras que si se encuentra desactivado el servidor entiende que el cliente no desea recibir tal evento (con el consecuente ahorro de tiempo de procesamiento que esto supone).

Para hacer más fácil la selección de eventos existen cadenas o nombres simbólicos (definidos en X11/X.h) que identifican a los eventos para no tener que recordar el bit asociado a cada uno de ellos. Así, para indicarle al servidor que se desean recibir los eventos de pulsación de teclado y botón del mouse bastaría con activar estos 2 bits mediante el operador de concatenamiento OR (|):

Ejemplo:


 XSelectInput( display, window, KeyPressMask | ButtonPressMask);

Si no se utiliza esta función el servidor tiene indicado por defecto no pasarle ningún evento extra al cliente (simplemente los activados por defecto), por lo que es recomendable su utilización previa a la entrada al bucle de eventos.

eventos

Las posibles máscaras de eventos a utilizar se van a comentar a continuación, teniendo en cuenta que los nombres finalizados en Mask son los identificadores de máscaras de eventos a utilizar en la función XSelectInput, mientras que los que no tienen dicho sufijo se refieren al nombre del propio evento y que deberá ser gestionado dentro del bucle principal de la aplicación, el bucle de procesado de eventos.


MASCARAS DE EVENTOS DE RATON Y TECLADO

Las principales son las siguientes:

ButtonPressMask: Nos avisa de pulsaciones del botón del mouse mediante un evento del tipo ButtonPress. Es decir, si seleccionamos el evento ButtonPressMask el servidor nos enviará los mensajes ButtonPress pertinentes a la ventana especificada.

ButtonReleaseMask: Máscara de evento referido a la liberación del botón del mouse, de tal modo que en caso de ser seleccionado se recibiría un evento del tipo ButtonRelease cada vez que el botón del ratón fuera liberado.

KeyPressMask: Esta máscara de evento indica al servidor de debe enviarnos todas las pulsaciones de teclado producidas en la ventana especificada, generando un evento KeyPress que podremos detectar en el bucle de mensajes. Como veremos, tras detectar este evento en el bucle de mensajes es posible obtener el código ASCII o scan code de esta tecla si así lo necesita nuestro programa, con funciones que comentaremos en su momento.

KeyReleaseMask: Similar a KeyPressMask, pero con el fin de que se nos indiquen las liberaciones de teclas mediante eventos KeyRelease.

KeymapStateMask: Permite la recepción de eventos tipo KeymapNotify relacionados con el mapa de teclado.

EnterWindowMask: Máscara que hace que el servidor nos avise cuando el usuario dé el foco principal a nuestra ventana, es decir, la haga la ventana activa, recibiendose un evento llamado EnterNotify, lo cual quiere decir que el puntero del mouse ha entrado en la ventana a la que se le informa del evento.

LeaveWindowMask: Similar a EnterWindowMask, pero que nos avisará mediante un mensaje LeaveNotify de que el usuario ha cambiado el foco (o que el puntero del ratón ha salido de dicha ventana) y que la ventana que recibe el mensaje no es la activa en ese momento.

ButtonMotionMask: Esta máscara hace que el servidor nos envie eventos relacionados con el movimiento del mouse cuando cualquier botón del mismo está pulsado (muy útil para gran cantidad de aplicaciones), recibiendo un evento MotionNotify, el mismo que se recibe ante cualquier movimiento del mouse. Esta máscara hace que este evento se genere mediante cualquier botón del mouse, mientras que las máscaras Button1MotionMask, Button2MotionMask, etc, hace que se genere ante el movimiento durante la pulsación del botón especificado (1, 2, etc.).

PointerMotionMask: Esta máscara hace que se reciba un evento MotionNotify cuando se produzca cualquier movimiento del puntero del ratón (sin necesitar que el botón sea pulsado, como en ButtonMotionMask).

PointerMotionHintMask: Permite que la notificación de eventos de movimiento (similares a PointerMotionMask) sea de forma no continua, a intervalos.

FocusChangeMask: Nos permitira recibir eventos FocusIn y FocusOut.


MASCARAS DE EVENTOS RELACIONADOS CON LAS VENTANAS

StructureNotifyMask: Esta máscara permite que el servidor le envie al cliente cualquier evento relacionado con el cierre, desmapeado, mapeado, configuración y circulación de ventanas. Es decir, si seleccionamos esta máscara se recibirán eventos como DestroyNotify, UnmapNotify, MapNotify, GravityNotify, ReparentNotify, ConfigureNotify y CirculateNotify. Estos eventos serán analizados en la sección dedicada a los eventos, mientras que ahora el lector simplemente debe ir conociendo las máscaras que le permitirán acceder a dichos eventos. A título de ejemplo, recibir un mensaje MapNotify significaría que la ventana acaba de ser mapeada o proyectada, mientras que recibir un mensaje ConfigureNotify nos indicaría que ha habido un cambio en la geometría de la ventana o en su orden de apilamiento, lo cual podria permitir a nuestra aplicación adaptarse a la nueva geometría. Para recibir este tipo de mensajes referentes a cambios en la ventana se hace pues necesario especificar en XSelectInput la máscara StructureNotifyMask.

SubStructureNotifyMask: esta máscara le permitirá obtener los mismos eventos que StructureNotifyMask con la adición del evento CreateNotify.

SubStructureRedirectMask: Permite la recepción de los eventos MapRequest y ConfigureRequest y su tratamiento en el bucle de mensajes. Nótese que cuando algún evento no es seleccionado (y, por tanto, no se hace nada con él en el bucle de eventos), X Window realiza para tal evento las acciones genéricas por defecto (o nada, si así lo especificamos). Esta máscara, más concretamente, permite que si un sólo cliente (por ejemplo: el propio gestor de ventanas) ha seleccionado ésta sobre una ventana, las peticiones de otros clientes sobre las ventanas hijas de la ventana sobre la que se está aplicando el Mask no será realizado por el servidor (cosa que se realiza habitualmente) sino que éste enviará un mensaje a este cliente para que realice él las acciones pertinentes. Gracias a estos eventos existen los gestores de ventanas o window managers, que son quienes tienen la última palabra en en aspecto final y geometría de las ventanas en X Window.

VisibilityChangeMask: Permite recibir eventos del tipo VisibilityNotify, que indican cambios de visibilidad en la ventana a la que se informa.

ResizeRedirectMask: Esta máscara da acceso al evento de ResizeRequest, sobre el cambio de tamaño de la ventana sobre la que se especifica la selección del evento.

PropertyChangeMask: Al activar este bit se permite la recepción de eventos del tipo PropertyNotify, que indican cambios de valores asociados a una propiedad.


EVENTOS RELACIONADOS CON GRAFICOS

ExposureMask: Al seleccionar esta máscara se recibirán los eventos de tipo Expose, que indican que una parte rectangular de la ventana se ha hecho visible, como por ejemplo en el caso de que una ventana de otra aplicación solapara en parte a nuestra ventana y de repente fuera cerrada o movida, con lo que el servidor se ve en la obligación de avisarnos de esta circunstancia para que nuestro programa sepa que puede dibujar en dicho área. Si más de una zona se liberara simultáneamente, se generará un evento por cada zona rectangular que se haga visible, lo que nos permitirá redibujar cada una de estas zonas con los datos gráficos que deban contener y que antes no eran necesarios al estar tapados por otra ventana.

ColormapChangeMask: Esta máscara permite la recepción de eventos ColormapNotify, relacionados con el cambio del mapa de colores (ColorMap) por parte de cualquier aplicación. Este mensaje es importante en los modos gráficos con paleta, como por ejemplo de 16 colores o de 8 bits por pixel (256 colores).


EVENTOS SIEMPRE SELECCIONADOS

Existe una serie de eventos siempre seleccionados, que son los siguientes: MappingNotify, ClientMessage (usado para la comunicación entre clientes), SelectionRequest, SelectionNotify y SelectionClear (eventos relacionados con la selección de secciones de textos para copiado y pegado del mismo con el mouse, y por tanto directamente relacionados con la comunicación entre clientes o el intercambio de texto). Estos eventos pueden considerarse como especiales, así como GraphicExpose y NoExpose.

En el cuadro 1 disponemos de un resumen de todas las máscaras de eventos y los mensajes que se generarán hacia nuestro bucle de mensajes al producirse estos.

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 EVENTOS                MáSCARAS DE EVENTOS
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 Eventos de teclado y ratón:

 KeyPress        ->     KeyPressMask
 KeyRelease      ->     KeyReleaseMask
 KeymapNotify    ->     KeymapStateMask
 ButtonPress     ->     ButtonPressMask
 ButtonRelease   ->     ButtonReleaseMask
 MotionNotify    ->     ButtonMotionMask
                        Button1MotionMask
                        Button2MotionMask
                        (etc.)
                        PointerMotionMask
                        PointerMotionHintMask
 EnterNotify     ->     EnterWindowMask
 LeaveNotify     ->     LeaveWindowMask

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 Eventos relacionados con ventanas:

 DestroyNotify
 MapNotify
 UnMapNotify
 CirculateNotify
 ReparentNotify
 ConfigureNotify
 GravityNotify   ->     StructureNotifyMask
 CreateNotify
 DestroyNotify
 MapNotify
 UnMapNotify
 CirculateNotify
 ReparentNotify
 ConfigureNotify
 GravityNotify   ->     SubStructureNotifyMask
 MapRequest
 ConfigureRequest->     SubStructureRedirectMask
 VisibilityNotify->     VisibilityChangeMask
 ResizeRequest   ->     ResizeRedirectMask
 PropertyNotify  ->     PropertyChangeMask
 FocusIn
 FocusOut        ->     FocusChangeMask

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 Eventos relacionados con gráficos:

 Expose          ->     ExposureMask
 ColormapNotify  ->     ColormapChangeMask

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 Eventos siempre seleccionados:

 MappingNotify
 ClientMessage
 SelectionNotify
 SelectionClear
 SelectionRequest

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -


PROPAGACION DE EVENTOS

Cuando un evento ocurre en una ventana hija de la ventana sobre la que tenemos asignada la lectura y procesado de eventos, este evento se propaga (pasa a otra ventana de orden superior, o la madre, mecanismo denominado propagación de eventos), y cuyo resultado será que el evento sea enviado a la ventana madre. La ventana sobre la que se generó el evento (que es la activa) se le denomina ventana origen o fuente, y es la única capaz de generar un evento de ratón o teclado (ya que es la que tiene el foco), aunque los eventos deben ser procesados por la ventana madre (que es la que nosotros definimos para dicho procesado). En la propagación de eventos pueden ocurrir principalmente 2 casos:

Si el evento ocurrido no hubiera sido seleccionado por la ventana (no hemos pedido su recepción), este evento se propaga hasta la ventana madre y de ahi se continúa propagando hasta llegar a la ventana raíz (Root Window).

Si el evento estaba seleccionado, simplemente se comunica al cliente para que reaccione en consecuencia. A título de ejemplo, supongamos que hacemos un programa de dibujo con una ventana principal (donde se va a dibujar) y una subventana desde donde se seleccionan las formas gráficas a utilizar (línea, mano alzada, círculo, etc.). Si el usuario pulsara en cualquiera de estas opciones, el evento sería propagado hasta la ventana madre (si así lo hemos seleccionado) para que el propio cliente sea el que cambie el estilo de dibujo al seleccionado.

El mecanismo de propagación descrito tiene dos excepciones: la primera de ellas es el enfoque del teclado, y la segunda es la posesión del teclado y puntero pedida directamente por un programa cliente.

Este segundo proceso es conocido como captura del ratón o teclado (grab), y es establecido por cualquier programa cliente que necesita que todos los eventos producidos por estos dispositivos (se pueden capturar de forma conjunta o independiente) le sean notificados sólo a él. Podemos distinguir entre 2 tipos de capturas o grab: el grab activo (que se mantiene hasta que el cliente lo desactiva manualmente) o el pasivo, donde es el propio programa quien especifica cuando tendrá lugar la captura (ante la pulsación de un determinado botón o tecla), estado que se mantendrá hasta que se repita la misma pulsación.

La captura de ratón y teclado resulta muy útil en juegos y otros tipos de aplicaciones que requieren una atención especial a periféricos como el ratón o teclado.

Sobre los eventos de teclado, cabe decir que en cada instante hay una sóla ventana que recibe el enfoque del teclado (o foco), de tal modo que todos los eventos de teclado estarán siendo recibidos sólo por la ventana que recibe el foco y sus ventanas hijas. Este foco lo tiene por defecto la ventana raíz o Root Window (el escritorio), y es el gestor de ventanas el que suele modificarlo ante las peticiones del usuario. Un programa cliente puede modificar este enfoque mediante la función XSetInputFocus, que no vamos a necesitar en estos primeros capítulos.


OBTENCION DE EVENTOS

Los eventos enviados por el servidor se almacenan en una cola de entrada al cliente de forma asíncrona. El servidor envía al cliente, como veremos en el siguiente número, una estructura de evento conteniendo los datos necesarios para que el cliente reconozca el evento y recupere la información asociada al mismo.

Si, por ejemplo, el usuario pulsa una tecla y la aplicación seleccionó con XSelectInput() la recepción de eventos de teclado (KeyPressMask), al producirse la pulsación el servidor enviará al cliente un evento/mensaje llamado KeyPress, y una estructura de datos que contendrá información sobre el evento, en este caso, la tecla pulsada. En caso de que el cliente no seleccionara KeyPressMask, el evento simplemente es filtrado.

Cada evento requiere una información concreta (teclado=teclas, ratón=posiciones, botones, etc.), de modo que existen diferentes tipos de estructuras de eventos, de modo que X Window dispone de una estructura unión de todas que nos permitirá referirnos a la información de todos estos eventos mediante una sola struct llamada XEvent. Cuando se reciben los eventos, es posible retirarlos de la cola o bien leerlos sin retirarlos de dicha cola, todo ello mediante funciones como XNextEvent(), XMaskEvent(), WindowEvent(), XCheck() o XPeekEvent(), funciones que serán comentadas en la próxima entrega.


SALIDA DE TEXTO A PANTALLA

Aunque no es el momento todavía de comentar el uso y carga de fuentes de texto y de contextos gráficos (GC), para hacer nuestros programas de ejemplo (y, porqué no, cualquier otro programa que use texto y que pretenda realizar lector mientras se avanza en el curso), será necesario saber sacar texto por pantalla, ya no en la xterm desde la que se lanza el programa sino en la misma ventana gráfica de nuestra aplicación. Para ello vamos a ver el modo de utilización de la función XDrawString, de tal modo que pueda ser usada para distinguir en pantalla los diferentes eventos durante nuestras primeras pruebas (Ej: "Ha ocurrido un evento KeyPress" o "Se ha generado un ButtonPress"), y así disponer de una sencilla opción de debuggeo.


XDrawString(display, drawable, gc, x, y, string, length)
Display *display;
Drawable drawable;
GC gc;
int x, y;
char *string;
int length;

Estos parámetros significan lo siguiente: Display es, como siempre, la comunicación entre el cliente y el servidor X; drawable es, de una forma sencilla, el lugar donde se dibujará; GC es el contexto gráfico a usar; length es la longitud de la cadena de texto a imprimir (se puede obtener con strlen()) y (X,Y) especifican las coordenadas de pixel donde se emplazará la esquina superior izquierda de la primera letra de la frase. Según se comenta en la documentación, cada imagen o carácter de la fuente es tratado como una máscara para una operación de rellenado en el drawable, de modo que como resultado de esa operación se obtenga una letra trazada en pantalla. El hecho de necesitar un GC (que podremos modificar como se verá más adelante), nos permitirá imprimir las letras con diferentes funciones, con varios estilos de rellenado con o sin clipping/recorte, etc.

Para poder utilizar esta función es necesario obtener primero todos sus parámetros. Para ello necesitaremos hacer uso de las funciones XLoadFont(), XCreateGC y XSetFont. El drawable será la propia ventana, es decir, el mismo manejador/identificador nos servirá.


Font XLoadFont(display, name)
Display *display;
char *name;

Carga la fuente especificada y devuelve su respectivo identificador de fuente. El nombre de la fuente es cualquiera de los nombres estándar de X Window. Nosotros comenzaremos por utilizar la fuente de 9x15 u 8x16, cuyos nombres son "9x15" y "8x16", ya que estos son unos tamaños proporcionales a las fuentes de letras en consola.


 Ejemplo: XLoadFont( display, "9x15" );

Si queremos saber las fuentes disponibles en el sistema es posible utilizar la función XListFonts que requiere los siguientes parámetros.


char **XListFonts(display, pattern, maxnames, actual_count_return)
Display *display;
char *pattern;
int maxnames;
int *actual_count_return;

Acepta la variable display, una cadena de búsqueda llamada pattern, como por ejemplo "9x*" (acabada en cero), de tal modo que se puedan especificar patrones de búsqueda (o simplemente "*", todas), maxnames, que es el número máximo de nombres que queremos que se nos devuelvan y actual_count_return, en donde se nos devolverá el número total de nombres de fuentes. No obstante esta función no la necesitaremos por el momento.


XSetFont(display, gc, font)
Display *display;
GC gc;
Font font;

Esta función pone la fuente especificada como fuente a utilizar en el GC especificado, aceptando para ello el display, el gc y la fuente (font) a utilizar. Este GC será creado con XCreateGC(), y no merece especial distinción (simplemente ver el ejemplo incluido más abajo).

Nota: para ver los diferentes mensajes de error que pueden generarse ante llamadas a estas funciones es recomendable la consulta de las páginas man de las diferentes funciones.


EJEMPLO DE UTILIZACION

Aunque no es un ejemplo en sí mismo, a continuación se ofrece una sección de código que explica en qué momento y cómo hay que utilizar las diferentes funciones comentadas arriba sobre la salida de textos:


void main()
{
  /* declaración de variables */
  Display *display;
  Window window;
  GC gc;
  Font font;
  int pantalla;
  unsigned long negro, blanco;

  /* inicializaciones */
  display = XOpenDisplay( NULL );
  font = XLoadFont( display, "9x15" );
  pantalla = DefaultScreen( display );
  negro = BlackPixel( display, pantalla );
  blanco = WhitePixel( display, pantalla );

  window = XCreateSimpleWindow( etc, etc, etc,)
  XSelectInput( etc, etc, etc );
  XMapWindow( display, window );

  /* VARIABLES GC y FONT */
  gc = XCreateGC( display, window, 0, NULL );
  XSetFont( display, gc, font );

  /* resto del programa */
       .. .. .. .. ..

  /* en cualquier momento del mismo: */
  XDrawString( display, window, gc, 100, 100, "Texto", 5 );

  /* más codigo del programa */
      .. .. .. .. ..
}

Lo que puede verse en el anterior código es que se necesita cargar un fuente y crear un GC que utilizará XDrawString para escribir en la ventana especificada.

Por otra parte, conociendo esta función y teniendo a nuestro alcance la salida de texto en pantalla y el conocimiento (gracias a este y el siguiente artículo) de los eventos, ya será posible implementar programas elaborados de texto en X Window, creando un cursor virtual (2 variables CursorX, CursorY) y una función tipo printf() que utilice estas variables para hacer de la ventana gráfica algo similar a una ventana de texto, incrementando CursorX en AnchoFuente pixels para entrar en la siguiente celdilla, o CursorY en AltoFuente para pasar a la siguiente línea.


EN LA PROXIMA ENTREGA

En el próximo capítulo se comentará la estructura XEvent y los diferentes eventos disponibles en X Window. Además se proporcionarán ejemplos prácticos con los que finalizaremos el estudio de mensajes y eventos, a título de resumen de los 3 capítulos relacionados con el tema, para comenzar a crear sencillos programas mediante algunas funciones gráficas y de texto de Xlib.

Figura 1: "Eventos y máscaras de eventos."

Santiago Romero


Volver a la tabla de contenidos.