Format of Java Swing Component Customization through Model, UIDelegate, and Component

Feedback


Question:

Assigned with the responsibility of developing a
custom swing component
, I successfully implemented the component in a trial application that included
jslider
, enabling users to zoom in and out of an image. Presently, I am perplexed about how to alter my code to comply with the Model, UIDelegate, and Component class format, as mandated. The following is the code for the test application I created.

package test;
import java.awt.*;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import javax.swing.*;
import javax.swing.border.EmptyBorder;
import javax.swing.event.*;
import java.io.File;
import java.net.URL;
import javax.imageio.ImageIO;
public class ZoomDemo extends JComponent implements ChangeListener {
JPanel gui;
/**
 * Displays the image.
 */
JLabel imageCanvas;
Dimension size;
double scale = 1.0;
private BufferedImage image;
public ZoomDemo() {
    size = new Dimension(10, 10);
    setBackground(Color.black);
    try {
         image = ImageIO.read(new File("car.jpg"));
    } catch (Exception ex) {
        ex.printStackTrace();
    }
}
public void setImage(Image image) {
    imageCanvas.setIcon(new ImageIcon(image));
}
public void initComponents() {
    if (gui == null) {
        gui = new JPanel(new BorderLayout());
        gui.setBorder(new EmptyBorder(5, 5, 5, 5));
        imageCanvas = new JLabel();
        JPanel imageCenter = new JPanel(new GridBagLayout());
        imageCenter.add(imageCanvas);
        JScrollPane imageScroll = new JScrollPane(imageCenter);
        imageScroll.setPreferredSize(new Dimension(300, 100));
        gui.add(imageScroll, BorderLayout.CENTER);
    }
}
public Container getGui() {
    initComponents();
    return gui;
}
public void stateChanged(ChangeEvent e) {
    int value = ((JSlider) e.getSource()).getValue();
    scale = value / 100.0;
    paintImage();
}
protected void paintImage() {
    int imageWidth = image.getWidth();
    int imageHeight = image.getHeight();
    BufferedImage bi = new BufferedImage(
            (int)(imageWidth*scale), 
            (int)(imageHeight*scale), 
            image.getType());
    Graphics2D g2 = bi.createGraphics();
    g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
            RenderingHints.VALUE_ANTIALIAS_ON);
    AffineTransform at = AffineTransform.getTranslateInstance(0, 0);
    at.scale(scale, scale);
    g2.drawRenderedImage(image, at);
    setImage(bi);
}
public Dimension getPreferredSize() {
    int w = (int) (scale * size.width);
    int h = (int) (scale * size.height);
    return new Dimension(w, h);
}
private JSlider getControl() {
    JSlider slider = new JSlider(JSlider.HORIZONTAL, 1, 500, 50);
    slider.setMajorTickSpacing(50);
    slider.setMinorTickSpacing(25);
    slider.setPaintTicks(true);
    slider.setPaintLabels(true);
    slider.addChangeListener(this);
    return slider;
}
public static void main(String[] args) {
    ZoomDemo app = new ZoomDemo();
    JFrame frame = new JFrame();
    frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    frame.setContentPane(app.getGui());
    app.setImage(app.image);
    // frame.getContentPane().add(new JScrollPane(app));  
    frame.getContentPane().add(app.getControl(), "Last");
    frame.setSize(700, 500);
    frame.setLocation(200, 200);
    frame.setVisible(true);
}
}

I must adhere to the class format provided in the code.

Component Class

package component;
import javax.swing.JComponent;
import javax.swing.JSlider;
import javax.swing.plaf.ComponentUI;
public class ProgressBar extends JComponent {
public static ComponentUI createUI(JComponent c) {
    return new ZoomUI();
}
public void installUI(JComponent c){
}
public void uninstallUI (JComponent c){
}
}

Model CLass

public class ZoomModel extends JSLider  {
}

UIDelegate Class

public class ZoomUI extends ComponentUI implements ChangeListener{
}

I would greatly appreciate any assistance in implementing my custom component in this specific format. As a newcomer to Swing, I’ve found the documentation on custom components to be confusing and unhelpful.

test application

