web-dev-qa-db-ja.com

すべてのフォームをブロックせずにShowDialogを使用できますか?

これを十分に明確に説明できるといいのですが。メインフォーム(A)があり、form.Show()を使用して1つの子フォーム(B)を開き、form.Show()を使用して2番目の子フォーム(C)を開きます。ここで、子フォームBがform.ShowDialog()を使用してフォーム(D)を開くようにします。これを行うと、フォームAとフォームCもブロックされます。モーダルダイアログを開き、それを開いたフォームのみをブロックする方法はありますか?

47
Jon Tackabury

AおよびCとは別のスレッドでフォームBを実行する場合、ShowDialog呼び出しはそのスレッドのみをブロックします。明らかに、それはもちろん仕事の些細な投資ではありません。

別のスレッドでフォームDのShowDialog呼び出しを実行するだけで、ダイアログがスレッドをまったくブロックしないようにすることができます。これには同じ種類の作業が必要ですが、アプリのメインスレッドから実行されるフォームは1つだけなので、作業量ははるかに少なくなります。

9
TheSmurf

複数のGUIスレッドを使用するのは難しい作業であり、これがそうするための唯一の動機である場合、私はそれに反対します。

より適切なアプローチは、Show()の代わりにShowDialog()を使用し、ポップアップフォームが戻るまで所有者フォームを無効にすることです。 4つの考慮事項があります。

  1. ShowDialog(owner)が使用される場合、ポップアップフォームはその所有者の上に残ります。 Show(owner)を使用する場合も同様です。または、Ownerプロパティを明示的に設定して、同じ効果を得ることができます。

  2. 所有者フォームのEnabledプロパティをfalseに設定すると、フォームは無効な状態を示します(子コントロールは「グレー表示」)が、ShowDialogを使用すると、所有者はフォームは引き続き無効になりますが、無効な状態は表示されません。

    ShowDialogを呼び出すと、所有者フォームはWin32コードで無効になり、その_WS_DISABLED_スタイルビットが設定されます。これにより、クリックしたときにフォーカスを取得したり「Ding」したりできなくなりますが、グレーになりません。

    フォームのEnabledプロパティをfalseに設定すると、特定のコントロールが自身を描画するときにチェックする追加のフラグが設定されます(基になるWin32サブシステムではなく、フレームワーク内)。このフラグは、コントロールを無効状態で描画するようにコントロールに指示します。

    したがって、ShowDialogで何が起こるかをエミュレートするには、フォームのEnabledプロパティをfalseに設定する代わりに、ネイティブの_WS_DISABLED_スタイルビットを直接設定する必要があります。これは、わずかな相互運用によって実現されます。

    _const int GWL_STYLE   = -16;
    const int WS_DISABLED = 0x08000000;
    
    [DllImport("user32.dll")]
    static extern int GetWindowLong(IntPtr hWnd, int nIndex);
    
    [DllImport("user32.dll")]
    static extern int SetWindowLong(IntPtr hWnd, int nIndex, int dwNewLong);
    
    void SetNativeEnabled(bool enabled){
        SetWindowLong(Handle, GWL_STYLE, GetWindowLong(Handle, GWL_STYLE) &
            ~WS_DISABLED | (enabled ? 0 : WS_DISABLED));
    }
    _
  3. ShowDialog()呼び出しは、ダイアログが閉じられるまで戻りません。ダイアログが処理を完了するまで、所有者フォームのロジックを一時停止できるため、これは便利です。 Show()呼び出しは、必ずしもこのように動作しません。したがって、Show()の代わりにShowDialog()を使用する場合は、ロジックを2つの部分に分割する必要があります。ダイアログが閉じられた後に実行する必要があるコード(所有者フォームの再有効化を含む)は、Closedイベントハンドラーによって実行する必要があります。

  4. フォームがダイアログとして表示される場合、そのDialogResultプロパティを設定すると自動的に閉じます。このプロパティは、DialogResult以外のNoneプロパティを持つボタンがクリックされるたびに設定されます。 Showで表示されたフォームは、このように自動的に閉じません。そのため、解雇ボタンの1つがクリックされたときに明示的に閉じる必要があります。ただし、DialogResultプロパティはボタンによって適切に設定されることに注意してください。

これら4つのことを実装すると、コードは次のようになります。

