Swing 主动渲染效率或如何将主动渲染与 GUI 小部件结合起来

发布于 2024-09-10 18:13:24 字数 6381 浏览 2 评论 0原文

继续上一个问题,我保留寻找将主动渲染与 Java 中的文本字段相结合的最佳方法。我尝试了几个选项,使用 BufferStrategy、VolatileImage 或在标准 AWT 中覆盖 update() 和 Paint(),但最终我使用了 Swing。

我在这里发布当前的情况,以防有人碰巧根据我的代码示例有新的见解,也许其他正在开发类似应用程序的人可能会从我的发现中受益。

目标是完成这三个壮举:

  • 在仅在必要时更新的背景缓冲区顶部渲染动画对象
  • 在渲染结果顶部使用文本字段
  • 毫无问题地调整窗口大小

下面是使用伟大的代码开发的演示应用程序的代码stackoverflower trashgod 的帮助。
两个注意事项:

1)严格刷新动画中上一步无效的区域似乎很容易出现视觉错误,因此我放弃了它。这意味着我现在每帧都会重新绘制整个背景缓冲区。

2) 将 BufferedImage 绘制到屏幕上的效率很大程度上取决于平台。 Mac 实现似乎无法正确支持硬件加速,这使得将背景图像重新绘制到输出窗口成为一项繁琐的任务,当然具体取决于窗口的大小。

上发现以下结果

我在 2.93 GHz 双核 iMac: Mac OS 10.5 :
640 x 480:0.9 毫秒,8 - 9%
1920 x 1100:5 毫秒,35 - 40%

Windows XP:
640 x 480:0.05 毫秒,0%
1920 x 1100:0.05 毫秒,0%

图例:
屏幕大小:绘制一帧的平均时间、应用程序的 CPU 使用率。

据我所知,下面的代码是实现我的目标的最有效的方法。任何新的见解、优化或测试结果都非常受欢迎!

问候, 马蒂斯

import java.awt.Color;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.GraphicsConfiguration;
import java.awt.GraphicsDevice;
import java.awt.GraphicsEnvironment;
import java.awt.GridLayout;
import java.awt.Rectangle;
import java.awt.Transparency;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.image.BufferedImage;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JTextField;
import javax.swing.Timer;

