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
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();
}
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'