Skip to main content

Swing Hack 8: An eyedropper tool

Posted by joshy on May 18, 2004 at 6:36 PM PDT

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]!

Related Topics >>