使用 AffineTransform 缩放图形

Scaling graphics with AffineTransform

我正在使用 Swing 制作一个 GUI,它使用 AffineTransform 来缩放绘制在 JInternalFrame 上的 Graphics2D 对象。问题是它在当前状态下有问题,我不知道为什么。

为什么我的代码缩放不正确?为什么将图形 "jump" 调整到面板顶部?

这是我的独立示例:

import javax.swing.*;
import java.awt.event.*;
import java.awt.*;
import java.awt.geom.AffineTransform;
import java.awt.geom.Ellipse2D;
import java.util.*;

public class MainPanel extends JFrame implements ActionListener{

    private static final double version = 1.0;
    private JDesktopPane desktop;
    public static RFInternalFrame frame;

    private java.util.List<Point> POINT_LIST = Arrays.asList(
            //Top Row
            new Point(50, 30),
            new Point(70, 30),
            new Point(90, 30),
            new Point(110, 30),
            new Point(130, 30),
            new Point(150, 30),
            new Point(170, 30),
            new Point(190, 30),
            new Point(210, 30),
            new Point(230, 30),

            //Circle of Radios
            new Point(140, 60),
            new Point(120, 80),
            new Point(100, 100),
            new Point(100, 120),
            new Point(120, 140),
            new Point(140, 160),
            new Point(160, 140),
            new Point(180, 120),
            new Point(180, 100),
            new Point(160, 80));

    public static void main(String[] args) {
        SwingUtilities.invokeLater(() -> createAndShowGui());
    }

    private static void createAndShowGui() {
        JFrame frame = new MainPanel();
        frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
        frame.setLocationByPlatform(false);
        frame.setVisible(true);
    }

    public MainPanel() {
        super("MainPanel " + version);

        //Make the big window be indented 50 pixels from each edge
        //of the screen.
        int inset = 50;
        Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
        setBounds(inset, inset,
                screenSize.width - inset * 7,
                screenSize.height - inset * 4);

        //Set up the GUI.
        desktop = new JDesktopPane(); //a specialized layered pane
        desktop.setBackground(Color.DARK_GRAY);

        createRFFrame(); //create first RFFrame
        createScenarioFrame(); //create ScenarioFrame

        setContentPane(desktop);
        setJMenuBar(createMenuBar());

        //Make dragging a little faster but perhaps uglier.
        desktop.setDragMode(JDesktopPane.OUTLINE_DRAG_MODE);
    }

    protected JMenuBar createMenuBar() {
        JMenuBar menuBar = new JMenuBar();

        //Set up the lone menu.
        JMenu menu = new JMenu("File");
        menu.setMnemonic(KeyEvent.VK_D);
        menuBar.add(menu);

        //Set up the first menu item.
        JMenuItem menuItem = new JMenuItem("Add Panel");
        menuItem.setMnemonic(KeyEvent.VK_N);
        menuItem.setAccelerator(KeyStroke.getKeyStroke(
                KeyEvent.VK_N, ActionEvent.ALT_MASK));
        menuItem.setActionCommand("new");
        menuItem.addActionListener(this);
        menu.add(menuItem);

        //Set up the second menu item.
        menuItem = new JMenuItem("Quit");
        menuItem.setMnemonic(KeyEvent.VK_Q);
        menuItem.setAccelerator(KeyStroke.getKeyStroke(
                KeyEvent.VK_Q, ActionEvent.ALT_MASK));
        menuItem.setActionCommand("quit");
        menuItem.addActionListener(this);
        menu.add(menuItem);

        return menuBar;
    }

    //React to menu selections.
    public void actionPerformed(ActionEvent e) {
        if ("new".equals(e.getActionCommand())) { //new
            createRFFrame();
        } else {
            //quit
            quit();
        }
    }

