简体   繁体   English

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

[英]Rendering Swing Components to an Offscreen buffer

I have a Java (Swing) application, running on a 32-bit Windows 2008 Server, which needs to render it's output to an off-screen image (which is then picked up by another C++ application for rendering elsewhere). 我有一个在32位Windows 2008 Server上运行的Java(Swing)应用程序,它需要将其输出呈现为屏幕外图像(然后由另一个C ++应用程序拾取以便在其他地方呈现)。 Most of the components render correctly, except in the odd case where a component which has just lost focus is occluded by another component, for example where there are two JComboBoxes close to each other, if the user interacts with the lower one, then clicks on the upper one so it's pull-down overlaps the other box. 大多数组件都能正确呈现,除非在奇怪的情况下,刚刚失去焦点的组件被另一个组件遮挡,例如,如果有两个JComboBox彼此接近,如果用户与较低的组件交互,则点击上面一个,所以它的下拉与另一个盒子重叠。

In this situation, the component which has lost focus is rendered after the one occluding it, and so appears on top in the output. 在这种情况下,失去焦点的组件在遮挡它之后呈现,因此出现在输出的顶部。 It renders correctly in the normal Java display (running full-screen on the primary display), and attempting to change the layers of the components in question does not help. 它在正常的Java显示中正确呈现(在主显示器上全屏运行),并且尝试更改有问题的组件的层无济于事。

I am using a custom RepaintManager to paint the components to the offscreen image, and I assume the problem lies with the order in which addDirtyRegion() is called for each of the components in question, but I can't think of a good way of identifying when this particular state occurs in order to prevent it. 我使用自定义RepaintManager将组件绘制到屏幕外图像,我假设问题在于为每个有问题的组件调用addDirtyRegion()的顺序,但我想不出一个好方法识别何时发生这种特定状态以防止它。 Hacking it so that the object which has just lost focus is not repainted stops the problem, but obviously causes the bigger problem that it is not repainted in all other, normal, circumstances. 黑客攻击以便刚刚失去焦点的对象不会被重新绘制可以解决问题,但显然会导致更大的问题,即在其他所有正常情况下都不会重新绘制它。

Is there any way of programmatically identifying this state, or of reordering things so that it does not occur? 有没有办法以编程方式识别这种状态,或重新排序事物,以便它不会发生?

Many thanks, 非常感谢,

Nick 缺口

[edit] Added some code as an example: [编辑]添加了一些代码作为示例:

Repaint manager and associated classes: 重绘经理和相关课程:

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);
    }
}

The Panel that is having the problems: 有问题的小组:

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);
    }
}

Using this example, if the user interacts with comboBox2, then with comboBox1, the pull-down from comboBox1 overlaps comboBox2, but comboBox2 is redrawn on top of it. 使用此示例,如果用户与comboBox2交互,则使用comboBox1,comboBox1的下拉重叠comboBox2,但comboBox2在其上重绘。

I found a couple of things that could contribute to what you are seeing. 我发现了一些可能有助于你所看到的东西。

In the updateSharedMemory code for handling a repaint of a Window, the code calls contentPane.paint . 在用于处理Window重绘的updateSharedMemory代码中,代码调用contentPane.paint This is the most likely culprit as the Window might not be your contentPane. 这是最可能的罪魁祸首,因为Window可能不是您的contentPane。 The code for JPopupMenu (which is used by JComboBox) may choose to present the popup as a heavyweight component. JPopupMenu(由JComboBox使用)的代码可以选择将弹出窗口呈现为重量级组件。 So, the Window could be the popup of one of the JComboBoxes. 因此,Window可以是其中一个JComboBox的弹出窗口。

Additionally, the sharedMemoryUpdater is scheduled on the EDT where it will run once the event queue is empty. 此外,sharedMemoryUpdater在EDT上进行调度,一旦事件队列为空,它将运行。 So, there may be a lag between when the addDirtyRegion is called and when updateSharedMemory is called. 因此,有可能是当之间的滞后addDirtyRegion被调用,当updateSharedMemory被调用。 In updateSharedMemory there is a call to region.component.paint . updateSharedMemory有一个对region.component.paint的调用。 If any of the already queued events change component , the actual results of the paint call could vary. 如果任何已排队的事件更改component ,则绘制调用的实际结果可能会有所不同。

Some suggestions as a result of testing: 作为测试结果的一些建议:

Create sharedMemoryUpdater like this: 像这样创建sharedMemoryUpdater

    private Runnable scheduled = null;

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

Then, in addDirtyRegion 然后,在addDirtyRegion

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

That will reduce the number of invocations of sharedMemoryUpdater (by 99% in my testing). 这将减少sharedMemoryUpdater的调用次数(在我的测试中减少了99%)。 As all of the calls to addDirtyRegion should be happening on the EDT, you shouldn't need a synchronize on scheduled , but adding won't hurt much. 因为所有对addDirtyRegion的调用addDirtyRegion应该在EDT上进行,所以你不应该在scheduled进行同步,但添加不会有太大影响。

Since there is a lag, the number of regions to process can become quite large. 由于存在滞后,因此要处理的区域的数量可能变得非常大。 In my tests I saw it exceed 400 at one point. 在我的测试中,我发现它一度超过400。

These changes will cut the time spent manipulating the region list as it's faster to create 1 new list than create all the entries needed to copy the existing list. 这些更改将减少操作区域列表所花费的时间,因为创建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;
}

I'm making one assumption: that your application is running in a real graphics environment (ie not headless). 我做了一个假设:您的应用程序在真实的图形环境中运行(即不是无头)。

I think you might want to take advantage of java.awt.Robot which was designed to imitate a user using an AWT/Swing application. 我想你可能想利用java.awt.Robot ,它是为了模仿使用AWT / Swing应用程序的用户而设计的。 It can do things like simulating key-presses, mouse clicks, and it can take screenshots ! 它可以执行诸如模拟按键,鼠标点击之类的操作,并且可以截取屏幕截图 The method is createScreenCapture(Rectangle) , and it returns a BufferedImage which should be perfect for most use cases. 该方法是createScreenCapture(Rectangle) ,它返回一个BufferedImage ,它应该适合大多数用例。

Here's an example, where I've included a number of vertical JComboBox es which overlap each other. 这是一个例子,我在其中包含了许多相互重叠的垂直JComboBox Opening one of these and pressing F1 will take a screenshot and show it in the preview panel below. 打开其中一个并按下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);
            }
        });
    }

}

You should be able to see the whole window including the window decorations, and the combo-box menu should appear on top of the other combo-boxes exactly as you see it on-screen. 您应该能够看到整个窗口,包括窗口装饰,组合框菜单应该显示在其他组合框的顶部,就像您在屏幕上看到的那样。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM