ImageComponent - eine JComponent, um ein BufferedImage anzuzeigen

Nachdem es in einem anderen Thread um Bilder ging, und ich nachdem ich dafür die ersten paar Zeilen code geschrieben hatte und dann dachte: “Joa, ich will die Bilder auch mal schnell anzeigen”, habe ich in klassischSTer Yak Shaving-Manier mal das gemacht, was schon ewig (eigentlich aber recht weit unten) auf meiner TODO-Liste stand: Eine JComponent, mit der man ein BufferedImage anzeigen kann.

Ja, das ist nicht viel mehr, als das, was ein add(new JLabel(new ImageIcon(image))); macht, ausgewalzt auf 700 Zeilen. Die wichtigsten “Erweiterungen” sind:
[ul]
[li]Die Alignment-Sachen sind etwas expliziter
[/li][li]Man kann von “Component-Koordinaten” in “Bildkoordinaten” umrechnen
[/li][li]Man kann das Bild automatisch an die Component-Größe anpassen.
[/li][/ul]
(Die Optionen für letzteres sinnvoll zu kombinieren, war komplizierter, als ich zunächst dachte, aber wenn ich auf die Uhr sehe, ist das vermutlich wieder mal so ein Fall, wo ich nach etwas Schlaf denke: Aaahrg, was hab’ ich denn DA gemacht?! :verzweifel: … mal sehen)

Früher oder später wird das wohl in meinem CommonUI-Repo landen, aber bis dahin dumpe ich es erstmal hier:

import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.image.BufferedImage;

import javax.swing.JPanel;
import javax.swing.SwingConstants;

/**
 * A generic component for displaying a <code>BufferedImage</code>. It offers
 * different options for adjusting the image based on the size of the 
 * component, and for aligning the image inside the component.<br>
 * <br>
 * <b>Note:</b> Take care to use the {@link #setImageAlignmentX(int)} and
 * {@link #setImageAlignmentY(int)} methods to change the alignment of
 * the image, and <b>not</b> the <code>setAlignmentX</code> and
 * <code>setAlignmentY</code> methods of the <code>JComponent</code> class!<br>
 * <br>
 * <a name="resizingBehavior"><b>Resizing behavior:</b></a><br>
 * <br>
 * The resizing behavior of the image in this component is governed by
 * four flags:<br>
 * <ul>
 *   <li>{@link #setShrinkingImage(boolean) shrinkingImage}</li>
 *   <li>{@link #setEnlargingImage(boolean) enlargingImage}</li>
 *   <li>{@link #setResizingToFit(boolean) resizingToFit}</li>
 *   <li>{@link #setMaintainingAspectRatio(boolean) maintainingAspectRatio}</li>
 * </ul><br>
 * When the <code>maintainingAspectRatio</code> flag is set to 
 * <b><code>false</code></b>, then the <code>shrinkingImage</code> and 
 * <code>enlargingImage</code> flags will have the effect of 
 * simply making the image smaller or larger than its original 
 * size, respectively, in order to match the space that is 
 * available in the component.<br>
 * <br>
 * When the <code>maintainingAspectRatio</code> flag is set to 
 * <b><code>true</code></b>, then the <code>shrinkingImage</code> and 
 * <code>enlargingImage</code> will still make the image smaller or  
 * larger than original size, respectively, maintaining the aspect ratio.
 * But the <i>goal</i> of these resize operations is then determined
 * by the <code>resizingToFit</code> flag. If the <code>resizingToFit</code> 
 * flag is set to <code>true</code>, then the image will be resized 
 * so that it completely fits into the available space. If the 
 * <code>resizingToFit</code> flag is set to <code>false</code>, then 
 * the image will be resized with the goal to fill all the space that 
 * is available in the component (even when parts of the image are 
 * then no longer visible).<br>
 * <br>
 * The <i>preferred size</i> of this component will by default be the same
 * size as the image, regardless of the resizing settings of this component.<br>
 * <br>
 * <br>
 * The class also offers methods for computing the point in the image that
 * corresponds to a point in the component. The {@link #getImagePoint(int, int)}
 * method may convert a point from component coordinates into a point in 
 * image coordinates. For example, the following code may be used to access
 * pixels of the image in a <code>MouseListener</code>:
 * <pre><code>
 * imageComponent.addMouseListener(new MouseAdapter() {
 *     public void mouseClicked(MouseEvent e) {
 *         Point imagePoint = imageComponent.getImagePoint(e.getX(), e.getY());
 *         if (imagePoint != null) {
 *             System.out.println("Clicked image at "+imagePoint);
 *         }
 *     }
 * });
 * </code></pre>
 */
