简体   繁体   中英

WPF loading animation for a UI process

In my WPF application I have an About Box with the application info and version. When this window is loaded it takes a little bit of time, especially if its the first time it's being opened. I'm trying to implement a loading animation while the window is opening so that the application continues to seem responsive.

I've tried using the C# BackgroundWorker to implement this, but it won't work because the process I'm trying to add a loading animation for (the about box opening) is a can only be run on the UI thread . I've tried creating a new thread and placing it in a STA apartment but it did not work.

This is the method in which I launch the about box and control the starting/stopping of the loading animation:

        private void AboutMenuItem_OnClick(object sender, RoutedEventArgs e)
    {
        LoadingCircle.Start();
        LoadingCircle.Visibility = Visibility.Visible;                    

        var aboutBox = new AboutBox { Owner = this };
        aboutBox.Show();

        LoadingCircle.Stop();
        LoadingCircle.Visibility = Visibility.Hidden;
    }

The loading circle does not appear and start moving until aboutBox.Show() is called, I can't understand why this is. If I run my application with the code above the loading circle will appear briefly before the window is loaded but it does not spin.

EDIT:

It seems that what creates the short delay is just the creation of the window, the code for creating the AboutBox is simple:

public partial class AboutBox : Window
{
    public AboutBox()
    {
        InitializeComponent();
    }

    private void Button_Click(object sender, RoutedEventArgs e)
    {
        Close();
    }
}

public class Version
{
    public string UiVersion { get; set; }
    public string ServiceVersion { get; set; }

    public static Version GetVersion()
    {
        var ver = new Version();

        Assembly assembly = Assembly.GetExecutingAssembly();
        FileVersionInfo fvi = FileVersionInfo.GetVersionInfo(assembly.Location);
        ver.UiVersion = fvi.FileVersion;

        ver.ServiceVersion = "<Service Version>";

        return ver;
    }
}

Here's the XAML:

<Window
         xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
         xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
         xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
         x:Class="NDO.PC.DataViewer.AboutBox" 
         mc:Ignorable="d" 
         Height="384" Width="600" Title="About NanoDrop One"  WindowStartupLocation="CenterOwner" AllowsTransparency="true" WindowStyle="None" Background="White">

<Window.Resources>
    <Style x:Key="ButtonFocusVisual">
        <Setter Property="Control.Template">
            <Setter.Value>
                <ControlTemplate>
                    <Rectangle Margin="2" SnapsToDevicePixels="true" Stroke="{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}" StrokeThickness="1" StrokeDashArray="1 2"/>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
    <LinearGradientBrush x:Key="ButtonNormalBackground" EndPoint="0,1" StartPoint="0,0">
        <GradientStop Color="#F3F3F3" Offset="0"/>
        <GradientStop Color="#EBEBEB" Offset="0.5"/>
        <GradientStop Color="#DDDDDD" Offset="0.5"/>
        <GradientStop Color="#CDCDCD" Offset="1"/>
    </LinearGradientBrush>
    <SolidColorBrush x:Key="ButtonNormalBorder" Color="#FF707070"/>
    <Style x:Key="OKButton" TargetType="{x:Type Button}">
        <Setter Property="FocusVisualStyle" Value="{StaticResource ButtonFocusVisual}"/>
        <Setter Property="Background" Value="{StaticResource ButtonNormalBackground}"/>
        <Setter Property="BorderBrush" Value="{StaticResource ButtonNormalBorder}"/>
        <Setter Property="BorderThickness" Value="1"/>
        <Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}"/>
        <Setter Property="HorizontalContentAlignment" Value="Center"/>
        <Setter Property="VerticalContentAlignment" Value="Center"/>
        <Setter Property="Padding" Value="1"/>
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate x:Name="OKButton" TargetType="{x:Type Button}">
                    <Border x:Name="Chrome" BorderBrush="{TemplateBinding BorderBrush}" Background="{TemplateBinding Background}"   SnapsToDevicePixels="true" CornerRadius="5" BorderThickness="1">
                        <ContentPresenter Name="TextName" HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" Margin="{TemplateBinding Padding}" RecognizesAccessKey="True" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" VerticalAlignment="{TemplateBinding VerticalContentAlignment}"/>
                    </Border>
                    <ControlTemplate.Triggers>
                        <Trigger Property="IsKeyboardFocused" Value="true">
                            <Setter Property="BorderBrush" TargetName="Chrome" Value="White"/>
                        </Trigger>
                        <Trigger Property="IsEnabled" Value="false">
                            <Setter Property="Foreground" Value="#ADADAD"/>
                        </Trigger>
                        <Trigger Property="IsMouseOver" Value="true">
                            <Setter Property="Background" Value="White" TargetName="Chrome"/>
                            <Setter Property="TextBlock.Foreground" Value="#FF0086FF" TargetName="TextName"/>
                            <Setter Property="BorderBrush" Value="#FF0086FF" TargetName="Chrome"/>
                        </Trigger>
                    </ControlTemplate.Triggers>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
