简体   繁体   中英

How to Change Border of JSpinner to a Custom Coloured Border with Round Corner of Adjustable radius i

I want to customize my JSpinner to give it a custom border with adjustable colour, adjustable border thickness, and round corners of adjustable radius. So that I can just set the border for the spinner and be done with it.

My Spinner code is as follows:

protected JSpinner createLabelledUpDownControl(JComponent parent, int initialValue, int minVal, int maxVal, String topLabelString, Font topLabelFont, Rectangle topLabelBounds, String topSubLabelString, Font topSubLabelFont, Rectangle topSubLabelBounds,String eachLabelString, Font eachLabelFont, Rectangle eachLabelBounds, String bottomLabelString, Font bottomLabelFont, Rectangle bottomLabelBounds ){
        @SuppressWarnings("serial")
        JSpinner spinner = new JSpinner(new SpinnerNumberModel(initialValue, minVal, maxVal, 1)){
            @Override
            public void paint(Graphics g){
                super.paint(g);
                Graphics2D g2D = (Graphics2D) g.create();
                RenderingHints qualityHints =  new RenderingHints(RenderingHints.KEY_ANTIALIASING,RenderingHints.VALUE_ANTIALIAS_ON);
                qualityHints.put(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY );
                g2D.setRenderingHints(qualityHints);  
            }
        };
        //spinner.setBorder(BorderFactory.createLineBorder(new Color(37, 54, 142), 4, true));

        //spinner.setBorder(new RoundedColouredBorder(30, new Color(37, 54, 142), 4));

        spinner.setBorder(new RoundedBorder(30, new Color(37, 54, 142), 4));

        spinner.setBounds(0, 0, parent.getWidth(), parent.getHeight());
        spinner.setFont(UI.getRegularArgentumSansFont().deriveFont(Font.BOLD, 88));
        spinner.setUI(new JSpinnerArrow(parent));


        JSpinner.DefaultEditor spinnerEditor = (JSpinner.DefaultEditor)spinner.getEditor();
        spinnerEditor.getTextField().setHorizontalAlignment(JTextField.CENTER);

        JComponent comp = spinner.getEditor();
        JFormattedTextField field = (JFormattedTextField) comp.getComponent(0);
        DefaultFormatter formatter = (DefaultFormatter) field.getFormatter();
        formatter.setCommitsOnValidEdit(true);

    if(parent != null){
            parent.add(spinner);
        }

        return spinner;
    }

and I give my spinner custom Arrows with the following class:

I set the Dimension of the arrows so that they change in size to what I want. Its all pretty straightforward and simple I think. But my issues happen when I try to give the arrow buttons a custom border and when I try to give my entire spinner a custom border as well.

private class JSpinnerArrow extends BasicSpinnerUI {

        private JComponent parent;

        public JSpinnerArrow(JComponent parent){
            this.parent = parent;
        }

        @Override
        protected Component createNextButton() {
            Component c = createArrowButton("/arrow-upDB.png");
            c.setName("Spinner.nextButton");
            installNextButtonListeners(c);
            return c;
        }

        @Override
        public void paint(Graphics g, JComponent component){
            super.paint(g, component);
            Graphics2D g2D = (Graphics2D) g.create();
            RenderingHints qualityHints =  new RenderingHints(RenderingHints.KEY_ANTIALIASING,RenderingHints.VALUE_ANTIALIAS_ON);
            qualityHints.put(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY );
            g2D.setRenderingHints(qualityHints);  
        }

        @Override
        protected Component createPreviousButton() {
            Component c = createArrowButton("/arrow-downDB.png");
            c.setName("Spinner.previousButton");
            installPreviousButtonListeners(c);
            return c;
        }

        private Component createArrowButton(String filename) {
            Image icon = UI.loadImage(filename);
            if(icon != null){
                JButton b = createButton(null, "", "", null);
                b.setIcon(new ImageIcon(icon));
                //b.setBorder(BorderFactory.createLineBorder(new Color(37, 54, 142), 4));
                b.setBackground(null);
                b.setBorder(new RoundedBorder(30, new Color(37, 54, 142), 4));
                b.setPreferredSize(new Dimension(65,160));
                return b;
            }
            return createButton(null, "", "", null);
        }
     }