public class ImageComponent extends JPanel
{
    /**
     * Serial UID
     */
    private static final long serialVersionUID = 1756839643063796636L;
    
    /**
     * The image that is rendered
     */
    private BufferedImage image;
    
    /**
     * The current bounds of the image inside this component
     */
    private final Rectangle imageBounds;
    
    /**
     * Whether the image should be shrunk depending on the component size
     * 
     * @see #setShrinkingImage(boolean)
     */
    private boolean shrinkingImage;

    /**
     * Whether the image should be enlarged depending on the component size
     * 
     * @see #setEnlargingImage(boolean)
     */
    private boolean enlargingImage;

    /**
     * Whether the goal of the resizing operations should be to fit the 
     * image completely into the available area (in contrast to filling 
     * the whole available area - see class comment for details) 
     * 
     * @see #setResizingToFit(boolean)
     */
    private boolean resizingToFit;
    
    /**
     * Whether the aspect ratio of the image should be maintained
     * 
     * @see #setMaintainingAspectRatio(boolean)
     */
    private boolean maintainingAspectRatio;
    
    /**
     * The alignment of the image, in x-direction
     * 
     * @see #setImageAlignmentX(int)
     */
    private int imageAlignmentX;

    /**
     * The alignment of the image, in y-direction
     * 
     * @see #setImageAlignmentY(int)
     */
    private int imageAlignmentY;
    
    /**
     * Creates a new, empty image component. <br>
     * <br>
     * The default settings of this component will have the effect that 
     * any image that is afterwards set with {@link #setImage(BufferedImage)} 
     * will be displayed in the center of the component, with its original 
     * size and aspect ratio.<br>
     * <ul>
     *   <li>
     *     The component will not 
     *     {@link #setShrinkingImage(boolean) shrink} the image
     *   </li>
     *   <li>
     *     The component will not 
     *     {@link #setEnlargingImage(boolean) enlarge} the image
     *   </li>
     *   <li>
     *     The goal of resize operations will be   
     *     {@link #setResizingToFit(boolean) resizing to fit} 
     *   </li>
     *   <li>
     *     The component will  
     *     {@link #setMaintainingAspectRatio(boolean) maintain the aspect ratio} 
     *   </li>
     *   <li>
     *     The {@link #setImageAlignmentX(int) image alignment in x-direction}
     *     will be set to <code>SwingConstants.CENTER</code>  
     *   </li>
     *   <li>
     *     The {@link #setImageAlignmentY(int) image alignment in y-direction}
     *     will be set to <code>SwingConstants.CENTER</code>  
     *   </li>
     * </ul> 
     * See <a href="#resizingBehavior">Resizing behavior</a> for details
     * about the resizing flags.
     */
    public ImageComponent()
    {
        this.image = null;
        this.imageBounds = new Rectangle();
        this.resizingToFit = true;
        this.shrinkingImage = false;
        this.enlargingImage = false;
        this.maintainingAspectRatio = true;
        this.imageAlignmentX = SwingConstants.CENTER;
        this.imageAlignmentY = SwingConstants.CENTER;
    }

    /**
     * Creates a new image component that shows the given image.<br>
     * <br>
     * The component will have the 
     * {@link ImageComponent#ImageComponent() default constructor} settings.
     * 
     * @param image The image to show
     */
    public ImageComponent(BufferedImage image)
    {
        this();
        setImage(image);
    }
    