public class SwingTest extends JPanel implements 
 ActionListener, 
 Runnable 
{
 private static final long serialVersionUID = 1L;

 private BufferedImage backgroundBuffer;
    private boolean repaintbackground = true;

    private static final int initWidth = 640;
    private static final int initHeight = 480;
    private static final int radius = 25;
    private final Timer t = new Timer(20, this);
    private final Rectangle rect = new Rectangle(); 

    private long totalTime = 0;
    private int frames = 0;
    private long avgTime = 0;

    public static void main(String[] args) {
        EventQueue.invokeLater(new SwingTest());
    }

    public SwingTest() {
        super(true);
        this.setPreferredSize(new Dimension(initWidth, initHeight));
        this.setLayout(null);
        this.setOpaque(false);
        this.addMouseListener(new MouseHandler());
    }

    @Override
    public void run() {
        JFrame f = new JFrame("SwingTest");
        f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        f.addComponentListener(new ResizeHandler());

/*      This extra Panel with GridLayout is necessary to make sure 
   our content panel is properly resized with the window.*/
        JPanel p = new JPanel(new GridLayout()); 
        p.add(this);
        f.add(p);
        f.pack();
        f.setLocationRelativeTo(null);
        f.setVisible(true);

        createBuffer();     
        t.start();
    }    

    @Override
    public void actionPerformed(ActionEvent e) {
        this.repaint();
    }    

    @Override
    protected void paintComponent(Graphics g) {
     long start = System.nanoTime();
     super.paintComponent(g);

     if (backgroundBuffer == null) createBuffer();
     if (repaintbackground) {

/*   Repainting the background may require complex rendering operations, 
   so we don't want to do this every frame.*/       
      repaintBackground(backgroundBuffer);
            repaintbackground = false;
     }

/*  Repainting the pre-rendered background buffer every frame
       seems unavoidable. Previous attempts to keep track of the 
       invalidated area and repaint only that part of the background buffer 
       image have failed. */
     g.drawImage(backgroundBuffer, 0, 0, null);
     repaintBall(g, backgroundBuffer, this.getWidth(), this.getHeight());
     repaintDrawTime(g, System.nanoTime() - start);
    }

    void repaintBackground(BufferedImage buffer) {    
     Graphics2D g = buffer.createGraphics();
  int width = buffer.getWidth();
  int height = buffer.getHeight();

  g.clearRect(0, 0, width, height);
  for (int i = 0; i < 100; i++) {
   g.setColor(new Color(0, 128, 0, 100));
   g.drawLine(width, height, (int)(Math.random() * (width - 1)), (int)(Math.random() * (height - 1)));
  }
    }

    void repaintBall(Graphics g, BufferedImage backBuffer, int width, int height) {
     double time = 2* Math.PI * (System.currentTimeMillis() % 3300) / 3300.;
        rect.setRect((int)(Math.sin(time) * width/3 + width/2 - radius), (int)(Math.cos(time) * height/3 + height/2) - radius, radius * 2, radius * 2);

        g.setColor(Color.BLUE);
        g.fillOval(rect.x, rect.y, rect.width, rect.height);
    }

    void repaintDrawTime(Graphics g, long frameTime) {
     if (frames == 32) {avgTime = totalTime/32; totalTime = 0; frames = 0;}
     else {totalTime += frameTime; ++frames; }
     g.setColor(Color.white);
     String s = String.valueOf(avgTime / 1000000d + " ms");
        g.drawString(s, 5, 16);
    }

    void createBuffer() {
        int width = this.getWidth();
        int height = this.getHeight();

        GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();
        GraphicsDevice gs = ge.getDefaultScreenDevice();
        GraphicsConfiguration gc = gs.getDefaultConfiguration();
        backgroundBuffer = gc.createCompatibleImage(width, height, Transparency.OPAQUE);        

        repaintbackground = true;
    }    

    private class MouseHandler extends MouseAdapter {

        @Override
        public void mousePressed(MouseEvent e) {
            super.mousePressed(e);
            JTextField field = new JTextField("test");
            Dimension d = field.getPreferredSize();
            field.setBounds(e.getX(), e.getY(), d.width, d.height);
            add(field);
        }
    }

    private class ResizeHandler extends ComponentAdapter {

     @Override
     public void componentResized(ComponentEvent e) {
      super.componentResized(e);
      System.out.println("Resized to " + getWidth() + " x " + getHeight());
      createBuffer();
     }    
    }
}

Continuing from a previous question, I keep searching for the optimal way to combine active rendering with textfields in Java. I tried several options, using BufferStrategy, VolatileImage or overriding update() and paint() in standard AWT, but I ended up using Swing.

I'm posting the current state of affairs here just in case someone happens to have new insights based on my code example, and perhaps others who are working on a similar app might benefit from my findings.

The target is to accomplish these three feats:

  • render an animating object on top of a background buffer that is updated only when necessary
  • use textfields on top of the rendered result
  • resize the window without problems

Below is the code of the demo application developed with the great help of stackoverflower trashgod.
Two notes:

1) Refreshing strictly the area that is invalidated by the previous step in the animation appears to be so much prone to visual errors that I gave up on it. This means I now redraw the entire background buffer every frame.

2) The efficiency of drawing a BufferedImage to screen is hugely dependent on the platform. The Mac implementation doesn't seem to support hardware acceleration properly, which makes repainting the background image to the output window a tedious task, depending of course on the size of the window.

I found the following results on my 2.93 GHz dualcore iMac:

Mac OS 10.5:
640 x 480: 0.9 ms, 8 - 9%
1920 x 1100: 5 ms, 35 - 40%

Windows XP:
640 x 480: 0.05 ms, 0%
1920 x 1100: 0.05 ms, 0%

Legend:
screen size: average time to draw a frame, CPU usage of the application.

As far as I can see, the code below is the most efficient way of accomplishing my goals. Any new insights, optimizations or test results are very welcome!

