ホーム WDIC palmware開発 掲示板 リンク

palmware開発

palmware開発中に発見したことを徒然なるままに書き綴って行くつもりのページです。 増えてきたらフレームのページに変更するつもり。 私の環境が「CodeWarrior for Palm OS Platform 日本語版バージョン8」なのでそれを前提にしています。
DOC形式
DOC形式の読み込み
DOC形式のフォーマット
圧縮形式の展開
機種固有機能対応
Clie NR70 のワイドハイレゾ対応
Treo 600 の 5-way Navigator 対応
メモ帳
メモ帳への書き出し
バグ
スタック破壊
その他
マルチセグメントアプリケーションへの移行
[辞書を引く]メニューの追加

DOC形式のデータベース読み込み

DOC形式は、タイプが 'TEXt'、クリエータが 'REAd'のデータベース形式で、 データベース名が文書名になっています。 というわけで以下のようなコードでドキュメントの一覧が読み出せます。

Boolean newSearch;
DmSearchStateType state;
UInt16 cardNo;
LocalID dbID;
Char docName[dmDBNameLength];
newSearch = true; NumDocs = 0;
while (1) {
	if (errNone != DmGetNextDatabaseByTypeCreator(newSearch, &state,
				'TEXt', 'REAd', false, &cardNo, &dbID))
		break;
	DmDatabaseInfo(cardNo, dbID, docName, NULL, NULL, NULL,
			NULL, NULL, NULL, NULL, NULL, NULL, NULL);
	newSearch = false;
	NumDocs ++;
}

DOC形式のフォーマット

形式に関してはthe pyrite project: doc formatを参考にしました。 DOC形式のフォーマットは大きく分けて3つの部分に分かれます。

ヘッダーレコードのフォーマット

typedef struct {
	UInt16 version;
	UInt16 reserved;
	UInt32 length;
	UInt16 numRecords;
	UInt16 recordSize;
} DocHeaderType;
ヘッダーレコードのフォーマット
version2 bytes0x0002:圧縮, 0x0001:非圧縮
reserved2 bytes予約(0)
length4 bytes圧縮前の文書サイズ
numRecords2 bytes文書レコード数
recordSize2 bytesレコードサイズ(通常 4096)

圧縮形式の展開

文書レコードが非圧縮(version: 0x0001)の場合にはそのまま読み出してレコードを繋げれば文書ができあがります。圧縮形式の場合には文書レコードを展開してやる必要があります。 圧縮形式の展開アルゴリズムは以下になります。
データ(N)展開内容
0x01 - 0x08 続くNバイトをそのまま出力する
0x09 - 0x7f そのまま出力する
0x80 - 0xbf 続くもう一バイトを使用してUInt16としてスライド辞書コピーをする
得られた UInt16 の下位14bitの内、上位11ビットがコピー開始位置(1-2047bytes)、下位3ビット+3がコピーバイト数(3-10bytes)となる
0xc0 - 0xff スペース(' ') + N ^ 0x80 を出力する

ソースを見てもらうのが一番早いと思うのでソースを提示します。 srcP が文書レコードのポインタ、dstPが出力する側のポインタです。 一つだけ注意しないといけないのは dstP 側に用意するメモリのサイズです。 ヘッダの recordSize より少し大きく用意しなければなりません。 恐らくスライドコピー分(10bytes)程度大きければよいと思っていますが、 念のため128bytes位多めに確保しておきます。

