WideStudio Programming (4-3)
~unicode対応との絡み

Version1.0でのサブクラス化にどんな問題があったかというと、このやり方では文字化けを起こすということです。もちろん常に化ける訳ではありません。それならリリースする前にとっくに見つかっているはずです。

タンゴレンはutf-8をベースに記述されています。UNICODEマクロは定義しておらず、Windows APIを直接呼び出す時は、utf-16leに変換して~W系のAPIを呼び出します。

正真正銘のunicodeアプリケーションなのですが、Windows用語でunicodeアプリケーションというのは文字列をutf-16leで扱っているプログラムのことです。(今更ながらではありますが)驚いたことに、これはウィンドウプロシージャの作り方にまで影響を与えているのです。

きっかけは、unicodeにしか無い文字を含むファイル名が、ウィンドウタイトルで化けてしまうのに気付いたことでした。当初はWideStudio/MWTのバグではないかと疑いました。で、ソースを調べましたが、特に問題はありません。色々実験してみると、ウィンドウを生成した直後にWSNtitleStringにunicode固有文字を入れておくと、ちゃんと表示されることも分かりました。しかしながら、同じ文字列を単語帳を読み込んだタイミングでセットしても化けてしまいます。不思議なことに同じ文字列はメイン画面の中のタイトル表示用のラベルでは問題なく表示されています。

途中の何かの処理が原因で、メイン画面のタイトルバーだけが、正しく表示されなくなっているようです。なので、何を実行するとおかしくなるのか、ウィンドウタイトルに文字列を表示させるテスト用のコードの場所を少しずつ変えて、調べていきました。で、突き止めました。

おかしくなるのは、メインウィンドウをサブクラス化したタイミングでした。

サブクラス化して呼び出されたウィンドウプロシージャが何か悪さをしているのかと思いましたが、固有の処理を全部コメントにして元のウィンドウプロシージャをCallWindowProcするようにしても、現象は再現します。つまり、ウィンドウプロシージャを置き換えていること、サブクラス化している事自体が原因のようです。

そろそろ種明かしをしましょう。

このところ自分でメッセージループを書くようなことは滅多に無く、Visual Studioでプログラムを作ればUNICODEマクロが定義されているかどうかで使われるAPIが自動的に全部切り替わってしまうので意識したこともなかったのですが、実は、

Windowsのウィンドウプロシージャには、ansi対応のものとunicode対応のものの、二種類がある

のです。などともったいぶって書いちゃいましたけど、メッセージループがどうなっているかをよく考えれば自明なことでした。

ansi系なら、メッセージループは実際には SendMessageA / GetMessageA / DispatchMessageA という風に「~A系」のメッセージ処理APIを使って組み立てられますし、unicode系(utf-16le系)なら SendMessageW / GetMessageW / DispatchMessageW という風に「~W系」のAPIを使って組み立てられます。特定のウィンドウがどちらのメッセージループを持っているかはIsWindowUnicode() API にウィンドウハンドルを渡してやれば判定してくれます。詳しくは分かりませんが開発ツールの方で何かフラグでも持たせるようになっているのでしょう。

WideStudio/MWTライブラリはUNICODEマクロを定義してutf-8ベースでコンパイルされていますが、タンゴレンの本体のコンパイルではUNICODEマクロは定義していません。Windows APIを直接呼び出す所は殆どありませんし、必要な場合はAとWを必要に応じて明示的に書き分ける方針でやっていました。従って、A/Wを明示しないで使っているAPIはansiタイプとしてプリプロセスされています。

さて、ここでもう一つ迂闊だったのは、SetWindowLong と CallWindowProc にも、AタイプとWタイプがあったということです。

ウィンドウのサブクラス化(ウィンドウプロシージャの入れ替え)というのはたぶん、DispatchMessageで呼び出される処理の置き換えです。

ウィンドウプロシージャ自体はただの関数ですから、それへのポインタを見ただけではそのウィンドウプロシージャがansiタイプなのかunicodeタイプなのかは分かりません。しかしながらWindowsは、置き換えられる対象がansi(shift-jis)系のウィンドウプロシージャなのか、unicode(utf-16le)系のプロシージャなのかをSetWindowLongAとSetWindowLongWのどちらを使って置き換えられたかで判定しているらしいのです。で、ご親切なことに、対象のウィンドウがunicodeタイプの場合、SetWindowLongAで置き換えられたウィンドウプロシージャを呼び出す時は、ウィンドウメッセージ中に文字列パラメータがあった場合、事前にshift-jisに変換した上で渡してくれるみたいなのですね。

また、ansi系のウィンドウプロシージャからCallWindowProcAで(保存しておいた元の)unicode系のウィンドウプロシージャを呼び出した場合(こちらはたぶんウィンドウハンドルを見て判定しているのでしょう)、またまたご親切なことに、今度はshift-jisになっている文字列パラメータをutf-16leに再度変換して渡してくれるみたいです。これに関してはCallWindowProcの解説のページをよく読むと、下の方にちらっと書いてありました。

http://msdn.microsoft.com/ja-jp/library/cc410622.aspx
Windows NT/2000:CallWindowProc 関数は、unicode から ANSI への変換を行います。ウィンドウプロシージャを直接呼び出した場合は、このような変換は行われません。

 

(SetWindowLongのドキュメントの方にはウィンドウプロシージャの呼び出し時点で文字コードの変換が発生するというようなことは書いてありませんが、変換されていると考えなければ辻褄が合いません。)

少なくとも元々shift-jisの範囲の文字しか使っていないプログラムはこれで動作しますが、文字列の中にshift-jisでは表現できないコードがあった場合、この場合例えばSetWindowTextWでutf-16le文字列を渡していたとしても、サブクラス化されたウィンドウプロシージャの処理で一旦shift-jisを経由するために特定の文字でだけ文字化けを起こしてしまうわけです。

そうなっているものに文句を言っても仕方ないのですが、中途半端に動いてしまうものだから、この現象に気付くのが遅れてしまいました。悔しいですねえ。

さて、解決方法ですが、それ自体は難しくありません。

固有の処理で文字列を扱わないのであれば、置き換えるウィンドウプロシージャはansi系もunicode系も同じコードで平気です。置換対象のウィンドウがansi系なのかunicode系なのかを判定してSetWindowLongとCallWindowProcのA系/W系を揃えてやれば、無駄なコード変換も発生せず、問題なく動作します。具体的には、

    WNDPROC old_proc;
    if( IsWindowUnicode(hwnd) )
        old_proc = (WNDPROC)SetWindowLongW(hwnd,GWL_WNDPROC,(DWORD)new_wndproc);
    else
        old_proc = (WNDPROC)SetWindowLongA(hwnd,GWL_WNDPROC,(DWORD)new_wndproc);

としてウィンドウプロシージャを置き換え、

        if( IsWindowUnicode(hWnd) )
            lResult = CallWindowProcW(ow_it->second,hWnd,message,wParam,lParam);
        else
            lResult = CallWindowProcA(ow_it->second,hWnd,message,wParam,lParam);

として元のウィンドウプロシージャを呼び出すようにしてやれば、万事解決します。

より詳しいことが知りたい方は、 http://www.attocraft.jp/contents/archive/DragAndDrop_ver1.1.zip に修正版のソースを置いておきますので、よろしければご覧下さい。尚、この記事の執筆時点ではまだタンゴレンVersion1.1はまだリリースされていません。

p.s.
長々と書いてきましたが、今更感強いです。こんなの、知ってる人は知ってる話ですよねえ … 。

(続く)