Friday, February 4, 2011

Dynamic splash screen in WPF

In WPF 3.5 SP1 new splash screen functionality was introduced. It’s now possible to add static image which is shown before main window is loaded.

Read more on MSDN: http://msdn.microsoft.com/en-us/library/cc656886.aspx

Unfortunately current implementation has some limitations. Since only static images are supported (BMP, GIF, JPEG, PNG, or TIFF format):
  • It’s not possible to use XAML to define splash screen design
  • It’s not possible to display dynamic informations like:
    • List of available plugins
    • Licensee information
    • Plugin being currently loaded. 

Solution overview
Solution for those issues is:
  1. Show built-in WPF splash screen
  2. Load regular WPF window (dynamic splash screen) which is pixel-perfect same as previously displayed splash screen but displays also dynamic information
  3. When all plugins and main window is loaded hide dynamic splash screen and show main window.


Implementation : DynamicSplashScreen class
DynamicSplashScreen is reusable class which  can be used as base class for your custom splash screen. Check inline comments for details.

public class DynamicSplashScreen:Window
{
    public DynamicSplashScreen()
    {
        //Defaults for splash screen
        this.ShowInTaskbar = false;
        this.WindowStartupLocation = WindowStartupLocation.Manual;
        this.ResizeMode = ResizeMode.NoResize;
        this.WindowStyle = WindowStyle.None;
        this.Topmost = true;

        this.Loaded += OnLoaded;
    }        

    private void OnLoaded(object sender, RoutedEventArgs e)
    {
        //calculate it manually since CenterScreen substracts  
        //taskbar's height from available area
        this.Left = (SystemParameters.PrimaryScreenWidth - this.Width) / 2;
        this.Top = (SystemParameters.PrimaryScreenHeight - this.Height) / 2;
    }
}

Creating pixel-perfect static splash screen image
Creating pixel-perfect static image can be error prone, maintenance hell if you start using external applications like Photoshop – each changed in dynamic version would have to be reflected in static image manually.

DynamicSplashScreen has another feature: capturing static splash screen image. It used build-in WPF functionality to get Control’s  “screen shot” dynamically.

You can use this method to generate image and add it to Visual Studio project with build action set to SplashScreen. This still requires some manual work, but it’s not possible (and reasonably) to generate splash screen at runtime.
public class DynamicSplashScreen:Window
{
    //.... see implementation above ..

    public void Capture(string filePath)
    {
        this.Capture(filePath, new PngBitmapEncoder());
    }

    public void Capture(string filePath, BitmapEncoder encoder)
    {
        RenderTargetBitmap bmp = new RenderTargetBitmap( 
                 (int)this.Width, (int)this.Height,  
                 96, 96, PixelFormats.Pbgra32);
        bmp.Render(this);


        encoder.Frames.Add(BitmapFrame.Create(bmp));

        using (Stream stm = File.Create(filePath))
        {
            encoder.Save(stm);
        }
    }
}

Usage
public partial class App : Application
{
    protected override void OnStartup(StartupEventArgs e)
    {
        // MyDynamicSplashScreen derives from DynamicSplashScreen and
        // defines layout and custom properties (like Licensee, Plugins, Message)
        MyDynamicSplashScreen splashScreen = new MyDynamicSplashScreen();
        splashScreen.Show();

        //set basic dynamic data on splash screen
        splashScreen.AvailablePlugins = new[] {"Plugin 1", "Plugin 2"};
        splashScreen.Licensee = "Mirosław Jedynak";

        //use during development to generate image and embed it in application
        //splashScreen.Capture(@"c:\StaticSplashScreen.png");

        var startupTask=new Task(() =>
            {
                //Load plugins in non-UI thread - may be time consuming
                for (int i = 0; i < 3; i++)
                {
                    //set custom message on screen
                    splashScreen.Dispatcher.BeginInvoke(
                        (Action) (()=>splashScreen.Message = "Loading:  Plugin " + i));
                            
                    Thread.Sleep(100);
                }
            });

        //when plugin loading finished, show main window
        startupTask.ContinueWith(t =>
            {
                MainWindow mainWindow = new MainWindow();

                //when main windows is loaded close splash screen
                mainWindow.Loaded += (sender, args) => splashScreen.Close();

                //set application main window;
                this.MainWindow = mainWindow;

                //and finally show it
                mainWindow.Show();
            }, TaskScheduler.FromCurrentSynchronizationContext());

        startupTask.Start();
    }       
}


Sample Dynamic splash screen
<Startup:DynamicSplashScreen x:Class="DynamicSplashScreenDemo.Startup.SplashScreen"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"  
        xmlns:Startup="clr-namespace:DynamicSplashScreenDemo.Startup"  
        Title="My application"   x:Name="spashScreen"  
        Width="500" Height="300">
    <Border BorderThickness="1" BorderBrush="Black">
        <Grid >
            <Rectangle>
                <Rectangle.Fill>
                    <LinearGradientBrush EndPoint="0,0" StartPoint="0.5,1.3">
                        <GradientStop Color="#FF07254E" Offset="1"/>
                        <GradientStop Color="White" Offset="0.369"/>
                        <GradientStop Color="White"/>
                    </LinearGradientBrush>
                </Rectangle.Fill>
            </Rectangle>
            
            <TextBlock  FontFamily="Verdana" FontWeight="Bold" FontSize="50"  
                VerticalAlignment="Top" HorizontalAlignment="Center"  
                TextAlignment="Center"  Margin="0,40,0,0" >
                <Run Text="Dynamic" Foreground="#FF006AB3" />
                <LineBreak/>
                <Run Text="splash screen" Foreground="#FF006AB3"/>
            </TextBlock>

            <TextBlock Text="{Binding ElementName=spashScreen, Path=Message}"  
             VerticalAlignment="Bottom"  Margin="10"/>
            <StackPanel VerticalAlignment="Bottom" HorizontalAlignment="Right"  
                 Margin="10">
                <ItemsControl  Margin="0,0,0,10" HorizontalAlignment="Right"
                ItemsSource="{Binding ElementName=spashScreen, Path=AvailablePlugins}"/>
                <StackPanel Orientation="Horizontal">
                    <TextBlock Text="Licensed to: "/>
                    <TextBlock Text="{Binding ElementName=spashScreen, Path=Licensee}"/>
                </StackPanel>
            </StackPanel>
        </Grid>
    </Border>
</Startup:DynamicSplashScreen>

Source: DynamicSplashScreenDemo_source.zip

Shout it
kick it on DotNetKicks.com

7 comments:

ali aghdam said...

thanks , it was very useful for me

John R said...

Hi,
Thank you very much for this.

I'd found this solution, http://szymonrozga.net/blog/?p=136
but it requires modifying Main(). I was looking for a splashscreen that enhanced the VS2010 feature.

thanks, I've learnt a lot from your example.

John

Karasb said...

Good idea. However, this method requires loading WPF environment (unlike in the native NET 3.5 SP1) that affects the rate of appearance of a splashscreen.

Mirosław Jedynak said...

i can't agree with you.

actually it uses native NET 3.5 SP1 splash screen to give first impression with static image. Then, when environment is loaded, dynamic splash is shown instead of static one

Telavian said...

This is very nice.

Anonymous said...

I must say thank you for this. It has to be about the best implementation I've seen yet.

Malc

Anonymous said...

I tried placing a label "Close" on the splash screen window. Then tried to close the application on LeftMouseButtonDown event. But that event was never fired. Can you help me with this.