while (srcP < srcP + recordSize) {
	UInt16 ch = * (UInt8 *) srcP;
	if (ch < 0x09) {
		/* 続くn(1〜8)文字をそのまま出力する */
		srcP ++;
		while (ch > 0) {
			*(dstP++) = *(srcP++);
			ch --;
		}
	} else if (ch < 0x80) {
		/* ASCII文字列をそのまま出力 */
		*(dstP++) = *(srcP++);
	} else if (ch < 0xc0) {
		/* スライド辞書コピー */
		UInt16 slide;	/* コピー開始位置 */
		UInt16 count;	/* コピー文字数 */
		ch <<= 8; ch += * (UInt8 *) (++srcP);  /* 2bytes を使用する */
		slide = (ch & 0x3fff) >> 3;
		count = (ch & 0x7) + 3;
		while (count > 0) {
			*dstP = *(dstP - slide);
			dstP ++; count --;
		}
		srcP ++;
	} else {
		/* スペース + ch ^ 0x80 */
		*(dstP++) = ' ';
		*(dstP++) = *(srcP++) ^ 0x80;
	}
}

Clie NR70系のワイド対応

行う必要があるのは以下の対応です。事前にハイレゾモードにしておく必要があります。
テーブルを使用している場合にはテーブルのサイズで行数が変えるようにする必要があります。
  1. SilkLibEnableResize()を使用して画面サイズの変更ができることを通知

    「Programmer's Companion for Sony CLIE」のサンプルプログラムをそのまま使用可能です。

  2. SysNotifyRegister()を使用してsysNotifyDisplayChangeEventを受け取れるようにする

    以下のようにアプリケーションのdbIDを獲得してSysNotifyUnregister()を呼んでやるだけです。

    err = DmGetNextDatabaseByTypeCreator(true, &state, 'appl',
    			appFileCreator, true, &cardNo, &dbID);
    if (!err)
    	SysNotifyUnregister(cardNo, dbID, sysNotifyDisplayChangeEvent,
    			sysNotifyNormalPriority);
    	
  3. PalmMain(PilotMain)にsysNotifyDisplayChangeEventを処理する関数を追加

    通常使ってない(と思う)PalmMainの第二引数がSysNotifyParamTypeへのポインタとなっています。 Wdicでは手抜きしてここからすぐにFrmGotoForm()を呼んでしまってますが、 本当は画面のサイズが変わったかを調べて、 変わっていたら各フォームの再描画関数を呼ぶというふうにした方がよいはずです。

    static UInt32 StarterPalmMain(UInt16 cmd, SysNotifyParamType *notifyP, UInt16 launchFlags)
    {
    	switch (cmd) {
    		case sysAppLaunchCmdNormalLaunch:
    			error = AppStart();
    			if (error) 
    				return error;
    			FrmGotoForm(MainForm);
    			AppEventLoop();
    			AppStop();
    			break;
    		case sysAppLaunchCmdNotify:
    			if (notifyP->notifyType != sysNotifyDisplayChangeEvent)
    				break;
    			formID = FrmGetActiveFormID();
    			if (formID)
    				FrmGotoForm(formID);
    			break;
    	}
    
  4. 再描画関数で画面サイズを得て、フォームの各オブジェクトを再配置/サイズ変更

    1. 画面サイズはWinGetDisplayExtent()を使って得られます
      WinGetDisplayExtent(NULL, &CurWinExtent);
      
    2. フォームのサイズ変更は以下のような関数を作りました(extentにCurWinExtentを渡す)
      void ClieSetWindowBounds(FormType *frmP, Coord extent)
      {
      	WinHandle win;
      	RectangleType r;
      	Coord diff =  extent - defaultDisplayExtent;
      
      	// extent form
      	win = FrmGetWindowHandle(frmP);
      	win = WinSetDrawWindow(win);
      	WinGetDrawWindowBounds(&r);
      	r.extent.y += diff;
      	WinSetWindowBounds(win, &r);
      }
      
    3. オブジェクトの位置変更/サイズ変更は以下のような関数を作りました(位置変更はclieChangeTop、サイズ変更はclieChangeExtentを渡す)
      void ClieChangeObjectBounds(FormType *frmP, Int16 objID,
      		Coord extent, ClieChangeEnum type)
      {
      	RectangleType r;
      	Coord diff =  extent - defaultDisplayExtent;
      	Int16 index = FrmGetObjectIndex(frmP, objID);
      	FrmGetObjectBounds(frmP, index, &r);
      	if (type == clieChangeTop)
      		r.topLeft.y += diff;
      	else
      		r.extent.y += diff;
      	FrmSetObjectBounds(frmP, index, &r);
      }
      

Treo 600の5-Way Navigation

  Treo 600 の 5-Way Navigation は Tungsten シリーズに搭載されているものとは、全く別のものです。Treo 600では5-Way Rockerと呼んでいるようです。

  Handspringの開発ホームページからダウンロード(登録が必要)できる「Developer Reference Guide: Handspring Treo 600 Communicator」に5-way Rockerについての記述がありますが、非常に厄介に見えます。

  1. まず手始めに5-Way Rockerが搭載されているかどうかの判断です。ソースを見た方が早いので以下を参照のこと
    if (FtrGet(hsFtrCreator, hsFtrIDNavigationSupported, &version) == errNone) {
    	hasFiveWayRocker = true;
    }
      

メモ帳への書き出し

PalmのホームページからダウンロードできるSDK40-ExampleのMemoMain.cのCreateRecord()を、 ちょっと直せば書き込みルーチンのできあがりです。

以下はその例です。 ソートはメモ帳を呼び出した時に行われるので考える必要はありません (Preferenceでレコード数を管理しており、レコードが追加された場合にソートしている)。

#define memoDBType		'DATA'
#define memoMaxLength	4096

static Err CopyToMemoPad(Char *note)
{
	UInt16 index;
	MemHandle memoRec;
	Char *dstP;
	UInt16 attr;
	DmOpenRef memoDB;
	UInt32 size;

	memoDB = DmOpenDatabaseByTypeCreator(memoDBType, sysFileCMemo, dmModeReadWrite);
	if (!memoDB) {
		FrmCustomAlert(InformationAlert,
				"メモ帳データベースのオープンに失敗しました", NULL, NULL);
		return errNone;
	}

	size = StrLen(note);
	if (size > memoMaxLength) {
		if (FrmCustomAlert(ConfirmationAlert, "データが4096文字以上です。\n",
				"4096文字以上は切り捨てられますがよろしいですか?", NULL)) {
			DmCloseDatabase(memoDB);
			return errNone;
		}
		size = memoMaxLength;
	}
	index = DmNumRecords(memoDB);
	memoRec = DmNewRecord(memoDB, &index, size);

	// If the allocate failed, display a warning.
	if (!memoRec) {
		ErrorMessage("データベースメモリの確保に失敗しました(%d bytes)", size);
		DmCloseDatabase(memoDB);
		return dmErrMemError;
	}

	dstP = MemHandleLock (memoRec);
	DmWrite(dstP, 0, note, size);
	MemHandleUnlock(memoRec);

	// Set the category of the new record to the current category.
	DmRecordInfo (memoDB, index, &attr, NULL, NULL);
	attr &= ~dmRecAttrCategoryMask;
	DmSetRecordInfo (memoDB, index, &attr, NULL);
	DmReleaseRecord (memoDB, index, true);
	DmCloseDatabase(memoDB);
	return errNone;
}

バグ

スタック破壊
Wdic Version0.7.00開発中にぶつかったバグ。以下のような現象に見えていました POSEが何もいわずに終了する場合にはどうしようもないのですが、何度もgremlinをトライしている内に以下ソースのCtlSetUsable()関数内で不正アドレスアクセスを起こす事象を捕まえました。この時のデバッガの変数表示を見るとfrmPが壊れているために不正アクセスを起こしているように見えました。
static void ViewFormInit(FormPtr frmP)
{
	HistType hist;
... 省略 ...
 	rec = UnpackHistory(&hist, CurViewIndex);
	SetStrToField(frmP, ViewWordField, hist.word);
	MemHandleUnlock(rec);
... 省略 ...
	if (CurViewIndex == InitViewIndex)
		CtlSetUsable(FrmGetObjectPtr(frmP, FrmGetObjectIndex(frmP, ViewBackButton)), false);

}
スタック破壊はCtlSetUsable()の前のUnpackHistory()関数内で起きていました。スタック破壊を起こしている時には、今回のように不正アドレスアクセスを起こした前の関数でスタック破壊を起こしていることが多いようです。 StrCopy()でデータベース内のdicName(辞書名)をコピーした際、文字列が想定していた長さを超えていたためにスタックのhistの領域超えて書き込まれていました。
MemHandle UnpackHistory(HistType *hist, UInt16 idx)
{
... 省略 ...
	rec = DmQueryRecord(HistDB, idx);
	packedHist = MemHandleLock(rec);
	StrCopy(hist->dicName, packedHist->dicName);
... 省略 ...
}


マルチセグメントアプリケーションへの移行

プログラムのサイズが64KBを超えたらマルチセグメントアプリケーションに移行しなければなりません。 CodeWarrior のスマートメモリモデルで作成していると64KBを超えても何もエラーがでないため注意が必要です。 私の場合には 80KB を超えた段階でいきなり作成したプログラムがPOSEやpalmデバイスで読み込めなくなって、 非常に驚きました。 ラージメモリモデルに変更したところ、64KBを超えているとメッセージが表示されてやっと問題に気付きました。

マルチセグメントへの移行で少しはまってしまった(ランタイムライブラリを入れ替える必要があることに気付かなかった)ので手順を書いておきます。 CodeWarriorの"Palm Documentation\\Targeting Palm OS" ディレクトリにある“Targeting_Palm_OS.pdf”の「Converting to a Multi-Segment Application」の章に詳しい説明があります。

  1. まずプロジェクトの設定(Alt+F7)を開き以下の設定を行います。
    • リンカの68Kリンカで、「単一セグメントにリンク」チェックを外す
    • コード生成の68Kプロセッサで、「コードモデル」をスマートかラージに設定
    • ターゲットの68Kターゲットで、「拡張モードA4/A5相対データ」にチェックをいれる
  2. PalmOSRuntime_XXXX を削除
  3. メニューの「プロジェクト」からファイルの追加を選んで、CodeWarriorのディレクトリから“Palm OS Support”→“CodeWarrior Libraries”→“Runtime”とたどって、「MSL Runtime Palm OS (2i).Lib」(2byte Int)もしくは「MSL Runtime Palm OS (4i).Lib」を追加する
  4. プロジェクトの「セグメント」タグを開いて、メニューの「プロジェクト」から「セグメントを作成」を選択し、そのままOKを選択
  5. 以下のルールを守って、各セグメントのコードとデータが64KBを超えないようにファイルを移動
    • MSL Runtime Palm OS (XX).Lib と PilotMain の含まれるファイルは必ず最初のセグメント
    • PilotMain が sysAppLaunchCmdNormalLaunch 以外のランチコードで起動された時に呼ばれる関数/変数は全て同じ(最初の)セグメント

[辞書を引く]メニューの追加

辞書を引くのメニューに関してはほとんど情報が見つかりませんでした。 きっとG-PalmさんのホームページのDataにある説明が見つからなければあきらめていたでしょう。

このページによると sysEditMenuJapLookupWord(10101)のメニューIDをもつメニューを作成し、 アプリケーション側では何もハンドリングしなければ、OSが自動的に辞書アプリを立ち上げるそうです。 このページでは EvtAddEventToQueue() を使用する方法を紹介していますが、 Wdicでは素直に、イベントハンドラでmenuOpenEventを受けた時にMenuAddItem()でsysEditMenuJapLookupWordのメニューを追加する方法を取っています。

	case menuOpenEvent:
		MenuAddItem(ViewCopyToMemoPad, sysEditMenuJapLookupWord, 0, "辞書を引く");
		break;
普通のフィールドを使っているアプリケーションの場合はこれだけで終わりです。

Wdicのようにフィールドがないアプリケーションはもう少し工夫が必要で、 以下のようなことを行っています。