首页 > 解决方案 > 如何将 JSpinner 的边框更改为具有可调节半径 i 的圆角的自定义彩色边框

问题描述

我想自定义我的 JSpinner 以给它一个自定义边框,该边框具有可调节的颜色、可调节的边框厚度和可调节半径的圆角。这样我就可以为微调器设置边框并完成它。

我的微调器代码如下:

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

我给我的微调器自定义箭头以下类:

我设置了箭头的尺寸,以便它们的大小改变为我想要的。我认为这一切都非常简单明了。但是当我尝试为箭头按钮提供自定义边框以及尝试为整个微调器提供自定义边框时,我的问题就会发生。

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

我已经尝试了以下结果:注意微调器文本区域是如何被向内剪裁的(我相信它也被奇怪地拉伸了......并且边框没有绘制在微调器的最右侧边缘。微调器结果来自类:圆角

在此处输入图像描述

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

我也尝试了以下方法来绘制我的边框:这让我得到了这个结果:Spinner Result from class: RoundedColouredBorder

在此处输入图像描述

这次由于某种原因边框不干净,微调器文本区域剪辑到微调器边框中,使其具有奇怪的外圆边缘但位于尖角边缘内。(不是我想要的)并且再次没有在微调器的右侧绘制边框。

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

        }
    }

我要画的是以下内容:

期望的结果

在此处输入图像描述

所以基本上为了我想要的结果,我想要整个微调器周围的圆形边框和每个箭头按钮周围的圆形边框,我可以调整颜色、边框的厚度和角的半径。

在我使用 RoundedBorder 和 RoundedColouredBorder 类的上述 2 次尝试中,我得到了非常奇怪的剪辑,并且边框不像我想要的那样干净。从 RoundedBorder 类获得的 Result 似乎将白色微调器文本区域切割成更小的尺寸,并以一种非常奇怪的方式重新拉伸它。我做错了什么?

标签: javaswingjspinner

解决方案


在这篇文章的中间部分之后是我的答案的代码,但我想先与你分享找出答案的努力,试图让你相信这是我(至少)能找到的最佳解决方案. 所以这里是:

  • 您需要在按钮和微调器周围绘制一个可调节圆弧半径的边框。因此,您需要定义自己的边界,因为没有其他边界可以这样做。实际上这LineBorder是我能找到的最接近的,因为有一个构造参数roundedCorners,它以不可调整的值对边界角的圆弧半径进行四舍五入。在研究了 的实现之后LineBorder,我认为您可以安全地将其子类化为始终具有圆角以及可调节的圆弧半径。所以你只需要覆盖paintBorder总是画圆角,然后为圆弧半径做一个设置器和吸气器。
  • 您需要一个具有自定义形状的按钮(例如RounRectangle2D根据您的问题)。这意味着不仅将使用自定义形状绘制它,而且该自定义形状还将用于定义鼠标光标是否位于按钮上方。我发现(这意味着我可能错了,但这是我最好的尝试)第二部分有两种选择:

    1. 覆盖ComponentUI.contains以定义按钮内的点。这意味着子类化ComponentUI(或更恰当地子类ButtonUI化以便能够设置按钮的 UI)。
    2. 覆盖Component.contains以定义按钮内的点。这意味着子类化Component(或更恰当地说,JButton在这种情况下是子类化)。

    您可能想知道这两个选项中的哪一个实际上用于定义按钮内部的点。好吧,两者都有,因为ComponentUI.contains委托的默认实现是Component.contains. 尽管如此,第二种选择似乎已经更好了,因为它看起来更像是独立于解放军的。但是,对于第一部分,您还需要仅在您定义的形状内而不是在其(正方形)边界内绘制按钮。这意味着覆盖paintupdate按钮(这意味着子类化JComponent,甚至更恰当地JButton)来设置自定义剪辑。因此,这导致我们对子类JButton化并同时解决这两个问题(加上可能独立于 PLAF)。

  • 您需要一个具有自定义形状的微调器。继我们需要对类进行子类化的原因之后JButton,我们还需要对类进行子JSpinner类化以提供我们的自定义形状。
  • 我还注意到,在您想要的结果中,两个按钮之间存在间隙。我还从搜索JSpinner其 UI 的实现中知道,有 3 个组件被添加到JSpinner(因为它是正常的Container):编辑器、下一个按钮和上一个按钮。那么谁负责设置添加到容器中的组件的位置和大小?... 它的LayoutManager. 因此,您还需要对此进行自定义LayoutManager,这将增加按钮之间的间隙,同时将它们放置在微调器中。的当前实现LayoutManger可以JSpinnerBasicSpinnerUI类中找到Handler。如果您想使用自己的自定义扩展其操作,我只是让您知道LayoutManager。在这篇文章的代码中,我还实现了一个自定义LayoutManager基于Handler类。
  • 在搜索了更多关于它的实现JSpinner及其 UI 之后,我发现微调器的 3 个组件创建如下:

    1. 编辑器是在JSpinner自身内部创建的,具体取决于SpinnerModel. 然后微调器的 UI 获取(使用JSpinner.getEditor)微调器的编辑器并对其进行初始化。
    2. 下一个按钮实际上是在微调器的 UI ( BasicSpinnerUI) 中创建的,然后添加到微调器中。
    3. 上一个按钮也在微调器的 UI 中创建,然后添加到微调器。

    所以这就需要子类化BasicSpinnerUI和覆盖BasicSpinnerUI.createPreviousButton,并BasicSpinnerUI.createNextButton返回JButton我们创建的具有自定义形状的自定义子类。

  • 最后,我将微调器按钮的创建从微调器BasicSpinnerUI移至微调器。这将允许我们在自定义微调器中使用 getter 和 setter 导出按钮,就像在默认实现中已导出编辑器一样JSpinner。只需修改 custom以从andBasicSpinnerUI中的自定义微调器中获取自定义按钮,就像使用's 编辑器所做的那样。这将允许对按钮进行更轻松的后期创建(例如即时)定制。createPreviousButtoncreateNextButtonBasciSpinnerUI.createEditorJSpinner

