user3042410
user3042410

Reputation: 85

WPF SplashScreen with ProgressBar

I have a WPF project, with a splash screen added by the Project Wizard. On the same splash screen, I want to add a progress bar style meter. Does someone have any idea how to do this?

Upvotes: 7

Views: 23629

Answers (3)

cboittin
cboittin

Reputation: 391

The answer by Ali doesn't work on .NET core. However, there is a workaround and it is, in my opinion, more straightforward. I believe it works on all "recent" versions of .NET (probably since Task.Run was added).

I used a similar approach, with another Window to use as a splash screen.

My App.xaml uses the Startup event instead of StartupUri. I believe this doesn't make any difference, though.

<Application x:Class="SplashScreenDemo.App"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Startup="OnStartup">
    <Application.Resources>
    </Application.Resources>
</Application>

I do not declare a new delegate type for the initialization. Instead, i simply use a method. Creating and displaying the main window could take some time, so i think it's better to only return when the window is done loading, by tuning in to its Loaded event. We use an EventWaitHandle for that.

public partial class App : Application
{
    public App()
    {
    }
    
    public static new App Current { get => Application.Current as App; }

    private void OnStartup(object sender, StartupEventArgs e)
    {
        var splashScreen = new SplashScreenDemo.Splash();
        splashScreen.Show();
    }

    internal void InitializeApplication(Splash splashWindow)
    {
        // fake workload, but with progress updates.
        Thread.Sleep(500);
        splashWindow.SetProgress(0.25);

        Thread.Sleep(500);
        splashWindow.SetProgress(0.5);

        Thread.Sleep(500);
        splashWindow.SetProgress(0.75);

        EventWaitHandle mainWindowLoaded = new ManualResetEvent(false);

        // Create the main window, but on the UI thread.
        Dispatcher.BeginInvoke((Action)(() =>
        {
            MainWindow = new Window1();
            MainWindow.Loaded += (sender, e) =>
            {
                mainWindowLoaded.Set();
            };
            splashWindow.SetProgress(0.9);
            MainWindow.Show();
            splashWindow.SetProgress(1);
        }));
        
        // Wait until the Main window has finished initializing and loading
        mainWindowLoaded.WaitOne();        
    }
}

The splash screen window could use some window attributes :

  • WindowStartupLocation="CenterScreen" self explanatory
  • WindowStyle="None" to avoid having a title bar
  • ResizeMode="NoResize" to prevent the user from moving or resizing it
  • Topmost="True" to make it always appear on top of other windows
<Window x:Class="SplashScreenDemo.Splash"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="Splash" Height="300" Width="300" WindowStartupLocation="CenterScreen"
    WindowStyle="None" ResizeMode="NoResize" Topmost="True">
    <Grid>
        <TextBlock Height="21" VerticalAlignment="Top" TextAlignment="Center" Text="Loading"/>
        <ProgressBar Name="progBar" Margin="20,100" Minimum="0" Maximum="1"/>
    </Grid>
</Window>

The splash screen's code becomes easier to understand, we simply call the init method asynchronously. We could also set focus to the main window before closing the splash screen, in some applications.

public partial class Splash : Window
{
    public Splash()
    {
        InitializeComponent();
        this.Loaded += Splash_Loaded;
    }

    private async void Splash_Loaded(object sender, RoutedEventArgs e)
    {
        await Task.Run(() =>
        {
            App.Current.InitializeApplication(this);
            App.Current.Dispatcher.BeginInvoke((Action)(() =>
            {
                Close();
            }));
        });
    }

    public void SetProgress(double progress)
    {
        Dispatcher.BeginInvoke((Action)(() => progBar.Value = progress));           
    }
}

Upvotes: 1

mihails.kuzmins
mihails.kuzmins

Reputation: 1380

I used .NET Core and when there was a new Thread Application.OnExit() was fired. So the only way how I managed to make it work is:

Application.cs:

