Oberflächen mit PixelShadern bearbeiten

In diesem Beispiel möchte ich zeigen, wie leicht man Effekte mit Pixelshadern auf den Bildschirm zaubern kann.

Dazu erstellen wir hier eine kleine Anwendung, die als erstes einen Screenshot vom aktuellen Bildschirm macht und dem Benutzer im Vollbild anzeigt. Genau genommen bedeutet das, das wir dem Benutzer versuchen zu seinen Originalbildschirm vorzutäuschen und mit einigen Effekten zu versehen.

Dazu erstellen wir als erstes eine neue Windows Presentation Foundation-Anwendung mit Visual Studio:

image

Als nächsten machen wir einen Screenshot vom Bildschirm.

Wie erstellt man einen Screenshot vom aktuellen Bildschirm?

In der .NET-Bibliothek “System.Windows.Forms” befindet sich der Namespace SystemInformation. Mit diesem lassen sich Informationen über den Bildschirm ermitteln. Diese nehmen wir als Hilfe für die CaptureScreen-Methode

  
 public Bitmap CaptureScreen()
 {
     Bitmap bmp = new Bitmap(SystemInformation.VirtualScreen.Width, SystemInformation.VirtualScreen.Height);
     Graphics g = Graphics.FromImage(bmp);
     g.CopyFromScreen(0, 0, 0, 0, bmp.Size);
     g.Dispose();
     return bmp;
 }

Die Bitmap-Klasse befindet sich in der Bibliothek “System.Drawing”.

Um aus einem Bitmap eine BitmapSource für ein WPF-Image machen zu können benötigt man die folgende Methode:

  
     public static BitmapSource CreateBitmapSource(System.Drawing.Bitmap bmp)
     {
         return Imaging.CreateBitmapSourceFromHBitmap(
             bmp.GetHbitmap(),
             IntPtr.Zero,
             Int32Rect.Empty,
             BitmapSizeOptions.FromEmptyOptions());
     }

Diese Methode rufen wir direkt zum Start auf, und weisen das Ergebnis einem Image-Objekt zu, das wir in der MainWindow.xaml wie folgt definiert haben.

 <Window
     x:Class="PixelShaderWPF.MainWindow"
     xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
     xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
     Title="MainWindow"
     Height="350"
     Width="525">
     <Grid>
         <Image
             x:Name="backgroundImage" />
     </Grid>
 </Window>

Der gesamte Code bisher:

 public partial class MainWindow : Window
 {
    public MainWindow()
    {
        InitializeComponent();
  
        // Create Screen Shot
        Bitmap bmp = CaptureScreen();
        BitmapSource bmpSrc = CreateBitmapSource(bmp);
        backgroundImage.Source = bmpSrc;
    }
  
    public Bitmap CaptureScreen()
    {
        Bitmap bmp = new Bitmap(SystemInformation.VirtualScreen.Width, SystemInformation.VirtualScreen.Height);
        Graphics g = Graphics.FromImage(bmp);
        g.CopyFromScreen(0, 0, 0, 0, bmp.Size);
        g.Dispose();
        return bmp;
    }
  
    public static BitmapSource CreateBitmapSource(System.Drawing.Bitmap bmp)
    {
        return Imaging.CreateBitmapSourceFromHBitmap(
            bmp.GetHbitmap(),
            IntPtr.Zero,
            Int32Rect.Empty,
            BitmapSizeOptions.FromEmptyOptions());
    }
 }

Wenn wir die Anwendung nun starten, erhalten wir die folgende Ansicht:

image

Bitte nicht wundern, ich habe zwei Bildschirme, und die Screenshot-Methode macht direkt von beiden einen Screenshot.

Jetzt wollen wir dem Benutzer dieses Bild über seinen gesamten Bildschirm legen und ihn damit vormachen, er würde auf seinem normalen Bildschirm arbeiten. Damit er aber jederzeit wieder in den normalen Modus kommt, geben wir ihm einen Schließen-Button.

 <Button
     x:Name="closeWindow"
     Content="Close Fake App"
     Click="closeWindow_Click" 
     VerticalAlignment="Top"
     HorizontalAlignment="Right"/>

Dahinter liegt der folgende Eventhandler:

 private void closeWindow_Click(object sender, RoutedEventArgs e)
 {
     App.Current.Shutdown();
 }

Wie kann man am leichtesten seine Anwendung in den Vollbildschirm-Modus schalten?

