X Window System, Client Programming, No.1


X Window System

X Window System はネットワーク環境での利用を目的に 開発されました。 いろいろな種類のOS上で動作しています。

以下では、Xlib を用いた X Window System のクライアント (アプリケーション)・プログラムの作り方を説明します。


サーバ・クライアント方式


X Window System は『サーバ・クライアント方式』に基づく ウィンドウ・システムです。

ユーザの手元の計算機で動作しているのが「Xサーバ」で、 画面(ビットマップ・ディスプレイ)の管理、、マウス、キーボード の読み取りなどの(ウィンドウ・システムとして必要な)資源の 管理を行なっています。

Xサーバと通信するのが「Xクライアント」です。 Xクライアントは、 X サーバにウィンドウを生成するような要求を送ったり、 Xサーバから送られてくるマウスやキーボードなどの状態変化を 受け取ったりします。 サーバとクライアントの間の通信はtcpプロトコルを用いていますので、 クライアントはXサーバの動作しているのと異なる計算機で 動いていても構いません。

ユーザの操作によって引き起こされるマウス、キーボードなどの 状態変更(「イベント」と呼びます)はまず X サーバが認識します。 そしてXサーバが、その事象がどのウィンドウ上で起きたもの かを判断して、事象が起きたウィンドウの持ち主のクライアントに イベントを送りつけます。

X Window Systemはサーバ・クライアント方式をとっていますので 以下のような利点を持っています。


Xクライアントのコンパイル方法

Xlibを使うクライアント・プログラムは などに置かれたヘッダファイルを必要としますので、 プログラムの先頭に以下のように記述します。
 必要なヘッダファイル
#include <X11/Xos.h> /* OSによるヘッダファイル名や定数などの定義の違いを吸収する */
#include <X11/Xlib.h> /* Xlib を使うための構造体の定義 */
#include <X11/Xutil.h> /* Xlib を使いやすくするユーティリティ関数の定義 */
/* 定数の定義を行う <X11/X.h> は X11/Xlib.h から読み込まれる */

このクライアント・プログラムをコンパイルするときには、 以下のようにコンパイル時のオプションとしてincludeディレクトリを 指定する必要があります。
コンパイル・オプション
    -I/usr/X11R6/include

また、ライブラリとして をリンクする必要がありますので、リンク時に以下のように オプションを指定します。
 リンク・オプション
   -L/usr/X11R6/lib -lX11

x0.c を直接コンパイルするには次のようにタイプします。

x0.cをコンパイルする
$ gcc -I/usr/X11R6/include -L/usr/X11R6/lib x0.c -o x0 -lX11
 x0.cを実行する
$ ./x0 


Xクライアントの書き方

