[岩間のホームページ]

非日本語対応 Windows アプリケーションで日本語を表示させるための一般的手法

Last Modified: Sunday, 21-Dec-1997 23:26:32 JST

ここでは、Pilot Desktop に限らず、非日本語対応 Windows アプリケーショ ンで日本語を表示させるパッチを作成するための一般的手法について解説します。 同様の手法で中文やハングルなども表示できると思います。


本ページの内容は筆者が独自に調査したものです。 調査はなるべく誤りのないように留意したつもりですが、調査は完璧というも のからは程遠いものです。それ故、このページの内容に基づきパッチを当てる ことは、ディスク上のデータの破壊などの危険性を伴ないます。十分注意して 下さい。

また、本ページの内容に基づいて行われてた全ての行為及びその結果について、 筆者は一切の責任を負わないものとします。特に、多くの商用アプリケーション にはライセンス中にリバースエンジニアリング禁止規定があることに留意して 下さい。


目次
  1. 基礎編:Windows プログラムでのフォントの取り扱い方
    1. CreateFont と CreateFontIndirect API
      1. CreateFont
      2. CreateFontIndirect
    2. ダイアログでのフォントの扱い
    3. オブジェクトコードレベルから見た Windows API の呼び出し
  2. 実践編:どのようにパラメータを書き変えるか
    1. ツール
    2. 如何にコードを埋め込むか?
      1. 同一の働きをする命令長の短いコードに置き換えを行い、空いた部分にコー ドを埋め込む。
      2. パラメータのデフォルト値 0 を利用する。
      3. いっそのことパラメータの設定を諦めてしまう(CreateFontIndirect)
    3. Resource Workshop を用いずにダイアログのデフォルトフォントを書き換える手法

基礎編:Windows プログラムでのフォントの取り扱い方

CreateFont と CreateFontIndirect API

まず、基礎編として、Windows プログラム中でのフォントの指定方法について 概括します。 Windows で画面表示やプリンタ出力のためのフォントを指定するためには、 論理フォントというものを作成する必要があります。この論理フォントの作成 の際に、フォントの大きさや、タイプフィスそして言語の種類(character set)を指定するようになっています。この時用いる API が CreateFont と CreateFontIndirect です。 そして作成した論理フォントをデバイスコンテキストに選択することに より、指定したフォントの表示が可能になります(ダイアログについては少々 流れが異なりますが、それについては後述します)。 したがって、日本語の表示できないアプリケーションを日本語表示させるため には、CreateFont や CreateFontIndirect の呼び出し時の character set に 関するパラメータを変更して、やれば日本語表示ができるようになります。

CreateFont

