ホーム ざれごと ワシントン州 ツール NT豆知識 Win32プログラミングノート 私的用語 ジョーク いろいろ ゲーム雑記 Favorites 掲示板 Mail
Win32・MFCの多言語対応は、現実的にはリソースに始まり、リソースに終わるのでしょう。 と根拠もなく決め付けたところで、リソースによる多言語化を調べてみます。
一番手っ取り早そうなのは、Visual C++のIDEを使って、リソースに言語属性を指定してやることでしょう。 一つのリソースファイルの中に、同じリソースIDで言語属性の違う複数のリソースを持つことが出来ます。 kernel32のリソースロードルーチンは、様々な判断に基づいて最適と思われるリソースをロードします。 DialogBoxなどのUSER APIも、kernel32のリソースルーチンを使うので自動的に言語アウェアになる……はずなんですが……。
一見良さげなのですが、この方法はちょっといまいちなのです。 何が問題かというと、Windowsのフレーバー・バージョンによって、リソースを見つける優先順位が異なるわけなんです。 なんでそんなことすんねん!と怒りがNLS方面の人にこみ上げてくる今日この頃。 あまりにルールがややこしいため、私はよく覚えていません。 同じNT系でも、NT4とWindows 2000では優先順位が変わっていて、結局kernel32に頼る限り、自分の意図した結果が得られることはなくなってしまった、といっても過言ではないでしょう。 彼らも「前のOSはダメダメ。俺の案が一番だ!」とかほざきながらいろいろ変更してきたんでしょうが、これほどOSのバージョンごとに挙動が違うのでは、話になりません。 結果として、リソースをkernel32 APIを使って多言語対応させるというのはほぼ不可能に近いと思われ、多言語対応の改良(?)のためAPIが使い物にならなくなるというあきれた事態になってしまったわけです。
例えば、Windows 2000では、従来の様々なロケール(スレッドロケールやシステムロケールなど)に加え、UIロケールというものをこしらえ、それがリソースのロードに最優先にきいてくる、という変更を行いました。 このUIロケールというものは、インストール時に決まってしまい、あとでプログラムごとに変更するなどということは出来ません。 少なくとも調べた範囲では。 この結果、英語版Windowsのシステムロケールを変更しても、スレッドロケールをプログラムから明示的に変更しても、リソースのロードの順位に影響を与えることは全く出来ないのです。 NT4でちゃんと動いていた多言語対応アプリケーションの努力は、全く水の泡になってしまったのです。
というわけで、単一のリソースファイル中における多言語属性指定のノウ・ハウなど私には書けません。無理です。
というわけで、これだけぐちゃぐちゃにしてしまった挙句、彼らの持ち出した解決策というのは、リソース専用DLLを作って、必要に応じてダイナミックにロードしろ、というものでした。 なんじゃそら。
まぁいいでしょう。それで行くしかないのなら、それで行きましょう。 MFCでは、AfxSetResourceHandle()というグローバル関数があり、リソースを読んでくるDLLまたはEXEのハンドルを教えてやることが出来ます。 これはこれでいいのですが、一部のリソースだけ多言語化したい、というときに困ってしまいます。 というのは、リソースがAfxSetResourceHandle()で指定したDLLからだけしか読まれなくなり、もとのEXEのリソースは全く無視されてしまうからです。
個人でちょこちょことプログラムを作っているとき、UIは開発が進むにつれ変化するものです。 また、一気に全てのリソースをローカライズするパワーも気力も時間もなかったりします。 なので、UIが確定したところからちまちまと多言語化していきたいところなのですが、AfxSetResourceHandle()を使う方法では、そうはいきません。
MFCを使ったDLLは、その性質から3種類あります。
なんでMFC拡張DLLなんぞがここで出てきたかというと、MFCがリソースの管理をしてくれる、という一点にあります。 自分でCDialogを継承したクラスを作り、FindResourceEx()やらを駆使して期待されるリソースを探すというコードを書いても良さげですが、 全てのリソースタイプについてやりたくもないし、MFCが内部で利用するリソースまでハック出来るわけではないし。 MFC内部にそこらへんの仕掛けがあるのなら、そこをハックすれば、リソースDLLに見つからないリソースはEXEから探す、という動作をMFCにやってもらえるかもしれません。
先に結論を言っちゃいます。
これで、うまくMFCをだまして、リソースを探すリンクリストに、目的のDLLをマップすることが出来ます。 InitInstance()あたりでやればいいでしょう。 リソースを使いたいモジュール、例えばEXEでやるのがミソです。 EXEのモジュールステートに、リソース専用DLLをリンクしてやる必要があるので。
DLL側ではMFCのコードを一切使っていないので、無用にでかいMFC拡張DLLにする必要がなくなります。 別のページに書いてみた、CRTも使わないごく小さなDLLを作ることが出来ます。 手元のコードでは、MFC拡張DLL にした場合 40KB を軽く越えていたのを、5KB 以下に収めることが出来ました。 ページサイズを考えると、.text と .rsrc でこのサイズはまぁ妥当ではないかと思います。
サンプルコード
HINSTANCE hinst = LoadLibrary(_T("rese3.dll")); ASSERT(hinst); static AFX_EXTENSION_MODULE resdll = { NULL, NULL }; VERIFY(AfxInitExtensionModule(resdll, hinst)); new CDynLinkLibrary(resdll);
リソースDLL が複数あれば、上記のコードを DLL の数だけ繰り返してやります。 後で登録したほうが優先順位が高くなります。 これで、リソースの検索順序が、
EXE → リソースDLLn → … → リソースDLL0 → MFCxx.DLL
となります。 EXE の優先順位が一番高いことに注意。 リソースDLL に入れたリソースは、EXE から削除しないと期待した結果になりませんので、念のため。
このハックはつい最近試しただけなので、見つけてない不具合があるかもしれません。 一応気をつけてください。
最初に試してみたのは、AppWizardからMFC拡張DLLを作り、EXEからそのリソースを参照することでした。
が、全くうまく行きません。
MFCでリソースを探すときには、AfxFindResourceHandle() が呼ばれます。
非DLL版のMFCでは単なるマクロで AfxGetResourceHandle() に define されているので意味がありませんが、DLL版の MFC では、モジュールステートに登録された拡張DLLを順番に探すようなコードが入っています。
うまく行きそうに思えたのですが、モジュールステートに罠がありました。
MFC拡張DLLのDllMain()では、次のようにしてモジュールステートに自DLLを登録しています。
static AFX_EXTENSION_MODULE TscalresjDLL = { NULL, NULL }; extern "C" int APIENTRY DllMain(HINSTANCE hInstance, DWORD dwReason, LPVOID lpReserved) { // Remove this if you use lpReserved UNREFERENCED_PARAMETER(lpReserved); if (dwReason == DLL_PROCESS_ATTACH) { TRACE0("TSCALRESJ.DLL Initializing!\n"); // Extension DLL one-time initialization if (!AfxInitExtensionModule(TscalresjDLL, hInstance)) return 0; // Insert this DLL into the resource chain // NOTE: If this Extension DLL is being implicitly linked to by // an MFC Regular DLL (such as an ActiveX Control) // instead of an MFC application, then you will want to // remove this line from DllMain and put it in a separate // function exported from this Extension DLL. The Regular DLL // that uses this Extension DLL should then explicitly call that // function to initialize this Extension DLL. Otherwise, // the CDynLinkLibrary object will not be attached to the // Regular DLL's resource chain, and serious problems will // result. new CDynLinkLibrary(TscalresjDLL); } ...
AfxInitExntentionModule() で、モジュール固有の AFX_EXTENSION_MODULE を初期化し、それを使って CDynLinkLibrary のインスタンスを作っているわけです。 CDynLinkLibrary のコンストラクタが、モジュールステートのDLLリストに自DLLを登録してくれるという次第。
陥穽は、このときに使われるモジュールステートが、DLL 固有のものであるところにありました。 EXE のものでもなく、MFCxx.DLL のものでもなく、自分で作った MFC拡張DLL のモジュールステートです。 つまり、この拡張DLL からリソースを参照するときには問題ないのですが、他のモジュールから参照しようとしても、モジュールステートが異なるので意味がないわけです。
上記のコードのコメントにもちょっと触れられているようですが、非常に分かりにくい。 少なくとも、私がコードを読んだ限りでは、CDynLinkLibrary のインスタンスを作るコンテクストが問題なんであって、new CDynLinkLibrary を行う関数をエクスポートしたところで意味はないと思います。 エクスポートすべきは AFX_EXTENSION_MODULE であって、呼び出し側で CDynLinkLibrary を new する必要があると思うのですが。
もちろん、この機構は EXE と 拡張DLL でリソースIDのバッティングを気にしなくても良くなるなど、もともとの目的には適うものです。 リソース専用DLL を楽に作ろうという、よこしまな目的にあたわないだけです。
というわけで、上にリストとして挙げたハックは、DllMain() に書かれているコードを、EXE のコンテクストで実行して、EXE のモジュールハンドルに登録してやる、ということを行っているのです。
完全な多言語化ではなく英語 + ローカライズ一言語ということであれば、言語ニュートラルなリソースを使って可能です。
ホーム ざれごと ワシントン州 ツール NT豆知識 Win32プログラミングノート 私的用語 ジョーク いろいろ ゲーム雑記 Favorites 掲示板 Mail