Palm Programmer's Laboratory
Palm OS Programmer's Companion Volume I/8-3
8-3 文字列
テキストを文字列として扱う場合、ストリングマネージャとテキストマネージャを使用します。
ストリングマネージャは Palm OS の全てのリリースでサポートされています。ストリングマネージャは strcpy や strcat などの C 言語標準の文字列操作関数に合わせて設計されています。C の標準ライブラリ関数は Palm OS には組み込まれていないことに注意して下さい。アプリケーションを小さくするためには、C の標準コールではなくストリングマネージャコールを使用して下さい。
テキストマネージャはマルチバイト文字列のサポートを提供するために Palm OS 3.1 から追加されました。テキストマネージャをサポートするシステムでは、文字列は単一バイト幅、あるいは 4 バイトを上限とするマルチバイト幅の文字で構成されます。前述の通り、文字変数は常に2バイト幅です。しかし、文字列に文字を追加した場合、システムはそれが下位の ASCII 文字であれば単一バイトに縮小します。そのため、操作している文字列はどれでも単一バイト文字とマルチバイト文字が混在している可能性があります。
アプリケーションは文字列の操作にテキストマネージャとストリングマネージャの両方を利用できます。Palm OS 3.1 以降のストリングマネージャ関数はマルチバイト文字を含む文字列を扱うことができます。テキストマネージャの関数は以下のような状況で使用して下さい。
- ストリングマネージャに対応する関数がない場合。
- マッチするような文字列の長さが重要な場合。例えば、2つの文字列を比較する場合、StrCompare か TxtCompare のいずれかを使用することができます。両者の違いは、StrCompare がマッチした文字の長さを返さないことです。TxtCompare は文字の長さを返します。
このセクションでは、以下のトピックを扱います。
- 文字列の操作
- 文字列ポインタの操作
- 表示テキストの切り詰め
- 文字列の比較
- 大域検索
- 文字列コンテンツの動的作成
- StrVPrintF 関数の使い方
- TIP
- Palm OS 3.1 より前からある関数の多くは、マルチバイト文字を含む文字列の扱いに関して変更が加えられています。FldGetTextLength や StrLen など、文字列長を返す全ての Palm OS 関数は、常に文字列のサイズを文字数ではなくバイト長で返します。同様に、文字列のオフセットを指定する関数は常にオフセットを文字でなくバイトとして扱います。
文字列の操作
文字を指すポインタを扱う場合、文字内の境界( マルチバイト文字の真ん中や最後 )を指さないように注意しなければなりません。例えば、テキストフィールドの挿入ポイント位置や選択状態を設定する場合、文字間の境界を指すオフセットを指定していることを保証しなければなりません。( 文字間の境界とは、オフセットが文字列の先頭あるいは末尾を指す場合を除き、ある文字の先頭と直前の文字の末尾の両方を指します )
文字列内の文字を順に反復したいとします。習慣的な C コードでは、文字ポインタやバイトカウンタを使用して一度に1文字ずつ反復を行なっていました。このようなコードはマルチバイト文字を扱うシステムでは正しく動作しません。1文字ずつ文字列内の文字を反復するには、以下のテキストマネージャ関数を使用します。
- TxtGetNextChar (TxtGlueGetNextChar) は文字列内の次の文字を取得します。
- TxtGetPreviousChar (TxtGlueGetPreviousChar) は、文字列内の直前の文字を取得します。
- TxtSetNextChar (TxtGlueSetNextChar) は文字列内の次の文字を変更します。文字列バッファを埋めるためにも使用できます。
これら3つの関数はそれぞれ対象の文字のサイズを返すため、それによって次の文字のオフセットを決定することができます。例えば、リスト 8.3 では特定の文字を見つけるまで1文字ずつ文字列内の文字を反復する方法を示しています。
リスト 8.3 文字列またはテキストの反復
Char* buffer; // assume this exists UInt16 bufLen = StrLen(buffer); // Length of the input text. WChar ch = 0; UInt16 i = 0; while ((i < bufLen) && (ch != chrAsterisk)) i+= TxtGlueGetNextChar(buffer, i, &ch));
テキストマネージャには、文字列内の文字を反復することなく文字数を調べることのできる関数も含まれています。
- TxtCharSize (TxtGlueCharSize) は、指定された文字が文字列中で占めるスペースを返します。
- TxtCharBounds (TxtGlueCharBounds) は、指定された文字列内の指定文字の境界を判定します。
リスト 8.4 不定境界の扱い
UInt32* charStart, charEnd; Char* fldTextP = FldGetTextPtr(fld); TxtGlueCharBounds(fldTextP, min(kMaxBytesToProcess, FldGetTextLength(fld)), &charStart, &charEnd); // process only the first charStart bytes of text.
文字列ポインタの操作
テキストマネージャに渡す文字列のポインタ操作は、テキストマネージャコールを使用する以外の方法で行なわないで下さい。テキストマネージャ関数が正しく動作するためには、文字列ポインタは文字の先頭バイトを指している必要があります。文字列ポインタを操作するためにテキストマネージャ関数を使用すれば、文字列ポインタが常に文字の先頭バイトを指していると仮定できます。テキストマネージャを使わないと、ポインタが文字内境界を指すリスクが発生します。
リスト 8.5 文字列ポインタの操作
// WRONG! buffer + kMaxStrLength is not // guaranteed to point to start of character. buffer[kMaxStrLength] = '\0'; // Right. Truncate at a character boundary. UInt32 charStart, charEnd; TxtCharBounds(buffer, kMaxStrLength, &charStart, &charEnd); TxtGlueSetNextChar(buffer, charStart, chrNull);
表示テキストの切り詰め
描画処理において文字列が利用可能なスペースに収まらない場合、切り詰める個所を決める必要があります。マルチバイト文字を含む文字列に対してこの作業を行う場合、以下の2つの関数が役に立ちます。
- WinDrawTruncChars (WinGlueDrawTruncChars) ── この関数は、指定された幅にあわせて文字列を描画します。その際、自動的に切り詰める個所を決定します。可能であれば、文字列全体を描画し、文字列がスペース内に収まらなければ、収まる文字数よりも1文字少ない文字数分を描画し、最後に省略記号である ... を付加します。
- FntWidthToOffset (FntGlueWidthToOffset) ── この関数は指定されたピクセル位置に表示される文字のバイトオフセットを返します。また、この関数はそのオフセットに収まらテキストの幅も返します。リスト 8.6 はスクリーン上に文字列を描画するのに何行必要とするのかを調べるために FntWidthToOffset を利用する例を示しています。この例ではピクセル位置として 160 を渡し、結果として widthToOffset に1行に表示することのできる文字列の最後の文字のバイトオフセットを取得しています。widthToOffset までの文字を描画すると、msg ポインタを widthToOffset 文字分だけ前に進め、FntWidthToOffset を再びコールして次の行に収まる文字数を取得しています。この処理を全ての文字が描画されるまで繰り返します。
リスト 8.6 複数行テキストの描画
Coord y; Char *msg; Int16 msgWidth; Int16 widthToOffset = 0; Int16 pixelWidth = 160; Int16 msgLength = StrLen(msg); while (msg && *msg) { widthToOffset = FntGlueWidthToOffset(msg, msgLength, pixelWidth, NULL, &msgWidth); WinDrawChars(msg, widthToOffset, 0, y); y += FntLineHeight(); msg += widthToOffset; msgLength = StrLen(msg); }
文字列の比較
文字列の比較には、テキストマネージャの関数である TxtCompare (TxtGlueCompare) と TxtCaselessCompare (TxtGlueCaselessCompare) を使用します。
マルチバイト文字を使用する文字エンコーディングでは、いくつかの文字は単一バイトまたはマルチバイトで正確に表現されます。すなわち、文字によっては単一バイト表現と2バイト表現の両方がある場合があります。ある文字列では単一バイト表現が使われ、別の文字列ではマルチバイト表現が使われている場合もありえますが、ユーザーはその文字が何バイトで保存されているかに関係なく、同じ文字としてマッチすることを期待します。TxtCompare と TxtCaselessCompare は同じ文字の単一バイト表現とマルチバイト表現を正しく比較します。
単一バイト文字がマルチバイト文字とマッチする場合があるため、長さの異なる2つの文字列でも内容が一致すると判断される場合があります。この理由により、TxtCompare と TxtCaselessCompare は、2つの文字列それぞれでマッチしたテキストの長さを返すための2つのパラメータをとります。詳細については Palm OS Programmer's API Reference の説明を参照して下さい。
ストリングマネージャの関数である StrCompare と StrCaselessCompare はこれらに対応しますが、マッチする文字列の長さを返さないことに注意して下さい。
大域検索
文字列比較の実行に関する特殊なケースとしては、システムの大域検索機能の実装があります。この機能を実装するためには、TxtFindString(TxtGlueFindString) をコールします。TxtCompare や TxtCaselessCompare と同様に、TxtFindString は単一バイト文字とそれに対応するマルチバイト文字を的確にマッチします。さらに、この関数はマッチしたテキストの長さを返します。システムからマッチしたレコードを表示するようにリクエストされた際、マッチしたテキストをハイライト表示するためにこの値が必要になるでしょう。
古いバージョンの Palm OS は FindStrInStr 関数を使用しています。FindStrInStr はマッチしたテキストの長さを返すことができません。そのかわり、文字列に含まれる文字は常に1バイト長であると仮定します。
ユーザーが検索ボタンをタップすると、システムは全てのアプリケーションに起動コード sysAppLaunchCmdFind を送信します。リスト 8.7 に、この起動コードに対する応答としてコールされる関数の例を示します。この関数はテキストマネージャの有無に関わらず全てのシステムで機能する大域検索を実装しています。ユーザーが検索結果ダイアログに表示された結果の1つをタップすると、システムはそのレコードを保有するアプリケーションに対して起動コード sysAppLaunchCmdGoTo を送信します。リスト 8.8 に起動コード sysAppLaunchCmdGoTo に対する応答の仕方を示します。
これら2つの例はコードの抜粋に過ぎません。これら2つの関数の完全な実装は、Palm OS SDK のサンプルコードを参照して下さい。
もし TxtFindString を( 大域検索機能ではなく )アプリケーション内部の検索の実装に使用したいのであれば、TxtFindString をコールする前に TxtGluePrepFindString をコールして文字列が正しいフォーマットであることを確かめる必要があります。( 大域検索機能では、コードが実行されるよりも前にシステムが文字列を用意しています。 )
リスト 8.7 大域検索の実装
static void Search (FindParamsPtr findParams) { UInt16 recordIndex = 0; DmOpenRef dbP; UInt16 cardNo = 0; LocalID dbID; MemoDBRecordPtr memoPadRecP; // Open the database to be searched. dbP = DmOpenDatabaseByTypeCreator(memoDBType, sysFileCMemo, findParams->dbAccesMode); DmOpenDatabaseInfo(dbP, &dbID, 0, 0, &cardNo, 0); // Get first record to search. memoRecP = GetRecordPtr(dbP, recordIndex); while (memoRecP != NULL) { Boolean done; Boolean match; UInt32 matchPos, matchLength; // TxtGlueFindString calls TxtFindString if it // exists, or else it implements the Latin // equivalent of it. match = TxtGlueFindString (&(memoRecP->note), findParams->strToFind, &matchPos, &matchLength); if (match) { done = FindSaveMatch (findParams, recordIndex, matchPos, 0, matchLength, cardNo, dbIDP); } MemPtrUnlock (memoRecP); if (done) break; recordIndex += 1; } DmCloseDatabase (dbP); }
リスト 8.8 マッチしたレコードの表示
static void GoToRecord (GoToParamsPtr goToParams, Boolean launchingApp) { UInt16 recordNum; EventType event; recordNum = goToParams->recordNum; ... // Send an event to goto a form and select the // matching text. MemSet (&event, sizeof(EventType), 0); event.eType = frmLoadEvent; event.data.frmLoad.formID = EditView; EvtAddEventToQueue (&event); MemSet (&event, sizeof(EventType), 0); event.eType = frmGotoEvent; event.data.frmGoto.recordNum = recordNum; event.data.frmGoto.matchPos = goToParams->matchPos; event.data.formGoto.matchLen = goToParams->matchCustom; event.data.frmGoto.matchFieldNum = goToParams->matchFieldNum; event.data.frmGoto.formID = EditView; EvtAddEventToQueue (&event); ... }
文字列コンテンツの動的作成
ローカライズされたアプリケーションで文字列を扱う場合、決して文字列を挟ハードコーディングしてはいけません。そのかわり、リソースに文字列を保存し、テキストを表示する際にはリソースを使うようにします。実行時に文字列コンテンツを作成する必要がある場合、文字列のテンプレートをリソースとして保存し、必要に応じて値を置き換えて下さい。
例えば、メモ帳アプリケーションの編集画面を考えます。タイトルバーには“Memo 3 of 10.”といった文字列が含まれています。表示されているメモの番号とメモの総数は実行時になるまでわかりません。
このような文字列を生成するには、テンプレートリソースとテキストマネージャ関数である TxtParamString (TxtGlueParamString) を使用します。TxtParamString を使うことで、^0, ^1 から ^3 までのシーケンスを探してそれぞれを異なる文字列に置き換えることができます。もっと多くのパラメータが必要であれば、TxtReplaceStr (TxtGlueReplaceStr) を使うことができます。これは ^9 まで置き換えることができますが、TxtReplaceStr は一度に1つの置き換えしかできません。
メモ帳のタイトルバーの例では、以下のような文字列リソースを作ることになります。
Memo ^0 of ^1
そして、コードは以下のようなものになるでしょう。
リスト 8.9 文字列テンプレートの使用
static void EditViewSetTitle (void) { Char* titleTemplateP; FormPtr frm; Char posStr [maxStrIToALen]; Char totalStr [maxStrIToALen]; UInt16 pos; UInt16 length; // Format as strings, the memo's postion within // its category, and the total number of memos // in the category. pos = DmPositionInCategory (MemoPadDB, CurrentRecord, RecordCategory); StrIToA (posStr, pos+1); if (MemosInCategory == memosInCategoryUnknown) MemosInCategory = DmNumRecordsInCategory(MemoPadDB, RecordCategory); StrIToA (totalStr, MemosInCategory); // Get the title template string. It contains // '^0' and '^1' chars which we replace with the // position of CurrentRecord within // CurrentCategory and with the total count of // records in CurrentCategory (). titleTemplateP = MemHandleLock( DmGetResource( strRsc, EditViewTitleTemplateStringString )); EditViewTitlePtr = TxtGlueParamString(titleTemplateP, posStr, totalStr, NULL, NULL); // Now set the title to use the new title // string. frm = FrmGetFormPtr (MemoPadEditForm); FrmSetTitle (frm, EditViewTitlePtr); MemPtrUnlock(titleTemplateP); }
StrVPrintF 関数の使用
C 言語の vsprintf 関数のように、StrVPrintF 関数は可変個の引数をとる関数から呼ばれ、その引数を文字列の書式化に使用できるようにデザインされています。このセクションでは、StrVPrintF 関数の使い方の概要を説明します。詳細については、C 言語標準のリファレンスブックにおける vsprintf と stdarg.h のマクロの使い方を参照して下さい。
StrVPrintF をコールする場合、関数に渡された( 固定引数の後ろにある )オプションの引数にアクセスするために stdarg.h の特殊なマクロを使用する必要があります。これはオプションの引数をとる関数の宣言において引数リストの最後に省略記号を使用したために必要になるものです。
MyPrintF( CharPtr s, CharPtr formatStr, ... );
この省略記号は、formatStr 引数に続いて0個以上のオプションの引数を関数に渡せることを示しています。これらのオプション引数は名前を持たないので、StrVPrintF に渡すためには stdarg.h のマクロを使用してそれらにアクセスする必要があります。
関数の中でこれらのマクロを使うには、まず va_list 型の args 変数を宣言します。
va_list args;
次に、va_start を使用して args 変数がオプション引数の先頭を指すように初期化します。
va_start( args, formatStr );
va_start マクロに渡す2つめの引数には、関数に渡された固定引数のうち最後のもの(つまりオプション引数の1つ前)を指定します。これで args 引数を StrVPrintF 関数の最後のパラメータとして渡せるようになりました。
StrVPrintF( text, formatStr, args );
終了したら、関数から復帰する前に va_end マクロを使用します。
va_end( args );
StrPrintF 関数と StrVPrintF 関数が実装しているのは、ANSI C 関数における vsprintf 関数の変換仕様のサブセットに過ぎないことに注意して下さい。詳細については StrVPrintF 関数のリファレンスを参照して下さい。