将Swing组件渲染到屏幕外缓冲区

时间:2010-05-25 20:48:15

标签: java swing

我有一个在32位Windows 2008 Server上运行的Java(Swing)应用程序,它需要将其输出呈现为屏幕外图像(然后由另一个C ++应用程序拾取以便在其他地方呈现)。大多数组件都能正确呈现,除非在奇怪的情况下,刚刚失去焦点的组件被另一个组件遮挡,例如,如果有两个JComboBox彼此接近,如果用户与较低的组件交互,则点击上面一个,所以它的下拉与另一个盒重叠。

在这种情况下,失去焦点的组件会在遮挡之后渲染,因此会出现在输出的顶部。它在正常的Java显示中正确呈现(在主显示器上全屏运行),并且尝试更改有问题的组件的层无济于事。

我使用自定义RepaintManager将组件绘制到屏幕外图像,我认为问题在于为每个相关组件调用addDirtyRegion()的顺序,但我想不出一个识别何时发生这种特定状态以防止它的好方法。黑客攻击以便刚刚失去焦点的对象不会被重新绘制可以解决问题,但显然会导致更大的问题,即在其他所有正常情况下都不会重新绘制它。

是否有任何方法可以通过编程方式识别此状态,或重新排序以使其不发生?

非常感谢,

尼克

[编辑] 添加了一些代码作为示例:

重新绘制经理及相关课程:

class NativeObject {
    private long nativeAddress = -1;

    protected void setNativeAddress(long address) {
        if ( nativeAddress != -1 ) {
            throw new IllegalStateException("native address already set for " + this);
        }
        this.nativeAddress = address;
        NativeObjectManager.getInstance().registerNativeObject(this, nativeAddress);
    }   
}

public class MemoryMappedFile extends NativeObject {
    private ByteBuffer buffer;

    public MemoryMappedFile(String name, int size)
    {
        setNativeAddress(create(name, size));
        buffer = getNativeBuffer();
    }

    private native long create(String name, int size);

    private native ByteBuffer getNativeBuffer();

    public native void lock();

    public native void unlock();

    public ByteBuffer getBuffer() {
        return buffer;
    }
}

private static class CustomRepaintManager extends RepaintManager{       
    class PaintLog {
        Rectangle bounds;
        Component component;
        Window window;

        PaintLog(int x, int y, int w, int h, Component c) {
            bounds = new Rectangle(x, y, w, h);
            this.component = c;
        }

        PaintLog(int x, int y, int w, int h, Window win) {
            bounds = new Rectangle(x, y, w, h);
            this.window= win;
        }
    }

    private MemoryMappedFile memoryMappedFile;
    private BufferedImage offscreenImage;
    private List<PaintLog> regions = new LinkedList<PaintLog>();
    private final Component contentPane;
    private Component lastFocusOwner;
    private Runnable sharedMemoryUpdater;
    private final IMetadataSource metadataSource;
    private Graphics2D offscreenGraphics;
    private Rectangle offscreenBounds = new Rectangle();
    private Rectangle repaintBounds = new Rectangle();

    public CustomRepaintManager(Component contentPane, IMetadataSource metadataSource) {
        this.contentPane = contentPane;
        this.metadataSource = metadataSource;
        offscreenBounds = new Rectangle(0, 0, 1920, 1080);  
        memoryMappedFile = new MemoryMappedFile("SystemConfigImage", offscreenBounds.width * offscreenBounds.height * 3 + 1024);
        offscreenImage = new BufferedImage(offscreenBounds.width, offscreenBounds.height, BufferedImage.TYPE_3BYTE_BGR);
        offscreenGraphics = offscreenImage.createGraphics();

        sharedMemoryUpdater = new Runnable(){
            @Override
            public void run()
            {
                updateSharedMemory();
            }
        };
    }

