C# と VB.NET の質問掲示板

ASP.NET、C++/CLI、Java 何でもどうぞ

C# と VB.NET の入門サイト

メッセージボックスが出ない

[トピック内 8 記事 (1 - 8 表示)]  << 0 >>

■86788 / inTopicNo.1)  メッセージボックスが出ない
  
□投稿者/ mbox (1回)-(2018/03/15(Thu) 21:51:00)

分類:[.NET 全般] 

以下のプログラムで、メニューをクリックするとメッセージボックスが2回出ることを期待しているのですが、
なぜか1個目のメッセージボックスが出ず、
2個目のメッセージボックス(2 ret1=No)しか出ません。
期待通りにメッセージボックスを出すにはどうすればいいのでしょうか?
(.net 4.0, xpでもwin7でも同じ現象)

using System;
using System.Windows.Forms;

class Form1 : Form
{
    [STAThread]
    static void Main()
    {
        Application.Run(new Form1());
    }

    public Form1()
    {
        Controls.Add(new UserControl1());

        Menu = new MainMenu(new MenuItem[] { new MenuItem("&File...", (sender, e) =>
        {
            var control = Controls[0];
            Controls.Remove(control);
            control.Dispose();

            Controls.Add(new UserControl1());
        })});
    }
}

class UserControl1 : UserControl
{
    public UserControl1()
    {
        var ret1 = MessageBox.Show("1");
        var ret2 = MessageBox.Show("2 ret1=" + ret1.ToString());

        Controls.Add(new Button());
    }
}

引用返信 編集キー/
■86789 / inTopicNo.2)  Re[1]: メッセージボックスが出ない
□投稿者/ Azulean (925回)-(2018/03/15(Thu) 22:15:14)
たとえば、オーナーを明示的に指定するとか。

class Form1 : Form
{
    public Form1()
    {
        Controls.Add(new UserControl1(null));

        Menu = new MainMenu(new MenuItem[] { new MenuItem("&File...", (sender, e) =>
    {
        var control = Controls[0];
        Controls.Remove(control);
        control.Dispose();

        Controls.Add(new UserControl1(this));
    })});
    }
}

class UserControl1 : UserControl
{
    public UserControl1(IWin32Window owner)
    {
        var ret1 = MessageBox.Show(owner, "1");
        var ret2 = MessageBox.Show(owner, "2 ret1=" + ret1.ToString());

        Controls.Add(new Button());
    }
}


(クリックによって消えようとしているメニューか親になっていてすぐに閉じられてしまっているのではないかな?)

引用返信 編集キー/
■86791 / inTopicNo.3)  Re[1]: メッセージボックスが出ない
□投稿者/ 魔界の仮面弁士 (1592回)-(2018/03/15(Thu) 23:20:07)
2018/03/15(Thu) 23:47:55 編集(投稿者)

No86788 (mbox さん) に返信
> 期待通りにメッセージボックスを出すにはどうすればいいのでしょうか?

Form1 側に手を加えたくないのなら、
UserControl1 側で DoEvents を回すとか。

public UserControl1()
{
 Application.DoEvents(); // ★
 var ret1 = MessageBox.Show("1");
 var ret2 = MessageBox.Show("2 ret1=" + ret1.ToString());
 Controls.Add(new Button());
}



DoEvents を避けたいなら、MessageBoxOptions.ServiceNotification を指定するとか。

public UserControl1()
{
 var ret1 = MessageBox.Show("1", "", MessageBoxButtons.OK, MessageBoxIcon.None, MessageBoxDefaultButton.Button1, MessageBoxOptions.ServiceNotification);
 var ret2 = MessageBox.Show("2 ret1=" + ret1.ToString(), "", MessageBoxButtons.OK, MessageBoxIcon.None, MessageBoxDefaultButton.Button1, MessageBoxOptions.ServiceNotification);
 Controls.Add(new Button());
}



ServiceNotification 指定が冗長なら、owner に NativeWindow でも渡しておくとか。

public UserControl1()
{
 var nw = new NativeWindow(); // ★
 var ret1 = MessageBox.Show(nw, "1");
 var ret2 = MessageBox.Show(nw, "2 ret1=" + ret1.ToString());
 Controls.Add(new Button());
}



MessageBox の引数を増やしたくなければ、自身のハンドルを先に作成しておくとか。

