初級X編程

緒論

本教程是有關X窗口編程的"would-be"系列教程的第一部。單方面來說,這個教程是沒用的,因爲一個真正的X窗口程序員通常會使用抽象級更高的庫,例如Modif(或者是它的自由版本lesstiff),GTK,QT或者其它類似的庫。但...也許我們應該從某個更易於學習理解的地方開始。因爲,知道它們到底是如何工作的應該永遠不是個壞主意吧。

讀過這個教程後,讀者應該能夠編寫非常簡單的X窗口圖形程序,但不會有具體的應用是用這樣的方法來寫的,對於那些情況,應該用上面提到的那些抽象級更高的庫。

 


X窗口系統的客戶/服務器模式

當初開發X窗口系統的主要目的只有一個,那就是靈活性。這個靈活性的意思就是說一件東西雖然看起來是在這工作,但卻實際上是工作在很遠的地方。因此,較低等級的實現部分就必須提供繪製窗口,處理用戶輸入,畫畫,使用顏色等工作的工具。在這個要求下,決定了系統被分成了兩部分,客戶端和服務器端。客戶端決定做什麼,服務器端執行真正的繪圖和接受用戶的輸入並把它發給客戶端。

這種模式與我們一般習慣的客戶端和服務器端的概念是正好相反的。在我們的情況下,用戶就坐在服務器端控制的機器前,而客戶端這時卻是運行在遠程主機上。服務器端控制着顯示屏,鼠標和鍵盤。一個客戶端也許正連接着服務器端,要求給它畫一個窗口(或者是一堆),並要求服務器端把用戶對它的窗口的輸入傳給它。結果,好幾個客戶端可能連接到了一個服務器端上-有的在運行一個電子郵件軟件,有的在運行一個網頁瀏覽器等。當用戶輸入了指令給窗口,服務器端就會把指令打包成事件傳給控制那個窗口的客戶端,客戶端根據接受到的事件決定幹什麼然後發送請求讓服務器端去畫什麼。

以上介紹的會話都是通過X消息協議傳輸的。該協議是實現在TCP/IP協議上的,它允許在一個網絡裏的客戶端訪問這個網絡裏的任何服務器端。最後,X服務器端可以和客戶端運行在同一臺機器上以獲得性能優化(注意,一個X協議事件可能會達到上百KB),例如使用共享內存,或者使用Unix域socket(在一個Unix系統的兩個進程間創建一個本地通道進行通信的方法)。


圖形用戶接口(GUI)編程-異步編程模式

不像我們通常的令人愉快的程序,一個GUI程序通常使用異步編程模式,也就是下面要介紹的"事件驅動編程"。這個"事件驅動編程"的意思是說程序通常都處於空閒狀態,等待從X服務器發來的事件,等收到了事件,才根據事件做相應的事情。一個事件可能是"用戶在屏幕某處x,y點擊了鼠標左鍵",或者是"你控制的窗口需要被重畫"。因爲程序要回應用戶的請求,同時還需要刷新自己的請求隊列,因此需要程序儘可能使用較短的事件來處理一個事件(例如,作爲一條公認的準則,不能超過200毫秒)。

這也暗示着當然存在需要程序處理很長時間才能完成的事件(例如一個到遠程服務器的網絡連接,或者是連接一個數據庫,或者是不幸的要處理一個超大文件的複製工作)。這都要求程序使用異步方式來處理而不是通常的同步方式。這時候就應該採用各種各樣的異步編程方法來進行這些耗時的工作了,或者乾脆把它們交給一個線程或進程來進行。

根據以上的說明,一個GUI程序就應該像以下的方式來工作:

  1. 進行初始化工作
  2. 連接X服務器
  3. 進行與X相關的初始化工作
  4. 進行循環
    1. 從X服務器那裏接受下一個事件
    2. 根據收到的事件發送各種繪圖指令給X服務器
    3. 如果事件是個退出事件,結束循環
  5. 關閉與X服務器的連接
  6. 進行資源釋放工作

Xlib的基本思想

X協議是非常複雜的,爲了大家不用再辛辛苦苦把時間浪費在實現它上面,就有了一個叫"Xlib"的庫。這個庫提供了訪問任何X服務器的非常底層的手段。因爲X協議已經被標準化了,理論上客戶程序使用任何Xlib的實現都可以訪問任何X服務器。在今天,這看起來可能很瑣碎,但如果回到那個使用字符終端和專有繪圖方法的時代,這應該是一個很大的突破吧。實際上,你很快發現圍繞瘦客戶機,窗口終端服務器等領域會有許多多麼令人興奮的事情。


X顯示

使用XLib的基本思想就是X顯示。它代表了一個打開的到X服務器的連接的結構。它隱藏了一個保存有從X服務器來的事件的隊列,和一個保存客戶程序準備發往服務器的請求隊列。在Xlib裏,這個結構被命名爲顯示"Display"。當我們打開了一個到X服務器的連接,庫就會返回一個指向這個結構的指針。然後,我們就可以使用這個指針來使用Xlib裏各種各樣的函數。


GC - 圖形上下文

當我們進行各種繪圖操作(圖形,文本等)的時候,我們也許會使用許多參數來指定如何繪製,前景,背景,使用什麼顏色,使用什麼字體等等,等等。爲了避免爲每個繪圖函數設置數量驚人的參數,我們使用一個叫"GC"的圖形上下文結構。我們在這個結構裏設置各種繪圖參數,然後傳給繪圖函數就行了。這應當是一個非常方便的方法吧,尤其當我們在進行一連串操作中使用相同的參數時。


對象句柄

當X服務器爲我們創建了各種各樣的對象的時候 - 例如窗口,繪圖區和光標 - 相應的函數就會返回一個句柄。這是一個存在在X服務器空間中的對象的一個標識-而不是在我們的應用程序的空間裏。在後面我們就可以使用Xlib的函數通過句柄來操縱這些對象。X服務器維護了一個實際對象到句柄的映射表。Xlib提供了各種類型來定義這些對象。雖然這些類型實際上只是簡單的整數,但我們應該繼續使用這些類型的名字 - 理由是爲了可移植。


Xlib結構的內存分配

Xlib的接口使用了各種類型的結構。有些可以由用戶直接來分配內存,有些則只能使用專門的Xlib庫函數來分配。在使用庫來分配的情況,庫會生成有適當初始參數的結構。這對大家來說是非常方便的,指定初始值對於不太熟練的程序員來說是非常頭疼的。記住-Xlib想要提供非常靈活的功能,這也就意味着它也會變得非常複雜。提供初始值設置的功能將會幫助那些剛開始使用X的程序員們,同時不會干擾那些高高手們。

 

在釋放內存時,我們使用與申請的同樣方法來釋放(例如,使用free()來釋放malloc()申請的內存)。所以,我們必須使用XFree()來釋放內存。

事件

一個叫"XEvent"的結構來保存從X服務器那裏接受到的事件。Xlib提供了非常大量的事件類型。XEvent包括事件的類型,以及與事件相關的數據(例如在屏幕什麼地方生成的事件,鼠標鍵的事件等等),因此,要根據事件類型來讀取相應的事件裏的數據。這時,XEvent結構使用c語言裏的聯合來保存可能的數據(如果你搞不清楚c的聯合是怎麼回事,那你就得花點時間再讀讀你的教科書了)。結果,我們就可能受到XExpose事件,一個XButton事件,一個XMotion事件等等。


編譯基於Xlib的程序

編譯基於Xlib的程序需要與Xlib庫連接。可以使用下面的命令行:

