Introduction
Usually we come across different types of eye dropper controls in designers. We can move the mouse over the desktop and other applications to pick the color under the mouse. A normal eye dropper will pick color only within the application like the one in Adobe Illustrator or Photoshop. But the control I posted here will helps you to choose color from anywhere even outside your application like the one in Expression Blend or Visual Studio Designer.
Background
http://wpfplayground.blogspot.in/2012/04/change-windows-cursor-globally-in-wpf.html
http://wpfplayground.blogspot.in/2012/04/capture-screenshot-in-wpf.html
http://www.pinvoke.net/
Using the code
The base idea is to pick color from the screen wherever the mouse moving. The underlying magic behind the implementation is, need to take a snap shot of the entire desktop. For every mouse move we going to pick the appropriate pixel information from the image.
Capturing Screenshot
Lets start with capturing the screen shot,
Capturing the screenshot is pretty easy with Windows Forms. But in WPF, we need to call pinvoke methods to do that. We need few methods from User32.dll and gdi32.dll.
public class InteropHelper
{
[DllImport("user32.dll")]
public static extern IntPtr GetDesktopWindow();
[DllImport("user32.dll")]
public static extern IntPtr GetDC(IntPtr hwnd);
[DllImport("gdi32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool BitBlt(IntPtr hDestDC, int x, int y, int nWidth, int nHeight, IntPtr hSrcDC, int xSrc, int ySrc, Int32 dwRop);
[DllImport("gdi32.dll")]
public static extern IntPtr CreateCompatibleBitmap(IntPtr hdc, int nWidth, int nHeight);
[DllImport("gdi32.dll", SetLastError = true)]
public static extern IntPtr CreateCompatibleDC(IntPtr hdc);
[DllImport("gdi32.dll", ExactSpelling = true, PreserveSig = true, SetLastError = true)]
public static extern IntPtr SelectObject(IntPtr hdc, IntPtr hgdiobj);
[DllImport("gdi32.dll")]
public static extern bool DeleteObject(IntPtr hObject);
[DllImport("user32.dll")]
public static extern int ReleaseDC(IntPtr hwnd, IntPtr dc);
}
Using these Interop Helpers take a screen shot of your desktop. The screen capture method will get parameters like X, Y, Width and height parameters. Since we going to take snap shot of the entire screen, get Width and Height of the screen using the static class SystemParameters.
public static BitmapSource CaptureRegion(IntPtr hWnd, int x, int y, int width, int height)
{
IntPtr sourceDC = IntPtr.Zero;
IntPtr targetDC = IntPtr.Zero;
IntPtr compatibleBitmapHandle = IntPtr.Zero;
BitmapSource bitmap = null;
try
{
sourceDC = InteropHelper.GetDC(InteropHelper.GetDesktopWindow());
targetDC = InteropHelper.CreateCompatibleDC(sourceDC);
compatibleBitmapHandle = InteropHelper.CreateCompatibleBitmap(sourceDC, width, height);
InteropHelper.SelectObject(targetDC, compatibleBitmapHandle);
InteropHelper.BitBlt(targetDC, 0, 0, width, height, sourceDC, x, y, InteropHelper.SRCCOPY);
bitmap = System.Windows.Interop.Imaging.CreateBitmapSourceFromHBitmap(
compatibleBitmapHandle, IntPtr.Zero, Int32Rect.Empty,
BitmapSizeOptions.FromEmptyOptions());
}
catch (Exception ex)
{
}
finally
{
DeleteObject(compatibleBitmapHandle);
ReleaseDC(IntPtr.Zero, sourceDC);
ReleaseDC(IntPtr.Zero, targetDC);
}
return bitmap;
}
Method invoke:
InteropHelper.CaptureRegion(InteropHelper.GetDesktopWindow(),(int)SystemParameters.VirtualScreenLeft,(int)SystemParameters.VirtualScreenTop, (int)SystemParameters.PrimaryScreenWidth,(int)SystemParameters.PrimaryScreenHeight);
Global Mouse position
Now we are done with taking the screen shot. Lets pick the color from the appropriate pixel by matching the mouse position. So now we need the Mouse Move event for not only the entire application but also outside of the app to get the mouse position. To achieve a global mouse move hook, we need some native method calls as explained in this article. But this is little bit complex. So I have started a timer while clicking the eye dropper button. And each tick of the timer, I am getting the mouse position using the following code,
System.Drawing.Point _point = System.Windows.Forms.Control.MousePosition;
Copy Pixel Information
Now we are done with getting the mouse position. Using this position get the appropriate pixel information from the BitmapSource
that we have taken. BitmapSource.CopyPixel
will give you an array of bytes, in which the first 3 values are enough to find the color.
int stride = (screenimage.PixelWidth * screenimage.Format.BitsPerPixel + 7) / 8;
pixels = new byte[screenimage.PixelHeight * stride];
Int32Rect rect = new Int32Rect((int)point.X, (int)point.Y, 1, 1);
screenimage.CopyPixels(rect, pixels, stride, 0);
rectcolor.Fill = new SolidColorBrush(Color.FromRgb(pixels[2], pixels[1], pixels[0]));
Global Mouse cursor
(The following implementation has not included in the attached sample and source code for safety reasons, since it will affect the client registry values. It was considered the following code may risky in certain conditions and ignored in the sample.)
Everything is fine except the mouse cursor. It is very obvious that we can change the cursor in WPF using FrameworkElement.Cursor
. But the trick is, it only works within your application and not outside your application Main Window. In case if you want to change the cursor for the entire OS, we don't have any direct way in WPF. But most of the developers worried why we need to change the entire Windows cursor. But take an example, if we are developing an eye dropper control in WPF (used to pick color). Not like the one in Illustrator or Photoshop (cannot pick color outside the application), but the one we have in Expression Blend or Visual Studio designer (can pick color even outside the application also).
In that cases, the cursor should be changed, because arrow cursor will not be a comfortable one to pick color. Normally cursor values resides in registry.
Registry Key : HKEY_CURRENT_USER\Control Panel\Cursors
Changing the values here will change the cursor, but your system needs a reboot to take effect (I can understand, none of the developers will accept this). To avoid that and make your app. taking immediate effect, you need to invoke a pinvoke
call.
The following method will refresh the cursor values,
[DllImport("user32.dll", EntryPoint = "SystemParametersInfo")]
public static extern bool SystemParametersInfo(int uAction, int uParam, string lpvParam, int fuWinIni);
Iterate through registry values and change the cursor path.
private void ChangeCursor()
{
RegistryKey pRegKey = Registry.CurrentUser;
pRegKey = pRegKey.OpenSubKey(@"Control Panel\Cursors");
paths.Clear();
foreach (var key in pRegKey.GetValueNames())
{
Object _key = pRegKey.GetValue(key);
paths.Add(key, _key.ToString());
Object val = Registry.GetValue(@"HKEY_CURRENT_USER\Control Panel\Cursors", key, null);
Registry.SetValue(@"HKEY_CURRENT_USER\Control Panel\Cursors", key, "foo.cur");
}
SystemParametersInfo(InteropHelper.SPI_SETCURSORS, 0, null, InteropHelper.SPIF_UPDATEINIFILE | InteropHelper.SPIF_SENDCHANGE);
}
Make sure you store the registry values before change it, so that you can restore the cursor to the default values.
private void ResetCursorToDefault()
{
RegistryKey pRegKey = Registry.CurrentUser;
pRegKey = pRegKey.OpenSubKey(@"Control Panel\Cursors");
foreach (string key in paths.Keys)
{
string path = paths[key];
Registry.SetValue(@"HKEY_CURRENT_USER\Control Panel\Cursors", key, path);
}
InteropHelper.SystemParametersInfo(InteropHelper.SPI_SETCURSORS, 0, null, InteropHelper.SPIF_UPDATEINIFILE | InteropHelper.SPIF_SENDCHANGE);
}