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

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

VS 2008のWPF-MFC相互運用

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

以下を参考に、MFC アプリから WPF を使おうとして、Visual Studio 2008 SP1 のバグに遭遇しました。
WPF ユーザー コントロールを HwndSource 経由でホストする Win32 アプリ……といいつつ、結局 C++/CLI による中継が必要になりますのでご注意。
なお、VS 2010 では C++/CLI のインテリセンスが効かないという致命的なダウングレードがあるため、MFC-WPF の相互運用をするならば、VS 2008/2012/2013 を使うことをお勧めします。

バグ現象

スクリーンショット (a) は、WPF ユーザー コントロール プロジェクトと、それを使う予定の MFC クライアント アプリ プロジェクトを格納したソリューション。

(a)
https://sygh-jp.github.io/content_hosting/my_program_ss/Wpf_Designer_Error_ss_2010_05_31a.png

一見何の変哲も無いが、ここで C# コードのタブ (.xaml.cs) へ移動し、WPF のプロジェクトをリビルドする。そして、XAML デザイナのタブ (.xaml) へ再度移動する。
すると、スクリーンショット (b) のように再読み込みを促す情報バーが出るので、クリックすると、スクリーンショット (c) のように IDE がクラッシュする。

(b)
https://sygh-jp.github.io/content_hosting/my_program_ss/Wpf_Designer_Error_ss_2010_05_31b.png

(c)
https://sygh-jp.github.io/content_hosting/my_program_ss/Wpf_Designer_Error_ss_2010_05_31c.png

再度ソリューションを開くと、スクリーンショット (d) のエラーが待っている。

「オブジェクト参照がオブジェクト インスタンスに設定されていません。」

なん……だと……

いわゆる NullReferenceException のハンドルされない例外が発生している。よくあるアプリケーションが落ちる原因のひとつだが、こいつが WPF デザイナー(Visual Studio IDE プロセス)内部で発生している。

(d)
https://sygh-jp.github.io/content_hosting/my_program_ss/Wpf_Designer_Error_ss_2010_05_31d.png

なんぞこれ……ということで調べてみたところ、MS のサポートページにひっそりと修正プログラムが公開されていた。

上記によると、Win32 プロジェクトがスタートアップ プロジェクトとして設定されているとバグるらしい(ソリューション エクスプローラーにおいてプロジェクト名がボールド体になっているものがスタートアップ)。
確かにスクリーンショット (e) のように、WPF のほうをスタートアップ プロジェクトに設定して、リビルドすると先ほどのエラーは発生しない。

(e)
https://sygh-jp.github.io/content_hosting/my_program_ss/Wpf_Designer_Error_ss_2010_05_31e.png

ともかく、修正プログラム 963035 を適用せよ、とのこと。下記へ飛ぶ。

修正プログラムは下記のサイトにアップロードされている、とのこと。なんというたらいまわし

このサイトの Downloads タブに目的の修正プログラム「VS90SP1-KB963035-x86.exe」のダウンロード リンクがある。
プログラム適用によって問題が一応修正されていることを確認。
開発者にとっては非常に重要な修正プログラムですが、Windows Update 経由では適用されません。また、2014年現在、修正プログラムへのリンクが切れているようです。代替の修正プログラムが公開されているのかどうかは不明。

しかし Win32 アプリをスタートアップにするとバグるっていう理由が意味不明です。WPF との相互運用を図るとき、普通にやる組み合わせだと思うんですが……最低限そのくらいはテストしてからリリースして欲しいです。

余談:Win32 アプリから WPF を使う3つの方法

  1. /clr を使って C++/CLI でホストコードを書く。
  2. /clr を使って C++/CLI でホストコードを書く。ただし C++/CLI のコードは DLL に押し込む。
  3. WPF アセンブリを COM 公開する。

1番目は自明なので説明は不要ですね。

2番目の方法を具体的に説明すると、/clr を有効にした Win32 DLL、MFC 拡張 DLL もしくは MFC 標準 DLL 内で WPF を使用して、ネイティブ MFC インターフェイスもしくは C 言語形式関数を公開する形にしておけば OK。例えば、ネイティブの HWND ウィンドウ ハンドルを受け取って WPF コントロールのホストとして使う C 言語形式関数を C++/CLI で実装して、WPF コントロールインスタンスを DLL 内で gcnew するようにラップしてしまえば、ネイティブ C++ で書かれた EXE(厳密に言うと、/clr を無効にした EXE)から普通に呼び出せます。ただし /clr を使う場合は CRT/MFC/ATL を静的リンクではなく動的リンクする必要があるので注意しましょう。

1番目および2番目の方法において、もし混合クラスのメンバーとしてマネージ オブジェクトを保持したい場合は、ラッパークラス テンプレートの gcroot を使います。

ちなみに C++/CLI だと、ネイティブ/マネージのうちどちらか片方のインターフェイスだけでなく、両方のインターフェイスを合わせ持つ混合アセンブリ(COM でいうデュアル I/F に近い)まで作れてしまいます。つまり、ネイティブ Win32/MFC 向けの関数/クラスと、C# or VB.NET 向けのクラス両方をひとつのアセンブリに含めるという変態的なことができてしまう。

C++/CLI を一切使いたくない or 使えない場合、3番目の方法として、WPF アセンブリを COM コンポーネントとして公開する方法があります。COM 公開するには、regasm でレジストリ登録する方法のほか、マニフェストで Side-by-Side アセンブリにする方法があるようです。

ATL で開発した従来の COM コンポーネント DLL に関しては、マニフェストで Side-by-Side アセンブリ化(プライベート アセンブリ化)し、実運用にも十分耐えうることを確認したことがあるんですが、WPF アセンブリに関しては未検証です。

もし純粋なネイティブ Win32 アプリ(従来のネイティブ MFCVB 6.0 などで開発したアプリ)から直接 WPF アセンブリを使いたい場合は、2番目もしくは3番目の方法を使う必要があります。ただし、境界面のやりとりに手間や労力がかかることは覚悟しておく必要があります。

CButtonとCMFCButtonの比較

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

Visual Studio 2008 SP1にて導入された、MFC Feature Pack付属のCMFCButtonは、従来のCButtonに比べてかなり強力です。微妙に使い勝手が違う部分もありますが、ボタンの文字色変更とか、画像とテキスト両方表示したいときとかに、わざわざオーナードローなんかしたくないって人は重宝するんじゃないかと。
とはいえ、.NETのWindows FormsやWPFに比べたら、それでも面倒なことには変わりないです。もしどうしてもMFCを使わないといけない理由がない限り、今後はWPFを使うべきでしょう。

とりあえずサンプルを下記に置いておきます。

