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:
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:
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).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).
JButton
class, we also need to subclass the JSpinner
class to provide our custom shape.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:
JSpinner
itself, depending on the SpinnerModel
. The UI of the spinner then gets (with JSpinner.getEditor
) the editor of the spinner and initializes it.BasicSpinnerUI
) 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.
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:
BasicSpinnerUI
subclass can be used for regular JSpinner
s.LayoutManager
will lay out any container having 3 components with names "Editor", "Previous" and "Next"...JSpinner
works fine on its own without any custom BasicSpinnerUI
on it.LineBorder
works fine on its own.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:
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.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:
JComponent
(and override its contains
method).BasicFormattedTextFieldUI
for the text field of the editor (and override its contains
method).or
BasicPanelUI
for the default editor of the spinner (and override its contains
method). 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.