_class FormB : Form{
    void Foo(){
        SetNativeEnabled(false); // defined above
        FormD f = new FormD();
        f.Closed += (s, e)=>{
            switch(f.DialogResult){
            case DialogResult.OK:
                // Do OK logic
                break;
            case DialogResult.Cancel:
                // Do Cancel logic
                break;
            }
            SetNativeEnabled(true);
        };
        f.Show(this);
        // function Foo returns now, as soon as FormD is shown
    }
}

class FormD : Form{
    public FormD(){
        Button btnOK       = new Button();
        btnOK.DialogResult = DialogResult.OK;
        btnOK.Text         = "OK";
        btnOK.Click       += (s, e)=>Close();
        btnOK.Parent       = this;

        Button btnCancel       = new Button();
        btnCancel.DialogResult = DialogResult.Cancel;
        btnCancel.Text         = "Cancel";
        btnCancel.Click       += (s, e)=>Close();
        btnCancel.Parent       = this;

        AcceptButton = btnOK;
        CancelButton = btnCancel;
    }
}
_
84
P Daddy

別のスレッドを使用できます(以下を参照)が、これは危険な領域になります-スレッドの影響(同期、クロススレッドアクセスなど)を理解している場合にのみ、このオプションに近づいてください。

[STAThread]
static void Main() {
    Application.EnableVisualStyles();
    Button loadB, loadC;
    Form formA = new Form {
        Text = "Form A",
        Controls = {
            (loadC = new Button { Text = "Load C", Dock = DockStyle.Top}),
            (loadB = new Button { Text = "Load B", Dock = DockStyle.Top})
        }
    };
    loadC.Click += delegate {
        Form formC = new Form { Text = "Form C" };
        formC.Show(formA);
    };
    loadB.Click += delegate {
        Thread thread = new Thread(() => {
            Button loadD;
            Form formB = new Form {
                Text = "Form B",
                Controls = {
                    (loadD = new Button { Text = "Load D",
                        Dock = DockStyle.Top})
                }
            };
            loadD.Click += delegate {
                Form formD = new Form { Text = "Form D"};
                formD.ShowDialog(formB);
            };
            formB.ShowDialog();  // No owner; ShowDialog to prevent exit
        });
        thread.SetApartmentState(ApartmentState.STA);
        thread.Start();
    };
    Application.Run(formA);
}

(明らかに、上記のように実際にコードを構造化することはありません-これは、動作を示す最短の方法です;実際のコードでは、フォームごとにクラスがあるなど)

11
Marc Gravell

考えられる解決策を要約し、1つの新しい選択肢(3aおよび3b)を追加したいと思います。しかし、最初に私たちが話していることを明確にしたい:

複数のフォームを持つアプリケーションがあります。フォームの特定のサブセットのみをブロックし、他のフォームはブロックしないモーダルダイアログを表示する必要があります。モーダルダイアログは、1つのサブセット(シナリオA)または複数のサブセット(シナリオB)でのみ表示できます。

