ゆとりーなの日記

日記的な事を書いて行くと思はれる

第4回〜文字列を描画する〜

四角形を書いたら文字も書きたくなるのが人情です。文字が画面に書ければデバッグ等もしやすくなりますし。
今回はID3DXFont*を使って実装します。他にも実装方法はありますが、私的にはこいつが一番手軽に文字列を書ける気がするのでこいつを使います。この子の問題点として遅いということがよく言われますが、簡単なセリフの描画位なら全然耐えますし、デバッグに使うにしてもよほど文字列で埋めない限りまず大丈夫です。あと、今回から"d3dx9.lib"(デバッグ時は"d3dx9d.lib")をリンクしとく必要があります。
ID3DXFont*を作るためにはIDirect3DDevice9*が必要なので、取り敢えずGraphicDeviceクラスにフォントを作成するメソッドを追加することにします。この際Applicationクラスの派生クラスのコンストラクタでもフォントが生成できるように、GraphicDeviceクラスをprotectedメンバとし、生成をApplicationクラスのコンストラクタに持っていくことにします。また、protectedメンバに持ってきたので、派生クラスにアクセスされたら困るメソッドをprivateに移動します。また、ID3DXFont*を生で扱うのは危険なので、ラップしたFontImplクラスを作ります。値渡しは何となく嫌ですが生ポインタ渡しも嫌なので間を取ってstd::shared_ptrの値渡しでフォントはやり取りすることにします。

// graphicdevice.h
#pragma once
#include <string>
#include <memory>
#include <boost/noncopyable.hpp>
#include <boost/intrusive_ptr.hpp>
#include <d3d9.h>
#include "deleter.h"

class FontImpl;

class GraphicDevice : private boost::noncopyable {
public:
    std::shared_ptr<FontImpl> createFont(int size, bool italic, const std::string &font_name);
private:
    // 内部実装関係
    bool beginScene();
    void endScene();
};

フォントの作成自体は簡単です。

// graphicdevice.cc

std::shared_ptr<FontImpl> GraphicDevice::createFont(int size, bool italic, const std::string &font_name) {
    assert(graphic_device_.get());
    ID3DXFont *temp_font(nullptr);
    // フォントを作って
    if (FAILED(D3DXCreateFont(graphic_device_.get(), size, 0, FW_NORMAL, 0, italic, SHIFTJIS_CHARSET, OUT_DEFAULT_PRECIS, DEFAULT_QUALITY, FIXED_PITCH | FF_DONTCARE, font_name.c_str(), &temp_font))) {
        throw std::runtime_error("フォントの作成に失敗");
    }
    // フォントを管理するクラスにスマポ化したフォントを渡す
    std::shared_ptr<FontImpl> font(std::make_shared<FontImpl>(boost::intrusive_ptr<ID3DXFont>(temp_font, false)));
    return font;
}

D3DXCreateFont関数に適切な引数を入れればそれでフォントは完成です。今回は、サイズ、イタリック体にするかどうか、フォント名を指定できるようにしました。それ以外の部分は上の設定で特に問題ないと思います。
これをApplicationクラスに組み込みなおします。

// application.h
#pragma once
#define _WIN32_WINNT 0x0501
#include <string>
#include <memory>
#include <boost/noncopyable.hpp>
#include <windows.h>

// ヘッダのインクルードの代わりに前方宣言
class GraphicDevice;
class DrawObject;

class Application : private boost::noncopyable {
public:
    // インターフェイス
protected:
    // protectedインターフェイス
    std::unique_ptr<GraphicDevice> graphic_device_;
private:
    // 内部実装関係
};

// application.cc
#include "application.h"
#include <cassert>
#include <stdexcept>
#include <commctrl.h>
// ヘッダのインクルードを実装ファイルの中身に
#include "graphicdevice.h"
#include "drawobject.h"

Application::Application(const std::string &caption, int client_width, int client_height, bool windowed) : graphic_device_(), window_handle_(nullptr), caption_(caption), client_width_(client_width), client_height_(client_height), windowed_(windowed) {
    init();
}

