如果这个问题得到了很多次的回答,我很抱歉,但我似乎无法找到适合我的答案。我想创建一个模态窗口,在我的应用程序执行长时间运行的任务时显示各种进度消息。这些任务在一个单独的线程上运行,我可以在进程的不同阶段更新进度窗口上的文本。跨线程通信都很好地工作。问题是我不能让窗口只在其他应用程序窗口(不是计算机上的每个应用程序)之上,保持最佳状态,阻止与父窗口的交互,并仍然允许工作继续。
这是我迄今为止所做的尝试:
首先,我的启动窗口是一个自定义类,它扩展了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()
的帖子。我尝试了这个,它正确地显示了对话框并且不允许我单击父窗口,但调用.ShowDialog()
会使程序挂起,等待您关闭对话框,然后它将继续运行代码。这显然不是我想要的。我想我可以在另一个线程上调用ShowDialog()
,这样该线程就会挂起但执行工作的线程不会......是推荐的方法吗?
我还考虑过根本不使用窗口的可能性,而是将一个完整大小的窗口元素放在页面上的其他内容之前。这可以工作,除了我打开其他窗口,我也希望能够在打开时使用启动画面。如果我使用了一个窗口元素,我将不得不在每个窗口重新创建它,而且我不能在我的自定义初始化类中使用我方便的UpdateSplashText
方法。
所以这让我想到了这个问题。处理这个问题的正确方法是什么?
感谢您抽出宝贵时间,感谢抱歉,但细节很重要:)
答案 0 :(得分:15)
您是正确的ShowDialog
为您提供了所需的大部分UI行为。
确实存在这样的问题,即只要你调用它就可以阻止执行。如何在显示表单后运行一些代码,但在显示之前定义它应该是什么?那是你的问题。
你可以在splash类中完成所有工作,但由于紧密耦合,这种做法相当糟糕。
您可以利用Loaded
Window
事件来定义在显示窗口后应该运行的代码,但是在显示窗口之前定义它的位置。
public static void DoWorkWithModal(Action<IProgress<string>> work)
{
SplashWindow splash = new SplashWindow();
splash.Loaded += (_, args) =>
{
BackgroundWorker worker = new BackgroundWorker();
Progress<string> progress = new Progress<string>(
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");
});
}
答案 1 :(得分:1)
您可以让进度窗口的构造函数采用Task
,然后确保task.Start
事件上的窗口调用OnLoaded
。然后使用父表单中的ShowDialog
,这将导致进度窗口启动任务。
请注意,您也可以在构建函数中调用task.Start
,或在调用ShowDialog
之前在父表单中调用{{1}}。无论哪个对你最有意义。
另一个选择就是在主窗口的状态栏中使用进度条,然后摆脱弹出窗口。如今,这种选择似乎越来越普遍。
答案 2 :(得分:1)
您可以使用Visibility
上的Window
属性在启动画面运行时隐藏整个窗口。
XAML
<Window ... Name="window" />
代码
window.Visibility = System.Windows.Visibility.Hidden;
//show splash
//do work
//end splash
window.Visibility = System.Windows.Visibility.Visible;
答案 3 :(得分:0)
@Servy接受的答案对我帮助很大!我想用async
和MVVM方法分享我的版本。它还包含一个小的延迟,以避免窗口闪烁&#34;因为操作太快。
对话方式:
public static async void ShowModal(Func<IProgress<string>, 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<string> progress = new Progress<string>(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,为我节省了很多时间。
答案 4 :(得分:-1)
我找到了一种方法,通过在一个单独的线程上调用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();
}
}