WPF模式进度窗口

如果这个问题得到了很多次回答,我很抱歉,但我似乎无法找到适合我的答案。 我想创建一个模态窗口,在我的应用程序执行长时间运行的任务时显示各种进度消息。 这些任务在一个单独的线程上运行,我可以在进程的不同阶段更新进度窗口上的文本。 跨线程通信都很好地工作。 问题是我不能让窗口只在其他应用程序窗口(不是计算机上的每个应用程序)之上,保持在最顶层,阻止与父窗口的交互,并仍然允许工作继续。

这是我到目前为止所尝试的内容:

首先,我的启动窗口是一个自定义类,它扩展了Window类,并具有更新消息框的方法。 我在早期创建了一个新的splash类实例,并根据需要显示/隐藏它。

在最简单的情况下,我实例化窗口并在其上调用.Show()

 //from inside my secondary thread this._splash.Dispatcher.Invoke(new Action(() => this._splash.Show()); //Do things //update splash text //Do more things //close the splash when done this._splash.Dispatcher.Invoke(new Action(() => this._splash.Hide()); 

这会正确显示窗口并继续运行我的代码来处理初始化任务,但它允许我单击父窗口并将其带到前面。

接下来我尝试禁用主窗口并在以后重新启用:

 Application.Current.Dispatcher.Invoke(new Action(() => this.MainWindow.IsEnabled = false)); //show splash, do things, etc Application.Current.Dispatcher.Invoke(new Action(() => this.MainWindow.IsEnabled = true)); 

这会禁用窗口中的所有元素,但我仍然可以单击主窗口并将其放在启动屏幕前面,这不是我想要的。

接下来,我尝试使用启动窗口中的最顶层属性。 这使得它保持在所有内容之前,并且与设置主窗口IsEnabled属性一起,我可以阻止交互,但这使得启动屏幕出现在一切,包括其他应用程序。 我也不想要那个。 我只是希望它成为这个应用程序中最顶层的窗口。

然后我找到了关于使用.ShowDialog()而不是.Show()post。 我试过这个,它正确地显示了对话框并且不允许我点击父窗口,但是调用.ShowDialog()使程序挂起等待你关闭对话框,然后它将继续运行代码。 这显然不是我想要的。 我想我可以在一个不同的线程上调用ShowDialog() ,这样该线程就会挂起但执行工作的线程不会……是推荐的方法吗?

我还考虑过根本不使用窗口的可能性,而是将一个完整大小的窗口元素放在页面上的其他内容之前。 这可以工作,除了我打开其他窗口,我希望能够在那些打开时使用启动画面。 如果我使用了一个窗口元素,我将不得不在每个窗口上重新创建它,我将无法在我的自定义splash类中使用我方便的UpdateSplashText方法。

所以这让我想到了这个问题。 处理这个问题的正确方法是什么?

感谢您的时间和对不起的长期问题,但细节很重要:)

您是正确的, ShowDialog为您提供了所需的大部分UI行为。

它的问题是,只要你调用它就可以阻止执行。 在显示表单后,你怎么可能运行一些代码,但在它显示之前定义它应该是什么? 那是你的问题。

您可以在splash类中完成所有工作,但由于紧耦合,这种做法相当糟糕。

你可以做的是利用WindowLoaded事件来定义在显示窗口后应该运行的代码,但是在显示它之前定义它的位置。

 public static void DoWorkWithModal(Action> work) { SplashWindow splash = new SplashWindow(); splash.Loaded += (_, args) => { BackgroundWorker worker = new BackgroundWorker(); Progress progress = new Progress( data => splash.Text = data); worker.DoWork += (s, workerArgs) => work(progress); worker.RunWorkerCompleted += (s, workerArgs) => splash.Close(); worker.RunWorkerAsync(); }; splash.ShowDialog(); } 

请注意,此方法旨在将样板代码封装在此处,以便您可以传入任何接受进度指示器的工作方法,并且它将在后台线程中执行该工作,同时显示具有从工作人员指示的进度的通用启动屏幕。

