在一个Xlib程序里,所有的动作都是被事件驱动的。针对事件"expose"的反应是在屏幕上画些什么。如果程序窗口的一部分被遮住,然后又露出来了(例如一个窗口遮住了另一个窗口),X服务器将会发送一个"expose"事件来让程序知道它的窗口的一部分应该被重新绘制。用户的输入(按下键盘,鼠标移动等)也是被做成一系列的事件。
使用事件面具来注册事件类型
一个程序在创建一个窗口(也可以是好几个)之后,它就应该告诉X服务器为那个窗口它希望接受哪些事件。在缺省情况下,没有事件会发给程序。程序可以注册很多鼠标事件(也可以叫"指针"),键盘事件,暴露事件等等。这么做完全是为了优化服务器-到-客户端的通信(例如,实在是没什么理由向地球另一端的程序发送它不感兴趣的东西)。
在Xlib里,我们使用函数XSelectInput()来注册要接受的事件。该函数接受3个参数 - 显示结构,一个窗口ID,和一个它想要接受的事件类型的面具。参数窗口ID允许我们为不同的窗口注册接受不同类型的事件。下面的例子展示了我们为窗口ID 为"win"的窗口注册"expose"事件:
XSelectInput(display, win, ExposureMask);
ExposureMask在头文件"X.h"中被定义,如果我们想注册更多的事件类型,我们可以使用逻辑"or",如下:
XSelectInput(display, win, ExposureMask | ButtonPressMask);
这样就即注册了事件"expose"也注册了一个鼠标按键事件。你应该注意到一个面具可以描述多种事件类型。
注意:一个经常出现的程序臭虫就是给程序添加了处理新的类型的事件的代码,却完全不记得在函数XSelectInput()里注册所追加的事件类型。这时候,程序员就可能会苦恼的在电脑前坐上个把小时去调试他的程序,疑惑"为什么我的程序不去注意我已经松开了按钮???",最后发现自己只注册了按钮按下的事件却没有注册松开的事件。
接收事件 - 编写事件循环
我们在注册了感兴趣的事件类型后,我们应该进入事件循环并且处理它们。有许多方法来实现事件循环,但比较一般且简单的如下:
/* 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"事件就行了。
暴露事件
暴露事件是程序最经常收到的事件中的一个。它会在以下几种情况下出现:
一个遮住我们的窗口的窗口被移走了,我们的窗口又重新露出来了。
我们的窗口被其它窗口打开了。
我们的窗口第一次被映射到屏幕上。
我们的窗口从最小化中恢复到打开状态。
你应该已经注意到这里有一个隐藏的假设 - 当窗口被遮住时被遮住的内容就丢失了。你也许会提出疑问为什么X服务器不保存那些内容。答案只有一个 - 节省内存。在某一个时刻,屏幕上可能会有大量的窗口,保存它们的内容将会需要非常大量的内存(例如,一个256色的分辨率为400*400的bitmap 图片需要至少160KB的内存来保存它。现在考虑一下有20个窗口的情况,这其中一些可能会有更大的尺寸)。实际上,确实有方法来告诉X服务器在特殊情况下保存窗口的内容,我们会在稍后介绍。
当我们取得了一个"expose"事件,我们应该从XEvent结构的"xexpose"成员中取得事件数据(在我们的例程里,它是"an_event.xexpose")。另外那个结构还包括一些有趣的域:
count
在服务器的事件队列里还有多少暴露事件。这在我们获得了多个暴露事件时非常有用 - 我们通常避免执行重画工作直到确定它是最后一个暴露事件的时候(如直到是0为止)。
Window window
发送该重画事件的窗口的ID(我们的程序为多个窗口注册了事件的时候)。
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;
获取用户输入
就目前来说,用户的输入主要从两个地方过来 - 鼠标和键盘。有各种各样的事件帮助我们来获取用户的输入 - 一个键盘上的键被按下了,一个键盘上的键被松开了,鼠标光标离开了我们的窗口,鼠标光标进入了我们的窗口等等。
鼠标按键事件和松开事件
我们为我们的窗口处理的第一个事件是鼠标按钮时间。为了注册一个这样的事件类型,我们将追加以下的面具
ButtonPressMask
通知我们窗口中的任何一个鼠标键按下动作
ButtonReleaseMask
通知我们窗口中的任何一个鼠标键松开动作
在我们的事件循环中通过switch来检查以下的事件类型
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;
鼠标光标的进入和离开事件
另一个程序通常会感兴趣的事件是,有关鼠标光标进入一个窗口的领域以及离开那个窗口的领域的事件。有些程序利用该事件来告诉用户程序现在在焦点里面。为了注册这种事件,我们将会在函数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反之
键盘焦点
在屏幕上同时会有很多窗口,但同一时间只能有一个窗口获得键盘的使用。X服务器是如何知道哪一个窗口可以发送键盘事件呢?这个是通过使用键盘焦点来实现的。在同一时间只能有一个窗口获得键盘焦点。Xlib函数里存在函数允许程序让指定窗口获得键盘焦点。用户通常使用窗口管理器来为窗口设置焦点(通常是点击窗口的标题栏)。
一旦我们的窗口获得了键盘焦点,每个键的按下和松开都将引起服务器发送事件给我们的程序(如果已经注册了这些事件的类型)。
键盘键按下和松开事件
如果我们程序控制的窗口获得了键盘焦点,它就可以接受按键的按下和松开事件。为了注册这些事件的类型,我们就需要通过函数XSelectInput()来注册下面的面具。
KeyPressMask
通知我们的程序什么时候按键被按下了
KeyPressMask
通知我们的程序什么时候按键被松开了
我们的事件循环中的分支检查将检查以下的事件类型
Window window
事件发送的目标窗口的ID(如果我们为多个窗口注册了事件)
unsigned int keycode
被按下(或松开)的键的编码。这是一些X内部编码,应该被翻译成一个键盘键符号才能方便使用,将会在下面介绍。
int x, y
从窗口的左上坐标算起,事件产生时鼠标光标在窗口中的坐标
Time time
事件被放进队列的时间。可以被用来实现双击的处理
unsigned int state
这个事件发生时鼠标按钮(或是键盘键)被按下的情况 - 如果有的话。这个成员使用按位或的方式来表示
Button1Mask
Button2Mask
Button3Mask
Button4Mask
ShiftMask
LockMask
ControlMask
Mod1Mask
Mod2Mask
Mod3Mask
Mod4Mask
它们的名字是可以扩展的,当第五个鼠标钮被按下时,剩下的属性就指明其它特殊键(例如Mod1一般是"ALT"或者是"META"键)
如我们前面所提到的,按键编码对我们来说是没有什么意义的,它是由连接着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;
你将会发现键盘键符号到物理键编码的转换的方法,程序应该小心的处理各种可能出现的情况。同时我们假设字母键的符号值是连续的。
X事件 - 一个完整的例子
我们将给一个完整的处理事件的例子events.c。给程序创建一个窗口,在上面进行一些绘画工作,然后进入一个事件循环。如果它获得了一个暴露事件 - 它重画整个窗口。如果它获得一个鼠标左键事件,它在鼠标光标出画一个黑点。如果鼠标的中间键被按下了,它在鼠标光标下画一个白点(例如擦出那个点)。应该注意这个图形是改变是如何被处理的。它对使用适当的颜色来绘制并不是很有效。我们需要对颜色的变化作一下记录,这样在下一个暴露事件来到时我们可以用正确的颜色来绘制。我们使用了一个(1000*1000)的巨大矩阵来保存像素。刚开始,所有的单元都被置成0。当画了一个点的时候,我们将该单元置成1。如果该点被画成白色,我们将该单元置成-1。我们不能仅仅把黑色置成0,否则我们刚开始画的将被误删掉。最后,用户按了键盘上任意的按钮,程序将退出。
当运行这个程序时,你也许会注意到移动的事件经常会漏画点。如果鼠标移动的很快,我们将收不到所有的事件。
结果,如果我们要处理这种情况,我们就需要记住上一次收到事件时的鼠标位置,然后应该画一条线在两点之间。
一般绘图程序都是这么做的。