X11: ウィンドウ背景とピクセル値

ウィンドウ背景
前回のプログラムでは、ウィンドウ属性の background_pixel に 0 を設定することで、ウィンドウの背景色を黒にしました。
(モニタがインデックスカラーの場合は、黒になるとは限りません)

この場合は、ウィンドウをリサイズした時など、再描画が必要なタイミングでは常にウィンドウの背景が描画されるので、何も描画しなくても問題はありませんでした。

しかし、X ウィンドウは基本的にウィンドウの中身 (描画内容) を保持しないため、再描画が必要なタイミングが来た時 (ウィンドウが表示された時やリサイズ時) は、クライアントがウィンドウに対して、適切な描画を行う必要があります。
背景色とパターン
ウィンドウ属性に、ウィンドウの背景色、または Pixmap (タイルパターン) が設定されていた場合、ウィンドウの再描画が必要になる時は、まず、その背景色かパターンで、ウィンドウの背景が描画されます。

ウィンドウに背景色もパターンも設定されていない場合 (デフォルトの場合)、ウィンドウの中身は未定義となります。

その場合は、クライアントが常に、ウィンドウへの描画を行う必要があります。
再描画が必要なタイミングで何も描画しなかった場合、そのウィンドウの後ろにある画面の状態が、ウィンドウの中身になったりします。

基本的に、ウィンドウの中身はすべてクライアントが描画するべきなので、ウィンドウ背景は、通常のウィンドウでは設定しません。
クライアントがウィンドウの中身全体を描画しているのに、ウィンドウ背景を設定してしまうと、X サーバーがウィンドウの背景を描画した後、そこからさらにクライアントの描画が行われてしまうので、背景の描画が無駄に行われてしまいます。
ピクセル値
ウィンドウ属性の background_pixel と border_pixel に設定するのは、色の「ピクセル値」です。これは RGB 値ではありません。

ウィンドウのビジュアルが 256 色パレットなどの場合は、パレットのインデックスになります。
クラスが TrueColor か DirectColor の場合は、unsigned long 値 (実際に使われるのは 32bit) に、RGB の3つの値を埋め込みます。
カラーマップからピクセル値を取得
カラーマップは、ディスプレイに表示する色の管理を行うためのものです。
パレットカラーの場合は、カラーマップに、使用する色を登録する形になります。

RGB 値からピクセル値を取得したい場合は、以下のようになります。

Screen *scr = DefaultScreenOfDisplay(display);
XColor col;

//0〜65535
col.red = red;
col.green = green;
col.blue = blue;
col.flags = DoRed | DoGreen | DoBlue; //使用するメンバ

XQueryColor(display, DefaultColormapOfScreen(display, scr), &col);

printf("pixel: %lu\n", col.pixel);

スクリーンにはデフォルトのカラーマップが存在するため、それを使います。
XQueryColor() は、指定したカラーマップを使って、RGB 値からピクセル値を取得します。

※XColor 構造体の red, green, blue メンバの値は、0〜65535 の範囲なので、注意してください。
白は、reg,green,blue = 65535 となります。
独自にピクセル値を計算
RGB 値を元にして色を描画したい場合、上記のように毎回 XQueryColor() を呼び出していたのでは、色の変換に時間がかかります。

現在のモニタでは、16,24,32 bit のハイカラーまたはフルカラーが使われるのが一般的であるため、その環境のみを対象とするのであれば、X 関数を使わずに、計算のみで RGB 値からピクセル値への変換を行うことができます。
(ルートウィンドウの depth が 15,16,24,32bit の場合)

TrueColor/DirectColor の場合は、ビジュアル情報の red_mask, green_mask, blue_mask の値を使うと、ピクセル値内の各 RGB 値のビット数とシフト数が計算できるので、それを使って色の変換が行えます。
(Visual 構造体のポインタから直接参照するか、XGetVisualInfo 関数を使って XVisualInfo 構造体から値を参照します)

「下位ビットを先頭として、最初にビットが 1 になる位置」がシフト数で、「ビットが 1 になっている数」がビット数になります。
red_mask が 0xff0000 であれば、シフト数は 16 で、ビット数は 8 です。

RGB 値からピクセル値を計算
RGB 値 (0〜255) からピクセル値を求める場合は、以下のような計算になります。

//0〜255 を、各ビット数の範囲に変換
r = r * ((1 << r_bits) - 1) / 255;
g = g * ((1 << g_bits) - 1) / 255;
b = b * ((1 << b_bits) - 1) / 255;

pixel = (r << r_shift) | (g << g_shift) | (b << b_shift);

16bit カラーの場合は、R,G,B がそれぞれ 5bit か 6bit になるので、0〜255 の値を 0〜31 or 0〜63 に変換する必要があります。

ピクセル値から RGB 値を計算
ピクセル値から RGB 値 (0〜255) を求める場合は、以下のようになります。