Xクライアントのプログラムは一般に次のような動作を行ないます。

  1. Xサーバと接続する
  2. ウインドウを生成する
  3. ウインドウをマップ(表示開始)する
  4. Xサーバから送られてくるイベントを処理する
  1. X サーバと接続する

    関数 XOpenDisplay() は X サーバとの接続を行います。

    Display *XOpenDisplay(char *displayname)  サーバと接続します
    
    displayname はサーバ名を表す文字列で、
    hostname:number.screen_number
        例:   ws3081.tsuda.ac.jp:0.0
    
    という形式で指定します。 NULL を渡すと環境変数 DISPLAY (これは、通常 :0.0 に設定されています) の値を用います。 返り値は、接続したサーバを表すDisplay構造体へのポインタとなります。

    Display構造体は次のように定義されています。

    Display構造体 (X11/Xlib.hから抜粋)
    typedef struct _XDisplay {
            int fd;                 /* Network socket. */
            struct _XSQEvent *head, *tail;  /* Input event queue. */
            int qlen;               /* Length of input event queue */
            char *display_name;     /* "host:display" string used on this connect*/
            int default_screen;     /* default screen for operations */
            int nscreens;           /* number of screens on this server*/
            Screen *screens;        /* pointer to list of screens */
            KeySym *keysyms;        /* This server's keysyms */
            XModifierKeymap *modifiermap;   /* This server's modifier keymap */
            ...(略) ...
    } Display;
    
    

    Display 構造体はXサーバの情報を保持しています。 Display 上に、実際にウインドを作ったりイベントが 発生したりする場所として screen があります。 複数の screen をサポートしている Display もあります。 あるサーバがサポートしているscreenは、Display構造体中の screens フィールドにリストとして保持されています。 Xlib では普通 screen を指定するのに、このリストの index (前から何番目のscreenか)を用いています。 一つしかscreen を用いないアプリケーション (ほとんどのアプリケーションがこれに当てはまります) ではDefaultScreen(display) というマクロ を使っておけばよいでしょう。 このマクロは display の default の screen 番号 (普通は0) を返しますので intで受けます。

    便利なマクロが用意されています。変数を Display *display; int screen; とすると以下のものがよく利用されます。 マクロ名の前に 'X'をつけたものも関数として用意されています。

  2. ウインドウを生成する

    ウインドウを生成する時に

    などを指定します。 たとえば、Exposure(書き直し)イベントを送らせるためには event_maskにExposureMask を指定してウィンドウを生成します。

    実際にウインドウの実体が X サーバ内に作られるのは Map 要求を 送った時なので、その前にいろいろな属性を再設定することもできます。 また、ウインドウが実際に作られた後でも属性 (ウィンドウの位置、大きさ、描画する色、など)の変更は可能です。

     ウインドを生成する関数
    Window XCreateWindow(
        Display *display,
        Window parent,
        int x,
        int y,
        unsigned int width,
        unsigned int height,
        unsigned int border_width,
        int depth,
        unsigned int class,
        Visual *visual,
        unsigned long valuemask,
        XSetWindowAttributes *attributes
    );
    
    ウィンドウの属性の種類
    #define CWBackPixmap            (1L<<0)         /* backgroundのPixmapを指定 */
    #define CWBackPixel             (1L<<1)         /* backgroundのPixel値(単色)を指定 */
    #define CWBorderPixmap          (1L<<2)         /* 枠線のPixmapを指定 */
    #define CWBorderPixel           (1L<<3)         /* 枠線のPixel値を指定 */
    #define CWBitGravity            (1L<<4)         /* resizeで保存される場所の指定 */
    #define CWWinGravity            (1L<<5)         /* 親window resize時の置き直し方 */
    #define CWBackingStore          (1L<<6)         /* 表示されない部分の内容を保持 */
    #define CWBackingPlanes         (1L<<7)         /* Backing Storeするべきplane */
    #define CWBackingPixel          (1L<<8)         /* Backing Storeされないplaneの色 */
    #define CWOverrideRedirect      (1L<<9)         /* window managerを無視する */
    #define CWSaveUnder             (1L<<10)        /* 隠すwindowの内容を保持 */
    #define CWEventMask             (1L<<11)        /* 送られるeventを指定 */
    #define CWDontPropagate         (1L<<12)        /* maskするeventを指定 */
    #define CWColormap              (1L<<13)        /* colormapを指定 */
    #define CWCursor                (1L<<14)        /* cursorを指定する */
    
    ウィンドウの属性を指定する構造体
    typedef struct {
        Pixmap background_pixmap;                   /* 背景色のPixmap */
        unsigned long background_pixel;             /* 背景色の指定 */
        Pixmap border_pixmap;                       /* 枠線のPixmap */
        unsigned long border_pixel;                 /* 枠線の色の指定 */
        int bit_gravity;                    /* resizeで保存されるべき場所 */
        int win_gravity;                    /* 親ウインドのresize時の置き直し方 */
        int backing_store;                  /* ウインドウの内容を保持するか否か */
        unsigned long backing_planes;       /* BackingStoreされるplane */
        unsigned long backing_pixel;        /* BackingStoreされないplaneの色 */
        Bool save_under;                    /* 隠された部分を保持するか否か */
        long event_mask;                    /* このウインドが処理するイベントの種類 */
        long do_not_propagate_mask;         /* マスクするイベントの種類 */
        Bool override_redirect;             /* ウインドマネージャを無視するかどうか */
        Colormap colormap;                  /* このウインドにおけるカラーマップ */
        Cursor cursor;                      /* カーソルの指定 */
    } XSetWindowAttributes;
    
    
  3. ウィンドウをマップ(表示開始)する
    表示に関する関数
    int XMapWindow(Display *display,Window w)      ウィンドウの表示を開始する
    
    

    Xサーバが実際にウィンドウを作るのはクライアントからそのウィンドウの Map (表示開始)要求が来た時です。 したがって、ウインドウを生成する要求の属性がおかしくても Map 要求を送る瞬間までエラーにはなりません。

    このように X Window System では、「要求が X サーバに送られた タイミング」と「実際に処理されるタイミング」がずれることがあるので デバッグには注意が必要です。

  4. サーバから送られてくるイベントを処理する

    クライアントはウィンドウを生成した後、関数XNextEvent() を用いて X サーバからイベントが送られてくるのを待ちます。

    イベントを受け取る関数
    int XNextEvent(Display *display,XEvent *event)
    
    

    サーバから送ってもらうイベントの種類を設定する方法は 次の2種類です。

     サーバに送ってもらうイベントを指定します
    int XSelectInput(Display *display,Window w,unsigned long mask)
    
    

    X サーバからは、イベントの種類によって XButtonEvent, XExposeEvent, XKeyEvent など、いろいろな構造体がクライアントに送られてきます。

    クライアントは、 X サーバから送られてきたデータを XEvent 共用体で一旦受けとり、 その最初の4byteを見て「送られてきたデータのtype」を判断して 正しい構造体に当てはめて解釈し直します。

    イベント構造体の中にはそのイベントの起きたウィンドウの 番号を保持するフィールドがあります。 クライアントが複数のウィンドウを扱っている場合は、 イベント構造体の中のウィンドウ番号を参照して どのウインドウで起きたイベントかを判断します。

    イベントを表す構造体
    typedef struct {       /* マウスのボタンに関するイベントを表す構造体 */
            int type;               /* of event */
            unsigned long serial;   /* # of last request processed by server */
            Bool send_event;        /* true if this came from a SendEvent request */
            Display *display;       /* Display the event was read from */
            Window window;          /* "event" window it is reported relative to */
            Window root;            /* root window that the event occured on */
            Window subwindow;       /* child window */
            Time time;              /* milliseconds */
            int x, y;               /* pointer x, y coordinates in event window */
            int x_root, y_root;     /* coordinates relative to root */
            unsigned int state;     /* key or button mask */
            unsigned int button;    /* detail */
            Bool same_screen;       /* same screen flag */
    } XButtonEvent;
    
    typedef struct {       /* 画面の書き直しに関するイベントを表す構造体 */
            int type;
            unsigned long serial;   /* # of last request processed by server */
            Bool send_event;        /* true if this came from a SendEvent request */
            Display *display;       /* Display the event was read from */
            Window window;
            int x, y;
            int width, height;
            int count;              /* if non-zero, at least this many more */
    } XExposeEvent;
    
    イベントを表す構造体のユニオン
    typedef union _XEvent {
            int type;               /* must not be changed; first element */
            XAnyEvent xany;
            XExposeEvent xexpose;
            XButtonEvent xbutton;
            XKeyEvent xkey;
            XMotionEvent xmotion;
            ... 略 ...
    } XEvent;
    
    
    イベント・マスク
    定数名意味
    NoEventMask イベントは必要なし
    ExposureMaskウインドの書き直し
    KeyPressMaskキーボードを押す
    KeyReleaseMaskキーボードを放す
    ButtonPressMaskマウスのボタンをどれか押す
    ButtonReleaseMaskマウスのボタンをどれか放す
    EnterWindowMaskマウスがウインドの中に入る
    LeaveWindowMaskマウスがウインドから出る
    PointerMotionMaskマウスが動く
    ButtonMotionMaskボタンが押されている間にマウスが動く
    ......
    イベント・タイプ
    定数名定数名
    KeyPress2FocusIn9
    KeyRelease3FocusOut10
    ButtonPress4KeymapNotify11
    ButtonRelease5Expose12
    MotionNotify6GraphicsExpose13
    EnterNotify7NoExpose14
    LeaveNotify8......

    Exposeイベント

    クライアントが送った Map 要求が X サーバに処理されてはじめて 実際に画面上にウインドウが作られます。 表示されていないウインドウに描画しても内容は (BackingStore を True にしていない限り) 保持されませんから、ウインドウが実際に作られる までクライアントが描画要求を送っても意味がありません。 したがって、Xサーバからクライアントに最初の Expose (再描画要求) イベントが送られてきてから最初の描画を行うべきです。


