自由線の描画(1)・点での描画

自由線の描画
まずは、ペイントソフトの基本として、
マウスの左ボタンドラッグでキャンバスに自由線を描画する」プログラムを作りたいと思います。

左ボタンが押されたら描画を開始し、ボタンが離されたら描画を終了します。
ボタンが押されている間は、ポインタの位置に点を描画します。

実際に線になるように描画するためには、直線描画をしなければならないのですが、それは次でやるとして、ここではとりあえず単純に点で描画してみます。
スクリーンショット


マウスを素早く動かすと点々…となり、このままでは線には見えません。
ソースコード
002_linepoint.c
#include "sptk.h"

#define WIDTH  300
#define HEIGHT 300
#define PIXCOL 0x0000ff

void winhandle(SPTK_EVENT *ev)
{
    switch(ev->type)
    {
        case SPTK_EVENT_BTTDOWN:
            if(ev->mouse.btt == SPTK_MOUSEBTT_LEFT)
                sptk_grab(NULL);
            break;
        case SPTK_EVENT_BTTUP:
            if(ev->mouse.btt == SPTK_MOUSEBTT_LEFT && sptk_isgrab())
                sptk_ungrab();
            break;
        case SPTK_EVENT_MOUSEMOVE:
            if(sptk_isgrab())
            {
                sptk_image_setpixel(sptk_window_get_image(), ev->mouse.x, ev->mouse.y, PIXCOL);
                sptk_update(NULL, ev->mouse.x, ev->mouse.y, 1, 1, 0);
            }
            break;
    
        case SPTK_EVENT_WINDOW_CLOSE:
            sptk_quit();
            break;
    }
}

int main()
{
    sptk_init("test", WIDTH, HEIGHT);
    
    sptk_window_set_handle(winhandle);
    
    sptk_run();

    return 0;
}
解説
SPTK_EVENT_BTTDOWN 時
マウスのボタンが押された時に来るイベントです。

左ボタン (SPTK_MOUSEBTT_LEFT) が押されていたら、描画を開始します。
ここでは、sptk_grab() でマウスをグラブしています。
グラブについて
通常、マウスカーソルがプログラムのウィンドウ外に出ている間は、マウスのイベントがそのウィンドウに来なくなります。
座標情報が取得できない上、ウィンドウ外でボタンが離された場合はそのイベントも受け取れないので、正しく処理させることができません。
そこで必要なのが、マウスの「グラブ (キャプチャ)」です。

マウスをグラブすると、グラブ中は特定のウィンドウに常にマウスイベントを送るようにすることができます。
これで、カーソルがウィンドウ外に出た場合でも座標を取得でき、ウィンドウ外でボタンが離されてもそのイベントを取得できます。

ただし、マウスイベントが常に一つのウィンドウに送られるということは、「他のウィンドウにはマウスイベントが送られない=他のウィンドウが操作できなくなる」ということなので、グラブは一時的なものでなければなりません。
グラブが必要なくなった時は、すぐにグラブを解除して通常の状態に戻しましょう。

SPTK では、sptk_grab() でマウスをグラブでき、sptk_ungrab() でグラブを解除できます。

void sptk_grab(SPTK_WIDGET *widget);
void sptk_ungrab(void);

widget: グラブするウィジェットを指定します。NULL で、メインウィンドウ自体をグラブします。
SPTK_EVENT_BTTUP 時
マウスボタンが離された時

ボタンが離されたら描画を終えます。
ここでは、sptk_ungrab() でグラブを解除します。
SPTK_EVENT_MOUSEMOVE 時
マウスカーソルが移動した時

グラブされている間 (左ボタンが押されている間)、カーソルの位置に点を描画して、その後画面を更新します。
sptk_image_setpixel() でイメージ上に点を描画し、sptk_update() で、イメージの内容をウィンドウに反映させるために更新要求を行います。
描画について
SPTK では、メインウィンドウが作成されると、同時に、ウィンドウのサイズと同じサイズのイメージ (SPTK_IMAGE) が作成されます。
このイメージに描画されている内容が、そのままウィンドウの内容として表示されます。
そのため、ウィンドウの内容を変更したい場合は、まずそのイメージに描画して、その後、イメージの内容をウィンドウに反映させる必要があります。
イメージへの描画
ウィンドウが内部で持っているイメージは sptk_window_get_image() で取得できます。

SPTK_IMAGE *sptk_window_get_image(void);

SPTK_IMAGE への描画には、sptk_image_*() 関数を使ってください。
色は、「0xRRGGBB」の値で指定します。
なお、SPTK_IMAGE のイメージのフォーマットは、OS やディスプレイのカラーモードなど環境によって異なるので、直接イメージのバッファを操作することは避けてください。

//点を打つ
void sptk_image_setpixel(SPTK_IMAGE *img,int x,int y,uint32_t col);

//指定色ですべて塗りつぶす
void sptk_image_clear(SPTK_IMAGE *img,uint32_t col);
ウィンドウの更新要求
イメージに描画するだけでは、ウィンドウに表示される内容は変化しません。
イメージの内容をウィンドウに反映させるためには、ウィンドウの更新要求を行う必要があります。

void sptk_update(SPTK_WIDGET *wg,int x,int y,int w,int h,int time);

wg位置の基準とするウィジェット。NULL でメインウィンドウ自体。
x,y,w,h更新範囲
timeミリセカンド単位の時間。
この時間後には必ず更新させる。
0 ですぐに更新させる。負の値で範囲の追加のみ行う。

この関数で更新範囲を指定すると、更新するタイミングが来た時に、イメージの内容がウィンドウに転送されます。
更新のタイミングについて
ウィンドウの更新処理は、sptk_update() を実行した時点ですぐに行われるわけではありません。
(time に 0 を指定した場合は除く)
実際には、更新する範囲を内部で記録しておき、他に処理するイベントがなくなった時点で、更新が行われます。

これは、イベントが溜まっている間はそちらの処理を優先して、余裕がある時にまとめて更新させることで、描画処理を最適化することができるという利点があります。
そのため、通常のプログラムでは更新のタイミングについて気にする必要はありません。
Windows API でもウィンドウ更新に関しては同じような仕組みになっています。

しかし、ペイントソフトなどでは更新タイミングが重要になってくる場合があります。
例えば、重い処理を行っている場合は、イベントが常に溜まってる状態に陥り、その結果なかなか更新が行われないという事態が起こることがあります。

ペイントソフトの場合は、キャンバスのスクロールなど、キャンバス全体を更新する必要がある場合によくそういう状態になるので、スクロール中はカクカクとした表示になって、「あれ?処理が重い?」と思うのですが、実際には負荷には余裕があるが、更新が行われていないだけということがあります。
SPTK の更新タイミング
ここでは、ペイントソフトを作るという前提なので、SPTK ライブラリ自体に更新タイミングを指定する機能を付けました。

sptk_update() 時に time で時間が指定されると、タイマーを起動させて、指定時間後には確実に更新が行われるようにしています。
時間の単位はミリセカンドです。1秒 = 1000ms。
time に 0 を指定すると、タイマーは使わずに、すぐに更新を行います。
time に負の値を指定すると、タイマーは起動させず、通常の更新タイミングに任せます。

なお、すでに更新用のタイマーが起動している状態で更新範囲が追加された場合は、タイマーは現状のまま継続させます。
追加された範囲は、次の更新タイミングの時に同時に転送されます。