Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Another Screensaver with WPF

0.00/5 (No votes)
27 Jan 2012 1  
Lessons learnt from writing a screensaver with WPF

Introduction

This project was created in order to help me learn how to code in .NET WPF. There are two ways in which the screensaver can be displayed: as a slide show or as a photo collage. The latter was inspired by "Desktop Collage" (with Deskscape from stardock.com) and Picasa's Photo Collage. And with the idea of Deskscape, this program also can be run in a Desktop-like background mode (but because of my limited knowledge, it cannot stay behind the desktop icons... yet).

This is not an article about WPF how-to, however. I'd like to point out some techniques that were beyond my common sense. :) If you have any questions, please post them in the comments section.

Credits

I am using the WPF NotifyIcon (http://www.codeproject.com/KB/WPF/wpf_notifyicon.aspx) for the application tray icon in Desktop mode. Thanks for this library, it really saved a lot of time. :]

Also, thanks to this article: Creating a Custom Settings Provider (http://www.codeproject.com/KB/vb/CustomSettingsProvider.aspx).

Application Features

  • Desktop mode: run screensaver as the bottom-most window.
  • Screensaver as slide show
  • Screensaver as photo collage
    • Initial background picture can be set.
    • Old pictures are de-saturated over time.
  • It has several picture sort order modes.
  • Supports four picture collections (use keys 1-4 to change them while running).
  • F2 - To toggle picture file path with size and file date.
  • F9 - Delete/Move/Rename currently shown picture.
  • F11 - Use the current picture's directory as a temporary collection. F10 - to revert to original collection.
  • F12 - Show the configuration dialog.
  • Run without parameters for desktop mode (need Hardcodet.Wpf.TaskbarNotification.dll in the same directory).
  • etc.

Background

How Windows Screensavers Work?

A screensaver is one of the least mentioned features in MSDN Help/Platform SDK. From my understanding, it is since Windows XP that screensaver implementations have changed from DLLs to EXEs with parameters. You can now write a screensaver as an exe file and just rename it to .scr, copy it to %WINDIR% (e.g., c:\windows), and that's all, you will get a new screensaver installed!

However, in order to correctly display the preview screen, and to display the settings dialog, the application must check for command line arguments. There are three possible options that can be passed from Windows.

/S Start screensaver
/C:99999 Show configuration dialog (over the parent window, handle = 99999)
/P 99999 Show preview screen (over the parent window, handle = 99999)

About the Code

Class Diagram

wpfscreensaver_cd.png

The class ScreenSaverEngine is a demigod, controller class. It contains all the logic that decides what is going to be displayed, as you can see in MainApp.cs.

There are three major concepts, which can be roughly explained as follows:

  • The picture source is any class that inherits from IPictureSource. Its duties are:
    • To keep and sort a picture list.
    • To periodically fire the PictureChanged event, which contains an ImageSource (a WPF class) to be displayed.
    • To perform a file operation on the current picture (!! - ok, not as cohesive as it should be, but I don't want to over complicate the project for now...)
  • The slide page is any class that inherits ISlidePage. Its main duty is to display the screensaver content, so it should also inherit any WPF visual component (mine is the Page class).
  • The window host is the PageHost class and its derivatives. It is just a container for a slide page.

Apply Aero's Glass Effect on a WPF (or Even WinForms) Window

I took this technique from the book, Windows Presentation Foundation: Unleased. The GlassHelper class has the following code snippet:

public bool ExtendGlassFrame(){
    var isVistable = Environment.OSVersion.Version.Major >= 6;
    if (!isVistable || !DwmIsCompositionEnabled())
        return false;
    var hwnd = new WindowInteropHelper(window).Handle;
    if (hwnd == IntPtr.Zero)
        throw new InvalidOperationException(
          "The Window must be shown before extending glass.");
    window.Background = Brushes.Transparent;

    var hwndSource = HwndSource.FromHwnd(hwnd);
    Debug.Assert(hwndSource != null);
    hwndSource.CompositionTarget.BackgroundColor = Colors.Transparent;

    var marginParam = margin;
    DwmExtendFrameIntoClientArea(hwnd, ref marginParam);

    hwndSource.AddHook(wndProc);

    return true;
}

IntPtr wndProc(IntPtr hwnd, int msg, IntPtr wParam, 
               IntPtr lParam, ref bool handled){
    if (msg == DwmCompositionChanged){
        ExtendGlassFrame();
        handled = true;
    }
    return IntPtr.Zero;
}
const int DwmCompositionChanged = 0x031E;
[DllImport("dwmapi.dll", PreserveSig = false)]
static extern void DwmExtendFrameIntoClientArea(IntPtr hWnd, 
                   ref Win32.MARGINS pMarInset);
[DllImport("dwmapi.dll", PreserveSig = false)]
static extern bool DwmIsCompositionEnabled();

The main point is to use Vista's Desktop Window Manager API via .NET Interop to apply the effect into a window handle. Thus, the method has to check for the OS version before trying to call those functions (Vista is Windows version 6.0, while Windows 7 is 6.1).

The interesting class is WindowInteropHelper. In WPF, we get the window handle using WPF's Window class. However, WPF is like the WinForms library in that you need to perform an actual window operation so that the actual window handle is created (e.g., by calling Show(), or, not sure if this works, the Hide() method).

Another useful class is HwndSource. This class is a bridge between the WPF world and the Win32 API world. It can take any window handle and can treat it like a WPF window. How it works exactly is still not clear to me. :/ But here, we use it to forcefully set the transparent background of our window class so that the WPF render engine won't paint a color over DWM's glass area.

To use this class, override OnSourceInitialized of the Window class and call it like this:

protected override void OnSourceInitialized(EventArgs e) {
    base.OnSourceInitialized(e);
    glassMaker = new GlassHelper(this, new Thickness(-1));
    glassMaker.ExtendGlassFrame();
}

I could not quickly find any useful information on OnSourceInitialized in MSDN. It is briefly described that it relates to the HwndSource class. My guess is it'll be called whenever the Window's handle is created.

Desktop Mode

Currently, the idea of Desktop mode is simple. Just push the Window to the bottom-most, right? But it's not that easy. WPF provides only the top-most mode. Fine, we can just use the Win32 API's SetWindowPos() to make it. The following code from the WindowExtension class shows how easy it is to do this:

static public void SetBottomMost(this Window w){
    var hwnd = new WindowInteropHelper(w).Handle;
    Debug.Assert(hwnd != IntPtr.Zero);
    Win32.SetWindowPos(hwnd, Win32.HWND_BOTTOM, 0, 0, 0, 0, 
       Win32.SWP_NOSIZE | Win32.SWP_NOMOVE | Win32.SWP_NOACTIVATE);
}

We are not done yet. You'll find out that our "Desktop mode" window is just like a normal window and that you can switch to and close it manually with Alt+F4. It doesn't look professional, does it? ^^; So here comes another trick, set the WS_EX_NOACTIVATE flag.

static public void SetNoActivate(this Window w){
    var hwnd = new WindowInteropHelper(w).Handle;
    Debug.Assert(hwnd != IntPtr.Zero);

    var newValue = Win32.GetWindowLong(hwnd, 
        Win32.GWL_EXSTYLE)|Win32.WS_EX_NOACTIVATE;

    Win32.SetWindowLong(hwnd, Win32.GWL_EXSTYLE, newValue);
}

And the usage is in the PageHost class:

public void SendToBottom(){
    this.SetNoActivate();
    this.SetBottomMost();
}

The flag prevents any window from being a foreground window. It also removes it from the taskbar. Sounds good. Yet another problem arises though:

When I create a tray icon with an About dialog, whenever the tray icon component shows a popup menu and it's closed, the focus of the pop up menu will fall back to our background window and activate it as a foreground window! This may be quickly fixed by calling SendToBottom() every time the window is activated, but it will still be brought to the foreground briefly and the effect is noticeable.

I fixed this issue by creating separate threads between the desktop-mode windows and the tray icon component, but since WPF's Application class is a singleton per appdomain, I had to create another separate AppDomain as well. :( This appdomain creation code can be found in the BackgroundSlideShowEngine class.

public void Start(PageHost[] slideShowList){
    var engineAssemblyPath = Assembly.GetExecutingAssembly().Location;
    Debug.Assert(engineAssemblyPath != null);
    var aboutDomain = AppDomain.CreateDomain("Background Domain");
    var helperTypeName = typeof (ForegroundDomain).FullName;
    var foregroundDomain = 
      (ForegroundDomain) aboutDomain.CreateInstanceFromAndUnwrap(
       engineAssemblyPath,helperTypeName);
    foregroundDomain.MainApplication = new SaverEngine(pictureSource, slideShowList);

    var aboutThread = new Thread(foregroundDomain.RunAbout);
    aboutThread.SetApartmentState(ApartmentState.STA);
    aboutThread.Start();

    screenSaverCheck.Start();
}

Notice that, like WinForms, all threads used with WPF must have Single Thread Apartment set.

Bitmap in WPF

Since most of graphic operations in WPF are based on vector, there are just a few classes that work with bitmap and most of them are derived from BitmapSource class. The most useful class is RenderTargetBitmap, as far as I know, it's the only class that can turn vector graphic into a bitmap. It plays the main role in Photo Collage screen saver especially in desaturation effect.

PhotoCollagePage class uses RenderTargetBitmap class for performing graphic operations and uses WriteableBitmap class for displaying the background. This page class overrides ArrangeOverride() to call resetViewBitmaps() with new size whenever the page size is changed.

void resetViewBitmaps(Size result) {
    viewBitmap = new RenderTargetBitmap
    (pageWidth, pageHeight, 96, 96, PixelFormats.Default);
    rebuildSurfaceBackground();
    // ...snipped...
}
void rebuildSurfaceBackground(){
    if (background != null){
        var imageRenderer = new Image
    {Source = background, Stretch = Stretch.UniformToFill};
        imageRenderer.Measure(new Size(viewBitmap.Width,viewBitmap.Height));
        imageRenderer.Arrange(new Rect(new Point(0,0), imageRenderer.DesiredSize));
        viewBitmap.Render(imageRenderer);
    }
    pageSurface.Source = currentViewBitmap = new WriteableBitmap(viewBitmap);
}
RenderTargetBitmap viewBitmap;
WriteableBitmap currentViewBitmap;
BitmapSource background;

Code in rebuildSurfaceBackgroun() shows how to paint bitmap (background) into viewBitmap object. It first creates a "WPF visual", Image object, sets the image source and the stretch mode, then calls Measure() and Arrange() respectively to imitate the layout mechanism of WPF, and finally calls Render() to transform the *adjusted* Image object into bitmap.

Both Measure() and Arrange() are methods of UIElement class. By explicitly calling these methods, you can arrange the UI element and its child with any size and position you want and it is a handy technique to paint the element behind the screen. :)

Picture Desaturation

There is currently no Desaturation effect in WPF and we have to implement it ourselves. It seems to me there is no method to direct access to bitmap memory (like WinForm's Bitmap class). However, BitmapSource class provides CopyPixels() to copy bitmap content to an array, and Create() to create a bitmap from an array. I've created RawBitmap class to wrap these ideas. The use of this class can be seen in PhotoCollagePage.desaturate().

static RawBitmap desaturate(BitmapSource bitmap){
    var raw = new RawBitmap(bitmap);
    var hsl = Hsla32.FromPbra32(raw.Data, raw.Width).Desaturate(0.87F, 1e-2F);
    return raw.CloneWithData(Rgba32.FromHsl(hsl));
}

Pixel data copied from BitmapSource has its format depending on BitmapSource.Format property. For simplicity, I use only PixelFormats.Pbgra32, which is 8 bits x 4 color channels (Red, Green, Blue, Alpha = Opacity). Class Hsla32 creates another array of 8 bits x 4 channels (Hue, Saturation, Lightness, Alpha) from Pbgra32 array and its Desaturate() just reduces the lightness value by the specific factor (0.87F in code) before being converted back to Pbgra32 again (I just notice Rgba32 is not the right name...it should be Bgra32 actually..)

This approach works fine but the desaturation effect is noticeable (if you focus on the screen: Perhaps it can be improved by making it one of WPF's Effect and applying animation on it.)

Future Improvements

  • Unexpected slow response of tray icon in Desktop mode
  • Enhance Desktop mode to display behind Desktop icons

History

  • 2009.12.07 - First posted with my limited time; more topics will be filled later
  • 2009.12.08 - Add topic 'Bitmap in WPF'

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here