    private boolean getLocationRelativeToContentPane(Component c, Point screen) {
        if(!c.isVisible()) {
            return false;
        }

        if(c == contentPane) {
            return true;
        }

        Container parent = c.getParent();
        if(parent == null) {
            System.out.println("can't get parent!");
            return true;
        }

        if(!parent.isVisible()) {
            return false;
        }

        while ( !parent.equals(contentPane)) {
            screen.x += parent.getX();
            screen.y += parent.getY();
            parent = parent.getParent();

            if(parent == null) {
                System.out.println("can't get parent!");
                return true;
            }
            if(!parent.isVisible()) {
                return false;
            }
        }
        return true;
    }

    protected void updateSharedMemory() {
        if ( regions.isEmpty() ) return;

        List<PaintLog> regionsCopy = new LinkedList<PaintLog>();

        synchronized ( regions ) {
            regionsCopy.addAll(regions);
            regions.clear();
        }

        memoryMappedFile.lock();
        ByteBuffer mappedBuffer = memoryMappedFile.getBuffer();
        int imageDataSize = offscreenImage.getWidth() * offscreenImage.getHeight() * 3;
        mappedBuffer.position(imageDataSize);

        if ( mappedBuffer.getInt() == 0 ) {
            repaintBounds.setBounds(0, 0, 0, 0);
        } else {
            repaintBounds.x = mappedBuffer.getInt();
            repaintBounds.y = mappedBuffer.getInt();
            repaintBounds.width = mappedBuffer.getInt();
            repaintBounds.height = mappedBuffer.getInt();
        }

        for ( PaintLog region : regionsCopy ) {
            if ( region.component != null  && region.bounds.width > 0 && region.bounds.height > 0) {
                Point regionLocation = new Point(region.bounds.x, region.bounds.y);
                Point screenLocation = region.component.getLocation();
                boolean isVisible = getLocationRelativeToContentPane(region.component, screenLocation);

                if(!isVisible) {
                    continue;
                }

                if(region.bounds.x != 0 && screenLocation.x == 0 || region.bounds.y != 0 && screenLocation.y == 0){
                    region.bounds.width += region.bounds.x;
                    region.bounds.height += region.bounds.y;
                }

                Rectangle2D.intersect(region.bounds, offscreenBounds, region.bounds);

                if ( repaintBounds.isEmpty() ){
                    repaintBounds.setBounds( screenLocation.x, screenLocation.y, region.bounds.width, region.bounds.height);
                } else {
                    Rectangle2D.union(repaintBounds, new Rectangle(screenLocation.x, screenLocation.y, region.bounds.width, region.bounds.height), repaintBounds);
                }

                offscreenGraphics.translate(screenLocation.x, screenLocation.y);

                region.component.paint(offscreenGraphics);

                DataBufferByte byteBuffer = (DataBufferByte) offscreenImage.getData().getDataBuffer();
                int srcIndex = (screenLocation.x + screenLocation.y * offscreenImage.getWidth()) * 3;
                byte[] srcData = byteBuffer.getData();

                int maxY = Math.min(screenLocation.y + region.bounds.height, offscreenImage.getHeight());
                int regionLineSize = region.bounds.width * 3;

                for (int y = screenLocation.y; y < maxY; ++y){
                    mappedBuffer.position(srcIndex);

                    if ( srcIndex + regionLineSize > srcData.length ) {
                        break;
                    }
                    if ( srcIndex + regionLineSize > mappedBuffer.capacity() ) {
                        break;
                    }
                    try {
                        mappedBuffer.put( srcData, srcIndex, regionLineSize);
                    }
                    catch ( IndexOutOfBoundsException e) {
                        break;
                    }
                    srcIndex += 3 * offscreenImage.getWidth();
                }

                offscreenGraphics.translate(-screenLocation.x, -screenLocation.y);
                offscreenGraphics.setClip(null);

            } else if ( region.window != null ){    
                repaintBounds.setBounds(0, 0, offscreenImage.getWidth(), offscreenImage.getHeight() );

                offscreenGraphics.setClip(repaintBounds);

                contentPane.paint(offscreenGraphics);

                DataBufferByte byteBuffer = (DataBufferByte) offscreenImage.getData().getDataBuffer();
                mappedBuffer.position(0);
                mappedBuffer.put(byteBuffer.getData());
            }
        }

        mappedBuffer.position(imageDataSize);
        mappedBuffer.putInt(repaintBounds.isEmpty() ? 0 : 1);
        mappedBuffer.putInt(repaintBounds.x);
        mappedBuffer.putInt(repaintBounds.y);
        mappedBuffer.putInt(repaintBounds.width);
        mappedBuffer.putInt(repaintBounds.height);
        metadataSource.writeMetadata(mappedBuffer);

        memoryMappedFile.unlock();
    }