    /**
     * Creates a new image component that shows the given image.<br>
     * <br>
     * If the given <code>fitImage</code> flag is <code>true</code>, then
     * it will {@link #setShrinkingImage(boolean) shrink} and
     * {@link #setEnlargingImage(boolean) enlarge} the image to always
     * be displayed as large as possible in the space that is available for
     * the component. See <a href="#resizingBehavior">Resizing behavior</a> 
     * for details about the resizing flags.<br>
     * <br>
     * The remaining settings will be the same as if the  
     * {@link ImageComponent#ImageComponent() default constructor} 
     * was used.
     * 
     * @param image The image to show
     * @param fitImage Whether the image should be 
     * {@link #setShrinkingImage(boolean) shrunk} and
     * {@link #setEnlargingImage(boolean) enlarged} to always 
     * {@link #setResizingToFit(boolean) fit} the size of the component.
     */
    public ImageComponent(BufferedImage image, boolean fitImage)
    {
        this();
        setImage(image);
        setShrinkingImage(fitImage);
        setEnlargingImage(fitImage);
    }
    
    /**
     * Set the image that should be displayed. If the given image is 
     * <code>null</code>, then no image will be displayed.
     * 
     * @param image The image
     */
    public final void setImage(BufferedImage image)
    {
        this.image = image;
        updateImageBounds();
        repaint();
    }
    
    /**
     * Returns the image that is displayed, or <code>null</code> if no
     * image is displayed
     * 
     * @return The image
     */
    public final BufferedImage getImage()
    {
        return image;
    }
    
    /**
     * Set whether the image should be shrunk depending on the component 
     * size.<br>
     * <br>
     * See <a href="#resizingBehavior">Resizing behavior</a> 
     * for details about the resizing flags.
     * 
     * @param shrinkingImage Whether the image should be shrunk
     */
    public final void setShrinkingImage(boolean shrinkingImage)
    {
        this.shrinkingImage = shrinkingImage;
        updateImageBounds();
        repaint();
    }
    
    /**
     * Returns whether the image should be shrunk depending on the component 
     * size.<br>
     * <br>
     * See <a href="#resizingBehavior">Resizing behavior</a> 
     * for details about the resizing flags.
     * 
     * @return Whether the image should be shrunk
     * 
     * @see #setShrinkingImage(boolean)
     */
    public final boolean isShrinkingImage()
    {
        return shrinkingImage;
    }
    
    /**
     * Set whether the image should be enlarged depending on the component 
     * size.<br>
     * <br>
     * See <a href="#resizingBehavior">Resizing behavior</a> 
     * for details about the resizing flags.
     * 
     * @param enlargingImage Whether the image should be enlarged
     */
    public final void setEnlargingImage(boolean enlargingImage)
    {
        this.enlargingImage = enlargingImage;
        updateImageBounds();
        repaint();
    }
    
    /**
     * Returns whether the image should be enlarged depending on the component 
     * size.<br>
     * <br>
     * See <a href="#resizingBehavior">Resizing behavior</a> 
     * for details about the resizing flags.
     * 
     * @return Whether the image should be enlarged
     * 
     * @see #setEnlargingImage(boolean)
     */
    public final boolean isEnlargingImage()
    {
        return enlargingImage;
    }
    
    /**
     * Set whether the goal of resizing operations should be to fit the
     * whole image into the available space (in contrast to covering 
     * the available space with the image). <br>
     * <br>
     * See <a href="#resizingBehavior">Resizing behavior</a> 
     * for details about the resizing flags.
     * 
     * @param resizingToFit Whether the resizing should try to fit the image
     */
    public final void setResizingToFit(boolean resizingToFit)
    {
        this.resizingToFit = resizingToFit;
        updateImageBounds();
        repaint();
    }
    
