[Winform]在Form里显示模态对话框ModalDialog

发布时间 2023-12-28 14:59:09作者: Echo_HR910

在Form里显示模态Dialog

问题

如何在WinForm的一个Form里面弹出一个模态Dialog?

背景

程序的框架是Winform,只有一个窗口MainForm。MainForm里面是一个TabControl,每个TabPage是一个Form,每个TabPage的Form相互独立,互不干扰,TabPage间可以随时切换。由于有某些需求,TabPage需要接受用户输入,并等待输入完成,才能执行后面的代码,此时,程序是需要阻塞等待输入的,所以需要弹出一个模态Dialog。

  1. 为什么不用MessageBox呢?因为MessageBox是直接弹出一个模态对话框且该对话框是一个新的窗口,这时候整个MainForm是伪阻塞状态,用户无法通过与MainForm的其他区域交互,包括点击标签页切换到其他TabPage。所以,我需要该对画框只在Form里显示。
  2. 为什么不用MDI呢? 最主要的原因是TabControl里的Form,其TopLevel属性是false的,如果想在Form里面添加MDI窗口,需要将Form的TopLevel属性设置为true,这时我将无法使用TabControl。

代码实现

创建一个CustomDialog类,继承Form

public class CustomDialog : Form{

}

创建CustomDialog成员变量

  1. 这里使用到了两个类, PanelControlContainer。其中Panel充当CustomDialog的容器。ControlContainer则是Panel的容器。
public class CustomDialog : Form{
    private Panel? _panelContainer;
    private ControlContainer? _parentContainer;
    private Form? _parentForm;
    // 声明Panel,ControlContainer和Form
}

定义一个ShowDialog方法

要显示模态Dialog,当然要是实现ShowDialog方法啦!这里定义了一个ShowDialog方法,和其他ShowDialog方法有些许不同,该方法的参数是ControlContainer类型, 用于接收一个控件作为父控件

public class CustomDialog : Form{
    public void ShowDialog(ControlContainer parentControl){
        //TODO
    }

设置CustomDialog.PaneContainer的属性和内容

这部分代码最主要实现了CustomDialog在它的父控件Form中显示的功能,PS:有点简单粗暴,但是有效(_)

public class CustomDialog : Form{
    private void AddDialogToTheView(){
        if(ContainerControl is null){
            throw new NullReferenceException(nameof(_parentContainer));
        }
        //panel的高度
        int panelHeight = 350;
        int panelWidth = 500;
        
        //panel显示的位置
        int startUpLocationX = (_parentContainer.ClientSize.Width - panelWidth) / 2;
        int startUpLocationY = (_parentContainer.ClientSize.Height - panelHeight) / 2;
        
        // 设置_panelContainer的属性
        _panelContainer = new Panel(){
            Height = panelHeight,
            Width = panelWidth,
            Location = new Point(startUpLocationX, startUpLocationY),
        };

        // 设置Dialog的属性
        TopLevel = false;
        DockStyle = DockStyle.Fill;
        //添加进Panel里面
        _panelContainer.Controls.Add(this);
        Contianer.Controls.Add(_panelContainer);
        // 显示Dialog
        Show();
        PanelControl.BringToFront();
        
    }

}

实现伪阻塞

要说实现这个CustomDialog哪里最难,应该是这个伪阻塞功能最难。前面的View相关的方案,一般人稍微思考一下都可以想出来。但是想优雅的实现CustomDialog的伪阻塞功能,确实不易

  • 如何阻塞一段代码?
    我最初的做法是这样的:
public void WaitForExit(Cancellationtoken token){
    while(!toke.IsCancellationRequested){
        Application.DoEvents();
    }
}
CancellationTokenSource source = new CancellationTokenSource();
WaitForExit(source.Token);

//user cancel
source.Cancel();

这个写法有效,但还是不够优雅 ? ps:切记请勿在UI线程中直接使用while(true){}

