SDL2 で GUI 部品を使う為のソース

チュートリアル

　ここでは、SDL_gui の具体的なプログラミングの方法を示します。最初から順番に呼んでいってください。ラベルの説明は他の部品に共通な事柄も説明しています。ボタンでは、マウス操作の方法を説明しています。

目次
 01 ラベル(01_gui_label.c)
 02 ボタン(02_gui_button.c)
 03 ドロップダウンリスト(03_gui_list.c)
 04 チェックボックス(04_gui_checkbox.c)
 05 ラジオボタン(05_radiobutton.c)
 06 テキストボックス(06_textinput.c)
 07 ダイアログボックス(07_dialog.c)
 08 描画のカスタマイズ例(08_example.c)
 09 その他
 10 コールバック関数について

01 ラベル

　前提知識として、SDL でウィンドウを作成する知識が必要です。

　ウィンドウを表示し、タイマーを使って定期的に描画を行う次の内容が理解できれば十分です。定期的に描画を行うのは、ボタンクリック時等でもたつきを感じさせないためです。そのため、タイマー間隔は 100 ミリ秒にしています。

#include <stdio.h>
#include <SDL.h>
#include <SDL_ttf.h>

#define SCR_WIDTH  320
#define SCR_HEIGHT 240

/* 描画に使うレンダラ */
SDL_Renderer *renderer = NULL;

/* 終了フラグ */
int quit_flg = 1;

/* フォントファイル（実在するフォントに変えてください） */
static char *font_path ="/usr/share/fonts/truetype/fonts-japanese-gothic.ttf";

/* 色の定義 */
SDL_Color darkkhaki = {0xbd, 0xb7, 0x6b, 0xff};

/* ウィンドウ内描画 */
static void draw(void *parm) {
    SDL_Renderer *renderer;
    SDL_Rect rect;

    renderer = (SDL_Renderer *)parm;

    SDL_SetRenderDrawColor(renderer, darkkhaki.r, darkkhaki.g, darkkhaki.b, SDL_ALPHA_OPAQUE);
    SDL_RenderClear(renderer);

    SDL_RenderPresent(renderer);
}

static Uint32 callbackfunc(Uint32 interval, void *param)
{
    SDL_Event event;
    SDL_UserEvent userevent;

    /* コールバックでSDL_USEREVENTイベントをキューに入れる。
    このコールバック関数は一定の周期で再び呼ばれる */

    userevent.type = SDL_USEREVENT;
    userevent.code = 0;
    userevent.data1 = &draw;
    userevent.data2 = param;

    event.type = SDL_USEREVENT;
    event.user = userevent;

    SDL_PushEvent(&event);
    return(interval);
}

int main(int argc, char* argv[]){
    SDL_Window *window;
    TTF_Font *font;
    SDL_Event event;
    SDL_TimerID my_timer_id;

    /* 初期化 */
    if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_TIMER) < 0) {
        printf("SDL_Init(): %s\n", SDL_GetError());
        exit(1);
    }
    TTF_Init();

    /* フォント取得 */
    font = TTF_OpenFont(font_path, 18);
    if( font == NULL ) {
        printf("Can not open font(%s)\n", font_path);
        exit(1);
    }

    /* ウィンドウ作成 */
    window = SDL_CreateWindow("GUI Test", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, SCR_WIDTH, SCR_HEIGHT, SDL_WINDOW_OPENGL);
    if( window == NULL ) {
        printf("Can not create window\n");
        exit(1);
    }
    renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED);
    if( renderer == NULL ) {
        printf("Can not create renderer\n");
        exit(1);
    }
    SDL_RenderSetLogicalSize(renderer, SCR_WIDTH, SCR_HEIGHT);

    my_timer_id = SDL_AddTimer(100, callbackfunc, (void *)renderer);
    quit_flg = 1;

    /* イベントループ */
    while( quit_flg == 1 ) {
        if( SDL_PollEvent(&event) ) {
            switch (event.type) {
            case SDL_USEREVENT: {
                void (*p) (void*) = event.user.data1;
                p(event.user.data2);
                break;
            }

            /* ループ終了 */
            case SDL_QUIT:
                quit_flg = 0;
                break;
            }
        } else
            SDL_Delay(100);
    }

    /* 終了処理 */
    if( my_timer_id ) SDL_RemoveTimer(my_timer_id);
    if( font ) TTF_CloseFont(font);
    if( renderer ) SDL_DestroyRenderer(renderer);
    if( window ) SDL_DestroyWindow(window);
    SDL_Quit();

    return 0;
}

