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的操作基本上都基于这个连接。 接着,可以组织一些窗口的参数,包括父窗口句柄,窗口位置,大小等属性。以这些参数调用XCreateWindowXCreateSimpleWindow就可以创建一个窗口,新创建的窗口默认是未映射的(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),如:FocusInFocusOutKeyPress等。同一类的事件可以用一个组表示,这个组称为事件掩码(EventMask)(本人理解)。如:KeyPressMask对应着KeyPress

在创建窗口时,可以选择对哪些事件感兴趣,那么应用程序将只会接收到感兴趣的事件。

获取事件

同Windows一样,在X Window系统中,针对与不同的目的,获取事件的方式可以分为阻塞非阻塞两种。

  • 阻塞模式

    Display *display;
    while(true)
    {
        XEvent event;
        // 阻塞,直到有事件发生
        XNextEvent(display, &event);
    
        /* 响应event */
        EventProcess(event);
    }
    

    XNextEvent相似,同样采用阻塞方式获取事件的函数还有XPeekEventXWindowEventXMaskEvent,但是这些函数也都有各自的区别。 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);
        }
    }
    

    同样采用非阻塞方式获取事件的还有XCheckMaskEventXCheckTypedEventXCheckTypedWindowEvent,这些消息都会将获取到的事件从事件队列中删除。 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;
    }
}

杂项