none
SaveFileDialogをモードレスで表示したい RRS feed

  • 質問

  • いつもお世話になっております
    VS2008 でC++/CLIを使ってフォームアプリを作成しています

    SaveFileDialogクラスを使って名前を付けて保存ダイアログを出しています。

    ShowDialog()メソッドを使ってダイアログ表示すると、親ウィンドウが操作できなくなってしまいます

    親ウィンドウも操作できるようにしたいのですが、Show()メソッドがなく

    SaveFileDialogをモードレス表示する解決策をどなかた教えて頂けないでしょうか?

    2012年7月13日 2:26

回答

  • そう言う見方はした事がなかったので気づきませんでしたが、 .NET Frameworkサイドではなかったのですね。

    ちなみに、Windows APIの方だと

    GetSaveFileName・GetOpenFileName・OPENFILENAME 構造体

    辺りがキーワードで、(Win32API(C言語)編 第55章 ファイルを開く・保存のコモンダイアログなどが参考になるかな)OPENFILENAME 構造体のhwndOwnerメンバ を NULLにするとモードレスに、 ウインドウハンドルを指定するとモーダルになります。

    .NETのフォームからHWND取得するにはHandleプロパティで出来ます(これに関してピンとかの関係がどうだったか記憶が定かでないのでそこは必要であれば念のため調べてください。)が、モードレスならばNULLでいいということで、今回は無用な心配かも。

    • 編集済み mr.setup 2012年7月13日 5:31
    • 回答としてマーク BB-X LARISSA 2012年7月17日 4:23
    2012年7月13日 5:14
  • Windowsの世界では、モーダルウィンドウの表示は、表示関数(メソッド)を呼び出すとそのウィンドウを閉じるまで、呼び出しメソッドから制御の戻らないものを指します。

    わかりやすい例でいえば、メッセージボックスをAPIで表示する場合が一番わかりやすいでしょう。

    MessageBox APIは、最初のパラメータにオーナーウィンドウを指定します。ここをNULLにして呼び出しても、有効なウィンドウハンドルを指定して呼び出しても、メッセージボックスを閉じるまでAPIからは帰ってきません。

    このような表示形式をウィンドウズでは、モーダル表示と呼びます。

    それ以外のウィンドウ表示(CreateWindow でWS_VISIBLEをつけて呼び出すなど)は、すべてモードレス表示となります。ただし、Windows でモードレスという表現をするのは、慣例的にダイアログと呼ばれるタイプのウィンドウを表示する場合のみとなっている場合が大半です。


    わんくま同盟,Microsoft MVP for Visual C++(Oct 2005-) http://blogs.wankuma.com/tocchann/

    • 回答としてマーク BB-X LARISSA 2012年7月17日 4:23
    2012年7月13日 6:08

