0

Sorry if my question doesn't adjust to the Stackoverflow requirements due to it is theorical but i don't know where else to ask.

For the past few weeks i've been trying to understand better how the Swing API works and it's components in order to create my own custom components. I've read tons of tutorials, searched in here, i am neck-deep in Java's swing source code and frankly... my mind is a mess.

As far as i understand, the swing components are composed of 3 parts:

  • the model: where the component state and data are stored
  • the UI delegate: which paints the component and
  • the JComponent: it ties everything together.

In this tutorial https://docs.oracle.com/javase/tutorial/uiswing/painting/step2.html there is a paragraph that says:

The paintComponent method is where all of your custom painting takes place. >This method is defined by javax.swing.JComponent and then overridden by your >subclasses to provide their custom behavior

Why is there a paintComponent method on a JComponent? Shouldn't it be an exclusive method of an UI delegate?

potato
  • 140
  • 8
  • What do you mean by exclusive on UI delegate? – 11thdimension Nov 16 '17 at 21:29
  • @11thdimension I mean that it sould only be a paintComponent method inside a UI delegate. – potato Nov 16 '17 at 22:05
  • `paintComponent` calls the `UIDelegate`'s paint method, it's the only way that the UI delegate knows when a component needs to be painted. This comes down to how the paint system works. Paint events cause components to be painted - this probably comes from it's reliance on AWT subsystem – MadProgrammer Nov 16 '17 at 22:06
  • Oh, and as a developer, I'm glad I don't need to create a UI delegate every time I want to customise the painting of the component, that'd be a lot of troublesome work (IMHO). The UI delegate is a plugin component to `JComponent` rather then a API layer – MadProgrammer Nov 16 '17 at 22:09
  • @MadProgrammer. Thank you, as always. The problem is the i want to create buttons with different shapes but without modifying the LookAndFeel. The code i ended up allows me to modify a button with the shape that i want but it removes the LookAndFeel. So, to accomplish this task, you suggested me (in another post) to create a new UIDelegate and in order to that i am trying to have a better knowledge on how Swing components work and not needing bothering people with noob questions. Turns out it's harder than i was expecting. :( – potato Nov 16 '17 at 22:20
  • The complexity of your issue will probably require you to use a custom look and feel - since you don't really want to keep the original properties of the look and feel (or at least you could steal some of them if you wanted to). Basically a UI delegate is installed into the `UIManager`, which is then installed into all instance of the component it's supporting (i.e. the `JButton`), when any `JButton` needs to be called, the instance of the delegate is notified, this means that while you might have many buttons, you only have a single instance of the delegate – MadProgrammer Nov 16 '17 at 23:38
  • I've done a [round button before](https://stackoverflow.com/questions/13386749/how-to-add-to-this-round-button-metal-background-in-java/13389263#13389263), but it's really only faking the concept – MadProgrammer Nov 16 '17 at 23:39

1 Answers1

1

Swing is based on AWT. So the initial restriction is based on how AWT paints it components. Because Swing is light weight, when a components root component is painted, the child, Swing, components need to be notified that they need to update as well. This is done by calling the paint methods of all the child components affected by the update (which in turn call the paintComponent methods).

Much of the decisions about the Swing API's paint chain are based around the concept of customisation. It reduces the complexity involved (by discouraging the overriding of paint and focus the functionality down t the paintComponent method).

The Look and Feel API is based on a "delegate" model. This means that the functionality used to perform a said action is "delegated" to some other object. This means that the UI delegate UI doesn't actually know when a component "needs" to be painted, but instead is told, by the component, that it needs to be painted. This makes it much more flexible and in many cases, easier to customise.

Based on your previous question a custom ButtonUI is probably one the better choices, this way you gain much more control over how the button is painted.

