Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Graphics Context misaligned on first paint

I've been working up an answer for another question and came across a bizare issue that I've not seen before...

Basically, the program uses a AffineTransform to provide translation, scaling and rotating of a Graphics element, simple enough stuff, done a thousand times before

The problem is, when the screen first appears, the output is not where it should be, but once I touch one of the controls (adjust one of the slides) it jumps to the right spot.

SliderSlider

Based on the screen shots, the Graphics content seems to be misplaced by the amount of the other controls.

If I remove the controls from the GUI, it appears in the right spot (in the center). If I resize the window, it doesn't fix the issue, it only fixes when one of the sliders triggers repaint on the DrawPane...

I've added diagnostics to the output and the all the values are the same - that is, they print the same values when the program first starts and when I adjust all slider values to their initial values.

If I remove the setRotation and setScale calls from the AffineTransform, it doesn't fix it. If I remove the setTranslation, the square isn't initially painted until the panel is updated (it's painted off screen)

If I use Graphics2D g2d = (Graphics)g; instead of g.create(), same result (and yes, I reset the transform before the paintComponent method exited ;))

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.geom.AffineTransform;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JSlider;
import javax.swing.SwingConstants;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;

public class Parker {

    public static void main(String[] args) {
        new Parker();
    }

    public Parker() {
        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                try {
                    UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
                } catch (Exception ex) {
                }

                JFrame frame = new JFrame("Testing");
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                frame.add(new ControlPane());
                frame.pack();
                frame.setLocationRelativeTo(null);
                frame.setVisible(true);
            }
        });
    }

    public class ControlPane extends JPanel {

        private JSlider slider; //declare slider
        private DrawPane myPanel; 
        public ControlPane() {
            setLayout(new BorderLayout());
            myPanel = new DrawPane();
            myPanel.setBackground(Color.cyan); //change background color

            slider = new JSlider(SwingConstants.VERTICAL, 0, 400, 100);// restrains the slider from scaling square to 0-300 pixels
            slider.setMajorTickSpacing(20); //will set tick marks every 10 pixels
            slider.setPaintTicks(true); //this actually paints the ticks on the screen

            slider.addChangeListener(
                    new ChangeListener() {
                        @Override
                        public void stateChanged(ChangeEvent e) {
                            myPanel.setScale(slider.getValue()); //Wherever you set the slider, it will pass that value and that will paint on the screen
                        }
                    }
            );

            JSlider rotate = new JSlider(SwingConstants.VERTICAL, 0, 720, 0);
            rotate.setMajorTickSpacing(20); //will set tick marks every 10 pixels
            rotate.setPaintTicks(true); //this actually paints the ticks on the screen

            rotate.addChangeListener(
                    new ChangeListener() {
                        @Override
                        public void stateChanged(ChangeEvent e) {
                            JSlider slider = (JSlider) e.getSource();
                            myPanel.setAngle(slider.getValue()); 
                        }
                    }
            );
            add(slider, BorderLayout.WEST);
            add(rotate, BorderLayout.EAST);

            add(myPanel);

            slider.setValue(0);
            slider.setValue(100);
            rotate.setValue(0);

        }

    }

    public class DrawPane extends JPanel {

        private double scale = 1;
        private double angle = 0;

        private final int rectWidth = 20;
        private final int rectHeight = 20;


        @Override
        protected void paintComponent(Graphics g)//paints obj on the screen
        {
            super.paintComponent(g); //prepares graphic object for drawing

            int originX = getWidth() / 2; 
            int originY = getHeight() / 2;

            int xOffset = -(rectWidth / 2);
            int yOffset = -(rectHeight / 2);

            g.setColor(Color.BLACK);
            Graphics2D g2d = (Graphics2D) g.create();
            AffineTransform at = new AffineTransform();
            at.translate(originX, originY);
            g2d.setTransform(at);
            g2d.scale(scale, scale);
            g2d.rotate(Math.toRadians(angle), 0, 0);
            g2d.fillRect(xOffset, yOffset, rectWidth, rectHeight);
            g2d.dispose();

            g.setColor(Color.RED);
            g.drawRect(originX + xOffset, originY + yOffset, rectWidth, rectWidth);
        }

        public void setAngle(double angle) {
            this.angle = angle;
            repaint();
        }

        public void setScale(int scale) {
            // Scaling is normalized so that 1 = 100%
            this.scale = (scale / 100d);
            repaint();
        }

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

    }

}

Basically, it appears that the Graphics context, for some reason is not been translated properly when it is first painted and I have no idea why...

ps- Test on Java 6 and Java 7 under Windows 7
pps- I've also tried setting the initial scale and rotation to other values before the screen was visible, same result