    /**
     * Returns whether the goal of resizing operations should be to fit the
     * whole image into the available space (in contrast to covering 
     * the available space with the image). <br>
     * <br>
     * See <a href="#resizingBehavior">Resizing behavior</a> 
     * for details about the resizing flags.
     * 
     * @return Whether the resizing should try to fit the image
     */
    public final boolean isResizingToFit()
    {
        return resizingToFit;
    }
    
    
    /**
     * Set the alignment of the image, in x-direction.<br>
     * <br>
     * This setting determines where the image is located when its size
     * is not the same as the size of the component:
     * <ul>
     *   <li>
     *     When the alignment is <code>SwingUtilities.LEFT</code>, 
     *     then the left border of the image is at the left border 
     *     of the component.
     *   </li>
     *   <li>
     *     When the alignment is <code>SwingUtilities.RIGHT</code>, 
     *     then the right border of the image is at the right border 
     *     of the component.
     *   </li>
     *   <li>
     *     When the alignment is <code>SwingUtilities.CENTER</code>, 
     *     then the (horizontal) center of the image is at the
     *     (horizontal) center of the component.
     *   </li>
     * </ul>
     * <br>
     * 
     * @param imageAlignmentX The image alignment
     * @throws IllegalArgumentException If the given alignment is not
     * <code>SwingUtilities.LEFT</code> or 
     * <code>SwingUtilities.RIGHT</code> or 
     * <code>SwingUtilities.CENTER</code>
     */
    public final void setImageAlignmentX(int imageAlignmentX)
    {
        if (imageAlignmentX != SwingConstants.LEFT &&
            imageAlignmentX != SwingConstants.RIGHT &&
            imageAlignmentX != SwingConstants.CENTER)
        {
            throw new IllegalArgumentException(
                "Alignment must be SwingConstants.LEFT " + 
                "or SwingConstants.RIGHT or SwingConstants.CENTER");
        }
        this.imageAlignmentX = imageAlignmentX;
        updateImageBounds();
        repaint();
    }
    
    /**
     * Returns the alignment of the image, in x-direction
     * 
     * @return The image alignment
     *  
     * @see #setImageAlignmentX(int)
     */
    public final int getImageAlignmentX()
    {
        return imageAlignmentX;
    }

    /**
     * Set the alignment of the image, in y-direction.<br>
     * <br>
     * This setting determines where the image is located when its size
     * is not the same as the size of the component:
     * <ul>
     *   <li>
     *     When the alignment is <code>SwingUtilities.TOP</code>, 
     *     then the top border of the image is at the top border 
     *     of the component.
     *   </li>
     *   <li>
     *     When the alignment is <code>SwingUtilities.BOTTOM</code>, 
     *     then the bottom border of the image is at the bottom border 
     *     of the component.
     *   </li>
     *   <li>
     *     When the alignment is <code>SwingUtilities.CENTER</code>, 
     *     then the (vertical) center of the image is at the
     *     (vertical) center of the component.
     *   </li>
     * </ul>
     * <br>
     * 
     * @param imageAlignmentY The image alignment
     * @throws IllegalArgumentException If the given alignment is not
     * <code>SwingUtilities.TOP</code> or 
     * <code>SwingUtilities.BOTTOM</code> or 
     * <code>SwingUtilities.CENTER</code>
     */
    public final void setImageAlignmentY(int imageAlignmentY)
    {
        if (imageAlignmentY != SwingConstants.TOP &&
            imageAlignmentY != SwingConstants.BOTTOM &&
            imageAlignmentY != SwingConstants.CENTER)
        {
            throw new IllegalArgumentException(
                "Alignment must be SwingConstants.TOP " + 
                "or SwingConstants.BOTTOM or SwingConstants.CENTER");
        }
        this.imageAlignmentY = imageAlignmentY;
        updateImageBounds();
        repaint();
    }
    
    /**
     * Returns the alignment of the image, in y-direction
     * 
     * @return The image alignment
     *  
     * @see #setImageAlignmentY(int)
     */
    public final int getImageAlignmentY()
    {
        return imageAlignmentY;
    }
    
