是否可以在不阻止所有表单的情况下使用ShowDialog?

时间:2009-01-09 15:30:22

标签: c# winforms showdialog

我希望我能够清楚地解释清楚。我有我的主表单(A),它使用form.Show()打开1个子表单(B),使用form.Show()打开第二个子表单(C)。现在我希望子窗体B使用form.ShowDialog()打开一个窗体(D)。当我这样做时,它会阻止形式A和C形式。有没有办法打开一个模态对话框,只让它阻止打开它的表单?

11 个答案:

答案 0 :(得分:82)

使用多个GUI线程是一项棘手的工作,如果这是您这样做的唯一动机,我会建议不要这样做。

更合适的方法是使用Show()而不是ShowDialog(),并禁用所有者表单,直到弹出窗体返回。只有四个注意事项:

  1. 使用ShowDialog(owner)时,弹出窗体会保留在其所有者的顶部。使用Show(owner)时也是如此。或者,您可以显式设置Owner属性,效果相同。

  2. 如果您将所有者表单的Enabled属性设置为false,则表单会显示禁用状态(子控件显示为“灰显”),而使用ShowDialog时,所有者表单仍然被禁用,但没有显示禁用状态。

    当您致电ShowDialog时,所有者表单在Win32代码中被禁用 - 其WS_DISABLED样式位被设置。这会导致它失去获得焦点的能力,并且在点击时失去“叮当”,但不会使它自己变成灰色。

    当您将表单的Enabled属性设置为false时,会设置一个额外的标志(在框架中,而不是底层的Win32子系统),某些控件会在他们自己绘制时进行检查。这个标志告诉控件在禁用状态下绘制自己。

    因此,为了模拟ShowDialog会发生什么,我们应该直接设置原生WS_DISABLED样式位,而不是将表单的Enabled属性设置为false。这是通过一点点互操作实现的:

    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(),则需要将逻辑分为两部分。解除对话框后应该运行的代码(包括重新启用所有者表单)应该由Closed事件处理程序运行。

  4. 当表单显示为对话框时,设置其DialogResult属性会自动将其关闭。只要单击DialogResult属性以外的None属性的按钮,就会设置此属性。显示为Show的表单不会自动关闭,因此我们必须在单击其中一个解雇按钮时明确关闭它。但请注意,按钮仍然可以正确设置DialogResult属性。

  5. 实现这四件事,您的代码就像:

    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;
        }
    }
    

答案 1 :(得分:10)

如果在A和C的单独线程上运行表单B,则ShowDialog调用将仅阻止该线程。显然,这当然不是一项微不足道的工作投资。

只需在单独的线程上运行Form D的ShowDialog调用,就可以让对话框完全阻止任何线程。这需要相同类型的工作,但更少,因为你只有一个表单运行你的应用程序的主线程。

答案 2 :(得分:9)

您可以使用单独的线程(如下所示),但这会进入危险区域 - 如果您了解线程的影响(同步,跨线程访问等),您应该只接近此选项:

[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);
}

(显然,您实际上不会像上面那样构造代码 - 这只是显示行为的最短方式;在实际代码中,每个表单都有一个类等等。)

答案 3 :(得分:6)

我想总结可能的解决方案并添加一个新的替代方案(3a和3b)。但首先我要澄清我们在谈论的内容:

我们有一个有多种形式的应用程序。需要显示模态对话框,该对话框仅阻止我们表单的某些子集而不阻止其他表单。模态对话框可能只显示在一个子集(方案A)或多个子集(方案B)中。

