6

I want to create a dialog with a custom shape and transparency, think info bubble pointing to some component.

To do so, I add a JPanel to a JDialog and overwrite the paintComponent(Graphics) method of the panel. The panel itself contains regular JLabels and JButtons. Works fine, but as soon as I use Graphics2D.setClip(Shape) in the panel draw code, the components are getting overdrawn by the background. If I don't set the clip (to a completely fresh Graphics2D object, no less), everything works fine. This is very puzzling to me and I have no idea what I can do to fix it.

P.S.: I cannot use setShape(Shape) on the JDialog because no anti aliasing is available there. P.P.S.: The actual usecase is to draw a large background image which must be cut off at exactly the info bubble shape.

The following SSCCE demonstrates the issue when you mouse over the 'x' in the top right corner multiple times.

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

import javax.swing.JButton;
import javax.swing.JDialog;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JTextArea;
import java.awt.Polygon;
import java.awt.Shape;


public class Java2DTransparencySSCE extends JDialog {

    JPanel panel;
    private JButton close;
    private JLabel headline;
    private JLabel mainText;

    public Java2DTransparencySSCE() {
        setLayout(new BorderLayout());
        setFocusable(false);
        setFocusableWindowState(false);
        setUndecorated(true);
        setBackground(new Color(0, 0, 0, 0));

        panel = new JPanel(new GridBagLayout()) {

            @Override
            public void paintComponent(final Graphics g) {
                super.paintComponent(g);
                Graphics2D gImg = (Graphics2D) g.create();

                // if the line below is removed, everything works..
                Shape oldClip= gImg.getClip();

                // both the shape and a standard rectangular clip break everything
                // gImg.setClip(50, 50, 50, 50);
                Polygon shape = new Polygon(new int[] {0, 100, 100, 50, 0}, new int[] {200, 200, 100, 50, 100}, 5);
                gImg.setClip(shape);
                gImg.setColor(new Color(255, 0, 0, 50));
                gImg.fill(shape);
                gImg.setClip(oldClip);

                gImg.dispose();
            }

        };
        panel.setOpaque(false);
        add(panel, BorderLayout.CENTER);

        headline = new JLabel("This is a title");
        mainText = new JLabel("<html><div style=\"line-height: 150%;width:100px \">This is some sort of text which is rather long and thus pretty boring to actually read. Thanks for reading anyway!</div></html>");
        close = new JButton("X");
        close.setBorderPainted(false);
        close.setContentAreaFilled(false);
        close.addActionListener(new ActionListener() {

            @Override
            public void actionPerformed(ActionEvent e) {
                System.exit(0);
            }
        });

        layoutPanel();
    }

    private void layoutPanel() {
        GridBagConstraints constraints = new GridBagConstraints();
        constraints.gridx = 0;
        constraints.gridy = 0;
        constraints.insets = new Insets(10, 10, 10, 10);
        constraints.fill = GridBagConstraints.BOTH;
        constraints.weightx = 1;
        panel.add(headline, constraints);

        constraints.gridx += 1;
        constraints.weightx = 0;
        constraints.fill = GridBagConstraints.NONE;
        panel.add(close, constraints);

        constraints.gridx = 0;
        constraints.gridy += 1;
        constraints.gridwidth = 2;
        constraints.weightx = 1;
        constraints.weighty = 1;
        constraints.fill = GridBagConstraints.BOTH;
        panel.add(mainText, constraints);

        pack();
    }

    public static void main(String[] args) {
        JFrame parent = new JFrame();
        JPanel pane = new JPanel(new BorderLayout());
        pane.add(new JTextArea("This is a text."));
        parent.setContentPane(pane);
        parent.setSize(400, 400);
        parent.setLocation(400, 300);
        parent.setDefaultCloseOperation(JDialog.EXIT_ON_CLOSE);
        parent.setVisible(true);

        JDialog dialog = new Java2DTransparencySSCE();
        dialog.setLocation(500, 400);
        dialog.setAlwaysOnTop(true);
        dialog.setVisible(true);
    }

}

EDIT: To circumvent this particular bug on Windows, I added the following which did not cause problems or performance hits in my use case (might vary for you):

JDialog dialog = new Java2DTransparencySSCE() {

        @Override
        public void paint(Graphics g) {
            g.setClip(null);
            super.paint(g);
        }
};
Marco
  • 1,430
  • 10
  • 18

4 Answers4

2

You should store original clip before your custom drawing and restore after.

Shape oldCLip=g2d.getClip();
...
your code
...
g2d.setClip(oldClip);
StanislavL
  • 56,971
  • 9
  • 68
  • 98
  • On a freshly created Graphics object I'm throwing away right after? I also edited my code to include this as it does not fix anything. I forgot to put it back in when writing the SSCE. – Marco Jun 11 '15 at 20:16
  • Can't you just cast to Graphics2D g rather than creating a new one? Also store/set new clip before calling super.paintComponent(g); and call setOpaque(false) to avoid background painting – StanislavL Jun 11 '15 at 21:13
1

On Mac OS X 10.9, Java 8, I see no anomaly. I see an identical appearance with the variation below in which a derived graphics context is neither created nor disposed. The API suggests that "programmers should call dispose when finished using a Graphics object only if it was created directly from a component or another Graphics object." I'm not sure how the implementations differ internally, but that may be the culprit.

image

@Override
public void paintComponent(final Graphics g) {
    super.paintComponent(g);
    Graphics2D gImg = (Graphics2D) g;
    Shape oldClip= gImg.getClip();
    Polygon shape = new Polygon(
        new int[] {  0, 100, 100, 50,   0},
        new int[] {200, 200, 100, 50, 100}, 5);
    gImg.setClip(shape);
    gImg.setColor(new Color(255, 0, 0, 50));
    gImg.fill(shape);
    gImg.setClip(oldClip);
}
trashgod
  • 203,806
  • 29
  • 246
  • 1,045
  • Thank you for testing it. I have now also tested the code at work on various Linux and OS X machines, works as per the screenshot above. On all tested Windows machines (7 and 8) however, the polygon went completely red and covered the contents of its children. I tried not disposing the Graphics, also made no difference. – Marco Jun 12 '15 at 11:03
  • That suggests a platform or driver anomaly. – trashgod Jun 12 '15 at 11:05
0

When mousing over the JButton, the panel is repainting itself only with the bounds needed to repaint - the bounds of the button (if you check oldClip it should be the bounds of the JButton). Changing the clip bounds results in alpha colors being a composite of each previous paint call as the clip is not being cleared by super.paintComponent AND the background of the JDialog is completely transparent.

I want to create a dialog with a custom shape and transparency, think info bubble pointing to some component.

Consider using a lightweight component approach - you can do so by setting the glass pane of the JFrame containing the dialog items, toggling the visibility and/or contents of the glass pane as needed.

copeg
  • 8,290
  • 19
  • 28
  • 1
    This pointed me in the right direction! This is still a bug of some sort between Java and Windows, however I was able to prevent it by overriding the paint(Graphics) method of the JDialog and calling g.setClip(null); before calling super.paint(g). Now it works as intended. In my case, it causes no problems or performance hits when resetting the clip there, so it works for me. – Marco Jun 12 '15 at 11:05
-1

You should call super.paintComponent(g) after all your draw code (it will be the last line of your override).

This way your drawing will be below the component childs.

Marcos Vasconcelos
  • 18,136
  • 30
  • 106
  • 167
  • That would only be correct for JLabels and the like, it's wrong for containers like JPanel. You'd actually paint the gray background over your custom stuff. Needless to say, does not work. – Marco Jun 11 '15 at 20:25