簡単なクライアントの例(0)

以下のプログラムをコンパイルして実行してみましょう。

x0.c
#include <stdio.h>
#include <X11/Xos.h>
#include <X11/Xlib.h>
#include <X11/Xutil.h>

int main() {
    Display *dpy;
    int scr;
    Window win;
    XSetWindowAttributes xswa;
    char *xservname=NULL;

    if ((dpy=XOpenDisplay(xservname)) == NULL) {
	fprintf(stderr,"can not open %s\n",XDisplayName(xservname));
	exit(-1);
    }
    scr = DefaultScreen(dpy);
    xswa.background_pixel = WhitePixel(dpy,scr);
    xswa.border_pixel = BlackPixel(dpy,scr);
    xswa.event_mask = ExposureMask;
    win = XCreateWindow(dpy,RootWindow(dpy,scr),0,0,320,240,1,
			CopyFromParent,CopyFromParent,CopyFromParent,
			CWBackPixel | CWBorderPixel | CWEventMask, &xswa);
    XMapWindow(dpy,win);
    for (;;) {
	XEvent ev;
	XNextEvent(dpy,&ev);
	switch (ev.type) {
	  case Expose:
	    XClearWindow(dpy,win);
	    break;
	  default:
	    printf("unknown event %d\n",ev.type);
	    break;
	}
    }
}

