Java Swing:在paintComponent方法中,如何知道要重新绘制什么?

3

我的组件比屏幕大,其中一部分没有显示出来(我会使用滚动条)。
paintComponent(g)中收到呼叫时,如何知道应该绘制哪个区域?


你应该监听滚动事件,然后在你的组件中更新视口大小,并根据此做出决策。 - moonwave99
3
JScrollPane 内部机制应该可以为您一步到位地完成所有这些工作。 - Hovercraft Full Of Eels
对你的渲染进行分析,看看是否需要进行优化。 - trashgod
3个回答

1

我不确定这是否是您的意思,但问题在于每次在JPanelpaintComponent(Graphics g)中接到电话时,您将不得不在JScrollPane上调用repaint(),否则JPanel上的更新将无法在JScrollPane中显示。

此外,我看到您想使用JScrollBar(或者可能您混淆了术语)?我建议使用JScrollPane

我制作了一个小例子,其中包含一个网格的JPanel,每2秒钟更改一次颜色(从红色到黑色,反之亦然)。JPanel/Grid比JScrollPane大;无论如何,我们都必须在JScrollPane实例上调用repaint(),否则网格颜色不会改变:

import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.SwingUtilities;
import javax.swing.Timer;

public class Test {

    public static void main(String[] args) throws Exception {
        SwingUtilities.invokeLater(new Runnable() {

            @Override
            public void run() {
                new Test().createAndShowUI();
            }
        });
    }

    private void createAndShowUI() {
        JFrame frame = new JFrame();
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        initComponents(frame);
        frame.setPreferredSize(new Dimension(400, 400));
        frame.pack();
        frame.setVisible(true);
    }

    private void initComponents(JFrame frame) {
        JScrollPane jsp = new JScrollPane();
        jsp.setViewportView(new Panel(800, 800, jsp));
        frame.getContentPane().add(jsp);
    }
}

class Panel extends JPanel {

    private int across, down;
    private Panel.Tile[][] tiles;
    private Color color = Color.black;
    private final JScrollPane jScrollPane;

    public Panel(int width, int height, JScrollPane jScrollPane) {
        this.setPreferredSize(new Dimension(width, height));
        this.jScrollPane = jScrollPane;
        createTiles();
        changePanelColorTimer();//just something to do to check if its repaints fine
    }

    @Override
    protected void paintComponent(Graphics g) {
        super.paintComponent(g);
        for (int i = 0; i < across; i++) {
            for (int j = 0; j < down; j++) {
                g.setColor(color);
                for (int k = 0; k < 5; k++) {
                    g.drawRect(tiles[i][j].x + k, tiles[i][j].y + k, tiles[i][j].side - k * 2, tiles[i][j].side - 2 * k);
                }
            }
        }
        updateScrollPane();//refresh the pane after every paint
    }

    //calls repaint on the scrollPane instance
    private void updateScrollPane() {
        jScrollPane.repaint();
    }

    private void createTiles() {
        across = 13;
        down = 9;
        tiles = new Panel.Tile[across][down];

        for (int i = 0; i < across; i++) {
            for (int j = 0; j < down; j++) {
                tiles[i][j] = new Panel.Tile((i * 50), (j * 50), 50);
            }
        }
    }

    //change the color of the grid lines from black to red and vice versa every 2s
    private void changePanelColorTimer() {
        Timer timer = new Timer(2000, new ActionListener() {

            @Override
            public void actionPerformed(ActionEvent e) {
                if (color == Color.black) {
                    color = Color.red;
                } else {
                    color = Color.black;
                }
            }
        });
        timer.setInitialDelay(2000);
        timer.start();
    }

    private class Tile {

        int x, y, side;

        public Tile(int inX, int inY, int inSide) {
            x = inX;
            y = inY;
            side = inSide;
        }
    }
}

Panel类中,如果我们在paintComponent(Graphics g)中注释掉updateScrollPane();这一行,那么我们就看不到网格改变颜色了。

1
您可以通过查询Graphics对象的剪辑边界来找到实际需要绘制的区域。
JavaDoc似乎对此方法有点过时:它说它可能返回null剪辑。然而,这显然从未发生过(其他Swing类也依赖于剪辑永远不是null!)。
以下MCVE演示了使用剪辑或绘制整个组件之间的差异:

ClipDemo

它包含一个大小为800x800的带有滚动条的JPanel。面板绘制了一组矩形,并打印出已绘制的矩形数量。

可以使用“使用剪辑边界”复选框启用和禁用剪辑。当使用剪辑时,仅重新绘制可见区域的面板,矩形的数量要少得多。(请注意,这里判断是否必须绘制矩形的测试相当简单:它只执行矩形与可见区域的交集测试。对于实际应用程序,将直接使用剪辑边界找出必须绘制哪些矩形)。