I have Tried the following with this result: Notice how the Spinner Text Area is being clipped inwards (I believe it is also being stretched oddly... and the border is not drawn on the far right side edge of the spinner. Spinner Result from class: RoundedBorder

在此处输入图片说明

 public static class RoundedBorder implements Border {
    private int radius;
    private int thickness;
    private Color color;
    public RoundedBorder(int radius, Color color, int thickness) {
        this.radius = radius;
        this.thickness = thickness;
        this.color = color;
    }

    public Insets getBorderInsets(Component c) {
        return new Insets(this.radius+1, this.radius+1, this.radius+2, this.radius);
    }

    public boolean isBorderOpaque() {
        return true;
    }

    public void paintBorder(Component c, Graphics g, int x, int y, int width, int height) {

        g.setColor(color);
        Graphics2D g2 = (Graphics2D) g;
        RenderingHints qualityHints =  new RenderingHints(RenderingHints.KEY_ANTIALIASING,RenderingHints.VALUE_ANTIALIAS_ON );
        qualityHints.put(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY );
        g2.setRenderingHints(qualityHints);   
        g2.setStroke(new BasicStroke((float)thickness));
        g.drawRoundRect(thickness, thickness, c.getSize().width - 2*thickness, c.getSize().height - 2*thickness, radius, radius);
        g2.setClip(thickness, thickness, width, height);
    }
}

And I have tried the following approach as well to draw my border: Which gets me this result: Spinner Result from class: RoundedColouredBorder

在此处输入图片说明

This time the border is not clean for some reason and the Spinner Text area clips into the spinner border giving it an odd outside round edge but inside sharp corner edge. (not what I want) and once again the border is not drawn on the right side of the spinner.

public static class RoundedColouredBorder implements Border {
        private int radius;
        private int thickness;
        private Color color;

        public RoundedColouredBorder(int radius, Color borderColor, int thickness) {
            this.radius = radius;
            this.color = borderColor;
            this.thickness = thickness;
        }

        public Insets getBorderInsets(Component c) {
            return new Insets(this.thickness+1, this.thickness+1, this.thickness+2, this.thickness);
        }

        public boolean isBorderOpaque() {
            return true;
        }

        public void paintBorder(Component c, Graphics g, int x, int y, int width, int height) {
            Dimension arcs = new Dimension(radius, radius);

            Graphics2D graphics = (Graphics2D) g;
            graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);

            //Draws the rounded panel with borders.
            graphics.setColor(color);
            graphics.fillRoundRect(0, 0, width + thickness, height + thickness, arcs.width, arcs.height); //paint background
            graphics.drawRoundRect(0, 0, width - thickness, height  -thickness, arcs.width, arcs.height); //paint border

        }
    }

What I am looking to draw is the following:

Desired Result

在此处输入图片说明

So basically for my desired result, I want a rounded border around the entire spinner and a rounded border around each arrow button for which I can adjust the colour, thickness of the border and radius of corners.

In my 2 above attempts using my RoundedBorder and RoundedColouredBorder classes I am getting really weird clipping and the border is not as clean as I want it to be. The Result obtained from RoundedBorder class seems to cut the white spinner text area to a smaller size and restretch it in a very odd way. What am I doing incorrectly?