package test;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import javax.swing.*;
import javax.swing.border.EmptyBorder;
import javax.swing.border.LineBorder;
import javax.swing.event.*;
import java.io.File;
import java.net.URL;
import javax.imageio.ImageIO;
import component.ZoomComponent;
public class ZoomDemo  extends JPanel implements PropertyChangeListener, ActionListener {
ZoomComponent zoomer;
JPanel board;
private BufferedImage image;
public ZoomDemo( ) {
    super(true);  
    setLayout(new BorderLayout( )); 
    board = new JPanel(true); 
    board.setPreferredSize(new Dimension(300, 300)); 
    board.setBorder(new LineBorder(Color.black, 5));
    zoomer = new ZoomComponent();
    add(board, BorderLayout.NORTH);
    add(zoomer, BorderLayout.SOUTH);
}
@Override
public void actionPerformed(ActionEvent arg0) {
    // TODO Auto-generated method stub
}
@Override
public void propertyChange(PropertyChangeEvent arg0) {
    // TODO Auto-generated method stub
}
public static void main(String[] args) {
    UIManager.getDefaults().put("ZoomComponentUI", "component.BasicZoomUI");
    ZoomDemo s= new ZoomDemo();
    JFrame frame = new JFrame("Sample Sketch Application"); 
    frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); 
    frame.setContentPane(s); 
    frame.pack( ); 
    frame.setVisible(true);
}

}



Solution:

After an amusing exploration of lesser-known API features, I suggest delving into “how to write a custom swing component” and its related resources. This will establish a foundation for comprehending the upcoming process.

Model

The Interface

As per my preference, I begin with interfaces as they offer greater flexibility. Then, you need to decide which model to extend depending on your specific requirements.

The most suitable option available to me was the

BoundedRangeModel

, which is utilized by both

JSlider

. This implies that I can effortlessly transfer this model not only to the view but also to a

JSlider

. As a result, I can effortlessly adjust the image by simply moving the slider. It’s a win-win situation.

import java.awt.Dimension;
import java.awt.Image;
import javax.swing.BoundedRangeModel;
public interface ZoomModel extends BoundedRangeModel {
    public Image getImage();
    public Dimension getScaledSize();
}

The Abstract

After that, I prefer creating an abstract rendition where I include the commonly used features that are likely to be identical for most executions. Although it might not be necessary, I am meticulous like that.

import java.awt.Dimension;
import java.awt.Image;
import javax.swing.DefaultBoundedRangeModel;
public abstract class AbstractZoomModel extends DefaultBoundedRangeModel implements ZoomModel {
    public AbstractZoomModel() {
        super(100, 0, 0, 200);
    }
    @Override
    public Dimension getScaledSize() {
        Dimension size = new Dimension(0, 0);
        Image image = getImage();
        if (image != null) {
            double scale = getValue() / 100d;
            size.width = (int) Math.round(image.getWidth(null) * scale);
            size.height = (int) Math.round(image.getHeight(null) * scale);
        }
        return size;
    }
}

In this definition, the key properties include a starting zoom level denoted by

100

, a maximum level represented by

200

, and a minimum level indicated by

0

. Additionally, the implementation of

getScaledSize

greatly simplifies certain tasks.

The Default…

In order to be helpful, we offer a default version of the model that is quite simple – it only requires an image reference as input.

import java.awt.Image;
public class DefaultZoomModel extends AbstractZoomModel {
    Image image;
    public DefaultZoomModel(Image image) {
        this.image = image;
    }
    @Override
    public Image getImage() {
        return image;
    }
}

One possibility is to generate implementations that retrieve images from a designated

URL

source.

The View

This is the component that you can add to your UI to enable its basic functionality and manage the model. The crucial aspect to note here is the implementation of property change support, which ensures that any change made to the model is promptly notified to the concerned parties. This feature holds great significance, as you will soon discover.

import java.awt.Color;
import java.awt.Dimension;
import javax.swing.JComponent;
import javax.swing.UIManager;
public class ZoomComponent extends JComponent {
    private static final String uiClassID = "ZoomComponentUI";
    private ZoomModel model;
    public ZoomComponent() {
        setBackground(Color.black);
        setFocusable(true);
        updateUI();
    }
    public void setModel(ZoomModel newModel) {
        if (model != newModel) {
            ZoomModel old = model;
            this.model = newModel;
            firePropertyChange("model", old, newModel);
        }
    }
    public ZoomModel getModel() {
        return model;
    }
    @Override
    public Dimension getPreferredSize() {
        ZoomModel model = getModel();
        Dimension size = new Dimension(100, 100);
        if (model != null) {
            size = model.getScaledSize();
        }
        return size;
    }
    public void setUI(BasicZoomUI ui) {
        super.setUI(ui);
    }
    @Override
    public void updateUI() {
        if (UIManager.get(getUIClassID()) != null) {
            ZoomUI ui = (ZoomUI) UIManager.getUI(this);
            setUI(ui);
        } else {
            setUI(new BasicZoomUI());
        }
    }
    public BasicZoomUI getUI() {
        return (BasicZoomUI) ui;
    }
    @Override
    public String getUIClassID() {
        return uiClassID;
    }
}