    /**
     * Set whether the aspect ratio of the image should be maintained, 
     * regardless of any possible resizing
     * 
     * @param maintainingAspectRatio Whether the aspect ratio should be 
     * maintained
     */
    public final void setMaintainingAspectRatio(boolean maintainingAspectRatio)
    {
        this.maintainingAspectRatio = maintainingAspectRatio;
        updateImageBounds();
        repaint();
    }
    
    /**
     * Returns whether the aspect ratio of the image should be maintained, 
     * regardless of any possible resizing
     * 
     * @return Whether the aspect ratio should be maintained
     * 
     * @see #setMaintainingAspectRatio(boolean)
     */
    public final boolean isMaintainingAspectRatio()
    {
        return maintainingAspectRatio;
    }
    

    @Override
    public Dimension getPreferredSize()
    {
        if (super.isPreferredSizeSet())
        {
            return super.getPreferredSize();
        }
        if (image == null)
        {
            return super.getPreferredSize();
        }
        int w = image.getWidth();
        int h = image.getHeight();
        return new Dimension(w, h);
    }
    
    @Override
    protected void paintComponent(Graphics gr)
    {
        super.paintComponent(gr);
        Graphics2D g = (Graphics2D)gr;
        updateImageBounds();
        if (image == null)
        {
            return;
        }
        g.drawImage(image, 
            imageBounds.x, imageBounds.y, 
            imageBounds.width, imageBounds.height, this);
    }
    
    /**
     * Update the {@link #imageBounds} based on the current state of
     * this component
     */
    private void updateImageBounds()
    {
        if (image == null)
        {
            imageBounds.x = 0;
            imageBounds.y = 0;
            imageBounds.width = 0;
            imageBounds.height = 0;
            return;
        }
        imageBounds.width = image.getWidth();
        imageBounds.height = image.getHeight();
        
        float scalingX = (float)getWidth() / image.getWidth();
        float scalingY = (float)getHeight() / image.getHeight();
        float minScaling = Math.min(scalingX, scalingY);
        float maxScaling = Math.max(scalingX, scalingY);

        if (maintainingAspectRatio)
        {
            if (shrinkingImage)
            {
                if (resizingToFit && minScaling < 1.0f)
                {
                    imageBounds.width = (int)(image.getWidth() * minScaling);
                    imageBounds.height = (int)(image.getHeight() * minScaling);
                }
                else if (!resizingToFit && maxScaling < 1.0f)
                {
                    imageBounds.width = (int)(image.getWidth() * maxScaling);
                    imageBounds.height = (int)(image.getHeight() * maxScaling);
                }
            }
            if (enlargingImage)
            {
                if (resizingToFit && minScaling > 1.0f)
                {
                    imageBounds.width = (int)(image.getWidth() * minScaling);
                    imageBounds.height = (int)(image.getHeight() * minScaling);
                }
                else if (!resizingToFit && maxScaling > 1.0f)
                {
                    imageBounds.width = (int)(image.getWidth() * maxScaling);
                    imageBounds.height = (int)(image.getHeight() * maxScaling);
                }
            }
        }
        else
        {
            if (shrinkingImage)
            {
                if (scalingX < 1.0f)
                {
                    imageBounds.width = (int)(image.getWidth() * scalingX);
                }
                if (scalingY < 1.0f)
                {
                    imageBounds.height = (int)(image.getHeight() * scalingY);
                }
            }
            if (enlargingImage)
            {
                if (scalingX > 1.0f)
                {
                    imageBounds.width = (int)(image.getWidth() * scalingX);
                }
                if (scalingY > 1.0f)
                {
                    imageBounds.height = (int)(image.getHeight() * scalingY);
                }
            }
        }
        
        imageBounds.x = computeX(getWidth(), imageBounds.width);
        imageBounds.y = computeY(getHeight(), imageBounds.height);
    }
    
