Home | Lesson | Game | Tool | Link

A03.GUIの表示

Irrlicht専用のメッセージボックスやボタンなどを表示します。
A02で紹介した日本語表示関数をFontクラス化することで、GUIに日本語も表示可能です。


ヘッダファイルの追加
日本語を入力する際に必要なヘッダを追加しておきます。
また、レシーバーでdeviceを使用するので、グローバル化しておきます。
#include <stdio.h>
#include <stdarg.h>
#include <wchar.h>
#include <locale.h>

IrrlichtDevice *DEV;

UTF→SJIS変換
Irrichtでは文字列は内部でUTFで扱われています。
Windowsユーザーの場合は、SJISの方が馴染みがあるのと、
台詞などのスクリプトファイルを外部から読み込んだ時のために
文字コードを変換する関数を用意しておきます。
Windowsに限定するのであればWideCharToMultiByteを使用することもできます。

変換ルーチンについては、KamoLandさんのを使用させて頂きました。

UTFをSJISに変換する関数です。
//UTF-16BE → SJIS へ文字列のコード変換
char* utf16be_to_sjis(const wchar_t *pUcsStr, int *nBytesOut)
{
	char *pAnsiStr = NULL;
	int nLen;

	if(!pUcsStr)
		return NULL;

	setlocale(LC_ALL, "Japanese");//これがないとUnicodeに変換されない

	nLen = wcslen(pUcsStr);

	if(pUcsStr[0] == 0xfeff || pUcsStr[0] == 0xfffe)
	{
		pUcsStr++; //先頭にBOM(Byte Order Mark)があれば,スキップする
		nLen--;
	}

	pAnsiStr = (char *)calloc((nLen+1), sizeof(wchar_t));
	if(!pAnsiStr)
		return NULL;

	//1文字ずつ変換する
	//まとめて変換すると、変換不能文字への対応が困難なので
	int nRet, i, nMbpos = 0;
	char *pcMbchar = new char[MB_CUR_MAX];

	for(i=0;i<nLen;i++)
	{
		//irrlicht用調整
		//変換時のゴミが入っているので無視する
		//例)終了をWCHARにすると8F4997B90000だが
		//内部では8FFF490097FFB9FF0000として格納されている
		if((pUcsStr[i] & 0xFF00) == 0xFF00)
		{
			pAnsiStr[nMbpos++] = pUcsStr[i] & 0x00FF;
		}
		//通常処理
		else
		{
			//変換処理
			nRet = wctomb(pcMbchar, pUcsStr[i]);
			switch(nRet)
			{
				case 1: //1バイト文字
					pAnsiStr[nMbpos++] = pcMbchar[0];
					break;
				case 2: //2バイト文字
					pAnsiStr[nMbpos++] = pcMbchar[0];
					pAnsiStr[nMbpos++] = pcMbchar[1];
					break;
				default: //変換不能
					pAnsiStr[nMbpos++] = ' ';
					break;
			}
		}
	}
	pAnsiStr[nMbpos] = '\0';

	delete [] pcMbchar;

	*nBytesOut = nMbpos;

	return pAnsiStr;
}

SJIS→UTF
SJISをUTFへ変換する関数です。
//SJIS → UTF-16BEへ文字列のコード変換
wchar_t* sjis_to_utf16be(const char *pAnsiStr, int *nBytesOut)
{
	int len;
	wchar_t *pUcsStr = NULL;

	if(!pAnsiStr)
		return NULL;

	setlocale(LC_ALL, "Japanese");  //これがないとUnicodeに変換されない

	len = strlen(pAnsiStr);
	*nBytesOut = sizeof(wchar_t)*(len);

	pUcsStr = (wchar_t *)calloc(*nBytesOut + 2, 1);
	if(!pUcsStr)
		return NULL;

	//変換処理
	mbstowcs(pUcsStr, pAnsiStr, len+1);

	return pUcsStr;
}

日本語表示のFontクラス化
日本語表示をFontクラス化することで、GUIでも使用可能になります。
実際の表示はPrintfを呼び出して、DrawImageで画像を描画しています。
//日本語表示関数をFontクラス化:これによりGUIなどでも使用可能に
class CJPFont : public IGUIFont
{
public:
	CJPFont(IVideoDriver* Driver);
	virtual ~CJPFont();