そして今、可能な解決策の要約:

  1. ShowDialog()で表示されるモーダルフォームをまったく使用しない

    アプリケーションの設計について考えてください。本当にShowDialog()メソッドを使用する必要がありますか?モーダルフォームが必要ない場合は、最も簡単でクリーンな方法です。

    もちろん、このソリューションは常に適しているとは限りません。 ShowDialog()が提供する機能がいくつかあります。最も注目すべきは、それが所有者を無効にします(ただし、グレー表示されない)ことであり、ユーザーはそれと対話できません。 P Daddy という非常に骨の折れる答えです。

  2. ShowDialog()動作をエミュレートします

    その方法の動作をエミュレートすることは可能です。繰り返しますが、 P Daddy's answer を読むことをお勧めします。

    a)使用EnabledFormプロパティの組み合わせ/ Show()を介して非モーダルとしてフォームを表示。その結果、無効なフォームはグレー表示されます。ただし、相互運用が不要な完全に管理されたソリューションです。

    b)グレー表示されている親フォームが気に入らない?いくつかのネイティブメソッドを参照し、親フォームでWS_DISABLEDビットをオフにします(もう一度- P Daddy の回答を参照).

    これらの2つのソリューションでは、処理する必要があるすべてのダイアログボックスを完全に制御する必要があります。 「部分的にブロックされたダイアログ」を表示するには特別な構造を使用する必要があり、それを忘れてはなりません。 Show()はブロックされておらず、ShowDialog()はブロックされているため、ロジックを調整する必要があります。システムダイアログ(ファイル選択、カラーピッカーなど)の処理が問題になる可能性があります。一方、ダイアログでブロックされないフォームに追加のコードは必要ありません。

  3. ShowDialog()の制限を克服する

    Application.EnterThreadModal および Application.LeaveThreadModal イベントがあることに注意してください。このイベントは、モーダルダイアログが表示されるたびに発生します。イベントは実際にはアプリケーション全体ではなくスレッド全体であることに注意してください。

    a)Application.EnterThreadModalイベントをリッスンしますダイアログでブロックされないフォームで/それらのフォームでWS_DISABLEDビットをオンにしますモーダルダイアログによってブロックされるべきではないフォームのみを調整する必要があります。また、表示されているモーダルフォームの親チェーンを検査し、この条件に基づいてWS_DISABLEDを切り替える必要があります(この例では、フォームAとCでダイアログを開く必要がありますが、フォームBとDはブロックしません)。

    b)ブロックしないフォームを非表示にして再表示します。モーダルダイアログが表示された後に新しいフォームを表示すると、ブロックされないことに注意してください。それを利用して、モーダルダイアログが表示されたら、ブロックされないように目的のフォームを再度非表示にして表示します。ただし、この方法ではちらつきが生じる場合があります。 Win APIでフォームの再描画を有効/無効にすることで理論的に修正できますが、それを保証するものではありません。

    c)Ownerプロパティをダイアログフォームに設定しますダイアログが表示されたときにブロックされるべきではないフォームで。私はこれをテストしませんでした。

    d)複数のGUIスレッドを使用しますTheSmurfからの回答

6
mancze

FormAの新しいスレッドでFormBを開始します。

        (new System.Threading.Thread(()=> {
            (new FormB()).Show();
        })).Start();

これで、ShowDialog()を使用して新しいスレッドで開かれたフォームは、FormBのみをブロックし、FormAまたはFormCはブロックしません。

4
Robert Venables

ここでソリューションを追加したかったのは、それが私にとってはうまくいくようで、単純な拡張メソッドにカプセル化できるからです。私がする必要があるのは、@ nightcoderが@PDaddyの答えにコメントしたように、フラッシュに対処することだけです。

public static void ShowWithParentFormLock(this Form childForm, Form parentForm)
{
  childForm.ShowWithParentFormLock(parentForm, null);
}

public static void ShowWithParentFormLock(this Form childForm, Form parentForm, Action actionAfterClose)
{
  if (childForm == null)
    throw new ArgumentNullException("childForm");
  if (parentForm == null)
    throw new ArgumentNullException("parentForm");
  EventHandler activatedDelegate = (object sender, EventArgs e) =>
  {
    childForm.Focus();
    //To Do: Add ability to flash form to notify user that focus changed
  };
  childForm.FormClosed += (sender, closedEventArgs) =>
    {
      try
      {
        parentForm.Focus();
        if(actionAfterClose != null)
          actionAfterClose();
      }
      finally
      {
        try
        {
          parentForm.Activated -= activatedDelegate;
          if (!childForm.IsDisposed || !childForm.Disposing)
            childForm.Dispose();
        }
        catch { }
      }
    };
  parentForm.Activated += activatedDelegate;
  childForm.Show(parentForm);
}
4
Justin Pihony

私が書いていたアプリケーションで同様の問題に直面していました。私のメインUIは、メインスレッドで実行されるフォームでした。モードレスダイアログとして実行したいヘルプダイアログがありました。これは実装するのが簡単で、ヘルプダイアログのインスタンスが1つしか実行されていないことを保証するまでです。残念なことに、私が使用したモーダルダイアログは、ヘルプダイアログもフォーカスを失います。これらのモーダルダイアログの一部が実行されているときに、ヘルプダイアログがあれば最も便利です。

ここで言及したアイデアを使用して、他の場所で、このバグを克服することができました。

メインUI内でスレッドを宣言しました。

Thread helpThread;

次のコードは、ヘルプダイアログを開くために発生するイベントを処理します。

private void Help(object sender, EventArgs e)
{
    //if help dialog is still open then thread is still running
    //if not, we need to recreate the thread and start it again
    if (helpThread.ThreadState != ThreadState.Running)
    {
        helpThread = new Thread(new ThreadStart(startHelpThread));
        helpThread.SetApartmentState(ApartmentState.STA);
        helpThread.Start();
    }
}