少し複雑なクライアントの書き方

  1. Xサーバと接続する
  2. ウィンドウを生成する
  3. グラフィック・コンテクスト (GC) を生成する
  4. ウィンドウをマップ(表示開始)する
  5. Xサーバから送られてくるイベントを処理する
  6. 描画を行なう

グラフィックス・コンテクスト

グラフィック・コンテクスト (Graphic Context, GC)は、 線の太さ・色・フォントの種類など、描画に必要な属性を まとめて保持しておくデータ構造です。

描画要求をクライアントからXサーバに送るたびに描画に必要な 属性情報も毎回送っていては、サーバ・クライアント間の通信量が 増大してしまいます。 そこで、実際のグラフィック・コンテクストをXサーバの側に 保持させておくというわけです。 クライアント側からはXサーバ上のグラフィック・コンテクスト は単なる番号 (つまり integer)として見えます。 描画に必要な属性情報を前もってX サーバの中の グラフィック・コンテクストに送り込んでしまい、 クライアントは描画要求を出す時にどの グラフィック・コンテクストを使うかを指定します。

グラフィック・コンテクストはウィンドウに対して生成されます。 あるウィンドウに対して生成した グラフィック・コンテクスト はその子孫のウィンドウでも利用することができます(逆はできません)。

グラフィック・コンテクストを扱う関数
GC XCreateGC(                       /* GCを作る */
    Display *display,
    Drawable d,
    unsigned long valuesmask_create,
    XGCValues *values
);
int XCopyGC(                          /* GCの内容を他のGCにコピーする */
    Display *display,
    GC src,
    unsigned long valuemask_copy,
    GC dest
);
int XChangeGC(                       /* GCの内容を変更する */
    Display *display,
    GC gc,
    unsigned long valuemask_change,
    XGCValues *values
);
int XFreeGC(Display *display, GC gc) /* GCを解放する */