　それでは、上のソースにラベルを追加していきます。

　まずは、ヘッダファイル、SDL_gui.h をインクルードします。

#include <stdio.h>
#include <SDL.h>
#include <SDL_ttf.h>
#include "SDL_gui.h"

　描画するときに、部品の先頭が必要なので、先頭の部品 ID 用の変数を定義します。

/* 部品の先頭(ルート) */
Uint32 ptop = 0;

/* 描画に使うレンダラ */

　ptop という名前にしました。部品を登録する際にこの変数を使っていきます。

　描画処理の中で、部品を描画する関数を呼び出します。(draw() 関数内)

    SDL_RenderClear(renderer);

    /* GUI 部品の描画 */
    draw_parts(renderer, ptop);

    SDL_RenderPresent(renderer);

  ラベルを作っていきます。今回はコールバック関数は設定しませんが、ボタンと同様にクリックされたときに関数を呼び出すこともできます。見た目が違うだけで、機能としては他の部品と代わりはありません。

　色を定義しておきます。black は文字の色、pink はラベルに使います。

/* 色の定義 */
SDL_Color black = {0, 0, 0, 0xff};
SDL_Color dimgray = {0x69, 0x69, 0x69, 0xff};
SDL_Color pink = {0xff, 0xc0, 0xcb, 0xff};
SDL_Color darkkhaki = {0xbd, 0xb7, 0x6b, 0xff};

　部品を作る際、表示位置とサイズを SDL_Rect * 型で関数に渡すので、変数を定義します。(main() 関数内)

    SDL_TimerID my_timer_id;
    SDL_Rect rect;

　テキストの色と、ウィンドウサイズを設定します。GUI 部品を追加する前に呼び出してください。その後、ラベルを登録します。(main() 関数内)

    my_timer_id = SDL_AddTimer(100, callbackfunc, (void *)renderer);
    quit_flg = 1;

    /* GUI 部品設定 */
    parts_set_textcolor(black, dimgray);  // テキストカラー
    parts_set_windowsize(SCR_WIDTH, SCR_HEIGHT);
    /* 年齢ラベルの作成(この部品をルート部品にします) */
    /* 左上の座標が(10, 10)で幅 30, 高さ 20 のラベル */
    rect.x = 10;
    rect.y = 10;
    rect.w = 60;
    rect.h = 20;
    ptop = add_parts(0, &rect, NULL, LABEL, "年齢", font, pink);

　parts_set_textcolor() 関数でテキストの色を黒色にします。テキストの色は部品に共通なので、部品ごとに変えることはできません。

　parts_set_windowsize() 関数でウィンドウの幅と高さを設定します。

　add_parts() 関数でラベル部品を登録します。登録した部品の部品 ID が戻り値なので、 ptop に代入しています。add_parts() 関数の最初の引数は、部品のリストの先頭部品の ID ですが、このラベルが最初の部品なので、0 を指定します。

　add_parts() 関数が返す部品 ID を利用して、コールバック関数を設定したり、テキストの配置を変えたりできます。今回は、作成だけするので使いません。

　今回は、ラベルにコールバック関数を登録していないので、マウスの処理は必要ありません。マウスの処理については、次のボタンで説明します。

02 ボタン

　ラベルの時と同様に、ボタンの色を決めます。

/* 色の定義 */
SDL_Color black = {0, 0, 0, 0xff};
SDL_Color dimgray = {0x69, 0x69, 0x69, 0xff};
SDL_Color pink = {0xff, 0xc0, 0xcb, 0xff};
SDL_Color darkkhaki = {0xbd, 0xb7, 0x6b, 0xff};
SDL_Color darkgray = {0xa9, 0xa9, 0xa9, 0xff};

