Thursday, 20 April 2017

Improving the Windows Photo Screensaver

Way back in 2011 I decided to setup the laptop screensaver to show my photos. The problem with this was although I had around 10,000 or so photos, the screensaver in shuffle mode was apparently using a random seed based on the photo folder properties. This meant I only saw the same photos, at least until I'd added or removed some.

( I have no idea if the latest incarnation on Windows 10 uses a time-based seed now, mainly because my own one has worked for me without a hitch for the last 6 years)

As you can guess, I wasn't particularly impressed with this so I decided to write my own photo display screensaver.

I wanted to write this using .NET and WPF so I first created a Window with 3 vertical columns. The reason for this was because I wanted portrait photos to be centered. The XAML is something like this.

<Window x:Class="WPFScreenSaver.PhotoScreenSaver"
    Title="PhotoScreenSaver" WindowStyle="None"

    WindowState="Maximized" ResizeMode="NoResize"
    Loaded="OnLoaded" Cursor="None" KeyDown="PhotoStack_KeyDown" 

    MouseDown="PhotoStack_MouseDown"
    MouseMove="PhotoStack_MouseMove" >
    <Grid Name="ImageGrid" Background="Black">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Name="Col1"/>
            <ColumnDefinition/>
            <ColumnDefinition Name="Col2"/>
        </Grid.ColumnDefinitions>
        <Image Name="ScreenImage" Stretch="Uniform" Grid.Column="1"/>
    </Grid>
</Window>


Note the KeyDown, MouseDown and MousrMove events are used to detect motion to stop the screensaver. In the code behind these call Application.Current.Shutdown(); to kill the screensaver.

The WindowStyle, WindowState and ResizeMode properties ensure the window fills the whole screen, especially on top of the Taskbar.

In the PhotoScreenSaver constructor, I run a file discovery step to obtain a list of all the filenames of files with .jpg extension from a root folder. (This root folder is stored in the Registry)

Then I create a new Random instance. This has a seed based on the current time.

A Timer is setup with callback to a method to ShowNextImage.

It's important to note here that the timer callback is not on the main UI thread which means we won't be able to update the image so we need to use Dispatcher to invoke it on that thread

if (this.Dispatcher.Thread != Thread.CurrentThread)
{

    this.Dispatcher.Invoke(new Action<Object>(ShowNextImage), new object[] { stateInfo });
}


I obtain an index to file to show using the random number generator. NextDouble returns a double between 0.0 and 1.0 so I can use this to multiply by the number of images to get an index.

var index = RandomGenerator.NextDouble() * imageFileCount;
var filename = ImageFiles[(int)index];

I don't worry about rounding since it's not really a problem if I get ImageFiles[7689] or ImageFiles[7688]

The image itself is loaded as a BitmapImage. This is then scaled to fit the screen.

To turn this from a photo display app into a fully fledged screensaver we need to do some work with the Application startup. In App.xaml we need to change Startup to point to our own startup method

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


The OnStartup in the code-behind class does two things. First it reads the command line arguments and second shows either the screensaver, a settings window or a preview. The arguments are:
  • /c  Show the settings dialog
  • /p Show a preview (I just call Shutdown for this)
  • /s or no argument, show the screensaver window
The last step to display the photo window I've just updated (and hence the blog!) Mainly because the original implementation didn't work with two monitors. I now enumerate each screen, determine its dimensions and create the photo window accordingly.

foreach (System.Windows.Forms.Screen screen in System.Windows.Forms.Screen.AllScreens)
{
    PhotoScreenSaver window = new PhotoScreenSaver();
    window.WindowStartupLocation = WindowStartupLocation.Manual;
    System.Drawing.Rectangle location = screen.Bounds;
  
    // Set window position and size
    window.Left = location.X;
    window.Top = location.Y;
    window.Width = location.Width;
    window.Height = location.Height;

    // Tip, if this isn't the primary window and you set
    // window.WindowState = WindowState.Maximized;
    // Before the window has been generated, it'll maximise into the primary
    // In any case, using normal seems fine.
    window.WindowState = WindowState.Normal;
}

//Show the windows
foreach (Window window in System.Windows.Application.Current.Windows)
{         
    window.Show();
}

The windows are entirely independent. I can improve this by sharing the ImageFiles list between the two and synchronising the updates but it really works quite nicely as it is.

Finally, after building the exe, to turn into a screensaver rename to Screensaver.scr. After this, you can right-click on the .scr file and select install.

No comments:

Post a Comment