GCの内容を指定する構造体
typedef struct {
        int function;           /* ビット計算の論理式 */
        unsigned long plane_mask;/* plane mask */
        unsigned long foreground;/* 文字や線の色 */
        unsigned long background;/* 背景色 */
        int line_width;         /* 線の太さ */
        int line_style;         /* 線のスタイル(実線、点線) */
        int cap_style;          /* 点の形(正方形、円) */
        int join_style;         /* 太い線のつなぎかた */
        int fill_style;         /* 塗りつぶし方 */
        int fill_rule;          /* EvenOddRule, WindingRule */
        int arc_mode;           /* ArcChord, ArcPieSlice */
        Pixmap tile;            /* 塗りつぶしの模様 */
        Pixmap stipple;         /* 線の模様 */
        int ts_x_origin;        /* offset for tile or stipple operations */
        int ts_y_origin;
        Font font;              /* フォント */
        int subwindow_mode;     /* ClipByChildren, IncludeInferiors */
        Bool graphics_exposures;/* boolean, should exposures be generated */
        int clip_x_origin;      /* origin for clipping */
        int clip_y_origin;
        Pixmap clip_mask;       /* bitmap clipping; other calls for rects */
        int dash_offset;        /* patterned/dashed line information */
        char dashes;
} XGCValues;

グラフィック・コンテクストの種類
#define GCFunction              (1L<<0)
#define GCPlaneMask             (1L<<1)
#define GCForeground            (1L<<2)
    ... 略 ...
#define GCArcMode               (1L<<22)

Graphics Functions 属性の値と意味
属性値意味属性値意味
GXclear0GXandsrc
GXandReversesrcGXcopysrc
GXandInvertedNOTGXnoopdst
GXxorsrcGXorsrc
GXnorNOTGXequivNOT
GXinvertNOTGXorReversesrc
GXcopyInvertedNOTGXorInvertedNOT
GXnandNOTGXset1
その他の属性
属性値の種類
lineLineSolid,LineOnOffDash,LineDoubleDash
capCapNotLast,CapButt,CapRound,CapProjecting
joinJoinMiter,JoinRound,JoinBevel
fillFillSolid,FillTiled,FillStippled,FillOpaqueStippled
fillEvenOddRule,WindingRule
subwindowClipByChildren,IncludeInferiors
SetClipRectanglesUnsorted,YSorted,YXSorted,YXBanded
coordinateCoordModeOrigin,CoordModePrevious
PolygonComplex,Nonconvex,Convex
Arc ArcChord, ArcPieSlice

描画関数


XDrawPoint(display,d,gc,x,y)                                        点を描く
XDrawLine(display,d,gc,x1,y1,x2,y2)                                 直線を描く
XDrawRectangle(display,d,gc,x,y,width,height)                       矩形を描く
XFillRectangle(display,d,gc,x,y,width,height)                       矩形を塗り潰す
XDrawArc(display,d,gc,x,y,width,height,angle1,angle2) (角度は1/64度)弧を描く
XFillArc(display,d,gc,x,y,width,height,angle1,angle2) (角度は1/64度)弧を塗り潰す
XDrawLines(display,d,gc,points,npoints,mode)                        多角形を描く
XFillPolygon(display,d,gc,points,npoints,shape,mode)                多角形を塗り潰す
XDrawString(display,d,gc,x,y,string,length)                         文字列を重ね書きする
XDrawString16(display,d,gc,x,y,string,length)                       2byte文字列を重ね書きする
XDrawImageString(display,d,gc,x,y,string,length)                    文字列を書く
XDrawImageString16(display,d,gc,x,y,string,length)                  2byte文字列を書く
XPutImage(display,d,gc,image,src_x,src_y,dst_x,dst_y,width,height)  ビットイメージを表示する
    Display *display;
    Drawable d;
    GC gc;
    int x,y,x1,y1,x2,y2,angle1,angle2;
    unsigned int width,height;
    XPoint *points;
    int npoints,mode,shape;
    char *string;
    int length;
    XImage *image;
    int src_x,src_y,dst_x,dst_y;

Drawable というデータ・タイプは Window と Pixmap (表示されないウインドウ) のどちらでもよいことを 表しています。 すなわち、上記の描画関数は Window と Pixmap の両方に 対して行うことができます。

BackingStore や SaveUnders を使わずにウインドウの内容を保持する時は、

  1. ウインドウと同じ大きさ・深さの Pixmap を用意し、
  2. ウインドウへの描画と同時に Pixmap にも描画し、
  3. ウインドウの Expose イベントが送られてきたら、 Pixmap の内容を関数 XCopyAreaを使ってコピーする