System Properties

awt.toolkit=sun.awt.windows.WToolkit
file.encoding=UTF-8
file.encoding.pkg=sun.io
file.separator=\
java.awt.graphicsenv=sun.awt.Win32GraphicsEnvironment
java.awt.printerjob=sun.awt.windows.WPrinterJob
java.class.path=C:\DevWork\personal\java\projects\wip\SystemProperties\build\classes
java.class.version=51.0
java.endorsed.dirs=C:\Program Files\Java\jdk1.7.0_51\jre\lib\endorsed
java.ext.dirs=C:\Program Files\Java\jdk1.7.0_51\jre\lib\ext;C:\Windows\Sun\Java\lib\ext
java.home=C:\Program Files\Java\jdk1.7.0_51\jre
java.io.tmpdir=C:\Users\shane\AppData\Local\Temp\
java.runtime.name=Java(TM) SE Runtime Environment
java.runtime.version=1.7.0_51-b13
java.specification.name=Java Platform API Specification
java.specification.vendor=Oracle Corporation
java.specification.version=1.7
java.vendor=Oracle Corporation
java.vendor.url=http://java.oracle.com/
java.vendor.url.bug=http://bugreport.sun.com/bugreport/
java.version=1.7.0_51
java.vm.info=mixed mode
java.vm.name=Java HotSpot(TM) 64-Bit Server VM
java.vm.specification.name=Java Virtual Machine Specification
java.vm.specification.vendor=Oracle Corporation
java.vm.specification.version=1.7
java.vm.vendor=Oracle Corporation
java.vm.version=24.51-b03
os.arch=amd64
os.name=Windows 7
os.version=6.1
path.separator=;
sun.arch.data.model=64
sun.cpu.endian=little
sun.cpu.isalist=amd64
sun.desktop=windows
sun.io.unicode.encoding=UnicodeLittle
sun.java.command=systemproperties.SystemProperties
sun.java.launcher=SUN_STANDARD
sun.jnu.encoding=Cp1252
sun.management.compiler=HotSpot 64-Bit Tiered Compilers
sun.os.patch.level=Service Pack 1
user.country=AU
user.dir=C:\DevWork\personal\java\projects\wip\SystemProperties
user.home=C:\Users\shane
user.language=en
user.name=shane
user.script=
user.timezone=
user.variant=

Updated

If I use...

g2d.translate(originX, originY);
g2d.scale(scale, scale);
g2d.rotate(Math.toRadians(angle), 0, 0);

Instead of the AffineTransform, it works fine. I've noted that it doesn't matter how I use the AffineTransform (translation only, rotation only, scale only) I seem to get the same results

Updated with example image

Example showing resizing frame...

ZoomSpinResizeFrame

nb The last position shift of the rectangle is a result of the MouseListener mentioned below

However, if I add a MosueListener to the DrawPane (either direclty within the class or externally via the myPanel instance) and call repaint on mouseClicked, it re-aligns correctly :P

Updated

If I translate a Shape, using the following

AffineTransform at = new AffineTransform();
at.translate(originX, originY);
at.scale(scale, scale);
at.rotate(Math.toRadians(angle), 0, 0);
g2d.setTransform(at);

Rectangle2D rect = new Rectangle2D.Double(xOffset, yOffset, rectWidth, rectHeight);
Shape shape = at.createTransformedShape(rect);

System.out.println(rect.getBounds());
System.out.println(shape.getBounds());

The resulting output is in align with expectations, but the (graphics) output is still wrong...

like image 807
MadProgrammer Avatar asked May 07 '14 01:05

MadProgrammer


1 Answers

The AffineTransform associated with the graphics context passed to paintComponent() is not always the identity transform. For reasons that aren't clear, the m12 entry has the value 38.0 initially and after resizing the enclosing frame. Trivially, one can modify the copy supplied by g.create().

Graphics2D g2d = (Graphics2D) g.create();
AffineTransform at = g2d.getTransform();
at.translate(originX, originY);
g2d.setTransform(at);
g2d.scale(scale, scale);
g2d.rotate(Math.toRadians(angle), 0, 0);
g2d.fillRect(xOffset, yOffset, rectWidth, rectHeight);
g2d.dispose();

Addendum: As @MadProgrammer observes, "If I remove the controls from the GUI, it appears … in the center." Indeed, the observed horizontal offset is precisely the preferred width of the slider in BorderLayout.WEST. I suspect that the origin is adjusted after a resize to meet the Component#paintAll() contract more easily: "The origin of the graphics context, its (0, 0) coordinate point, is the top-left corner of this component."

like image 139
trashgod Avatar answered Oct 17 '22 09:10

trashgod