Click here to Skip to main content
Click here to Skip to main content

Eye Dropper control in WPF

, 4 May 2012 CPOL
Rate this:
Please Sign up or sign in to vote.
Eye dropper control, used to pick color from the environment like the one in Expression Blend or Visual Studio Designer.

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

        // http://msdn.microsoft.com/en-us/library/dd144871(VS.85).aspx
        [DllImport("user32.dll")]
        public static extern IntPtr GetDC(IntPtr hwnd);

        // http://msdn.microsoft.com/en-us/library/dd183370(VS.85).aspx
        [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);

        // http://msdn.microsoft.com/en-us/library/dd183488(VS.85).aspx
        [DllImport("gdi32.dll")]
        public static extern IntPtr CreateCompatibleBitmap(IntPtr hdc, int nWidth, int nHeight);

        // http://msdn.microsoft.com/en-us/library/dd183489(VS.85).aspx
        [DllImport("gdi32.dll", SetLastError = true)]
        public static extern IntPtr CreateCompatibleDC(IntPtr hdc);

        // http://msdn.microsoft.com/en-us/library/dd162957(VS.85).aspx
        [DllImport("gdi32.dll", ExactSpelling = true, PreserveSig = true, SetLastError = true)]
        public static extern IntPtr SelectObject(IntPtr hdc, IntPtr hgdiobj);

        // http://msdn.microsoft.com/en-us/library/dd183539(VS.85).aspx
        [DllImport("gdi32.dll")]
        public static extern bool DeleteObject(IntPtr hObject);

        // http://msdn.microsoft.com/en-us/library/dd162920(VS.85).aspx
        [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
            {
                // gets the main desktop and all open windows
                sourceDC = InteropHelper.GetDC(InteropHelper.GetDesktopWindow());

                //sourceDC = User32.GetDC(hWnd);
                targetDC = InteropHelper.CreateCompatibleDC(sourceDC);

                // create a bitmap compatible with our target DC
                compatibleBitmapHandle = InteropHelper.CreateCompatibleBitmap(sourceDC, width, height);

                // gets the bitmap into the target device context
                InteropHelper.SelectObject(targetDC, compatibleBitmapHandle);

                // copy from source to destination
                InteropHelper.BitBlt(targetDC, 0, 0, width, height, sourceDC, x, y, InteropHelper.SRCCOPY);

                // Here's the WPF glue to make it all work. It converts from an
                // hBitmap to a BitmapSource. Love the WPF interop functions
                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);
                //Take a backup.

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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Jawahar Suresh Babu
Software Developer (Senior)
India India
Jawahar working as a Senior Development Engineer in Aditi Technologies,Bangalore, India. Specialist in all XAML frameworks. Very passionate on UX Design and Development. Skilled in Expression Blend, Design, WPF, Silverlight, Windows Phone 7/8, Windows 8. Good knowledge in Entity Framework, SQLite and SQL Server also. Also had good experience with PRISM, MVVM, Caliiburn Micro and other design patterns.
 
He developed few products for Syncfusion Inc. Also working on some freelancing projects. Worked as a lead developer of Metro Studio from Syncfusion Inc.
 
An active freelancer. http://xamlfactory.elance.com
 
http://about.me/jawahars
 
http://wpfplayground.com/
Follow on   Twitter   LinkedIn

Comments and Discussions

 
Bug[My vote of 1] Will crash on 64Bit systems PinmemberKamran Behzad29-Oct-13 19:17 
SuggestionHad problems - Suggest altering the code to make it more "crash-safe". PinmemberAC333-May-12 20:55 
GeneralRe: Had problems - Suggest altering the code to make it more "crash-safe". PinmemberJawahar Suresh Babu4-May-12 16:34 
Hi,
 
Thanks for your feedback. I understand the risk behind the implementation. And I have reverted the particular code in attached sample and source code. I will look for a way that change the cursor without affecting the registry and post here if I find something.
 
Thanks again. Big Grin | :-D
 
Regards,
Jawahar

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Terms of Use | Mobile
Web04 | 2.8.141220.1 | Last Updated 5 May 2012
Article Copyright 2012 by Jawahar Suresh Babu
Everything else Copyright © CodeProject, 1999-2014
Layout: fixed | fluid