syghの新フラグメント置き場

プログラミングTipsやコード断片の保管場所です。お絵描きもときどき載せます。

MFCのCFileDialog::SetDefExt()の引数の型について

(これは2010-06-05に書いた故OCNブログの記事を移植したものです)

バグの発見、MS Connect への報告と顛末

Visual Studio 2008/2010 の MFC 9.0/10.0 では、CFileDialog::SetDefExt() の引数のが間違っていて、本来 LPCTSTR (LPCWSTR for UNICODE / LPCSTR for MBCS) とすべきところを LPCSTR (MBCS) としてしまっています。
OS が Vista 以降でなおかつ Vista スタイルのファイル ダイアログの場合は、関数内部で LPCSTR 引数を CStringW 変数で受け取って変換しているんですが、OS が XP 以前もしくは非 Vista スタイルのレガシーダイアログの場合は、受け取った LPCSTR ポインタをそのまま SendMessage() に渡しています。
したがって、プロジェクト設定で Unicode 文字セットを使用するようにしている場合、SendMessage() は SendMessageA() ではなく SendMessageW() に置き換わり、非 Vista スタイルで素直に LPCSTR を渡してしまうと文字化けが発生します。
逆に Vista スタイルの場合、LPCWSTR を LPCSTR に強制キャストして渡してしまうと、CStringW の LPCSTR を受け取るコンストラクタが誤認するので、文字化けが発生します。
実際には、単に afxdlgs.h および dlgfile.cpp の関数インターフェイスが間違っているだけなので、非 Vista スタイルであれば LPCTSTR を無理やり LPCSTR に再解釈キャストしてから渡してしまえば(つまりコンパイラをだましてしまえば)一応正常に動作するんですが、Vista スタイルを使う場合は絶対に SetDefExt() に LPCWSTR を使うことはできません。

昔(2010年頃)、下記の URL で MS Connect 宛てに問題を報告したんですが、Connect の日本語版が米本国のサイトに統合されたときに消されてしまったようです(バグトラッキングを消去するなんて最低の行為ですね……)。確かそのときのタイトルは「MFCのCFileDialog::SetDefExt()の引数の型が不適切」というものでした。

当時 VS 2010 がまだベータの頃、VS 2008 で問題を発見して報告したんですが、MS からの回答は下記のとおりでした。

"To clarify, most likely this issue will be fixed in the next major release, e.g. Visual Studio 2012."

なん……だと

要するに、「例えば」VS 2012で「たぶん」修正されるというだけの回答でした。

その後のバグ修正状況と回避策

Visual Studio 2012 の MFC 11.0 で、確かに件の問題点は修正されていることを確認しました。MSDNにも記載があります。

ただしプロジェクト ラウンドトリップ機能を使うなどして MFC 9.0/10.0 と 11.0 以降をいったりきたりする可能性がある場合、SetDefExt() を呼び出す箇所では下記のような対処をしておく必要がありそうです。MFC 8.0 以前はどうなっていたのか調べていないので未対応です*1

#if (0x0900 <= _MFC_VER) && (_MFC_VER < 0x0B00)
#pragma message("MFC 9.0 / 10.0 CFileDialog::SetDefExt()")
if (m_bVistaStyle)
{
    this->SetDefExt(CStringA(strExt));
}
else
{
    this->SetDefExt(reinterpret_cast<LPCSTR>(static_cast<LPCTSTR>(strExt)));
}
#else
this->SetDefExt(strExt);
#endif

サンプル

かつて MS Connect にアップロードしたサンプルコードを、とりあえず VS 2008/2010/2012/2013 でビルド・比較できるようにしたものを下記にさらしておきます。
「ファイルの種類を変更すると、入力したファイル名の拡張子部分を自動的に更新するファイル ダイアログ」とか作るとき、とりあえずの回避方法として使ってやってください。

ちなみに、CFileDialog::SetControlText()は非Vistaスタイルのレガシーダイアログ専用機能であり、Vistaスタイルのダイアログではうまく動作しない模様です。ただしファイル名入力欄のテキストボックス文字列を設定する場合に関しては、代わりにIFileDialog::SetFileName()を使えばいいらしいです。実はVistaダイアログはCOMコンポーネントになっていて、VS 2008以降のCFileDialogはコンストラクタの引数フラグによってWin32 APIベースの旧ダイアログとCOMベースの新ダイアログを切り替えるラッパーになっています。だったらVistaスタイルのダイアログでもSetControlText()が動作するようにうまくラップしてくれよと言いたいところですが……

バグを見つけたら報告しましょう

MS Connect はいろいろ問題点もありますが、真面目に報告すればちゃんとリプライが来ますし、MSが修正するに足るバグだと判断すれば次期バージョンで修正してくれます。Visual Studio .NET Framework をより良くしたいという志があるのであれば、「いつか誰かが報告/修正してくれるだろう」という楽観的な態度でただ静観しているのではなく、積極的にバグ報告して改善に貢献したほうがよいです。ただし Connect では基本的に英語でコミュニケーションをとることになるので覚悟しましょう。日本のオペレーターが、こちらと米本国チームとのやりとりを翻訳して仲介してくれることもありますが、テクニカルライティングを知らないせいか翻訳がグダグダなことが多いので、英語で直接やりとりしたほうが手っ取り早いです*2

例えば下記の問題点は Visual C++ 2012 Update 4 で見つけたものですが、VC 2013 Update 2 ではすでに修正されていました。

なおVisual Studio 2013ではCommunityエディションが追加され、個人開発者や教育機関などであればATL/MFCが無償で利用できるようになりました。すでにレガシーAPIとなっているATL/MFCによる開発案件というのは今後積極的に増えることはないと思いますが、無償化によってATL/MFCのバグ報告が活発になり、品質が向上することを期待したいものです。

2015-11-06追記:
Windows 10には、非Vistaスタイルのレガシーダイアログがまともにリサイズできないデグレードがあるようです。レガシーダイアログを使っているアプリはまだ存在するし、古くてメンテナンスされていないクローズドソースのアプリだとお手上げなので、MSはデグレードを認めてさっさと直すべきでしょう。Windows 10はデグレードのひどい極めてお粗末なOSです。

*1:昔学生の頃に買った VS 2003、VS 2005 のインストール ディスクは一応持っているので調べようと思えば調べられるんですが、今更調べても需要がないのでやりません。

*2:そもそも英語のリーディング・ライティングスキルがないのに、まともなプログラミングなんてできるはずがありませんが……