	virtual void draw(const wchar_t* text, const rect<s32>& position, 
			SColor color, bool hcenter=false, bool vcenter=false, 
			const rect<s32>* clip=0);
	virtual s32 getCharacterFromPos(const wchar_t* text, s32 pixel_x)const ;
	virtual dimension2d<s32> getDimension(const wchar_t *text)const ;
	virtual s32 getKerningHeight()const {return 0;}
	virtual s32 getKerningWidth(const wchar_t *thisLetter=0, 
			const wchar_t *previousLetter=0)const {return 0;}
	//virtual EGUI_FONT_TYPE getType();
	virtual void setKerningHeight(s32 kerning){}
	virtual void setKerningWidth(s32 kerning){}
private:
	s32 getLen(const wchar_t* text);
	IVideoDriver* Driver;
};

Fontクラス詳細
Fontクラスの詳細ですが、ここは読み飛ばしても大丈夫です。
こういう感じに定義するものと思ってください。
//コンストラクタ
CJPFont::CJPFont(IVideoDriver* driver)
{
	Driver = driver;
}

//デストラクタ
CJPFont::~CJPFont()
{
	if(Driver)
		Driver->drop();
}

//文字の描画
void CJPFont::draw(const wchar_t* text, const rect<s32>& position, 
	SColor color, bool hcenter, bool vcenter, const rect<s32>* clip)
{
	int len;
	char* buf = NULL;

	//マルチバイト文字列をchar型に変換(Windows専用 UTF->SJIS)
//	len = WideCharToMultiByte(CP_ACP, 0, text, -1, NULL, 0, NULL, NULL);
//	buf = (char*)malloc(len);
//	WideCharToMultiByte(CP_ACP, 0, text, -1, buf, len, NULL, NULL);

	buf = utf16be_to_sjis(text, &len);

	//中央寄せの処理 描画位置の微調整を行います
	dimension2d<s32> textDimension;
	position2d<s32> offset = position.UpperLeftCorner;
	if(hcenter || vcenter)
	{
		textDimension = getDimension(text);

		if(hcenter)
			offset.X = ((position.getWidth()
			 	- textDimension.Width)>>1) + offset.X;

		if(vcenter)
			offset.Y = ((position.getHeight()
				 - textDimension.Height)>>1) + offset.Y;
	}

	//Printfで描画(クリッピングを行っていないので注意)
	Printf(Driver, offset.X, offset.Y, color.color, buf);
	free(buf);
}

//現在の位置からどの文字か取得します
s32 CJPFont::getCharacterFromPos(const wchar_t* text, s32 pixel_x)
{
	s32 x = 0;
	s32 i = 0;

	while(text[i])
	{
		if(text[i] < 0x0080)
			 x += 8;
		else x += 16;

		if(x >= pixel_x)
			return i;

		++i;
	}

	return -1;
}

//描画領域サイズの取得
dimension2d<s32> CJPFont::getDimension(const wchar_t *text)
{
	dimension2d<s32> dim;

	int len;
	len = getLen(text);

	//改行コードは視野にいれてないので注意
	dim.Width  = len*8;
	dim.Height = 16;

	return dim;
}

//文字列長を取得します
s32 CJPFont::getLen(const wchar_t* text)
{
	s32 len = 0;
	s32 i   = 0;

	while(text[i])
	{
		//1バイト文字か2バイト文字かの判定
		if(text[i] < 0x0080)
			 len += 1;
		else len += 2;

		i++;
	}

	return len;
}

レシーバー設定
キーボードやマウスのイベントのように、GUIのイベントもレシーバーで処理します。
特定のボタンに対応した処理をここに記述、またはここから呼び出します。
ここで使用しているIDは、各GUIを作成した際に引数でつけたIDになっています。
class MyEventReceiver : public IEventReceiver
{
public:
    IGUIWindow* window;

