Отображение экрана ожидания в WPF - PullRequest
7 голосов
/ 06 марта 2009

Я пытаюсь отобразить диалоговое окно ожидания для длительной работы. Проблема в том, что это однопоточный, хотя я и заставляю 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, которую я скрываю и показываю.

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

Ответы [ 4 ]

17 голосов
/ 06 марта 2009

Делать это однопоточным действительно будет больно, и это никогда не сработает так, как вам бы хотелось. В конечном итоге окно в 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.Argument).

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

16 голосов
/ 06 марта 2009

Я нашел способ! Благодаря этой теме .

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

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

4 голосов
/ 18 сентября 2013

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

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

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

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

Примечание: Причина выбора Application.Current.Dispatcher, но Dispatcher.CurrentDispatcher объясняется здесь .

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

Для этого WinForms разработчики выполнили Application.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) лучше здесь . ИМХО вариант № 1 все же лучше, чем № 2.

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

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

Загрузка WpfLoadingOverlay.zip из этого github (это был пример из статьи " Отзывчивость 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 голосов
/ 06 марта 2009

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

yield return 30;

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

Это описано здесь более подробно, с примером кода.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...