</Window.Resources>

<Grid x:Uid="ImageGrid" x:Name="ImageGrid" Grid.Row="0"  VerticalAlignment="Top" HorizontalAlignment="Left">
    <Grid.RowDefinitions>
        <RowDefinition Height="79"/>
        <RowDefinition Height="13"/>
        <RowDefinition Height="*" />
    </Grid.RowDefinitions>
    <Image  x:Uid="AboutImage" x:Name="AboutImage" Source="Resources/AboutPageImage.jpg" Width="600" Height="79" Stretch="UniformToFill" />
    <Border Grid.Row="1" x:Uid="Border_1" Margin="0,0,0,0" Height="13" MinWidth="600" VerticalAlignment="Bottom">
        <Border.Background>
            <LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
                <LinearGradientBrush.RelativeTransform>
                    <TransformGroup>
                        <ScaleTransform CenterY="0.5" CenterX="0.5"/>
                        <SkewTransform CenterY="0.5" CenterX="0.5"/>
                        <RotateTransform Angle="90" CenterY="0.5" CenterX="0.5"/>
                        <TranslateTransform/>
                    </TransformGroup>
                </LinearGradientBrush.RelativeTransform>
                <GradientStop Color="#FFE5EAEE" Offset="1"/>
                <GradientStop Color="#FF0086FF" Offset="0.36"/>
            </LinearGradientBrush>
        </Border.Background>
    </Border>

    <Grid Margin="36,18,36,36" Grid.Row="2" Height="238">
        <Grid.ColumnDefinitions>
            <ColumnDefinition/>
            <ColumnDefinition Width="128px"/>
        </Grid.ColumnDefinitions>
        <StackPanel>
            <TextBlock FontFamily="Segoe UI Semibold" FontSize="20" FontWeight="Bold" Foreground="#FF0086FF" Margin="0,0,0,12" VerticalAlignment="Top" HorizontalAlignment="Left"><Run Text="About Application"/></TextBlock>
            <StackPanel Orientation="Horizontal">
                <TextBlock Foreground="#FF0086FF" 
                           Text="Software UI version: " />
                <TextBlock Foreground="#FF0086FF" 
                       Margin="5,0,0,0" 
                       Text="{Binding UiVersion}" />
            </StackPanel>
            <StackPanel Orientation="Horizontal">
                <TextBlock Foreground="#FF0086FF" 
                           Text="Software Service version: " />
                <TextBlock Foreground="#FF0086FF" 
                       Margin="5,0,0,0" 
                       Text="{Binding ServiceVersion}" />
            </StackPanel>
         </StackPanel>
        <Image Grid.Column="1" Source="Resources/TS_logo_rgb 200x61with spacing.png" Width="128" VerticalAlignment="Bottom" Margin="0,0,-38,-18"/>
        <Button Height="25" Width="75" Background="#FF0086FF" BorderBrush="{x:Null}" Foreground="White" VerticalAlignment="Bottom" HorizontalAlignment="Left" Click="Button_Click" Style="{DynamicResource OKButton}" Content="OK"/>
    </Grid>
</Grid>

The issue that you are trying to do everything on the UI thread. You need to background or otherwise not block on the long running logic.

I would recommend using await/async . Something like this would work:

    LoadingCircle.Start();
    LoadingCircle.Visibility = Visibility.Visible;                    

    var aboutBox = new AboutBox { Owner = this };
    await Task.Run(() => 
    {
        aboutBox.Init();
    });
    aboutBox.Show();

    LoadingCircle.Stop();
    LoadingCircle.Visibility = Visibility.Hidden;

That puts the long logic into an asynchronous Task and stops execution of just the current method until it returns. It does not block the calling thread. Note that you need to mark your event handler async for this.

I created a fake "Init" method on the About box so that your constructor can do effectively nothing (but do it on the UI thread).

Also, these problems kind of disappear when doing WPF the "right" way with MVVM, so consider using that pattern in the future. It forces you to separate your view and business logic so threading the latter becomes trivial.

As was mentioned previously, you should analyze why the window takes so long to load. There are some other options you can do:

  1. Move code around so that the window displays fast and the other code then takes a hit. Use the Busy Indicator on the actual window to flag that it is loading

  2. Don't load the about window on click, load it prior, like at startup, and just show it when the button is clicked.

  3. Use the Window_Loaded event on the about window to do work, put that work in other threads. Don't use the constructor of the window to do stuff.

Busy Indicator

You should checkout the WPF Toolkit (also available through nuget) it has a control called a Busy Indicator .

The busy indicator is a control that just shows a progress bar or other content and darkens out the background. You just set a property IsBusy to true and it turns on. So if your about window is doing a lot of work, you can show this on the about window until that work is complete.

Here is a tutorial.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM