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

Making of a Color Spy utility with WTL

0.00/5 (No votes)
30 Sep 2003 1  
Making of color picker utility using WTL and recap of clipboard management APIs.

Introduction

Want to check out colors used in a Web page? ColorSpy is a lightweight desktop utility written with WTL. It lets anyone who loves WTL play with colors. What ColorSpy does is three things:

  1. Samples a pixel color (well, colors) on screen
  2. Reads a RGB triplet (#rrggbb form) from HTML source and shows its color in color box (see screenshot shown above)
  3. Reads a color name and shows its color with RGB triplet

Environment

ColorSpy was created with VC++ 6.0 SP5 with WTL 7.0, and was mainly tested on my machine Win2k with SP3. To build, you will need WTL installed. At the time of writing this, MS WTL can be found here. If you haven't tried it out, go get it.

Picking up and sampling a pixel color on screen

In practice, I created a transparent window with WS_EX_TRANSPARENT style, which covered the entire screen, but soon realized that use of transparent window would not work; you can see all pixel colors through the transparent window, but mouse events won't get through.

The way ColorSpy picks up the pixel color was inspired by other utility apps (e.g. WindowFinder). It adapts one of the golden rules of Windows programming found in those utilities. The golden rule is that when you find there is no appropriate message to response to, you make it yourself. ColorSpy also uses a timer to create its application specific message (WM_APP_COLORSPY) periodically and then responses to the message by itself, picking up a color of the pixel located by mouse cursor:

  // Retrieve pixel color

  ::GetCursorPos (&m_cursor);
  HDC hdc = ::GetDC(NULL); //entire screen

  m_clrPixel = ::GetPixel(hdc, m_cursor.x, m_cursor.y);
  ::ReleaseDC(NULL, hdc);  //release dc

Controlling over the timer will be a key for sampling colors. When holding down CTRL+SHIFT, by killing the timer, ColorSpy samples a pixel color and stores it in colorbox. It becomes inactive until you hit the colorbox again.

  // Use CTRL+SHIFT keys for sampling pixel color

  if (::GetAsyncKeyState(VK_CONTROL) < 0 && 
                 ::GetAsyncKeyState(VK_SHIFT) < 0)
  {
    m_colorBox.Lock();
    ((CEdit)m_infoBox).SetReadOnly(FALSE);
  }

If you kept running timer instead, you could always resume color picking using CTRL+SHIFT as a toggle key... but customization is all up to you... there is always more than one way to do it.

Reading color codes from HTML source

When ColorSpy is inactive (while no timer ticking), ColorSpy has a different work to do; it reads a color code (in '#RRGGBB' form including preceding '#') from an HTML file. For example, you visit a web site and view source or open an HTML file in the editor you use.

Selecting the color code and simply pressing CTRL+C will give you a color you have selected.

To do this, ColorSpy utilizes a clipboard management feature called Clipboard viewer window, so it can read color codes clipped by the user. Here is a template class sample that turns ColorSpy into a clipboard viewer window for color-code-watching extension:

template <typename T> 
class CColorSpyCBViewer
{
public:
  HWND m_hWndNextCBViewer;
  bool m_bRegistered;

  BEGIN_MSG_MAP(CColorSpyCBViewer< T >)
    ALT_MSG_MAP(1)
    // for CWindowImpl based windows

    MESSAGE_HANDLER(WM_CREATE, OnCreate) 
    // for CDialogImpl based windows

    MESSAGE_HANDLER(WM_INITDIALOG, OnCreate) 
    MESSAGE_HANDLER(WM_DESTROY, OnDestroy)
    // Notification handlers

    MESSAGE_HANDLER(WM_DRAWCLIPBOARD, OnDrawClipboard)
    // necessary indeed

    MESSAGE_HANDLER(WM_CHANGECBCHAIN, OnChangeCbChain) 
  END_MSG_MAP()
          
  CColorSpyCBViewer() : m_hWndNextCBViewer(0), m_bRegistered(false) {}
  
  LRESULT OnCreate(UINT uMsg, WPARAM wParam, 
                  LPARAM lParam, BOOL& bHandled)
  {
    bHandled = FALSE;
    T* pT = static_cast<T*>(this);

    // Join the members of clipboard viewer chain

    m_hWndNextCBViewer = pT->SetClipboardViewer(); //can be nothing

    m_bRegistered = true;
    return 0;
  }
      
  LRESULT OnDestroy(UINT uMsg, WPARAM wParam, 
                   LPARAM lParam, BOOL& bHandled)
  {
    T* pT = static_cast<T*>(this);

    // Say good-bye to the members

    if (IsWindow(m_hWndNextCBViewer))
      pT->ChangeClipboardChain(m_hWndNextCBViewer);
    return 0;
  }
      
  LRESULT OnDrawClipboard(UINT uMsg, WPARAM wParam, LPARAM lParam, 
                            BOOL& /*bHandled*/)
  {
      // Viewer receives whenever the content of

      // the clipboard is changed

      // wParam is a handle to the window that

      // has put clipboard data

      
      T* pT = static_cast<T*>(this);
      
      // Registering this window as a CB wiewer window

      if (m_bRegistered == false) // calling ::SetClipboardViewer() in 

                                        // OnCreate handler

      {
            ATLTRACE("Registering... wParam: %#x\n", wParam);
            return 0; // do nothing but return gracefully

      }

    ATLTRACE("OnDrawClipboard(): Wnd %#x put CB data\n", wParam);

    if (IsClipboardFormatAvailable(CF_TEXT) && pT->OpenClipboard())
    {
      CString text;
      HANDLE hData = ::GetClipboardData( CF_TEXT );
      text = (char*)::GlobalLock( hData );
      ::GlobalUnlock( hData );
      CloseClipboard();

      pT->SetColor(text);
    }
    
    // Pass WM_DRAWCLIPBOARD to the next window

    // in clipboard viewer chain

    RouteMessage(uMsg, wParam, lParam);
    return 0;
  }
      
  LRESULT OnChangeCbChain(UINT /*uMsg*/, WPARAM wParam, LPARAM lParam, 
                           BOOL& /*bHandled*/)
  {
    T* pT = static_cast<T*>(this);

    // Clipboard wiewer window is supposed to

    // respond to WM_CHANGECBCHAIN 

    // to maintain the chain.

    HWND hWndRemove = reinterpret_cast<HWND>(wParam);
    HWND hWndAfter = reinterpret_cast<HWND>(lParam);
    
    if (m_hWndNextCBViewer == hWndRemove)
    {
          m_hWndNextCBViewer = hWndAfter;
    }
    
    // pass the WM_CHANGECBCHAIN (0x030D)

    RouteMessage(WM_CHANGECBCHAIN, wParam, lParam);
    return 0;
  }
  
  void RouteMessage(UINT uMsg, WPARAM wParam, LPARAM lParam)
  {
    // Toss WM_CHANGECBCHAIN message to the next Clipboard wiewer in the

    // viewer chain to notify of the update of chain.

    if (m_hWndNextCBViewer)
      ::SendMessage(m_hWndNextCBViewer, uMsg, wParam, lParam);
  }
};

Clipboard (viewer window) management

Previously, I stated incorrectly. CB viewer windows are really supposed to respond to WM_CHANGECBCHAIN message and refresh a pointer to a handler to next viewer window.

If you search web pages on clipboard viewer, you may find SDK documentation like this:

Creating a clipboard viewer window

A clipboard viewer window displays the current content of the clipboard, and receives messages when the clipboard content changes.

To create a clipboard viewer window, your application must do the following:

  • Add the window to the clipboard viewer chain.
  • Process the WM_CHANGECBCHAIN message.
  • Process the WM_DRAWCLIPBOARD message.
  • Remove the window from the clipboard viewer chain before it is destroyed.

Here is my observation and vital update of CB viewer on Windows 2000 SP3. That is, the result on your machine would be most likely the same:

  • Add the window to the clipboard viewer chain. => TRUE. With WTL, you do that with CWindow::SetClipboardWindow()
  • Process the WM_CHANGECBCHAIN message. => TRUE
  • Process the WM_DRAWCLIPBOARD message. => TRUE but not quite. SDK documentation says "WPARAM is not used...", or "must be zero...". WM_DRAWCLIPBOARD, however, comes with WPARAM filled, and WPARAM is actually a handle to the window that has placed CB data. Probably I would not have realized what WPARAM value was, if I was using MFC. WTL is nice.
  • Remove the window from the clipboard viewer chain before it is destroyed. => TRUE. Make sure to call CWidnow::ChangeClipboardChain() on your way out.

The documentation on clipboard management API may deserve some remarks. Or does it?

Color names. Tell me your name.

If sampled color value matches the RGB value defined in Color name-to-RGB value table, ColorSpy shows its name in ToolTip. While ColorSpy is inactive, you can try color names like, darkviolet, navy, navajowhite and so on.

Before we start, you might want to pull up the source code for this update. To support color names (hereinafter called colornames), ColorSpy utilizes a modified version of RGB color name-to-value table defined in rgbtable.h, which was taken from Xpm library from XFree86 (See References). As supplemental information, ColorSpy displays color name as a tooltip text only if the sampled pixel color value matches any RGB value defined in the table. In addition to color triplets (#RRGGBB), it can also interpret a color name and display its color and RGB triplet by looking up the table.

I never thought this color table support would eventually lead to another story to tell you about.

While testing ColorSpy with colornames, I found that there is some unique color name interpretation in web browsers. Can you tell the names of the following colors rendered beautifully? Take a guess. You can run ColorSpy to check RGB values of these colors now.

Now time is up, sir. If you thought these were Green something, nice try. These are called Gray (grayXX). Unlike those browser unrecognizable color names, with which resultant color may depend on the type of browser you use. As you can see, these names are, however, somehow interpreted.

Because of this interpretation - RGB(0x00, 0xA0, 0xXX), where XX is numeric part of Gray color names except for gray100, which is black - I had to comment out the grayXX part from the original color table to avoid confusion. Color names may be informative, but they may not be that essential. Once you played around with color names and found your favorite color name, you can use triplet of the color instead. Oh BTW, this interpretation is a common among the browsers. Bottom line - Confusing yet intriguingly worthwhile.

Quick check on Desktop color settings

This feature is rather for developers, not for web designers. Instead of checking with Display Properties applet in Control Panel, you can check your desktop color settings from system color types (COLOR_XXXX defined in WinUser.h). For example,

  • COLOR_INFOBK, which is background color of ToolTips
  • COLOR_ACTIVECAPTION, which is color of the title bar for the active window (Go to Control Panel > Display > Color1 in Appearance)
  • COLOR_GRADIENTACTIVECAPTION, which is active titlebar background color. (Color2)

All system color types are defined in CSysColors class. There are around 30 different display aspects as system color types. In colorbox.h, CSysColors is also used to determine whether ColorSpy should use system color brush based on your desktop settings as shown below:

#define HANDLE_SYS_COLOR_BRUSH(index) \
  if (m_clrBackground == theSystemColors[index].clr) \
  { \
    return brh.CreateSysColorBrush(index); \
  }
    
  HBRUSH CreateBrush()
  {
    // Use system cache

    CBrushHandle brh;

    // whatever color is requested, try matching your system color settings

    // first; chances are the most of time it will hit one of them if you 

    // are not exploring the world of colors.


    HANDLE_SYS_COLOR_BRUSH(COLOR_BACKGROUND); // Desktop color (same as 

                     // COLOR_DESKTOP which sounds more descriptive)

    [...] // rest omitted

  }

When your mouse makes you nuts

You have tried out the earlier version and realized that it is hard to work on tity pictures. Why not try magnifier? This feature may be helpful especially when sampling pixel colors in a dense-colored picture.

Some utilities offer a magnifier, presents a zoomed picture, but somewhat make you feel like a machine when working on a single pixel. Why? Because you still have to pin-point a single pixel on on-screen while watching the zoomed picture. After all 2 px. mouse movement is still 2px. distance so no matter how hard you look at the magnifier, it only tells you the mouse cursor is still pointing to the wrong spot unless you can work on the zoomed picture.

ColorSpy's magnifier acts like a clickable panel with variable magnification levels. So take a snapshot first. You don't have to pin-point the pixel on screen. Then click a magnified image as many as you like. I hope with this feature you no longer have to fire up another magnifier application just for easier viewing. And next time you feel like a machine, you might go like "stinky mouse or is it? software? (possibly, with creepy background music)".

Simplicity over fancy stuff

My ColorSpy is a simple app. I am hoping it will be part of your desktop goodies because of its simplicity. With source code you can make your own ColorSpy. Idea is all yours.

This is my first attempt to write an article and I have always admired tireless work of folks here at CodeProject, contributing their articles. Crappy rating would be welcome if you could first tell me the reason why you have read my article...

Happy WTL programming!

References

  • rgbtable.h - hard coded rgb.txt RGB color database. FTP

History

  • 7/6/2003 Initial.
  • 7/20/2003 Colorname for designers. System color types for developers.
  • 7/24/2003 Updated article due to my bogus description on Clipboard (viewer window) management. See discussion below. Thank you, Mike-.
  • 9/29/2003 Introduced new features, clickable magnifier and Hue-Saturation-Lightness (Brightness) display...

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