　ボタンなので DarkGray ですね。ボタンが押されたときに終了するように、コールバック関数を準備します。

/* ボタンのコールバック関数 */
void fbutton(Uint32 parts, void *data) {
    quit_flg = 0;
}

/* ウィンドウ内描画 */

　コールバック関数は、引数として部品 ID と、部品登録時に指定した値を受け取ります。今回は終了させるだけなので引数は使いません。

  部品にコールバック関数を設定するので、add_parts() 関数の戻り値を受け取れるように変数を定義します。

    SDL_TimerID my_timer_id;
    SDL_Rect rect;
    Uint32 parts;

　部品の登録は次のように行います。

    /* 閉じるボタンの作成 */
    /* ウィンドウの右下に配置します */
    rect.x = 255;
    rect.y = 205;
    rect.w = 60;
    rect.h = 30;
    parts = add_parts(ptop, &rect, NULL, BUTTON, "閉じる", font, darkgray);
    /* ボタンにコールバック関数を設定する */
    parts_set_callback(parts, fbutton);

    /* イベントループ */

　コールバック関数は、parts_set_callback() 関数で行います。部品 ID とその部品に設定する関数を引数にします。（parts_set_callbacl() を関数と言っていますが、実はマクロで定義しています。）

　最後に、マウスの処理をイベントループ内に追加していきます。

            case SDL_MOUSEMOTION:
                mouse_motion_parts(ptop, event.button.x, event.button.y);
                break;

            case SDL_MOUSEBUTTONUP:
                mouse_button_up_parts(ptop, event.button.x, event.button.y);
                break;

            case SDL_MOUSEBUTTONDOWN:
                mouse_button_down_parts(ptop, event.button.x, event.button.y);
                break;

            /* ループ終了 */
            case SDL_QUIT:

　マウス処理は、マウスが動いた時(SDL_MOUSEMOTION)、マウスボタンが押された時(SDL_MOUSEBUTTONDOWN)、マウスボタンが放された時(SDL_MOUSEBUTTONUP) の 3 つのイベントで関数をそれぞれ呼び出します。それぞれの関数の引数は、部品のルート、イベントの有った xy 座標です。

　最後に作成した部品データを解放します。

    if( window ) SDL_DestroyWindow(window);
    free_parts(ptop);
    SDL_Quit();


　これで、ボタンが表示され、ボタンをクリックするとプログラムが終了します。

03 ドロップダウンリスト

　ドロップダウンリストは、マウスボタンを押している間リストを表示し、リストの中から選択する部品です。今回は、年齢を選択するドロップダウンリストを作ります。

　ドロップダウンリストの色を決める。

/* 色の定義 */
SDL_Color black = {0, 0, 0, 0xff};
SDL_Color dimgray = {0x69, 0x69, 0x69, 0xff};
SDL_Color white = {0xff, 0xff, 0xff, 0xff};
SDL_Color pink = {0xff, 0xc0, 0xcb, 0xff};
SDL_Color darkkhaki = {0xbd, 0xb7, 0x6b, 0xff};
SDL_Color darkgray = {0xa9, 0xa9, 0xa9, 0xff};

　白色にします。

　次に、リストデータを定義します。

/* ドロップダウンリストのリストデータ */
char *list[] = { "18歳未満", "18〜30歳", "31〜50歳", "51〜59歳", "60歳以上", NULL };
int list_index = 0;

/* ボタンのコールバック関数 */

　リストデータは文字列のポインタ配列です(char **)。リストの最後は NULL ポインタにします。また、リストの何番目が選択されたか番号を入れる変数も定義します。リストの先頭が 0 番目と数えます。ここで、list_index に入れたものを初期状態の選択にします。

　リストが選択されたときの呼ばれるコールバック関数を定義します。

/* リストのコールバック関数 */
void flist(Uint32 parts, void *data) {
    list_index = (int)data;
}

/* ウィンドウ内描画 */