CButton、CMFCButtonともに、XPとVista/7、そしてビジュアル スタイルとクラシック スタイルで外見が大きく異なるボタンがあることに注意。違いを生み出しているのはコモンコントロールのバージョン(クラシックはVer.5.x、XPのビジュアルはVer.6.0、Vista/7のビジュアルはVer.6.1)ですが、このあたりはGUI開発者にとってかなり悩ましいところです。BS_BITMAPを適用して(BitmapプロパティがTrue)、WS_EX_STATICEDGEを適用して(Static EdgeプロパティがTrue)、CButton::SetBitmap()でビットマップを割り当てたボタンに至っては、なんかもう別物(;´д`)っていうか相当残念な感じになってます。

あと、以前から気になっていたんですが、チェックボックスラジオボタンをプッシュボタン形式にしてビジュアル スタイルを適用すると、かなり表示がキモいです。プッシュボタン形式というのは、BS_PUSHLIKEが適用された状態(Push LikeプロパティがTrue)のことです。Windowsのボタンはマウスでクリックしたまま離さないでいると、押し込んでいる状態を表す感じで外観が変わりますが、あのステートを使ってチェック状態を表してるつもりの変なヤツです。トグルボタンとも呼ばれます。
さらに、ビジュアル スタイルでチェックON状態のプッシュボタン形式チェックボックスにマウスオーバーすると、キモさ倍増です。自分はWindowsのクラシック スタイルが大嫌いなので、XP/Vistaでも常にビジュアル スタイルを適用していますが、いくらなんでもこれだけはクラシック スタイルのほうがON/OFFの状態が分かりやすい。サンプルではフォントを変えたり色を変えたりして、少しでも分かりやすくしようと努めてますけど、無駄な努力っていうか見苦しいだけ。MSはXPやVistaのリリース前に、一般ユーザーにビジュアル スタイルのUIをテストしてもらわなかったんでしょうか? コンシューマーゲームとかだったら、UIとか操作性は必ず一般ユーザーの反応をチェックすると思うんですが。

以下でもXP Visual Styleのトグルボタンにてホバーしたときの外観の問題点について述べられています。ちなみにtable内のspan要素のborderスタイルのoutset/insetによってunselected/selectedの外観が比較されていますが、Firefoxでは正しく表示されないようです。
devblogs.microsoft.com

なので、自分はプッシュボタン形式のチェックボックスラジオボタンはお勧めしません。「どうしても昔ながらのプッシュボタンがいいんだよ!」って方は、SetWindowTheme() APIで意図的にビジュアル スタイルを切る、という方法もありますが……みんなが慣れているであろう普通のチェックボックスラジオボタンにしておいたほうがいいと思います。

ちなみにCMFCButtonのフォントはデフォルトで必ずMS UI Gothicになるようです。親ダイアログのフォントをメイリオとかMeiryo UIとかにしても連動してくれません。親ダイアログのフォントに合わせたかったら、OnInitDialog()とかでSetFont()を呼んで明示的に設定してやる必要があります。

なお、CMFCButton::EnableWindowsTheming()は再描画メッセージを発行しません。なので、呼び出したあとはクライアント全体あるいはアプリのフレーム全体をInvalidateして再描画する必要があります。それにしても、なんでstaticメソッドなんでしょうか……個別のコントロールごとに呼べません。CMFCButtonの実装を見る限り、個別設定するならばpublicメンバー変数(orz*1)のm_bDontUseWinXPTheme(orz*2)にむりやりTRUEを設定すればいいみたいです。

スクリーンショット1(XP、ビジュアル スタイル)
https://sygh-jp.github.io/content_hosting/my_program_ss/mfc_btn_test_ss_xp01.png

スクリーンショット2(XP、クラシック スタイル)
https://sygh-jp.github.io/content_hosting/my_program_ss/mfc_btn_test_ss_xp02.png

スクリーンショット3(Vista/7、ビジュアル スタイル)
https://sygh-jp.github.io/content_hosting/my_program_ss/mfc_btn_test_ss_seven01.png

スクリーンショット4(Vista/7、クラシック スタイル)
https://sygh-jp.github.io/content_hosting/my_program_ss/mfc_btn_test_ss_seven02.png

*1:残念ながらもう慣れましたが、MFCカプセル化の設計が根本的におかしいです。フィールドをpublic/protectedで公開するとか平気でやらかしてくれていますが、どこの素人プログラマーですか……

*2:否定形の名前を変数名に付けるなと言いたいです。m_disablesVisualStyleもしくはm_usesClassicStyleにするべきでしょう。

CScrollViewの代わりにCViewでスクロール

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

MFCのCScrollViewは便利なんですが、画像編集ソフトのようなものを作ろうとしたとき、いかんせん細かい制御がしづらいというか、CScrollView::SetScrollSizes()などのユーティリティ メンバー関数を呼び出すタイミングが微妙というか、デバイス座標系と論理座標系が入り乱れて混乱するというか、とにかくスクロールは自前でやったほうがかえって楽だったりします。実はそんなに手間じゃありません。

サンプル:
MfcScrlVwTest.zip
(VS 2010 SP1でビルド、Windows XPと7で動作確認済み)

(left, top, right, bottom) = (-4,000, +4,000, +4,000, -4,000)の論理座標空間を、クライアント領域にマッピングしています。また、Y軸はいわゆる下が正となる2D座標系ではなく、座標変換を使って上が正となるように設定しています。ビューポートをいろいろ設定するだけで、拡大縮小・平行移動とスクロール バーの連動が簡単に実装できるんですが、GDIのビューポートは、OpenGLDirect3Dのそれとは毛色がちょっと違うので最初戸惑いました。まぁいまさらGDIっていうのも時代錯誤な感じですが……

で、サンプルのソースはほとんどMfcScrlVwTestView.h/cppしかいじってません。あとはフレームワークが自動生成したコードをビルドするだけで、わりと見栄えのいいアプリになるのはMFC Feature Packの良いところです。実装したのはスクロールの確認と簡単なダブル バッファリングだけで、完全な印刷処理は実装してないので、そのへんはご自由にどうぞ。

なお、ダブル バッファリングにおけるバック バッファ→フロント バッファの転送処理は、転送元と転送先ともにデバイス座標系に戻して行なったほうが分かりやすいです。

Direct2D

CViewでスクロール バーを扱うコツがつかめたら、グラフィックスのレンダリングには可能な限りGDIではなくDirect2Dを使うようにしましょう。Direct2Dは明示的なバック バッファの作成が不要で、EndDraw(もしくはDXGIスワップチェーンをPresentするタイミング)でフリップしてくれます。ちなみにDirect2DはGDIのデバイス コンテキストにもレンダリングできるようになっていますが、GDI/GDI+とは違って、Direct2Dでは直接印刷(プリンターのデバイス コンテキストに描画)できません。ID2D1RenderTarget::EndDraw()メソッドがエラーコードを返します。なので、印刷時のみGDI+で同じ描画処理を実行するようにするか、ビットマップに一旦描画してプリンターに転送する必要があります。ただし、Windows 8で追加されたDirect2D 1.1であれば印刷用のメタデータ出力もサポートしています。

ATL::CPathユーティリティ

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

Visual C++ 付属の ATL にはわりと便利なユーティリティ クラスがあるんですが、あまり知られてないのでひとつ紹介します。

ATL::CPath は Windows Shell API の Path*関数を薄くラップしたクラスなんですが、ATL::CString とともに Windows アプリでは重宝するかと。
atlpath.h をインクルードすれば、非 ATL/MFC プロジェクトであっても使えるようになります。

#include <atlpath.h>

void AtlPathTest()
{
    const LPCWSTR pAnswerStr[] = { L"No", L"Yes" };
    {
        const CPathW paths[] = {
            CPathW(L"C:\\test_file.txt"),
            CPathW(L"test_file.txt"),
            CPathW(L".\\test_file.txt"),
            CPathW(L"..\\test_file.txt"),
            CPathW(L"\\\\127.0.0.1\\shared\\test_file.txt"),
        };

        for (int i = 0; i < sizeof(paths) / sizeof(*paths); ++i)
        {
            wprintf(L"'%s' is relative path ? %s\n", static_cast<LPCWSTR>(paths[i]), pAnswerStr[paths[i].IsRelative()]);
        }
        // --> N, Y, Y, Y, N
        puts("");
    }

    {
        // '\\' の代わりに '/' を使うと、意図した結果にならない。
        const LPCWSTR srcPath = L"C:\\directory\\test_file.txt";
        CPathW path = srcPath;
        wprintf(L"RemoveFileSpec('%s') = \n", srcPath);
        path.RemoveFileSpec();
        wprintf(L"'%s'\n\n", static_cast<LPCWSTR>(path)); // 'C:\directory'
    }

    {
        const LPCTSTR srcPath = _T("C:\\directory\\test_file.txt");
        CPath path = srcPath;
        _tprintf(_T("StripPath('%s') = \n"), srcPath);
        path.StripPath();
        _tprintf(_T("'%s'\n\n"), static_cast<LPCTSTR>(path)); // 'test_file.txt'

        path = srcPath;
        _tprintf(_T("RemoveExtension('%s') = \n"), srcPath);
        path.RemoveExtension();
        _tprintf(_T("'%s'\n\n"), static_cast<LPCTSTR>(path)); // 'C:\directory\test_file'
    }

    {
        const LPCSTR path1 = "C:\\directory";
        const LPCSTR path2 = "test_file.txt";
        CPathA path = path1;
        path += path2;
        printf("'%s' + '%s' = \n", path1, path2);
        printf("'%s'\n\n", static_cast<LPCSTR>(path)); // 'C:\directory\test_file.txt'
    }
}

CString は C++ 標準の std::string や std::wstring に比べて、速度面やメモリ効率面においてかなり最適化されているし、ANSIUnicode 変換も簡単にできて、COM 用の _bstr_t との相互変換もサポートされているなど、VBDelphi の文字列に引けを取らない機能を持っているので、ATL/MFC 開発ではまず CString をいかに使いこなせるか、が重要になってきます。ちなみに MFC と ATL は有償ライブラリなので、無償版(Express エディション)の Visual C++ では使えません*1。また、Windows 以外の OS や Visual C++ 以外のコンパイラーへの移植性を考えると、標準 C/C++ ライブラリの使い方も習得しておいたほうがよいでしょう。なお、Unicode 版の CStringW に関しては Windows ストア アプリ開発でも使えますが、MBCS 版の CStringA はデスクトップ アプリ専用です。

あと実際のデバッグで役に立つ機能といえば、ATLTRACE() と ATLASSERT() マクロです。MFC には同等機能として TRACE() と ASSERT() というマクロが存在しますが、ATL 版は非 MFC プロジェクトでも使えるので便利です。ちなみに標準 C ライブラリの assert() マクロは、MSVC 実装ではアサーション失敗時のプログラム停止位置が DebugBreak() 呼び出しまでさかのぼってしまうため使いづらいので、可能であれば ATL 版や MFC 版のアサート、もしくは CRT 版の _ASSERTE() を使うと楽になります*2

*1:Visual Studio 2013 以降は小規模チームや個人開発に限り無償で利用可能な Community エディションが用意されていて、Community エディションでは Professional エディション以上と同様に ATL/MFC を使うことができます。

*2:Visual Studio 2013 では改善されていて、assert() マクロでもアサーションが失敗した位置で停止するようになっています。

WinSDKおよびATLのCOMユーティリティ

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

MSXMLなどのCOMタイプ ライブラリを#importしたときなど、COMを扱うときによく見かけるのが、Visual C++ CRTヘルパーの _bstr_t クラスと _variant_t クラス (comutil.h) です。それぞれ、COMのBSTR文字列のラッパーと、VARIANT型のラッパーとなっています。
ただしC++プログラマーとしてはこれらがグローバル名前空間で定義されてやがることに殺意を覚えます。シンボル名がアンダースコアで始まっているのは、処理系依存のデータ型であることを誇示しているのか……
あとアンダースコアなしのエイリアス bstr_tvariant_t がtypedefじゃなく#defineなのも情けないです。これらのエイリアスは使わないほうがよいでしょう。MSの#define好きは異常*1

似たようなラッパーはATL (atlcomcli.h) にも用意されていて、ATL::CComBSTRATL::CComVariantが該当するのですが、ついでにATL::CComSafeArray (atlsafe.h) なんかもあります。MFC専用だとCOleVariantというのもあります。

なお、_variant_tは、MFCライブラリでもたまに使われているのを見かけます(CMFCPropertyGridProperty::SetValue()/GetValue()とか)。

正直VC++ネイティブで文字列を扱う場合、他の環境への移植性を考慮しないで良いのであれば、迷わずCStringA/CStringW/CStringを使うのがベストです*2。そもそもテンプレート機能まである静的型付け言語C++で何が悲しくてVariantなんぞ使わないといけないのかとも思いますが、VBスクリプト言語との相互運用を考慮して設計されたCOMデュアル インターフェイスの境界では、たとえ非効率でもBSTRやVARIANT、SAFEARRAYを使わざるを得ません。で、COMのBSTRは::SysAllocString()/::SysFreeString()、SAFEARRAYは::SafeArrayCreate()/::SafeArrayDestroy()でヒープ管理されるので、こういうRAIIラッパーがないと発狂するでしょう。ただ、ラッパークラスはCRTアロケータを使っていない時点で速度が出ない(とくに文字列の結合が毎回再割り当てを伴うために遅いらしい)し、下手な使い方をするとメモリーリークの原因になるので、COMのRAIIラッパーを使用する場合はCOM境界のみに限定するべきです。ちなみにATL::CComBSTRはアタッチ・デタッチがやや特殊で、使い方を誤ると簡単にメモリーリークするらしいので、極力_bstr_tを使ったほうが無難らしいです。

実装を確認してみたところ、一応 CComBSTR::operator& にはアサーションが入っているので、デバッグビルドであれば誤った使い方をしたときに気付くはずです。

*1:Win32 APIでも、enumを使うべき所なのにやたら#define使っているのがかなりうっとうしいです。あと悪名高いmin/maxマクロも後世に語り継がれる大ポカでしょう。

*2:C++標準のstd::string/wstringより最適化されていて高機能です。Windowsの文字列リソースを直接読み込めるLoadString()メソッドも用意されています。

MFC-CLI 相互運用時の注意点‏

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

それなりに規模の大きい MFC プロジェクトなどで、共通言語ランタイムのサポート /clr を追加して .NET ハイブリッド アプリケーションを作るときに、起動時に EETypeLoadException 例外が発生して実行できない場合、コンパイル オプションに /GF(同一文字列の削除)を追加します。

VS 2008 IDE から設定する場合は、「構成プロパティ」→「C/C++」→「コード生成」→「文字列プール」を「はい」にします。
特にデバッグ ビルドで最適化を行なわない場合はこのオプションが OFF になっていることが多いので注意。ただし特定の最適化オプションを有効にすると自動で ON になることもあるらしいです。

今、仕事で関わっている案件では、これまでまともなビルドマスターやモジュール構成管理者がいなかったらしく、ひとつのEXEのプロジェクト ファイルですさまじい数のソースファイルがコンパイル&リンクされます。どうやらMFC拡張DLLの存在を知らなかったらしい(MFC拡張DLLを使えば簡単かつ効率的にMFCアプリを分割ビルドできるのに……)。

この状態で、すべてのソースを/clrコンパイルすると、ただでさえネイティブのみの場合でも長かったリンク時間がさらに長くなります。

多分、「x86 + MSIL」や「x64 + MSIL」の混在コードをリンクする処理に時間がかかっているみたいです。なので、すべて /clr を使ってコンパイルするのではなく、ネイティブのみでコンパイルできる部分は明示的に /clr なしで通常どおりコンパイルするようにしないと厳しいかもしれません。

なお、/clr ありと /clr なしとでは、プリコンパイル済みヘッダー(pch)を共有できないので、pchを作成するヘッダーとソース(通例stdafx.hとstdafx.cpp)を明示的に分ける必要があります。

例えば、

  • ネイティブ用に stdafx.h, stdafx.cpp, $(TargetName).pch
  • /clr 用に stdclr.h, stdclr.cpp, $(TargetName)_clr.pch

とか。

はっきり言ってめんどいことこの上なくて死にそうなので、もういっそネイティブのみを使うコードと/clrを使うコードを別々のプロジェクト(ネイティブEXEと混合DLL、もしくはネイティブDLLと混合EXE)に分離してしまったほうが良いです。

方法 : /clr に移行する

あと、C#はともかくC++/CLIをまともに使える開発者というのはかなり少ないので、MFCしか使えない人達には、

・/clrを使うホストEXE側のコードはいじらないで、ネイティブのMFC拡張DLLのコードだけいじるようにしてね(要するに筐体はいじらないで部品だけいじるようにしてね)

もしくは

・/clrを使うMFC拡張DLLにWPFラッパーのネイティブMFCインターフェイスのみを公開してあげるから、ホストとなるネイティブEXE側のコードだけいじるようにしてね(要するに部品はブラックボックスとして使ってね)

というルール作りも必要かと。そうでないと/clrオプションのもとにとんでもないコードを書き始める危険性があります。C#の素人にはunsafeを許可させないで制約を課したほうがいいのと似たようなものです(状況は逆だけど)。

ネイティブ・マネージ混在デバッグの注意点

VS 2008でx64のネイティブ・マネージ混在コードをデバッグするとき、EXE側の「構成プロパティ→デバッグ→デバッガのタイプ」を「混合」にしておくと、デバッグ実行時にブレークポイントが機能しなかったりステップインできなかったり、さらにASSERT(ATLASSERT)などによるアサートに引っかかったときプロセスが落ちる(アサート ダイアログで「再試行」とか「無視」を選択すると、本来続行可能な場面でも落ちる)現象が発生することがあるので、もしx64ネイティブ コードのほうをデバッグしたかったら「ネイティブ」に設定しておく必要があります。ただし「ネイティブ」に設定すると、今度はマネージ コードに対してブレークポイントが効かなくなったり、.NET側でIDE出力ウィンドウへのUnicode文字の出力ができなくなったりするので注意。これは結構面倒。x86のネイティブ・マネージ混在コードをデバッグ実行するときは「混合」でもアサートで落ちることはないのに……なので、64bit対応のMFC + .NETハイブリッド アプリケーションを作る場合は、まず32bit版で十分デバッグしてバグを取り除いておいたほうがいいです。64bit版でしかテスト・デバッグできないコードに関しては、面倒だがあきらめてデバッグ設定で回避するしかなさそう。なお、「デバッガのタイプ」のデフォルト設定「自動」は、EXEのプロジェクトがネイティブかそれとも/clrありの混合か、に左右されます。

ちなみにC#のコードからWin32のネイティブDLL関数をP/Invoke呼び出ししている場合に、デバッガでネイティブ関数内にステップインする場合、C#プロジェクト設定の「デバッグ」タブにある「アンマネージ コード デバッグを有効にする」にチェックを入れておきます。VC++もVC#も、こういったデバッグ時の設定に関しては、.vcprojとか.csprojといったプロジェクト ファイルではなく、.vcproj.<Windows Log-in User Name>.userとか.csproj.userとかのユーザー設定ファイルに書き込まれるので、開発者ごと(端末ごと)に逐一設定してやる必要があることに注意しましょう。

C++/CLI のインテリセンス

VC 2010ではC++/CLI言語のインテリセンスが動作しません(バグではなく仕様)。VC 2012ではちゃんと復活しています。なんで2010で一度死んだのかは不明ですが、C++0x対応やコンパイル前文法チェック機構の強化などを優先した関係上、C++のインテリセンスを再設計する必要があって、その余波でC++/CLI対応が後回しになったのかもしれません。ちなみにVC 2012では Windows ストア アプリ(WinRT)用に新たに C++/CX 拡張モードが追加されています。なお、C++/CX と C++/CLI は混在できません。

従来のWin32ネイティブ業務アプリをWPFに移行したいと考えたとき、全部をWPFに置き換えるのは現実的にいって不可能なので(WPF+C#自体は生産性は高いけどC#C++ほどきめ細かい制御ができないし、逆にネイティブC/C++GUIコード資産をWPFから使うにはあまりに制約が多い上に労多くして実りが少ない)、普通はHwndSourceとか使ってMFCアプリ内にWPFコントロールを埋め込む方法をとって徐々にUI部品などから.NETへ移行していく、またパフォーマンスが要求される部分だけC++実装を残しておいてC++/CLIでマネージインターフェイスを書く、という方法をとると思うんですが、肝心のC++/CLI言語に対するIDEサポートがおざなりだと致命的になります。C++/CLIは安易に使うと(ネイティブC++以上に)地獄を見るという、完全にマニア向けの言語ですが、MSにはもっとC++/CLIを使った相互運用のメリット・デメリットや具体的シナリオを説明するページを作成して欲しいです。

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