    @Override
    public void addDirtyRegion(JComponent c, int x, int y, int w, int h) {
        super.addDirtyRegion(c, x, y, w, h);
        synchronized ( regions ) {
            regions.add(new PaintLog(x, y, w, h, c));
        }
        SwingUtilities.invokeLater(sharedMemoryUpdater);
    }

    @Override
    public void addDirtyRegion(Window window, int x, int y, int w, int h) {
        super.addDirtyRegion(window, x, y, w, h);
        synchronized (regions) {    
            regions.add(new PaintLog(x, y, w, h, window));
        }
        SwingUtilities.invokeLater(sharedMemoryUpdater);
    }
}

遇到问题的小组:

private static class EncodingParametersPanel extends JPanel implements ActionListener
{
    private JLabel label1 = new JLabel();
    private JComboBox comboBox1 = new JComboBox();

    private JLabel label2 = new JLabel();
    private JComboBox comboBox2 = new JComboBox();

    private JLabel label3 = new JLabel();
    private JComboBox comboBox3 = new JComboBox();

    private JButton setButton = new JButton();

    public EncodingParametersPanel()
    {
        super(new BorderLayout());

        JPanel contentPanel = new JPanel(new VerticalFlowLayout());
        JPanel formatPanel = new JPanel(new VerticalFlowLayout());

        sdiFormatPanel.setBorder(BorderFactory.createTitledBorder(BorderFactory.createLoweredBevelBorder(), "Format"));

        label1.setText("First Option:");
        label2.setText("Second Option:");
        label3.setText("Third OPtion:");

        setButton.addActionListener(this);

        formatPanel.add(label1);
        formatPanel.add(comboBox1);
        formatPanel.add(label2);
        formatPanel.add(comboBox2);
        formatPanel.add(label3);
        formatPanel.add(comboBox3);

        contentPanel.add(formatPanel);

        contentPanel.add(setButton);

        add(contentPanel);
    }
}

使用此示例,如果用户与comboBox2交互,则使用comboBox1,comboBox1的下拉重叠comboBox2,但comboBox2将重新绘制在其上。

2 个答案:

答案 0 :(得分:0)

我发现了一些可能有助于你所看到的事情。

在用于处理窗口重绘的updateSharedMemory代码中,代码调用contentPane.paint。这是最可能的罪魁祸首,因为Window可能不是您的contentPane。 JPopupMenu(由JComboBox使用)的代码可以选择将弹出窗口呈现为重量级组件。因此,Window可以是其中一个JComboBox的弹出窗口。

此外,sharedMemoryUpdater在EDT上进行调度,一旦事件队列为空,它将运行。因此,在调用addDirtyRegion和调用updateSharedMemory之间可能存在延迟。在updateSharedMemory中,有region.component.paint的电话。如果任何已排队的事件发生更改component,则绘制调用的实际结果可能会有所不同。

测试后的一些建议:

像这样创建sharedMemoryUpdater

    private Runnable scheduled = null;

    sharedMemoryUpdater = Runnable {
        public void run() {
            scheduled = null;
            updateSharedMemory();
        }
    }