  • 最后我的写法是这样的:
    在这里我使用了一个Winform中默认没有的命名空间:System.Windows.Threading
    在csproj里开启wpf的命名空间
<enableWpf>true</enableWpf>

这里其实是借鉴了wpf的模态Dialog的实现方式,具体可以参考wpf的源码;有现成的轮子?直接偷!?

public class CustomDialog : Form{
    private DispatcherFrame? CurrentDispatcherFrame;
    private void WaitForExit(){
        try{
            
        ComponentDispatcher.PushModal();
        CurrentDispatcherFrame = new DispatcherFrame(true);
        Dispatcher.PushFrame(CurrentDispatcherFrame);
        }
        finally{
            ComponentDispatcher.PopModal();
        }
    }
}

当调用WaitForExit()方法后,程序就进入伪阻塞状态了,此时UI线程仍然能绘制UI;直到调用CurrentDispatcherFrame?.Continue = false;,WaitForExit才会退出伪阻塞状态。

细节优化

  • 这个时候,整个CustomDialog的大体实现基本完成了,下一步就是优化细节

重写Form的Closed事件

当调用CustomDialog的Close()方法时,会触发Form.OnClosed事件,此时阻塞状态将会退出

protected override void OnClosed(EventArgs e){
    base.OnClosed(e);
    if(CurrentDispatcherFrame is not null){
        CurrentDispatcherFrame.Continue = false;
        CurrentDispatcherFrame = null;
    }
}

在ParentForm中注册关闭事件

在CustomDialog弹出的状态,如果用户想退出程序,点击MainForm的关闭按钮,此时是关闭不了的。MainForm是需要等CustomDialog关闭后才能关闭的,而CustomDialog需要等待用户关闭才能关闭。此时需要将MainForm的关闭事件注册到CustomDialog的关闭事件上。

  1. ParentForm_Closing事件
private void ParentForm_Closing(object? sender, CancelEventArgs e){
    this.Close();//ParentForm关闭时,关闭CustomDialog
}
  1. ParentForm订阅关闭事件
public void ShowDialog(ContainerControl _parentContainer){

    if(_parentContainer is Form containerForm && containerForm.TopLevel){
        this.Owner = containerForm;
    }
    _parentForm = _parentContainer.ParentForm;
    _parentForm.Closing += ParentForm_Closing;//订阅关闭事件
    
    //TO DO
}

重写Form的Closing事件

订阅了closing事件记得也要取消订阅

protected override void OnClosing(CancelEventArgs e){
    base.OnClosing(e);
    if(_parentForm is not null){
        _parentForm.Closing -= ParentForm_Closing;
    }
}

完整的ShowDialog方法

public IAsyncResult ShowDialogAsync(ContainerControl _parentContainer){
    var asyncResult = _parentContainer.BeginInvoke(new Action(() => 
    {
        if(_parentContainer is Form containerForm && containerForm.TopLevel){
            this.Owner = containerForm;
        }
        _parentForm = _parentContainer.ParentForm;
        _parentForm.Closing += ParentForm_Closing;//订阅关闭事件

        AddDialogToTheView(); //已完成
        WaitForExit(); //已完成
        RemoveTheDialogFromTheView();//TODO 这里懒得写了
    }));
    return asyncResult;
    }

同步方法

public void ShowDialog(ContainerControl _parentContainer){
    var asyncResult = ShowDialogAsync(_parentContainer);
    asyncResult.AsyncWaitHandle.WaitOne();
}

结语

  • 至此,CustomDialog已经可以使用了。定制的DialogForm,只需要继承CustomDialog即可。其他交互逻辑在子类中实现即可

其他细节

当Form改变的时候,自动调整CustomDialog到Form中间:向_parentContainer订阅SizeChanged事件

protected void _parentContainer_SizeChanged(object sender, EventArgs e){
    if (sender is not ContainerControl control || _containerPanel is null)
    {
        return;
    }

    _panelContainer.Location = new Point((control.ClientSize.Width - _containerPanel.Width) / 2, (control.ClientSize.Height - _containerPanel.Height) / 2);
}