    virtual bool OnEvent(const SEvent& event)
    {
        //GUIのイベント処理
        if(event.EventType == EET_GUI_EVENT)
        {
            s32 id = event.GUIEvent.Caller->getID();//イベントが起こったID
            IGUIEnvironment* env = DEV->getGUIEnvironment();

            switch(event.GUIEvent.EventType)
            {
                //ボタンが押された時の処理
                case EGET_BUTTON_CLICKED:
                    //終了ボタン
                    if(id == 101)
                    {
                        //デバイスを終了させます
                        DEV->closeDevice();
                        return true;
                    }
                    //拡張版ボタン
                    if(id == 102)
                    {
                        IVideoDriver *driver = DEV->getVideoDriver();
                        dimension2d<s32> screen = driver->getScreenSize();

                        //メッセージボックスの大きさと位置指定(画面中央に表示)
                        s16 w = 240;
                        s16 h = 120;
                        s16 x = (screen.Width  - w)/2;
                        s16 y = (screen.Height - h)/2;
                        window = env->addWindow(
                            rect<s32>(x,y,x+w,y+h), true, L"メッセージボックス");

                        //クローズボタンを非表示にします
                        window->getCloseButton()->setVisible(false);

                        //最大化ボタンを表示します
                        //IGUIButton *MaxBtn = window->getMaximizeButton();
                        //MaxBtn->setVisible(true);

                        //最小化ボタンを表示します
                        //IGUIButton *MinBtn = window->getMinimizeButton();
                        //MinBtn->setVisible(true);

                        //メッセージ
                        env->addStaticText(L"アイコンつき\nサンプルテキスト",  
                            rect<s32>(60,40,150,50), false, false, window);

                        //画像の追加
                        ITexture *tex;
                        tex = driver->getTexture("question.png");
                        env->addImage(tex, position2d<s32>(20,30), true, window, 1000); 

                        //ボタンの配置
                        env->addButton(rect<s32>( 15,80, 15+100,80+30), 
                            window, 1001, L"はい");
                        env->addButton(rect<s32>(125,80,125+100,80+30), 
                            window, 1002, L"キャンセル");

                        return true;
                    }
                    //通常版ボタン
                    if(id == 103)
                    {
                        window = env->addMessageBox(L"IrrLicht MessageBox", 
                            L"これは通常版のメッセージボックスです。",true, EMBF_YES);
                        return true;
                    }
                    //拡張版メッセージボックスで作成したボタン
                    if(id > 1000)
                    {
                        //ウインドウが1つしか表示されないのを仮定してこの書き方
                        window->remove();
                    }
                    break;
                default:
                    break;
            }
        }

        return false;
    }
private:
};

GUIスキン設定
ここからGUIの設定になります。
envを作成し、skinを取得します。そのskinに自作したFontクラスを当てはめます。
これにより、GUIでも日本語が表示できるわけです。
	//GUIスキン設定
	IGUIEnvironment* env = DEV->getGUIEnvironment();
	IGUISkin* skin;
	skin = env->getSkin();				//スキン取得
	CJPFont *font;
	font = new CJPFont(driver);				//日本語Fontクラス用意
	skin->setFont(font);				//日本語Fontクラス割り当て
	skin->setColor(EGDC_ACTIVE_CAPTION, 0xFFFFFF00);	//アクティブは黄色に
	skin->setDefaultText(EGDT_WINDOW_CLOSE , L"");	//ヘルプメッセージなしに

透明度設定
デフォルトのGUIは標準で半透明になっています。
後ろ側も見れてなかなか良いのですが、重要なメッセージを表示している時は、
少し問題があるので、不透明に設定しなおしています。
	//スキンの透明度変更
	s32 i;
	SColor col;
	for(i=0;i<EGDC_COUNT;++i)
	{
		col = env->getSkin()->getColor((EGUI_DEFAULT_COLOR)i);	//カラー取得
		col.setAlpha(0xFF);				//不透明に
		env->getSkin()->setColor((EGUI_DEFAULT_COLOR)i, col);	//反映
	}


スキンカラー
設定できるスキンカラーには以下があります。
この値を使用して、特定のスキンだけ変更することも可能です。