然后可以这样调用:

 public void Foo() { DoWorkWithModal(progress => { Thread.Sleep(5000);//placeholder for real work; progress.Report("Finished First Task"); Thread.Sleep(5000);//placeholder for real work; progress.Report("Finished Second Task"); Thread.Sleep(5000);//placeholder for real work; progress.Report("Finished Third Task"); }); } 

您可以使用Window上的Visibility属性在启动屏幕运行时隐藏整个窗口。

XAML

  

 window.Visibility = System.Windows.Visibility.Hidden; //show splash //do work //end splash window.Visibility = System.Windows.Visibility.Visible; 

您可以让进度窗口的构造函数接受一个Task ,然后确保窗口调用OnLoaded事件上的OnLoaded 。 然后从父窗体使用ShowDialog ,这将导致进度窗口启动任务。

请注意,您也可以在构造函数中调用task.Start ,或者在调用ShowDialog之前在任何位置调用父窗体。 无论哪个对你最有意义。

另一个选择就是在主窗口的状态条中使用进度条,然后摆脱弹出窗口。 如今,这种选择似乎越来越普遍。

@Servy接受的答案对我帮助很大! 我想用async和MVVM方法分享我的版本。 它还包含一个小延迟,以避免太快操作的“窗口闪烁”。

对话方法:

 public static async void ShowModal(Func, Task> workAsync, string title = null, TimeSpan? waitTimeDialogShow = null) { if (!waitTimeDialogShow.HasValue) { waitTimeDialogShow = TimeSpan.FromMilliseconds(300); } var progressWindow = new ProgressWindow(); progressWindow.Owner = Application.Current.MainWindow; var viewModel = progressWindow.DataContext as ProgressWindowViewModel; Progress progress = new Progress(text => viewModel.Text = text); if(!string.IsNullOrEmpty(title)) { viewModel.Title = title; } var workingTask = workAsync(progress); progressWindow.Loaded += async (s, e) => { await workingTask; progressWindow.Close(); }; await Task.Delay((int)waitTimeDialogShow.Value.TotalMilliseconds); if (!workingTask.IsCompleted && !workingTask.IsFaulted) { progressWindow.ShowDialog(); } } 

用法:

  ShowModal(async progress => { await Task.Delay(5000); // Task 1 progress.Report("Finished first task"); await Task.Delay(5000); // Task 2 progress.Report("Finished second task"); }); 

再次感谢@Servy,为我节省了很多时间。

我找到了一种方法来通过在一个单独的线程上调用ShowDialog()来完成这项工作。 我在对话框类中创建了自己的ShowMe()HideMe()方法来处理工作。 我还捕获Closing事件以防止关闭对话框,以便我可以重复使用它。

这是我的启动画面类的代码:

 public partial class StartupSplash : Window { private Thread _showHideThread; public StartupSplash() { InitializeComponent(); this.Closing += OnCloseDialog; } public string Message { get { return this.lb_progress.Content.ToString(); } set { if (Application.Current.Dispatcher.Thread == System.Threading.Thread.CurrentThread) this.lb_progress.Content = value; else this.lb_progress.Dispatcher.Invoke(new Action(() => this.lb_progress.Content = value)); } } public void ShowMe() { _showHideThread = new Thread(new ParameterizedThreadStart(doShowHideDialog)); _showHideThread.Start(true); } public void HideMe() { //_showHideThread.Start(false); this.doShowHideDialog(false); } private void doShowHideDialog(object param) { bool show = (bool)param; if (show) { if (Application.Current.Dispatcher.Thread == System.Threading.Thread.CurrentThread) this.ShowDialog(); else Application.Current.Dispatcher.Invoke(new Action(() => this.ShowDialog())); } else { if (Application.Current.Dispatcher.Thread == System.Threading.Thread.CurrentThread) this.Close(); else Application.Current.Dispatcher.Invoke(new Action(() => this.Close())); } } private void OnCloseDialog(object sender, CancelEventArgs e) { e.Cancel = true; this.Hide(); } }