After about the middle of this post is the code of my answer, but I feel like sharing first with you the effort of finding out what was the answer, to try and convience you that this is the best solution I (at least) could find. So here it goes:

  • You need to draw a border around the buttons and spinner with adjustable arc radius. So you need to define your own border because no other border does that. Actually the LineBorder is the closest I could find, because there is a construction parameter roundedCorners which rounds with a non-adjustable value the arc radius of the corners of the border. After looking into the implentation of LineBorder , I think you can safely subclass it to always have rounded corners with also an adjustable arc radius. So you only need to override paintBorder to always paint rounded corners, and then make a setter and getter for the arc radius.
  • You need a button which will be of a custom shape (such as a RounRectangle2D as per your question). That means that not only it will be painted with a custom shape, but that custom shape will also be used to define whether the mouse cursor is over the button or not. As I found out (which means I might be wrong, but this is my best try) you have two options for the second part:

    1. Override ComponentUI.contains to define which points are inside the button. That means subclassing ComponentUI (or more approprietly subclassing ButtonUI to be able to set the UI of the button).
    2. Override Component.contains to define which points are inside the button. That means subclassing Component (or more approprietly, subclassing JButton in this case).

    You may wonder which of the two options is actually used to define which points are inside the button. Well, both, because the default implementation of ComponentUI.contains delegates to Component.contains . Despite that, the second option already seems better as it looks more like PLAF-independent. But, for the first part, you also need the button to be painted only inside the shape you define and not inside its (square) bounds. That means overriding paint and update of the button (which means subclassing JComponent , or even more approprietly JButton ) to set a custom clip. So that leads us to subclassing JButton and solve both problems at once (plus PLAF-independently probably).

  • You need a spinner which will be of a custom shape. Following the reasons we need to subclass the JButton class, we also need to subclass the JSpinner class to provide our custom shape.
  • I've also noticed, in your desired result, that there is a gap between the two buttons. I also know from searching the implementation of JSpinner and its UI, that there are 3 components which are added to the JSpinner (as it is a normal Container ): the editor, the next button and the previous button. So who is responsible to set the location and size of the components added to a container?... Its LayoutManager . So you also need a custom LayoutManager for this, which will add the gap between the buttons while laying them out in the spinner. The current implementation of the LayoutManger of the JSpinner can be found inside BasicSpinnerUI as the Handler class. I'm just letting you know in case you want to extend its operation with your own custom LayoutManager . In the code of this post I also implement a custom LayoutManager based on the Handler class.
  • After searching a bit more on the implementation of JSpinner and its UI, I found out that the spinner's 3 components are created as follows:

    1. The editor is created inside the JSpinner itself, depending on the SpinnerModel . The UI of the spinner then gets (with JSpinner.getEditor ) the editor of the spinner and initializes it.
    2. The next button is created actually inside the spinner's UI ( BasicSpinnerUI ) and then added to the spinner.
    3. The previous button is created also inside the spinner's UI and then added to the spinner.

    So that creates the need to subclass BasicSpinnerUI and override BasicSpinnerUI.createPreviousButton and BasicSpinnerUI.createNextButton to return our custom JButton subclass we created having a custom shape.

  • Finally, I moved the creation of the buttons of the spinner from the BasicSpinnerUI to the spinner. That will allow us to export the buttons with a getter and setter in the custom spinner, just like the editor is already exported in the default implementation of JSpinner . Just modified the custom BasicSpinnerUI to get the custom buttons from the custom spinner in createPreviousButton and createNextButton , just like the BasciSpinnerUI.createEditor does with the JSpinner 's editor. This will allow easier post-creation (eg on-the-fly) customization of the buttons.

Note, I made the classes as independent as possible, which means:

  1. The custom BasicSpinnerUI subclass can be used for regular JSpinner s.
  2. The custom LayoutManager will lay out any container having 3 components with names "Editor", "Previous" and "Next"...
  3. The custom JSpinner works fine on its own without any custom BasicSpinnerUI on it.
  4. The custom LineBorder works fine on its own.
  5. The custom JButton works fine on its own. It is just a JButton with a custom shape.

But all the above need to be combined to create your desired result.

Final observation:

According to the documentation of JComponent.isOpaque " An opaque component paints every pixel within its rectangular bounds. ". Well that rectangular keyword is a problem, because we need the spinner (and its both buttons, and its editor and its text field) to be of a custom shape. So make sure to call setOpaque(false) on the spinner, on both its buttons, on the editor of the spinner and on the text field of the editor of the spinner, because we paint and act like a custom shape in each case.

To sum up, some working code:

