SDL2で非矩形の画像ビューアを作成

# はじめに

旬な話題ではありませんが、マルチメディアライブラリのSimple Directmedia Layer(以下SDL2)がv2.0.4から、非矩形のウィンドウを取り扱うことができるようになっていました。[^1]
この機能を利用して、D言語＋SDL2で簡単な画像ビューアを作成します。

Windows10 dmd v2.077.0＋SDL v2.0.7＋SDL_image v2.0.2
の32/64bit版でのみ動作確認をしています。

SDL2のバイナリを[ここ](https://www.libsdl.org/download-2.0.php)から、SDL_imageのバイナリを[ここ](https://www.libsdl.org/projects/SDL_image/)から、それぞれzipファイルで拾ってきて、EXEと同じ場所に作成したlibraryフォルダ内に配置してください。

DerelictSDL2を使ったWindowsでのSDLの導入手順は、lunatea氏の記事を参考にしました。
[WindowsでDerelict3を使用する時のメモ](https://qiita.com/lunatea/items/be779e81bab7ddc0fd84)
特に「DLLを任意のフォルダにまとめる」は、DLLが散らかってしまうことで
ライブラリの導入を躊躇した経験があるので、非常に勉強になりました。

今回のソースコードは[こちら](https://github.com/nak2yoshi/something)

以下、デモと解説です。

[^1]: 正確には非矩形のウィンドウは以前から作成できたが、タイトルバー外をマウスドラッグしてウィンドウを移動できるようにするのが難しかった。v2.0.4で簡単にできるようになった。


# デモ

実行後、ウィンドウ内に__透過色指定のあるPNG画像__をドラッグ＆ドロップします。(v2.0.7時点では透過色なしの画像の処理に不具合があるので、__必ず透過色指定のあるPNGを使用してください。__)
ウィンドウ上部の本来タイトルバーがあった部分の描画が乱れるので、可能であれば
ドロップする画像の上部に余白を多めにとっておくと吉です。

![anim.gif](https://qiita-image-store.s3.amazonaws.com/0/87446/222fa235-fe94-8456-b46e-0554404f29e9.gif)


※おまけでホイールでウィンドウの透明度を変えられます。


# 解説

## 処理の流れ

1. ウィンドウの作成<br>
（~~`SDL_CreateWindow()`~~__`SDL_CreateShapedWindow()`__を使用）<br>
↓
2. 画像ファイルの読み込み＆テクスチャに変換<br>
↓
4. 画像のα値を基にウィンドウの形状を決定<br>
（α値がなければ画像の左上隅の色を透過色として使用）<br>
↓
5. 非矩形ウィンドウに設定<br>
（__`SDL_SetWindowShape()`__を使用）<br>
↓
6. 対応するレンダラを作成<br>
↓
7. ウィンドウをドラッグで移動可能にする<br>
（__`SDL_SetWindowHitTest()`__を使用）<br>
↓
8. イベントループを実行する

※1～5は、特に変わったところがないので省略します。__サンキューDerelict！__

## ウィンドウをドラッグで移動可能にする

非矩形ウィンドウにはタイトルバーがないため、ウィンドウ内のドラッグを
あたかもタイトルバーをドラッグしたように転送してやる必要があります。

これは`SDL_SetWindowHitTest`関数に、適切な内容のコールバック関数を設定してあげるとできます。
コールバック関数は、C言語側から呼び出されるのですが、__`extern (C)`__と記述してあげればよしなにやってくれます。簡単ですね。

```d:main
// タイトルバーやウィンドウ枠の代わりになる(ドラッグで移動やサイズ変更可能な)領域を設定
enforceSdl(SDL_SetWindowHitTest(window, &hitTestCallback, cast(void*)null) == 0);
```

```d:
// SDL_SetWindowHitTestに渡されるコールバック関数
extern (C) SDL_HitTestResult
hitTestCallback(SDL_Window* window, const(SDL_Point)* area, void* data) nothrow @nogc
{
    // クリックされた座標(area.x, area.y)とSDL_GetWindowSizeなどを利用して
    // ドラッグ可能な領域を限定したり、サイズ変更可能な領域を指定したりなども可能
    return SDL_HITTEST_DRAGGABLE;
}
```

## イベントループを実行する

発生したイベントの種類ごとに、対応する処理を行います。

- 閉じるボタンの代わりにEscキーでウィンドウを閉じる処理
- ウィンドウに画像ファイルをドロップされた時の処理

などを記述します。

`switch`文だとこんな感じになります。

```d:main
    // イベントループ
    mainLoop: for (SDL_Event event; SDL_WaitEvent(&event);)
    {
        switch (event.type)
        {
            // SDLライブラリの終了
            case SDL_QUIT:
                break mainLoop;
            // 閉じるボタンの代わりにEscキーで終了
            case SDL_KEYDOWN:
                if (event.key.keysym.sym == SDLK_ESCAPE)
                    break mainLoop;
                break;
            // ドロップファイルの処理
            case SDL_DROPFILE:
            {
                char* droppedPath = event.drop.file;
                scope(exit) SDL_free(droppedPath);
                /* コード省略 */
                break;
            }
            default:
                break;
        }
    }
}
```

シンプルなのですが、扱うイベントの種類が増えてくると、状態の管理などが大変そうに思いました。
そのため、[連想配列](https://dlang.org/spec/hash-map.html)と[デリゲート](https://dlang.org/spec/function.html#closures)で以下のようにしてみました。

```d:main
    // イベントに対応するアクションの定義
    bool delegate(SDL_Event)[typeof(SDL_Event.type)] action;

    // SDLライブラリの終了
    action[SDL_QUIT] = (SDL_Event event) { return false; };
    // 閉じるボタンの代わりにEscキーで終了
    action[SDL_KEYDOWN] = (SDL_Event event) { return event.key.keysym.sym != SDLK_ESCAPE; };
    // ドロップファイルの処理
    action[SDL_DROPFILE] = (SDL_Event event) {
        char* droppedPath = event.drop.file;
        scope(exit) SDL_free(droppedPath);
        /* コード省略 */
        return true;
    };

    // イベントループ
    for (SDL_Event event; SDL_WaitEvent(&event);)
    {
        if (event.type in action)
        {
            // アクション実行
            if ( !action[event.type](event) )
                break;
        }
    }
```

これなら、イベントの種類が増えても対応する処理を簡単に登録できますし、変更や削除も容易です。
試しにマウスホイールを回すとウィンドウの透明度が変わるようにしてみましょう。

```d:main
    // マウスホイールの処理
    action[SDL_MOUSEWHEEL] = (SDL_Event event) {
        const direction = (event.wheel.direction == SDL_MOUSEWHEEL_NORMAL ? 1 : -1);
        float opacity; // 不透明度 (0.0f～1.0f)
        enforceSdl(SDL_GetWindowOpacity(window, &opacity) == 0);
        // ホイールを奥に回した
        if (event.wheel.y == direction) {
            opacity = opacity > 0.9f ? 1.0f : opacity + 0.1f;
        }
        // ホイールを手前に回した
        else if (event.wheel.y == -direction) {
            opacity = opacity < 0.2f ? 0.1f : opacity - 0.1f;
        }
        enforceSdl(SDL_SetWindowOpacity(window, opacity) == 0);
        return true;
    };
```

要らなくなったら、いつでも削除できます。

```d:
    action.remove(SDL_MOUSEWHEEL);
```

実用するなら、一括登録・削除などの機能を加えてクラスにまとめてしまうのがよいと思います。

実はSDL2.0には既に[`SDL_SetEventFilter`](http://sdl2referencejp.osdn.jp/SDL_SetEventFilter.html)や[`SDL_AddEventWatch`](http://sdl2referencejp.osdn.jp/SDL_AddEventWatch.html)などが用意されているのですが、せっかくD言語で書いているのにC言語のコールバック関数ばかりを書くのもなあと思ったので、このようになりました。


# 余談

SDL2は、Mercurialというバージョン管理システムで開発されているようです。
https://hg.libsdl.org/SDL
changelogを見ると、リリース間隔が年単位で空いているところもあったりして、歴史のあるライブラリの趣きがあります。
2016年頃からGithubへの(非公式)ミラーリングが始まったようで、以前よりソースコードを見る人が増えたのかもしれません。開発も活性化しているみたいです。