void Application::init() {
    const WNDCLASSEX window_class = {
        sizeof(WNDCLASSEX),
        CS_HREDRAW | CS_VREDRAW,
        DefWindowProc,
        0,
        0,
        GetModuleHandle(nullptr),
        static_cast<HICON>(LoadImage(nullptr, MAKEINTRESOURCE(IDI_APPLICATION), IMAGE_ICON, 32, 32, LR_DEFAULTSIZE | LR_SHARED)),
	    static_cast<HCURSOR>(LoadImage(nullptr, MAKEINTRESOURCE(IDC_ARROW), IMAGE_CURSOR, 0, 0, LR_DEFAULTSIZE | LR_SHARED)),
	    static_cast<HBRUSH>(GetStockObject(BLACK_BRUSH)),
	    nullptr,
	    kWindowClassName,
	    nullptr,
    };
    if (!RegisterClassEx(&window_class)) {
        throw std::runtime_error("ウィンドウクラスの登録に失敗");
    }
    RECT rect = {
        0,
        0,
        client_width_,
        client_height_
    };
    if (!AdjustWindowRect(&rect, kWinsowStyle, FALSE)) {
        throw std::runtime_error("ウィンドウサイズの取得に失敗");
    }
    window_handle_ = CreateWindow(kWindowClassName, caption_.c_str(), kWinsowStyle, CW_USEDEFAULT, CW_USEDEFAULT, rect.right - rect.left, rect.bottom - rect.top, nullptr, nullptr, GetModuleHandle(nullptr), nullptr);
    if (!window_handle_) {
        throw std::runtime_error("ウィンドウの作成に失敗");
    }
    if (!SetWindowSubclass(window_handle_, subClassProcedure, reinterpret_cast<UINT_PTR>(this), 0)) {
        throw std::runtime_error("ウィンドウプロシージャの関連付けに失敗");
    }
    ShowWindow(window_handle_, SW_RESTORE);
    // 遅延初期化
    graphic_device_.reset(new GraphicDevice(window_handle_, client_width_, client_height_, windowed_));
}

さて、ApplicationクラスでGraphicDeviceクラスをメンバ変数として持つことになったわけですが、GraphicDeviceクラスの初期化には、ウィンドウハンドル等の情報が必要です。ところがApplicationクラスの初期化子の段階ではウィンドウハンドルはまだ得られていません。これでは初期化ができないではないかと思うかもしれませんが、ポインタを使った遅延初期化を行えば一気に解決します。生ポインタは色々危険なのでstd::unique_ptrを使うことにします。ウィンドウハンドルを得た後に、Application::initメソッド内部で遅延初期化してやります。
ここで"application.h"で"graphicdebice.h"をインクルードしていないことに注目です。ある型がスマポの中身の型としてや、生ポインタ、参照及びこれらの引数、戻り値としてのみヘッダに宣言されている場合、その型の実装は必要ないのです。つまりその型のヘッダのインクルードが不要になります。代わりに

class(structの場合も) 型名;

といった書式の前方宣言を使います。前方宣言を使うことのメリットはヘッダの依存関係を減らしてコンパイル時間の減少が見込めることです。というわけでポインタの引数しか宣言されていないDrawObjectについても前方宣言にしてみました。地味にGraphicDeviceクラスのところでもFontクラスを前方宣言しています。
続いてさっきからノータッチだったFontImplクラスについてです。このクラスはID3DDXFontを管理するクラスです。

// fontimpl.h
#pragma once
#include <boost/noncopyable.hpp>
#include <boost/intrusive_ptr.hpp>
#include <boost/signals2.hpp>
#include <d3dx9.h>
#include "deleter.h"

class FontImpl : public boost::signals2::trackable, private boost::noncopyable {
public:
    explicit FontImpl(boost::intrusive_ptr<ID3DXFont> font);
private:
    friend class GraphicDevice;
    friend class DrawObject;
    void onLostDevice();
    void onResetDevice();
    boost::intrusive_ptr<ID3DXFont> font_;
};

// fontimpl.cc
#include "fontimpl.h"

FontImpl::FontImpl(boost::intrusive_ptr<ID3DXFont> font) :font_(font) {
}

void FontImpl::onLostDevice() {
    font_->OnLostDevice();
}

void FontImpl::onResetDevice() {
    font_->OnResetDevice();
}

