Время от времени в приложении появляются "долгие операции" во время которых интерфейс тормозит и пользователь не понимает что происходит с приложением. Обычно такие операции выносятся в фоновый поток, а в основном потоке приложения показываем прогресс выполнения работы или некую анимацию дающую понять, что приложение не повисло. Но что делать, если работа выполняется в основном потоке и вынести ее в фоновый нельзя (например, идет чтение из визуальных компонентов)? Вот об этом и поговорим под катом.
Итак еще раз задача. В приложении есть "длительная операция", которая выполняется в потоке интерфейса и нужно пользователю показать анимацию, чтобы он не волновался что все пропало.
Для показа анимации я добавил вот такое окно в приложение:
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WpfApplication1"
mc:Ignorable="d"
Title="BusyWindow" Height="300" Width="300" WindowStyle="None" Background="Transparent" AllowsTransparency="True">
<Grid>
<Grid Background="Transparent" HorizontalAlignment="Center" VerticalAlignment="Center">
<Canvas RenderTransformOrigin="0.5,0.5" HorizontalAlignment="Center" VerticalAlignment="Center" Width="50" Height="50" >
<Ellipse Width="10" Height="10" Canvas.Left="20" Canvas.Top="0" Stretch="Fill" Fill="Green" Opacity="1.0"/>
<Ellipse Width="10" Height="10" Canvas.Left="40" Canvas.Top="20" Stretch="Fill" Fill="Green" Opacity="0.1"/>
<Ellipse Width="10" Height="10" Canvas.Left="20" Canvas.Top="40" Stretch="Fill" Fill="Green" Opacity="0.4"/>
<Ellipse Width="10" Height="10" Canvas.Left="0" Canvas.Top="20" Stretch="Fill" Fill="Green" Opacity="0.7"/>
<Canvas.RenderTransform>
<RotateTransform Angle="0" />
</Canvas.RenderTransform>
</Canvas>
<Canvas RenderTransformOrigin="0.5,0.5" HorizontalAlignment="Center" VerticalAlignment="Center" Width="50" Height="50" >
<Ellipse Width="10" Height="10" Canvas.Left="20" Canvas.Top="0" Stretch="Fill" Fill="Green" Opacity="0.01"/>
<Ellipse Width="10" Height="10" Canvas.Left="40" Canvas.Top="20" Stretch="Fill" Fill="Green" Opacity="0.2"/>
<Ellipse Width="10" Height="10" Canvas.Left="20" Canvas.Top="40" Stretch="Fill" Fill="Green" Opacity="0.5"/>
<Ellipse Width="10" Height="10" Canvas.Left="0" Canvas.Top="20" Stretch="Fill" Fill="Green" Opacity="0.8"/>
<Canvas.RenderTransform>
<RotateTransform Angle="30" />
</Canvas.RenderTransform>
</Canvas>
<Canvas RenderTransformOrigin="0.5,0.5" HorizontalAlignment="Center" VerticalAlignment="Center" Width="50" Height="50" >
<Ellipse Width="10" Height="10" Canvas.Left="20" Canvas.Top="0" Stretch="Fill" Fill="Green" Opacity="0.05"/>
<Ellipse Width="10" Height="10" Canvas.Left="40" Canvas.Top="20" Stretch="Fill" Fill="Green" Opacity="0.3"/>
<Ellipse Width="10" Height="10" Canvas.Left="20" Canvas.Top="40" Stretch="Fill" Fill="Green" Opacity="0.6"/>
<Ellipse Width="10" Height="10" Canvas.Left="0" Canvas.Top="20" Stretch="Fill" Fill="Green" Opacity="0.9"/>
<Canvas.RenderTransform>
<RotateTransform Angle="60" />
</Canvas.RenderTransform>
</Canvas>
<Grid.RenderTransform>
<RotateTransform x:Name="SpinnerRotate" CenterX="25" CenterY="25" />
</Grid.RenderTransform>
<Grid.Triggers>
<EventTrigger RoutedEvent="FrameworkElement.Loaded">
<BeginStoryboard>
<Storyboard x:Name="Animation">
<DoubleAnimationUsingKeyFrames Duration="0:0:12" RepeatBehavior="Forever" SpeedRatio="12" Storyboard.TargetName="SpinnerRotate" Storyboard.TargetProperty="(RotateTransform.Angle)">
<DiscreteDoubleKeyFrame KeyTime="00:00:00" Value="0" />
<DiscreteDoubleKeyFrame KeyTime="00:00:01" Value="30" />
<DiscreteDoubleKeyFrame KeyTime="00:00:02" Value="60" />
<DiscreteDoubleKeyFrame KeyTime="00:00:03" Value="90" />
<DiscreteDoubleKeyFrame KeyTime="00:00:04" Value="120" />
<DiscreteDoubleKeyFrame KeyTime="00:00:05" Value="150" />
<DiscreteDoubleKeyFrame KeyTime="00:00:06" Value="180" />
<DiscreteDoubleKeyFrame KeyTime="00:00:07" Value="210" />
<DiscreteDoubleKeyFrame KeyTime="00:00:08" Value="240" />
<DiscreteDoubleKeyFrame KeyTime="00:00:09" Value="270" />
<DiscreteDoubleKeyFrame KeyTime="00:00:10" Value="300" />
<DiscreteDoubleKeyFrame KeyTime="00:00:11" Value="330" />
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Grid.Triggers>
</Grid>
</Grid>
</Window>
Кода у этого окна нет, все на триггерах. Итак к пример. На главном окне я добавил кнопку, вот с таким магическим кодом:
Thread.Sleep(4000);
}
Это и есть эмуляция "длительной операции". Понятно, что после нажатия этой кнопки приложение "висит" четыре секунды и непонятно, что с ним происходит. Давайте покажем мотылятор. Решение в лоб:
ShowBusy();
Thread.Sleep(4000);
HideBusy();
}
_busyWindow = new BusyWindow();
_busyWindow.Left = this.Left + this.Width / 2;
_busyWindow.Top = this.Top + this.Height / 2;
_busyWindow.Show();
}
private void HideBusy()
{
_busyWindow.Close();
}
Позволяет показать мотылятор, но т.к. основной поток остановлен, то и анимация не происходит. Иллюзия что приложению плохо сохраняется.
Попытка вынести создание и показ окна в отдельный поток ни к чему хорошему не приводит. Изменив код вот так:
Task.Factory.StartNew(AnimationThreadStartingPoint);
}
private void AnimationThreadStartingPoint()
{
_busyWindow = new BusyWindow();
_busyWindow.Left = this.Left + this.Width / 2;
_busyWindow.Top = this.Top + this.Height / 2;
_busyWindow.Show();
}
При нажатии на кнопку мы получим вот такое печальное сообщение:
Ок, отказываемся от новомодных Task-ов и возвращаемся к привычным Thread-ам, избавляемся от межпотокового взаимодействия и добавляем блокировки в целях избегания гонок:
lock (_busyWindowSync)
{
if (_busyWindow == null)
{
double left = Dispatcher.Invoke((Func<double>)(() => this.Left + this.Width / 2));
double top = Dispatcher.Invoke((Func<double>)(() => this.Top + this.Height / 2));
Thread newWindowThread = new Thread(new ParameterizedThreadStart(AnimationThreadStartingPoint));
newWindowThread.SetApartmentState(ApartmentState.STA);
newWindowThread.IsBackground = true;
newWindowThread.Start(new Point() { X = left, Y = top });
}
}
}
private void AnimationThreadStartingPoint(object position)
{
lock (_busyWindowSync)
{
if (_busyWindow == null)
{
_busyWindow = new BusyWindow();
_busyWindow.Left = ((Point)position).X;
_busyWindow.Top = ((Point)position).Y;
_busyWindow.Show();
}
}
System.Windows.Threading.Dispatcher.Run();
}
private void HideBusy()
{
lock (_busyWindowSync)
{
if (_busyWindow != null)
{
_busyWindow.Dispatcher.BeginInvoke((Action)_busyWindow.Close);
}
}
}
Все, теперь несмотря на то, что главный поток приложения "висит", анимация показывается и пользователь спокойно ждет окончания длительной операции. Лень делать гифку, поэтому придется поверить мне на слово, что она вертится:
Итак еще раз задача. В приложении есть "длительная операция", которая выполняется в потоке интерфейса и нужно пользователю показать анимацию, чтобы он не волновался что все пропало.
Для показа анимации я добавил вот такое окно в приложение:
<Window x:Class="WpfApplication1.BusyWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WpfApplication1"
mc:Ignorable="d"
Title="BusyWindow" Height="300" Width="300" WindowStyle="None" Background="Transparent" AllowsTransparency="True">
<Grid>
<Grid Background="Transparent" HorizontalAlignment="Center" VerticalAlignment="Center">
<Canvas RenderTransformOrigin="0.5,0.5" HorizontalAlignment="Center" VerticalAlignment="Center" Width="50" Height="50" >
<Ellipse Width="10" Height="10" Canvas.Left="20" Canvas.Top="0" Stretch="Fill" Fill="Green" Opacity="1.0"/>
<Ellipse Width="10" Height="10" Canvas.Left="40" Canvas.Top="20" Stretch="Fill" Fill="Green" Opacity="0.1"/>
<Ellipse Width="10" Height="10" Canvas.Left="20" Canvas.Top="40" Stretch="Fill" Fill="Green" Opacity="0.4"/>
<Ellipse Width="10" Height="10" Canvas.Left="0" Canvas.Top="20" Stretch="Fill" Fill="Green" Opacity="0.7"/>
<Canvas.RenderTransform>
<RotateTransform Angle="0" />
</Canvas.RenderTransform>
</Canvas>
<Canvas RenderTransformOrigin="0.5,0.5" HorizontalAlignment="Center" VerticalAlignment="Center" Width="50" Height="50" >
<Ellipse Width="10" Height="10" Canvas.Left="20" Canvas.Top="0" Stretch="Fill" Fill="Green" Opacity="0.01"/>
<Ellipse Width="10" Height="10" Canvas.Left="40" Canvas.Top="20" Stretch="Fill" Fill="Green" Opacity="0.2"/>
<Ellipse Width="10" Height="10" Canvas.Left="20" Canvas.Top="40" Stretch="Fill" Fill="Green" Opacity="0.5"/>
<Ellipse Width="10" Height="10" Canvas.Left="0" Canvas.Top="20" Stretch="Fill" Fill="Green" Opacity="0.8"/>
<Canvas.RenderTransform>
<RotateTransform Angle="30" />
</Canvas.RenderTransform>
</Canvas>
<Canvas RenderTransformOrigin="0.5,0.5" HorizontalAlignment="Center" VerticalAlignment="Center" Width="50" Height="50" >
<Ellipse Width="10" Height="10" Canvas.Left="20" Canvas.Top="0" Stretch="Fill" Fill="Green" Opacity="0.05"/>
<Ellipse Width="10" Height="10" Canvas.Left="40" Canvas.Top="20" Stretch="Fill" Fill="Green" Opacity="0.3"/>
<Ellipse Width="10" Height="10" Canvas.Left="20" Canvas.Top="40" Stretch="Fill" Fill="Green" Opacity="0.6"/>
<Ellipse Width="10" Height="10" Canvas.Left="0" Canvas.Top="20" Stretch="Fill" Fill="Green" Opacity="0.9"/>
<Canvas.RenderTransform>
<RotateTransform Angle="60" />
</Canvas.RenderTransform>
</Canvas>
<Grid.RenderTransform>
<RotateTransform x:Name="SpinnerRotate" CenterX="25" CenterY="25" />
</Grid.RenderTransform>
<Grid.Triggers>
<EventTrigger RoutedEvent="FrameworkElement.Loaded">
<BeginStoryboard>
<Storyboard x:Name="Animation">
<DoubleAnimationUsingKeyFrames Duration="0:0:12" RepeatBehavior="Forever" SpeedRatio="12" Storyboard.TargetName="SpinnerRotate" Storyboard.TargetProperty="(RotateTransform.Angle)">
<DiscreteDoubleKeyFrame KeyTime="00:00:00" Value="0" />
<DiscreteDoubleKeyFrame KeyTime="00:00:01" Value="30" />
<DiscreteDoubleKeyFrame KeyTime="00:00:02" Value="60" />
<DiscreteDoubleKeyFrame KeyTime="00:00:03" Value="90" />
<DiscreteDoubleKeyFrame KeyTime="00:00:04" Value="120" />
<DiscreteDoubleKeyFrame KeyTime="00:00:05" Value="150" />
<DiscreteDoubleKeyFrame KeyTime="00:00:06" Value="180" />
<DiscreteDoubleKeyFrame KeyTime="00:00:07" Value="210" />
<DiscreteDoubleKeyFrame KeyTime="00:00:08" Value="240" />
<DiscreteDoubleKeyFrame KeyTime="00:00:09" Value="270" />
<DiscreteDoubleKeyFrame KeyTime="00:00:10" Value="300" />
<DiscreteDoubleKeyFrame KeyTime="00:00:11" Value="330" />
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Grid.Triggers>
</Grid>
</Grid>
</Window>
Кода у этого окна нет, все на триггерах. Итак к пример. На главном окне я добавил кнопку, вот с таким магическим кодом:
private void button_Click(object sender, RoutedEventArgs e)
{Thread.Sleep(4000);
}
Это и есть эмуляция "длительной операции". Понятно, что после нажатия этой кнопки приложение "висит" четыре секунды и непонятно, что с ним происходит. Давайте покажем мотылятор. Решение в лоб:
private void button_Click(object sender, RoutedEventArgs e)
{ShowBusy();
Thread.Sleep(4000);
HideBusy();
}
BusyWindow _busyWindow = null;
private void ShowBusy()
{_busyWindow = new BusyWindow();
_busyWindow.Left = this.Left + this.Width / 2;
_busyWindow.Top = this.Top + this.Height / 2;
_busyWindow.Show();
}
private void HideBusy()
{
_busyWindow.Close();
}
Позволяет показать мотылятор, но т.к. основной поток остановлен, то и анимация не происходит. Иллюзия что приложению плохо сохраняется.
Попытка вынести создание и показ окна в отдельный поток ни к чему хорошему не приводит. Изменив код вот так:
private void ShowBusy()
{Task.Factory.StartNew(AnimationThreadStartingPoint);
}
private void AnimationThreadStartingPoint()
{
_busyWindow = new BusyWindow();
_busyWindow.Left = this.Left + this.Width / 2;
_busyWindow.Top = this.Top + this.Height / 2;
_busyWindow.Show();
}
При нажатии на кнопку мы получим вот такое печальное сообщение:
Ок, отказываемся от новомодных Task-ов и возвращаемся к привычным Thread-ам, избавляемся от межпотокового взаимодействия и добавляем блокировки в целях избегания гонок:
BusyWindow _busyWindow = null;
object _busyWindowSync = new object();
private void ShowBusy()
{lock (_busyWindowSync)
{
if (_busyWindow == null)
{
double left = Dispatcher.Invoke((Func<double>)(() => this.Left + this.Width / 2));
double top = Dispatcher.Invoke((Func<double>)(() => this.Top + this.Height / 2));
Thread newWindowThread = new Thread(new ParameterizedThreadStart(AnimationThreadStartingPoint));
newWindowThread.SetApartmentState(ApartmentState.STA);
newWindowThread.IsBackground = true;
newWindowThread.Start(new Point() { X = left, Y = top });
}
}
}
private void AnimationThreadStartingPoint(object position)
{
lock (_busyWindowSync)
{
if (_busyWindow == null)
{
_busyWindow = new BusyWindow();
_busyWindow.Left = ((Point)position).X;
_busyWindow.Top = ((Point)position).Y;
_busyWindow.Show();
}
}
System.Windows.Threading.Dispatcher.Run();
}
private void HideBusy()
{
lock (_busyWindowSync)
{
if (_busyWindow != null)
{
_busyWindow.Dispatcher.BeginInvoke((Action)_busyWindow.Close);
}
}
}
Все, теперь несмотря на то, что главный поток приложения "висит", анимация показывается и пользователь спокойно ждет окончания длительной операции. Лень делать гифку, поэтому придется поверить мне на слово, что она вертится:
Комментариев нет:
Отправить комментарий