cc prog.c -o prog -lX11

如果編譯器報告找不到X11庫,可以試着加上"-L"標誌,像這樣:

cc prog.c -o prog -L/usr/X11/lib -lX11

或者這樣(針對使用X11的版本6)

cc prog.c -o prog -L/usr/X11R6/lib -lX11

在SunOs 4 系統上,X的庫被放到了 /usr/openwin/lib

cc prog.c -o prog -L/usr/openwin/lib -lX11

等等,具體情況具體分析


打開,關閉到一個X服務器的連接

一個X程序首先要打開到X服務器的連接。我們需要指定運行X服務器的主機的地址,以及顯示器編號。X窗口允許一臺機器開多個顯示。然而,通常只有一個編號爲"0"的顯示。如果我們想要連接本地的顯示(例如進行顯示的機器同時又是客戶程序運行的機器),我們可以直接使用":0"來連接。現在我們舉例,連接一臺地址是"simey"的機器的顯示,我們可以使用地址"simey:0",下面演示如何進行連接


#include <X11/Xlib.h>   /* defines common Xlib functions and structs. */
.
.
/* this variable will contain the pointer to the Display structure */
/* returned when opening a connection.                             */
Display* display;

/* open the connection to the display "simey:0". */
display = XOpenDisplay("simey:0");
if (display == NULL) {
    fprintf(stderr, "Cannot connect to X server %s/n", "simey:0");
    exit (-1);
}

注意,通常要爲X程序檢查是否定義了環境變量"DISPLAY",如果定義了,可以直接使用它來作爲XOpenDisplay()函數的連接參數。

當程序完成了它的工作且需要關閉到X服務器的連接,它可以這樣做:

XCloseDisplay(display);

這會使X服務器關閉所有爲我們創建的窗口以及任何在X服務器上申請的資源被釋放。當然,這並不意味着我們的客戶程序的結束。


檢查一個顯示的相關基本信息

一旦我們打開了一個到X服務器的連接,我們應該檢查與它相關的一些基本信息:它有什麼樣的屏幕,屏幕的尺寸(長和寬),它支持多少顏色(黑色和白色?灰度級?256色?或更多),等等。我們將演示一些有關的操作。我們假設變量"display"指向一個通過調用XOpenDisplay()獲得的顯示結構。


/* this variable will be used to store the "default" screen of the  */
/* X server. usually an X server has only one screen, so we're only */
/* interested in that screen.                                       */
int screen_num;

/* these variables will store the size of the screen, in pixels.    */
int screen_width;
int screen_height;

/* this variable will be used to store the ID of the root window of our */
/* screen. Each screen always has a root window that covers the whole   */
/* screen, and always exists.                                           */
Window root_window;

/* these variables will be used to store the IDs of the black and white */
/* colors of the given screen. More on this will be explained later.    */
unsigned long white_pixel;
unsigned long black_pixel;

/* check the number of the default screen for our X server. */
screen_num = DefaultScreen(display);

/* find the width of the default screen of our X server, in pixels. */
screen_width = DisplayWidth(display, screen_num);

/* find the height of the default screen of our X server, in pixels. */
screen_height = DisplayHeight(display, screen_num);

/* find the ID of the root window of the screen. */
root_window = RootWindow(display, screen_num);

/* find the value of a white pixel on this screen. */
white_pixel = WhitePixel(display, screen_num);

/* find the value of a black pixel on this screen. */
black_pixel = BlackPixel(display, screen_num);

還有很多其它的宏來幫助我們獲取顯示的信息,你可以在Xlib裏的參考裏找到。另外還有很多相當的函數可以完成相同的工作。


創建一個基本的窗口 - 我們的"Hello world"程序

在我們獲得一些窗口的基本信息之後,我們就可以開始創建我們的第一個窗口了。Xlib支持好幾個函數來創建窗口,它們其中的一個是XCreateSimpleWindow()。這個函數使用很少的幾個參數來指定窗口的尺寸,位置等。以下是它完整的參數列表:

 

 

Display* display
指向顯示結構的指針
Window parent
新窗口的父窗口的ID。
int x
窗口的左上X座標(單位爲屏幕像素)
int y
窗口的左上Y座標(單位爲屏幕像素)
unsigned int width
窗口的寬度(單位爲屏幕像素)
unsigned int height
窗口的高度(單位爲屏幕像素)
unsigned int border_width
窗口的邊框寬度(單位爲屏幕像素)
unsigned long border
用來繪製窗口邊框的顏色
unsigned long background
用來繪製窗口背景的顏色

讓我們創建一個簡單的窗口,它的寬度是屏幕寬的1/3,高度是屏幕高的1/3,背景色是白色,邊框是黑色,邊框的寬度是2個像素。該窗口將會被放置到屏幕的左上角。


/* this variable will store the ID of the newly created window. */
Window win;

/* these variables will store the window's width and height. */
int win_width;
int win_height;

/* these variables will store the window's location. */
int win_x;
int win_y;

/* calculate the window's width and height. */
win_width = DisplayWidth(display, screen_num) / 3;
win_height = DisplayHeight(display, screen_num) / 3;

/* position of the window is top-left corner - 0,0. */
win_x = win_y = 0;

/* create the window, as specified earlier. */
win = XCreateSimpleWindow(display,
                          RootWindow(display, screen_num),
                          win_x, win_y,
                          win_width, win_height,
                          win_border_width, BlackPixel(display, screen_num),
                          WhitePixel(display, screen_num));

事實上我們創建窗口並不意味着它將會被立刻顯示在屏幕上,在缺省情況下,新建的窗口將不會被映射到屏幕上-它們是不可見的。爲了能讓我們創建的窗口能被顯示到屏幕上,我們使用函數XMapWindow():

XMapWindow(win);

如果想察看目前爲止我們所舉的例子的代碼,請參看源程序simple-window.c。你將會發現兩個新的函數 - XFlush() 和XSync()。函數XFlush()刷新所有處於等待狀態的請求到X服務器 - 非常像函數fflush()刷新所有的內容到標準輸出。XSync()也刷新所有處於等待狀態的請求,接着等待X服務器處理完所有的請求再繼續。在一個一般的程序裏這不是必須的(據此你可以發現我們什麼時候只是寫一個一般的程序),但我們現在把它們提出來,嘗試在有或沒有這些函數的情況下程序不同的行爲。


在窗口裏繪製

在窗口裏繪圖可以使用各種繪圖函數 - 畫點,線,圈,矩形,等等。爲了能在一個窗口裏繪圖,我們首先需要定義各種參數 - 如線的寬度,使用什麼顏色,等等。這都需要使用一個圖形上下文(GC)。


申請一個圖形上下文(GC)

如我們已經提到的,一個圖形上下文定義一些參數來使用繪圖函數。因此,爲了繪製不同的風格,我們可以在一個窗口裏使用多個圖形上下文。使用函數XCreateGC()可以申請到一個新的圖形上下文,如以下例(在這段代碼裏,我們假設"display"指向一個顯示結構,"win"是當前創建的一個窗口的ID):


/* this variable will contain the handle to the returned graphics context. */
GC gc;

/* these variables are used to specify various attributes for the GC. */
/* initial values for the GC. */
XGCValues values = CapButt | JoinBevel;
/* which values in 'values' to check when creating the GC. */
unsigned long valuemask = GCCapStyle | GCJoinStyle;

/* create a new graphical context. */
gc = XCreateGC(display, win, valuemask, &values);
if (gc < 0) {
    fprintf(stderr, "XCreateGC: /n");
}

