Search code examples
javaswinggraphicsjpanelbufferstrategy

JPanel position whacked by BufferStrategy


I have a JFrame to which I add a JPanel. I'm doing some animation, so I implement a BufferStrategy for rendering. I also a rendering loop to keep it rendering while running.

If I run the program like normal, the JPanel renders correctly. Of course, then there's no animation. If I run it with the loop and hte BufferedStrategy, the JPanel extends to the full size of the application, and underneath the title bar of the JFrame. I can't find a good reason for this to be happening, but it's frustrating because I need to do some precise drawing, and can't have some of it hidden underneath the title bar.

I assume it's because I'm not calling super.paintComponent(), but I really shouldn't call it anyway, since I'm rendering on my own, not within the normal Swing pipeline.

Is there some API call I need to make to make the JPanel position itself correctly within my render call?

import java.awt.Graphics;
import java.awt.image.BufferStrategy;
import javax.swing.JFrame;

public class MainFrame extends JFrame implements Runnable {

    private static final long serialVersionUID = 2190062312369662956L;

    protected ViewPanel _viewPanel = null;

    public MainFrame() {
        setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );

        createGui();
    }

    protected void createGui() {

        setSize( 600, 400 );
        setTitle( "Exact Positioning" );
        setVisible( true );
        setResizable( false );

        _viewPanel = new ViewPanel();
        _viewPanel.init();

        // the layout type shouldn't matter since this is the only component in the frame
        add( _viewPanel );
    }

    @Override
    public void run() {

        // setup
        this.createBufferStrategy( 2 );
        BufferStrategy buffStrategy = this.getBufferStrategy();

        // render loop
        while( true ) {

            Graphics g = null;
            try {
                g = buffStrategy.getDrawGraphics();
                _viewPanel.render( g );
            } finally {
                g.dispose();
            }
            buffStrategy.show();

            // pause a tad
            try {
                Thread.sleep( 500 );
            } catch (InterruptedException e) {
                // Required catch block
                e.printStackTrace();
            } catch (Exception e) {
                System.out.println( "Sorry, don't know what happened: " + e.toString() );
                e.printStackTrace();
            }
        }
    }


    public static void main(String[] args) {

        Thread t1 = new Thread(new MainFrame()); 
        t1.start();
        // if I start the app this way, the JPanel draws correctly
//      MainFrame a = new MainFrame(); 
    }
}

The JPanel:

import java.awt.Color;
import java.awt.Graphics;
import java.util.Random;

import javax.swing.JPanel;

public class ViewPanel extends JPanel {

    private static int APP_WIDTH = 600;
    private static int APP_HEIGHT = 400;

    private static final long serialVersionUID = -8019663913250286271L;

    public ViewPanel() {
        setBackground(Color.GRAY);
    }

    public void init() {
    }

    @Override
    protected void paintComponent(Graphics g) {

        super.paintComponent( g );

        render( g );
    }

    // Where I do the drawing. It's called from the rendering loop in the JFrame
    public void render( Graphics g ) {

        // refresh the background since we're not relying on paintComponent all the time
        Color bgc = getBackground();
        g.setColor( bgc );
        g.fillRect( 0, 0, APP_WIDTH, APP_HEIGHT );

        // just paint a moving box
        drawBox( g );

        // draw a line to prove correctness. In the loop, you can see part of this line is hidden
        // underneath the title bar
        g.setColor( Color.red );
        g.drawLine(0, 0, APP_WIDTH, APP_HEIGHT);
    }

    protected void drawBox( Graphics g ) {

        // get a random color
        Random ran = new Random();
        int red = ran.nextInt( 255 );
        int grn = ran.nextInt( 255 );
        int ble = ran.nextInt( 255 );
        Color colour = new Color( red, grn, ble );
        g.setColor( colour );

        // get a random position        
        int x = ran.nextInt( APP_WIDTH - 50);
        int y = ran.nextInt( APP_HEIGHT - 50);

        // draw it
        g.fillRect( x, y, 50, 50 );
    }
}

Solution

  • Swing uses it's own rendering engine, which is a passive implementation. You're attempting to circumvent this with your own, active, rendering engine, the two are going to butt heads.

    Because the BufferStrategy belongs to the JFrame, it's created within the confines of it, so 0x0 will be the top left position of the JFrame, not the JPanel.

    Swing's rendering engine will do this translation automatically for you.

    You have two basic choices.

    1. Don't base the rendering off a JPanel and simply have a "render" class which does this independently (and use a Canvas instead of the JFrame as the bases for the BufferStrategy)
    2. Use a Swing Timer as the primary rendering engine

    Swing Timer based example...

    import java.awt.Color;
    import java.awt.Dimension;
    import java.awt.EventQueue;
    import java.awt.Graphics;
    import java.awt.event.ActionEvent;
    import java.awt.event.ActionListener;
    import java.util.Random;
    import javax.swing.JFrame;
    import javax.swing.JPanel;
    import javax.swing.Timer;
    
    public class Test {
    
        public static void main(String[] args) {
            new Test();
        }
    
        public Test() {
            EventQueue.invokeLater(new Runnable() {
                @Override
                public void run() {
                    JFrame frame = new JFrame();
                    frame.add(new ViewPanel());
                    frame.pack();
                    frame.setLocationRelativeTo(null);
                    frame.setVisible(true);
                }
            });
        }
    
        public static class ViewPanel extends JPanel {
    
            private static int APP_WIDTH = 600;
            private static int APP_HEIGHT = 400;
    
            private static final long serialVersionUID = -8019663913250286271L;
    
            public ViewPanel() {
                setBackground(Color.GRAY);
                Timer timer = new Timer(5, new ActionListener() {
                    @Override
                    public void actionPerformed(ActionEvent e) {
                        repaint();
                    }
                });
                timer.start();
            }
    
            public void init() {
            }
    
            @Override
            public Dimension getPreferredSize() {
                return new Dimension(APP_HEIGHT, APP_HEIGHT);
            }
    
            @Override
            protected void paintComponent(Graphics g) {
                super.paintComponent(g);
                render(g);
            }
    
            // Where I do the drawing. It's called from the rendering loop in the JFrame
            public void render(Graphics g) {
    
                // refresh the background since we're not relying on paintComponent all the time
                Color bgc = getBackground();
                g.setColor(bgc);
                g.fillRect(0, 0, APP_WIDTH, APP_HEIGHT);
    
                // just paint a moving box
                drawBox(g);
    
                // draw a line to prove correctness. In the loop, you can see part of this line is hidden
                // underneath the title bar
                g.setColor(Color.red);
                g.drawLine(0, 0, APP_WIDTH, APP_HEIGHT);
            }
    
            protected void drawBox(Graphics g) {
    
                // get a random color
                Random ran = new Random();
                int red = ran.nextInt(255);
                int grn = ran.nextInt(255);
                int ble = ran.nextInt(255);
                Color colour = new Color(red, grn, ble);
                g.setColor(colour);
    
                // get a random position        
                int x = ran.nextInt(APP_WIDTH - 50);
                int y = ran.nextInt(APP_HEIGHT - 50);
    
                // draw it
                g.fillRect(x, y, 50, 50);
            }
        }
    }