BREW で GUI プログラミング - 1 / 2 -
BREW には実用レベルで使える GUI コンポーネントが標準で備わっていません。通常、GUI の実装ではコンポーネント別にオブジェクト指向的なプログラミングがなされます。つまり、すべてのイベントが適切なイベントハンドラに送られ、適切に処理されるようにプログラミングします。これはBREW アプリ開発における最大のボトルネックです。
オブジェクト指向的な GUI を設計
今回は、動的な仕様変更にも充分に耐えられるオブジェクト指向的な GUI の設計を、すぐに使えるソースコードと共に示します。
具体的には、コンポーネントをアプリ上に配置し、イベントをアプリからコンポーネントに送って処理させる形で GUI プログラミングを行います。
アプリの概要
- 起動すると、左の画面が表示されます。
- ラベル、テキストフィールド、コンボボックス、ラジオボタン、チェックボックス、ボタンといった基本的なコンポーネントが配置されています。
※ 画面の大きさの都合上、BREW SDK 2.1 のエミュレータを使用しています。
※ テキストフィールドは、エミュレータではうまく動かない箇所がありますが、携帯端末上では問題ありません。
コンポーネント構造体
ここで宣言するコンポーネントは、それ自身の領域やテキストを持つ構造体として宣言されます。これらの情報により、フォーカスがあたっているとき、あたっていないときの描画の更新をコンポーネント別に行います。また、ラジオボタンやチェックボックスなら、チェックされているかどうかなど、コンポーネントの種類に応じた情報も持ちます。
■コンポーネント構造体の宣言
// コンポーネントの種類 typedef enum ComponentType { COMPONENT_NULL = -1, COMPONENT_LABEL, COMPONENT_TEXTFIELD, COMPONENT_COMBOBOX, COMPONENT_RADIOBUTTON, COMPONENT_CHECKBOX, COMPONENT_BUTTON } ComponentType; // コンポーネント typedef struct Component { // 種類 ComponentType type; // 領域 AEERect rect; // テキスト AECHAR* text; // コンポーネントの種類に依存する変数 union { ITextCtl* textctl; // TEXTFIELD CheckBox checkbox; RadioButton radiobutton; ComboBox combobox; } component; } Component;
構造体中 union 内の構造体やポインタの宣言は、コンポーネントの種類によって保持する内容が違うので、省メモリ設計のために union 宣言されています。union 内の構造体をすべてポインタとして持ち、動的にメモリ管理することで、さらなる省メモリ設計も可能です。
アプレット構造体には、コンポーネントを保持するための変数を宣言します。
// アプレット構造体 typedef struct GUIApp { AEEApplet a; // アプレット構造体の先頭メンバは必ず AEEApplet 型にすること。 // 画面描画用 IGraphics* graphics; int width; int height; int font_height; // コンポーネント Component components[20]; int component_length; // コンポーネントの数 int component_index; // 現在フォーカスを持つコンポーネントのインデックス } GUIApp;
コンポーネントの初期化
アプリ開始時に、コンポーネントを初期化します。
// アプレットが開始したときに呼び出される static boolean OnAppStart(GUIApp* app) { AEERect r; AECHAR wstring[16]; //(略) app->component_length = 0; app->component_index = 0; // コンポーネントを追加 // ラベル r.x = 0; r.y = 0; r.dx = app->width; r.dy = app->font_height; Component_Init(&app->components[app->component_length], app, COMPONENT_LABEL, &r, "GUIApp"); app->component_length++; // テキストフィールド r.x = 0; r.y = r.dy + MARGIN; STRTOWSTR("2006", wstring, 16 * sizeof(AECHAR)); r.dx = IDISPLAY_MeasureText(app->a.m_pIDisplay, AEE_FONT_NORMAL, wstring) + MARGIN; r.dy = app->font_height + MARGIN; Component_Init(&app->components[app->component_length], app, COMPONENT_TEXTFIELD, &r, "2006"); Component_SetTextFieldSize(&app->components[app->component_length], 4); app->component_length++; //... Draw(app); return TRUE; }
Component_Init 関数で、コンポーネントの種類や領域、テキストを渡して構造体を初期化します。
Component_Init 関数はすべてのコンポーネントを初期化するために汎用的に設計されています。
// コンポーネントの初期化 static void Component_Init(Component* component, GUIApp* app, ComponentType type, AEERect* rect, char* text) { component->type = type; component->rect = *rect; if (text != NULL) { int len = STRLEN(text) + 1; component->text = MALLOC(len * sizeof(AECHAR)); STRTOWSTR(text, component->text, len * sizeof(AECHAR)); } else { component->text = NULL; } switch (type) { case COMPONENT_LABEL: break; case COMPONENT_TEXTFIELD: component->component.textctl = NULL; ISHELL_CreateInstance(app->a.m_pIShell, AEECLSID_TEXTCTL, (void**)&component->component.textctl); ITEXTCTL_SetRect(component->component.textctl, &component->rect); ITEXTCTL_SetText(component->component.textctl, component->text, -1); ITEXTCTL_SetProperties(component->component.textctl, TP_FRAME | TP_FIXSETRECT | TP_MULTILINE); break; //... } return; }
テキストフィールドに文字数制限をしたい場合など、Component_Init 関数では設定しきれない情報を、Component_SetTextFieldSize 関数などの関数を用意して補助します。
// テキストフィールドに文字数を指定 static void Component_SetTextFieldSize(Component* component, uint16 size) { switch (component->type) { case COMPONENT_TEXTFIELD: ITEXTCTL_SetMaxSize(component->component.textctl, size); break; } return; }
コンポーネントの描画
配置したコンポーネントを描画しましょう。
// 画面描画 static void Draw(GUIApp* app) { int i; AEERect rect; rect.x = 0; rect.y = 0; rect.dx = app->width; rect.dy = app->height; // 画面をクリア IDISPLAY_FillRect(app->a.m_pIDisplay, &rect, RGB_WHITE); // 最後のコンポーネントから描画 for (i = app->component_length - 1; i >= 0; i--) { Component_Draw(&app->components[i], app, i == app->component_index); } IDISPLAY_Update(app->a.m_pIDisplay); IGRAPHICS_Update(app->graphics); return; }
Component_Draw 関数でコンポーネントの描画を行っています。この関数の内部は、コンポーネントの種類に応じて領域に描画するコードです。
// コンポーネントの描画 static void Component_Draw(Component* component, GUIApp* app, int isFocused) { switch (component->type) { case COMPONENT_LABEL: IDISPLAY_DrawText(app->a.m_pIDisplay, AEE_FONT_NORMAL, component->text, -1, 0, 0, &component->rect, IDF_ALIGN_CENTER | IDF_ALIGN_MIDDLE); break; case COMPONENT_TEXTFIELD: ITEXTCTL_Redraw(component->component.textctl); IDISPLAY_DrawRect(app->a.m_pIDisplay, &component->rect, (isFocused) ? (MAKE_RGB(0, 255, 0)) : (MAKE_RGB(0, 0, 0)), 0, IDF_RECT_FRAME); break; //... } return; }
isFocused 引数は、現在そのコンポーネントにフォーカスがあたっていることを示すブール値です。
フォーカスの移動
キーイベントを処理して、コンポーネントから他のコンポーネントへフォーカスを移動させます。次のコードは、コンポーネントを配置した順番にそって、フォーカスを前後のコンポーネントに移動させます。
// キーが押されたときに呼び出される。 static boolean OnKey(GUIApp* app, uint16 key) { switch (key) { case AVK_DOWN: case AVK_RIGHT: SetFocusNextComponent(app); Draw(app); return TRUE; case AVK_UP: case AVK_LEFT: SetFocusPreviousComponent(app); Draw(app); return TRUE; case AVK_SELECT: // コンポーネントをセレクト Component_Select(app->components + app->component_index, app); Draw(app); return TRUE; } return FALSE; }
セレクトキーが押されたときの処理は後述します。
コンポーネントがフォーカスを取得できるかどうか
フォーカスを次のコンポーネントへ動かす SetFocusNextComponent 関数の宣言をみてみましょう。
// 次のコンポーネントへ static void SetFocusNextComponent(GUIApp* app) { int original = app->component_index; int index; // コンポーネントを配列の順番にチェックする for (index = (original == app->component_length - 1) ? (0) : (original + 1); index != original; index = (index == app->component_length - 1) ? (0) : (index + 1)) { if (Component_SetFocused(app->components + index)) { app->component_index = index; break; } } return; }
関数中に、Component_SetFocused 関数が呼ばれている箇所があります。この関数は、コンポーネントにフォーカスを当てて、正しくフォーカスが設定された場合に TRUE を返す関数です。
たとえば、ラベルコンポーネントにはフォーカスを当てる必要がありません。ですから、ラベルコンポーネントにフォーカスが移動される場合は、Component_SetFocused は FALSE を返します。その結果を見て、フォーカスはさらにその前後のコンポーネントに移動します。
// コンポーネントがフォーカスを取得 static int Component_SetFocused(Component* component) { switch (component->type) { case COMPONENT_LABEL: return FALSE; case COMPONENT_TEXTFIELD: ITEXTCTL_SetActive(component->component.textctl, TRUE); return TRUE; case COMPONENT_COMBOBOX: case COMPONENT_RADIOBUTTON: case COMPONENT_CHECKBOX: case COMPONENT_BUTTON: return TRUE; } return FALSE; }
テキストコントロールは、フォーカスが当たったときに Active にします。
コンポーネントを選択する
フォーカスを持つコンポーネントは種類に応じてセレクトキーを処理します。チェックボックスはチェックされ、コンボボックスは選択画面を表示たり、選択を決定したりします。
// セレクトキーが押されたときに、コンポーネントを選択 static void Component_Select(Component* component, GUIApp* app) { switch (component->type) { case COMPONENT_TEXTFIELD: break; case COMPONENT_COMBOBOX: if (component->component.combobox.selected) { component->component.combobox.index = component->component.combobox.cursor; component->component.combobox.selected = FALSE; } else { component->component.combobox.cursor = component->component.combobox.index; component->component.combobox.selected = TRUE; } break; case COMPONENT_RADIOBUTTON: if (!component->component.radiobutton.checked) { int i; for (i = 0; i < app->component_length; i++) { if (app->components[i].type == COMPONENT_RADIOBUTTON) { app->components[i].component.radiobutton.checked = FALSE; } } component->component.radiobutton.checked = TRUE; } break; case COMPONENT_CHECKBOX: component->component.checkbox.checked ^= 1; break; case COMPONENT_BUTTON: if (app->component_index == 16) { DBGPRINTF("** Create!"); } else if (app->component_index == 17) { DBGPRINTF("** Cancel!"); } break; } return; }
コンポーネントのイベント処理
テキストコントロールとコンボボックスがフォーカスを持つ場合、アプリ全体のイベント処理に先んじてコンポーネントで処理を行う必要があります。
static boolean GUIApp_HandleEvent(GUIApp* app, AEEEvent eCode, uint16 wParam, uint32 dwParam) { if (eCode == EVT_APP_START) { return OnAppStart(app); } else if (Component_HandleEvent(app->components + app->component_index, app, eCode, wParam, dwParam)) { // コンポーネントがイベントを処理した場合 Draw(app); return TRUE; } else { switch (eCode) { case EVT_APP_STOP: return OnAppStop(app); case EVT_KEY: return OnKey(app, wParam); } } return FALSE; }
具体的には、テキストコントロールがフォーカスを持つ場合には、すべてのイベントをテキストコントロールに渡します。また、テキストフィールドからフォーカスが外れる場合に送られる EVT_CTL_TAB を正しく処理します。
コンボボックスが選択画面を表示している場合は、コンボボックス内でのカーソルの操作、選択の決定やキャンセルなどのキーイベント処理を行います。
// コンポーネントが選択されている場合にイベントを処理 static int Component_HandleEvent(Component* component, GUIApp* app, AEEEvent evt, uint16 wp, uint32 dwp) { switch (component->type) { case COMPONENT_TEXTFIELD: if (ITEXTCTL_IsActive(component->component.textctl)) { if (ITEXTCTL_HandleEvent(component->component.textctl, evt, wp, dwp)) { return TRUE; } else if (evt == EVT_CTL_TAB) { // 別のコンポーネントへ ITEXTCTL_SetActive(component->component.textctl, FALSE); if (wp == 0) { SetFocusPreviousComponent(app); } else { SetFocusNextComponent(app); } return TRUE; } } break; case COMPONENT_COMBOBOX: if (component->component.combobox.selected && evt == EVT_KEY) { switch (wp) { case AVK_DOWN: if (component->component.combobox.cursor < component->component.combobox.size - 1) { component->component.combobox.cursor++; } return TRUE; case AVK_UP: if (component->component.combobox.cursor > 0) { component->component.combobox.cursor--; } return TRUE; case AVK_CLR: component->component.combobox.selected = FALSE; return TRUE; } } break; } return FALSE; }
さらに美しい設計のために
GUIApp の全体の説明はこれで終わりです。C 言語でできる限りきれいな設計を目指したつもりですが、残る課題もたくさんあります。
- 差分描画 : 必要なコンポーネントだけ再描画する
- コンテナ : ウインドウやタブなどのコンテナコンポーネント
- コンポーネントの属性 : フォーカスを取得できるかどうかなどの属性を設定できるようにする
- 相対座標 : 座標を相対的に扱うことで、コンポーネント内の描画を簡便にする
- 見た目 : 色、縁取り、動き(ボタンが凹む、など)を取り入れる
- テキストフィールド : 文字入力画面からもどってきたときのフォーカスの描画など
- イベント処理 : ボタンが押された場合の処理などに関数ポインタを使う
- コンポーネントの機能や種類 : ラジオボタンのグループ機能など
- コンポーネントの省メモリ化 : コンポーネントの共用体が持つ構造体変数をポインタにする
汎用的な GUI プログラミングには乗り越えなければならない壁がたくさんあるのです。