The UI Delegate

Let’s move on to the more exciting aspects. As per usual norms, a UI delegate concept can be given using the

abstract

code.

import javax.swing.plaf.ComponentUI;
public abstract class ZoomUI extends ComponentUI {       
}

As a result, additional representatives will develop.

Basic UI Delegate

Typically, it is expected that you offer a fundamental implementation that handles most of the intricate tasks while enabling other implementations to intervene and modify them according to their preference.

import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Image;
import java.awt.event.ActionEvent;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseWheelEvent;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.ActionMap;
import javax.swing.InputMap;
import javax.swing.JComponent;
import javax.swing.KeyStroke;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.plaf.ComponentUI;
public class BasicZoomUI extends ZoomUI {
    private ZoomComponent zoomComponent;
    private MouseAdapter mouseHandler;
    private ChangeListener changeHandler;
    private Action zoomIn;
    private Action zoomOut;
    private PropertyChangeListener propertyChangeHandler;
    protected ChangeListener getChangeHandler() {
        if (changeHandler == null) {
            changeHandler = new ChangeHandler();
        }
        return changeHandler;
    }
    protected void installMouseListener() {
        mouseHandler = new MouseAdapter() {
            @Override
            public void mouseClicked(MouseEvent e) {
                zoomComponent.requestFocusInWindow();
            }
            @Override
            public void mouseWheelMoved(MouseWheelEvent e) {
                int amount = e.getWheelRotation();
                ZoomModel model = zoomComponent.getModel();
                if (model != null) {
                    int value = model.getValue();
                    model.setValue(value + amount);
                }
            }
        };
        zoomComponent.addMouseListener(mouseHandler);
        zoomComponent.addMouseWheelListener(mouseHandler);
    }
    protected void installModelPropertyChangeListener() {
        propertyChangeHandler = new PropertyChangeListener() {
            @Override
            public void propertyChange(PropertyChangeEvent evt) {
                ZoomModel old = (ZoomModel) evt.getOldValue();
                if (old != null) {
                    old.removeChangeListener(getChangeHandler());
                }
                ZoomModel newValue = (ZoomModel) evt.getNewValue();
                if (newValue != null) {
                    newValue.addChangeListener(getChangeHandler());
                }
            }
        };
        zoomComponent.addPropertyChangeListener("model", propertyChangeHandler);
    }
    protected void installKeyBindings() {
        zoomIn = new ZoomInAction();
        zoomOut = new ZoomOutAction();
        InputMap inputMap = zoomComponent.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW);
        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_ADD, 0), "zoomIn");
        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_SUBTRACT, 0), "zoomOut");
        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_PLUS, 0), "zoomIn");
        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_MINUS, 0), "zoomOut");
        ActionMap actionMap = zoomComponent.getActionMap();
        actionMap.put("zoomIn", zoomIn);
        actionMap.put("zoomOut", zoomOut);
    }
    protected void installModelChangeListener() {
        ZoomModel model = getModel();
        if (model != null) {
            model.addChangeListener(getChangeHandler());
        }
    }
    @Override
    public void installUI(JComponent c) {
        zoomComponent = (ZoomComponent) c;
        installMouseListener();
        installModelPropertyChangeListener();
        installKeyBindings();
        installModelChangeListener();
    }
    protected void uninstallModelChangeListener() {
        getModel().removeChangeListener(getChangeHandler());
    }
    protected void uninstallKeyBindings() {
        InputMap inputMap = zoomComponent.getInputMap(JComponent.WHEN_FOCUSED);
        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_ADD, 0), "donothing");
        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_SUBTRACT, 0), "donothing");
        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_PLUS, 0), "donothing");
        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_MINUS, 0), "donothing");
        AbstractAction blank = new AbstractAction() {
            @Override
            public void actionPerformed(ActionEvent e) {
            }
        };
        ActionMap actionMap = zoomComponent.getActionMap();
        actionMap.put("zoomIn", blank);
        actionMap.put("zoomOut", blank);
    }
    protected void uninstallModelPropertyChangeListener() {
        zoomComponent.removePropertyChangeListener(propertyChangeHandler);
        propertyChangeHandler = null;
    }
    protected void uninstallMouseListener() {
        zoomComponent.removeMouseWheelListener(mouseHandler);
        mouseHandler = null;
    }
    @Override
    public void uninstallUI(JComponent c) {
        uninstallModelChangeListener();
        uninstallModelPropertyChangeListener();
        uninstallKeyBindings();
        uninstallMouseListener();
        mouseHandler = null;
        zoomComponent = null;
    }
    @Override
    public void paint(Graphics g, JComponent c) {
        super.paint(g, c);
        paintImage(g);
    }
    protected void paintImage(Graphics g) {
        if (zoomComponent != null) {
            ZoomModel model = zoomComponent.getModel();
            Image image = model.getImage();
            Dimension size = model.getScaledSize();
            int x = (zoomComponent.getWidth() - size.width) / 2;
            int y = (zoomComponent.getHeight() - size.height) / 2;
            g.drawImage(image, x, y, size.width, size.height, zoomComponent);
        }
    }
    public static ComponentUI createUI(JComponent c) {
        return new BasicZoomUI();
    }
    protected ZoomModel getModel() {
        return zoomComponent == null ? null : zoomComponent.getModel();
    }
    protected class ChangeHandler implements ChangeListener {
        @Override
        public void stateChanged(ChangeEvent e) {
            zoomComponent.revalidate();
            zoomComponent.repaint();
        }
    }
    protected class ZoomAction extends AbstractAction {
        private int delta;
        public ZoomAction(int delta) {
            this.delta = delta;
        }
        @Override
        public void actionPerformed(ActionEvent e) {
            ZoomModel model = getModel();
            if (model != null) {
                model.setValue(model.getValue() + delta);
            }
        }
    }
    protected class ZoomOutAction extends ZoomAction {
        public ZoomOutAction() {
            super(-5);
        }
    }
    protected class ZoomInAction extends ZoomAction {
        public ZoomInAction() {
            super(5);
        }
    }
}

