7.1 Color picker with X11

by Marko Riedel, from a suggestion by Stefan Urbanek

7.1.1 Idea

This recipe is very simple. We build an application with a single window that contains a color well. There are two entries on the application’s menu: one to quit the application, and another to pick a color. The cursor changes to a red target cursor when the user clicks the latter, and she may then copy the color of any pixel on the screen to the color well by clicking on the pixel. The color picker code is isolated in a function that is called GSReadPixelFromScreen. There is a controller class that acts as a delegate to the application and responds to the action pick:.

7.1.2 Implementation

Start with the usual include directives. We need the cursor font for the target cursor.

 
#include <Foundation/Foundation.h> 
#include <AppKit/AppKit.h> 
 
#include <X11/Xlib.h> 
#include <X11/cursorfont.h>

The function GSReadPixelFromScreen picks a color in several steps:

The first step is to obtain the current display and its root window.

 
NSColor *GSReadPixelFromScreen() 
{ 
    Display *display = XOpenDisplay(NULL); 
    Window root = RootWindow(display, DefaultScreen(display));

We prepare the cursor from the font cursor with a target cross shape by setting the color components of its foreground and background colors. The foreground is red (RGB is (max,0,0)) and the background white (RGB (max,max,max).) The color components are unsigned, so we get the maximum value by using -1 without having to know the actual size of the components.

 
    Cursor cursor = XCreateFontCursor(display, XC_tcross); XColor fgc, bgc; 
 
    fgc.red = -1; fgc.green = 0; fgc.blue = 0; 
    bgc.red = -1; bgc.green = -1; bgc.blue = -1; 
    XRecolorCursor(display, cursor, &fgc, &bgc);

The next step is to grab the pointer. No other application will receive button clicks until we explicitly release the pointer. We must wait until the user chooses a pixel, i.e. until XNextEvent returns a button release, in which case we release the pointer. We receive no other events because the event mask argument to XGrabPointer only includes button release events.

 
    XGrabPointer(display, root, 0, ButtonReleaseMask, 
                 GrabModeAsync, GrabModeAsync, None, 
                 cursor, CurrentTime); 
 
    XEvent event; 
    XNextEvent(display, &event); 
 
    XUngrabPointer(display, CurrentTime);

The location of the button press in root window coordinates tells us what pixel to fetch. We ask the server for a one pixel image at that location and read the pixel once we have the image. We may then free the image since it is no longer needed.

 
    XImage *ximage = 
        XGetImage(display, root, 
                   event.xbutton.x_root, event.xbutton.y_root, 
                   1, 1, -1, ZPixmap); 
    unsigned long p = XGetPixel(ximage, 0, 0); 
    XDestroyImage(ximage);

We require the color components rather than the pixel value, so we query the server for the color components of the selected pixel in the default colormap. (We could have extracted the components from the pixel value ourselves e.g. for TrueColor visuals, but we choose to keep it simple.)

 
    XColor result; result.pixel = p; 
    XQueryColor(display, 
                DefaultColormap(display, DefaultScreen(display)), 
                &result);

This concludes the definition of the color picker function. We have the color components and use them to obtain the appropriate NSColor object, taking care to scale the values; -1 provides the maximum value for each component when converted to an unsigned short integer.

 
    #define MX ((unsigned short)-1) 
    return 
        [NSColor colorWithDeviceRed:(float)result.red/MX 
                 green:(float)result.green/MX 
                 blue:(float)result.blue/MX 
                 alpha:1.0]; 
}

It remains to implement the controller. It must assemble the window with the color well after the application finishes launching and pick colors when the user clicks the corresponding menu item.

 
@interface Controller : NSObject 
{ 
    NSColorWell *well; 
} 
 
- (void)applicationDidFinishLaunching:(NSNotification *)notif; 
- pick:(id)sender; 
 
@end

First define the dimensions of the window’s content view, allocate the window and initialize it with the title and the minimum size.

 
@implementation Controller 
 
#define WELLDIM 200 
 
- (void)applicationDidFinishLaunching:(NSNotification *)notif 
{ 
    NSRect wframe = 
        NSMakeRect(0, 0, WELLDIM, WELLDIM); 
    NSWindow *window = 
        [[NSWindow alloc] 
            initWithContentRect:wframe 
            styleMask:NSTitledWindowMask  NSResizableWindowMask 
            backing:NSBackingStoreBuffered 
            defer:NO]; 
    [window setMinSize:wframe.size]; 
    [window setTitle:@”Pick”];

The well has the same size as the window’s content frame and starts out containing the color “blue.”

 
    well = [[NSColorWell alloc] initWithFrame:wframe]; 
    [well setColor:[NSColor blueColor]];

We place the well in the view hierarchy by making it the window’s content view. We are done with the window, so we center it and place it on screen.

 
    [window setContentView:well]; 
    [window center]; 
    [window makeKeyAndOrderFront:self]; 
}

The action pick is very simple: it invokes GSReadPixelFromScreen and sets the well’s color to the color that it returns.

 
- pick:(id)sender 
{ 
    [well setColor:GSReadPixelFromScreen()]; 
    return self; 
} 
 
@end

The main function allocates an autorelease pool, the controller and the application object.

 
int main(int argc, char** argv, char **env) 
{ 
    NSAutoreleasePool *pool = [NSAutoreleasePool new]; 
 
    Controller *con = [Controller new]; 
 
    NSApplication *app = [NSApplication sharedApplication];

The menu contains an entry that quits the application and another one that invokes the color picker.

 
    NSMenu *menu = [NSMenu new]; 
    [menu addItemWithTitle: @”Pick” 
          action:@selector(pick:) 
          keyEquivalent:@””]; 
    [menu addItemWithTitle: @”Quit” 
          action:@selector(terminate:) 
          keyEquivalent:@”q”]; 
    [NSApp setMainMenu:menu];

It remains to connect the controller to the application and start the event loop.

 
    [app setDelegate:con]; 
    [app run]; 
 
    [pool release]; 
    exit(0); 
}