此示例还显示了滚动窗格的一些棘手内部情况:当关闭闪烁并移动滚动条时,可以看到尽管整个可见区域发生了变化,但实际上只需重新绘制很小的区域(即由于滚动而变为可见的区域)。另一部分只是通过拷贝前面的内容来移动它。可以使用JViewport.html#setScrollMode修改此行为。

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.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

import javax.swing.JCheckBox;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.SwingUtilities;
import javax.swing.Timer;

public class PaintRegionTest
{
    public static void main(String[] args) throws Exception
    {
        SwingUtilities.invokeLater(new Runnable()
        {
            @Override
            public void run()
            {
                createAndShowGUI();
            }
        });
    }

    private static void createAndShowGUI()
    {
        JFrame frame = new JFrame();
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

        final PaintRegionPanel paintRegionPanel = new PaintRegionPanel();
        paintRegionPanel.setPreferredSize(new Dimension(800, 800));

        final Timer timer = new Timer(1000, new ActionListener()
        {
            @Override
            public void actionPerformed(ActionEvent e)
            {
                paintRegionPanel.changeColor();
            }
        });
        timer.setInitialDelay(1000);
        timer.start();

        JScrollPane scrollPane = new JScrollPane(paintRegionPanel);
        frame.getContentPane().setLayout(new BorderLayout());
        frame.getContentPane().add(scrollPane, BorderLayout.CENTER);

        JPanel controlPanel = new JPanel(new FlowLayout());

        final JCheckBox blinkCheckbox = new JCheckBox("Blink", true);
        blinkCheckbox.addActionListener(new ActionListener()
        {
            @Override
            public void actionPerformed(ActionEvent e)
            {
                if (blinkCheckbox.isSelected())
                {
                    timer.start();
                }
                else
                {
                    timer.stop();
                }
            }
        });
        controlPanel.add(blinkCheckbox);


        final JCheckBox useClipCheckbox = new JCheckBox("Use clip bounds");
        useClipCheckbox.addActionListener(new ActionListener()
        {
            @Override
            public void actionPerformed(ActionEvent e)
            {
                paintRegionPanel.setUseClipBounds(
                    useClipCheckbox.isSelected());
            }
        });
        controlPanel.add(useClipCheckbox);

        frame.getContentPane().add(controlPanel, BorderLayout.SOUTH);

        frame.setPreferredSize(new Dimension(400, 400));
        frame.pack();
        frame.setLocationRelativeTo(null);
        frame.setVisible(true);
    }
}

class PaintRegionPanel extends JPanel
{
    private Color color = Color.BLACK;
    private boolean useClipBounds = false;

    void setUseClipBounds(boolean useClipBounds)
    {
        this.useClipBounds = useClipBounds; 
    }

    void changeColor()
    {
        if (color == Color.BLACK)
        {
            color = Color.RED;
        }
        else
        {
            color = Color.BLACK;
        }
        repaint();
    }

    @Override
    protected void paintComponent(Graphics gr)
    {
        super.paintComponent(gr);
        Graphics2D g = (Graphics2D)gr;

        g.setColor(color);

        Rectangle clipBounds = g.getClipBounds();
        Rectangle ownBounds = new Rectangle(0,0,getWidth(),getHeight());

        System.out.println("clipBounds: " + clipBounds);
        System.out.println(" ownBounds: " + ownBounds);

        Rectangle paintedRegion = null;
        if (useClipBounds)
        {
            System.out.println("Using clipBounds");
            paintedRegion = clipBounds;
        }
        else
        {
            System.out.println("Using ownBounds");
            paintedRegion = ownBounds;
        }

        int counter = 0;

        // This loop performs a a simple test see whether the objects 
        // have to be painted. In a real application, one would 
        // probably use the clip information to ONLY create the
        // rectangles that actually have to be painted:
        for (int x = 0; x < getWidth(); x += 20)
        {
            for (int y = 0; y < getHeight(); y += 20)
            {
                Rectangle r = new Rectangle(x + 5, y + 5, 10, 10);
                if (r.intersects(paintedRegion))
                {
                    g.fill(r);
                    counter++;
                }
            }
        }
        System.out.println("Painted "+counter+" rectangles ");
    }

}

一个旁注:对于许多应用场景而言,这样的“优化”几乎是不必要的。绘制的元素已经与剪辑区域相交,因此可能不会获得太多性能提升。当“准备”要绘制的元素计算成本很高时,可以考虑这个选项。(在示例中,“准备”指的是创建Rectangle实例,但可能存在更复杂的模式)。但在这些情况下,可能还有更优雅和更简单的解决方案,而不是手动检查剪辑边界。

0

所有的答案都是错误的。所以我决定回答这个问题,尽管这个问题已经两年了。

我认为正确的答案是在paintComponent(Graphics g)方法中调用g.getClipBounds()。它将返回控件坐标系中无效并必须重新绘制的区域的矩形。


你说得对。虽然这个问题比较老,但我也添加了另一个答案。我想知道原始答案是如何占据优势的,因为其中有一些非常严重的怪异之处... - Marco13

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