Trying to get the buttons to follow the current look and feels color schemes would be very difficult, but you could try having a look at the src.jar which is installed with the JDK, which includes many implementations of look and feels (and if your on Windows you should get the Windows look and feel as well, if your on Mac, then you don't get either Mac or Windows )

I started by having a look at BasicButtonUI and ButtonUI to get a better understanding of there properties. I pulled some of more interesting methods into a custom ShapeButtonUI...

public class ShapeButtonUI extends BasicButtonUI {

    private Shape shape;

    public ShapeButtonUI(Shape shape) {
        this.shape = shape;
    }

    protected Color getSelectColor() {
        return UIManager.getColor(getPropertyPrefix() + "select");
    }

    protected Color getDisabledTextColor() {
        return UIManager.getColor(getPropertyPrefix()
                        + "disabledText");
    }

    protected Color getFocusColor() {
        return UIManager.getColor(getPropertyPrefix() + "focus");
    }

    @Override
    protected void installDefaults(AbstractButton b) {
        super.installDefaults(b);
    }

    @Override
    protected void uninstallDefaults(AbstractButton b) {
        super.uninstallDefaults(b);
    }

    @Override
    public void paint(Graphics g, JComponent c) {
        Graphics2D g2d = (Graphics2D) g.create();
        g2d.setClip(shape);
        Rectangle bounds = shape.getBounds();
        LinearGradientPaint lgp = new LinearGradientPaint(
                        new Point(bounds.x, bounds.y),
                        new Point(bounds.x, bounds.y + bounds.height),
                        new float[]{0, 1},
                        new Color[]{c.getBackground().brighter(), c.getBackground().darker()});
        g2d.setPaint(lgp);
        g2d.fill(shape);
        g2d.dispose();
        g2d = (Graphics2D) g.create();
        g2d.setColor(c.getForeground());
        g2d.draw(shape);
        g2d.dispose();
        super.paint(g, c);
    }

    @Override
    protected void paintButtonPressed(Graphics g, AbstractButton b) {
        super.paintButtonPressed(g, b);
    }

    @Override
    protected void paintFocus(Graphics g, AbstractButton b,
                    Rectangle viewRect, Rectangle textRect, Rectangle iconRect) {
        super.paintFocus(g, b, viewRect, textRect, iconRect);
    }

    @Override
    protected void paintText(Graphics g, AbstractButton b, Rectangle textRect, String text) {
        super.paintText(g, b, textRect, text);

//          ButtonModel model = b.getModel();
//          FontMetrics fm = SwingUtilities2.getFontMetrics(c, g);
//          int mnemIndex = b.getDisplayedMnemonicIndex();
//
//          /* Draw the Text */
//          if (model.isEnabled()) {
//              /**
//               * * paint the text normally
//               */
//              g.setColor(b.getForeground());
//          } else {
//              /**
//               * * paint the text disabled **
//               */
//              g.setColor(getDisabledTextColor());
//          }
//          SwingUtilities2.drawStringUnderlineCharAt(c, g, text, mnemIndex,
//                          textRect.x, textRect.y + fm.getAscent());

    }

    @Override
    public Dimension getMinimumSize(JComponent c) {
        Rectangle bounds = shape.getBounds();
        return new Dimension(bounds.x + bounds.width + 1, bounds.y + bounds.height + 1);
    }

    @Override
    public Dimension getPreferredSize(JComponent c) {
        Rectangle bounds = shape.getBounds();
        return new Dimension(bounds.x + bounds.width + 1, bounds.y + bounds.height + 1);
    }

    @Override
    public Dimension getMaximumSize(JComponent c) {
        Rectangle bounds = shape.getBounds();
        return new Dimension(bounds.x + bounds.width + 1, bounds.y + bounds.height + 1);
    }

}

Most of these you probably don't need to worry about, but you should at least know they exist as you may want to customise some of the other properties/functionality later.

This delegate is intended to be installed on a button as needed, as apposed to installing it as the default UI delegate for buttons. The reason for this is the need for the shape object. This allows each button to have it's own shape if you want.

You could seed a single shape into the UIManager's properties as well and use that instead, but I've not bothered for this example.

I then created my own shape/path...

public class PointerPath extends Path2D.Double {

    public PointerPath() {
        moveTo(1, 1);
        lineTo(150, 1);
        lineTo(198, 100);
        lineTo(150, 198);
        lineTo(1, 198);
        lineTo(50, 100);
        closePath();
    }

}

And the applied it to a button...

ShapeButtonUI shapeUI = new ShapeButtonUI(new PointerPath());
JButton btn = new JButton("That way");
btn.setUI(shapeUI);

Which finally generated something like...

Go that way

Now, this is a really basic example, as realistically, the button should be sizing itself around the text (with some additional padding/margin information), but this would require a multi part shape, so we knew which sections could be resized and in what directions, so, complicated.

MadProgrammer
  • 343,457
  • 22
  • 230
  • 366
  • Thank you so much for everything. Yes, i've been taking a look at Java's source files but i always end up getting lost following the breadcrumbs: trying to understand how the JComponents work, where they come from (parent classes) and where each method leads. When i think i reached the end of the path and understand what does what, i've already forgot why i was there. I still need to learn so much. – potato Nov 25 '17 at 17:48