gtkmm と SDL2 を組み合わせて使う

能書き

Gtk+ には特殊なウィジェットとして socket, plug ウィジェットがあり, 親プロセスの持つ socket ウィジェット内に 別プロセスから plug ウィジェットを用いて描画させることが出来る.

これだけでも充分面白いが,socket ウィジェットウィジェットでありながら内部にウィンドウシステムのネイティブなウィンドウハンドルを持っているため,アンドキュメントだが plug ウィジェットに限らず他のGUIツールキットからでも描画させることが出来る.

SDL2には複数ウィンドウを作る機能があり,ウィンドウを作成する方法としてSDL_CreateWindowFromを用いると既存のウィンドウハンドル上にSDLのウィンドウを作成出来る.

今回はこの二つを組み合わせてみた.

プラットフォーム

この記事では(書くのがめんどくさいので) Linux 上での実行方法・コードだけ載せるが SDL2 も GTK もこれらの機能はクロスプラットフォームで提供しているので Windows でも Mac でもウィンドウハンドルの型を変えることによって同様の結果を得られる……はずである.多分.

Windows では HWND を使って動作することを確認したが,Mac は持ってないので確認できません.

実行結果

f:id:nyaocat:20141008231349p:plain

VM内の Ubuntu で実行.Gtk で作成されたウィンドウのいちウィジェット内に SDL2 で描画している.

コード,コンパイル,実行方法

sdl.cpp

#include <iostream>
#include <cmath>
#include <SDL.h>
#include <X11/Xlib.h>

int main() {

  ::Window windowid;

  std::cout << "Window ID: " << std::flush;
  std::cin >> windowid;

  if (SDL_VideoInit(NULL) < 0) {
    std::cerr << "Couldn't initialize SDL video: " << SDL_GetError()
              << std::endl;
    exit(1);
  }
  SDL_Window *window = SDL_CreateWindowFrom((void *)windowid);
  if (!window) {
    std::cerr << "Couldn't create SDL window: " << SDL_GetError() << std::endl;
    exit(1);
  }

  SDL_Renderer *render = SDL_CreateRenderer(window, -1, SDL_RENDERER_SOFTWARE);
  if (!render) {
    std::cerr << "Couldn't create renderer: " << SDL_GetError() << std::endl;
    exit(1);
  }
  SDL_EventState(SDL_SYSWMEVENT, SDL_ENABLE);
  SDL_SetRenderDrawColor(render, 0xA0, 0xA0, 0xA0, 0xFF);
  SDL_RenderClear(render);

  int const center_x = 200;
  int const center_y = 150;
  int const radius = 100;
  double rotate = 0.0;

  while (true) {
    SDL_Event ev;
    while (SDL_PollEvent(&ev)) {
      if (ev.type == SDL_QUIT)
        return 0;
    }
    SDL_SetRenderDrawColor(render, 0, 0, 0, 255);
    SDL_RenderClear(render);
    SDL_SetRenderDrawColor(render, 255, 0, 0, 255);
    rotate += 0.05;
    SDL_RenderDrawLine(render, center_x + radius * std::cos(rotate),
                       center_y + radius * std::sin(rotate),
                       center_x - radius * std::cos(rotate),
                       center_y - radius * std::sin(rotate));
    SDL_RenderPresent(render);
  }
}

これ自体は赤い線がぐるぐる回るアニメーションをするだけのコードです.

gtk.cpp

#include <iostream>
#include <cmath>
#include <gtkmm.h>
#include <gtkmm/socket.h>

void on_button() { std::cout << "Button" << std::endl; }

int main(int argc, char **argv) {
  Gtk::Main kit(argc, argv);

  Gtk::Window main_window;
  Gtk::VBox *vbox = manage(new Gtk::VBox());

  Gtk::Socket *sock = manage(new Gtk::Socket());
  sock->set_size_request(400, 300);
  vbox->pack_start(*sock, true, true);

  Gtk::Button *button = (new Gtk::Button(Gtk::Stock::OK));
  button->signal_clicked().connect(sigc::ptr_fun(on_button));
  vbox->pack_start(*button, true, true);

  main_window.add(*vbox);
  main_window.show_all();

  std::cout << "Window ID: " << sock->get_id() << std::endl;

  Gtk::Main::run(main_window);
  return 0;
}

他に何もウィジェットを置かないと GTK 本当に使ってるんだか分からないので無意味にボタンをひとつ置く.

コンパイル

g++ sdl.cpp -o sdl `pkg-config sdl2 x11 --cflags --libs`
g++ gtk.cpp -o gtk `pkg-config gtkmm-3.0 --cflags --libs`

実行方法.

端末を2つ用意し,それぞれで先ほど作成したsdl, gtk を実行する. すると,gtk の方が WindowID を出力するので, それを入力待ち状態になっている sdl の方に入力する.

そうすると,スクリーンショットの様な実行画面が得られる.

他環境について

Windows では GDK_WINDOW_HWND(sock->get_window()->gobj()) で HWND が取得出来るので,あとは ::Window を HWND にしてやれば同様に動かせる.

Mac は持ってないので知らない.SDL2 の SDL_CreateWindowFrom のテストコードに Mac 用のコードもあったので多分動く.

あと,この記事中に特に gtkmm-3.0 の新機能などは使ってないので gtkmm-2.0 でも動く.

あとあと,Windows 専用になるが Dxlib というゲームライブラリも SetUserWindow で用いるウィンドウハンドルも事前に指定可能になるので,Dxlib と組み合わせて使うことも出来る.というかウィンドウハンドルを事前に指定可能なやつはなんでも出来る.

わからないこと

わざわざ別プロセスにしなくとも,同じプロセス内で SDLGtk コードを記述しても動くはず……だと思うのだけど実際試したらセグフォで落ちる.何故だ.

Wxwidgets や Qt でもこういうことが出来るのかも気になる.

これで出来ること

SDL の描画に GtkGUI ウィジェットをぼかぼか付けれるのでゲームエディタっぽいことが出来る. プロセス間通信が面倒だし最近のゲームエンジン使った方が高機能なのは勿論だが……しかしこういうのも楽しいと思う.