public UserControl1()
{
 if(!IsHandleCreated) { CreateHandle(); } // ★
 var ret1 = MessageBox.Show("1");
 var ret2 = MessageBox.Show("2 ret1=" + ret1.ToString());
 Controls.Add(new Button());
}



Form1 側を修正して良いのであれば、Dispose は後で行うようにするとか。

public Form1()
{
 Controls.Add(new UserControl1());
 Menu = new MainMenu(new MenuItem[] { new MenuItem("&File...", (sender, e) =>
 {
  using(var control = Controls[0])
  {
   Controls.Remove(control);
   Controls.Add(new UserControl1());
  }
 })});
}



Dispose は先に行いたいなら、フォーカスを受け取れるダミーコントロールを残しておくとか。

public Form1()
{
 Controls.Add(new UserControl1());
 Controls.Add(new Control()); // ★
 Menu = new MainMenu(new MenuItem[] { new MenuItem("&File...", (sender, e) =>
 {
  var control = Controls[0];
  Controls.Remove(control);
  control.Dispose();
  Controls.Add(new UserControl1());
 })});
}



ダミーコントロールを使いたくないなら、自フォームにフォーカスを戻すとか。

public Form1()
{
 Controls.Add(new UserControl1());
 Menu = new MainMenu(new MenuItem[] { new MenuItem("&File...", (sender, e) =>
 {
  var control = Controls[0];
  Controls.Remove(control);
  control.Dispose();
  Focus(); // ★
  Controls.Add(new UserControl1());
 })});
}
引用返信 編集キー/
■86795 / inTopicNo.4)  Re[2]: メッセージボックスが出ない
□投稿者/ にゃるら (8回)-(2018/03/16(Fri) 11:21:57)
Altキー(またはF10キー)でメニューを選択状態にして、Enterキーを押しっぱなしにしてみてください。
メッセージ2が出るとは思いますが、勝手に閉じるのが見えると思います。
メニューをクリックしたときにそのことが表示されているメッセージボックスにも伝わっているのかもしれないですね。

では何が起きているのだろう?
最初に出すメッセージボックス代わりにFormクラスを使ってみました。
Formのイベントをいくつか拾えるようにDebug.WriteLineを入れてみました。
SetVisiableCore(true)は呼ばれているけど、shownイベントが起きていないようですね。
表示しようとするときに何か起きて、そのままクローズ処理が走っているようです。
うーん、なんだろう?

public Form1()
{
    Controls.Add(new UserControl1());

    Menu = new MainMenu(new MenuItem[] { new MenuItem("&File...", (sender, e) =>
    {
        var control = Controls[0];
        Controls.Remove(control);
        control.Dispose();

        Controls.Add(new UserControl1());
    })});
}

class UserControl1 : UserControl
{
    private static bool init = true;

    public UserControl1()
    {
        Controls.Add(new Button());

        if (init)
        {
            init = false;
            return;
        }

        using (var form2 = new Form2())
        {
            var ret1 = form2.ShowDialog();
            var ret2 = MessageBox.Show($"2 ret1={ret1}");
        }
    }
}

class Form2 : Form
{
    public Form2()
    {                
        var btnCancel = new Button();
        btnCancel.DialogResult = DialogResult.Cancel;
        btnCancel.Location = new Point(10, 10);
        btnCancel.Size = new Size(100, 23);

        btnCancel.MouseDown += (sender, e) => System.Diagnostics.Debug.WriteLine("form2.btnCancel.mousedown");
        btnCancel.Click += (sender, e) => System.Diagnostics.Debug.WriteLine("form2.btnCancel.click");
        btnCancel.KeyDown += (sender, e) => System.Diagnostics.Debug.WriteLine("form2.btnCancel.keydown");
        btnCancel.KeyPress += (sender, e) => System.Diagnostics.Debug.WriteLine("form2.btnCancel.keypress");
        Controls.Add(btnCancel);

        CancelButton = btnCancel;
        StartPosition = FormStartPosition.CenterScreen;
        Size = new Size(500, 400);

        Load += (sender, e) => System.Diagnostics.Debug.WriteLine("form2.load");
        Shown += (sender, e) => System.Diagnostics.Debug.WriteLine("form2.show");
        FormClosing += (sender, e) => System.Diagnostics.Debug.WriteLine($"form2.closing(sender:{sender.GetType().FullName}, reason:{e.CloseReason})");
        FormClosed += (sender, e) => System.Diagnostics.Debug.WriteLine($"form2.closed(sender:{sender.GetType().FullName}, reason:{e.CloseReason})");
    }

