C,GDK,Cairoで"何も無い"画面に自由な描画をする



# 概要
プリミティブ好きかい？うん！大好きさ！
というわけで、本記事では僕がGDKの練習用に制作したプログラムを紹介します。

「X11をいじってみたいけどXlibはめんどくさそう ーー というわけでGDKを使いたい」といった方、あるいはプリミティブな環境での描画に興味のある方、是非参考にしてみてください。

### やること

デスクトップ環境もウィンドウマネージャもない、
ただただ真っ暗闇の画面の上で、自由な描画を行います。

なお、本記事で使用する言語は全て**C言語**です。
また、ここでは**X11を搭載したOS**でのビルド及び実行を前提としています。
(Uinx系/Linux系OSならたいてい動くと思います MacOSは知らないけど...)

### 完成像
(クソ荒い画像ですが許して...)

![recorded_2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/558987/17e2c076-98ca-144a-6cdc-e1aab9575f8e.gif)

### 本記事での実行環境

Ubuntu 19.10


### 前提知識
 
X11についてのある程度の知識、Cairoの基本的な使い方は知っている前提とします。

Cairoについて知らない場合は、公式レファレンスでも見ながら...

また、X11(Xlib)及びGDK or GTKについての知識や開発経験などがあれば、より読みやすいと思います。

あと、GDKについてはマイナーなので、下記で簡単に説明しておきます。


##### GDK (GDK3) についての簡単な説明
端的に言うと、X11とかのラッパーライブラリです。(X11以外のフレームワークにも対応するみたいですが)
GUIツールキットである**GTK**プロジェクト内に存在するライブラリのため、本来はGTKを使用したプログラムの中で利用されることが多いですが、独立したライブラリとしてもちゃんと機能します。



# 仕組み

X11における**root window(根ウィンドウ)**にCairoのコンテキストを直接描画することで、”極限まで”プリミティブな環境での自由な描画を実現します。
(極端な話”最も“とは言いにくいが…まあ少なくとも”最もに限りなく近い”とは言える)

X11の機能にはGDKからアクセスします。

# 環境構築 / ビルド


**再度申しますが、ここではX11を搭載したOSでのビルド及び実行を前提としています。**

GTKをインストールしたほうが早いです。
(GDKだけの導入もできるのかもしれないのですが...本人、GDKを個別でインストールしたことがなく、説明できないだけです...)

まあ、パパっとしちゃいましょう

##### 導入

``` console:Debian系の場合
$ sudo apt install libgtk-3-dev
```

