X Window系统是在Unix和类Unix系统上建立图形用户界面的标准工具包和协议,11代表版本11。X Window系统基于C/S(客户端/服务器)模型,与通常意义上的服务器不同的是,X Window的服务器一般是指使用者本地的计算机。X Window只是一套协议,现在在类Unix系统中较常用的实现是X.Org的参考实现。
在Linux平台上,与X Window系统相关的结构和操作都定义在Xlib
中,Xlib
库的的主要作用是窗口管理和事件处理。
1984年,Bob Scheifler 和 Jim Gettys 制订了X的早期原则:
- 除非没有它就无法完成一个真正完整的应用程序,否则不用增加新的功能。
- 决定一个系统不是什么和决定它是什么同样重要。与其去适应整个世界的需要,宁可使得系统可以扩展,如此才能以持续兼容的方式来满足新增需求
- 只有完全没实例时,才会比只有一个实例来的糟。
- 如果问题没完全弄懂,最好不要去解决它
- 如果可以通过10%的工作量得到90%的预期效果,应该用更简单的办法解决。
- 尽量避免复杂性。
- 提供机制而不是策略,有关用户接口的开发实现,交给实际应用者自主。
事实上,已经有很多成熟的对X11进行封装的库,但是我目前并没有使用,我想先从底层原生API开始学习,以后再考虑使用封装库。
程序模型
与Win32程序一样,基于X11的程序也是由一个循环构成,收到消息,处理消息。
while(connected to server)
{
Receive next event
Handle the event
}
Graphical Programming in X basically follows the asynchronous model i.e. “I won’t do anything until you ask me to”.
实体对象
- Display:X Window采用C/S模型,Display可以认为是Client与Server之间的连接的句柄。从Client的角度来讲,可以将Display看成一个Server
- Window:作为一个句柄,代表一个窗口实例。
- Visual:表示特定的视觉信息的组合,如颜色深度,颜色缓冲等。应用程序必须选择当前驱动支持的视觉信息组合。
- XEvent:表示一个窗口事件。
窗口相关操作
创建
既然是C/S模型,那么在进行任何操作之前,都得先创建一个与服务器的连接。使用XOpenDisplay
函数就可以完成该任务,该函数返回一个Display
结构的指针。后面的对于X11的操作基本上都基于这个连接。
接着,可以组织一些窗口的参数,包括父窗口句柄,窗口位置,大小等属性。以这些参数调用XCreateWindow
或XCreateSimpleWindow
就可以创建一个窗口,新创建的窗口默认是未映射的(Unmapped)。
// 建立连接
Display *display = XOpenDisplay(NULL);
// 准备参数
Visual *visual = DefaultVisual(display, 0);
Window parent = DefaultRootWindow(display);
int x = 0, y = 0;
int width = 320, height = 200;
int border_width = 0;
int depth = CopyFromParent;
unsigned int class = CopyFromParent;
unsigned long value_mask = 0;
XSetWindowAttributes *pAttr = NULL;
// 创建窗口
Window window = XCreateWindow(display, parent, x, y, width, height, border_width, depth, class, visual, value_mask, pAttr);
属性
对于一个窗口来说,经常会使用到的属性无外乎窗口标题、大小、位置、颜色深度、可见性等。
-
窗口标题
Display *display; Window window; // 设置标题 XStoreName(display, window, "Hello X11"); char buf[256]; // 获取标题 XFetchName(display, window, &buf);
事实上
XStoreName
不一定会设置窗口标题。查看XStoreName
的手册,有这样的解释:The
XStoreName
function assigns the name passed to window_name to the specified window. A window manager can display the window name in some prominent place, such as the title bar, to allow users to identify windows easily.也就是说,具体的行为是由窗口管理器(Window Manager)定义的。至少在我运行的平台上,该函数可以设置窗口标题。
-
窗口位置
Display *display; Window window; Window root, parent, *child; unsigned int nchild = 0; // 查询window对应的 root window // 一般root window 代表桌面 XQueryTree(display, window, &root, &parent, &child, &nchild); if(child) XFree(child); int x, y; Window cw; // 将window的原点(0, 0)转换到root window坐标系中 XTranslateCoordinates(display, window, root, 0, 0, &x, &y, &cw);
关于窗口位置的获取,有许多人会使用下面的方法:
Display *display; Window window; XWindowAttributes attr; XGetWindowAttributes(display, window, &attr);
代码意思很清楚,获取窗口的属性,而且
attr
中也有x,y
字段。但是实际上这样做是得不到结果的。得到的x,y
一直都是0,0
。目前还没弄明白attr
里面的x,y
字段到底有什么用。 -
窗口大小
Display *display; Window window; XWindowAttributes attr; XGetWindowAttributes(display, window, &attr); int window_widht = attr.width; int window_height = attr.height;
虽然不能获取到位置,但是却可以获取到窗口的大小。需要注意的是,上面获取到的宽高指的是窗口内部,不包括边框大小。
关闭
对于X Window来说,有两种方式可以关闭窗口。
-
用户通过代码关闭窗口
Display *display; Window window; // 待关闭的窗口 XDestroyWindow(display, window);
上面的代码可以关闭一个窗口,但是并没有断开与X Server的连接(Display),所以程序还可以继续发出请求。 如果不需要再进行任何请求了,那么可以通过
XCloseDisplay(display)
来关闭连接。 -
由窗口管理器(Window Manager,简称WM)来关闭窗口 当用户点击窗口上的关闭按钮时,默认情况下会由WM来关闭窗口。需要注意的是,用户所看到的关闭按钮、最大最小化按钮、以及标题栏等都是由WM附加到窗口上的。也就是说,这些不是由X Server创建的,因此对这些按钮的操作响应默认都是由WM来处理的。 对于关闭按钮,大多数的WM会执行如下操作:关闭窗口(XDestroyWindow),断开与X Server的连接(XCloseDisplay)。
对于有些程序,当程序结束运行时,会有关闭窗口、断开连接的操作,如果是先由WM关闭了窗口(比如点击关闭按钮),那么再执行用户编写的关闭窗口操作时,可能会发生错误(因为该窗口已经由WM关闭了)。
很明显,这种关闭窗口的方式不能满足用户的需求,因此,有一种办法可以“介入”WM关闭窗口的流程。 在创建窗口的时候,需要为窗口设置协议,告诉WM,当用户点击关闭按钮时,应该向用户程序发送事件,而不是直接关闭窗口。
/* 创建窗口 */ Display *display; Window window; Atom atom = XInternAtom(display, "WM_DELETE_WINDOW", false); XSetWMProtocols(display, window, &atom, 1);
通过上面的设置,在Window接收
ClientMessage
消息时,可以通过下面的代码来处理点击关闭按钮事件。while(true) { XEvent event; XNextEvent(display, &event); if(event.type == ClientMessage) { if(event.xclient.data.l[0] == atom) { // 执行用户定义的关闭窗口操作 } } }
窗口事件
关于窗口事件的处理,涉及到两个基本步骤:第一步是获取窗口事件,第二步是对事件进行响应。
事件
不同的事件有不同的事件类型(EventType),如:FocusIn
,FocusOut
,KeyPress
等。同一类的事件可以用一个组表示,这个组称为事件掩码(EventMask)(本人理解)。如:KeyPressMask
对应着KeyPress
。
在创建窗口时,可以选择对哪些事件感兴趣,那么应用程序将只会接收到感兴趣的事件。
获取事件
同Windows一样,在X Window系统中,针对与不同的目的,获取事件的方式可以分为阻塞和非阻塞两种。
-
阻塞模式
Display *display; while(true) { XEvent event; // 阻塞,直到有事件发生 XNextEvent(display, &event); /* 响应event */ EventProcess(event); }
与
XNextEvent
相似,同样采用阻塞方式获取事件的函数还有XPeekEvent
,XWindowEvent
,XMaskEvent
,但是这些函数也都有各自的区别。XNextEvent
会将获取到event从事件队列中删除掉。XPeekEvent
不会从事件队列中删除获取到的事件。XWindowEvent
检查特定Window的特定掩码事件,会将获取到event从事件队列中删除掉。XMaskEvent
检查特定掩码的事件,会将获取到event从事件队列中删除掉。 -
非阻塞方式
Display *display; Window window; long event_mask = StructureNotifyMask | ExposureMask; while(true) { XEvent event; // 非阻塞 while(XCheckWindowEvent(display, window, event_mask, &event)) { /* 响应event */ EventProcess(event); } }
同样采用非阻塞方式获取事件的还有
XCheckMaskEvent
,XCheckTypedEvent
,XCheckTypedWindowEvent
,这些消息都会将获取到的事件从事件队列中删除。XCheckWindowEvent
检查特定窗口的特定掩码事件。XCheckMaskEvent
检查特定掩码的事件。XCheckTypedEvent
检查特定类型的事件。XCheckTypedWindowEvent
检查特定窗口的特定类型的事件。
当使用XCheckWindowEvent
来进行非阻塞获取事件时,有一点需要注意的是XCheckWindowEvent无法获取没有Mask的消息,比如 ClientMessage。所以对于像ClientMessage
这样的消息,可能要使用XCheckTypedWindowEvent
来获取。
响应事件
可以根据event.type
来对事件进行区分,并加以响应。代码结构如下。
void EventProcess(XEvent& event)
{
switch(event.type)
{
case KeyPress:
{
/* 处理程序 */
}
break;
case KeyRelease:
{
/* 处理程序 */
}
break;
default:break;
}
}
杂项
- Linux上的OpenGL编程,简单介绍了X11与OpenGL的关联操作。
- Xlib Programming Manaul,Xlib编程手册。
- Xlib Programming,这个网站也可以参考一下。