请注意,我使类尽可能独立,这意味着:

  1. 自定义BasicSpinnerUI子类可用于常规JSpinners。
  2. 自定义LayoutManager将布置任何具有 3 个组件的容器,这些组件的名称分别为“Editor”、“Previous”和“Next”...
  3. 自定义JSpinner可以自行正常工作,无需任何自定义BasicSpinnerUI
  4. 自定义LineBorder工作正常。
  5. 自定义JButton工作正常。它只是JButton一个自定义形状。

但是以上所有内容都需要结合起来才能创建您想要的结果。

最后观察:

根据JComponent.isOpaque一个不透明的组件在其矩形边界内绘制每个像素。 ”的文档。好吧,矩形关键字是个问题,因为我们需要微调器(以及它的两个按钮、它的编辑器和它的文本字段)具有自定义形状。因此,请确保调用setOpaque(false)微调器、其按钮、微调器编辑器以及微调器编辑器的文本字段,因为我们在每种情况下都像自定义形状一样进行绘制和操作。

总而言之,一些工作代码:

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

请注意,我研究的是弧宽和高度而不是半径。

和样本输出:

样本输出

显然,您需要背景颜色为白色,而不是图片中的红色,但我将其保留为红色只是为了演示它的外观。

我在这个答案中没有解决的问题:

好吧,微调器的编辑器的文本字段以及编辑器本身,由于所需的形状而变成了一种奇怪的形状。所以,你需要左边的圆角,右边的方角!这意味着一个自定义Shape(可能是形状的联合,例如一个Area),它将定义哪些点位于文本字段/编辑器内。在这种情况下,默认编辑器JSpinner使用 aJFormattedTextField作为文本字段,使用 instanceofJSpinner.DefaultEditor作为编辑器。如前所述,对于自定义微调器和自定义按钮,您有两个选择:子类化JFormattedTextField(and ),或为/JSpinner.DefaultEditor创建自定义 UI 。这些解决方案存在一些问题:JFormattedTextFieldJSpinner.DefaultEditor

  1. 中没有setTextField(要使用)或createTextField(要被覆盖),JSpinner.DefaultEditor唯一的方法是用自定义的替换它是在编辑器中找到并删除相应的组件并添加具有相同特征的新组件.
  2. 如果您喜欢子类化JSpinner.DefaultEditor,那么您还需要子类化JSpinner.ListEditor,JSpinner.NumberEditorJSpinner.DateEditor, 还要JSpinner重写其createEditor方法以根据模型返回新类型。

他们(尽可能简单)的解决方案似乎是:

  • JComponent通过子类化(并覆盖其contains方法)从头开始创建新类型的编辑器。
  • 为编辑器的文本字段创建自定义BasicFormattedTextFieldUI(并覆盖其contains方法)。

或者

  • 微调器的默认编辑器的子类BasicPanelUI(并覆盖其contains方法)。
  • 为编辑器的文本字段创建自定义BasicFormattedTextFieldUI(并覆盖其contains方法)。

这些问题似乎可以解决,但会进一步扩大解决方案(并且已经有 400 多行代码和注释)。所以我选择不解决这些问题,结果是文本字段和编辑器在圆形微调器内是方形的。这意味着如果用户单击文本字段可以在某些区域(角)获得焦点,而用户实际上单击了圆形微调器的边框。文本字段和编辑器不会在角落的圆形微调器上绘制自己,因为我们将它们设置为不透明!背景颜色由微调器本身处理。


推荐阅读