10 X Events

在一个Xlib程序里,所有的动作都是被事件驱动的。针对事件”expose”的反应是在屏幕上画些什么。如果程序窗口的一部分被遮住,然后又露出来了(例如一个窗口遮住了另一个窗口),X服务器将会发送一个”expose”事件来让程序知道它的窗口的一部分应该被重新绘制。用户的输入(按下键盘,鼠标移动等)也是被做成一系列的事件。

10.1 Registering For Event Types Using Event Masks

在Xlib里,我们使用函数XSelectInput()来注册要接受的事件。该函数接受3个参数 - 显示结构,一个窗口ID,和一个它想要接受的事件类型的面具。参数窗口ID允许我们为不同的窗口注册接受不同类型的事件。下面的例子展示了我们为窗口ID为”win”的窗口注册”expose”事件:

XSelectInput(display, win, ExposureMask);

ExposureMask在头文件”X.h”中被定义,如果我们想注册更多的事件类型,我们可以使用逻辑”or”,如下:

XSelectInput(display, win, ExposureMask | ButtonPressMask);

这样就即注册了事件”expose”也注册了一个鼠标按键事件。你应该注意到一个Mask可以描述多种事件类型。

注意:一个经常出现的程序Bug就是给程序添加了处理新的类型的事件的代码,却完全不记得在函数XSelectInput()里注册所追加的事件类型。这时候,程序员就可能会苦恼的在电脑前坐上个把小时去调试他的程序,疑惑”为什么我的程序不去注意我已经松开了按钮???”,最后发现自己只注册了按钮按下的事件却没有注册松开的事件。

10.2 Receiving Events - Writing The Events Loop

我们在注册了感兴趣的事件类型后,我们应该进入事件循环并且处理它们。有许多方法来实现事件循环,但比较一般且简单的如下:

 
/* this structure will contain the event's data, once received. */
XEvent an_event;
 
/* enter an "endless" loop of handling events. */
while (1) {
    XNextEvent(display, &an_event);
    switch (an_event.type) {
      case Expose:
        /* handle this event type... */
        .
        .
        break;
      default: /* unknown event type - ignore it. */
        break;
    }
}

函数XNextEvent从X服务器那里取得新的事件。如果没有,它就会处于阻塞状态直到接受到了一个事件。函数返回后,事件的数据就会被放到第二个类型为XEvent的参数里。前面取得的事件变量的”type”域指明了该事件的类型。Expose是一个告诉我们窗口的一部分需要重画的事件的类型。在处理过这个事件后,我们就返回去取得下一个要处理的事件。很明显,我们应该提供给用户一些方法去结束程序。一般发个”quit”事件就行了。

10.3 Expose Events

暴露事件是程序最经常收到的事件中的一个。它会在以下几种情况下出现:

  • 一个遮住我们的窗口的窗口被移走了,我们的窗口又重新露出来了。

  • 我们的窗口被其它窗口打开了。

  • 我们的窗口第一次被映射到屏幕上。

  • 我们的窗口从最小化中恢复到打开状态。

你应该已经注意到这里有一个隐藏的假设 - 当窗口被遮住时被遮住的内容就丢失了。你也许会提出疑问为什么X服务器不保存那些内容。答案只有一个 - 节省内存。在某一个时刻,屏幕上可能会有大量的窗口,保存它们的内容将会需要非常大量的内存(例如,一个256色的分辨率为400*400的bitmap图片需要至少160KB的内存来保存它。现在考虑一下有20个窗口的情况,这其中一些可能会有更大的尺寸)。实际上,确实有方法来告诉X服务器在特殊情况下保存窗口的内容,我们会在稍后介绍。

当我们取得了一个”expose”事件,我们应该从XEvent结构的”xexpose”成员中取得事件数据(在我们的例程里,它是”an_event.xexpose”)。另外那个结构还包括一些有趣的域:

/* 在服务器的事件队列里还有多少暴露事件。这在我们获得了多个暴露事件时非常有用 - 我们通常避免执行重画工作直到确定它是最后一个暴露事件的时候(如直到是0为止)。 */
count
/* 发送该重画事件的窗口的ID(我们的程序为多个窗口注册了事件的时候)。 */
Window window
/* 从窗口的左上算起,需要被重画的区域的左上坐标。 */
int x, y
/* 需要被重画区域的宽高。 */
int width,height

在我们的演示程序中,我们无视了那个需要被重画的区域,而是重画了整个窗口,这是非常低效的,我们在后面将会演示一些只重画需要重画的区域的技术。