最早コピー禁止はお約束です。
殆どのメソッドを外部に公開したくないのでprivateになっています。しかし描画を担当するDrawObjectクラス、フォントリソースを管理するGraphicDeviceクラスは内部にアクセスしなければいけないのでfriend指定しています。
コンストラクタではID3DXFontの保存を行っています。
FontImpl::onLostDeviceメソッドとFontImpl::onResetDeviceメソッドではデバイスロストが起きた後のデバイスリセットの前後で呼ぶ必要のあるID3DXFontのメソッドを呼び出しています。
FontImplクラスを使った文字列描画メソッドをDrawObjectクラスに追加します。

// drawobject.h
#pragma once
#include <string>
#include <memory>
#include <boost/noncopyable.hpp>
#include <boost/intrusive_ptr.hpp>
#include <d3d9.h>
#include <d3dx9.h>
#include "deleter.h"

class FontImpl;

class DrawObject : private boost::noncopyable {
public:
    // インターフェイス
    void drawText(const std::string &text, float x, float y, const D3DXCOLOR &color, std::shared_ptr<FontImpl> font);
private:
    // 内部実装関係
};

// drawobject.cc
#include "fontimpl.h"

void DrawObject::drawText(const std::string &text, float x, float y, const D3DXCOLOR &color, std::shared_ptr<FontImpl> font) {
    assert(font.get());
    // 表示座標設定
    RECT rect = {
        static_cast<LONG>(x),
        static_cast<LONG>(y),
        0,
        0
    };
    font->font_->DrawText(nullptr, text.c_str(), -1, &rect, DT_NOCLIP, color);
}

DrawObject::drawTextメソッドでは、表示文字列と表示左上座標と文字色を指定できるようにしてみました。実際の描画はID3DXFont::DrawTextメソッドで行います。座標指定にはRECT構造体を使います。DT_NOCLIPを指定すると表示座標の右下を気にしなくてよくなり、若干高速になるようなので指定してみました。
最後に残ったデバイスロスト関係の話をします。ID3DXFontはデバイスロストから復旧するためにIDirect3DDevice9::Resetを呼ぶ前にID3DXFont::OnLostDeviceメソッド、読んだ後にID3DXFont::OnResetDeviceメソッドを呼ぶ必要があります。これを実現するために、boost.signals2を使うことにします。
まず、GraphicDeviceクラスに以下の追加を行います。

// graphicdevice.h
#pragma once
#include <string>
#pragma warning (disable: 4996)
#include <memory>
#pragma warning (default: 4996)
#include <boost/noncopyable.hpp>
#include <boost/intrusive_ptr.hpp>
#pragma warning (disable: 4512)
#include <boost/signals2.hpp>
#pragma warning (default: 4512)
#include <d3d9.h>
#include "deleter.h"

class FontImpl;

class GraphicDevice : private boost::noncopyable {
public:
    // インターフェイス
private:
    // 内部実装関係
    boost::signals2::signal<void()> release_;
    boost::signals2::signal<void()> reset_;
};

#pragma warning (disable: 番号)は番号の警告を無視、#pragma warning (default: 番号)は番号の警告の無視を止めることを指定します。VC++の/W4と/WXのコンビを使うと、boostのコンパイル時で詰んだりするので、boostのコンパイルがらみで警告が出るときは諦めてこの構文を使って警告を無視します。間違ってもほかの部分でこの構文を使って警告を消しに行ったりしてはいけません。
boost::signals2::signalのテンプレート引数には<関数の戻り値(引数1、引数2・・・)>といったものを指定します。これでこの変数には、connectメソッドを使って指定した戻り値と引数を持つ関数や関数オブジェクトを入れていくことができます。そして、この変数のoperator()の呼び出しを行うと、入れた順に関数、関数オブジェクトが実行されます。これを使ってデバイスロスト前後の処理をまとめて行うことにします。