import java.awt.Color;
import java.awt.Component;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Insets;
import java.awt.LayoutManager;
import java.awt.Shape;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.geom.Path2D;
import java.awt.geom.RoundRectangle2D;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JFormattedTextField;
import javax.swing.JFrame;
import javax.swing.JSpinner;
import javax.swing.JSpinner.DefaultEditor;
import javax.swing.JTextField;
import javax.swing.SpinnerModel;
import javax.swing.SpinnerNumberModel;
import javax.swing.border.LineBorder;
import javax.swing.plaf.basic.BasicSpinnerUI;

public class Main {

    //This is a LineBorder only that it always paints a RoundRectangle2Ds instead of Rectangle2Ds.
    public static class CustomLineBorder extends LineBorder {
        private double arcw, arch;

        public CustomLineBorder(Color color, int thickness, double arcw, double arch) {
            super(color, thickness);
            this.arcw = arcw;
            this.arch = arch;
        }

        //Note: the implementation of this paintBorder is inspired by the superclass.
        @Override
        public void paintBorder(Component c, Graphics g, int x, int y, int width, int height) {
            if ((thickness > 0) && (g instanceof Graphics2D)) {
                Graphics2D g2d = (Graphics2D) g;
                Color oldColor = g2d.getColor();
                g2d.setColor(lineColor);
                Path2D path = new Path2D.Double(Path2D.WIND_EVEN_ODD);
                path.append(new RoundRectangle2D.Double(x, y, width, height, thickness, thickness), false);
                path.append(new RoundRectangle2D.Double(x + thickness, y + thickness, width - 2 * thickness, height - 2 * thickness, arcw, arch), false);
                g2d.fill(path);
                g2d.setColor(oldColor);
            }
        }

        public void setArcWidth(double arcw) {
            this.arcw = arcw;
        }

        public void setArcHeight(double arch) {
            this.arch = arch;
        }

        public void setLineColor(Color lineColor) {
            this.lineColor = lineColor;
        }

        public double getArcWidth() {
            return arcw;
        }

        public double getArcHeight() {
            return arch;
        }
    }

    public static class CustomJButton extends JButton {
        private double arcw, arch;

        public CustomJButton(double arcw, double arch) {
            this.arcw = arcw;
            this.arch = arch;
        }

        public void setArcWidth(double arcw) {
            this.arcw = arcw;
            revalidate(); //Not sure if needed.
            repaint();
        }

        public void setArcHeight(double arch) {
            this.arch = arch;
            revalidate(); //Not sure if needed.
            repaint();
        }

        public double getArcWidth() {
            return arcw;
        }

        public double getArcHeight() {
            return arch;
        }

        @Override
        public Dimension getPreferredSize() {
            //Here you set the preferred size of the button to something which takes into account the arc width and height:
            Dimension sz = super.getPreferredSize();
            sz.width = Math.max(sz.width, Math.round((float) getArcWidth()));
            sz.height = Math.max(sz.height, Math.round((float) getArcHeight()));
            return sz;
        }

        //Note that the width/height/arcw/arch of the component are not constant. Thats why we create a new instance of RoundRectangle2D.Double every time...
        protected Shape createShape() {
            return new RoundRectangle2D.Double(0, 0, getWidth(), getHeight(), getArcWidth(), getArcHeight());
        }

        //Paint only inside the createShape's Shape:
        @Override
        public void paint(Graphics g) {
            Graphics2D g2d = (Graphics2D) g.create();
            g2d.setClip(createShape());
            super.paint(g2d);
            g2d.dispose();
        }

        //Update only inside the createShape's Shape:
        @Override
        public void update(Graphics g) {
            Graphics2D g2d = (Graphics2D) g.create();
            g2d.setClip(createShape());
            super.update(g2d);
            g2d.dispose();
        }

        //Tell which points are inside this button:
        @Override
        public boolean contains(int x, int y) {
            return createShape().contains(x, y);
        }
    }

    //The implementation of this subclass is inspired by the private static class Handler of the BasicSpinnerUI:
    public static class CustomJSpinnerLayout implements LayoutManager {
        private final int gap; //You can make this non-final and add setter and getter, but remember
        //to call revalidate() on the spinner whenever you change this gap of this class...