以下是一个例子,演示我们收到任何”expose”事件时如何在一个窗口中画一条直线。这是其中的事件循环的case段的代码:

  case Expose:
    /* if we have several other expose events waiting, don't redraw. */
    /* we will do the redrawing when we receive the last of them.    */
    if (an_event.xexpose.count > 0)
        break;
    /* ok, now draw the line... */
    XDrawLine(display, win, gc, 0, 100, 400, 100);
    break;

10.4 Getting User Input

就目前来说,用户的输入主要从两个地方过来 - 鼠标和键盘。有各种各样的事件帮助我们来获取用户的输入 - 一个键盘上的键被按下了,一个键盘上的键被松开了,鼠标光标离开了我们的窗口,鼠标光标进入了我们的窗口等等。

Mouse Button Click And Release Events

我们为我们的窗口处理的第一个事件是鼠标按钮时间。为了注册一个这样的事件类型,我们将追加以下的面具

  • ButtonPressMask:通知我们窗口中的任何一个鼠标键按下动作(这是事件掩码,XSelectInput中使用)

  • ButtonReleaseMask:通知我们窗口中的任何一个鼠标键松开动作

  • ButtonPress:在我们的窗口上一个鼠标键被按下了 (具体事件类型,判断事件类型是否等于鼠标按下)

  • ButtonRelease:在我们的窗口上一个鼠标键被松开了

在事件结构里,通过”an_event.xbutton”来获得事件的类型,另外它还包括下面这些有趣的内容:

  • Window window:事件发送的目标窗口的ID(如果我们为多个窗口注册了事件)

  • int x, y:从窗口的左上坐标算起,鼠标键按下时光标在窗口中的坐标

  • int button:鼠标上那个标号的按钮被按下了,值可能是Button1,Button2,Button3

  • Time time:事件被放进队列的时间。可以被用来实现双击的处理

下面的例子,将演示我们如何在鼠标的位置画点,无论我们何时收到编号为1的按钮的”鼠标按下”的事件时我们画一个黑点,收到编号为2的按钮的”鼠标按下”的事件时我们擦掉那个黑点(例如画一个白点)。我们假设现在有两个GC,gc_draw使用下面的代码

  case ButtonPress:
    /* store the mouse button coordinates in 'int' variables. */
    /* also store the ID of the window on which the mouse was */
    /* pressed.                                               */
    x = an_event.xbutton.x;
    y = an_event.xbutton.y;
    the_win = an_event.xbutton.window;
 
    /* check which mouse button was pressed, and act accordingly. */
    switch (an_event.xbutton.button) {
        case Button1:
            /* draw a pixel at the mouse position. */
            XDrawPoint(display, the_win, gc_draw, x, y);
            break;
        case Button2:
            /* erase a pixel at the mouse position. */
            XDrawPoint(display, the_win, gc_erase, x, y);
            break;
        default: /* probably 3rd button - just ignore this event. */
            break;
    }
    break;

Mouse Pointer Enter And Leave Events

另一个程序通常会感兴趣的事件是,有关鼠标光标进入一个窗口的领域以及离开那个窗口的领域的事件。有些程序利用该事件来告诉用户程序现在在焦点里面。为了注册这种事件,我们将会在函数XSelectInput()里注册几个面具。

  • EnterWindowMask:通知我们鼠标光标进入了我们的窗口中的任意一个

  • LeaveWindowMask:通知我们鼠标光标离开了我们的窗口中的任意一个

我们的事件循环中的分支检查将检查以下的事件类型

  • EnterNotify:鼠标光标进入了我们的窗口

  • LeaveNotify:鼠标光标离开了我们的窗口

这些事件类型的数据结构通过例如”an_event.xcrossing”来访问,它还包含以下有趣的成员变量:

  • Window window:事件发送的目标窗口的ID(如果我们为多个窗口注册了事件)

  • Window subwindow:在一个进入事件中,它的意思是从那个子窗口进入我们的窗口,在一个离开事件中,它的意思是进入了那个子窗口,如果是”none”,它的意思是从外面进入了我们的窗口。

  • int x, y:从窗口的左上坐标算起,事件产生时鼠标光标在窗口中的坐标

  • int mode:鼠标上那个标号的按钮被按下了,值可能是Button1,Button2,Button3

  • Time time:事件被放进队列的时间。可以被用来实现双击的处理

  • unsigned int state:这个事件发生时鼠标按钮(或是键盘键)被按下的情况 - 如果有的话。这个成员使用按位或的方式来表示

    • Button1Mask

    • Button2Mask

    • Button3Mask

    • Button4Mask

    • ShiftMask

    • LockMask

    • ControlMask

    • Mod1Mask

    • Mod2Mask

    • Mod3Mask

    • Mod4Mask

