Xプログラミング

メインページ > 工学 > 情報技術 > プログラミング > Xプログラミング
ここでは、Unix上でGUIを扱う基礎となる、X Window Systemを扱います。
X Window System(以下、単に「X」と呼ぶ)はUNIX上でGUIを扱う手段として長く利用されています。現在では、いわゆるUNIXワークステーションだけでなく、一般のデスクトップパソコンでも、Xを利用することができます。 たとえば、Linuxのシステムを使えばもちろんXを利用することができるでしょう。また、UNIXベースであるMac OS Xでも“X11”というサーバを使えば、Aquaインタフェースと並行してXを利用できます。 また、(この解説では基本的に対象外ですが)Windowsであっても“Cygwin/X”などを利用すれば、やはりXを使うことができます。
Xを用いたプログラミングは、「点を打つ」「線を引く」といった単体で利用することが難しい機能を利用するものです。これらの機能だけを使うプログラミングは効率的だとは言い難いため、コンピュータに詳しい開発者であっても直接にXを使うプログラミングを敬遠し、Qt などのツールキットを利用するプログラマーも多くいます。実際、Xを用いたプログラミングを知らなくてもツールキットだけでグラフィカルなプログラムを作成することも可能です。しかし、たとえば次のような理由から X を用いるプログラミングの知識が必要とされる場合があります。
第一に、ツールキットが提供してくれるものだけで全ての用が足りるわけではありません。ツールキットが提供するウィジェットに不足を感じ、GTK+のソースを読んで内容を調べるようなときには、Xの知識がなければ読み進めることが困難です。
第二に、GUIを用いた単純なゲームなどを開発する場合は、ツールキットを用いるよりもxcbの関数を直接用いた方がプログラムは作成しやすく、かつ読みやすくできます。これは、ゲームで使いたいインターフェースは、しばしばウィジェットとして提供されているものとは別物であることが多いからです。ゲームのメニューやキャラクターやアイテムなどの要素をツールキットが提供する一般的な枠組みにを当てはめていくよりも作成者がそれぞれのゲームに適した枠組みを一から書き起こす方が開発は容易です。この場合に開発者は xcb を直接に用いたプログラミングを行うことになります。
前置きが長くなりましたが、ここから実際のX Window Systemプログラミングを行なっていきます。ただし、その前に実際にプログラムを動かす前に、いくつか知っておく必要があることがあります。プログラミングに慣れた人なら、手元にXのソースコードを置いておくことを勧めます。これらを閲覧することでX Window Systemの動作やプログラミング手法になじみやすくなります。X Window Systemは、
からダウンロードすることが出来ます。現在の系列はX11と呼ばれ、その中の最新版はR7.7です。X11系列のR6とR7ではビルドシステムに大きな変化が現われました。R6まではimakeが使われていましたが、R7以降ではGNUのAutotoolsを利用して各々の部分を個別にビルド出来るようになったのです。これは、個々のプログラムの独立性を高める上で大きな前進といえそうですが、ソースを読むという立場からは、全てのソースをひとまとめでダウンロード出来た方が便利です。ここではX11R6.8を利用します。R6系列では最も上のディレクトリはxc/が含まれています。xc/以下ではlib/, include/, program/などのサブディレクトリが含まれています。これらについてはXクライアントのプログラムをする上で何度か触れる機会が出て来ます。
ここまではソースを読む手間を惜しまない人向けです。ここからはXクライアントを作成する人全員が知っておく必要があります。ここでは2点重要な点をあげます。
1点目はXプログラムを動かすには、Xサーバが必要ということです。X Window System Sysyemはサーバクライアント型プログラムです。XサーバはXクライアントからの要求を受け取り、その内容を画面に描画していきます。program/Xserver以下のファイルがXサーバの本体です。Xサーバのプログラム名はXの1文字です。これについてのmanは$man Xで読むことが出来ます。man自体の長さもなかなかのものですが、SEE ALSOの数が非常に多く、読みはじめると相当の時間がかかりそうです。
我々が通常X Window Systemプログラムと呼ぶのはXクライアントのプログラムを指します。そのため、Xクライアントを動かすためには、Xサーバが動いている必要があります。xtermやgdmなどもXクライアントに含まれるため、これらが起動している環境では当然Xサーバも起動しています。一方、通常コンソールで作業しているならXサーバを立ち上げる必要があります。
簡潔にXサーバを立ち上げるには
コマンドを使うのがよいでしょう。このコマンドはXサーバ(X)に加え、最初のXクライアント(デフォルトではxterm)を立ち上げます。xinitからXサーバを起動した場合には、最初のXクライアントを終了した時点でXサーバも停止します。これはXとXクライアントを別々に立ち上げたときの振舞いとは異なっているので、注意が必要です。(xinitコマンドはprogram/xinit/xinit.cで定義されています。)
環境によっては、Xを立ち上げるのにstartxを利用する事を推奨しています。startxは、xinitと似ていますが、より多くのXクライアントを起動しようとします。これらはXプログラミングを行なう上では必要ないので、xinitを利用すればよいでしょう。(startxもprogram/xinit内のstartx.cppで定義されています。cppの拡張子ですがstartx自体はshのシェルスクリプトでc++とは関係がありません。)
2点目として、Xクライアントのコンパイルについて述べます。X Window Systemプログラミングでは通常Cを利用します。w:Gtk+やw:SDLに関していえば、これらはCPlusPlusやw:Pythonなどから利用することができるため、間接的には他の言語を利用してXプログラミングを行なうことは可能です。しかし、Xサーバへの通信を定義しているXlibは、Cのライブラリとして作成されているため、Cを利用するのがよいでしょう。
残念なことに、Xlibの関数を使用するためのヘッダファイルやライブラリをCコンパイラが見つけてくれるとは限りません。Xlib関連のファイルは/usr/以下ではなく/usr/X11R6(環境によってかわります。)以下に置かれています。これを見つけさせるためにコンパイラには次のようなフラグを与える必要があります。コンパイラがccと仮定すると、
が求められるコマンドです。ここで、-lX11は必ずソースファイルより後につけて下さい。(理由はccのmanまたはinfoの-lオプションに関する項にあります。ccは関数をファイル名が置かれた順に探すのです。)他にも-Wallなどをつけておくと引数の順番間違いなどを見つけるのに有用です。
pkg-configコマンドが利用できる環境なら、リンクのオプションを pkg-config x11 --libs で、コンパイルのオプションを pkg-config x11 --cflags で取得できます。
GUIを用いたプログラミングをするために、まずXサーバにGUIを描画したい領域を確保させます。Xサーバ上で確保された長方形の領域はウィンドウと呼ばれます。
最初にプログラム例を示します。このプログラムは2.6系列のLinuxで実行できることを確認していますが、システムによってはXlibの場所などが異なっており、コンパイルできないかも知れません。詳しいXlibの場所については、開発元、またはディストリビュータに聞いてください。
ここでいくつかの関数が用いられているので順に解説して行きます。各々の関数には対応した名前のmanファイルが存在しそれらの説明を読むことができます。
システムによってはMANPATH変数が適切に設定されておらず、manが利用できないかも知れません。ソースコードを持っているなら、doc/man/X11内にXOpenDisplayのmanが存在します(manファイルはgzipで圧縮されたテキストファイルです。そのため、エディタを使えばmanコマンドに頼らずともmanを読むことができます。)。
Xサーバとの通信を行い、クライアントが利用するリソースを手に入れる関数です。これ以降Xサーバとの通信はここで得たDisplay変数を用いて行われます。
引数にNULLを指定すると、現在使用されているXサーバがXサーバとして利用されます。(XOpenDisplayのmanから)
Xクライアントは長方形の領域を取得し、その中で描画を行います。ここで領域のことをWindowと呼び、Windowを取得するための関数がXCreateSimpleWindowです。XCreateWindowという関数ではより詳細な設定ができますが、ここでは使用しません。
引数は順にDisplay*,親ウィンドウ,x座標,y座標,幅,高さ,枠の幅,枠の色, 開いたウィンドウの色となっています。ここで2点についてコメントします。
まず、親ウィンドウという引数に付いてです。ウィンドウには親子関係があり、子ウィンドウは親ウィンドウが指定した範囲より外に描画を行うことができません。ここでは任意の位置にウィンドウを開きたいので、ルートウィンドウを親ウィンドウとして指定します。ルートウィンドウはXサーバが所持しているウィンドウでXサーバが使える全ての座標を占めているウィンドウです。そのため、ルートウィンドウを親ウィンドウとしているウィンドウは、サーバが使い得る全ての座標を利用することができます。
Xサーバが提供するルートウィンドウを利用するにはDefaultRootWindowを利用します。(lib/X11/Xlib.h内で定義されているマクロです。中身はDisplay構造体から対応するウィンドウを取り出しています。)
2点目は色についてです。色は数値によって表されます。Xウィンドウが実際に使える色はハードウェアによって決まっています。筆者の環境ではRGB値がそれぞれ0-255の値を取ることができます。このような環境ではRGBのそれぞれを16進数で指定することが可能です。rgb値をr,g,bとすると、色は次の式で表されます。
例えば白では(r,g,b) = (0xff, 0xff, 0xff)ですが、このときには与える色は
で表されます。例では0を与えていますが、これは、(r,g,b)=(0,0,0)で黒に対応します。
実際に実行すると画面左上に黒のウィンドウが開かれるはずです。ここまでで描画したい領域を押さえる方法を紹介しました。次項では実際に描画を行う方法を紹介します。
前項では領域を押さえる方法を習得しました。次に、領域内に点を打つ方法を紹介します。使える領域内には多くのw:ピクセルがあります。ここで扱うのは個々のピクセルを好みの色で塗りつぶす方法です。どのような絵もピクセルの集合で表されることを考慮すれば、これだけであらゆる絵がかけることが分かります。
逆に点を打つだけでは解決できないことを3点あげます。
1点目として、2Dのアニメーションを行いたいときには、打つべき点が多くあるため、1点ずつ描画を行っていると表示がちらつく問題をあげます。
2点目は静止画でも3Dの描画を行うには、どのピクセルをどの色で塗るかを決めなければなりません。
最後に、一般的なツールキットが提供するようなマウスに対する対応(例えばウィンドウ中のある領域をクリックすると'OK'が選択されるなど)は絵を書くこととは別に行う必要があります。
しかし、点が打てるなら、それを用いて線を引くことができます(斜め線の場合はどのピクセルを塗るかを決定するアルゴリズムが必要になります。w:ブレゼンハムのアルゴリズムを参照してください。)。線が引ければ、それを繰り返すことで長方形も作ることができます。結局点を打つことは全ての描画の基本になることに注意する必要があります。
点を打つには、XDrawPoint関数を使います。XMapWindowを使った後、
を追加します。追加した後にXFlush(dpy);を書かないとXDrawPointがXサーバに届かないので注意が必要です。うまく行くと画面の中心に黒い点が打たれるはずです。
ここでGCについてコメントします。XDrawPointの3つめの引数はGC(Graphics Context)という構造体です。GCとは描画される点や線の性質を定める性質を持っており、点の色などを設定するときにはこの量を設定する必要があります。
GCを利用するには、作ったGCをXサーバに登録する必要があります。GCの登録には
を利用します。(lib/X11/CrGC.c内の関数です。)
この関数は
のプロトタイプを持ちます。3番目と4番目が実際に値を登録する部分です。2番目のDrawableは新しく出て来た変数ですが、これについては後述します。
GCにはいくつかの設定項目がありますが、ここでは点の色を変更します。XDrawPointで用いられるのはGCのforegroundという項目です。
残念なことにこれを設定するには2段階の手順を踏む必要があります。まず、3番目の変数で変更したい値を指定します。次に、4番目の変数に実際に変更する値を与えます。この2つの手順によってforegroundの登録がなされます。
実際のコードは次のようになります。ここでは0xff0000(赤)の前景色を持つGCを登録します。
ここまででGCが登録されました。XGCValuesの詳細はXCreateGCのmanを参照してください。
ここでは変更する項目が1つなのでわからないのですが、3番目の引数であるunsigned longは、複数の要素を指定するためにビット演算を使用します。例えば、foregroundに加えてline_widthを変更したいときには、
を使用します。ビット演算についてはC言語を参照してください。
ここまでで3,4番目の引数に付いて述べて来ました。GCについての説明の最後にDrawableについて説明します。
Drawableは、WindowとPixmapの2つを合わせた名称です。(ちなみに、Pixmapで各ピクセルが1ビットで表されるとき、これをbitmapと呼びます。)(Xlib-C Language Interface http://xjman.dsl.gr.jp/X11R6/X11/ )これらはどちらもXDrawPointなどの描画関数で描画を行うことができるので、2つを同じような仕方で用いられると便利です。
一見するとWindowやPixmapはいくつかの内容を含んだ構造体だと思うかも知れません(実際GtkWindowは関数ポインタをいくつも保持している大きな構造体です。(gtk+/gtk/gtkwindow.c))。しかし、実際にはこれらの変数自体は単純な整数型のtypedefになっています。クライアント側では個々のWindowは番号だけで管理されています(include/X.hを参照。)。ウィンドウの場所や大きさはXサーバの側で保持するのでクライアント側ではこれで十分なわけです。
ここまでで図形を扱う方法を見て来ました。ここで、任意の図形を表示する方法について考察します。既にウィンドウ中の任意のピクセルを、任意の色で塗りつぶす方法を得ました。この方法だけを利用して、好きな図形を書くことができます。例えば、色を書き込んだ整数型の配列を用いて、
として絵を描き、
とすれば正しい図が描けます。
しかし、この方法は手間がかかる上、図に間違いがあったとき訂正するのも困難です。図を描画するためにはできればGUIツールを使いたいところでしょう。ただし、この場合は扱うファイル形式の問題が生じます。仮に絵がかけたとしてもそれをプログラム中で扱う方法が難しいなら、それはやはり使いづらくなってしまいます。
Xに付属のツールとしては、
を利用することができます。(program/bitmap以下のファイル。)bitmapはXでいうビットマップファイルを生成するGUIツールです。ここでいうビットマップファイルについては、Xlib内にそれらを扱う関数(XReadBitmapFileなど)が存在するため、移植性は完璧です。残念ながらビットマップファイルは白と黒の1色ずつしか扱うことができないので、現在のカラーディスプレイで使うには力不足です。
Xlib内ではないもののXとともに配布されているファイル形式としてxpm形式があります。xpm形式はGIMP, w:ImageMagickなどの著名なOSSで作ることができ、手軽に扱えることがメリットです。Xlib内には無いもののXと同時に配布されるため移植性も高いことが期待されます。加えて、xpmファイルから直接X内のデータに変換する関数が付属しているため、非常に簡単に扱えます。
それ以外のw:PNGやw:JPEGを扱う場合には、外部のライブラリを用いることが普通です。w:Gtk+やw:SDL_imageでは、これらのファイルを直接ロードする関数が提供されます。(Gtk+:gdk-pixbuf/io-*を利用, SDL_image:IMG_Loadを利用)この種のファイルを使うのなら、これらのツールを用いたプログラミングを考えた方がよいでしょう。
既にXは、クロスプラットフォームのGUIを提供することを述べました。もちろん、プラットフォーム毎の描画における差異は、ドライバによって吸収する必要があります。描画に関していえば、Xのドライバはprogram/Xserver/hw以下の各項目内で定義されています。顕著な例ではhw/darwin以下ではMac OS X上でXを動かす方法を定めています(w:en:XDarwinを参照)。ここではMac OS Xの描画エンジンであるw:quartzを利用してXサーバの各機能が記述されています。
クロスプラットフォームで描画ができることは大きなメリットです。しかし、コンピュータの操作という観点からは、図の描画だけでなくそれらを入力機器で操作する方法も各プラットフォーム毎に定義する必要があります。Xは実際に各プラットフォーム毎に入力機器を扱う方法を持っています。これらもprogram/Xserver/hw以下で定義されています。特にUnix系のOSについては、program/Xserver/hw/xfree86/os-support以下に記述されています。例えば、2.6系のLinuxでは、マウスはデバイスファイルを通じて使用します。os-support/linux/lnx_mouse.cでは、よく用いられるデバイスファイルである/dev/mouse, /dev/psauxが存在するかを確かめ、それらをopenしてマウスからの入力を読みとっています。
ここでは、Xを用いて入力機器を扱う方法を述べます。これらの入力は、X上でイベントとして扱われます。イベントは、Xサーバ上に用意されているキュー(XNextEventのmanより)で、各クライアントはXサーバからイベントを受け取ることで入力を受け取ります。
適切に設定されたXサーバ上には入力機器から自動的にイベントが集められます。我々はXクライアントにこれを取得するよう仕向ける必要があります。イベントの取得には2段階の手順が必要となります。まず、用いるウィンドウがどのイベントを受け取るかを定めます。2番目に、実際にイベントを受け取ります。
受け取るイベントを定めるには、XSelectInputを用います。
ここでは、event_maskが重要です。この整数は、どのイベントを受け取るかをビット演算で定めます。ビット演算にはinclude/X11/X.hで定義されている値を扱います。例えば、マウスのボタンが押されるイベントを検出したいときには、ButtonPressMaskを利用します。また、キーボードのキーが押されたことを検出するには、KeyPressMaskを利用します。
次に、実際にイベントを受け取る手順を述べます。イベントを受け取るには、XNextEventを利用します。
XNextEventは、XEvent*で指定された構造体にXサーバからのイベントを受け取ります。XNextEventは、Xサーバにイベントが無いときには、イベントが生じるまで待ちます。(XNextEventのmanより)
受け取ったイベントはXEventのtype要素を用いて分類されます。それぞれのイベントに応じて行う行動を記述すれば、イベントに対して対応できます。
ここでは、ButtonPressだけを扱います。
ButtonPress以外のイベントも扱いたいなら、case文を増やしていけば対応できます。XNextEventで使ったevはtype以外にもいくつかの要素を持っています。これを扱うには、対応するイベント構造体へのキャストを利用する必要があります。ButtonPressならevに対して、
(program/xev/xev.cより)とします。これで、eは、evの要素を持つXButtonEvent型の変数となりました。XButtonEventにはx, y, buttonなどの変数があります。(XButtonEventのmanより)これらを利用するには、ButtonPressのcase文下のprintfを、
とすればボタンが押された時のカーソルの座標と押されたボタンの種類がわかります。
イベントについて更に詳しく知りたいときには、
を利用するとよいでしょう。xevは、Xが扱うことができるあらゆるイベントを記述する簡潔なXクライアントです。ソースはprogram/xev/xev.cにあり、イベントの要素を取り出す方法が多く見つかります。
Windows上のフリーウェアでは時計がよく見られます。実際には時計のプログラミングは非常に簡単で、サンプルとしても適切です。ここでは、Xを用いて簡単なデジタル時計を作ります。
まず、CUIベースで定期的に時間を表示するプログラムを利用します。実際にはここまでで時計としての役目は果たしており、Xを利用するのはより見映えをよくするためです。
C言語のライブラリには、time()という関数があります。この関数はコンピュータが保持している現在時刻を返す関数です。この関数を利用して現在の時、分、秒を獲得し、定期的に表示することが当面の目的となります。実際に現在の時刻を獲得するには次のようなプログラムを書くことになります(このプログラムはGNU/Linux上でコンパイルできることを確認していますが、異なったシステム上では関数の使い方が違っているかもしれません。)。
ここでtime()は現在時刻を1970年1月1日00:00:00(UTC)からの経過秒を返します(timeのmanより)。ここで得た値は読みやすくないので、localtime()関数を利用して現在の時刻に変換します。localtimeの返り値はstruct tm* であり、この構造体は秒、分、時間などの情報をintとして持っています。具体的には tm_hour, tm_min, tm_sec などが対応する値です(localtimeのmanより)。
得られた値を定期的に表示するにはwhile(1)などの無限ループを利用します。単純にループをするとw:CPUを専有するので、sleepを利用して時間を取ります。ここでは1秒ごとに結果を表示するので、1秒ずつ休めばよいでしょう。ループは次のようになります。
ここで、sleep(m)はm秒休みます(sleepのmanより)。
ここまででCUIベースで時計を作る方法を述べました。GUIを利用するときでも時刻を取得するまでは同一で、時刻の表示が変わるだけです。ここでは表示する数字をbitmapを使って、ビットマップとして作成する方針を取ります。既に述べたようにbitmapを利用すると最大2色しか色が使えません。そのかわり、ファイルサイズが小さくてすみます。ここではサンプルなのでこれでよいでしょう。
数字を絵で表わすには様々な方法がありますが、ここではw:電卓などで用いられる方法を利用します。例えば、2は
のようになります。これらを保存するには幅3、高さ5のビットマップがあれば十分です。
Xで利用するビットマップを作成するには
を利用すると便利です。bitmapはXに付属のGUIツールです。詳しい使い方はbitmapのmanを参照して下さい。それぞれのファイルにone.xbm, two.xbm, ... などのファイル名をつけます(w:en:xbmは、w:ImageMagickのidentifyを用いてつけたものです。この名前は任意のものにして構いません。)。このときxbmの文字列はone_bits, two_bits, ... などの文字列を定義するファイルになります。
ここで、ビットマップファイルを定義する方法を説明します。bitmapでは、それぞれのピクセルを整数型のビットに対応させ、対応するビットが0か1かで絵を表わします。上の2の例でいえば、第1行は3つのビットが全て埋まっているので111という整数を対応させます。これは16進数では0x7に対応するので第1行は0x7に対応します。更に第2行は100に対応するので値は0x4です。同じ様に値を当てはめると、
の5つの数値が上の図形を表わすことがわかります。実際にbitmapの出力を見ると確かにこれが正しいことがわかります。
Xlibはプログラム中でbitmapを利用するために、いくつかの関数を提供しています。しかし、bitmapはw:モノクロのディスプレイで使うことを想定しているためか、多くの関数はw:色深度が24bitのディスプレイでは使えません。一方、現在のほとんどの家庭用コンピュータは24bit以上の色深度を持っています。
大きい色深度を持っているシステムではbitmapを利用するために
を利用します。fg, bgはそれぞれ前景色(foreground color), 背景色(background color)を指しており、与えたい色を指定します。これらはそれぞれ数字の色と背景の色になるので適当な色を定めて下さい。dataにはビットマップを定義する文字ポインタを与えます。ここでは、one_bits, two_bits, ... などが対応するデータとなります。
ここでは簡単のため、Pixmapの配列を作り、pix[0], ... , pix[9] がそれぞれ0, ..., 9 の数字に対応するとします。
ここまでで10個の数字を定めたPixmapを作成しました。これらをWindow上に表示するには
を利用します。XCopyAreaは2つのDrawable間で絵をコピーする関数です。詳しい使い方はXCopyAreaのmanを参照して下さい。
printf関数で表示を行なうときと違って2桁の数は桁ごとに取得する必要があります。2桁の数の10の位、1の位を取得するには2桁の数を
として、
と
がそれぞれ対応します。例えば秒を表示する時計を作るには、
とすれば正しく数字が表示されるでしょう。
ここまでで基本的な時計の作り方を見ました。時計については全体のデザインや数字の使い方などで様々な工夫がなされています。(w:ベクター、w:窓の杜を参照)好みの時計を探してみるとよいでしょう。