        private Component nextButton;
        private Component previousButton;
        private Component editor;

        public CustomJSpinnerLayout(int gap) {
            this.gap = gap;
            nextButton = null;
            previousButton = null;
            editor = null;
        }

        //Only recognizes 3 components ("Next", "Previous" and "Editor"). Others are not layed out.
        @Override
        public void addLayoutComponent(String constraints, Component c) {
            switch (constraints) {
                case "Next": nextButton = c; break;
                case "Previous": previousButton = c; break;
                case "Editor": editor = c; break;
            }
        }

        @Override
        public void removeLayoutComponent(Component c) {
            if (c == nextButton)
                nextButton = null;
            else if (c == previousButton)
                previousButton = null;
            else if (c == editor)
                editor = null;
        }

        @Override
        public Dimension preferredLayoutSize(Container parent) {
            return minimumLayoutSize(parent);
        }

        //Only recognizes 3 components ("Next", "Previous" and "Editor"). Others are not taken into account.
        @Override
        public Dimension minimumLayoutSize(Container parent) {
            Dimension next = nextButton.getPreferredSize();
            Dimension prev = previousButton.getPreferredSize();
            Dimension edit = editor.getPreferredSize();
            Insets pari = parent.getInsets();
            int totalHeight = Math.max(edit.height, next.height + prev.height + gap);
            int buttonMaxWidth = Math.max(next.width, prev.width);
            return new Dimension(buttonMaxWidth + edit.width + pari.left, totalHeight + pari.top + pari.bottom);
        }

        //Only recognizes 3 components ("Next", "Previous" and "Editor"). Others are not layed out.
        @Override
        public void layoutContainer(Container parent) {
            if (editor != null || nextButton != null || previousButton != null) {
                //Warning: does not account for component orientation (eg leftToRight or not).
                Dimension prnt = parent.getSize();
                Dimension next = nextButton.getPreferredSize();
                Dimension prev = previousButton.getPreferredSize();
                Insets i = parent.getInsets();
                int maxButtonWidth = Math.max(next.width, prev.width);
                int buttonHeight = Math.round((prnt.height - gap) / 2f);
                editor.setBounds(i.left, i.top, prnt.width - i.left - i.right - maxButtonWidth, prnt.height - i.top - i.bottom);
                nextButton.setBounds(prnt.width - maxButtonWidth, 0, maxButtonWidth, buttonHeight);
                previousButton.setBounds(prnt.width - maxButtonWidth, prnt.height - buttonHeight, maxButtonWidth, buttonHeight);
            }
        }
    }

    public static class CustomBasicSpinnerUI extends BasicSpinnerUI {

        //Works like createEditor() of BasicSpinnerUI, in that it gets the spinner's button from the spinner itself.
        @Override
        protected Component createPreviousButton() {
            if (spinner instanceof CustomJSpinner) {
                CustomJButton prev = ((CustomJSpinner) spinner).getButtonPrevious();
                prev.setInheritsPopupMenu(true); //Inspired by the code of the private BasicSpinnerUI.createArrowButton().
                prev.setName("Spinner.previousButton"); //Required by the code of BasicSpinnerUI.createPreviousButton().
                installPreviousButtonListeners(prev); //Required by the code of BasicSpinnerUI.createPreviousButton().
                return prev;
            }
            return super.createPreviousButton(); //If this UI is added to a non CustomJSpinner, then return default implementation.
        }

        //Works like createEditor() of BasicSpinnerUI, in that it gets the spinner's button from the spinner itself.
        @Override
        protected Component createNextButton() {
            if (spinner instanceof CustomJSpinner) {
                CustomJButton next = ((CustomJSpinner) spinner).getButtonNext();
                next.setInheritsPopupMenu(true); //Inspired by the code of the private BasicSpinnerUI.createArrowButton().
                next.setName("Spinner.nextButton"); //Required by the code of BasicSpinnerUI.createNextButton().
                installNextButtonListeners(next); //Required by the code of BasicSpinnerUI.createNextButton().
                return next;
            }
            return super.createNextButton(); //If this UI is added to a non CustomJSpinner, then return default implementation.
        }