现在总结可能的解决方案:

  1. 不要使用通过ShowDialog()显示的模式表单

    考虑应用程序的设计。你真的需要使用ShowDialog()方法吗?如果您不需要模态形式,那么这是最简单,最干净的方式。

    当然,这种解决方案并不总是合适的。 ShowDialog()为我们提供了一些功能。最值得注意的是它禁用了所有者(但不要灰显),用户无法与之交互。非常令人筋疲力尽的答案提供了P Daddy

  2. 模仿ShowDialog()行为

    可以模拟该mathod的行为。我再次建议阅读P Daddy's answer

    a)在Enabled 上使用 Form属性的组合,并通过Show()将表单显示为非模态。因此,禁用的表单将显示为灰色。但它是完全托管的解决方案,无需任何互操作。

    b)不要让父表格变灰?引用一些原生方法,关闭父表单上的WS_DISABLED(再次 - 请参阅P Daddy的回答)。

    这两个解决方案要求您可以完全控制需要处理的所有对话框。您必须使用特殊构造来显示"部分阻止对话框"不要忘记它。您需要调整逻辑,因为Show()是非阻止的,ShowDialog()正在阻止。处理系统对话框(文件选择器,颜色选择器等)可能是个问题。另一方面,表格上不需要任何额外的代码,不能被对话框阻止。

  3. 克服ShowDialog()

    的限制

    请注意,有Application.EnterThreadModalApplication.LeaveThreadModal个事件。只要显示模态对话框,就会引发此事件。请注意,事件实际上是线程范围的,而不是应用程序范围的。

    a)在表单中收听Application.EnterThreadModal事件,不得被对话框阻止,在这些表单中启用WS_DISABLED。您只需要调整不应被模态对话框阻止的表单。您可能还需要检查所显示的模态表单的父链,并根据此条件切换WS_DISABLED(在您的示例中,如果您还需要通过表单A和C打开对话框,但不要阻止表单B和d)。

    b)隐藏并重新显示不应被阻止的表单。请注意,在显示模式对话框后显示新表单时,不会阻止它。利用它,当显示模态对话框时,隐藏并再次显示所需的表单,以便它们不被阻止。然而,这种方法可能会带来一些闪烁。理论上可以通过启用/禁用Win API中的表单重绘来修复它,但我不能保证。

    c)在窗体上设置Owner属性为对话框窗体,在显示对话框时不应阻止。我没有测试过这个。

    d)使用多个GUI线程Answer from TheSmurf

答案 4 :(得分:4)

在FormA的新主题中启动FormB:

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

现在,使用ShowDialog()在新线程中打开的任何表单只会阻止FormB而不是FormA或FormC

答案 5 :(得分:4)

我只是想在这里添加我的解决方案,因为它似乎对我有用,并且可以封装到一个简单的扩展方法中。我唯一需要做的就是处理闪烁的@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);
}

答案 6 :(得分:3)

我在写作的应用程序中遇到了类似的问题。我的主UI是在主线程上运行的表单。我有一个帮助对话框,我想作为无模式对话框运行。这很容易实现,甚至可以确保我只运行一个帮助对话框实例。不幸的是,我使用的任何模态对话框都会导致帮助对话框失去焦点 - 当某些模态对话框运行时,有一个帮助对话框会有最大的帮助。

使用这里提到的想法,在其他地方,我设法克服了这个错误。

我在主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);
    ...
}

这可以确保线程在任何给定时间只有一个实例。线程本身运行对话框,并在对话框关闭后停止。由于它在单独的线程上运行,因此从主UI中创建模式对话框不会导致帮助对话框挂起。我确实需要添加

helpDialog.Abort();

到主UI的表单关闭事件,以确保在应用程序终止时关闭帮助对话框。

我现在有一个无模式的帮助对话框,它不受我的主UI中产生的任何模态对话框的影响,这正是我想要的。这是安全的,因为主UI和帮助对话框之间不需要通信。

答案 7 :(得分:2)

以下是帮助我在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
}

答案 8 :(得分:0)

也许一个子窗口(详见 ChildWindow )将是一个更优雅的解决方案,它可以避免GUI的单独线程的所有问题。

答案 9 :(得分:0)

使用示例:

(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));
    }
}

答案 10 :(得分:0)

我来到这个线程的目的是做类似的事情:拥有多个独立的父窗体,它们在同一进程中运行,每个窗体都有自己的模态对话框。我打算用一个比喻来证明它是可能的,它将是多个Word文档,每个文档都有自己的“查找和替换”窗口。

我确定我之前曾经做过此事,但是尝试一下,您知道吗,即使Microsoft Word也无法做到这一点。它的行为完全符合OP所述:打开模式对话框(如使用Ctrl + H进行查找和替换)后,所有Word文档都将被阻止并且无法与之交互。 更糟糕的是,仅单击原始父文档会导致模式对话框闪烁。其他人只是没有回应,也没有暗示他们为什么被阻止或需要关闭什么。

这确实可能是一个非常令人困惑的用户体验。我感到惊讶的是,微软自己的旗舰办公软件就是这种情况(而且我以前从未注意到过)。但是,这也帮助我以自己的方式适应了自己的应用程序。

进一步的确认来自.NET documentation本身,该状态表明“ ShowDialog显示了该窗口,并禁用了应用程序中的所有其他窗口 ,并且仅在关闭窗口时返回。”


我意识到从技术上讲这不是一个“解决方案”。那些真的觉得他们的应用程序需要支持完全独立的父窗体的人可以尝试workaround by @PDaddy,它看起来非常详尽。

但是,希望这会使那些像我一样只对自己的UI保持谨慎并认为自己做错了事的人感到安心。你不疯显然,这就是Windows的工作方式。