    protected override void SetVisibleCore(bool value)
    {
        System.Diagnostics.Debug.WriteLine($"form2.SetVisibleCore(value:{value})");
        base.SetVisibleCore(value);
    }
}

引用返信 編集キー/
■86796 / inTopicNo.5)  Re[3]: メッセージボックスが出ない
□投稿者/ にゃるら (9回)-(2018/03/16(Fri) 17:05:19)
No86795 (にゃるら さん) に返信

うーん、こういうことなのでしょうか?

モーダルで表示される直前にForm1から親子関係が解かれたユーザコントロールは、
一時引受人であるParkingwindowさんの管理下に入る。
ユーザコントロールが破棄されるときに、Parkingwindowさんは自分が引き受けているハンドルが
すべてなくなったことをチェックする処理を遅延実行する。
メッセージボックスは、オーナーが指定されていないので、親ウィンドウがParkingWindowさんになる。
遅延していたチェック処理が走り、Parkingwindowさんは自分が引き受けているハンドルが
すべてなくなったことで自分をDestoryしようとする。

その結果、モーダルウィンドウは親ウィンドウを失うことになるため、閉じられる。

これを回避するためには、

・モーダルウィンドウの親ウィンドウを明示的に指定する
・ユーザコントロールが破棄されたあと、ParikingWindowsさんが引き受けているハンドルがないのかを
チェックするまでの間は新しいモーダルウィンドウは開かない

といった感じのことで対応しなくちゃいけないので、魔界の仮面弁士さんが挙げられた様々な対応案の
どれかを採用すれば解決できるってことなんですかね。。。

「オーナーとなるウィンドウを渡してモーダルウィンドウを出す」としておくのが吉っぽいですね。
引用返信 編集キー/
■86798 / inTopicNo.6)  Re[4]: メッセージボックスが出ない
□投稿者/ 魔界の仮面弁士 (1594回)-(2018/03/16(Fri) 21:58:21)
No86795 (にゃるら さん) に返信
> Enterキーを押しっぱなしにしてみてください。
> メッセージ2が出るとは思いますが、勝手に閉じるのが見えると思います。

勝手に閉じたというよりは、メッセージボックスの default button が
Enter キーによって押下されたということではないでしょうか。


No86796 (にゃるら さん) に返信
> うーん、こういうことなのでしょうか?


以下、私の認識。(多分に推測を含みます)


.NET Framework のソースコードを読んでみると明らかですが、
MessageBox.Show メソッドの内部実装は、最終的に MessageBox API を呼び出します。

そして MessageBox API は、第一引数にオーナーウィンドウの ハンドルを渡す
仕様になっています。(ただし、この第一引数は NULL でも構いません)
https://msdn.microsoft.com/en-us/library/windows/desktop/ms645505.aspx


そして、今回問題になっているコードですが、試しに
 var ret1 = MessageBox.Show("1");
 var e1 = Marshal.GetLastWin32Error();
 var ret2 = MessageBox.Show("2 ret1=" + ret1.ToString());
 var e2 = Marshal.GetLastWin32Error();
として実行すると、"1" が表示されない場合には、
 e1 が 6 (すなわち ERROR_INVALID_HANDLE)
 e2 が 0 (すなわち NO_ERROR)
という値が返される状況になっていました。


MessageBox.Show の内部実装を見る限りでは、owner を null にした場合、
GetActiveWindow API で得られたウィンドウが使われるようなので、今回の場合、
おそらくこの段階で invalid な HWND が使われてしまったのではないでしょうか。

そしてその結果、既に破棄されたウィンドウハンドルが
MessageBox API の第一引数に渡されることになり、この P/Invoke が
ERROR_INVALID_HANDLE を理由に失敗していたのだと推察します。
親ハンドルが不正のため、"1" のメッセージはそもそも表示されません。


また、呼び出しが失敗した場合、MessageBox API は固定値 0 を返す仕様になっています。
ですから、呼び出されなかった場合には
 var ret1 = (DialogResult)0;
に相当する処理結果になりそうなのですが…実際には
DialogResult.None ではなく、DialogResult.No が返されています。

何故、この段階で None ではなく No が返されるのかといえば、
 var ret1 = Win32ToDialogResult(0);