注意,應該考慮一下變量"valuemask"和"values"的角色。因爲一個圖形上下文有數量驚人的屬性,而且通常我們只需要設置裏面的一部分,所以我們需要告訴XCreateGC()什麼屬性是我們需要設置的,這也就是變量"valuemask"的作用。我們接着就可以通過"values"來指定真正的值。在這個例子裏,我們定義了圖形上下文裏的兩個屬性:

 

1 當繪製一個多重部分的線時,線在連接時使用"Bevelian"風格

2 一條線的終端被畫直而不是圓形

其它未指定的屬性GC將會使用缺省值。

一旦我們創建了一個圖形上下文,我們就可以在各種繪圖函數裏用它,我們也可以爲了適應別的函數來變更它的屬性。


/* change the foreground color of this GC to white. */
XSetForeground(display, gc, WhitePixel(display, screen_num));

/* change the background color of this GC to black. */
XSetBackground(display, gc, BlackPixel(display, screen_num));

/* change the fill style of this GC to 'solid'. */
XSetFillStyle(display, gc, FillSolid);

/* change the line drawing attributes of this GC to the given values. */
/* the parameters are: Display structure, GC, line width (in pixels), */
/* line drawing  style, cap (line's end) drawing style, and lines     */
/* join style.                                                        */
XSetLineAttributes(display, gc, 2, LineSolid, CapRound, JoinRound);

如果你想了解全部的圖形上下文的屬性設定方法,請參考函數XCreateGC()的用戶文檔。我們在這裏爲了避免過於複雜只使用了一小部分非常簡單的屬性。


繪圖的基本元素 - 點,線,矩形,圓...

在我們創建了一個GC後,我們就可以通過GC在一個窗口裏使用一系列的Xlib函數,這個函數的集合被稱爲"繪圖的基本元素"。爲了簡便,讓我們通過例子來看一看它們是怎麼工作的。這裏我們假設"gc"是一個前面創建好的GC,"win"是一個已經創建好的窗口的句柄。


/* draw a pixel at position '5,60' (line 5, column 60) of the given window. */
XDrawPoint(display, win, gc, 5, 5);

/* draw a line between point '20,20' and point '40,100' of the window. */
XDrawLine(display, win, gc, 20, 20, 40, 100);

/* draw an arc whose center is at position 'x,y', its width (if it was a     */
/* full ellipse) is 'w', and height is 'h'. Start the arc at angle 'angle1'  */
/* (angle 0 is the hour '3' on a clock, and positive numbers go              */
/* counter-clockwise. the angles are in units of 1/64 of a degree (so 360*64 */
/* is 360 degrees).                                                          */
int x = 30, y = 40;
int h = 15, w = 45;
int angle1 = 0, angle2 = 2.109;
XDrawArc(display, win, gc, x-(w/2), y-(h/2), w, h, angle1, angle2);

/* now use the XDrawArc() function to draw a circle whose diameter */
/* is 15 pixels, and whose center is at location '50,100'.         */
XDrawArc(display, win, gc, 50-(15/2), 100-(15/2), 15, 15, 0, 360*64);

/* the XDrawLines() function draws a set of consecutive lines, whose     */
/* edges are given in an array of XPoint structures.                     */
/* The following block will draw a triangle. We use a block here, since  */
/* the C language allows defining new variables only in the beginning of */
/* a block.                                                              */
  {
    /* this array contains the pixels to be used as the line's end-points. */
    XPoint points[] = {
      {0, 0},
      {15, 15},
      {0, 15},
      {0, 0}
    };
    /* and this is the number of pixels in the array. The number of drawn */
    /* lines will be 'npoints - 1'.                                       */
    int npoints = sizeof(points)/sizeof(XPoint);

    /* draw a small triangle at the top-left corner of the window. */
    /* the triangle is made of a set of consecutive lines, whose   */
    /* end-point pixels are specified in the 'points' array.       */
    XDrawLines(display, win, gc, points, npoints, CoordModeOrigin);
  }

/* draw a rectangle whose top-left corner is at '120,150', its width is */
/* 50 pixels, and height is 60 pixels.                                  */
XDrawRectangle(display, win, gc, 120, 150, 50, 60);

/* draw a filled rectangle of the same size as above, to the left of the  */
/* previous rectangle. note that this rectangle is one pixel smaller than */
/* the previous line, since 'XFillRectangle()' assumes it is filling up   */
/* an already drawn rectangle. This may be used to draw a rectangle using */
/* one color, and later to fill it using another color.                   */
XFillRectangle(display, win, gc, 60, 150, 50, 60);

 

如果你覺得已經抓住使用這些函數的要點,那我們後面的說明就會變得簡單。我們將提到其它一些使用相同方法的函數。例如,XFillArc()使用與XDrawArc()相同的參數,但它只畫一個圓的內部(相似的,XFillRectangle()只畫一個矩形區的內部)。另外還有一個函數XFillPolygon()負責填充一個多邊形的內部區域。它使用的參數差不多與XDrawLines()相同。但是要注意,如果提供在矩陣裏的最後一個參數所代表的點與第一個點不在同一個位置上,函數XFillPolygon()會添加一條虛擬的線來連接那兩個點。這兩個函數的另外一個不同是,XFillPolygon()使用一個附加的參數,形狀,這個參數可以幫助X服務器來優化它的繪圖工作。你可以在手冊裏找到詳細的內容。以上的函數還存在它們的複數繪製版本,命名爲XFillArcs()和XFillRectangles()。

 

以上的說明請參看程序 simple-drawing.c

 


X 事件

在一個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"事件就行了。


暴露事件

暴露事件是程序最經常收到的事件中的一個。它會在以下幾種情況下出現:

  1. 一個遮住我們的窗口的窗口被移走了,我們的窗口又重新露出來了。
  2. 我們的窗口被其它窗口打開了。
  3. 我們的窗口第一次被映射到屏幕上。
  4. 我們的窗口從最小化中恢復到打開狀態。

你應該已經注意到這裏有一個隱藏的假設 - 當窗口被遮住時被遮住的內容就丟失了。你也許會提出疑問爲什麼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 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

如我們前面所提到的,按鍵編碼對我們來說是沒有什麼意義的,它是由連接着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,否則我們剛開始畫的將被誤刪掉。最後,用戶按了鍵盤上任意的按鈕,程序將退出。

當運行這個程序時,你也許會注意到移動的事件經常會漏畫點。如果鼠標移動的很快,我們將收不到所有的事件。結果,如果我們要處理這種情況,我們就需要記住上一次收到事件時的鼠標位置,然後應該畫一條線在兩點之間。一般繪圖程序都是這麼做的。


字體結構

爲了支持靈活的字體,定義了一個字體結構,類型XFontStruct。該結構被用來包含一個字體的信息,被用來幫助一些函數處理字體的選擇和文本繪圖。


裝載一個字體

作爲繪製文本的第一步,我們使用字體裝載函數,例如XLoadQueryFont()。該函數要求X服務器裝載給定名字的字體。如果字體被發現了,服務器裝載那個字體,返回一個XFontStruct結構的指針。如果字體沒有被發現,或者是裝載失敗了,返回一個值NULL。每個字體也許會有兩個名字。一個是長字符串,指明瞭字體的全部屬性(字體族,字體尺寸,斜體/黑體/是否有下劃線等等)。另一個是短的別名,爲各自的服務器所配置。作爲一個例子,我們嘗試裝載"*-helvetica-*-12-*"字體:

 


/* this pointer will hold the returned font structure. */
XFontStruct* font_info;