        //Creates the default LayoutManager for the JSpinner.
        //Could be replaced by a call to setLayout on the custom JSpinner.
        @Override
        protected LayoutManager createLayout() {
            return new CustomJSpinnerLayout(8);
        }
    }

    public static class CustomJSpinner extends JSpinner {
        private CustomJButton next, prev; //Maintain a reference to the buttons, just like the JSpinner does for the editor...
        private double arcw, arch;

        public CustomJSpinner(SpinnerModel model, double arcw, double arch) {
            super(model);
            this.arcw = arcw;
            this.arch = arch;
            next = new CustomJButton(arcw, arch);
            prev = new CustomJButton(arcw, arch);
        }

        public void setButtonPrevious(CustomJButton prev) {
            this.prev = prev;
            revalidate();
            repaint();
        }

        public void setButtonNext(CustomJButton next) {
            this.next = next;
            revalidate();
            repaint();
        }

        public CustomJButton getButtonPrevious() {
            return prev;
        }

        public CustomJButton getButtonNext() {
            return next;
        }

        public void setArcWidth(double arcw) {
            this.arcw = arcw;
            revalidate(); //Not sure if needed.
            repaint();
        }

        public void setArcHeight(double arch) {
            this.arch = arch;
            revalidate(); //Not sure if needed.
            repaint();
        }

        public double getArcWidth() {
            return arcw;
        }

        public double getArcHeight() {
            return arch;
        }

        //Note that the width/height/arcw/arch of the component are not constant. Thats why we create a new instance of RoundRectangle2D.Double every time...
        protected Shape createShape() {
            return new RoundRectangle2D.Double(0, 0, getWidth(), getHeight(), arcw, arch);
        }

        //Paint only inside the createShape's Shape:
        @Override
        public void paint(Graphics g) {
            Graphics2D g2d = (Graphics2D) g.create();
            g2d.setClip(createShape());
            Color old = g2d.getColor();
            g2d.setColor(getBackground());
            g2d.fillRect(0, 0, getWidth(), getHeight());
            g2d.setColor(old);
            super.paint(g2d);
            g2d.dispose();
        }

        //Update only inside the createShape's Shape:
        @Override
        public void update(Graphics g) {
            Graphics2D g2d = (Graphics2D) g.create();
            g2d.setClip(createShape());
            Color old = g2d.getColor();
            g2d.setColor(getBackground());
            g2d.fillRect(0, 0, getWidth(), getHeight());
            g2d.setColor(old);
            super.update(g2d);
            g2d.dispose();
        }

        //Tell which points are inside this spinner:
        @Override
        public boolean contains(int x, int y) {
            return createShape().contains(x, y);
        }
    }

    private static void initCustomJButton(CustomJButton cjb, String text, Color nonRolloverBorderColor, Color rolloverBorderColor, int borderThickness) {
        cjb.setOpaque(false); //Mandatory.
        cjb.setText(text); //Could be setIcon...

        //All the folllowing steps of this method are optional (remove them, edit them, etc as you like).

        //Add a CustomLineBorder to the CustomJButton (upon your request):
        CustomLineBorder clb = new CustomLineBorder(nonRolloverBorderColor, borderThickness, cjb.getArcWidth(), cjb.getArcHeight());
        cjb.setBorder(clb);

        //Create the mouse rollover effect of changing the color of the border of the button when the mouse hovers over the button:
        cjb.addMouseListener(new MouseAdapter() {
            @Override
            public void mouseEntered(MouseEvent mevt) {
                clb.setLineColor(rolloverBorderColor);
                cjb.repaint();
            }

            @Override
            public void mouseExited(MouseEvent mevt) {
                clb.setLineColor(nonRolloverBorderColor);
                cjb.repaint();
            }
        });
    }