すべての返信

  • よくよくかんがえてみましたが、まさかこれって自分で同じようなフォームを作ってShowで表示するしかなさげでしょうか
    2012年7月13日 5:01
  • 名前を付けて保存・ファイル開くの両方共の、モードレスでの表示機能は持っていません。APIレベルでモードレスでの表示機能を用意していません。

    どうしてもモードレス表示を必須とするのであれば、自作することになります。


    わんくま同盟,Microsoft MVP for Visual C++(Oct 2005-) http://blogs.wankuma.com/tocchann/

    2012年7月13日 5:01
  • そう言う見方はした事がなかったので気づきませんでしたが、 .NET Frameworkサイドではなかったのですね。

    ちなみに、Windows APIの方だと

    GetSaveFileName・GetOpenFileName・OPENFILENAME 構造体

    辺りがキーワードで、(Win32API(C言語)編 第55章 ファイルを開く・保存のコモンダイアログなどが参考になるかな)OPENFILENAME 構造体のhwndOwnerメンバ を NULLにするとモードレスに、 ウインドウハンドルを指定するとモーダルになります。

    .NETのフォームからHWND取得するにはHandleプロパティで出来ます(これに関してピンとかの関係がどうだったか記憶が定かでないのでそこは必要であれば念のため調べてください。)が、モードレスならばNULLでいいということで、今回は無用な心配かも。

    • 編集済み mr.setup 2012年7月13日 5:31
    • 回答としてマーク BB-X LARISSA 2012年7月17日 4:23
    2012年7月13日 5:14
  • hwndOwner をNULLにすると、モードレスになるのではなく、オーナーウィンドウの指定がないモーダル表示になります。モードレスとオーナーレス(オーナーなし)は全く関係ありません。


    わんくま同盟,Microsoft MVP for Visual C++(Oct 2005-) http://blogs.wankuma.com/tocchann/

    2012年7月13日 5:43
  • いろんなサイトにそう言う感じの事が書いてありましたし(公式ではない)、実際やってみても目的の動作になるため、そう思っていましたが、厳密にはどちらもモーダルなのですか?

    そうなると、その判断基準って言うのは厳密にはどこにあるのでしょうか?ただ、とりあえず、「その基準において」モードレスでなかったとしても

    >親ウィンドウが操作できなくなってしまいます親ウィンドウも操作できるようにしたいのですが

    と言う要件は満たせるはずです。ただ、XP以外ではまだ試してないということがあるので、OSによって違う動作をするようでしたら教えていただけるとありがたいです。

    • 編集済み mr.setup 2012年7月13日 6:00
    2012年7月13日 5:55
  • Windowsの世界では、モーダルウィンドウの表示は、表示関数(メソッド)を呼び出すとそのウィンドウを閉じるまで、呼び出しメソッドから制御の戻らないものを指します。

    わかりやすい例でいえば、メッセージボックスをAPIで表示する場合が一番わかりやすいでしょう。

    MessageBox APIは、最初のパラメータにオーナーウィンドウを指定します。ここをNULLにして呼び出しても、有効なウィンドウハンドルを指定して呼び出しても、メッセージボックスを閉じるまでAPIからは帰ってきません。

    このような表示形式をウィンドウズでは、モーダル表示と呼びます。

    それ以外のウィンドウ表示(CreateWindow でWS_VISIBLEをつけて呼び出すなど)は、すべてモードレス表示となります。ただし、Windows でモードレスという表現をするのは、慣例的にダイアログと呼ばれるタイプのウィンドウを表示する場合のみとなっている場合が大半です。


    わんくま同盟,Microsoft MVP for Visual C++(Oct 2005-) http://blogs.wankuma.com/tocchann/

    • 回答としてマーク BB-X LARISSA 2012年7月17日 4:23
    2012年7月13日 6:08
  • なるほど、「そのウィンドウを閉じるまで、呼び出しメソッドから制御の戻らないもの」は納得ですね。

    そういう意味ならば確かに自作するしかないと思います。

    とはいえ、上記のとおり「親ウインドウに対する束縛」の有無という話であればhwndOwnerにNULLセットで出来るはずです。(そういえば、こういう「その関数呼び出しの位置から制御が戻らない」「でも別ウインドウのメッセージはUIスレッドから出来る(?)」のって内部的にはどうやってるんでしたっけ?)

    ただし、hwndOwnerにNULLセットだと少なくともこちらの環境で、その後別ウインドウクリックで、その背後にファイルダイアログが行ってしまう形になりました。(ようはNULL==HWND_DESKTOPという感じでしょうか)

    これだと困るという場合も、やはり自作するしかないと思います。

    2012年7月13日 6:25
  • 呼び出しメソッド内で、メッセージループを回しておき(MessageBox は内部で専用のメッセージループを回します)、ウィンドウが閉じるのを待つようにすれば、モーダル表示は任意のウィンドウで可能です。

    MFCのDoModal は、この形で実装されており、ダイアログも実際はモードレスで表示しておいて、自前のメッセージループを回すようになっています。

    NULL == HWND_DESKTOP は雰囲気としては間違ってはいませんが、値としてはNULL(なにも指し示さない)はNULLです。指し示す先がないNULLオブジェクトなので、暗黙的であってもどれかのウィンドウを指し示すなどはありません。もちろん、そのハンドルに対して何かを行うということもできません。


    わんくま同盟,Microsoft MVP for Visual C++(Oct 2005-) http://blogs.wankuma.com/tocchann/

    2012年7月13日 7:02
  • 回答ありがとうございます。

    GetSaveFileNameの挙動がどう影響するか、まずやってみようと思ってコードをかきました

            OPENFILENAME ofn;		ofn.lStructSize = sizeof(ofn);         // 構造体のサイズ
    	    static TCHAR filename_full[MAX_PATH];   // ファイル名(フルパス)を受け取る領域
    	    static TCHAR filename[MAX_PATH];        // ファイル名を受け取る領域
    
    		ofn.hwndOwner = nullptr;                  // コモンダイアログの親ウィンドウハンドル
    		ofn.lpstrFilter = _T("text(*.txt)\0*.txt\0All files(*.*)\0*.*\0\0"); // ファイルの種類
    		ofn.lpstrFile = filename_full;         // 選択されたファイル名(フルパス)を受け取る変数のアドレス
    		ofn.lpstrFileTitle = filename;         // 選択されたファイル名を受け取る変数のアドレス
    		ofn.nMaxFile = sizeof(filename_full);  // lpstrFileに指定した変数のサイズ
    		ofn.nMaxFileTitle = sizeof(filename);  // lpstrFileTitleに指定した変数のサイズ
    		ofn.Flags = OFN_FILEMUSTEXIST;         // フラグ指定
    		ofn.lpstrTitle = _T("ファイルを開く"); // コモンダイアログのキャプション
    		ofn.lpstrDefExt = _T("txt");           // デフォルトのファイルの種類
    
    		// ファイルを開くコモンダイアログを作成
    		if( !GetOpenFileName( &ofn ) )
    		{
    			return;
    		}


    ですがリンクエラーが出ています

    エラー 1 error LNK2001: 外部シンボル ""extern "C" int __stdcall GetOpenFileNameW(struct tagOFNW *)" (?GetOpenFileNameW@@$$J14YGHPAUtagOFNW@@@Z)" は未解決です。


    2012年7月13日 7:06
  • なるほど、その中でまたそういう世界が構築されてるからこそ、ああいう挙動が出来るわけですね。

    確かにNULL自体はNULLですが、NULL == HWND_DESKTOPの件に関しては・・・一応私は各種APIの内部を実装した人ではないため、細かい挙動について断定は避けることにします。

    (HWND_DESKTOPというものが#defineされてる以上は、指定されたのがHWND_DESKTOP(現状ではNULLと等しい)と等しかったら、内部でHWND_DESKTOPみたいなイメージで組んでる、ようなAPIもある可能性があると思うので)

    詳しい解説ありがとうございます。

    • 編集済み mr.setup 2012年7月13日 7:21
    2012年7月13日 7:13
  • GetOpenFileNameW 関数がどこにあるかわからないというリンクエラーですね。こういうエラーが出た場合、まずAPIのヘルプリファレンスを参照し、どのインポートライブラリをリンクすればいいかを調べてください。

    GetOpenFileName は、コモンダイアログのDLLにありますので、Comdlg32.lib をリンクすれば解決します。APIによってリンクするライブラリが異なります。ここで聞くより(タイミングが合えば、即答かもしれませんが)、圧倒的に早い時間で必要なライブラリを見つけられますよ。

    ヘルプリファレンスがない場合は、MSDNライブラリのオンライン版(http://msdn.microsoft.com/en-us/library/default.aspx)で、検索すればすぐに見つかります。APIについては日本語ではなく、どんなに基礎的なものであっても「必ず」英語で検索しないとヒットしません。


    わんくま同盟,Microsoft MVP for Visual C++(Oct 2005-) http://blogs.wankuma.com/tocchann/

    2012年7月13日 7:14
  • >BB-X LARISSAさん

    Windows API関連のリンクエラーは、大抵は簡単に解決出来るお決まりパターンがありますw

    一例としては

    1.その関数名を検索かけると

    2.例えばここが見つかるので

    3.下の方を見ると「対応情報」の中に『インポートライブラリ:Comdlg32.lib を使用 』と書いてありますから

    4.そのとーりにComdlg32.libを参照に追加すれば良い、と言う事になります。

    (これで「リンクエラー」自体を解決出来なかったらVisual Studioのインストール時エラーとかぐらいかな)

    -----------追記-----------

    おっと、ちょいと遅かったためとっちゃんさんのコメントと一部かぶりましたねw 失礼しました。

    • 編集済み mr.setup 2012年7月13日 7:23
    2012年7月13日 7:18
  • 御二方とも、大変ありがとうございます。今後はライブラリからAPIを参照することを行うようにします。

    プロジェクトのプロパティから追加の依存ファイルにComdlg32.libを追加して、ビルドが通るようになりました。

    しかし、GetSaveFileNameをコールすると、何も表示されないまま即Returnに入ってしまいます・・。

            OPENFILENAME ofn;		ofn.lStructSize = sizeof(ofn);         // 構造体のサイズ
    	    static TCHAR filename_full[MAX_PATH];   // ファイル名(フルパス)を受け取る領域
    	    static TCHAR filename[MAX_PATH];        // ファイル名を受け取る領域
    
            memset(&ofn, 0, sizeof(ofn));
    		ofn.hwndOwner = nullptr;                  // コモンダイアログの親ウィンドウハンドル
    		ofn.lpstrFilter = _T("text(*.txt)\0*.txt\0All files(*.*)\0*.*\0\0"); // ファイルの種類
    		ofn.lpstrFile = filename_full;         // 選択されたファイル名(フルパス)を受け取る変数のアドレス
    		ofn.lpstrFileTitle = filename;         // 選択されたファイル名を受け取る変数のアドレス
    		ofn.nMaxFile = sizeof(filename_full);  // lpstrFileに指定した変数のサイズ
    		ofn.nMaxFileTitle = sizeof(filename);  // lpstrFileTitleに指定した変数のサイズ
    		ofn.Flags = OFN_FILEMUSTEXIST;         // フラグ指定
    		ofn.lpstrTitle = _T("ファイルを開く"); // コモンダイアログのキャプション
    		ofn.lpstrDefExt = _T("txt");           // デフォルトのファイルの種類
    
    		// ファイルを開くコモンダイアログを作成
    		if( !GetSaveFileName( &ofn ) )
    		{
    			return;
    		}

    -----追記------

    returnの前にGetLastErrorすると、エラーコードは87でした。

    http://ir9.jp/prog/ayu/win32err.htm

    で調べてみると、

    ERROR_INVALID_PARAMETER 87 0x00000057 パラメータが間違っています。

    となっていたので、

    ofn.lStructSize = sizeof(ofn);         // 構造体のサイズ

    を追記したらうまく表示できました。





    2012年7月13日 7:41
  • OPENFILENAME 構造体は、複数バージョンあるので、おそらくどこかにポインタをセットするところがあってそれがなくてエラー。。。なのだと思います。<未確認です。

    あと、memset(0埋め)するタイミングが間違っています。

    OPENFILENAME ofn;
    memset( &ofn, 0, sizeof(ofn) ); // ZeroMemory( &ofn, sizeof(ofn) ); でもよい
    ofn.lStructSize = sizeof(ofn);
    // 以下省略
    

    です。多分転記ミスだと思いますけど、念のため。


    わんくま同盟,Microsoft MVP for Visual C++(Oct 2005-) http://blogs.wankuma.com/tocchann/

    2012年7月13日 7:58
  • 一応。。。

    ヘッダーで #define HWND_DESKTOP ((HWND)0) とあるから、NULLは、デスクトップウィンドウと同義であると思われがちですが、いくつかのAPIは、NULL == ウィンドウを指していないのでエラーリターンになっていたはずです。

    ShowWindow( NULL, SW_MINIMIZED ) とか NULL == HWND_DESKTOP で処理しちゃうと困ったチャンなことになりますからね。


    わんくま同盟,Microsoft MVP for Visual C++(Oct 2005-) http://blogs.wankuma.com/tocchann/

    2012年7月13日 8:05
  • 初期化タイミングは、そうですね。
    ofn.lStructSize = sizeof(ofn);
    のあとにクリアしたらうまくいかなくてもおかしくないです。

    おまけとして、filename_fullやfilenameは、staticである必要はないはずです。

    ヘッダーで #define HWND_DESKTOP ((HWND)0) とあるから、NULLは、デスクトップウィンドウと同義であると思われがちですが、いくつかのAPIは、NULL == ウィンドウを指していないのでエラーリターンになっていたはずです。

    ShowWindow( NULL, SW_MINIMIZED ) とか NULL == HWND_DESKTOP で処理しちゃうと困ったチャンなことになりますからね。

    いくつかのAPIはそうなっていると思います。ShowWindowの場合は、解説にNULL入れるとこうなりますよ~的な事が書いてなかったので、そもそも第一引数がNULLのとき「将来も同じように特定の動作をする」という保証があるのかどうか分かりませんが

    CreateWindow関係の引数に指定してたり

    HDC hdc = GetDC( HWND_DESKTOP );

    ReleaseDC( HWND_DESKTOP, hdc );

    といったコードは見た事がありました。(いずれも公式ではない)

    GetDC関数の解説には「HWND_DESKTOP」ではなく「NULL」の場合と書いてあるので、素直にNULLの方が依存してなくていいのかも、という気もしますが。

    2012年7月13日 8:26
  • 本題と直接は関係ないですが、

    OPENFILENAME ofn = { sizeof OPENFILENAME };

    の1行でゼロクリアした上でサイズも指定できます。
    # C言語仕様で、{} で初期化すると構造体メンバーの順に値が格納されるので先頭にあるlStructSizeにサイズが入る。同じく仕様により {} で指定が足りなかったメンバーについてはゼロクリアする。

    2012年7月13日 8:32
  • 回答ありがとうございます

    初期化タイミングについては、すみません。

    頂いたアドバイスのとおり修正しました

    肝心の動作ですが、

    名前を付けて保存ダイアログを出しながら、親ウィンドウでは次々とイベントを受信して処理をしたいのですが

    やっぱりとっちゃんさんのいうとおり、制御が戻ってこないようでイベントをハンドリングできませんでした

    自作しかないのかなーー。。。

    2012年7月13日 9:29
  • それはこちらの環境とは動作が異なりますね。(全体のコードがないため、推測込みですが)

    ・一応、フォームは.NETのものですか? ネイティブ(MFCなど)のものですか?

    ・上記コードはどういうコード内に設置していますか?

    ・「イベント」と言うのは、ボタンクリックとかで発生するやつのことですか?そういうものであれば(メッセージループが内部で回ってると考えて、制御が返って来なくても)出来る、っていうことは、実際に.NETのフォームであっても、ネイティブの自作ウインドウであっても、こちらでは確認済みなのですが

    もしこれが全く別の独自のイベントであれば、コードが見えないと何とも・・・

    ※ただ、本筋としては、ファイルダイアログは制御を返すまで親ウインドウを束縛する方が普通だと思います。ファイルダイアログを表示しながら別ウインドウをいじりたい理由はどんなものなのでしょうか?

    (例えば多くのファイルを扱う場合は、複数ファイルのドラッグ&ドロップに対応するとか、の方がユーザーとしては楽ですし)

    • 編集済み mr.setup 2012年7月13日 9:46
    2012年7月13日 9:43
  • >一応、フォームは.NETのものですか? ネイティブ(MFCなど)のものですか?

    フォームは.NETです

    >上記コードはどういうコード内に設置していますか?

    特定のボタンクリックイベント内に設置しています

    >「イベント」と言うのは、ボタンクリックとかで発生するやつのことですか?

    独自イベントになっています

    シリアル受信用のDLLがあって、DLL内のEventHandlerにイベントを追加しておき

    シリアル受信したときにイベントが発生するようになっています


    2012年7月13日 9:51
  • 試してみました。

    テスト用に以前のCLITestという名前のC++/CLIのフォームアプリを使い,button1を追加

    メンバ関数OFN__を用意しておき

    void CLITest::Form1::OFN__(){ OPENFILENAME ofn = { sizeof ofn }; TCHAR filename_full[MAX_PATH]; // ファイル名(フルパス)を受け取る領域 TCHAR filename[MAX_PATH]; // ファイル名を受け取る領域 ofn.hwndOwner = NULL; // コモンダイアログの親ウィンドウハンドル ofn.lpstrFilter = _T("text(*.txt)\0*.txt\0All files(*.*)\0*.*\0\0"); // ファイルの種類 ofn.lpstrFile = filename_full; // 選択されたファイル名(フルパス)を受け取る変数のアドレス ofn.lpstrFileTitle = filename; // 選択されたファイル名を受け取る変数のアドレス ofn.nMaxFile = sizeof(filename_full); // lpstrFileに指定した変数のサイズ ofn.nMaxFileTitle = sizeof(filename); // lpstrFileTitleに指定した変数のサイズ ofn.Flags = OFN_FILEMUSTEXIST|OFN_EXPLORER|OFN_OVERWRITEPROMPT; // フラグ指定 ofn.lpstrTitle = _T("ファイルを開く"); // コモンダイアログのキャプション ofn.lpstrDefExt = _T("txt"); // デフォルトのファイルの種類 // ファイルを開くコモンダイアログを作成 if( !GetSaveFileName( &ofn ) )return;

    // 選択されたファイル名を表示 ::MessageBox( NULL, filename_full, _T("OK"), MB_OK ); }


    メインexe側のbutton1のクリックでこれを呼び出し

    private: System::Void button1_Click(System::Object^  sender, System::EventArgs^  e) {
    	OFN__();
    }




    DLLとしてもうひとつ、今度はC#でフォームアプリ「WindowsFormsApplication1」を作り、こちらはテストの手間を省くためForm1とbutton1をpublicに。ついで

    public static class ABCD {
            public static Form1 form;
            public static void open() {
                form = new Form1();
                form.Show();
                form.Text = "222";
           }
     }


    を用意しておき、これをメインexe側のForm1_Loadで呼び出しました。

    private: System::Void Form1_Load(System::Object^  sender, System::EventArgs^  e) {
    
    	WindowsFormsApplication1::ABCD::open();
    	WindowsFormsApplication1::ABCD::form->button1->Click += gcnew EventHandler( this, &Form1::UpdateWindowText );
    
    }


    UpdateWindowTextというのは、メイン側のForm1のメンバで、以下の通り

    void UpdateWindowText(System::Object^ sender, System::EventArgs^ e){
    		static int i=0;
    		Text = "" + ++i;
    }

    これで、exeを実行するとフォームをLoadするときにDLLのABCDをいじって別プロセスでもフォームを表示させ

    DLLサイドのフォームのボタンをクリックする事でexe側のフォームのTextが変化する、という事になります。

    あとはこれが、ファイルダイアログ表示中に変化すれば、OKのはずです。結果は

    ①がメイン側のフォームで、②がDLLサイドで、③がファイルダイアログです。ファイルダイアログ起動中にbutton1をクリックすると、このように変化しました。

    こちらでは出来てしまったので

    シリアル受信用のDLLがあって、DLL内のEventHandlerにイベントを追加しておき

    この辺の事でなにか問題がある・・・のかもしれません。ちょっと分かんないですが。(諸事情あって今ちょっと時間的にきつくなりつつあるので、しばらく返信できないかもしれません。)

    シリアル受信とファイルダイアログが同時進行するべき理由も欲しいかもしれません。

    それが妥当なものであり、多少特殊なニーズであれば、これ自体は出来たとしても、その用途に置いては自作の必要があるかもしれませんし。

    • 編集済み mr.setup 2012年7月13日 11:01
    2012年7月13日 10:56
  • 素晴らしい実証ありがとうございます

    後で細かくかきますが、私の勘違いでした、申し訳ありません。

    無事にイベントが発生していました。

    取り急ぎご報告です。

    2012年7月15日 17:47
  • 実証して頂いた通りのコードでこちらでもイベント発生していました

    何がわたしの勘違いだったかというと

    イベント処理で、下記のようなコードを書いていました

    if(!global_flag){
    	global_flag = true;
    	// ~長い処理~
    	global_flag = false;
    }

    長い処理を行ってる最中にファイルダイアログを開くと、global_flagはtrueのままになる

    (SaveFileDialogから処理が返ってこない為と思われる)

    次のイベントが発生しても、処理を走らずに抜けていました。

    フラグを用いない実装にしてクリアできました。

    おかげさまで、無事にSaveFileDialogをモードレス(厳密にはオーナーウィンドウnullのダイアログ)で表示することができました

    大変ありがとうございました





    2012年7月17日 4:18