Отображение экрана ожидания в 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, которую я скрываю и показываю.
обновление: я действительно не хочу использовать фоновый работник, если я не должен. В коде есть несколько мест, где произойдет этот запуск и остановка.
5 ответов
Я нашел путь! Благодаря этой теме.
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);
}
Эта функция должна вызываться непосредственно перед длительной операцией. Это заставит поток пользовательского интерфейса обновляться.
Делать это однопоточно действительно будет больно, и это никогда не будет работать так, как вы хотели бы. В конечном итоге окно в 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).
Это действительно самый простой способ, вместо того, чтобы пытаться сделать его однопоточным.
Проверьте мое всестороннее исследование этой очень деликатной темы. Если вы ничего не можете сделать для улучшения фактической производительности, у вас есть следующие опции для отображения ожидающего сообщения:
Вариант № 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();
}
}
Другой вариант - написать свою долгосрочную подпрограмму как функцию, которая возвращает IEnumerable<double>
чтобы указать прогресс, и просто сказать:
yield return 30;
Это указывало бы, например, на 30% пути. Затем вы можете использовать таймер WPF для его запуска в фоновом режиме в качестве сопрограммы.
Короткий ответ
Упрощая предыдущие ответы, вы можете просто создать такую задачу с ОЧЕНЬ НЕБОЛЬШИМ изменением кода.
private void Button_Click(object sender, RoutedEventArgs e)
{
this.Cursor = System.Windows.Input.Cursors.Pen;
WaitScreen.Visibility = Visibility.Visible;
Task.Factory.StartnNew(()=>{
// Do something long here
for (Int32 i = 0; i < 100000000; i++)
{
String s = i.ToString();
}
}).ContinueWith(()=>{
WaitScreen.Visibility = Visibility.Collapsed;
this.Cursor = System.Windows.Input.Cursors.Arrow;
}, TaskScheduler.FromCurrentSynchronizationContext());
}
Масштабируемый ответ
Если вы хотите, чтобы он был более масштабируемым, вы можете создатьRunLongTask(Action action)
метод:
private void RunLongTask(Action action)
{
IsBusy = true;
Task.Factory.StartnNew(action).ContinueWith(()=>{
IsBusy = false;
}, TaskScheduler.FromCurrentSynchronizationContext());
}
Где можно связатьIsBusy
к вашим свойствам управления окном, как некоторыеIsEnabled
илиVisibility
свойство. (Для наглядности нужен конвертер, который я добавлю в конце ответа)
<Grid Name="OverlayGrid" Visibility={Binding IsBusy, Converter={local:BoolVisibilityCollapseConverter}}">...</Grid>
<!-- where local is defined at the Window element, referring to the namespace where you created the converter code -->
Затем вы используете его как:
RunLongTask(SomeParameterlessMethodName);
или
RunLongTask(()=>{
//long
//long
//long
});
Преобразователи, используемые
using System.Globalization;
using System.Windows;
using System.Windows.Data;
using System.Windows.Markup;
[ValueConversion(typeof(bool), typeof(Visibility))]
class BoolVisibilityCollapseConverter : MarkupExtension, IValueConverter
{
//convert from source to target
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
bool v = (bool)value;
if (v)
return Visibility.Visible;
else
return Visibility.Collapsed;
}
//convert from target to source
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new InvalidOperationException("BoolVisibilityHideConverter is intended to be bound one way from source to target");
}
public override object ProvideValue(IServiceProvider serviceProvider)
{
return this;
}
}
[ValueConversion(typeof(bool), typeof(Visibility))]
class InverseBoolVisibilityCollapseConverter : MarkupExtension, IValueConverter
{
//convert from source to target
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
bool v = !(bool)value;
if (v)
return Visibility.Visible;
else
return Visibility.Collapsed;
}
//convert from target to source
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new InvalidOperationException("InverseBoolVisibilityCollapseConverter is intended to be bound one way from source to target");
}
public override object ProvideValue(IServiceProvider serviceProvider)
{
return this;
}
}