protected override void OnStartup(StartupEventArgs e)
{
    base.OnStartup(e);
    _mySplashScreenWindow.Show();


    Task.Run(async () =>
    {
        for (var i = 1; i <= 20; i++)
        {
            _mySplashScreenWindow.Dispatcher.Invoke(() =>
            {
                _mySplashScreenWindow.SomeTextBlock.Text = i.ToString();
                _mySplashScreenWindow.Progress = i; // or i / 20 for %
            });
            await Task.Delay(250);
        }
    })
    .ContinueWith(_ =>
    {
        MainWindow = null;

        var mainWindow = new MainWindow();
        // Or if you have access to the SplashScreen in the MainWindow, you can subscribe there
        mainWindow.Loaded += (sender, args) =>
        {
            _mySplashScreenWindow.Close();
            _mySplashScreenWindow = null;
        }

        MainWindow = mainWindow;
        mainWindow.ShowDialog();
    }, TaskScheduler.FromCurrentSynchronizationContext());
}

Upvotes: 0

Ali Adlavaran
Ali Adlavaran

Reputation: 3735

Here is my scheme for doing this. My motivation for doing it this way is that I don't want to have the initialization code running on the UI Thread, and normally I want the initialization code on my App class (not a Splash screen).

Basically, I set the App StartupUri to my splash screen, which gets the ball rolling.

On the splash screen, I invoke a delegate back on the application. This is run on a worker thread. In the splash screen, I handle the EndInvoke, and close the Window.

In the application initialization delegate, I do the work, and at the end, create and open the normal main Window. During the work load, I also have a method on Slash that allows me to update progress.

OK,the code is fairly short, and doesn't include the main window code (which is unaffected by all this), but it ducks and dives with anonymous delegates, so read it carefully, and ideally play with it in the debugger.

Here is the code....

<Application x:Class="SplashScreenDemo.App"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    StartupUri="Splash.xaml">
    <Application.Resources>

    </Application.Resources>
</Application>

app code behind...

internal delegate void Invoker();
public partial class App : Application
{
    public App()
    {
        ApplicationInitialize = _applicationInitialize;
    }
    public static new App Current
    {
        get { return Application.Current as App; }
    }
    internal delegate void ApplicationInitializeDelegate(Splash splashWindow);
    internal ApplicationInitializeDelegate ApplicationInitialize;
    private void _applicationInitialize(Splash splashWindow)
    {
        // fake workload, but with progress updates.
        Thread.Sleep(500);
        splashWindow.SetProgress(0.2);

        Thread.Sleep(500);
        splashWindow.SetProgress(0.4);

        Thread.Sleep(500);
        splashWindow.SetProgress(0.6);

        Thread.Sleep(500);
        splashWindow.SetProgress(0.8);

        Thread.Sleep(500);
        splashWindow.SetProgress(1);

        // Create the main window, but on the UI thread.
        Dispatcher.BeginInvoke(DispatcherPriority.Normal, (Invoker)delegate
        {
            MainWindow = new Window1();
            MainWindow.Show();
        });           
    }
}

splash xaml (actually, nothing too interesting here...)

<Window x:Class="SplashScreenDemo.Splash"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="Splash" Height="300" Width="300">
    <Grid>
        <TextBlock Height="21" Margin="91,61,108,0" VerticalAlignment="Top">Splash Screen</TextBlock>
        <ProgressBar Name="progBar" Margin="22,122,16,109" Minimum="0" Maximum="1"/>
    </Grid>
</Window>

splash code-behind...

public partial class Splash : Window
{
    public Splash()
    {
        InitializeComponent();
        this.Loaded += new RoutedEventHandler(Splash_Loaded);
    }

    void Splash_Loaded(object sender, RoutedEventArgs e)
    {
        IAsyncResult result = null;

        // This is an anonymous delegate that will be called when the initialization has COMPLETED
        AsyncCallback initCompleted = delegate(IAsyncResult ar)
        {
            App.Current.ApplicationInitialize.EndInvoke(result);

            // Ensure we call close on the UI Thread.
            Dispatcher.BeginInvoke(DispatcherPriority.Normal, (Invoker)delegate { Close(); });
        };

        // This starts the initialization process on the Application
        result = App.Current.ApplicationInitialize.BeginInvoke(this, initCompleted, null);
    }

    public void SetProgress(double progress)
    {
        // Ensure we update on the UI Thread.
        Dispatcher.BeginInvoke(DispatcherPriority.Normal, (Invoker)delegate { progBar.Value = progress; });           
    }
}

As the work is done on a worker thread, the progress bar will update nicely, and any animation you have on the splash screen will keep the entertainment rolling.

Upvotes: 12

Related Questions