/* try to load the given font. */
char* font_name = "*-helvetica-*-12-*";
font_info = XLoadQueryFont(display, fond_name);
if (!font_info) {
    fprintf(stderr, "XLoadQueryFont: failed loading font '%s'/n", font_name);
}

給一個圖形上下文分配字體

在我們裝載了字體後,我們需要把它分配給一個GC。假設一個叫"gc"的GC已經存在了,下面是我們如何做:


XSetFont(display, gc, font_info->fid);

"fid"領域是一個XFontStruct結構用來爲各種請求識別各種裝載的字體。


在一個窗口中繪製文本

我們一旦爲我們的GC裝載了字體,我們就可以使用例如函數XDrawString(),在我們的窗口裏繪製文本。該函數可以在窗口裏的一個給定位置裏繪製文本。給定的位置將是從被繪製的文本的左下算起,下面是它的例子:


/* assume that win_width contains the width of our window, win_height        */
/* contains the height of our window, and 'win' is the handle of our window. */

/* some temporary variables used for the drawing. */
int x, y;

/* draw a "hello world" string on the top-left side of our window. */
x = 0;
y = 0;
XDrawString(display, win, gc, x, y, "hello world", strlen("hello world"));

/* draw a "middle of the road" string in the middle of our window. */
char* text_string = "middle of the road";
/* find the width, in pixels, of the text that will be drawn using */
/* the given font.                                                 */
int string_width = XTextWidth(font_info, text_string, strlen(text_string));
/* find the height of the characters drawn using this font.        */
int fond_height = font_info->ascent + font_info->descent;
x = (win_width - string_width) / 2;
y = (win_width - font_height) / 2;
XDrawString(display, win, gc, x, y, "hello world", strlen("hello world"));

以下的說明應該可以使程序更清楚:

  • 函數XTextWidth()被用來預測字符串的長度,當它使用指定字體進行繪製時。它被用來檢查那裏是開始那裏是結束使它看起來佔據着窗口的中央
  • 一個字體的兩個名字爲"ascent"和"descent"的屬性用來指定字體的高。基本上,一個字體的字符是畫在一條假象的橫線上的。一些字符被畫在橫線上面,一些畫在下面。最高的字符是被畫在"font_info->ascent"線上的,最低的部分則在"font_info->descent"線下面。因此,這兩個值得和指明瞭字體的高度。

上面的源程序可以參考文件simple-text.c

 

 

 

 

窗口們的組織體系

當窗口們被顯示在X服務器上時,它們通常按照一定組織體系來排序 - 每個窗口可以有子窗口,每個子窗口又可以有自己的子窗口。讓我們來查看這個組織體系的一些特性,看看它們是如何來影響例如繪畫和事件等處理。


根窗口,父窗口和子窗口

每一個屏幕上都有一個根窗口。根窗口總是佔據整個屏幕尺寸。這個窗口無法被銷燬,改變尺寸或者圖標化。當一個應用程序創建了一些窗口,它先創建至少一個頂層窗口。在被映射到屏幕上後,這個窗口成爲一個根窗口的直接子窗口。這個窗口在被映射到屏幕上之前,窗口管理器被告知什麼發生了,然後,窗口管理器獲得特權成爲新頂層窗口的"父親"。這通常被用來增加一個會包含新窗口的窗口和繪製框架,標題欄,系統菜單等。

一旦一個頂層窗口(當然它實際上不是一個頂層窗口,因爲窗口管理器已經成爲它的父窗口了)被創建了,應用程序可以在它裏面創建它的子窗口。一個子窗口只能在它的父窗口裏顯示 - 如果試圖把它移動到外面,出去的部分將被父窗口的邊框給切掉。任何窗口都可以包含一個以上的子窗口,在這種情況下,這些子窗口將被放置在應用的內部棧上。當一個頂層窗口被打開 - 它的所有子窗口也將隨着它被打開。

 

以下例子演示如何在一個給定的叫"win"的窗口裏打開一個子窗口。

 

Lets see how to create a child window inside a given window 'win'.


/* this variable will store the handle of the newly created child window. */
Window child_win;

/* these variables will store the window's width and height. */
int child_win_width = 50;
int child_win_height = 30;

/* these variables will store the window's location.         */
/* position of the child window is top-left corner of the    */
/* parent window. - 0,0.                                     */
int child_win_x = 0;
int child_win_y = 0;

/* create the window, as specified earlier. */
child_win = XCreateSimpleWindow(display,
                                win,
                                child_win_x, child_win_y,
                                child_win_width, child_win_height,
                                child_win_border_width,
                                BlackPixel(display, screen_num),
                                WhitePixel(display, screen_num));

 


事件傳遞

先前我們已經討論了事件傳遞 - 如果一個窗口收到了一個它不處理的事件 - 它就把該事件發到它的父窗口去。如果那個父窗口也不處理該事件 - 那個父窗口就把該事件發到它的父窗口上去,接下來依此類推。這種行爲對一個簡單的Xlib程序是沒什麼用的,但對於抽象級更高的繪圖庫是有用的。這些抽象級更高的繪圖庫通常把某個特定窗口的事件聯繫到一個函數上去。在這種情況下,發送事件到特定的窗口並用適當的函數來處理就非常有用。


與窗口管理器進行交互

在我們察看了如何創建和繪製窗口之後,我們回過頭來看一下我們的窗口是如何與它們的環境 - 整個屏幕和其它窗口進行交互的。首先,我們的程序需要與窗口管理器進行交互。窗口管理器有責任裝飾被繪製的窗口(例如增加框架,一個圖標化的按鈕,一個系統菜單,一個標題欄),同時在窗口被圖標化時繪製圖標。它還管理屏幕裏的窗口排列順序以及其它可管理的任務。我們需要給它各種提示以讓它以我們需要的方式來對待我們的窗口。


窗口屬性

許多與窗口管理器交流的參數都通過叫"properties"的數據來傳遞。X服務器把這些屬性貼到各種窗口上,同時把它們存儲成一種可以被各種架構的系統所能讀取的格式(記住,一個X客戶程序可能運行在一臺遠程主機上)。屬性可以是各種類型 - 數字,字符串,等等。大部分的窗口管理器提示函數使用文本屬性。一個叫XStringListToTextProperty()的函數可以把C語言的字符串轉換成X文本屬性,轉換後的結果就可以傳給各色Xlib函數。以下是一個例子:

 


/* This variable will store the newly created property. */
XTextProperty window_title_property;

/* This is the string to be translated into a property. */
char* window_title = "hello, world";

/* translate the given string into an X property. */
int rc = XStringListToTextProperty(&window_title,
                                   1,
                                   &window_title_property);
/* check the success of the translation. */
if (rc == 0) {
    fprintf(stderr, "XStringListToTextProperty - out of memory/n");
    exit(1);
}

 

函數XStringListToTextProperty()接收一個C字符串矩陣(在我們的例子裏只有一個)和一個指向XTextProperty型變量的指針爲參數,合併C字符串裏的屬性把值傳到XTextProperty型變量裏。成功時它返回一個非0值,失敗時返回0(例如,沒有足夠的內存來完成操作)。


設置窗口名字和圖標名字

我們需要做的第一件事就是給我們的窗口設置名字。使用函數XSetWMName()。窗口管理器也許會把這個名字顯示在窗口標題欄或是在任務欄上。該函數接受3個參數:一個指向顯示的指針,一個窗口句柄,和一個包含有我們設置的名字的XTextProperty變量。下面是我們如何做的:


/* assume that window_title_property is our XTextProperty, and is */
/* defined to contain the desired window title.                   */
XSetWMName(display, win, &window_title_property);

爲了設置我們的窗口的圖標化名字,我們將用相同的方式使用函數XSetWMIconName()。


/* this time we assume that icon_name_property is an initialized */
/* XTextProperty variable containing the desired icon name.      */
XSetWMIconName(display, win, &icon_name_property);

設置滿意的窗口尺寸

在各種情況下,我們希望讓窗口管理器知道我們指定的窗口尺寸以及只允許用戶在我們的限定下改變窗口尺寸。例如,一個終端窗口(像xterm),我們總是要求我們的窗口可以包含全部的行和列,因此我們就不能從中間截斷我們的顯示。在其它情況下,我們不希望我們的窗口可以被改變尺寸(像絕大部分的對話框窗口),等等。我們可以依賴窗口管理器的這個尺寸信息,雖然它可能被簡單的忽視掉。我們首先需要創建一個數據結構來包含該信息,填充必要的數據,然後使用函數XSetWMNormalHints()。下面是如何操作:


/* pointer to the size hints structure. */
XSizeHints* win_size_hints = XAllocSizeHints();
if (!win_size_hints) {
    fprintf(stderr, "XAllocSizeHints - out of memory/n");
    exit(1);
}

/* initialize the structure appropriately. */
/* first, specify which size hints we want to fill in. */
/* in our case - setting the minimal size as well as the initial size. */
win_size_hints->flags = PSize | PMinSize;
/* next, specify the desired limits.                             */
/* in our case - make the window's size at least 300x200 pixels. */
/* and make its initial size 400x250.                            */
win_size_hints->min_width = 300;
win_size_hints->min_height = 200;
win_size_hints->base_width = 400;
win_size_hints->base_height = 250;

/* pass the size hints to the window manager. */
XSetWMNormalHints(display, win, win_size_hints);

/* finally, we can free the size hints structure. */
XFree(win_size_hints);

請查看你的手冊來獲取尺寸提示的完整信息。


設置各種窗口管理器提示

使用函數XSetWMHints()還可以設置許多其它的窗口管理器提示。該函數使用一個XWMHints結構來傳遞參數給窗口管理器。下面是例子:


/* pointer to the WM hints structure. */
XWMHints* win_hints = XAllocWMHints();
if (!win_hints) {
    fprintf(stderr, "XAllocWMHints - out of memory/n");
    exit(1);
}

/* initialize the structure appropriately. */
/* first, specify which hints we want to fill in. */
/* in our case - setting the state hint as well as the icon position hint. */
win_hints->flags = StateHint | IconPositionHint;
/* next, specify the desired hints data.                         */
/* in our case - make the window's initial state be iconized,    */
/* and set the icon position to the top-left part of the screen. */
win_hints->initial_state = IconicState;
win_hints->icon_x = 0;
win_hints->icon_y = 0;

/* pass the hints to the window manager. */
XSetWMHints(display, win, win_hints);

/* finally, we can free the WM hints structure. */
XFree(win_hints);

請查閱手冊以獲取全部選項的詳細信息。


設置一個程序的圖標

在用戶圖標化了我們的程序的時候,爲了讓窗口管理器能爲我們的程序設置一個圖標,我們使用上面提到的函數XSetWMHints。但是,首先我們需要創建一個包含有圖標數據的像素圖。X服務器使用像素圖來操作圖片,將在後面介紹它的詳細使用。在這裏,我們只是向你展示如何爲你的程序設置圖標。我們假設你已經得到了一個X bitmap格式的圖標文件。教程爲了方便提供了一個圖標文件"icon.bmp" ,下面是代碼:


/* include the definition of the bitmap in our program. */
#include "icon.bmp";

/* pointer to the WM hints structure. */
XWMHints* win_hints;

/* load the given bitmap data and create an X pixmap containing it. */
Pixmap icon_pixmap = XCreateBitmapFromData(display,
                                           win,
                                           icon_bitmap_bits,
                                           icon_bitmap_width,
                                           icon_bitmap_height);
if (!icon_pixmap) {
    fprintf(stderr, "XCreateBitmapFromData - error creating pixmap/n");
    exit(1);
}

/* allocate a WM hints structure. */
win_hints = XAllocWMHints();
if (!win_hints) {
    fprintf(stderr, "XAllocWMHints - out of memory/n");
    exit(1);
}

/* initialize the structure appropriately. */
/* first, specify which size hints we want to fill in. */
/* in our case - setting the icon's pixmap. */
win_hints->flags = IconPixmapHint;
/* next, specify the desired hints data.           */
/* in our case - supply the icon's desired pixmap. */
win_hints->icon_pixmap = icon_pixmap;

/* pass the hints to the window manager. */
XSetWMHints(display, win, win_hints);

/* finally, we can free the WM hints structure. */
XFree(win_hints);

你可以使用程序例如"xpaint"來創建使用X bitmap格式的文件。

我們提供文件simple-wm-hints.c來總結這一節,這段程序包括創建一個窗口,設置窗口管理器提示爲在上面顯示,以及一個簡單的事件循環。它允許用戶調整參數以察看提示是如何影響程序的行爲的。這可以幫助你瞭解X程序的可移植性。


簡單窗口操作

對我們的窗口,我們可以做更多的一些事情。例如,改變它們的尺寸,打開或關閉它們,圖標化它們等。Xlib提供了一系列函數來完成上面提到的功能。


映射和解除一個窗口的映射

首先我們對窗口作的一對操作是映射它到屏幕上去和解除它的映射。映射一個窗口的操作將會使一個窗口顯示在屏幕上,如我們在簡單窗口程序例子裏所看到的。解除映射操作將會把窗口從屏幕裏移除出去(雖然作爲一個邏輯結點它仍然在X服務器裏)。這個可以提供產生窗口被隱藏(映射解除)和再顯示(映射)的效果。例如,我們的程序裏有一個對話框,我們不需要每次在需要它顯示的時候都重新創建一個窗口,我們只是以映射解除的狀態創建一次,在用戶需要的時候簡單的把它映射到屏幕上去就行了。這比每一次都創建它和銷燬它要快多了,當然,這需要在客戶端和服務器端同時使用更多的內存。

你應該還記得映射操作是使用函數XMapWindow()。映射解除操作是使用函數XUnmapWindow(),下面是如何使用它們:


/* make the window actually appear on the screen. */
XMapWindow(display, win);

/* make the window hidden. */
XUnmapWindow(display, win);

除非整個窗口被其它窗口給覆蓋了,一個暴露事件將在映射操作後發給應用程序。


在屏幕移動一個窗口

我們想做的另一個操作是在屏幕裏移動窗口。使用函數XMoveWindow()可以完成這個操作。它接受窗口的新座標,使用的方法和函數XCreateSimpleWindow()是一樣的。一下是調用的例子:


/* move the window to coordinates x=400 and y=100. */
XMoveWindow(display, win, 400, 100);

注意當窗口移動的時候,窗口的部分可能後被遮住或被重新暴露,這樣我們就可能會收到暴露事件。


改變窗口尺寸

接下來我們要做的是改變一個窗口的尺寸。使用函數XResizeWindow()可以完成這個操作:


/* resize the window to width=200 and height=300 pixels. */
XResizeWindow(display, win, 200, 300);

我們可以合併移動和改變尺寸操作爲一個操作,使用函數XMoveResizeWindow():


/* move the window to location x=20 y=30, and change its size */
/* to width=100 and height=150 pixels.                        */
XMoveResizeWindow(display, win, 20, 30, 100, 150);

改變窗口們的棧順序 - 提升和降低

到目前爲止我們已經改變了一個單獨窗口的許多屬性。接下來我們將看看窗口之間的屬性。其中一個就是它們的棧屬性。也就是說,窗口是如何在屏幕上排列的。最前面的窗口我們說它是在棧頂,最後面的窗口我們說它是在棧底。下面演示我們如何改變窗口的棧順序:


/* move the given window to the top of the stack. */
XRaiseWindow(display, win1);

/* move the given window to the bottom of the stack. */
XLowerWindow(display, win1);

圖標化和恢復一個窗口

在這裏我們將要講解的最後一個操作就是如何把一個窗口變換成圖標狀態和恢復它。使用函數XIconifyWindow()來把一個窗口變換成圖標狀態,使用函數XMapWindow()來恢復它。爲了幫助理解爲什麼圖標化函數沒有一個對應的反函數,我們必須理解當一個窗口被圖標化時,實際發生的事情是那個窗口被解除映射了,而它的圖表被映射了。結果,如果想使哪個窗口在出現,我們只需要簡單的映射它一下就行了。圖標實際上是另一個窗口,只不過它與我們的窗口有非常強的聯繫關係。下面演示如何圖標化一個窗口並恢復它:


/* iconify our window. Make its icon window appear on the same */
/* screen as our window (assuming we created our window on the */
/* default screen).                                            */
XIconifyWindow(display, win, DefaultScreen(display));

/* de-iconify our window. the icon window will be automatically */
/* unmapped by this operation.                                  */
XMapWindow(display, win);

獲得一個窗口的信息

與可以爲窗口設置許多屬性相同,我們也可以要求X服務器提供這些屬性的值。例如,我們可以檢查窗口現在在屏幕裏什麼位置,當前尺寸,是否被映射了等等。函數XGetWindowAttributes()可以幫助我們獲取那些信息:


/* this variable will contain the attributes of the window. */
XWindowAttributes win_attr;

/* query the window's attributes. */
Status rc = XGetWindowAttributes(display, win, &win_attr);

結構體XWindowAttributes包含了很多數據域,下面是它的一部分:

int x, y;
窗口的位置,相對於它的父窗口。
int width, height;
窗口的寬和高(單位,像素)。
int border_width
窗口的邊框寬度
Window root;
根窗口,也就是我們的窗口在那個窗口裏被顯示了。

這個函數有些問題,就是它返回的是相對於父窗口的位置。這對一些窗口的操作(例如XMoveWindow)是沒有什麼意義的。爲了解決這個問題,我們需要使用兩步的操作。首先,我們找出窗口的父窗口的ID。然後我們在使用它來確定窗口相對於屏幕的座標。我們使用兩個前面沒有介紹的函數來完成這個計算,XQueryTree()和XTranslateCoordinates()。這兩個函數的功能超出了我們的需要,所以我們只關注我們需要的:


/* these variables will eventually hold the translated coordinates. */
int screen_x, screen_y;
/* this variable is here simply because it's needed by the          */
/* XTranslateCoordinates function below. For its purpose, see the   */
/* manual page.                                                     */
Window child_win;

/* this variable will store the ID of the parent window of our window. */
Window parent_win;
/* this variable will store the ID of the root window of the screen    */
/* our window is mapped on.                                            */
Window root_win;
/* this variable will store an array of IDs of the child windows of    */
/* our window.                                                         */
Window* child_windows;
/* and this one will store the number of child windows our window has. */
int num_child_windows;

/* finally, make the query for the above values. */
XQueryTree(display, win,
           &root_win,
           &parent_win,
           &child_windows, &num_child_windows);

/* we need to free the list of child IDs, as it was dynamically allocated */
/* by the XQueryTree function.                                            */
XFree(child_windows);

/* next, we make the coordinates translation, from the coordinates system */
/* of the parent window, to the coordinates system of the root window,    */
/* which happens to be the same as that of the screen, since the root     */
/* window always spans the entire screen size.                            */
/* the 'x' and 'y' values are those previously returned by the            */
/* XGetWindowAttributes function.                                         */
XTranslateCoordinates(display, parent_win, root_win,
                      win_attr.x, win_attr.y, &screen_x, &screen_y,
                      &child_win);

/* at this point, screen_x and screen_y contain the location of our original */
/* window, using screen coordinates.                                         */

你可以看到Xlib有時候會讓我們處理問題時變得很麻煩。

以上的內容可以參考例子window-operations.c 程序。


使用顏色來繪製彩虹

到目前爲止,我們的繪製操作都只使用了黑白兩色。現在我們就看看如何使用豐富的顏色來繪製。


顏色映射

首先,是沒有完全足夠的顏色的。屏幕控制器同時只能支持有限的顏色。因此,一個應用程序不能只是要求使用顏色“輕紫紅”就盼望這個顏色能被支持。每個應用分配它自己所需要的顏色,如果全部的16或256色入口都已經在使用了,下一個顏色的分配就會失敗。

結果,就介紹使用“一個顏色映射”。一個顏色映射是一個與屏幕控制器同時可以支持的顏色數相同的表。每個表中的節點都爲每種顏色包含一個RGB(紅,綠和藍)。當一個應用想在屏幕上繪製時,它並不指定使用什麼顏色,而是指定使用映射表裏那一個節點,因此,改變表裏某個節點的值將會改變程序繪製的顏色。

爲了能讓程序員使用他想要的顏色來繪製,提供了顏色映射分配函數。你可以要求分配一個顏色映射節點來對應一個RGB值,然後一個節點的索引值返回給你。如果表滿了,這個操作將會失敗。你可以接下來要求一個相近的顏色來滿足你的需要。這意味着一個相近的顏色將會被繪製到屏幕上去。

在當今的X服務器使用的現代顯示器上,一般都可以支持上百萬的顏色,上面那個限制也許看起來挺傻的,但是記住還有很多古舊的顯示卡和顯示器在被使用。使用顏色映射,你可以不必考慮服務器的屏幕硬件細節就可以使用它們。在一個支持上百萬的顯示器上,任何顏色的分配請求都應該會成功。在一個職能支持有限顏色的顯示器上可能會使用一個相近顏色來代替你的要求,這可能不好看,但你的程序仍然能工作。


分配和釋放顏色映射

當你使用Xlib繪製的時候,你可以選擇屏幕的標準顏色映射,或者爲你的窗口分配一個新的顏色映射。在後一種情況下,每次鼠標移動到你的窗口上時,你窗口的顏色映射都將替換缺省的屏幕映射,然後你就會看到屏幕花一下其它的窗口顏色也改變了。實際上,這和你在使用“-install”選項運行X應用時效果一樣。

系統定義了宏DefaultColormap來獲取屏幕的標準顏色映射:


Colormap screen_colormap = DefaultColormap(display, DefaultScreen(display));

上面的調用將會返回第一個屏幕的缺省顏色映射的句柄(再多餘的提醒一下,一個X服務器可以同時支持數個不同的屏幕,每個屏幕都可以有自己的資源)。

另一個選項,分配一個顏色映射,像下面這樣工作:


/* first, find the default visual for our screen. */
Visual* default_visual = DefaultVisual(display, DefaultScreen(display));
/* this creates a new color map. the number of color entries in this map */
/* is determined by the number of colors supported on the given screen.  */
Colormap my_colormap = XCreateColormap(display,
                                       win,
                                       default_visual,
                                       AllocNone);