では次に以下に CreateFont の宣言を参照しより具体的に見ていきます。 (BC5 附属のヘルプ Win32 Programes's Reference より引用。以下特に断りが ない限り、同 Reference からの引用です。)

HFONT CreateFont(
    int  nHeight,	// logical height of font 
    int  nWidth,	// logical average character width 
    int  nEscapement,	// angle of escapement 
    int  nOrientation,	// base-line orientation angle 
    int  fnWeight,	// font weight 
    DWORD  fdwItalic,	// italic attribute flag 
    DWORD  fdwUnderline,	// underline attribute flag 
    DWORD  fdwStrikeOut,	// strikeout attribute flag 
    DWORD  fdwCharSet,	// character set identifier 
    DWORD  fdwOutputPrecision,	// output precision 
    DWORD  fdwClipPrecision,	// clipping precision 
    DWORD  fdwQuality,	// output quality 
    DWORD  fdwPitchAndFamily,	// pitch and family 
    LPCTSTR  lpszFace 	// address of typeface name string 
   );
上から判るように、論理フォントの作成には多くのパラメータがありますが、 日本語の表示のためだけでしたら、fdwCharSet のみを書き変えれば OK です。 タイプフェイス名すら書き変える必要はありません。 Windows はパラメータの値を参照して、パラメータの値に近いフォントを選択 してくれます。 fdwCharSet のパラメータは以下のようなものが wingdi.h で定義されていま す。
#define ANSI_CHARSET            0
#define DEFAULT_CHARSET         1
#define SYMBOL_CHARSET          2
#define SHIFTJIS_CHARSET        128
#define HANGEUL_CHARSET         129
#define GB2312_CHARSET          134
#define CHINESEBIG5_CHARSET     136
#define OEM_CHARSET             255
(以下略)
ということで、日本語フォントを選択するためにはfdwCharSet を 128(0x80) にすれば良いことが判ります。

CreateFontIndirect

次に CreateFontIndirect について見てみます。


HFONT CreateFontIndirect(
    CONST LOGFONT  *lplf 	// address of logical font structure  
   );	

ここで LOGFONT 構造体は

typedef struct tagLOGFONT { // lf 
   LONG lfHeight; 
   LONG lfWidth; 
   LONG lfEscapement; 
   LONG lfOrientation; 
   LONG lfWeight; 
   BYTE lfItalic; 
   BYTE lfUnderline; 
   BYTE lfStrikeOut; 
   BYTE lfCharSet; 
   BYTE lfOutPrecision; 
   BYTE lfClipPrecision; 
   BYTE lfQuality; 
   BYTE lfPitchAndFamily; 
   TCHAR lfFaceName[LF_FACESIZE]; 
} LOGFONT; 

と定義されています。見てわかるように、LOGFONT 構造体の要素は CreateFont のパラメータと同一です。 つまり、CreateFont で沢山のパラメータを並べる代りに、LOGFONT 構造体に 予め値を代入し、そのポインタを渡すことで間接的(Indirect)にパラメータを 渡すのが、この CreateFontIndirect になります。

ダイアログでのフォントの扱い

ダイアログのフォントについては、通常リソースで定義します。 Borland C++ 4.5 以前に附属の Resource Workshop や、 Borland C++ 5.0 以降の IDE のリソースエディタなどではこのリソースを コンパイル/逆コンパイルが可能ですので、exe や dll ファイルのリソースを 編集することができます。 また、プログラム中からフォントを変更することもできます。 これは、SendMessage API を用いダイアログに論理フォントのハンドル (hfont)をパラメータとして WM_SETFONT メッセージを送ることにより行いま す。
LRESULT SendMessage(

    HWND  hwnd,	// handle of destination window
    UINT  uMsg,	// message to send
    WPARAM  wParam,	// first message parameter
    LPARAM  lParam 	// second message parameter
   );	

WM_SETFONT 

wParam = (WPARAM) hfont;            // handle of font 
lParam = MAKELPARAM(fRedraw, 0);    // redraw flag 
ダイアログの仕様上、フォントさえ日本語フォントを選択すれば、多くの場合 問題なく日本語の入力/表示が可能となるようです。さすがに禁則処理などは してくれませんが。

オブジェクトコードレベルから見た Windows API の呼び出し

以上の知識があればソースコードレベルで日本語表示ができるようになります。 しかし、オブジェクトコードしか存在しない場合も多々あります。そこで、こ こでは Windows API の呼び出しがオブジェクトレベルではどのように行われ るのかを見ます。

CreateFont

68xxxxxxxx       push   xxxxxxxx	// Pointer of Typeface Name
6a04             push   +00	// fdwPitchAndFamily	DEFAULT_PITCH
6a00             push   +00	// fdwQuality		DEFAULT_QUALITY
6a20             push   +20	// fdwClipPrecision	CLIP_TT_ALWAYS
6a04             push   +04	// fdwOutputPrecison	OUT_TT_PRECIS
6a00             push   +00	// fdwCharSet		ANSI_CHARSET
6a00             push   +00	// fdwStrikeOut		FALSE
6a00             push   +00	// fdwUnderLine		FALSE
6a00             push   +00	// fdwItalic		FALSE
68bc020000       push   000002bc	// fnWeit	FW_BOLD
6a00             push   +00	// nOrientation
6a00             push   +00	// nEscapement
6a00             push   +00	// nWidth
6a0e             push   +0e	// nHeight
ff15185a0410     call   dword ptr [GDI32.CreateFontA]
これは典型的な CreateFont API 呼び出しのパターンです。(CreateFont では なく CreateFontA を呼びだしていますが、ここでは特に気にする必要はあり ません)。 このように呼び出しパラメータは、C で記載した場合の左から順にスタックに 積まれていき、API をコールすることで実行されます。戻り値は eax レジス タに格納されるようです(きちんと確認したわけではありませんが)。 上の例では、
6a00             push   +00	// fdwCharSet		ANSI_CHARSET
6a80             push   -80	// fdwCharSet		SHIFTJIS_CHARSET
と書き変えてやれば日本語フォントが生成されることになります。

CreateFontIndirect

パラメータを間接的に与える CreateFontIndirect では、事態はもう少々複雑 になります。上で見たように LOGFONT 型のポインタがパラメータとなるため、 ポインタの値を基に相対位置を指定し、LOGFONT 型の変数を書きかえてやらな くてはなりません。 ここで先程のLOGFONT構造体の宣言を思いだして下さい。 7番目のパラメータである lfCharSet はポインタからのオフセット値は LONG * 5 + BYTE * 3 = 4 * 5 + 1 * 3 = 23 = 0x17 となります。 例えば
8d8d6cffffff     lea    ecx,dword ptr [ebp+ffffff6c]	// 6c = -94
51               push   ecx
ff15xxxxxxxx     call   dword ptr [GDI32.CreateFontIndirectA]
というコードがあった場合、
b080             mov    al,80
884583           mov    [ebp-7d],al	// -94 + 17 = -7d
といったようなコードを API 呼び出しより前の適当な場所に挿入できれば日 本語のフォントが作成されるわけです(挿入できるかどうかは、なかなか難し い問題なのですが...)。

実践編:どのようにパラメータを書き変えるか

以上の知識があれば適当な逆アセンブラ、バイナリエディタ、リソースエディ タ(できればデバッガ)などを用意することにより、日本語フォントを生成し、 表示するようプログラムを書き変えることが可能となります。 しかしながら、実際にバイナリの書き変えをする場合には種々の制限があり、 なかなか上手く行かない場合も多いのが現実です。 そこでここでは、実際にバイナリを直接書き変える際の Tips を列挙していき ます。

ツール

参考までに、筆者が使用しているツールを以下に示します。
バイナリエディタ
Binary Editor BZ Version 0.95
βバージョンのため、バギーな点も少々ありますが、シンプルにして強 力なバイナリエディタです。(ベクターデザイン発行 PACK シリーズ収録)
逆アセンブラ
DisWin Ver 0.11 (DOS Generic)
Windows 実行ファイル用の逆アセンブラ(プログラム自体は DOS アプリです)。 API 呼び出し命令はエントリ名まで解析してくれるので、非常に便利です。 (ベクターデザイン発行 PACK シリーズ収録)
デバッガ及びリソースエディタ
Borland C++ 5.01

如何にコードを埋め込むか?

API 呼び出しのパラメータを変更する場合、該当する値を直接書き変えられれ ば良いのですが、そうもいかない場合があります(レジスタに格納された値を パラメータとしてプッシュする場合等)。その場合は、プログラムコー ド自体を書き変える必要があるのですが、この時コード全体の大きさを増減させる ことはできません。また、ジャンプやコール命令の飛び先が狂うようなコード の変更も出来ません(該当するジャンプやコール命令の飛び先を書きかえてや ればこの問題は回避できますが、かなり大変な作業となります)。

この制限は特に CreateFontIndirect 呼び出しによるフォント呼び出しの際に 問題となります。LOGFONT 型のデータに直接値を代入しているようなコードで したら問題は少ないのですが、通常予め LOGFONT 型のデータが格納されてい るところに、変更が必要なパラメータのみ値を変更するというような使われ方 をされる場合が多いためです(例えば同一ウィンドウ中で強調部分のみボール ド体を使う場合、現在のデバイスコンテキストからフォントの情報を読み 出し、fwWeit パラメータのみ書き変えて新たに論理フォントを生成する、と いった使われ方が一般的なようです)。

このような場合以下のような対応策が考えられます。

同一の働きをする命令長の短いコードに置き換えを行い、空いた部分にコー ドを埋め込む。

これは、コンパイラの最適化が不十分であることに期待するものです。 例えば API 呼び出しはパラメータをスタックにプッシュするので
6820000000   push    00000020
6a20         push    +20
は等価であることが利用できます。

パラメータのデフォルト値 0 を利用する。

CreateFont API は 0 をデフォルト値として取るパラメータ が多いことを利用して、
6a00             push   +00
6a00             push   +00
6a00             push   +00
というコードがあった場合
33c0             xor    eax,eax
50               push   eax
50               push   eax
50               push   eax
と書き換えることができます。この場合1バイト稼いだだけですが、 lfOutPrecision, lfClipPrecision, lfQuality, lfPitchAndFamily などのパ ラメータはデフォルトの 0 でもそれほど支障がない場合も多いので、細かい 設定を諦め、これらの値も 0 とすることにより多くの空きを作ることができ ます(もっとも CreateFont での日本語表示のためには 6a80 (push -80) とい う2バイトのデータを埋めこめればいいので、ここまでする必要はほとんどあ りません。)

いっそのことパラメータの設定を諦めてしまう(CreateFontIndirect)

CreateFontIndirect 呼び出しが行われている場合はレジスタの値を直接 LOGFONT 型の要素にコピーしたり、別の変数の値をコピーしていることがあり ます(その場合、CreateFontIndirect の前に GetTextMetrics などで現在のフォ ントのパラメータを読み出している場合が多い)。 そして、上のデフォルト値を使うテクニックを用いても書き換えのためのコー ドに余裕がない場合があります。

このような場合は、lfItalic, lfUnderLine, lfStrikeOut といったパラメー タは、TRUE, FALSE の2値を取ることを考慮して、これらのパラメータに値 を代入しているコードを削除し、そこに lfCharSet パラメータへの代入のコー ドを書き込むという手法があります。この場合、これらのパラメータは不定と なりますが、最悪でも本来イタリックであるべきフォントがイタリックでなく なるといった程度で、プログラムによっては十分許容できる程度の問題ですみ ます。

Resource Workshop を用いずにダイアログのデフォルトフォントを書き換 える手法


この手法はやってみたらたまたま上手く行ったというレベルであり、十分な確 認作業を行っていません。

ダイアログのデフォルト設定のフォントは、デフォルト設定のフォント以下の 長さを持つフォントであれば、書き換えることができます。 例えば MS Sans Serif -> FixedSys は可。Arial -> FixedSys は不可、とい う具合になります。 ダイアログのデフォルトフォントはバイナリ中に以下のような形で格納されて いるようです。

xx xx xx xx xx xx xx xx xx xx xx xx xx xx 4e 00 ..............N.
6f 00 74 00 65 00 20 00 65 00 64 00 69 00 74 00 o.t.e. .e.d.i.t.
6f 00 72 00 00 00 08 00 4d 00 53 00 20 00 53 00 o.r.....M.S. .S.
61 00 63 00 73 00 20 00 53 00 65 00 72 00 69 00 a.n.s. .S.e.r.i.
66 00 00 00 xx xx xx xx xx xx xx xx xx xx xx xx f...............
この例では、"Note Editor" という名前のダイアログに 8 ポイントの "MS Sans Serif" が設定されています。各データ/文字は2バイトを1単位とし て格納されているようです。 これを FixedSys に書き換えるには、
xx xx xx xx xx xx xx xx xx xx xx xx xx xx 4e 00 ..............N.
6f 00 74 00 65 00 20 00 45 00 64 00 69 00 74 00 o.t.e. .e.d.i.t.
6f 00 72 00 00 00 08 00 46 00 69 00 78 00 65 00 o.r.....F.i.x.e.
64 00 53 00 79 00 73 00 20 00 20 00 20 00 20 00 d.S.y.s. . . . .
20 00 00 00 xx xx xx xx xx xx xx xx xx xx xx xx  ...............
としてやれば良いようです(フォント名の後にスペース(0x0020)を挿入し、 文字列長を変更しないようにするのがポイントです)。
[岩間のホームページ]
ご意見、ご感想などはお気軽にこちらまでお寄せ下さい。皆さまからのフィー ドバックをお待ちしております。
naoz-i@tt.rim.or.jp