覆盖所有其他组件(Swing,Java)

3
在我的应用程序中,我需要绘制网格线,就像Photoshop一样 - 例如,用户可以在文档上拖动线条来帮助对齐图层。现在,问题是我能够绘制这样的线条(只是使用Line2D进行简单的Java2D绘画),但我不能将这样的线条保持在其他所有内容的顶部,因为当子组件绘制自己时,我的网格线会被擦除。
程序结构如下:JFrame -> JPanel -> JScrollPane -> JPanel -> [许多其他像图层一样的JPanel]
作为测试,我将绘图代码添加到了JFrame中,它正确地显示了我的Line2D实例位于其他所有内容的顶部。然而,当我在需要子组件重新绘制自身的任何子组件中执行任何操作时,JFrame中绘制的线条会被擦除。
我知道这是预期的Swing行为 - 也就是说,它只会重新绘制那些已更改的区域。但是,我正在寻找一些方法来持续在其他所有内容的顶部绘制网格线。
我唯一能够使其工作的方法是使用一个Swing定时器,每10毫秒调用我的根组件上的repaint(),但它会消耗大量CPU。
更新 下面是一个示例的工作代码。请注意,在我的实际应用程序中,我有数十个不同的组件可能会触发repaint(),而且它们都没有对执行网格线绘制的组件的引用(当然我可以将其传递给每个人,但那似乎是最后的选择)。
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();
    }
}

3
为什么不在paintComponent方法结束时绘制网格线,而是在开头绘制?此外,如果需要更具体的帮助,请考虑创建并发布一个SSCCE(http://sscce.org),展示您的问题。 - Hovercraft Full Of Eels
我已经尝试过了,但问题在于当子组件重新绘制时,父组件(位于层次结构的最顶部,无法通过未知调用getParent()来访问)没有重新绘制。我会尝试获取一个可工作的代码并在此展示。 - Rafael Steil
@Rafael Steil 请看一下我的修改。 - mKorbel
1
@RafaelSteil,你在下面的评论中一直谈论一个滚动窗格,但是你的SSCCE没有滚动窗格,那么它如何准确反映你试图解决的问题呢? - camickr
5个回答

5
如果您想涂漆放置在JScrollPane上的JComponents,那么您可以绘制到JViewPort,例如这里 编辑:
1)因为您的代码将错误的容器绘制到了JFrame,当然可以绘制到JFrame,但是您必须提取RootPane或GlassPane 2)您必须学习如何使用LayoutManagers,我让您的代码保持原始大小,不好看而且很糟糕
3)绘制到GlassPaneJViewPort
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);
    }
}

编辑:2,如果您期望在固定边界上的单行

输入图像描述 输入图像描述 输入图像描述

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

我尝试过,但(至少在我的测试中),当滚动窗格的子组件重新绘制时,视口的stageChanged(ChangeListener的方法)不会被调用(只有当我执行其他操作时,例如使用滚动条)。如果我的子组件在它们的位置上固定不动,那么它将起作用,但问题是用户可能会将它们拖到绘图区域上方。 - Rafael Steil
@Rafael Steil 这是一个非常理论性的问题,请您以 http://sscce.org/ 的形式发布代码,以演示您的问题。 - mKorbel
好的,我创建了一个非常小和简单的示例,展示了这个问题。 - Rafael Steil
我理解布局管理器和其他所有内容是如何工作的,你不能要求SSCCE并期望它遵循所有最佳实践和模式,因为这根本没有解释问题的任何意义。无论如何,我在这里找到了一个解决方案,在其中绘制滚动窗格,并将其引用传递给触发重绘事件的任何人。不像我想要的那么完美,但是它能工作。 (请别误会,我感谢你的帮助。这里的讨论帮助我找到了一个替代解决方案)。 - Rafael Steil
等一下,两分钟我把一些 JComponents 放到带有红线的 JPanel 中。 - mKorbel

4

一种可能的解决方案是重写JPanel的repaint方法,使其调用contentPane的repaint方法。另一个要点是你不应该直接在JFrame中绘制网格线,而是应该在其contentPane中绘制。与我通常推荐的相反,我认为你最好重写contentPane的paint方法(或其他包含JPanel的paint方法),而不是它的paintComponent方法,这样它可以在子元素被绘制后再进行调用。例如:

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

2

我知道这是一篇旧的文章,但我最近遇到了同样的问题...
你应该覆盖paintChildren而不是paintpaintComponent。 根据JComponent.paint文档

Swing调用此方法来绘制组件。应用程序不应直接调用paint,而应使用repaint方法安排组件重新绘制。
此方法实际上将绘画工作委托给三个受保护的方法:paintComponent、paintBorder和paintChildren。为了确保子项出现在组件自身之上,它们按列出的顺序调用。通常情况下,组件及其子项不应在分配给边框的插入区域内绘制。子类可以像往常一样只覆盖此方法。如果子类只想专门化UI(外观和感觉)委托的绘图方法,则只需覆盖paintComponent。

所以,如果您需要重写Swing组件的绘制,应当覆盖paintChildren方法。

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

网格将位于您的子组件之上^^。

1

Swing在JFrames(和类似组件)中使用JLayeredPane。使用分层窗格,您可以将仅绘制的组件定位在主内容上方。

此代码使用放置在JLayeredPane中的组件来定位(并自动重绘)任意装饰物,从而避免了覆盖任何给定组件的paint()方法的需要。


1
假设父级框架已经有了要绘制的所有网格线列表,您可以让每个子框架绘制自己个人的线条。伪代码如下:
gridlines = getParentsGridLines()
gridlines.offsetBasedOnRelativePosition()
drawStuff()

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接