テストするときは、[GTKの公式チュートリアル](https://developer.gnome.org/gtk3/stable/gtk-getting-started.html)などから適当に貼り付けてビルドしましょう

##### ビルド

[cc]にはgcc、clang等のコンパイラのコマンドを各自指定してください。

pkg-configを使いましょう。

``` console:GTKと一緒にビルド
[cc] -Os -pedantic -pthread -Wall test.c  $(pkg-config --libs --cflags gtk+-3.0) -o test
```

また、本記事のプログラムの場合ではGDKのみを利用するため、GTKを参照せずに必要な分だけでビルドすることもできます。ただしpkg-configはないので...長い

```console:必要なものだけでビルド。長い！
[cc] -Os -pedantic -pthread -Wall test.c -I/usr/include/gtk-3.0 -I/usr/include/cairo -I/usr/include/pango-1.0 -I/usr/include/glib-2.0 -I/usr/include/gdk-pixbuf-2.0 -I/usr/include/glib-2.0/include -lgdk-3 -lcairo -o test
```

詳しくはこちら : [GTK公式リファレンス](https://developer.gnome.org/gtk3/stable/gtk-compiling.html)

しかしながら、どういった状況にしろ、個人的には前者をおすすめします。ビルドしてしまえば双方の差異はほとんどありませんし、なんせ後者だと環境によってインクルードするディレクトリが若干変わってくる可能性があるので、随時トラブルシューティングしなければならずビルドまでの手間が多くなってしまう可能性があります。ですから、相当几帳面な人でなければ前者の方がかなり楽です。

では、ビルド環境ができたら、いよいよ主題に入ります。

# 主題

まず、Cairoのコンテキストをroot windowに適用する為の下地プログラムを作成します。

途中の「 . . . 」にはCairoの描画関数が入ります。

### ベースとなるプログラム

```c:xtest.c

#include <gdk/gdk.h>

#include <stdio.h>
#include <stdbool.h>

// 前宣言
GdkDisplay * display;
GdkScreen * screen;
GdkWindow * win_root;

GdkDrawingContext * win_root_context;
cairo_t * win_root_cr;
cairo_region_t * win_root_region;

// ループのたびに1加算される変数
int clk = 0;

bool main_loop() {

    // 描画コンテキストの呼び出し・描画の開始
    // [ gdk_window_begin_draw_frame(GdkWindow *, cairo_region_t *) : GdkDrawingContext * ] cairo_region_t * に定義された描画範囲の描画を初期化&有効化/描画コンテキストの出力 (ここでは範囲はmain()にて定義済み)
    // [ gdk_drawing_context_get_cairo_context (GdkDrawingContext *) : cairo_t * ] GDKの描画コンテキストからCairoコンテキストを取得(紛らわしい！) 
    win_root_context = gdk_window_begin_draw_frame(win_root, win_root_region);
    win_root_cr = gdk_drawing_context_get_cairo_context (win_root_context);

    // --- Cairo ---

        .
        .
        .

    // -------

    // 描画の終了
    // [ gdk_window_begin_draw_frame(GdkWindow *, cairo_region_t *) ] gdk_window_begin_draw_frame で有効化された描画範囲を無効化
    gdk_window_end_draw_frame(win_root, win_root_context);

    // 一定時間が経過したら、終了。
    return (++clk < 3000);
}



int main(){

    // ディスプレイの呼び出し
    display = gdk_display_open("");
    if(display == NULL){
        printf("couldn't open display\n");
        return 0;
    } 

    // スクリーンの呼び出し
    screen = gdk_display_get_default_screen(display);
    if(screen == NULL){
        printf("couldn't open screen\n");
        return 0;
    }

    // rootウインドウの呼び出し
    win_root = gdk_screen_get_root_window(screen);
    if(win_root == NULL){
        printf("couldn't open window\n");
        return 0;
    }

    // Cairoでの描画範囲を指定。
    // [ gdk_window_get_witdh/height(GdkWindow *) : guint ] GDKウインドウ幅/高さを取得
    // ここでは全体
    cairo_rectangle_int_t win_root_region_rect =
        {0, 0,
        gdk_window_get_width (win_root),
        gdk_window_get_height(win_root)};
    
    // 上記指定した描画範囲を適用
    // [ cairo_region_create_rectangle(cairo_rectangle_int_t *) : cairo_region_t * ] 指定矩形範囲の描画領域を作成
    win_root_region = cairo_region_create_rectangle(&win_root_region_rect);

    // rootウインドウの可視化
    gdk_window_show(win_root);

    // メインループ。main_loopがfalseを返したらおわり
    while(main_loop());
}
```


試しに、途中の「 . . . 」を消してビルドしてみてください。
エラーがなければOK。

### 試験実行

さて、続いて実行に入ります。
この時点で生成された実行ファイルですが、一度普通に実行してみましょう。

…何も起こりません。
何の出力もなく、一定時間後に勝手に処理が終了すればOKです。

では、どのようにして実行するかと言いますと、`startx`コマンドを使います。(まあ普通
X11の初期化処理時に参照されるファイル`~/.xinitrc `内に必要なコマンドを入れておくことで、startxコマンドを使った時にさっきの実行ファイルを最初に実行することができます。
そこで初めて描画内容が反映されます。

それでは、`~/.xinitrc`を次のように設定し、新規保存(or上書き保存)しましょう。


```console:.xinitrc
exec /path/to/execute/file
```

…まあ、といった感じですね。

というわけで、`startx`を実行しましょう。
ただしその前に、GUIが無効になっているか、GUI無しの別ttyに移行しているか、どちらかの状態になっていることを確認してください。
(なっていない場合、実行はうまくいきません。既に有効になっているXサーバが存在し、初期化処理ができないからです。)

Ubuntuの場合は`Ctrl`+`alt`+`f1`(f1でできない場合はf2、f3...も試してみてください。f1でtty1、f2でtty2...に移行します)で、即座に別ttyに移行できます。
戻る場合は、GUIが有効化されていたttyを探しましょう。


```console
$ startx
```

真っ暗の画面が出てきて、一定時間に勝手に終了したら成功です。
**準備段階終了**

___

それでは、この「 . . . 」の部分を編集し、図形を描画する作業に入ります。


### 描画

さて、さっきのコード内に

```c
    // --- Cairo ---

        .
        .
        .

    // ------- //
```

といった部分があると思います。
ここに、Cairoの描画関数を実装することになります。

領域内に次の文を追加しましょう。

##### モデル1

```c:xtest_0.c

    // --- Cairo ---

    // 矩形を生成
    cairo_set_source_rgba(win_root_cr, 0.5,0.5,0.5,1.0);

    // 塗り潰し
    cairo_rectangle(win_root_cr, (clk/2)%2000,100,500,500);
    cairo_fill_preserve(win_root_cr);

    // 枠線
    cairo_set_source_rgba(win_root_cr, 1.0,1.0,1.0,1.0);
    cairo_set_line_width(win_root_cr, 1.0);
    cairo_stroke(win_root_cr);
    
    // ------- //

```

実行すると…

![recorded_2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/558987/bbf35605-5c17-0059-4006-989ff9e9701e.gif)

**なんか映りましたね？？なんか映ってください！！(映るはず)**


これが映れば、違うモデルでもちゃんと動きます。
さっきのはPerukii謹製のモデルだったのですが、今度は[Cairo samples](https://www.cairographics.org/samples/)から引用していろいろ動かしてみましょう。

##### モデル2

```c:xtest_1.c

    // --- Cairo ---

    // Cairo samplesでは cr となっているものはCairoコンテキスト。
    // ここでは win_root_cr となります

    double xc = 512.0;
    double yc = 512.0;
    double radius = 500.0;
    double angle1 = 45.0  * (M_PI/180.0);  /* angles are specified */
    double angle2 = 180.0 * (M_PI/180.0);  /* in radians           */

    cairo_set_line_width (win_root_cr, 10.0);
    cairo_arc (win_root_cr, xc, yc, radius, angle1, angle2);
    cairo_stroke (win_root_cr);

    /* draw helping lines */
    cairo_set_source_rgba (win_root_cr, 1, 0.2, 0.2, 0.6);
    cairo_set_line_width (win_root_cr, 6.0);

    cairo_arc (win_root_cr, xc, yc, 10.0, 0, 2*M_PI);
    cairo_fill (win_root_cr);

    cairo_arc (win_root_cr, xc, yc, radius, angle1, angle1);
    cairo_line_to (win_root_cr, xc, yc);
    cairo_arc (win_root_cr, xc, yc, radius, angle2, angle2);
    cairo_line_to (win_root_cr, xc, yc);
    cairo_stroke (win_root_cr);
    
    // ------- //

```

![Screenshot from 2020-05-18 09-25-46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/558987/8af0fd03-f5a3-ed1b-b91c-d64afc0fd5a7.png)

##### モデル3

```c:xtest_2.c

    // --- Cairo ---

    int              w, h;
    cairo_surface_t *image;

    image = cairo_image_surface_create_from_png("Image.png");
    w = cairo_image_surface_get_width (image);
    h = cairo_image_surface_get_height (image);

    cairo_translate (win_root_cr, 128.0, 128.0);
    cairo_rotate (win_root_cr, 45* M_PI/180);
    cairo_scale  (win_root_cr, 256.0/w, 256.0/h);
    cairo_translate (win_root_cr, -0.5*w, -0.5*h);

    cairo_set_source_surface (win_root_cr, image, 0, 0);
    cairo_paint (win_root_cr);
    cairo_surface_destroy (image);

    // ------- //

```

![Screenshot from 2020-05-18 09-54-28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/558987/c29627e9-9dd1-0a36-07ee-7a445a9f89f2.png)

あらかわいい

### ちなみに...

本記事のプログラムではwhileでループしていますが、GTKを使えば`g_timed_out`、`gtk_main`、`gtk_main_quit`を使ってもっと正確な間隔での描画ができます。

また、X11の終了条件は「一番最初に起動したタスクが終了した時」ですから、
例えば`~/.xinitrc`を

```console:.xinitrc
exec /path/to/execute/file & [アプリケーション]
```
とすると、whileループや`gtk_main` を通さずとも[アプリケーション]が終了するまでずっと描画していてくれます。
(ただし1度行われた描画の情報が残るだけなので、描画内容の更新はできません)

[アプリケーション]には、例えば`xterm`とかのターミナルを入れると、その他のタスクも追加で動かすことができるようになります。これを使って、キャプチャソフトやスクリーンショットソフトを起動すれば、描画内容のキャプチャもできます。


# まとめ

GDKを使ってプリミティブな描画をしました。
この記事を機に、当界隈の人口をもっと増やせればなあと思います(望み薄)。
おわり。