// graphicdevice.cc
std::shared_ptr<FontImpl> GraphicDevice::createFont(int size, bool italic, const std::string &font_name) {
    assert(graphic_device_);
    ID3DXFont *temp_font;
    if (FAILED(D3DXCreateFont(graphic_device_.get(), size, 0, FW_NORMAL, 0, italic, SHIFTJIS_CHARSET, OUT_DEFAULT_PRECIS, DEFAULT_QUALITY, FIXED_PITCH | FF_DONTCARE, font_name.c_str(), &temp_font))) {
        temp_font = nullptr;
    }
    std::shared_ptr<FontImpl> font(std::make_shared<FontImpl>(boost::intrusive_ptr<ID3DXFont>(temp_font, false)));
    // リセット前に呼ぶ処理の登録
    release_.connect(boost::bind(&FontImpl::onLostDevice, font.get()));
    // リセット後に呼ぶ処理の登録
    reset_.connect(boost::bind(&FontImpl::onResetDevice, font.get()));
    return font;
}

メソッドとインスタンスから関数オブジェクトを作るのにはstd::bindでもなくラムダ式でもなくboost::bindを使っていますがこれには意味があります。boost::signals2::signal::connectメそソッドで接続したはいいが接続したメソッドのインスタンスが落ちたらやばそうな気がします。そんなときのためにFontImplクラスはboost::signals2::trackableを継承しているのです。この子を継承していれば、インスタンスが落ちた時、自動的に接続を絶ってくれます。ところがこの処理を行う関数オブジェクトを構築できる機能を持っているのが現状boost::bindしかないみたいなのでおとなしくboost::bindを使います。各フォントのリセット前後の処理を接続したrelease_とreset_のoperator()呼び出しをデバイスのリセット前後に呼び出せばID3DXFontのデバイスロスト後のリセット対策は完成です。

// graphicdevice.cc
void GraphicDevice::endScene() {
    assert(graphic_device_);
    graphic_device_->EndScene();
    switch (graphic_device_->Present(nullptr, nullptr, nullptr, nullptr)) {
    case D3DERR_DEVICELOST:
	if (graphic_device_->TestCooperativeLevel() == D3DERR_DEVICENOTRESET) {
            // リセット前に呼ぶファントの処理をまとめて呼ぶ
            release_();
	    if (FAILED(graphic_device_->Reset(&present_parameters_))) {
                throw std::runtime_error("デバイスロストからの復旧に失敗");
	    }
            // リセット後に呼ぶフォントの処理をまとめて呼ぶ
            reset_();
            init();
        }
        break;
    case D3DERR_DRIVERINTERNALERROR:
	throw std::runtime_error("内部ドライバエラーが発生");
	break;
    default:
	break;
    }
}

以上で文字列表示に関することは終わりました。試しに昨日のTestApplicationクラスに文字列描画コードを書いて試してみます。

// testapplication.h
#pragma once
#include "application.h"
#include <memory>

class FontImpl;

class TestApplication : public Application {
public:
    TestApplication(const std::string &caption, int client_width, int client_height, bool windowed);
protected:
    virtual void update(DrawObject *draw_object);
private:
    std::shared_ptr<FontImpl> font_;
    std::shared_ptr<FontImpl> big_font_;
    std::shared_ptr<FontImpl> italic_font_;
};

// testapplication.cc
#include "testapplication.h"
#include <cassert>
#include "drawobject.h"
#include "graphicdevice.h"
#include "fontimpl.h"

TestApplication::TestApplication(const std::string &caption, int client_width, int client_height, bool windowed) : Application(caption, client_width, client_height, windowed), font_(graphic_device_->createFont(16, false, "MS ゴシック")), big_font_(graphic_device_->createFont(32, false, "MS ゴシック")), italic_font_(graphic_device_->createFont(16, true, "MS ゴシック")) {
}

void TestApplication::update(DrawObject *draw_object) {
    assert(draw_object);
    draw_object->drawText("C++とwin32とDirectXとBoostとLuaで作る弾幕シューテイング講座", 0.f, 0.f, D3DXCOLOR(1.f, 1.f, 1.f, 1.f), font_);
    draw_object->drawText("C++とwin32とDirectXとBoostとLuaで作る弾幕シューテイング講座", 0.f, 16.f, D3DXCOLOR(1.f, 0.f, 0.f, 1.f), big_font_);
    draw_object->drawText("C++とwin32とDirectXとBoostとLuaで作る弾幕シューテイング講座", 0.f, 48.f, D3DXCOLOR(1.f, 1.f, 0.f, 1.f), italic_font_);
}

実行画面

バイスロストを発生させても大丈夫です。