Instead of creating platform specific implementations, I have chosen to remain with the fundamental delegate.

Putting it all together

In addition to this, the installation of the delegate is necessary to utilize any of the given tools.

UIManager.getDefaults().put("ZoomComponentUI", "your.awesome.package.name.BasicZoomUI");

Modify the

your.awesome.package.name

to match the name of your package.

Runnable Example

 import java.awt.BorderLayout;
 import java.awt.Dimension;
 import java.awt.EventQueue;
 import java.awt.Graphics;
 import java.awt.Graphics2D;
 import java.io.File;
 import java.io.IOException;
 import javax.imageio.ImageIO;
 import javax.swing.JFrame;
 import javax.swing.JPanel;
 import javax.swing.JScrollPane;
 import javax.swing.JSlider;
 import javax.swing.UIManager;
 import javax.swing.UnsupportedLookAndFeelException;
 public class TestZoom100 {
      public static void main(String[] args) {
           new TestZoom100();
      }
      public TestZoom100() {
           EventQueue.invokeLater(new Runnable() {
                @Override
                public void run() {
                     try {
                          UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
                     } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) {
                          ex.printStackTrace();
                     }
                     UIManager.getDefaults().put("ZoomComponentUI", "your.awesome.package.name.BasicZoomUI");
                     try {
                          DefaultZoomModel model = new DefaultZoomModel(ImageIO.read(new File("/your/awesome/image.jpg")));
                          model.setValue(50);
                          ZoomComponent zoomComp = new ZoomComponent();
                          zoomComp.setModel(model);
                          JSlider slider = new JSlider(model);
                          JFrame frame = new JFrame("Testing");
                          frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                          frame.add(new JScrollPane(zoomComp));
                          frame.add(slider, BorderLayout.SOUTH);
                          frame.pack();
                          frame.setLocationRelativeTo(null);
                          frame.setVisible(true);
                     } catch (IOException exp) {
                          exp.printStackTrace();
                     }
                }
           });
      }
 }

Remember to rename the package for

BasicZoomUI

with the name of the package in which it is saved. Also, don’t forget to provide the image file.

Frequently Asked Questions