　コールバック関数には、部品 ID と選択された番号が渡されます。

　ドロップダウンリストを登録します。

    /* 年齢を選択するドロップダウンリスト */
    rect.x = 75;
    rect.y = 10;
    rect.w = 100;
    rect.h = 20;
    parts = add_parts(ptop, &rect, (void *)list_index, DROPDOWNLIST, (char *)list, font, white);
    parts_set_callback(parts, flist);

    /* 閉じるボタンの作成 */

　add_part の 5 番目の引数にリストデータを渡しますが、5 番目の引数は char * なのでキャストしています。

　マウスのイベント処理はボタンを見てください。

04 チェックボックス

　チェックボックスを作ります。チェックボックスの色はウィンドウと同じ色でいいので、新たに定義する必要はありません。今回はチェックの状態を保存する変数の定義を行います。

/* チェックボックスの状態取得 */
int check = 0;

/* ボタンのコールバック関数 */

　check は、0 なら未チェック状態、1 ならチェック状態でチェックボタンが作られます。

　コールバック関数で、状態が変わったら 変数 check に代入するようにします。

/* チェックボックスのコールバック関数 */
void fcheck(Uint32 parts, void *data) {
    check = (int)data;
}

/* ウィンドウ内描画 */

　チェックの状態はコールバック関数の 2 番目の引数で渡されます。0 のときが未チェックで 1 の時にチェックされています。

