SDL2 で GUI 部品を作る(ボタン、チェックボックス)

 GUI 部品のボタンを作りました。マウス操作でボタンも浅れるようにしました。これを独立したソースにまとめます。

 まず、GUI 部品を作るためのデータを決めます。必要なデータは、GUI 部品の種類、部品の左上の x, y 座標、幅、高さ、ボタンに割り当てる値、テキストを表示する位置、表示するテキスト、マウスの状態、クリックされたときのコールバック関数、ボタンの色、文字のフォントです。これらを構造体にします。

ボタンの種類

typedef enum _parts_type {
    LABEL,
    BUTTON,
    CHECKBOX,
} PARTS_TYPE;

 ラベル、ボタン、チェックボックスを作ります。後で部品の種類は増やそうと思います。

文字位置

typedef enum _text_align {
    TEXT_LEFT,
    TEXT_CENTER,
    TEXT_RIGHT
} TEXT_ALIGN;

 文字位置は、左揃え、中央揃え、右揃えが出来るようにします。

GUI 部品のデータを構造体にまとめる

typedef struct _parts {
    Uint32 id;         // チェック用
    int x;             // 部品の左上の x 座標
    int y;             // 部品の左上の y 座標 
    int w;             // 部品の幅
    int h;             // 部品の高さ
    void *value;       // 部品固有の値
    PARTS_TYPE type;   // 部品の種類(LABEL, BUTTON, CHECKBOX)
    int mouse;         // マウスの状態(初期値は必ず 0 にする)
    TEXT_ALIGN align;  // テキストの表示位置
    char *string;      // 部品に表示する文字列(無ければ NULL)
    TTF_Font *font;    // テキスト描画に使用するフォント
    SDL_Color *color;  // 部品の基本色
    // マウスクリックされたときに呼ぶ関数
    void (*execute)(void *);
    // 部品の表示をカスタマイズする関数(未使用)
    void (*draw)(SDL_Renderer *renderer, Uint32);
    struct _parts *next; // 次のデータへのポインタ
} PARTS;

 GUI 部品のデータはリストにするので、最後に次のデータへのポインタを追加しています。あとは、ボタンの表示をカスタマイズできるようにカスタマイズ用の関数も追加しました。これは普通は使わないと思いますが、ボタンに画像を表示する場合に使えると思います。

構造体のメンバへアクセスするマクロ

 構想体のメンバへ直接アクセスすすのは危険だと言われます。関数経由でアクセスを提供し、メンバ自体は外のプログラムから隠蔽すべきです。しかし、関数呼び出しはオーバーヘッドがかかるので、マクロで作っておきます。

/* セッター */
#define parts_set_x(a, b) \
   ((((PARTS *)(a))->id == (Uint32)(a)) ? (((PARTS *)(a))->x = (b)):0)
#define parts_set_y(a, b) \
   ((((PARTS *)(a))->id == (Uint32)(a)) ? (((PARTS *)(a))->y = (b)):0)
#define parts_set_width(x, y) \
   ((((PARTS *)(x))->id == (Uint32)(x)) ? (((PARTS *)(x))->w = (y)):0)
#define parts_set_height(x, y) \
   ((((PARTS *)(x))->id == (Uint32)(x)) ? (((PARTS *)(x))->h = (y)):0)
#define parts_set_text(x, y) \
   ((((PARTS *)(x))->id == (Uint32)(x)) ? (((PARTS *)(x))->string = (y)):NULL)
#define parts_set_callback(x, y) \
   ((((PARTS *)(x))->id == (Uint32)(x)) ? (((PARTS *)(x))->execute = (y)):NULL)
#define parts_set_font(x, y) \
   ((((PARTS *)(x))->id == (Uint32)(x)) ? (((PARTS *)(x))->font = (y)):NULL)
#define parts_set_color(x, y) \
   ((((PARTS *)(x))->id == (Uint32)(x)) ? (((PARTS *)(x))->color = (y)):NULL)
#define parts_set_value(x, y) \
   ((((PARTS *)(x))->id == (Uint32)(x)) ? (((PARTS *)(x))->value = (y)):NULL)
#define parts_set_draw(x, y) \
   ((((PARTS *)(x))->id == (Uint32)(x)) ? (((PARTS *)(x))->draw = (y)):NULL)
#define parts_set_next(x, y) \
   ((((PARTS *)(x))->id == (Uint32)(x)) ? (((PARTS *)(x))->next = (y)):NULL)
#define parts_set_mouse(x, y) \
   ((((PARTS *)(x))->id == (Uint32)(x)) ? (((PARTS *)(x))->mouse = (y)):0)
#define parts_set_type(x, y) \
   ((((PARTS *)(x))->id == (Uint32)(x)) ? (((PARTS *)(x))->type = (y)):0)
#define parts_set_align(x, y) \
   ((((PARTS *)(x))->id == (Uint32)(x)) ? (((PARTS *)(x))->align = (y)):0)

/* ゲッター */
#define parts_get_x(a) \
     (((PARTS *)(a))->id == (Uint32)(a) ? ((PARTS *)(a))->x:-1)
#define parts_get_y(a) \
     (((PARTS *)(a))->id == (Uint32)(a) ? ((PARTS *)(a))->y:-1)
#define parts_get_width(x) \
     (((PARTS *)(x))->id == (Uint32)(x) ? ((PARTS *)(x))->w:-1)
#define parts_get_height(x) \
     (((PARTS *)(x))->id == (Uint32)(x) ? ((PARTS *)(x))->h:-1)
#define parts_get_text(x) \
     (((PARTS *)(x))->id == (Uint32)(x) ? ((PARTS *)(x))->string:NULL)
