在WPF中显示“等待”屏幕

时间:2009-03-05 21:08:39

标签: c# .net wpf multithreading xaml

我正在尝试为长时间运行的操作显示请等待对话框。问题是因为这是单线程,即使我告诉WaitScreen显示它永远不会。有没有办法可以更改该屏幕的可见性并立即显示?我将Cursor调用作为示例包含在内。在我调用this.Cursor之后,光标立即更新。这正是我想要的行为。

private void Button_Click(object sender, RoutedEventArgs e)
{
  this.Cursor = System.Windows.Input.Cursors.Pen;
  WaitScreen.Visibility = Visibility.Visible;

  // Do something long here
  for (Int32 i = 0; i < 100000000; i++)
  {
    String s = i.ToString();
  }

  WaitScreen.Visibility = Visibility.Collapsed;
  this.Cursor = System.Windows.Input.Cursors.Arrow; 
}

WaitScreen只是一个Z-index为99的网格,我隐藏并显示。

更新:我真的不想使用后台工作者,除非我必须这样做。代码中有许多地方会出现这种启动和停止。

4 个答案:

答案 0 :(得分:17)

单线程做它真的会很痛苦,它永远不会像你想的那样工作。该窗口最终将在WPF中变黑,程序将更改为“无响应”。

我建议使用BackgroundWorker来完成长时间运行的任务。

这并不复杂。这样的事情会起作用。

private void DoWork(object sender, DoWorkEventArgs e)
{
    //Do the long running process
}

private void WorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
    //Hide your wait dialog
}

private void StartWork()
{
   //Show your wait dialog
   BackgroundWorker worker = new BackgroundWorker();
   worker.DoWork += DoWork;
   worker.RunWorkerCompleted += WorkerCompleted;
   worker.RunWorkerAsync();
}

然后,您可以查看ProgressChanged事件以显示进度(如果您愿意,请记住将WorkerReportsProgress设置为true)。如果您的DoWork方法需要一个对象(在e.Argument中可用),您也可以将参数传递给RunWorkerAsync。

这实际上是最简单的方法,而不是单独尝试单线程。

答案 1 :(得分:16)

我找到了办法!感谢this thread

public static void ForceUIToUpdate()
{
  DispatcherFrame frame = new DispatcherFrame();

  Dispatcher.CurrentDispatcher.BeginInvoke(DispatcherPriority.Render, new DispatcherOperationCallback(delegate(object parameter)
  {
    frame.Continue = false;
    return null;
  }), null);

  Dispatcher.PushFrame(frame);
}

需要在长时间运行之前调用该函数。那将强制UI线程更新。

答案 2 :(得分:4)

查看我对这个非常微妙主题的全面研究。如果您无法改善实际性能,则可以使用以下选项显示等待消息:

选项#1 执行代码以与执行实际任务相同的方法同步显示等待消息。只需将这一行放在一个漫长的过程之前:

Dispatcher.CurrentDispatcher.Invoke(DispatcherPriority.Normal, (Action)(() => { /* Your code to display a waiting message */ }));

它将在 Invoke()结束时处理主调度程序线程上的待处理消息。

注意:选择Application.Current.Dispatcher的原因,但解释了Dispatcher.CurrentDispatcher here

选项#2 显示“等待”屏幕并更新UI(处理待处理消息)。

要做 WinForms 开发人员执行Application.DoEvents方法。 WPF提供了两种替代方案来实现类似的结果:

选项#2.1 使用DispatcherFrame class

MSDN

中查看一个有点笨重的例子
[SecurityPermissionAttribute(SecurityAction.Demand, Flags = SecurityPermissionFlag.UnmanagedCode)]
public void DoEvents()
{
    DispatcherFrame frame = new DispatcherFrame();
    Dispatcher.CurrentDispatcher.BeginInvoke(DispatcherPriority.Background, new DispatcherOperationCallback(ExitFrame), frame);
    Dispatcher.PushFrame(frame);
}

public object ExitFrame(object f)
{
    ((DispatcherFrame)f).Continue = false;
    return null;
}

选项#2.2 调用空操作

Dispatcher.CurrentDispatcher.Invoke(DispatcherPriority.Background, (Action)(() => { }));

参见讨论哪一个(2.1或2.2)更好here。恕我直言选项#1仍然优于#2。

选项#3 在单独的窗口中显示等待消息。

当您显示的不是简单的等待消息,而是动画时,它会派上用场。在我们等待另一个长渲染操作完成的同时渲染加载动画是一个问题。基本上,我们需要两个渲染线程。您不能在一个窗口中拥有多个渲染线程,但是您可以使用自己的渲染线程将加载动画放在一个新窗口中,并使其看起来不像是一个单独的窗口。

this github下载 WpfLoadingOverlay.zip (这是文章“ WPF响应:呈现期间的异步加载动画”的示例,但我不能在网上找到它)或者看看下面的主要想法:

public partial class LoadingOverlayWindow : Window
{
    /// <summary>
    ///     Launches a loading window in its own UI thread and positions it over <c>overlayedElement</c>.
    /// </summary>
    /// <param name="overlayedElement"> An element for overlaying by the waiting form/message </param>
    /// <returns> A reference to the created window </returns>
    public static LoadingOverlayWindow CreateAsync(FrameworkElement overlayedElement)
    {
        // Get the coordinates where the loading overlay should be shown
        var locationFromScreen = overlayedElement.PointToScreen(new Point(0, 0));

        // Launch window in its own thread with a specific size and position
        var windowThread = new Thread(() =>
            {
                var window = new LoadingOverlayWindow
                    {
                        Left = locationFromScreen.X,
                        Top = locationFromScreen.Y,
                        Width = overlayedElement.ActualWidth,
                        Height = overlayedElement.ActualHeight
                    };
                window.Show();
                window.Closed += window.OnWindowClosed;
                Dispatcher.Run();
            });
        windowThread.SetApartmentState(ApartmentState.STA);
        windowThread.Start();

        // Wait until the new thread has created the window
        while (windowLauncher.Window == null) {}

        // The window has been created, so return a reference to it
        return windowLauncher.Window;
    }

    public LoadingOverlayWindow()
    {
        InitializeComponent();
    }

    private void OnWindowClosed(object sender, EventArgs args)
    {
        Dispatcher.InvokeShutdown();
    }
}

答案 3 :(得分:3)

另一种选择是将长时间运行的例程编写为返回IEnumerable<double>以指示进度的函数,然后说:

yield return 30;
例如,这将表明30%的通过时间。然后,您可以使用WPF计时器在“背景”中执行它作为合作协程。

It's described in some detail here, with sample code.