などの方法を取りますが、この時にPixmap を使います。


簡単なクライアントの例(1)

「グラフィックス・コンテクストを生成し、絵を描画する」 プログラム x1.c を(x0.c を変更して)作成しましょう。
x1.c
#include <stdio.h>
#include <X11/Xos.h>
#include <X11/Xlib.h>
#include <X11/Xutil.h>

int main() {
    Display *dpy;
    int scr;
    Window win;
    XSetWindowAttributes xswa;
    char *xservname=NULL;
    GC gc;
    XGCValues gcv;

    if ((dpy=XOpenDisplay(xservname)) == NULL) {
	fprintf(stderr,"can not open %s\n",XDisplayName(xservname));
	exit(-1);
    }
    scr = DefaultScreen(dpy);
    xswa.background_pixel = WhitePixel(dpy,scr);
    xswa.border_pixel = BlackPixel(dpy,scr);
    xswa.event_mask = ExposureMask;
    win = XCreateWindow(dpy,RootWindow(dpy,scr),0,0,320,240,1,
			CopyFromParent,CopyFromParent,CopyFromParent,
			CWBackPixel | CWBorderPixel | CWEventMask, &xswa);
    gcv.foreground = BlackPixel(dpy,scr);
    gcv.background = WhitePixel(dpy,scr);
    gc = XCreateGC(dpy,win,GCForeground|GCBackground,&gcv);
    XMapWindow(dpy,win);
    for (;;) {
	XEvent ev;
	XPoint p[6] = {{60,190},{30,235},{107,207},{12,207},{89,235},{60,190}};
	XNextEvent(dpy,&ev);
	switch (ev.type) {
	  case Expose:
	    XClearWindow(dpy,win);
	    XDrawLine(dpy,win,gc,10,10,110,60);
	    XDrawRectangle(dpy,win,gc,10,70,100,50);
	    XFillRectangle(dpy,win,gc,10,130,100,50);
	    XDrawArc(dpy,win,gc,150,10,100,50,0,45*64);
	    XDrawArc(dpy,win,gc,150,10,100,50,90*64,45*64);
	    XDrawArc(dpy,win,gc,150,10,100,50,180*64,45*64);
	    XDrawArc(dpy,win,gc,150,10,100,50,270*64,45*64);
	    XDrawArc(dpy,win,gc,150,70,100,50,0*64,360*64);
	    XFillArc(dpy,win,gc,150,130,100,50,0*64,270*64);
	    XFillPolygon(dpy,win,gc,p,6,Complex,CoordModeOrigin);
	    break;
	  default:
	    printf("unknown event %d\n",ev.type);
	    break;
	}
    }
}
 x1.cをコンパイルする
$ gcc -I/usr/X11R6/include -L/usr/X11R6/lib x1.c -o x1 -lX11

 x1.cを実行する
$ ./x1 



課題

  1. x0.cをダウンロードして、コンパイルし、実行させてみましょう。
  2. x1.cをダウンロードして、コンパイルし、実行させてみましょう。
  3. エディタとしてemacsを起動して下さい。
  4. アプリケーション→debianメニュー→アプリケーション→エディタ→emacs21-mule-canna-wnn

  5. x2.cを新規にopenして(x1.cをinsertし)編集して下さい。
  6. 「オリジナリティあふれる美しい絵を表示する」 プログラム x2.c を作成して下さい。 絵には動物か乗物を登場させることが条件です。

作成したプログラムが正しく動作することを確認したら、x2.c を「宿題提出Web:コンピュータネットワーク:課題1」 http://nw.tsuda.ac.jp/tsuda/handin/up.php?id=2006/network/kadai1 から提出して下さい。

Webを見るには、Mozilla が使えるはずです。

アプリケーション→インターネット→Mozilla Web Browser

提出した後は、正しく提出されていることを http://nw.tsuda.ac.jp/tsuda/handin/list.php?id=2006/network/kadai1 で必ず確認しておいて下さい。

提出〆切は来週の授業開始時刻です。


Yoshihisa Nitta

http://nw.tsuda.ac.jp/