#define parts_get_callback(x) \
     (((PARTS *)(x))->id == (Uint32)(x) ? ((PARTS *)(x))->execute:NULL)
#define parts_get_font(x) \
     (((PARTS *)(x))->id == (Uint32)(x) ? ((PARTS *)(x))->font:NULL)
#define parts_get_color(x) \
     (((PARTS *)(x))->id == (Uint32)(x) ? ((PARTS *)(x))->color:NULL)
#define parts_get_value(x) \
     (((PARTS *)(x))->id == (Uint32)(x) ? ((PARTS *)(x))->value:NULL)
#define parts_get_draw(x) \
     (((PARTS *)(x))->id == (Uint32)(x) ? ((PARTS *)(x))->draw:NULL)
#define parts_get_next(x) \
     (((PARTS *)(x))->id == (Uint32)(x) ? ((PARTS *)(x))->next:NULL)
#define parts_get_mouse(x) \
     (((PARTS *)(x))->id == (Uint32)(x) ? ((PARTS *)(x))->mouse:-1)
#define parts_get_type(x) \
     (((PARTS *)(x))->id == (Uint32)(x) ? ((PARTS *)(x))->type:-1)
#define parts_get_align(x) \
     (((PARTS *)(x))->id == (Uint32)(x) ? ((PARTS *)(x))->align:-1)

 引数に PARTS * を想定したマクロです。マクロは安全のために括弧を多用しますがそのせいでわかりにくくなってしまいます。

マウスハンドラ

 マウスの動作に合わせてコールバックを呼んだり、表示を変えるのでマウスハンドラが必要になります。

 マウスイベントは、SDL_MOUSEMOTION, SDL_MOUSEBUTTONUP, SDL_MOUSEBUTTONDOWN です。それぞれ、マウスイベントが発生した座標を受け取り、どの部品上で発生したイベントか判断します。

mouse_motion_parts(event.button.x, event.button.y);
mouse_button_up_parts(event.button.x, event.button.y);
mouse_button_down_parts(event.button.x, event.button.y);

 上の関数をそれぞれのイベントで呼び出します。

GUI 部品を作る関数

 GUI 部品を登録する関数で部品データを登録します。

Uint32 add_parts(SDL_Rect *, void *, PARTS_TYPE, char *, TTF_Font *, SDL_Color *);

 最初の引数は部品の位置と幅、高さを表す SDL_Rect のポインタ、次の引数はコールバック関数へ渡す値、部品のタイプ、部品に表示する文字列へのポインタ、使用するフォントと、部品の色です。

 コールバック関数やテキスト配置などは、必ずしも必要ないものなので、それらは、必要に応じて作成した後で、戻り値を利用してセッターで設定できます。

 戻り値は作成した部品を表す 32bit 値です。

 また、文字色やウィンドウサイズは関数を用意して設定するようにしました。

GUI 部品描画関数

void draw_parts(SDL_Renderer *);

 描画処理中に上の関数を呼び出せば、GUI 部品が描画されます。 これらの関数と必要な内部関数を SDL_gui.c にまとめました。

使ってみる

 以下のようなソースを作成します。

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

#define SCR_WIDTH  320
#define SCR_HEIGHT 240

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

/* GUI 部品の色の定義 */
static SDL_Color text_color = {0x1f, 0x1f, 0x1f, 0xff};
static SDL_Color label_color = {0xaf, 0x80, 0x80, 0xff};
static SDL_Color button_color = {0xaf, 0xaf, 0xaf, 0xff};
static SDL_Color checkbox_color = {0xaf, 0xaf, 0x8f, 0xff};

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

    renderer = (SDL_Renderer *)parm;

    SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND);
    SDL_SetRenderDrawColor(renderer, 0xaf, 0xaf, 0x8f, SDL_ALPHA_OPAQUE);
    SDL_RenderClear(renderer);

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

    SDL_RenderPresent(renderer);
}

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;
    SDL_Renderer *renderer;
    TTF_Font *font;
    SDL_Event event;
    SDL_TimerID my_timer_id;
    SDL_Rect rect;
    int quit_flg;

    /* 初期化 */
    if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_TIMER) < 0) {
        fprintf(stderr, "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;

    /* GUI 部品設定 */
    parts_set_textcolor(text_color);  // テキストカラーを設定する
    parts_set_windowsize(SCR_WIDTH, SCR_HEIGHT); // ウィンドウサイズを設定する
    /* ボタン作成 */
    rect.x = 10;
    rect.y = 10;
    rect.w = 90;
    rect.h = 30;
    add_parts(&rect, (void *)1, BUTTON, "ボタン", font, &button_color);
    /* チェックボックス作成 */
    rect.x = 10;
    rect.y = 80;
    rect.w = 200;
    rect.h = 30;
    add_parts(&rect, (void *)0, CHECKBOX, "チェックボックス", font, &checkbox_color);
    /* ラベル作成 */
    rect.x = 10;
    rect.y = 220;
    rect.w = 310;
    rect.h = 20;
    add_parts(&rect, NULL, LABEL, "ラベル", font, &label_color);

    /* イベントループ */
    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_MOUSEMOTION:
                mouse_motion_parts(event.button.x, event.button.y);
                break;

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

            case SDL_MOUSEBUTTONDOWN:
                mouse_button_down_parts(event.button.x, event.button.y);
                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);
    free_parts();
    SDL_Quit();

    return 0;
}

 上記ソースと、SDL_gui.o をリンクしてできた実行ファイルを動かします。

ラベル、ボタン、チェックボックス
押されたボタンとチェックを付けたチェックボックス

 ダイアログっぽくなりましたが、せめて、ドロップダウンリストとテキスト入力ボックスくらいは作りたいです。

SDL2

Posted by sirius