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

Jon*_*ury 48 c# showdialog winforms

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

P D*_*ddy 86

使用多个GUI线程是一件棘手的事情,如果这是你这样做的唯一动机,我会建议不要这样做.

一种更合适的方法是使用Show()代替ShowDialog()和禁用所有者表单,直到弹出窗体返回.只有四个注意事项:

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

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

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

    当您将表单的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));
    }
    
    Run Code Online (Sandbox Code Playgroud)
  3. ShowDialog()解除对话框之前,调用不会返回.这很方便,因为您可以暂停所有者表单中的逻辑,直到对话框完成其业务.这种Show()呼吁必然不会这样.因此,如果您要使用Show()而不是ShowDialog(),则需要将逻辑分为两部分.解除对话框后应该运行的代码(包括重新启用所有者表单)应该由Closed事件处理程序运行.

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

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

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;
    }
}
Run Code Online (Sandbox Code Playgroud)

  • 当您使用ShowDialog()打开表单,然后尝试单击父窗体时,子窗体闪烁并播放铃声.但是使用你的代码,它不会发生,即你不能聚焦父表单,是的,但是子表单不会闪烁,也不会播放声音.因此,新行为使用户感到困惑. (2认同)

Mar*_*ell 11

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

[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);
}
Run Code Online (Sandbox Code Playgroud)

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


The*_*urf 9

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

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

  • Jon T - 它没有必要,但你最好知道你在做什么,并希望2之间没有任何互动 - 听起来像是麻烦. (4认同)

Jus*_*ony 6

我只是想在这里添加我的解决方案,因为它似乎对我来说效果很好,并且可以封装成一个简单的扩展方法。我唯一需要做的就是处理 @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);
}
Run Code Online (Sandbox Code Playgroud)


man*_*cze 6

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

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

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

  1. 不要使用通过显示模式窗体ShowDialog()在所有

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

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

  2. 模仿ShowDialog()行为

    可以模拟该mathod的行为.我再次建议阅读P Daddy的答案.

    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线程.TheSmurf的回答.


Rob*_*les 5

在 FormA 的新线程中启动 FormB:

        (new System.Threading.Thread(()=> {
            (new FormB()).Show();
        })).Start();
Run Code Online (Sandbox Code Playgroud)

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