    /*
     * ActivateAllAction activates all radios on the panel, essentially changes the color
     * of each ellipse from INACTIVE to ACTIVE
     */
    private class ActivateAllAction extends AbstractAction {
        public ActivateAllAction(String name) {
            super(name);
            int mnemonic = (int) name.charAt(1);
            putValue(MNEMONIC_KEY, mnemonic);
        }

        /*
         * This will find the selected tab and extract the DrawEllipses instance from it
         * Then for the actionPerformed it will call activateAll() from DrawEllipses
         */
        @Override
        public void actionPerformed(ActionEvent e) {
            Component comp = desktop.getSelectedFrame();
            if (comp instanceof DrawEllipses){
                DrawEllipses desktop = (DrawEllipses) comp;
                desktop.activateAll();
            }
        }
    }

    /*
     * DeactivateAllAction deactivates all radios on the panel, essentially changes the color
     * of each ellipse from ACTIVE to INACTIVE
     */
    private class DeactivateAllAction extends AbstractAction {
        public DeactivateAllAction(String name) {
            super(name);
            int mnemonic = (int) name.charAt(0);
            putValue(MNEMONIC_KEY, mnemonic);
        }

        /*
         * This will find the selected tab and extract the DrawPanel2 instance from it
         * Then for the actionPerformed it will call activateAll() from DrawEllipses
         */
        @Override
        public void actionPerformed(ActionEvent e) {
            Component comp = desktop.getSelectedFrame();
            if (comp instanceof DrawEllipses){
                DrawEllipses desktop = (DrawEllipses) comp;
                desktop.deactivateAll();
            }
        }
    }

    /*
     * Define a JPanel that will hold the activate and deactivate all JButtons
     */
    protected JPanel btnPanel() {
        JPanel btnPanel = new JPanel();

        btnPanel.setBorder(BorderFactory.createLoweredSoftBevelBorder());

        //Set the layout of the frame to a grid bag layout
        btnPanel.setLayout(new GridBagLayout());

        //Creates constraints variable to hold values to be applied to each aspect of the layout
        GridBagConstraints c = new GridBagConstraints();

        //Column 1
        c.gridx = 0;
        btnPanel.add(new JButton(new ActivateAllAction("Activate All")));

        //Column 2
        c.gridx = 1;
        btnPanel.add(new JButton(new DeactivateAllAction("Deactivate All")));
        return btnPanel;
    }

    //not used currently
    protected JPanel drawPanel() {
        JPanel drawPanel = new JPanel();
        drawPanel.setBorder(BorderFactory.createLoweredSoftBevelBorder());
        DrawEllipses drawEllipses = new DrawEllipses(POINT_LIST);
        drawPanel.add(drawEllipses);

        return drawPanel;

    }

    //Create a new internal frame.
    protected void createRFFrame() {
        RFInternalFrame iframe = new RFInternalFrame();
        iframe.setLayout(new BorderLayout());

        DrawEllipses drawEllipses = new DrawEllipses(POINT_LIST);
        iframe.add(drawEllipses);
        iframe.add(btnPanel(), BorderLayout.SOUTH);

        iframe.setVisible(true);
        desktop.add(iframe);

        try {
            iframe.setSelected(true);
        } catch (java.beans.PropertyVetoException e) {}
    }

    protected void createScenarioFrame() {
        ScenarioInternalFrame frame = new ScenarioInternalFrame();
        frame.setLayout(new BorderLayout());

        frame.setVisible(true);
        desktop.add(frame);

        try {
            frame.setSelected(true);
        } catch (java.beans.PropertyVetoException e) {}
    }

    //Quit the application.
    protected void quit() {
        System.exit(0);
    }

}

@SuppressWarnings("serial")
class DrawEllipses extends JPanel {
    private double translateX; //
    private double translateY; //
    protected static double scale; //
    private static final int OVAL_WIDTH = 15;
    private static final Color INACTIVE_COLOR = Color.RED;
    private static final Color ACTIVE_COLOR = Color.green;
    private java.util.List<Point> points; //
    private java.util.List<Ellipse2D> ellipses = new ArrayList<>();
    private Map<Ellipse2D, Color> ellipseColorMap = new HashMap<>();

