3

In my application I need to draw grid lines just like those like Photoshop has - e.g, an user can drag lines over the document to help him align layers. Now, the problem is that I am able to draw such lines (it's just plain simple Java2D painting using Line2D), but I am not being able to keep such lines on top of everything else, because when children components draw themselves, my grid line is erased.

The program structure is something like this: JFrame -> JPanel -> JScrollPane -> JPanel -> [many others JPanels, which are like layers]

As a test, I added the draw code to JFrame, which correctly shows my Line2D instance on top of everything else. However, when I do anything in an child component that requires that child to repaint itself, the line that was drawn in the JFrame is erased.

I understand that this is the expected Swing behavior - that is, it will only repaint those areas that have changed. However, I am looking for some approach that continuously draws line grid lines on top of everything else.

The only way I was able to get it working was to use a Swing Timer that calls repaint() on my root component every 10ms, but it consumes a lot of CPU.

UPDATE
Working code of an example is below. Please mind that in my real application I have dozens of different components that could trigger a repaint(), and none of them have a reference to the component that does the grid line drawing (of course I can pass it to everyone, but that appears to be the latest option)

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.geom.Line2D;

import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;

public class GridTest extends JFrame {
    public static void main(String[] args) {
        new GridTest().run();
    }

    private void run() {
        setLayout(null);
        setPreferredSize(new Dimension(200, 200));
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

        final JPanel p = new JPanel();
        p.setBounds(20, 20, 100, 100);
        p.setBackground(Color.white);
        add(p);

        JButton b = new JButton("Refresh");
        b.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                // When I call repaint() here, the paint() method of
                // JFrame it's not called, thus resulting in part of the
                // red line to be erased / overridden.

                // In my real application application, I don't have
                // easy access to the component that draws the lines
                p.repaint();
            }
        });
        b.setBounds(0, 150, 100, 30);
        add(b);

        pack();
        setVisible(true);
    }

    @Override
    public void paint(Graphics g) {
        super.paint(g);

        Graphics2D gg = (Graphics2D)g.create();
        Line2D line = new Line2D.Double(0, 50, getWidth(), 50);
        gg.setStroke(new BasicStroke(3));
        gg.setColor(Color.red);
        gg.draw(line);
        gg.dispose();
    }
}
Rafael Steil
  • 4,538
  • 4
  • 25
  • 30
  • 3
    Why not draw the gridlines at the end of the paintComponent method, not the beginning? Also, for more specific help, consider creating and posting an [SSCCE](http://sscce.org) that shows your problem. – Hovercraft Full Of Eels Nov 24 '11 at 17:22
  • I did that, but the problem is that when a child component repaints itself, the parent component (far top in the hierarchy, can't reach it without unknown calls to getParent()) does not. I will try to get a working code to show here. – Rafael Steil Nov 24 '11 at 17:41
  • @Rafael Steil please see my edit – mKorbel Nov 24 '11 at 19:17
  • 1
    @RafaelSteil, you keep talking about a scrollpane in the comments below, yet your SSCCE doesn't have a scroll pane so how it is an accurate reflection of the problem you are trying to solve? – camickr Nov 24 '11 at 20:02

5 Answers5

5

if you want to paint over JComponents placed to the JScrollPane then you can paint to the JViewPort, example here

EDIT:

1) beacuse your code painted to the wrong Container, to the JFrame, sure is possible to paint to the JFrame, but you have to extract RootPane or GlassPane

2) you have to learn how to LayoutManagers works, I let your code with original Sizing, not nice and very bad

3) paint to the GlassPane or JViewPort

import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.geom.Line2D;
import javax.swing.*;

public class GridTest extends JFrame {
    private static final long serialVersionUID = 1L;

    public static void main(String[] args) {
        new GridTest().run();
    }