という処理が行われるためのようです。
このメソッドの実装は下記をご覧ください。
https://referencesource.microsoft.com/#System.Windows.Forms/winforms/Managed/System/WinForms/MessageBox.cs,bbe9ce90a8f7d475


この推察が正しいなら、回避策としては、正しいオーナーを渡せば良いわけですから、
Azulean さんの No86789 のコードが、解決策の一つになるかと思います。


もしくは、MessageBox API は、オーナーの HWND を NULL にすることを認めていますので、
API の第一引数に対して、意図的に IntPtr.Zero が渡るようにすることでも、
今回の問題を回避することができます。

 private class NullWindow : IWin32Window
 {
  // IntPtr IWin32Window.Handle => IntPtr.Zero;
  IntPtr IWin32Window.Handle { get { return IntPtr.Zero; } }
 }
 public UserControl1()
 {
  var nw = new NullWindow();
  var ret1 = MessageBox.Show(nw, "1");
  var ret2 = MessageBox.Show(nw, "2 ret1=" + ret1.ToString());
  Controls.Add(new Button());
 }


とはいえ、NULL ハンドルな IWin32Window を作るだけなら、
わざわざ上記のようなクラスを自作せずとも、
No86791 に書いた NativeWindow を利用できます。
引用返信 編集キー/
■86800 / inTopicNo.7)  Re[5]: メッセージボックスが出ない
□投稿者/ mbox (2回)-(2018/03/17(Sat) 15:11:32)
どの回答も非常にためになりました。ありがとうございました。

回避策としてはこんな方法もありました。
var control = Controls[0];
//Controls.Remove(control); //★
control.Dispose();
こうすると、親コントロールがあればそのControlsからRemoveするのは自動的にやってくれるし、
フォーカスの移動も自動でやってくれるようなので、当初の期待通りの結果になりました。
RemoveしてからDisposeだと、一時的な親がわりとして
WindowsFormsParkingWindow?なるものが登場して、変なことになるようです。
なので、RemoveせずにDisposeするのが良い?、ようです。


No86798 (魔界の仮面弁士 さん) に返信
>  var ret1 = MessageBox.Show("1");
>  var e1 = Marshal.GetLastWin32Error();
>  var ret2 = MessageBox.Show("2 ret1=" + ret1.ToString());
>  var e2 = Marshal.GetLastWin32Error();
> として実行すると、"1" が表示されない場合には、
>  e1 が 6 (すなわち ERROR_INVALID_HANDLE)
>  e2 が 0 (すなわち NO_ERROR)
> という値が返される状況になっていました。

今私の手元にある環境(xp)では結果が微妙に違っていました。
Marshal.GetLastWin32Error()だと常に0になっちゃっていたので、
[DllImport("kernel32.dll")] static extern int GetLastError();
を使ったら、"1" が表示されない場合には、
e1 が 1400 (ERROR_INVALID_WINDOW_HANDLE)
となっていました。


解決済み
引用返信 編集キー/
■86806 / inTopicNo.8)  Re[5]: メッセージボックスが出ない
□投稿者/ にゃるら (10回)-(2018/03/19(Mon) 11:49:51)
2018/03/19(Mon) 12:00:02 編集(投稿者)
2018/03/19(Mon) 11:59:57 編集(投稿者)

<pre><pre>■No86798 (魔界の仮面弁士 さん) に返信

MessageBoxがFormとは異なる実装とは思っていましたが、
似たようなことをしていると思い、MessageBoxクラスまでは
追いかけていませんでした。。。

public static DialogResult Show(string text)
{
    return ShowCore(null, text, String.Empty, MessageBoxButtons.OK, MessageBoxIcon.None, MessageBoxDefaultButton.Button1, 0, false);
}

であれば、オーナーを指定していないときはnull扱いなので、
GetActiveWindowsを呼び出す形でMessageBoxAPIに置き換えたソースコードを書いてみました(文末)。


実行結果ですが、環境は以下になります。
  Windows7 SP1 (日本語) 64bit
  Visual Studio 2017

まずは質問者様と同じForms.MessageBoxを使った場合は、
1回目は、
  Result = No
  LastError(1150): ???
で2回目以降は
  Result = No
  LastError(1400): ERROR_INVALID_WINDOW_HANDLE
でした。

一方で、MessageBoxAPIに置き換えた方は、
1回目は、
  AciveWindow(788866):System.Windows.Forms.Application+ParkingWindow
  Result = None
  LastError(1400): ERROR_INVALID_WINDOW_HANDLE