    /**
     * Compute the y-coordinate for the image, based on the given space
     * that is available and the space that is needed for the image, and the 
     * current {@link #getImageAlignmentX() image alignment in x-direction}.
     * 
     * @param available The available space
     * @param needed The needed space
     * @return The x-coordinate for the image
     */
    private int computeX(int available, int needed)
    {
        if (imageAlignmentX == SwingConstants.LEFT)
        {
            return 0;
        }
        else if (imageAlignmentX == SwingConstants.RIGHT)
        {
            return available - needed;
        }
        return (available - needed) / 2;
    }

    /**
     * Compute the y-coordinate for the image, based on the given space
     * that is available and the space that is needed for the image, and the 
     * current {@link #getImageAlignmentY() image alignment in y-direction}.
     * 
     * @param available The available space
     * @param needed The needed space
     * @return The y-coordinate for the image
     */
    private int computeY(int available, int needed)
    {
        if (imageAlignmentY == SwingConstants.TOP)
        {
            return 0;
        }
        else if (imageAlignmentY == SwingConstants.BOTTOM)
        {
            return available - needed;
        }
        return (available - needed) / 2;
    }
    
    /**
     * Returns a new rectangle that describes the bounds of the
     * {@link #getImage() current image} as it is currently displayed 
     * in this component, or <code>null</code> if there is no image. 
     * 
     * @return The current image bounds, or <code>null</code>
     */
    public Rectangle getImageBounds()
    {
        if (image == null)
        {
            return null;
        }
        return new Rectangle(imageBounds);
    }
    
    
    /**
     * Returns whether the current {@link #getImageBounds() image bounds}
     * contain the given point in this component
     * 
     * @param componentPointX The x-coordinate of the point in this component
     * @param componentPointY The y-coordinate of the point in this component
     * @return Whether the image contains the specified point
     */
    public final boolean imageContains(
        int componentPointX, int componentPointY)
    {
        return imageBounds.contains(componentPointX, componentPointY);
    }
    
    /**
     * Returns the point of the image that is currently displayed at the
     * specified location in this component, or <code>null</code> if 
     * the image does not cover the specified point 
     *  
     * @param componentPointX The x-coordinate of the point in this component
     * @param componentPointY The y-coordinate of the point in this component
     * @return The point in the image, or <code>null</code> 
     */
    public final Point getImagePoint(
        int componentPointX, int componentPointY)
    {
        if (image == null)
        {
            return null;
        }
        if (!imageBounds.contains(componentPointX, componentPointY))
        {
            return null;
        }
        int localX = componentPointX - imageBounds.x;
        int localY = componentPointY - imageBounds.y;
        float relativeX = (float)localX / imageBounds.width;
        float relativeY = (float)localY / imageBounds.height;
        int absoluteX = (int)(relativeX * image.getWidth());
        int absoluteY = (int)(relativeY * image.getHeight());
        int imagePointX = 
            Math.max(0, Math.min(image.getWidth()-1, absoluteX));
        int imagePointY = 
            Math.max(0, Math.min(image.getHeight()-1, absoluteY));
        return new Point(imagePointX, imagePointY);
    }

    
}

Und ein kleiner Test:

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Graphics2D;
import java.awt.GridLayout;
import java.awt.Point;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.Arrays;
import java.util.function.Consumer;

import javax.swing.BorderFactory;
import javax.swing.DefaultListCellRenderer;
import javax.swing.JCheckBox;
import javax.swing.JComboBox;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JPanel;
import javax.swing.SwingConstants;
import javax.swing.SwingUtilities;