Die Windows-Bibliothek user32.dll beinhaltet Funktionen mit denen man seinen Bildschirm “screenshoten” kann. Diese Funktionalität habe ich in die Klasse FullScreenHelper ausgelagert, damit der Code übersichtlich bleibt.

 public static class FullScreenHelper
 {  
     public static void GoFullscreen(this Window window)  
     {      
         // Make window borderless      
         window.WindowStyle = WindowStyle.None;      
         window.ResizeMode = ResizeMode.NoResize;      
         
         // Get handle for nearest monitor to this window      
         WindowInteropHelper wih = new WindowInteropHelper(window);      
         IntPtr hMonitor = MonitorFromWindow(wih.Handle, MONITOR_DEFAULTTONEAREST);      
  
         // Get monitor info      
         MONITORINFOEX monitorInfo = new MONITORINFOEX();    
         monitorInfo.cbSize = Marshal.SizeOf(monitorInfo);     
         GetMonitorInfo(new HandleRef(window, hMonitor), monitorInfo);    
  
         // Create working area dimensions, converted to DPI-independent values  
         HwndSource source = HwndSource.FromHwnd(wih.Handle);    
         if (source == null) return; 
         // Should never be null     
         if (source.CompositionTarget == null) 
             return; 
  
         // Should never be null     
         Matrix matrix = source.CompositionTarget.TransformFromDevice;    
         RECT workingArea = monitorInfo.rcMonitor;    
         Point dpiIndependentSize =       
             matrix.Transform(         
             new Point(            
                 workingArea.Right - workingArea.Left,  
                 workingArea.Bottom - workingArea.Top));   
         // Maximize the window to the device-independent working area ie   
         // the area without the taskbar.    
         // NOTE - window state must be set to Maximized as this adds certain  
         // maximized behaviors eg you can't move a window while it is maximized, 
         // such as by calling Window.DragMove     
         window.MaxWidth = dpiIndependentSize.X;    
         window.MaxHeight = dpiIndependentSize.Y;     
         window.WindowState = WindowState.Maximized;
     }  
     
     // Nearest monitor to window  
     const int MONITOR_DEFAULTTONEAREST = 2; 
     
     // To get a handle to the specified monitor  
     
     [DllImport("user32.dll")]  
     static extern IntPtr MonitorFromWindow(IntPtr hwnd, int dwFlags); 
  
     // To get the working area of the specified monitor 
     [DllImport("user32.dll")] 
     public static extern bool GetMonitorInfo(HandleRef hmonitor, [In, Out] MONITORINFOEX monitorInfo);  
     
     // Rectangle (used by MONITORINFOEX)  
     [StructLayout(LayoutKind.Sequential)]  
     public struct RECT 
     {     
         public int Left;   
         public int Top;     
         public int Right;     
         public int Bottom;  
     } 
     
     // Monitor information (used by GetMonitorInfo()) 
     [StructLayout(LayoutKind.Sequential)]  
     public class MONITORINFOEX  
     {   
         public int cbSize;  
         public RECT rcMonitor;
         // Total area   
         public RECT rcWork;
         // Working area  
         public int dwFlags;  
         [MarshalAs(UnmanagedType.ByValArray, SizeConst = 0x20)]     
         public char[] szDevice;
     }
 }

Diese verwenden wir direkt zu beginn in unserer Anwendung im Loaded-Ereignis.

 this.Loaded += (s, e) =>
     {
         // Activate Fullscreen
         FullScreenHelper.GoFullscreen(this);
     };

Möchte man nun auf diesen Fake-Bildschirm einen Pixelshader-Effekt anwenden, kann man sehr einfach einen der einbauten Effekte, wie z.B. Blur verwenden. Dazu habe ich einige Buttons in die MainWindow.xaml eingebaut und mit Eventhandlern versehen.

 <StackPanel 
     HorizontalAlignment="Left">
     <Button
         Content="Reset all Effects"
         x:Name="resetEffects"
         Click="resetEffects_Click" />
     <Button
         Content="Blur Effect"
         x:Name="blurEffect" 
         Click="blurEffect_Click"/>
     <Button
         Content="Monochrome Effect"
         x:Name="monochromeEffect"
         Click="monochromeEffect_Click" />
 </StackPanel>

Die Eventhandler sind wie folgt definiert in der MainWindow.xaml.cs-Datei:

 private void blurEffect_Click(object sender, RoutedEventArgs e)
 {
    System.Windows.Media.Effects.BlurEffect blur = new System.Windows.Media.Effects.BlurEffect();
    backgroundImage.Effect = blur;
 }
  
 private void monochromeEffect_Click(object sender, RoutedEventArgs e)
 {
    Microsoft.Samples.ShaderEffects.MonochromeEffect monochrome = new Microsoft.Samples.ShaderEffects.MonochromeEffect();
    backgroundImage.Effect = monochrome;
 }
  
 private void resetEffects_Click(object sender, RoutedEventArgs e)
 {
    backgroundImage.Effect = null;
 }

Wichtiger Hinweis: Leider bringt WPF nur wenige Pixelshader-Effekte mit sich. Weitere Pixelshader befinden sich in allerdings im Win7ToGo-Projekt auf Codeplex (Link)

Der Blur-Effekt sieht da wie folgt aus:

image

Der Monochrome-Effekt sieht so aus:

image

Damit kann man schon so manchen Benutzer “verwirren” und einige Späße machen.

Treibt man nun das Ganze etwas weiter, so kann man die Effekte auch mit Animationen verbinden. Ein Beispiel wäre z.B. das man beim Klicken der Maus einen Ripple/Tropfen-Effekt auf die Oberfläche zaubert.

image

Dazu benötigt man wirklich nicht viel Code.

 void MainWindow_MouseLeftButtonDown(object sender, System.Windows.Input.MouseButtonEventArgs e)
 {
     System.Windows.Point p = e.GetPosition(this);
     Microsoft.Samples.ShaderEffects.RippleEffect ripple = new Microsoft.Samples.ShaderEffects.RippleEffect();
     
     double centerX = p.X / backgroundImage.ActualWidth;
     double centerY = p.Y / backgroundImage.ActualHeight;
     ripple.Center = new System.Windows.Point(centerX, centerY);
     ripple.Amplitude = .5;
     ripple.Frequency = 50;
     ripple.Phase = 0;
     
     backgroundImage.Effect = ripple;
  
     DoubleAnimation da = new DoubleAnimation();
     da.From = 0;
     da.To = 50;
     da.AutoReverse = true;
  
     da.Duration = new Duration(TimeSpan.FromSeconds(5));
     ripple.BeginAnimation(Microsoft.Samples.ShaderEffects.RippleEffect.FrequencyProperty, da);
 }

Das gesamte Beispiel kann hier heruntergeladen werden: Codeplex-Projekt