で2回目からもParkingwindowのハンドルは違うものが出ます。
ただ、何回かしていると(時には2回目から)
  AciveWindow(919896):WindowsFormsApp9.Form1
  Result = OK
  LastError(0): ???
となりました。

私の呼び出し方に問題がもしなかったとすれば、GetActiveWindow/MessageBox以外の
API呼び出し等も影響を与えていそうです。

ただ、なんにしても「(呼び出し側にとって)不特定なハンドル」が親ウィンドウになってしまうことが
原因ということになんら変わらないわけなので、

・メインとなるダイアログクラス以外は、
  - 親ハンドルを渡す(Azulean さんの No86789 のコード)
  - NativeWindowを渡す(魔界の仮面弁士 さんの No86791のNativeWindow指定) <-- これはGetWindowLongレベルの知識が必要なので微妙

・サービスであればMessageBoxOptions.ServiceNotificationを指定する(魔界の仮面弁士 さんの No86791のServiceNotification指定)

としておくのが良いのかなと個人的には思いました。

自分(もしくは周りの人)でMessageBoxのオーナーを指定しない実装がなかったので、結構面白い内容でした!


using System;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using System.Windows.Forms;

namespace WindowsFormsApp9
{
    public class Form1 : Form
    {
        [STAThread]
        static void Main()
        {
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);
            Application.Run(new Form1());
        }

        public Form1()
        {
            Controls.Add(new UserControl1());

            Menu = new MainMenu(new MenuItem[] { new MenuItem("&File...", (sender, e) =>
            {
                var control = Controls[0];
                Controls.Remove(control);
                control.Dispose();

                Controls.Add(new UserControl1());
            })});
        }

        class UserControl1 : UserControl
        {
            static bool first = true;

            public UserControl1()
            {
                if (first)
                {
                    first = false;
                }
                else
                {

#if true
                    var ret1 = System.Windows.Forms.MessageBox.Show("1");
#else
                    var activeWindow = GetActiveWindow();
                    var c = Control.FromHandle(activeWindow);
                    System.Diagnostics.Debug.WriteLine($"AciveWindow({activeWindow}):{(c != null? c.GetType().FullName: "null")}");

                    var ret1 = (DialogResult)MessageBox(new HandleRef(null, activeWindow), "出ないはずメッセージ", "確認", MB_OK | MB_ICONEXCLAMATION | MB_APPLMODAL);
#endif
                    System.Diagnostics.Debug.WriteLine($"Result = {ret1}");
                    GetLastErrorCheck();

                    var ret2 = System.Windows.Forms.MessageBox.Show("2 ret1=" + ret1.ToString());
                }

                Controls.Add(new Button());
            }
        }

        [DllImport("kernel32.dll")]
        static extern int GetLastError();

        [DllImport("user32.dll", CharSet = System.Runtime.InteropServices.CharSet.Auto),
                    SuppressMessage("Microsoft.Usage", "CA2205:UseManagedEquivalentsOfWin32Api")]
        [ResourceExposure(ResourceScope.None)]
        public static extern int MessageBox(HandleRef hWnd, string text, string caption, int type);

        [DllImport("user32.dll", ExactSpelling = true, CharSet = CharSet.Auto)]
        [ResourceExposure(ResourceScope.Process)]
        public static extern IntPtr GetActiveWindow();

        const int MB_OK = 0x00000000;
        const int MB_ICONEXCLAMATION = 0x00000030;
        const int MB_APPLMODAL = 0x00000000;

        const int ERROR_INVALID_HANDLE = 6;
        const int ERROR_INVALID_WINDOW_HANDLE = 1400;

        private static void GetLastErrorCheck()
        {
            var lastError = GetLastError();
            switch (lastError)
            {
                case ERROR_INVALID_HANDLE:
                    System.Diagnostics.Debug.WriteLine($"LastError({lastError}): ERROR_INVALID_HANDLE");
                    break;
                case ERROR_INVALID_WINDOW_HANDLE:
                    System.Diagnostics.Debug.WriteLine($"LastError({lastError}): ERROR_INVALID_WINDOW_HANDLE");
                    break;
                default:
                    System.Diagnostics.Debug.WriteLine($"LastError({lastError}): ???");
                    break;
            }
        }
    }    
}
</pre></pre>

解決済み
引用返信 編集キー/

このトピックをツリーで一括表示


トピック内ページ移動 / << 0 >>

このトピックに書きこむ