    private void run() {
        setLayout(null);
        setPreferredSize(new Dimension(200, 200));
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

        final JPanel p = new JPanel() {
            private static final long serialVersionUID = 1L;

            @Override
            public void paint(Graphics g) {

                super.paint(g);
                Graphics2D gg = (Graphics2D) g.create();
                Line2D line = new Line2D.Double(0, 50, getWidth(), 50);
                gg.setStroke(new BasicStroke(3));
                gg.setColor(Color.red);
                gg.draw(line);
                //gg.dispose();

            }
        };
        p.setBounds(20, 20, 100, 100);
        p.setBackground(Color.white);
        add(p);

        JButton b = new JButton("Refresh");
        b.addActionListener(new ActionListener() {

            @Override
            public void actionPerformed(ActionEvent e) {
                p.repaint();
            }
        });
        b.setBounds(0, 150, 100, 30);
        add(b);

        pack();
        setVisible(true);
    }
}

EDIT: 2, if you expecting single line, on the fixed Bounds

enter image description here enter image description here enter image description here

import java.awt.*;
import java.awt.event.*;
import java.awt.geom.Line2D;
import javax.swing.*;
import javax.swing.border.LineBorder;

public class GridTest extends JFrame {
    private static final long serialVersionUID = 1L;

    public static void main(String[] args) {
        new GridTest().run();
    }

    private void run() {
        setPreferredSize(new Dimension(200, 200));
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

        final JPanel p = new JPanel() {
            private static final long serialVersionUID = 1L;

            @Override
            public void paint(Graphics g) {
                super.paint(g);
                Graphics2D gg = (Graphics2D) g.create();
                Line2D line = new Line2D.Double(0, 50, getWidth(), 50);
                gg.setStroke(new BasicStroke(3));
                gg.setColor(Color.red);
                gg.draw(line);
                gg.dispose();
            }
        };
        JPanel p1 = new JPanel();
        p1.setBorder(new LineBorder(Color.black,1));
        JPanel p2 = new JPanel();
        p2.setBorder(new LineBorder(Color.black,1));
        JPanel p3 = new JPanel();
        p3.setBorder(new LineBorder(Color.black,1));
        p.setLayout(new GridLayout(3,0));
        p.add(p1);
        p.add(p2);
        p.add(p3);
        p.setBounds(20, 20, 100, 100);
        p.setBackground(Color.white);
        add(p, BorderLayout.CENTER);

        JButton b = new JButton("Refresh");
        b.addActionListener(new ActionListener() {

            @Override
            public void actionPerformed(ActionEvent e) {
                p.repaint();
            }
        });
        add(b, BorderLayout.SOUTH);

        pack();
        setVisible(true);
    }
}
Community
  • 1
  • 1
mKorbel
  • 109,525
  • 20
  • 134
  • 319
  • I tried, but (at least in my test), when a child component of the scroll pane repaints itself, the viewPort's stageChanged (of ChangeListener) is not called (only when I do something else, like using the scroll bar). If my children components were fixed in their locations it would work, but the problem is that the user may drag them over the draw area – Rafael Steil Nov 24 '11 at 17:43
  • @Rafael Steil that very theoretical question, please post code in http://sscce.org/ form that demostrated your issues – mKorbel Nov 24 '11 at 17:45
  • Ok, I created a very small and simple example that shows the problem. – Rafael Steil Nov 24 '11 at 18:04
  • I understand how layout managers and everything else works, you can't ask for SSCCE and expect it to adhere to all best practices and patterns that simply doesn't add anything to explain the problem. Anyway, I found I solution here, where I draw to the scroll pane, and pass a reference of it to anyone who triggers repaint events. Not as slick as I wanted it to be, but works. (Please don't get me wrong, I appreciate the help. The discussion here helped me to find an alternative solution). – Rafael Steil Nov 24 '11 at 19:26
  • :-) wait minute, two I put some JComponents to JPanel with Red Line – mKorbel Nov 24 '11 at 19:35
4

One possible solution is to override the JPanel's repaint method so that it calls the contentPane's repaint method instead. Another point is that you probably shouldn't draw grid lines directly in the JFrame but rather in its contentPane. Counter to what I usually recommend, I think you're better off overriding the either the contentPane's paint method (or that of some other containing JPanel), not its paintComponent method so that it can call after the children have been painted. For example:

