Swing Hack 8: An eyedropper tool

On the plane back from California I decided I've had enough with politics for a while and I'm ready to get back to coding. One thing I've always thought was missing from Swing is a good color chooser. Swing provides a color chooser model and a default color chooser, but it's always felt unfinished. Another 3rd party opportunity I suppose.

In my ideal color chooser we would have several different ways of selecting color, varying by color space model. This is pretty straightforward as you can see from the [Color Selector Tutorial]. What's not so easy in a Photoshop style eyedropper. If you aren't familiar with it, this is a tool that lets you click anywhere on the screen to select the color under the cursor.

Most paint tools give you an eyedropper, but I've never seen a Java program do it. This is because getting a screenpixel requires native access, usually locked off from Java programs. Java 1.3 introduced a new method to the Robot class, getPixelColor(), which can retrieve the color anywhere on the screen. The problem is that you don't get mouse events once the cursor leaves your JFrame. Fine if you only want to select colors from your own application, but we want to select anywhere on the screen. Java 1.5 introduces new APIs for getting complete mouse events, but that doesn't help us today. (though I'll be covering the 1.5 additions in future articles).

The answer to this tricky problem, of course, is to cheat! The program below makes a screenshot and then paints it into a JFrame which fills the entire screen. Our screenshot is indistingiushable from the real desktop except that nothing in the background updates. However, since we only need this while the user selects a color it should work fine. What I've designed is a color scheme selector with any eyedropper. Once the user selects a color by clicking somewhere on the screen the panel will show compatible colors by determining the midpoint between the selected color and the max/min values of brightness and saturation.

Here's what it will look like:

Eye Dropper

The following code is built on a simple prototype Swing framework I've been working on (a desktop equivalent to Servlet and Applet). It defines a basic lifecycle of init(), initComponents(), initLayout(), and initEventHandlers().

First we need to calculate the size of the screen and make our screenshot.

package net.java.demo.colordesigner;

import java.text.*;
import java.util.List;
import java.awt.*;
import javax.swing.*;
import net.java.swing.application.*;
import java.awt.event.*;
import javax.swing.event.*;

public class ColorDesigner extends 
                SingleFrameApplication {

    public JButton eyedropper, quit;
    public JComponent colormap;
    public JFrame rootFrame;
    public Image background_image;
    public Robot robot;
    public Dimension screen_size;
    public Container contentPane;
    public JComponent button_panel;
    public JPanel image_panel;
    public JPanel control_panel;
    public JPanel color_panel;
    public ColorLabel selected_color;
    public ColorLabel color_rich, color_pale, 
                color_bright, color_dark;
    public Font color_font;

    /* init code */
    public void init(JFrame rootFrame, 
                  Container contentPane, List args) {
        try {
        this.rootFrame = rootFrame;
        this.contentPane = contentPane;
        this.color_font = new Font("Monospaced",Font.PLAIN,14);
        
        // take a screenshot
        screen_size = Toolkit.getDefaultToolkit().getScreenSize();
        Rectangle rect = new Rectangle(0,0,
             (int)screen_size.getWidth(),
             (int)screen_size.getHeight());
        this.robot = new Robot();
        background_image = robot.createScreenCapture(rect);
        
        super.init(rootFrame,contentPane,args);
        
        } catch (Exception ex) {
            p(ex.toString());
        }
        
    }

In initComponents() we expand the frame to fill the screen and turn off the window decorations (the borders, title, and min/max buttons) Next we fill it with a JPanel that just paints the screenshot to the background and holds the sub components. The rest is your usual collection of panels and buttons.

    public void initComponents(Container contentPane) {
        rootFrame.setSize(screen_size);
        rootFrame.setUndecorated(true);
        
        image_panel = new JPanel() {
            public void paintComponent(Graphics g) {
                super.paintComponent(g);
                g.drawImage(background_image,0,0,null);
            }
        };
        image_panel.setPreferredSize(screen_size);
        
        control_panel = new JPanel();
        control_panel.setSize(200,200);

        button_panel = new Box(BoxLayout.X_AXIS);
        quit = new JButton("Quit");
        eyedropper = new JButton("Eye Dropper");
        
        color_panel = new JPanel();
        selected_color = createLabel();
        color_bright = createLabel();
        color_dark = createLabel();
        color_pale = createLabel();
        color_rich = createLabel();
    }

    public ColorLabel createLabel() {
        ColorLabel label = new ColorLabel();
        label.setFont(color_font);
        label.setOpaque(true);
        label.setBackground(Color.blue);
        return label;
    }

I'll skip showing the initLayout function since it's a very straightforward gridbag layout. In initEventHandlers() we subclass MouseInputAdapter to set the selected color on every mouse press and drag. getSMidpoint() and getBMidpoint() calculate the midpoints in HSB color space (Hue, Saturation, and Brightness) between the two specified colors.

    public void initEventHandlers() {

        MouseInputAdapter mia = new MouseInputAdapter() {
            public void mousePressed(MouseEvent evt) {
                setSelectedColor(robot.getPixelColor(evt.getX(),
                         evt.getY()));
            }
            public void mouseDragged(MouseEvent evt) {
                setSelectedColor(robot.getPixelColor(evt.getX(),
                         evt.getY()));
            }
        };
        
        image_panel.addMouseListener(mia);
        image_panel.addMouseMotionListener(mia);
    }
    
    public void setSelectedColor(Color color) {
        selected_color.setColor(color);
        color_bright.setColor(getBMidpoint(color,Color.white));
        color_dark.setColor(getBMidpoint(color,Color.black));
        color_rich.setColor(getSMidpoint(color,Color.red));
        color_pale.setColor(getSMidpoint(color,Color.white));
    }
    
    
    public Color getSMidpoint(Color start, Color end) {
        float[] hsb1 = Color.RGBtoHSB(start.getRed(),
                    start.getGreen(),start.getBlue(),null);
        float[] hsb2 = Color.RGBtoHSB(end.getRed(),
                    end.getGreen(),end.getBlue(),null);
        float[] hsb = new float[3];
        //hsb[0] = hsb1[0];
        //hsb[1] = 1;
        hsb1[1] = (hsb1[1]+hsb2[1])/2;
        //hsb[2] = hsb1[2];
        return Color.getHSBColor(hsb1[0],hsb1[1],hsb1[2]);
    }
    
    public Color getBMidpoint(Color start, Color end) {
        float[] hsb1 = Color.RGBtoHSB(start.getRed(),
                     start.getGreen(),start.getBlue(),null);
        float[] hsb2 = Color.RGBtoHSB(end.getRed(),
                     end.getGreen(),end.getBlue(),null);
        float[] hsb = new float[3];
        hsb[0] = hsb1[0];
        hsb[1] = hsb1[1];
        hsb[2] = (hsb1[2]+hsb2[2])/2;
        return Color.getHSBColor(hsb[0],hsb[1],hsb[2]);
    }

I've created a custom component called a ColorLabel which is simply a block of color with the hex value displayed in the center. It has a little bit of smarts to change the color of the text so it always contrasts with the background and you'll never get black on black.

In production this code would be integrated into a color chooser dialog which would only show the screen sized frame when the dialog is visible, but this demo code shows off the idea more simply. Enjoy the [code]!

Talk to me about it on Twitter

Posted May 18th, 2004

Tagged: java.net