Regards,
Mattijs

import java.awt.Color;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.GraphicsConfiguration;
import java.awt.GraphicsDevice;
import java.awt.GraphicsEnvironment;
import java.awt.GridLayout;
import java.awt.Rectangle;
import java.awt.Transparency;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.image.BufferedImage;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JTextField;
import javax.swing.Timer;

public class SwingTest extends JPanel implements 
 ActionListener, 
 Runnable 
{
 private static final long serialVersionUID = 1L;

 private BufferedImage backgroundBuffer;
    private boolean repaintbackground = true;

    private static final int initWidth = 640;
    private static final int initHeight = 480;
    private static final int radius = 25;
    private final Timer t = new Timer(20, this);
    private final Rectangle rect = new Rectangle(); 

    private long totalTime = 0;
    private int frames = 0;
    private long avgTime = 0;

    public static void main(String[] args) {
        EventQueue.invokeLater(new SwingTest());
    }

    public SwingTest() {
        super(true);
        this.setPreferredSize(new Dimension(initWidth, initHeight));
        this.setLayout(null);
        this.setOpaque(false);
        this.addMouseListener(new MouseHandler());
    }

    @Override
    public void run() {
        JFrame f = new JFrame("SwingTest");
        f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        f.addComponentListener(new ResizeHandler());

/*      This extra Panel with GridLayout is necessary to make sure 
   our content panel is properly resized with the window.*/
        JPanel p = new JPanel(new GridLayout()); 
        p.add(this);
        f.add(p);
        f.pack();
        f.setLocationRelativeTo(null);
        f.setVisible(true);

        createBuffer();     
        t.start();
    }    

    @Override
    public void actionPerformed(ActionEvent e) {
        this.repaint();
    }    

    @Override
    protected void paintComponent(Graphics g) {
     long start = System.nanoTime();
     super.paintComponent(g);

     if (backgroundBuffer == null) createBuffer();
     if (repaintbackground) {

/*   Repainting the background may require complex rendering operations, 
   so we don't want to do this every frame.*/       
      repaintBackground(backgroundBuffer);
            repaintbackground = false;
     }

/*  Repainting the pre-rendered background buffer every frame
       seems unavoidable. Previous attempts to keep track of the 
       invalidated area and repaint only that part of the background buffer 
       image have failed. */
     g.drawImage(backgroundBuffer, 0, 0, null);
     repaintBall(g, backgroundBuffer, this.getWidth(), this.getHeight());
     repaintDrawTime(g, System.nanoTime() - start);
    }

    void repaintBackground(BufferedImage buffer) {    
     Graphics2D g = buffer.createGraphics();
  int width = buffer.getWidth();
  int height = buffer.getHeight();

  g.clearRect(0, 0, width, height);
  for (int i = 0; i < 100; i++) {
   g.setColor(new Color(0, 128, 0, 100));
   g.drawLine(width, height, (int)(Math.random() * (width - 1)), (int)(Math.random() * (height - 1)));
  }
    }

    void repaintBall(Graphics g, BufferedImage backBuffer, int width, int height) {
     double time = 2* Math.PI * (System.currentTimeMillis() % 3300) / 3300.;
        rect.setRect((int)(Math.sin(time) * width/3 + width/2 - radius), (int)(Math.cos(time) * height/3 + height/2) - radius, radius * 2, radius * 2);

        g.setColor(Color.BLUE);
        g.fillOval(rect.x, rect.y, rect.width, rect.height);
    }

    void repaintDrawTime(Graphics g, long frameTime) {
     if (frames == 32) {avgTime = totalTime/32; totalTime = 0; frames = 0;}
     else {totalTime += frameTime; ++frames; }
     g.setColor(Color.white);
     String s = String.valueOf(avgTime / 1000000d + " ms");
        g.drawString(s, 5, 16);
    }

    void createBuffer() {
        int width = this.getWidth();
        int height = this.getHeight();

        GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();
        GraphicsDevice gs = ge.getDefaultScreenDevice();
        GraphicsConfiguration gc = gs.getDefaultConfiguration();
        backgroundBuffer = gc.createCompatibleImage(width, height, Transparency.OPAQUE);        

        repaintbackground = true;
    }    

    private class MouseHandler extends MouseAdapter {

        @Override
        public void mousePressed(MouseEvent e) {
            super.mousePressed(e);
            JTextField field = new JTextField("test");
            Dimension d = field.getPreferredSize();
            field.setBounds(e.getX(), e.getY(), d.width, d.height);
            add(field);
        }
    }

    private class ResizeHandler extends ComponentAdapter {

     @Override
     public void componentResized(ComponentEvent e) {
      super.componentResized(e);
      System.out.println("Resized to " + getWidth() + " x " + getHeight());
      createBuffer();
     }    
    }
}

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

