4

I was given the task to create a custom swing component. I have my component functioning properly in a test application which includes JSlider that is used to zoom in and out on an Image. However I am required to present my custom component in a Model, UIDelegate, and Component class format and I am totally lost on how to convert my code so that it follows this format. Here is the code for my test application.

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);
}
}

The following code is the class format i need to follow

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{

}

Any help on how I can implement my custom component in this format would be greatly appreciated. I am very new to Swing and documentation I have found on custom components has been very confusing and of little help.

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);
}

}

Wheels99
  • 87
  • 7
  • 1
    Your model class isn't right, it should model the data, the the UI, so may have the image and zoom level contained within, for example.. – MadProgrammer Nov 07 '14 at 01:29
  • For the model, you should reuse or extend an existing Swing model. The UIdelegate (“user interface” class) extends ComponentUI. It is responsible for painting the component and interpreting the user’s mouse or keyboard actions, and so it should implement appropriate Listeners and interfaces. The Component class extends JComponent. These are the instructions I received but do not understand how to make it work. – Wheels99 Nov 07 '14 at 01:38
  • *"you should reuse or extend an existing Swing mode"* Like what? – MadProgrammer Nov 07 '14 at 01:42
  • I assumed it means extend an existing model such as "JSLider" as it does in my code – Wheels99 Nov 07 '14 at 01:44
  • 1
    `JSlider` isn't a model, it's a component ;) – MadProgrammer Nov 07 '14 at 01:44
  • Oh ok sorry that makes more sense. As I said I am very new to java and swing and finding these instructions very difficult to interpret – Wheels99 Nov 07 '14 at 01:46
  • You might to have a look at [How to Write a Custom Swing Component](https://today.java.net/pub/a/today/2007/02/22/how-to-write-custom-swing-component.html#ui-delegate) – MadProgrammer Nov 07 '14 at 02:14

1 Answers1

9

Okay, so that was a fun adventure into parts of the API I don't use :), start by having a read through How to Write a Custom Swing Component and it's associated links, this will give you the ground work to understand what is about to happen...

Model

The Interface

Personally, I always start with an interface, life is better with interfaces and it gives you more flexibility. Now, which model should you extend from (based on your requirements)...?

Well, the best choice I could find was the BoundedRangeModel, which is also used by the JSlider...this actually means that I can not only pass this model to the view, but to a JSlider and without any extra work, have the slider change the image!! Win-Win

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

Next, I like to make an abstract version, this is where I put "common" functionality, which is likely to be the same for most implementations, in this case, it might not be required, but I'm finckle like this...

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;
    }

}

So, you can see here, I've defined some basic properties, a starting zoom level of 100, a max level of 200 and a minimum level of 0, plus I've implemented the getScaledSize, which is used a bit and makes life easier...

The Default...

Now, because we like been nice, we provide a "default" implementation of the model. This is pretty basic in that all it does it takes a reference to an image...

import java.awt.Image;

public class DefaultZoomModel extends AbstractZoomModel {
    Image image;

    public DefaultZoomModel(Image image) {
        this.image = image;
    }

    @Override
    public Image getImage() {
        return image;
    }

}

You could create implementations that download images from an URL for example...

The View

Okay, this is the actually component itself, which gets added to your UI. It contains the basic functionality need to construct and prepare the UI delegate and manage the model. The key thing of interest here is the use of the property change support to provide notification of the change to the model, this is important as you will see...

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

Now the other fun stuff...If we follow standard convention, you would normally provide an abstract concept of the UI delegate, for example...

import javax.swing.plaf.ComponentUI;

public abstract class ZoomUI extends ComponentUI {       
}

From this, other delegates will grow...

Basic UI Delegate

Convention would normally suggest you provide a "basic" implementation, doing a lot of the heavy lifting, but allowing other implementations the opportunity to jump in change things to there likely

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);
        }

    }

}

From here you could go and devise platform specific implementations, but I've decided to stick with the basic delegate...

Putting it all together

If that wasn't enough, before you can use any of it, you must install the delegate...

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

nb: Change your.awesome.package.name to reflect your actual package name...

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();
                     }
                }
           });
      }

 }

Don't forget to change the package name for the BasicZoomUI to the package name you have it stored in and actually specify a image file ;)

MadProgrammer
  • 343,457
  • 22
  • 230
  • 366
  • Thank you so much! this has made more sense then any documentation i have read so far. It is greatly appreciated!! :) – Wheels99 Nov 07 '14 at 06:42
  • Now it's your job to understand what it's doing, as I've not documented most of the code ;) - Imagine some one now point to a piece of code and asking "why did you do that?" – MadProgrammer Nov 07 '14 at 06:43
  • yes thank you very much your response was very clear and made it much easier to understand. I am now attempting to get it to work in a test application but i should be able to figure that out on my own you have helped enough already. Thank you! – Wheels99 Nov 07 '14 at 07:12
  • I'm embarrassed that you have done all this work and I am still having trouble getting it to run in a test application..can you provide any help on how to properly test the component? – Wheels99 Nov 07 '14 at 07:29
  • I have added the test application code i have so far into my original question. I am having trouble understanding how to set the image and get the component to appear in the panel/ – Wheels99 Nov 07 '14 at 07:38
  • Via the model. The image is provide by the `ZoomModel` – MadProgrammer Nov 07 '14 at 07:40
  • Im sorry but I still do not understand how to bring this all together I cannot even seem to get the slider to appear in my panel let alone the image – Wheels99 Nov 07 '14 at 07:49
  • Don't worry too much about the slider, the model feeds information to the delegate, the component acts as the glue between the two. You need a model which controls not only the range the user can zoom, but the current zoom value and the original image... – MadProgrammer Nov 07 '14 at 09:02
  • You are a lifesaver!! thanks so much for all of your work you are an excellent human being :) – Wheels99 Nov 07 '14 at 09:24