　チェックボタンを登録します。

    /* チェックボックスの作成 */
    rect.x = 10;
    rect.y = 205;
    rect.w = 200;
    rect.h = 30;
    parts = add_parts(ptop, &rect, (void *)check, CHECKBOX, "利用規約に同意しま
す", font, darkkhaki);
    parts_set_callback(parts, fcheck);

    /* 閉じるボタンの作成 */

　これでチェックボックスが作成されます。

05 ラジオボタン

　ラジオボタンで性別を選択できるようにします。ラジオボタンの色もウィンドウの色と同じでいいです。

　ラジオボタンは、一つの変数を複数のラジオボタンに設定することで、一つのグループになります。その変数には、選択されたラジオボタンの部品 ID が入ります。そのため、後でどのボタンが押されたかどうか調べるために、ラジオボタンの部品 ID の配列を定義しておきます。

/* ラジオボタン用データ */
Uint32 group = 0;
int sex = 0;
char *radio_item[2] = { "男性", "女性" };
Uint32 radio_parts[2] = { 0, 0 };

　変数 group は、まとめたいラジオボタンに設定する変数です。この変数には、自動的に選択されたラジオボタンの部品 ID が入ります。

　変数 sex には、選択されたラジオボタンの番号を入れます。

　配列 radio_item は、ラジオボタンに表示する文字列です。

　配列 radio_parts には作成したラジオボタンの部品 ID を入れます。

　コールバック関数は以下のようにします。

/* ラジオボタンのコールバック関数 */
void fradiobutton(Uint32 parts, void *data) {
    int i;

    for( i = 0; i < 2; i++ ) {
        if( radio_parts[i] == *((Uint32 *)data) )
            break;
    }
    sex = i;
}

/* ウィンドウ内描画 */

　コールバック関数の 2 番目の引数に選択されたラジオボタンの部品 ID が渡されます。それを、配列 radio_part から探して、何番目のラジオボタンが押されたのかを変数 sex に代入します。

　ラジオボタンを登録しますが、ラジオボタン以外に、ラベルも登録します。


    /* 性別ラベルの作成 */
    rect.x = 10;
    rect.y = 35;
    rect.w = 60;
    rect.h = 20;
    add_parts(ptop, &rect, NULL, LABEL, "性別", font, pink);

    /* 性別選択ラジオボタンの作成 */
    rect.x = 75;
    rect.y = 35;
    rect.w = 60;
    rect.h = 20;
    radio_parts[0] = add_parts(ptop, &rect, (void *)&group, RADIOBUTTON, radio_item[0], font, darkkhaki);
    parts_set_callback(radio_parts[0], fradiobutton);
    rect.x = 140;
    rect.y = 35;
    rect.w = 60;
    rect.h = 20;
    radio_parts[1] = add_parts(ptop, &rect, (void *)&group, RADIOBUTTON, radio_item[1], font, darkkhaki);
    parts_set_callback(radio_parts[1], fradiobutton);
    /* 初期状態で選択されているラジオボタンの設定 */
    group = radio_parts[0];

　ラジオボタンの登録では、3 番目の引数に変数 group のアドレスを指定します。そして、同じ関数をコールバック関数として設定します。

　登録した順番に部品 ID を配列 radio_part に代入します。

　初期状態ではどのラジオボタンも選択されません。変数 group にラジオボタンの部品 ID を設定すれば、その部品が選択されます。今回は、最初に登録したラジオボタンの部品 ID を設定します。

06 テキストボックス

　テキストボックスは、使う前にバッファの確保が必要になります。バッファはテキストボックスで共有するので、複数のテキストボックスを使う場合でも確保は一度で十分です。

　テキストボックスをクリックすると編集状態になります。編集状態から戻るには、Enter キーをタイプするか、テキストボックス外をクリックします。コールバック関数は編集が確定したこのときに呼ばれます。

　テキストボックスの編集では、コピー＆ペーストは使えません。編集中の文字列の選択もできません。IME で日本語の入力はできます。変換候補も表示されますが、文節の変更等は表示されません。

　入力されたテキストを格納する領域を定義します。デフォルトでテキストボックスに文字列を入れておくには、この領域にその文字列を設定します。何も表示しない場合は、空の文字列を設定します（""）。

/* 氏名入力用テキストボックスの変数 */
char name[80] = "";


/* ボタンのコールバック関数 */

　utf8 は最大 4 バイト使います。上の定義では、最低 20 文字分になります。

　ラベルとテキストボックスを登録します。

    /* お名前ラベルの作成 */
    rect.x = 10;
    rect.y = 60;
    rect.w = 60;
    rect.h = 20;
    add_parts(ptop, &rect, NULL, LABEL, "お名前", font, pink);

    /* 氏名入力用テキストボックスの作成 */
    rect.x = 75;
    rect.y = 60;
    rect.w = 200;
    rect.h = 20;
    add_parts(ptop, &rect, NULL, TEXTINPUT, name, font, white);
    /* テキストバッファの確保 */
    text_buffer_allocate_parts(20);

    /* チェックボックスの作成 */

　add_parts の5 番目の引数は、必ず確保した領域にしてください。

　text_buffer_allocate_parts() 関数でバッファを確保します。引数は確保する文字数です。

　イベントループにテキスト関連のイベント処理を追加していきます。

                break;

            case SDL_TEXTINPUT:
                textinput_parts(event.text.text);
                break;

            case SDL_TEXTEDITING:
                textediting_parts(event.edit.text, event.edit.start, event.edit.length);
                break;

            case SDL_KEYDOWN: {
                keydown_parts(ptop, event.key.keysym.sym);
                break;
            }

            /* ループ終了 */

　SDL_TEXTINPUT, SDL_TEXTEDITING, SDL_KEYDOWN イベントでそれぞれ、textinput_parts() 関数、textediting_parts() 関数、keydown_parts() 関数を呼び出します。

　終了時にテキストバッファを解放します。

    free_text_buffer_parts();
    free_parts(ptop);
    SDL_Quit();

　テキストボックスにコールバック関数も利用できます。コールバック関数は、引数で入力された文字列を受け取ることができます。そこで文字列チェック等を行うことができます。今回は、受け取るだけなので、コールバック関数は必要ありません。

07 ダイアログボックス

　ダイアログボックスは、関数を呼び出すと、ダイアログボックスを表示してその場でイベントループします。

　閉じるボタンのコールバックを修正し、チェックボックスの確認と、終了確認のダイアログボックスを出します。

/* ボタンのコールバック関数 */
void fbutton(Uint32 parts, void *data) {
    int result;

    if( check == 0 ) {
        message_dlg(renderer, 250, 160, "利用規約に同意してください", parts_get_font(parts));
    } else {
        result = ok_cancel_dlg(renderer, 250, 160, "終了してもよろしいですか？", "はい", "いいえ", parts_get_font(parts));
        if( result == 1 )
            quit_flg = 0;
    }
}

 message_dlg() 関数は ok ボタンだけあります。ok_cancel_dlg() 関数は、２つのボタンがあり、左のボタンが押された場合は 1 、右のボタンが押された場合は 0 が返ります。

08 描画のカスタマイズ

　部品の描画をカスタマイズする方法を説明します。

　描画カスタマイズ関数を利用することで、イメージなどを部品に貼り付けることができます。部品を描画した後、テキストを描画する前に、描画カスタマイズ関数は呼ばれます。引数でレンダラと、対象の部品 ID が渡されます。

　コールバック関数と描画カスタマイズ関数を定義します。

/* コールバック関数 */
void quit(Uint32 parts, void *data) {
    quit_flg = 0;
    /* １ 秒後に終了 */
    SDL_Delay(1000);
}

/* 部品描画のカスタマイズ */
void label_draw(SDL_Renderer *renderer, Uint32 parts) {
    SDL_Rect rect;

    rect.x = parts_get_x(parts);
    rect.y = parts_get_y(parts);
    rect.w = parts_get_width(parts);
    rect.h = parts_get_height(parts);

    switch( parts_get_mouse(parts) ) {
    case 1: // マウスダウン
        if( bomb3 != NULL )
            SDL_RenderCopy(renderer, bomb3, NULL, &rect);
        break;
    case 2: // マウスオーバー
        if( bomb2 != NULL )
            SDL_RenderCopy(renderer, bomb2, NULL, &rect);
        break;
    default:
        if( bomb1 != NULL )
            SDL_RenderCopy(renderer, bomb1, NULL, &rect);
        break;
    }
}

　ラベルを登録します。

    /* ラベルの作成(この部品をルート部品にします) */
    rect.x = 20;
    rect.y = 20;
    rect.w = SCR_WIDTH - 40;
    rect.h = SCR_HEIGHT - 40;
    /* テキスト表示なし(5 番目の引数を NULL) */
    ptop = add_parts(0, &rect, NULL, LABEL, NULL, font, pink);
    /* 描画カスタマイズ関数の登録 */
    parts_set_draw(ptop, label_draw);
    /* コールバック関数の登録（ラベルでも登録すればマウスアップで呼ばれます） */
    parts_set_callback(ptop, quit);

　描画カスタマイズ関数を parts_set_draw() 関数で設定します。

　ラベルですが、クリックされたら終了するようにコールバック関数も設定しています。
 
　予めテクスチャは読み込んでおいてください。

09 その他

　GUI 部品のルートを複数作って、複数の GUI 画面を使うことができます。

　parts_hide() 関数を使い、部品を非表示にすることができます。非表示にした部品を再表示するには、parts_unhide() 関数を使います。

　フラットボタンを並べてメニューのようなものを作製することも可能です。

10 コールバック関数について

　コールバック関数の引数の 1 番目は、マウスでクリックされた部品の ID が渡されます。

　2 番目の引数は、void * 型で渡されますが、部品の種類によって渡されるものが異なります。

・LIST, BUTTON, FLATBUTTON, RADIOBUTTON
　リスト、ボタン、フラットボタン、ラジオボタンでは、add_pert() 関数で設定した値(value) がそのまま渡されます。コーツバック関数の中で、必要な型にキャストして使います。

・CHECKBOX
　チェックボックスでは、チェックの状態が渡されます。0 が未チェック、1 がチェックした状態を表します。コールバック関数では、int 型にキャストして使ってください。

・DROPDOWNLIST
　ドロップダウンリストでは、選択されたリストの番号 (最初が 0) が渡されます。int 型にキャストして使ってください。

・TEXTINPUT
　テキストボックスでは、add_parts() 関数の 5 番目の引数で指定した文字列を格納する領域のアドレスが渡されます。この領域には既に入力された文字列が格納してありますchar * 型にキャストして使います。