然后,在addDirtyRegion

    if (scheduled == null) {
        scheduled = sharedMemoryUpdater;
        SwingUtilities.invokeLater(sharedMemoryUpdater);
    }

这将减少sharedMemoryUpdater的调用次数(在我的测试中减少99%)。由于所有对addDirtyRegion的调用都应在EDT上进行,因此您不需要在scheduled上进行同步,但添加不会造成太大影响。

由于存在滞后,因此要处理的区域数量会变得非常大。在我的测试中,我发现它一度超过400。

这些更改将减少操作区域列表所花费的时间,因为创建1个新列表比创建复制现有列表所需的所有条目更快。

private final Object regionLock = new Opject;
private List<PaintLog> regions = new LinkedList<PaintLog>();

// In addDirtyRegions()
synchronized(regionLock) {
    regions.add(...);
}

// In updateSharedMemory()
List<PaintLog> regionsCopy;
List<PaintLog> tmp = new LinkedList<PaintLog>()
synchronized(regionLock) {
    regionsCopy = regions;
    regions = tmp;
}

答案 1 :(得分:0)

我做了一个假设:您的应用程序在真实的图形环境中运行(即不是无头)。

我认为您可能希望利用旨在模仿使用AWT / Swing应用程序的用户的java.awt.Robot。它可以执行诸如模拟按键,鼠标点击之类的操作,并且可以截取屏幕截图!方法为createScreenCapture(Rectangle),它返回BufferedImage,对于大多数用例来说应该是完美的。

这是一个例子,其中我包含了许多相互重叠的垂直JComboBox es。打开其中一个并按下F1将截取屏幕截图并在下面的预览面板中显示。

import java.awt.AWTException;
import java.awt.BorderLayout;
import java.awt.GridLayout;
import java.awt.Rectangle;
import java.awt.Robot;
import java.awt.event.ActionEvent;
import java.awt.event.KeyEvent;
import java.awt.image.BufferedImage;

import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.ImageIcon;
import javax.swing.JComboBox;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.KeyStroke;
import javax.swing.SwingUtilities;
import javax.swing.border.TitledBorder;

public class ScreenshotTester {
    public static void main(String[] args) {
        SwingUtilities.invokeLater(new Runnable() {
            @Override
            public void run() {
                final JFrame f = new JFrame("Screenshot Tester");
                f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

                f.setLayout(new BorderLayout(10, 10));

                final JPanel preview = new JPanel();
                preview.setBorder(new TitledBorder("Screenshot"));
                f.add(preview, BorderLayout.CENTER);

                final JPanel testPanel = new JPanel(new GridLayout(3, 1));
                testPanel.add(new JComboBox(new String[] { "a", "b" }));
                testPanel.add(new JComboBox(new String[] { "c", "d" }));
                testPanel.add(new JComboBox(new String[] { "e", "f" }));
                f.add(testPanel, BorderLayout.NORTH);

                Action screenshotAction = new AbstractAction("Screenshot") {
                    @Override
                    public void actionPerformed(ActionEvent ev) {
                        try {
                            Rectangle region = f.getBounds();
                            BufferedImage img = new Robot().createScreenCapture(region);
                            preview.removeAll();
                            preview.add(new JLabel(new ImageIcon(img)));
                            f.pack();
                        } catch (AWTException e) {
                            JOptionPane.showMessageDialog(f, e);
                        }
                    }
                };

                f.getRootPane().getActionMap().put(screenshotAction, screenshotAction);
                f.getRootPane().getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(
                        KeyStroke.getKeyStroke(KeyEvent.VK_F1, 0), screenshotAction);
                f.pack();
                f.setLocationRelativeTo(null);
                f.setVisible(true);
            }
        });
    }

}

你应该可以看到整个窗口,包括窗口装饰,组合框菜单应该出现在其他组合框的顶部,就像你在屏幕上看到的一样。