void startHelpThread()
{
    using (HelpDialog newHelp = new HelpDialog(resources))
    {
        newHelp.ShowDialog();
    }
}

また、このコードが最初に実行されたときにnullオブジェクトを参照していないことを確認するために、コンストラクターに追加されたスレッドの初期化が必要でした。

public MainWindow()
{
    ...
    helpThread = new Thread(new ThreadStart(startHelpThread));
    helpThread.SetApartmentState(ApartmentState.STA);
    ...
}

これにより、スレッドは常に1つのインスタンスのみを持つようになります。スレッド自体がダイアログを実行し、ダイアログが閉じられると停止します。別のスレッドで実行されるため、メインUIからモーダルダイアログを作成しても、ヘルプダイアログはハングしません。追加する必要がありました

helpDialog.Abort();

メインUIのフォームを閉じるイベントに追加して、アプリケーションが終了したときにヘルプダイアログが閉じるようにします。

これで、メインUIから生成されたモーダルダイアログの影響を受けないモードレスヘルプダイアログができました。これはまさに私が望んでいたものです。メインUIとヘルプダイアログの間で通信が必要ないため、これは安全です。

3
Elgar Storm

この質問に対するいくつかの回答に基づいて、ダイアログが非ダイアログウィンドウをブロックしないようにするために、WPFで使用しているヘルパーを次に示します。

public static class WindowHelper
{
    public static bool? ShowDialogNonBlocking(this Window window)
    {
        var frame = new DispatcherFrame();

        void closeHandler(object sender, EventArgs args)
        {
            frame.Continue = false;
        }

        try
        {
            window.Owner.SetNativeEnabled(false);
            window.Closed += closeHandler;
            window.Show();

            Dispatcher.PushFrame(frame);
        }
        finally
        {
            window.Closed -= closeHandler;
            window.Owner.SetNativeEnabled(true);
        }
        return window.DialogResult;
    }

    const int GWL_STYLE = -16;
    const int WS_DISABLED = 0x08000000;

    [DllImport("user32.dll")]
    static extern int GetWindowLong(IntPtr hWnd, int nIndex);

    [DllImport("user32.dll")]
    static extern int SetWindowLong(IntPtr hWnd, int nIndex, int dwNewLong);

    static void SetNativeEnabled(this Window window, bool enabled)
    {
        var handle = new WindowInteropHelper(window).Handle;
        SetWindowLong(handle, GWL_STYLE, GetWindowLong(handle, GWL_STYLE) &
            ~WS_DISABLED | (enabled ? 0 : WS_DISABLED));
    }
}

使用法:

if(true == window.ShowDialogNonBlocking())
{
    // Dialog result has correct value
}
2
ghord

例の使用:

(new NoneBlockingDialog((new frmDialog()))).ShowDialogNoneBlock(this);

ソースコード:

class NoneBlockingDialog
{
    Form dialog;
    Form Owner;

    public NoneBlockingDialog(Form f)
    {
        this.dialog = f;
        this.dialog.FormClosing += new FormClosingEventHandler(f_FormClosing);
    }

    void f_FormClosing(object sender, FormClosingEventArgs e)
    {
        if(! e.Cancel)
            PUtils.SetNativeEnabled(this.Owner.Handle, true);
    }

    public void ShowDialogNoneBlock(Form owner)
    {
        this.Owner = owner;
        PUtils.SetNativeEnabled(owner.Handle, false);
        this.dialog.Show(owner);
    }
}

partial class PUtils
{
            const int GWL_STYLE = -16;
    const int WS_DISABLED = 0x08000000;


    [DllImport("user32.dll")]
    static extern int GetWindowLong(IntPtr hWnd, int nIndex);


    [DllImport("user32.dll")]
    static extern int SetWindowLong(IntPtr hWnd, int nIndex, int dwNewLong);


    static public void SetNativeEnabled(IntPtr hWnd, bool enabled)
    {
        SetWindowLong(hWnd, GWL_STYLE, GetWindowLong(hWnd, GWL_STYLE) & ~WS_DISABLED | (enabled ? 0 : WS_DISABLED));
    }
}
0
Paul

おそらく子ウィンドウ(詳細についてはChildWindowを参照)がより洗練されたソリューションとなり、すべての問題を回避できますGUIの個別のスレッド。

0
DermFrench