Отображение экрана "Wait" в WPF

Я пытаюсь отобразить диалоговое окно ожидания для длительной работы. Проблема в том, что это однопоточный, хотя я говорю WaitScreen отображать его никогда не делает. Есть ли способ изменить видимость этого экрана и заставить его немедленно отображаться? В качестве примера я включил вызов Курсора. Сразу после того, как я позвоню.Курсор, курсор обновляется немедленно. Именно такого поведения я и хочу.

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-индексом из 99, которые я скрываю и показываю.

update: я действительно не хочу использовать фонового работника, если мне не нужно. В коде есть несколько мест, где будет происходить этот запуск и остановка.

4 ответов


делать это однопоточным действительно будет больно, и это никогда не будет работать так, как вам хотелось бы. Окно в конечном итоге станет черным в 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). Вы также можете передать параметр RunWorkerAsync, если вашим методам DoWork нужен объект (доступный в e.Аргумент).

это действительно самый простой способ, а не пытаться сделать это отдельно.


Я нашел способ! Спасибо этой теме.

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);
}

эта функция должна вызываться непосредственно перед длительной операцией. Это заставит поток пользовательского интерфейса обновиться.


проверьте мои всесторонние исследования этой очень деликатной темы. Если вы ничего не можете сделать, чтобы улучшить фактическую производительность, у вас есть следующие параметры для отображения ожидающего сообщения:

#1 выполнить код для синхронного отображения ожидающего сообщения в том же методе, который выполняет реальную задачу. Просто поставьте эту строку перед длительным процессом:

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

он будет обрабатывать ожидающие сообщения в главном потоке диспетчера в конце Invoke ().

Примечание: Причина выбора приложения.Текущий.Диспетчер, но диспетчер.CurrentDispatcher объясняется здесь.

#2 отображение экрана "ожидание" и обновление пользовательского интерфейса (обработка ожидающих сообщений).

для этого WinForms разработчики выполнили приложение.Функция doevents метод. WPF предлагает две альтернативы для достижения аналогичного результаты:

#2.1 С помощью класс DispatcherFrame.

Проверьте немного громоздкий пример из 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) лучше здесь. Вариант IMHO #1 по-прежнему лучше, чем #2.

#3 отображение ожидающего сообщения в отдельном окно.

это удобно, когда вы отображаете не простое ожидающее сообщение, а анимацию. Визуализация анимации загрузки в то же время, когда мы ждем завершения другой длительной операции рендеринга, является проблемой. В принципе, нам нужно два потока рендеринга. Вы не можете иметь несколько потоков рендеринга в одном окне, но вы можете поместить свою анимацию загрузки в новое окно с собственным потоком рендеринга и сделать его похожим на отдельный окно.

скачать WpfLoadingOverlay.zip С этот github (это был образец из статьи"WPF Responsiveness: асинхронная анимация загрузки во время рендеринга", но я больше не могу найти его в Интернете) или посмотрите на основную идею ниже:

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();
    }
}

другой вариант-написать свою длительную процедуру как функцию, которая возвращает IEnumerable<double> чтобы указать прогресс, и просто сказать:

yield return 30;

это будет указывать на 30% пути через, например. Затем вы можете использовать таймер WPF для его выполнения в "фоновом режиме" в качестве совместной корутины.

это описано в некоторых деталях здесь, с примерами кода.