[Winform]在Form里顯示模態對話框ModalDialog
問題
如何在WinForm的一個Form里面彈出一個模態Dialog?
背景
程序的框架是Winform,只有一個窗口MainForm。MainForm里面是一個TabControl,每個TabPage是一個Form,每個TabPage的Form相互獨立,互不干擾,TabPage間可以隨時切換。由于有某些需求,TabPage需要接受用戶輸入,并等待輸入完成,才能執行后面的代碼,此時,程序是需要阻塞等待輸入的,所以需要彈出一個模態Dialog。
- 為什么不用MessageBox呢?因為MessageBox是直接彈出一個模態對話框且該對話框是一個新的窗口,這時候整個MainForm是偽阻塞狀態,用戶無法通過與MainForm的其他區域交互,包括點擊標簽頁切換到其他TabPage。所以,我需要該對畫框只在Form里顯示。
- 為什么不用MDI呢? 最主要的原因是TabControl里的Form,其TopLevel屬性是false的,如果想在Form里面添加MDI窗口,需要將Form的TopLevel屬性設置為true,這時我將無法使用TabControl。
代碼實現
創建一個CustomDialog類,繼承Form類
public class CustomDialog : Form{
}
創建CustomDialog成員變量
- 這里使用到了兩個類,
Panel和ControlContainer。其中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的關閉事件上。
ParentForm_Closing事件
private void ParentForm_Closing(object? sender, CancelEventArgs e){
this.Close();//ParentForm關閉時,關閉CustomDialog
}
- 向
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);
}

浙公網安備 33010602011771號