public class ImageComponentTest
{
    public static void main(String[] args) throws IOException
    {
        SwingUtilities.invokeLater(() -> createAndShowGUI());
    }
    
    
    private static void createAndShowGUI()
    {
        JFrame f = new JFrame();
        f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        
        BufferedImage image = createTestImage(40, 10, 10);
        f.getContentPane().setLayout(new BorderLayout());
        
        ImageComponent imageComponent = new ImageComponent(image);
        f.getContentPane().add(imageComponent, BorderLayout.CENTER);
        
        imageComponent.addMouseListener(new MouseAdapter()
        {
            @Override
            public void mouseClicked(MouseEvent e)
            {
                Point imagePoint =
                    imageComponent.getImagePoint(e.getX(), e.getY());
                if (imagePoint != null)
                {
                    System.out.println("Clicked image at " + imagePoint);
                }
            }
        });
        
        JPanel controlPanel = new JPanel(new GridLayout(0,1));
        
        controlPanel.add(createCheckBox("shrinkingImage", false, 
            imageComponent::setShrinkingImage));
        controlPanel.add(createCheckBox("enlargingImage", false, 
            imageComponent::setEnlargingImage));
        controlPanel.add(createCheckBox("resizingToFit", true, 
            imageComponent::setResizingToFit));
        controlPanel.add(createCheckBox("maintainingAspectRatio", true, 
            imageComponent::setMaintainingAspectRatio));
        
        Integer imageAlignmentsX[] =  
        {
            SwingConstants.CENTER,
            SwingConstants.LEFT,
            SwingConstants.RIGHT,
        };
        String stringsX[] =
        {
            "CENTER",
            "LEFT",
            "RIGHT"
        };
        controlPanel.add(createComboBox(
            "imageAlignmentX", imageAlignmentsX, stringsX,
            imageComponent::setImageAlignmentX));

        Integer imageAlignmentsY[] =  
        {
            SwingConstants.CENTER,
            SwingConstants.TOP,
            SwingConstants.BOTTOM,
        };
        String stringsY[] =
        {
            "CENTER",
            "TOP",
            "BOTTOM"
        };
        controlPanel.add(createComboBox(
            "imageAlignmentY", imageAlignmentsY, stringsY,
            imageComponent::setImageAlignmentY));
        
        
        JPanel p = new JPanel(new BorderLayout());
        p.setBorder(BorderFactory.createTitledBorder("Controls"));
        p.add(controlPanel, BorderLayout.NORTH);
        f.getContentPane().add(p, BorderLayout.EAST);
        
        
        
        f.setSize(800,800);
        f.setLocationRelativeTo(null);
        f.setVisible(true);
    }
    
    private static JComponent createCheckBox(
        String name, boolean checked, Consumer<Boolean> consumer)
    {
        JCheckBox checkBox = new JCheckBox(name, checked);
        checkBox.addActionListener(
            e -> consumer.accept(checkBox.isSelected()));
        return checkBox;
    }
    
    private static JComponent createComboBox(String label, 
        Integer values[], String names[],
        Consumer<Integer> consumer)
    {
        JPanel p = new JPanel(new BorderLayout());
        JComboBox<Integer> comboBox = new JComboBox<Integer>(values);
        comboBox.setRenderer(new DefaultListCellRenderer()
        {
            @Override
            public Component getListCellRendererComponent(
                JList<?> list, Object value, int index,
                boolean isSelected, boolean cellHasFocus)
            {
                super.getListCellRendererComponent(
                    list, value, index, isSelected, cellHasFocus);
                Integer integer = (Integer)value;
                int valueIndex = 
                    Arrays.asList(values).indexOf(integer);
                setText(names[valueIndex]);
                return this;
            }
        });
        comboBox.addActionListener(
            e -> consumer.accept((Integer)comboBox.getSelectedItem()));
        p.add(new JLabel(label), BorderLayout.WEST);
        p.add(comboBox, BorderLayout.CENTER);
        return p;
    }
    
    
    
    
    private static BufferedImage createTestImage(
        int tileSize, int tilesX, int tilesY)
    {
        BufferedImage image = 
            new BufferedImage(tilesX * tileSize, tilesY * tileSize,
                BufferedImage.TYPE_INT_ARGB);
        Graphics2D g = image.createGraphics();
        for (int x=0; x<tilesX; x++)
        {
            for (int y=0; y<tilesY; y++)
            {
                if (((x+y) & 1) == 0)
                {
                    g.setColor(Color.WHITE);
                }
                else
                {
                    g.setColor(Color.BLACK);
                }
                g.fillRect(x*tileSize, y*tileSize, tileSize, tileSize);
            }
        }
        g.dispose();
        return image;
    }
    
    

}