它们的名字是可以扩展的,当第五个鼠标钮被按下时,剩下的属性就指明其它特殊键(例如Mod1一般是”ALT”或者是”META”键)

  • Bool focus:当值是True的时候说明窗口获得了键盘焦点,False反之

The Keyboard Focus

在屏幕上同时会有很多窗口,但同一时间只能有一个窗口获得键盘的使用。X服务器是如何知道哪一个窗口可以发送键盘事件呢?这个是通过使用键盘焦点来实现的。在同一时间只能有一个窗口获得键盘焦点。Xlib函数里存在函数允许程序让指定窗口获得键盘焦点。用户通常使用窗口管理器来为窗口设置焦点(通常是点击窗口的标题栏)。一旦我们的窗口获得了键盘焦点,每个键的按下和松开都将引起服务器发送事件给我们的程序(如果已经注册了这些事件的类型)。

Keyboard Press And Release Events

如我们前面所提到的,按键编码对我们来说是没有什么意义的,它是由连接着X服务器的键盘产生的硬件级编码并且是与某个型号的键盘相关的。为了能解释到底是哪个按键产生的事件,我们把它翻译成已经被标准化了的按键符号。我们可以使用函数XKeycodeToKeysym()来完成这个翻译工作。该函数使用3个参数:一个显示的指针,要被翻译的键盘编码,和一个索引(我们在这里使用”0”)。标准的Xlib键编码可以参考文件”X11/keysymdef.h”。在下面的例子里我们使用函数XkeycodeToKeysym来处理按键操作,我们讲演示如何以以下顺序处理按键事件:按”1”键将会在鼠标的当前位置下画一个点。按下”DEL”键将擦除那个点。按任何字母键(a至z,大写或小写)将在标准输出里打印。其它的按键将会被无视。假设下面的”case”段代码是在一个消息循环中。

 
  case KeyPress:
    /* store the mouse button coordinates in 'int' variables. */
    /* also store the ID of the window on which the mouse was */
    /* pressed.                                               */
    x = an_event.xkey.x;
    y = an_event.xkey.y;
    the_win = an_event.xkey.window;
    {
        /* translate the key code to a key symbol. */
        KeySym key_symbol = XKeycodeToKeysym(display, an_event.xkey.keycode, 0);
        switch (key_symbol) {
            case XK_1:
            case XK_KP_1: /* '1' key was pressed, either the normal '1', or */
                          /* the '1' on the keypad. draw the current pixel. */
                XDrawPoint(display, the_win, gc_draw, x, y);
                break;
            case XK_Delete: /* DEL key was pressed, erase the current pixel. */
                XDrawPoint(display, the_win, gc_erase, x, y);
                break;
            default:  /* anything else - check if it is a letter key */
		if (key_symbol >= XK_A && key_symbol <= XK_Z) {
		    int ascii_key = key_symbol - XK_A + 'A';
		    printf("Key pressed - '%c'/n", ascii_key);
		}
		if (key_symbol >= XK_a && key_symbol <= XK_z) {
		    int ascii_key = key_symbol - XK_a + 'a';
		    printf("Key pressed - '%c'/n", ascii_key);
		}
                break;
        }
    }
    break;

你将会发现键盘键符号到物理键编码的转换的方法,程序应该小心的处理各种可能出现的情况。同时我们假设字母键的符号值是连续的。

10.5 X Events - A Complete Example

我们将给一个完整的处理事件的例子 src/03_events.c 。给程序创建一个窗口,在上面进行一些绘画工作,然后进入一个事件循环。如果它获得了一个暴露事件 - 它重画整个窗口。如果它获得一个鼠标左键事件,它在鼠标光标出画一个黑点。如果鼠标的中间键被按下了,它在鼠标光标下画一个白点(例如擦出那个点)。应该注意这个图形是改变是如何被处理的。它对使用适当的颜色来绘制并不是很有效。我们需要对颜色的变化作一下记录,这样在下一个暴露事件来到时我们可以用正确的颜色来绘制。我们使用了一个(1000*1000)的巨大矩阵来保存像素。刚开始,所有的单元都被置成0。当画了一个点的时候,我们将该单元置成1。如果该点被画成白色,我们将该单元置成-1。我们不能仅仅把黑色置成0,否则我们刚开始画的将被误删掉。最后,用户按了键盘上任意的按钮,程序将退出。

当运行这个程序时,你也许会注意到移动的事件经常会漏画点。如果鼠标移动的很快,我们将收不到所有的事件。结果,如果我们要处理这种情况,我们就需要记住上一次收到事件时的鼠标位置,然后应该画一条线在两点之间。一般绘图程序都是这么做的。