    public DrawEllipses(java.util.List<Point> points) {
        this.points = points; //
        translateX = 0; //
        translateY = 0; //
        scale = 1; //
        setOpaque(true); //
        setDoubleBuffered(true); //


        for (Point p : points) {
            int x = p.x - OVAL_WIDTH / 2;
            int y = p.y - OVAL_WIDTH / 2;
            int w = OVAL_WIDTH;
            int h = OVAL_WIDTH;
            Ellipse2D ellipse = new Ellipse2D.Double(x, y, w, h);
            ellipses.add(ellipse);
            ellipseColorMap.put(ellipse, INACTIVE_COLOR);
        }

        MyMouseAdapter mListener = new MyMouseAdapter();
        addMouseListener(mListener);
        addMouseMotionListener(mListener);
    }

    @Override
    protected void paintComponent(Graphics g) {
        super.paintComponent(g);
        AffineTransform tx = new AffineTransform(); //
        tx.translate(translateX, translateY); //
        tx.scale(scale, scale); //

        Graphics2D g2 = (Graphics2D) g;
        g2.setTransform(tx);
        g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
                RenderingHints.VALUE_ANTIALIAS_ON);
        for (Ellipse2D ellipse : ellipses) {
            g2.setColor(ellipseColorMap.get(ellipse));
            g2.fill(ellipse);
        }
    }

    private class MyMouseAdapter extends MouseAdapter {
        @Override
        public void mousePressed(MouseEvent e) {
            for (Ellipse2D ellipse : ellipses) {
                if (ellipse.contains(e.getPoint())) {
                    Color c = ellipseColorMap.get(ellipse);
                    c =  (c == INACTIVE_COLOR) ? ACTIVE_COLOR : INACTIVE_COLOR;
                    ellipseColorMap.put(ellipse, c);
                }
            }
            repaint();
        }
    }

    //Used for button click action to change all ellipses to ACTIVE_COLOR
    public void activateAll(){
        for (Ellipse2D ellipse : ellipses){
            ellipseColorMap.put(ellipse, ACTIVE_COLOR);
        }
        repaint();
    }

    //Used for button click action to change all ellipses to INACTIVE_COLOR
    public void deactivateAll(){
        for (Ellipse2D ellipse : ellipses){
            ellipseColorMap.put(ellipse, INACTIVE_COLOR);
        }
        repaint();
    }
}

class RFInternalFrame extends JInternalFrame implements ComponentListener {
    protected static double scale = 1; //
    static int openFrameCount = 0;
    static final int xOffset = 300, yOffset = 0;

    public RFInternalFrame() {
        super("RF Panel #" + (++openFrameCount),
                true, //resizable
                true, //closable
                true, //maximizable
                true);//iconifiable

        setSize(300, 300);
        setMinimumSize(new Dimension(300, 300));
        addComponentListener(this);

        if (openFrameCount == 1) {

            setLocation(0,0);
        }
        else if (openFrameCount <= 4) {

            //Set the window's location.
            setLocation(xOffset * (openFrameCount - 1), yOffset * (openFrameCount - 1));
        }
        else if (openFrameCount == 5) {

            setLocation(xOffset - 300, yOffset + 300);
        }
        else if (openFrameCount == 6) {

            setLocation(xOffset + 600, yOffset + 300);
        }
    }

    @Override
    public void componentResized(ComponentEvent e) {
        String str = "";
        if (getWidth() < 300) {
            str = "0." + getWidth();
        } else {
            str = "1." + (getWidth() - 300);
            System.out.println(getWidth() - 300);
        }
        double dou = Double.parseDouble(str);
        MainPanel.frame.scale = dou;
        repaint();
    }

    @Override
    public void componentMoved(ComponentEvent componentEvent) {

    }

    @Override
    public void componentShown(ComponentEvent componentEvent) {

    }