注意,window參數是用來只允許X服務器爲指定屏幕分配顏色映射。我們接下來可以使用分配來的顏色映射給同一個屏幕裏的任意一個窗口使用。


分配和釋放顏色節點

一旦我們獲得了顏色映射的訪問,我們就可以開始分配顏色。使用函數XAllocNameColor()和XAllocClor()來完成這個工作。首先函數XAllocNameColor()獲得顏色的名字(例如"紅","藍","棕"等等)然後獲得能使用的實際相近顏色。函數XAllocColor()訪問RGB顏色。兩個函數都使用XColor結構,它有以下的一些數據域:

unsigned long pixel
顏色映射節點的索引。
unsigned short red
RGB顏色值的紅色部分。
unsigned short green
RGB顏色值的綠色部分。
unsigned short blue
RGB顏色值的藍色部分。

下面是使用的例子:


/* this structure will store the color data actually allocated for us. */
XColor system_color_1, system_color_2;
/* this structure will store the exact RGB values of the named color.  */
/* it might be different from what was actually allocated.             */
XColor exact_color;

/* allocate a "red" color map entry. */
Status rc = XAllocNamedColor(display,
                             screen_colormap,
                             "red",
                             &system_color_1,
                             &exact_color);
/* make sure the allocation succeeded. */
if (rc == 0) {
    fprintf(stderr,
            "XAllocNamedColor - allocation of 'red' color failed./n");
}
else {
    printf("Color entry for 'red' - allocated as (%d,%d,%d) in RGB values./n",
           system_color_1.red, system_color_1.green, system_color_1.blue);
}

/* allocate a color with values (30000, 10000, 0) in RGB. */
system_color_2.red = 30000;
system_color_2.green = 10000;
system_color_2.blue = 0;
Status rc = XAllocColor(display,
                        screen_colormap,
                        &system_color_2);
/* make sure the allocation succeeded. */
if (rc == 0) {
    fprintf(stderr,
            "XAllocColor - allocation of (30000,10000,0) color failed./n");
}
else {
    /* do something with the allocated color... */
    .
    .
}

使用一個眼色繪製

我們在分配了希望的顏色之後,我們可以使用它們繪製文本或圖形。爲了達到目的,我們需要把獲得的顏色設置給一些GC(圖形上下文)作爲前景色和背景色,然後使用設置好的GC來進行繪製。使用函數XSetForeground()和XSetBackground()來進行,如下:


/* use the previously defined colors as the foreground and background  */
/* colors for drawing using the given GC. assume my_gc is a handle to */
/* a previously allocated GC.                                         */
XSetForeground(display, my_gc, screen_color_1.pixel);
XSetForeground(display, my_gc, screen_color_2.pixel);

如你所見,這個是個使用的例子。實際的繪製工作使用我們以前介紹的繪圖函數。注意,爲了使用各種顏色完成繪製工作,我們可以使用兩種方法。我們可以在調用繪圖函數前改變GC的值,也可以使用代表不同顏色的GC。由你自己根據情況使用哪種方法。注意,使用多個GC降消耗X服務器額外的資源,但這樣可以使你的代碼顯的更緊湊。

作爲使用顏色繪製的例子,請察看例子程序color-drawing.c 。這是程序simple-drawing.c 的一個拷貝,我們只是添加了顏色的部分在裏面。


X Bitmaps和Pixmaps

一個被稱爲多媒體的程序所有作的一件事情就是顯示圖片。在X的世界裏,使用bitmaps和pixmaps來實現這個功能。在爲我們的程序設置圖標的介紹裏我們已經使用了它們。現在讓我們進一步對它們進行研究,看看在一個窗口裏是如何繪製它們的。

在進入之前有一點需要注意,Xlib不能處理許多現在流行的圖片格式,例如gif,jpeg或tiff。這些被留給了應用程序(或者是一些圖形處理庫)來轉換成X服務器可以接受的x bitmaps和x pixmaps。


什麼是一個X Bitmap?和X Pixmap?

一個Xbitmap是一個有X窗口系統定義的雙色圖形格式。在保存在一個文件裏的時候,bitmap數據看起來就像一段C源程序。它包括定義圖片寬高的變量,一個包含比特值得矩陣(矩陣的尺寸=寬*高),和一個可選的熱點位置(將會在後面的鼠標光標的部分進行解釋)。

一個X pixmap是一個X窗口系統在內存裏保存圖像的格式。該格式可以儲存黑色和白色的圖片(例如X bitmaps)也可以保存帶顏色的圖片。這實際上是X協議唯一能支持的圖片格式,任何圖片如果想被顯示在屏幕上前都要先被轉換成這種格式。

實際上,一個X pixmap應該被認爲是一個沒有被繪製到屏幕上的窗口。許多在窗口上的圖形操作也可以工作於X pixmap,只不過使用X pixmap ID來代替窗口ID。事實上,如果你查看手冊,你會發現所有的這些函數都是接受一個叫"可畫"參數而不是一個窗口參數。因爲這兩種類型都是可畫的,它們都可以被用在例如函數XDrawArc(),XDrawText()等等。


從一個文件裏讀取一個Bitmap

在圖標的程序裏,我們已經看過如何從一個文件裏把一個Bitmap裝載到內存裏。前面我們使用的方法是使用C預編譯器"#include"來進行,下面我們看看如何直接從文件裏讀取。


/* this variable will contain the ID of the newly created pixmap.    */
Pixmap bitmap;

/* these variables will contain the dimensions of the loaded bitmap. */
unsigned int bitmap_width, bitmap_height;

/* these variables will contain the location of the hot-spot of the   */
/* loaded bitmap.                                                    */
int hotspot_x, hotspot_y;

/* this variable will contain the ID of the root window of the screen */
/* for which we want the pixmap to be created.                        */
Window root_win = DefaultRootWindow(display);

/* load the bitmap found in the file "icon.bmp", create a pixmap      */
/* containing its data in the server, and put its ID in the 'bitmap'  */
/* variable.                                                          */
int rc = XReadBitmapFile(display, root_win,
                         "icon.bmp",
                         &bitmap_width, &bitmap_height,
                         &bitmap,
                         &hotspot_x, &hotspot_y);
/* check for failure or success. */
switch (rc) {
    case BitmapOpenFailed:
        fprintf(stderr, "XReadBitmapFile - could not open file 'icon.bmp'./n");
        break;
    case BitmapFileInvalid:
        fprintf(stderr,
                "XReadBitmapFile - file '%s' doesn't contain a valid bitmap./n",
                "icon.bmp");
        break;
    case BitmapNoMemory:
        fprintf(stderr, "XReadBitmapFile - not enough memory./n");
        break;
    case BitmapSuccess:
        /* bitmap loaded successfully - do something with it... */
        .
        .
        break;
}

注意對於給定的bitmap參數"root_win"什麼作用也不起 - 讀取的bitmap並不與這個窗口相聯繫。這個窗口句柄只是被用來指明bitmap所使用的屏幕。這是非常重要的,bitmap必須支持與屏幕相同數量的顏色,這樣它才能發揮作用。


在一個窗口裏繪製圖形

一旦我們獲得了從bitmap裏生成的pixmap的句柄,我們就可以使用函數XCopyPlane()把它繪製到窗口裏。這個函數可以幫助我們指定什麼(一個窗口,甚至另一個pixmap)可以畫到這個pixmap上去。


/* draw the previously loaded bitmap on the given window, at location   */
/* 'x=100, y=50' in that window. we want to copy the whole bitmap, so   */
/* we specify location 'x=0, y=0' of the bitmap to start the copy from, */
/* and the full size of the bitmap, to specify how much of it to copy.  */
XCopyPlane(display, bitmap, win, gc,
          0, 0,
          bitmap_width, bitmap_height,
          100, 50,
          1);