EGDC_3D_DARK_SHADOW     最も部分
EGDC_3D_SHADOW          暗い部分
EGDC_3D_FACE            表面
EGDC_3D_HIGH_LIGHT      最も明るい部分
EGDC_3D_LIGHT           明るい部分
EGDC_ACTIVE_BORDER      アクティブウインドウのボーダー
EGDC_ACTIVE_CAPTION     アクティブウインドウのキャプション
EGDC_APP_WORKSPACE      MDIアプリケーションの背景
EGDC_BUTTON_TEXT        ボタンのテキスト
EGDC_GRAY_TEXT          disable状態のテキスト
EGDC_HIGH_LIGHT         コントロールで選択状態のアイテム
EGDC_HIGH_LIGHT_TEXT    コントロールで選択状態のアイテムのテキスト
EGDC_INACTIVE_BORDER    非アクティブウインドウのボーダー
EGDC_INACTIVE_CAPTION   非アクティブウインドウのキャプション
EGDC_TOOLTIP            ヘルプメッセージのテキストカラー
EGDC_TOOLTIP_BACKGROUND ヘルプメッセージの背景
EGDC_SCROLLBAR          スクロールバーの矢印
EGDC_WINDOW             ウインドウの背景
EGDC_WINDOW_SYMBOL      クローズボタンなどのウインドウシンボル
EGDC_ICON               ツリー/リストのアイコン
EGDC_ICON_HIGH_LIGHT    ツリー/リストの選択されたアイコン
EGDC_COUNT              この値は使用されません トータルのカウント数を意味します

GUIの配置
add〜でGUIを作成します。
作成する際に、IDを引数で指定していますが、これがレシーバーで使用されます。
終了ボタンをSJISからUTFに変換して設定しています。
ファイルから読み込んだ内容を表示したい際も、このように行うとよいでしょう。
	//変換のテスト
	wchar_t* buf;
	int len;
	char* mess = "終了";
	buf = sjis_to_utf16be(mess, &len);

	//GUI配置
	//env->addEditBox(L"", rect<s32>(10,10,10+200,10+30), true);
	env->addStaticText(L"スタティックテキスト", rect<s32>(10,50,10+200,50+30), true);
	env->addButton(rect<s32>(10,100,10+200,100+30), 0, 101, buf);
	env->addButton(rect<s32>(10,150,10+200,150+30), 0, 102, L"拡張版");
	env->addButton(rect<s32>(10,200,10+200,200+30), 0, 103, L"通常版");


addStaticText
スタティックテキストを追加します。
IGUIStaticText* addStaticText(
	const wchar_t*   text,  
	const rect<s32>& rectangle,  
	bool             border         = false,  
	bool             wordWrap       = true,  
	IGUIElement*     parent         = 0,  
	s32              id             = -1,  
	bool             fillBackground = false 
)
text           表示するテキストを設定します。
rectangle      スタティックテキストを表示する位置を指定します。
border         枠を表示するかどうかを指定します。
wordWrap       複数行表示する際に、ラップさせるかどうかを指定します。
parent         親(例えばウインドウ)を指定します。親が動けば一緒に動きます。
id             IDを指定します。
fillBackground 背景をぬりつぶすかどうか指定します。


addButton
ボタンを追加します。
IGUIButton* addButton(
	const rect<s32>& rectangle,
	IGUIElement*     parent      = 0,
	s32              id          = -1,
	const wchar_t*   text        = 0,
	const wchar_t*   tooltiptext = 0
)
rectangle   ボタンを表示する位置を指定します。
parent      親(例えばウインドウ)を指定します。親が動けば一緒に動きます。
id          IDを指定します。レシーバーで処理するときに使用します。
text        ボタンに表示するテキストを指定します。
tooltiptext ボタンのヘルプメッセージを指定します。


他のGUI
上で紹介したもの以外にも、たくさんのGUIが用意されています。
以下に代表的なものを紹介します。
addCheckBox チェックボックスを追加します。
addEditBox  エディットボックスを追加します。
addListBox  リストボックスを追加します。

描画
GUIもシーンと同じように、メインループの中でdarwAllを呼び出して描画します。
	while(DEV->run())
	{
		driver->beginScene(true,true,0xFF6060FF);

		//シーンの描画
		smgr->drawAll();

		//GUI描画
		env->drawAll();

		driver->endScene();
	}


GUIの話
Irrlichtにはエディットボックスもあるのですが、Fontクラス化を行っただけでは、
エディットボックス内で日本語の入力はできません。
また、ライブラリを修正しないとIME変換ウインドウも左上に表示されてしまいます。

あると便利なのですが、作成したプログラムにGUIの雰囲気が合わない場合も多々あるので、
同等の機能を持ったGUIを自作した方が早いかもしれません。

使い辛いためか、次回バージョンからはGUIを外すオプションも実装されるみたいです。

ダウンロード
今回作成したファイル一式です。

宿題
1.ファイルから読み込んだ情報をStaticTextに表示させてみましょう。
2.ボタンを押すと、画像が出てくるようにしましょう。