    @Override
    public void componentHidden(ComponentEvent componentEvent) {

    }
}

class ScenarioInternalFrame extends JInternalFrame {
    static int openFrameCount = 0;
    static final int xOffset = 300, yOffset = 300;

    public ScenarioInternalFrame() {
        super("Test Scenario" + (++openFrameCount),
                true, //resizable
                true, //closable
                true, //maximizable
                true);//iconifiable

        //...Create the GUI and put it in the window...

        //...Then set the window size or call pack...
        setSize(600, 300);

        //Set the window's location.
        setLocation(xOffset, yOffset);
    }
}

据我了解,Graphics object 已经包含一个转换,该转换执行转换以考虑内部框架标题栏的高度。当您替换转换时,您将丢失此转换,因此您的代码将绘制在标题栏下方框架的顶部。

  1. 不要更改传递给 paintComponent() 方法的图形 object 的属性。而是创建一个您可以自定义的 Graphics2D object。
  2. 创建新转换时,您需要先应用现有转换,然后再添加新转换。

基本结构类似于:

super.paintComponent(g);

Graphics2D g2 = (Graphics2D)g.create();

AffineTransform tx = new AffineTransform(); //
tx.concatenate( g2.getTransform() );
tx.scale(...); 
g2.setTransform(tx);

// do custom painting

g2.dispose(); // release Graphics resources

这对绘画有帮助。你还有几个问题(我无法解决):

  1. 您的比例值永远不会更新。您应该将 ComponentListener 添加到 DrawEllipse 面板。您可能想在面板中创建一个 setScale() 方法,您调用该方法以在调整面板大小时设置比例。

  2. 一旦绘制了缩放的圆圈,MouseListener 将无法工作。所有圆圈的位置都会不同,因为它们已被缩放。您可以在遍历圆列表时缩放每个圆。

此外,当您有问题时 post 一个适当的 SSCCE 来演示问题。您有一个关于在面板上使用变换的简单问题。因此,创建一个带有面板的框架并在面板上绘制几个圆圈来测试这个概念。

其他代码与问题无关。菜单项无关紧要,第二个内部框架无关紧要。 MouseListener 单击代码无关紧要。我们没有时间阅读 100 行代码来理解这个问题。

编辑:

我更改了代码的顺序。在将转换设置为图形 object.

之前,必须调用 tx.scale(...) 方法

根据我的经验,Swing 上的绘画将使用双缓冲区完成。意味着您创建绘图缓冲区(即 ImageBuffer)。您将所有绘图逻辑应用于绘图缓冲区的图形,包括转换,然后最后将您的缓冲区绘制到组件的图形中。

我就是这样解决你的问题的...

class DrawEllipses extends JComponent { // I change from JPanel to JComponent, this might not be necessary though...
...
...
protected void paintComponent(Graphics g) {
    super.paintComponent(g);

    // create the drawing buffer.
    BufferedImage bi = new BufferedImage(this.getWidth(), this.getHeight(), BufferedImage.TYPE_INT_RGB);
    Graphics big = bi.getGraphics();

    // prepare transform
    AffineTransform tx = new AffineTransform(); //
    tx.translate(translateX, translateY); //
    tx.scale(scale, scale); //

    // get the buffer graphics and paint the background white.
    Graphics2D g2 = (Graphics2D) big;
    g2.setColor(Color.WHITE);
    g2.fillRect(0, 0, this.getWidth(), this.getHeight());

    // apply drawing logic to the Graphics of the buffer
    g2.setTransform(tx);
    g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
            RenderingHints.VALUE_ANTIALIAS_ON);
    for (Ellipse2D ellipse : ellipses) {
        g2.setColor(ellipseColorMap.get(ellipse));
        g2.fill(ellipse);
    }

    // finally, draw the buffer to the component graphics.
    g.drawImage(bi, 0, 0, null);
}

试一试...希望它有效并有所帮助。