弥繁 2024-09-17 18:13:24

我有一些观察结果:

  1. 您改进的 repaintDrawTime() 非常具有可读性,但它是 微基准 并遵守 主机操作系统的变幻莫测。我不禁想知道 XP 结果是否是该系统有限时钟分辨率的产物。我在 Windows 7 和 Ubuntu 10 上看到了非常不同的结果。

  2. 如果您不使用空布局,则不需要额外的面板; JPanel 的默认布局是 FlowLayout,而 f.add(this) 只是将其添加到框架默认 BorderLayout 的中心.

  3. 重复的构造函数调用可能非常耗时。

    考虑更换

    g.setColor(new Color(0, 128, 0, 100));
    

    private static Final Color color = new Color(0, 128, 0, 100);
    ...
    g.setColor(颜色);
    

    或者,一个简单的颜色查找表可能有用,例如

    私有最终队列 clut = new LinkedList<颜色>();
    

I have a few observations:

  1. Your improved repaintDrawTime() is very readable, but it is a micro-benchmark and subject to the vagaries of the host OS. I can't help wondering if the XP results are an artifact of that system's limited clock resolution. I see very different results on Windows 7 and Ubuntu 10.

  2. If you don't use a null layout, you won't need the extra panel; the default layout for JPanel is FlowLayout, and f.add(this) simply adds it to the center of the frame's default BorderLayout.

  3. Repeated constructor invocations can be time consuming.

    Consider replacing

    g.setColor(new Color(0, 128, 0, 100));
    

    with

    private static final Color color = new Color(0, 128, 0, 100);
    ...
    g.setColor(color);
    

    Alternatively, a simple color lookup table, may be useful, e.g.

    private final Queue<Color> clut = new LinkedList<Color>();
    
十秒萌定你 2024-09-17 18:13:24

我不认为将 BufferedImage 传递给此方法有什么意义:

void repaintBall(Graphics g, BufferedImage backBuffer, int width, int height) {
 double time = 2* Math.PI * (System.currentTimeMillis() % 3300) / 3300.;
    rect.setRect((int)(Math.sin(time) * width/3 + width/2 - radius), (int)(Math.cos(time) * height/3 + height/2) - radius, radius * 2, radius * 2);

    g.setColor(Color.BLUE);
    g.fillOval(rect.x, rect.y, rect.width, rect.height);
}

您似乎从未在方法主体中使用过它。

我能够将此类的图形部分移植到我自己的 JPanel 构造函数类中,并且它极大地改进了我的游戏图形,但我从来不需要使用这样的方法,在这种方法中我传入 BufferedImage 作为参数,但从来没有使用它。

I don't see a point for passing in the BufferedImage to this method:

void repaintBall(Graphics g, BufferedImage backBuffer, int width, int height) {
 double time = 2* Math.PI * (System.currentTimeMillis() % 3300) / 3300.;
    rect.setRect((int)(Math.sin(time) * width/3 + width/2 - radius), (int)(Math.cos(time) * height/3 + height/2) - radius, radius * 2, radius * 2);

    g.setColor(Color.BLUE);
    g.fillOval(rect.x, rect.y, rect.width, rect.height);
}

It doesn't seem like you use it ever in the method body.

I was able to transplant the graphics portion of this class into my own JPanel constructor class, and it improved the graphics of my game a lot, but I never needed to use a method like this where I pass in a BufferedImage as an argument but never use it.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文