如你所見,我們可以只拷貝制定的矩形區而不是整個pixmap。另外還需要注意的是函數XCopyPlane的最後一個參數(那個結尾的"1")。該參數指定了那個平面被從源裏拷貝出來。對於bitmaps,我們通常只拷貝平面1。到了下面我們討論顏色深度的時候你就能確切的明白爲什麼這麼做。


創建一個Pixmap

有時我們需要創建一個沒有初始化的pixmap,這樣我們可以接下來在它上面繪製。這對圖像繪製程序是非常有用的。另外,這對讀取各種格式的圖像數據也是非常有用的。


/* this variable will store the handle of the newly created pixmap. */
Pixmap pixmap;

/* this variable will contain the ID of the root window of the screen */
/* for which we want the pixmap to be created.                        */
Window root_win = DefaultRootWindow(display);

/* this variable will contain the color depth of the pixmap to create. */
/* this 'depth' specifies the number of bits used to represent a color */
/* index in the color map. the number of colors is 2 to the power of   */
/* this depth.                                                         */
int depth = DefaultDepth(display, DefaultScreen(display));

/* create a new pixmap, with a width of 30 pixels, and height of 40 pixels. */
pixmap = XCreatePixmap(display, root_win, 30, 40, depth);

/* just for fun, draw a pixel in the middle of this pixmap. */
XDrawPoint(display, pixmap, gc, 15, 20);

在一個窗口裏繪製一個Pixmap

我們在獲得了一個pixmap的句柄後,我們就可以使用它在窗口裏繪製,使用函數XCopyArea()。


/* draw the previously loaded bitmap on the given window, at location   */
/* 'x=100, y=50' in that window. we want to copy the whole bitmap, so   */
/* we specify location 'x=0, y=0' of the bitmap to start the copy from, */
/* and the full size of the bitmap, to specify how much of it to copy.  */
XCopyPlane(display, bitmap, win, gc,
          0, 0,
          bitmap_width, bitmap_height,
          100, 50,
          1);

如你所見,我們可以拷貝指定的矩形區域而不是整個pixmap。

另外一個需要被強調注意的是 - 可以在同一個屏幕上創建不同深度的pixmap。當我們進行拷貝作業時(往一個窗口上拷貝pixmap等等),我們應該保證源和目標是用相同的深度。如果兩個的深度不一樣,操作將會失敗。除非我們使用前面介紹的函數XCopyPlane()可以完成這個操作。在那樣一種情況下,我們拷貝指定的平面到窗口上去,實際上指定了每一個被拷貝的顏色位。這個操作可以製作許多特殊的效果,但這超出了本文的範圍。


釋放一個Pixmap

最後,當我們完成了對一個pixmap的操作,我們應該釋放它所佔的資源。使用函數XFreePixmap()。


/* free the pixmap with the given ID. */
XFreePixmap(display, pixmap);

在釋放一個pixmap之後 - 我們絕對不能再訪問它。

作爲總結這一章,看一下程序draw-pixmap.c


改變鼠標光標

我們經常看到改變鼠標光標的程序(經常被稱爲X光標)。例如,一個正在埋頭工作的程序經常會把光標變成沙漏,提示用戶需要等待才能處理新的請求。如果沒有這麼個提示方法,用戶會認爲程序已經卡住了。下面讓我們看看如何爲我們的窗口改變鼠標光標。


創建和銷燬鼠標光標

系統提供了兩個方法來創建光標。其中一個是使用系統預定義的形狀,由Xlib支持。另一個是有程序提供一個bitmap來顯示。

使用前一種方法時,我們使用一個特殊的字體名字"cursor"和相應的函數XCreateFontCursor()。該函數接受一個形狀指示然後返回一個代表生成的光標的句柄。文件


列出了系統支持的光標類型,下面的是其中的三個:

 

XC_arrow
X服務器顯示的普通光標。
XC_pencil
一個筆狀的光標。
XC_watch
一個表狀沙漏

使用這些符號來創建光標是非常簡單的:


#include <X11/cursorfont.h>    /* defines XC_watch, etc. */

/* this variable will hold the handle of the newly created cursor. */
Cursor watch_cursor;

/* create a sand watch cursor. */
watch_cursor = XCreateFontCursor(display, XC_watch);

另一種創建鼠標光標的方法時使用一對pixmaps。一個pixmap定義了光標的形狀,另一個是個面具,來指定前一個的什麼內容被顯示。其它的內容將變成透明的。創建一個那樣的光標使用函數XCreatePixmapCursor()。下面的例子裏,我們將使用"icon.bmp"來創建光標。我們將假設它已經被裝載到內存裏去了,並已經被轉換成pixmap,返回的句柄被保存到"bitmap"變量裏。我們希望它是完全透明的。也就是說,只有黑色顏色的部分會被確實畫在屏幕上。爲了實現這個效果,我們將會既用它來做光標pixmap且做面具pixmap。希望你能明白爲什麼這樣...


/* this variable will hold the handle of the newly created cursor. */
Cursor icon_cursor;

/* first, we need to define foreground and background colors for the cursor. */
XColor cursor_fg, cursor_bg;

/* access the default color map of our screen. */
Colormap screen_colormap = DefaultColormap(display, DefaultScreen(display));

/* allocate black and while colors. */
Status rc = XAllocNamedColor(display,
                             screen_colormap,
                             "black",
                             &cursor_fg,
                             &cursor_fg);
if (rc == 0) {
    fprintf(stderr, "XAllocNamedColor - cannot allocate 'black' ??!!??/n");
    exit(1);
}
Status rc = XAllocNamedColor(display,
                             screen_colormap,
                             "white",
                             &cursor_bg,
                             &cursor_bg);
if (rc == 0) {
    fprintf(stderr, "XAllocNamedColor - cannot allocate 'white' ??!!??/n");
    exit(1);
}

/* finally, generate the cursor. make the 'hot spot' be close to the */
/* top-left corner of the cursor - location (x=5, y=4). */
icon_cursor = XCreatePixmapCursor(display, bitmap, bitmap,
				  &cursor_fg, &cursor_bg,
                                  5, 4);

上面需要說明的是參數"hot spot"。當我們定義了一個光標,我們需要指明光標裏的哪一個像素用來生成各種鼠標事件。通常,我們根據習慣來指定一個看起來像"hot spot"的點。例如一個箭頭形狀的光標,我們就會選擇箭頭尖爲"hot spot"。

最後,我們不需要再使用光標時,我們可以使用函數XFreeCursor()來釋放它的資源:


XFreeCursor(display, icon_cursor);

設置一個窗口的鼠標光標

我們在創建了光標後,就可以告訴X服務器把它貼到我們的任何窗口上去。使用函數XDefineCursor(),X服務器就會在每一次光標移進指定的窗口時改變光標的形狀。稍候我們可以使用函數XUndefinCursor()來撤銷剛纔的指定。這樣,鼠標再移進指定的窗口時就會使用缺省的光標。


/* attach the icon cursor to our window. */
XDefineCursor(display, win, icon_cursor);

/* detach the icon cursor from our window. */
XUndefineCursor(display, win);

作爲例子,請查看程序cursor.c

 

 

http://blog.csdn.net/kaku_you/archive/2003/03/31/15437.aspx

http://blog.csdn.net/kaku_you/archive/2003/07/07/15438.aspx

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章