import java.awt.BasicStroke;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Stroke;
import java.awt.event.ActionEvent;

import javax.swing.*;

@SuppressWarnings("serial")
public class GridTest2 extends JPanel {
   private static final Stroke LINE_STROKE = new BasicStroke(3f);
   private boolean drawInPaintComponent = false;

   public GridTest2() {
      final JPanel panel = new JPanel() {
         @Override
         public void repaint() {
            JRootPane rootPane = SwingUtilities.getRootPane(this);
            if (rootPane != null) {
               JPanel contentPane = (JPanel) rootPane.getContentPane();
               contentPane.repaint();
            }
         }
      };
      panel.setBackground(Color.white);
      panel.setPreferredSize(new Dimension(100, 100));

      JPanel biggerPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
      biggerPanel.setBorder(BorderFactory.createEmptyBorder(20, 20, 0, 0));
      biggerPanel.setOpaque(false);
      biggerPanel.add(panel);

      JButton resetButton = new JButton(new AbstractAction("Reset") {
         public void actionPerformed(ActionEvent arg0) {
            panel.repaint();
         }
      });
      JPanel btnPanel = new JPanel();
      btnPanel.add(resetButton);

      setLayout(new BorderLayout());
      add(biggerPanel, BorderLayout.CENTER);
      add(btnPanel, BorderLayout.SOUTH);
   }

   @Override
   public Dimension getPreferredSize() {
      return new Dimension(300, 300);
   }

   @Override
   protected void paintComponent(Graphics g) {
      super.paintComponent(g);
      if (drawInPaintComponent ) {
         drawRedLine(g);
      }
   }

   @Override
   public void paint(Graphics g) {
      super.paint(g);
      if (!drawInPaintComponent ) {
         drawRedLine(g);
      }
   }

   private void drawRedLine(Graphics g) {
      Graphics2D g2 = (Graphics2D) g;
      g2.setStroke(LINE_STROKE);
      g2.setColor(Color.red);
      g2.drawLine(0, 50, getWidth(), 50);
   }

   private static void createAndShowGui() {
      GridTest2 mainPanel = new GridTest2();

      JFrame frame = new JFrame("GridTest2");
      frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
      frame.getContentPane().add(mainPanel);
      frame.pack();
      frame.setLocationByPlatform(true);
      frame.setVisible(true);
   }

   public static void main(String[] args) {
      SwingUtilities.invokeLater(new Runnable() {
         public void run() {
            createAndShowGui();
         }
      });
   }
}
Hovercraft Full Of Eels
  • 283,665
  • 25
  • 256
  • 373
2

I know it's an old post, but I've had the same problem recently...
You should override paintChildren instead of paint or paintComponent. From the JComponent.paint documentation:

Invoked by Swing to draw components. Applications should not invoke paint directly, but should instead use the repaint method to schedule the component for redrawing.
This method actually delegates the work of painting to three protected methods: paintComponent, paintBorder, and paintChildren. They're called in the order listed to ensure that children appear on top of component itself. Generally speaking, the component and its children should not paint in the insets area allocated to the border. Subclasses can just override this method, as always. A subclass that just wants to specialize the UI (look and feel) delegate's paint method should just override paintComponent.

So if you

@Override
protected void paintChildren(Graphics g){
    super.paintChildren(g);
    paintGrid(g);
}

the grid will be on top of your children components ^^

Ilario
  • 662
  • 11
  • 23
1

Assuming the parent frame already has a list of all the grid lines it has to draw, what you can do is get each child frame to draw its own personal bits of the lines. In pseudocode:

gridlines = getParentsGridLines()
gridlines.offsetBasedOnRelativePosition()
drawStuff()
Toomai
  • 3,974
  • 1
  • 20
  • 22
1

Swing uses a JLayeredPane within JFrames (and similar components). Using the layered pane, you can position paint-only components over your main content.

This code uses components placed within the JLayeredPane to position (and automatically repaint) arbitrary decorations above the main content of any component, thus obviating the need to override the paint() method of any given component.

technomage
  • 9,861
  • 2
  • 26
  • 40