r = (pixel & r_mask) >> r_shift
g = (pixel & g_mask) >> g_shift
b = (pixel & b_mask) >> b_shift

//15,16bit の場合、0〜255 の範囲に変換
r = r * 255 / ((1 << r_bits) - 1)
g = g * 255 / ((1 << g_bits) - 1)
b = b * 255 / ((1 << b_bits) - 1)

まず、マスク値で AND して、他の値を 0 にした後、左シフトして、各 RGB 値のビット数の範囲の値を取得します。
16bit カラーの場合は、その後、0〜31 or 0〜63 の値を 0〜255 に変換する必要があります。
プログラム
RGB 値からピクセル値を計算して、背景色を変化させていくプログラムです。

ウィンドウ内で左ボタンを押すたびに、黒→赤→青→白... と、背景色が変更されます。
左ボタン以外を押すと、終了します。

以降、共通で使うような関数は util.c にまとめてあるので、メインページからソースコードをダウンロードして、それも一緒にコンパイルしてください。

$ cc -o run 05a-pixel.c util.c -lX11

<05a-pixel.c>
#include <X11/Xlib.h>
#include "util.h"

unsigned int g_colors[] = {0,0xff0000, 0x00ff00, 0x0000ff, 0xffffff, 0xffff00, 0xff00ff, 0x00ffff};
int g_current = 0;

int main(int argc,char **argv)
{
    Display *disp;
    XSetWindowAttributes attr;
    Window win;
    XEvent ev;
    
    disp = XOpenDisplay(NULL);
    if(!disp) return 1;

    set_display(disp);

    //ウィンドウ作成

    attr.background_pixel = rgb_to_pixel(g_colors[g_current]);
    attr.event_mask = ButtonPressMask;

    win = XCreateWindow(disp, DefaultRootWindow(disp),
        0, 0, 200, 200, 0,
        CopyFromParent, CopyFromParent, CopyFromParent,
        CWBackPixel | CWEventMask, &attr);

    XMapWindow(disp, win);

    //イベント

    while(1)
    {
        XNextEvent(disp, &ev);

        switch(ev.type)
        {
            case ButtonPress:
                if(ev.xbutton.button == Button1)
                {
                    g_current++;
                    if(g_current == sizeof(g_colors) / sizeof(int)) g_current = 0;
                    
                    XSetWindowBackground(disp, win, rgb_to_pixel(g_colors[g_current]));
                    XClearWindow(disp, win);
                }
                else
                    goto END;
        }
    }

    //

END:
    XDestroyWindow(disp, win);

    XCloseDisplay(disp);

    return 0;
}
背景色の変更
void XSetWindowBackground(Display *display, Window w, unsigned long background_pixel);

ウィンドウの背景色を変更します。
指定ピクセル値で埋められた Pixmap を使用する形になります。
※ウィンドウは再描画されません。
ウィンドウ全体を背景で再描画
void XClearWindow(Display *display, Window w);

ウィンドウ全体の範囲を、ウィンドウ背景で再描画します。
ルートウィンドウの背景
ルートウィンドウは、モニタ全体を覆っている、最上位のウィンドウです。
1つのスクリーンには、必ず一つだけあります。

ルートウィンドウはすべてのウィンドウの背面に表示されるので、「ルートウィンドウの背景=画面の背景色または壁紙」と考えることができます。

つまり、ルートウィンドウの背景色を設定すれば、その色で塗りつぶされ、背景の Pixmap を設定すれば、その画像が表示されることになります。

X のデスクトップにおける背景色や壁紙は、この方法で設定されています。

※ルートウィンドウの上に、それを覆うようなデスクトップのウィンドウが重ねられている場合、この方法では変更できません。
プログラム
ルートウィンドウの背景色を青に設定するプログラムです。

一度設定すると、X サーバーが起動している間は、永続的に設定が維持されます。
元に戻したい場合は、ログアウトしてください。

$ cc -o run 05b-rootbkgnd.c util.c -lX11

05b-rootbkgnd.c
#include <X11/Xlib.h>
#include "util.h"

int main(int argc,char **argv)
{
    Display *disp;
    Window root;
    
    disp = XOpenDisplay(NULL);
    if(!disp) return 1;

    set_display(disp);

    //ルートウィンドウの背景を変更

    root = DefaultRootWindow(disp);

    XSetWindowBackground(disp, root, rgb_to_pixel(0x0000ff));

    XClearWindow(disp, root);

    XCloseDisplay(disp);

    return 0;
}

X.Org の xsetroot コマンドで、「$ xsetroot -solid "#0000ff"」を実行すると、同じことができます。

XClearWindow() を実行しなかった場合、背景色はすぐには適用されません。
ウィンドウを動かしたり、サイズを変更したりすると、その部分だけ背景が変わります。

なお、ウィンドウの背景属性は、X 関数を使って取得することができないので、前の値に戻すことはできません。