    public static void main(String[] args) {

        //Setup parameters:
        double arcw = 50, arch = 50;
        int borderThickness = 2;
        Color borderMainColor = Color.CYAN.darker(), buttonRolloverBorderColor = Color.CYAN;

        //Create the spinner:
        CustomJSpinner spin = new CustomJSpinner(new SpinnerNumberModel(), arcw, arch);

        //Customizing spinner:
        spin.setUI(new CustomBasicSpinnerUI()); //Mandatory first step!
        spin.setOpaque(false); //Mandatory.
        spin.setBorder(new CustomLineBorder(borderMainColor, borderThickness, spin.getArcWidth(), spin.getArcHeight())); //Upon your request.
        spin.setPreferredSize(new Dimension(200, 200)); //Optional.
        spin.setBackground(Color.RED); //Obviously needs to be changed to "Color.WHITE", but for demonstration let it be "Color.RED".

        //Customizing spinner's buttons:
        initCustomJButton(spin.getButtonNext(), "Next", borderMainColor, buttonRolloverBorderColor, borderThickness);
        initCustomJButton(spin.getButtonPrevious(), "Prev", borderMainColor, buttonRolloverBorderColor, borderThickness);

        //Customizing spinner's editor:
        JComponent editor = spin.getEditor();
        editor.setOpaque(false); //Mandatory.
        if (editor instanceof DefaultEditor) {
            JFormattedTextField jftf = ((DefaultEditor) editor).getTextField();
            jftf.setOpaque(false); //Mandatory.
            jftf.setHorizontalAlignment(JTextField.CENTER); //Upon your request.
            //jftf.setFont(new Font(Font.MONOSPACED, Font.ITALIC, 25));
        }

        JFrame frame = new JFrame("Customized JSpinner");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.getContentPane().add(spin);
        frame.pack();
        frame.setLocationRelativeTo(null);
        frame.setVisible(true);
    }
}

Note, I worked on arc width and height instead of radius.

And sample output:

样本输出

Obviously you need the background color to be white, instead of red as is in the picture, but I will leave it red just to demonstrate how it looks like.

What I do not address in this answer:

Well, the text field of the editor of the spinner, as well as the editor itself, turn out to have a weird shape as a result of the desired one. So, you will need rounded corners on the left side, and square corners on the right side! That means a custom Shape (probably union of shapes, such as an Area ) which will define which points are inside the text field / editor. In this case, the default editor of the JSpinner uses a JFormattedTextField as the text field and an instanceof JSpinner.DefaultEditor as the editor. As we covered previously, for the custom spinner and for the custom button, you have two options: subclass the JFormattedTextField (and JSpinner.DefaultEditor ), or create a custom UI for the JFormattedTextField / JSpinner.DefaultEditor . There are some problems with those solutions:

  1. There is no setTextField (to be used) or createTextField (to be overriden) in the JSpinner.DefaultEditor , and the only way to replace it with a custom one is to find and remove the corresponding component in the editor and add the new one with the same characteristics.
  2. If you feel like subclassing JSpinner.DefaultEditor , then you will need to subclass also JSpinner.ListEditor , JSpinner.NumberEditor and JSpinner.DateEditor , but also JSpinner to override its createEditor method to return the new types depending on the model.

And their (as simple as possible) solutions seem to be:

  • Create new types of editors from scratch by subclassing JComponent (and override its contains method).
  • Create a custom BasicFormattedTextFieldUI for the text field of the editor (and override its contains method).

or

  • Subclass BasicPanelUI for the default editor of the spinner (and override its contains method).
  • Create a custom BasicFormattedTextFieldUI for the text field of the editor (and override its contains method).

These problems seem solvable but would enlarge even more the solution (and it is already 400+ lines of code and comments). So I chose not to address those problems and the result is that the text field and the editor are square inside the rounded spinner. Which means that there are regions (the corners) where the text field can get focus if the user clicks there, while the user actually clicked on the borders of the rounded spinner for example. The text field and editor are not going to paint themselves over the rounded spinner at the corners, because we set them to non-opaque! The background color is handled by the spinner itself.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM