将组件绘制到BufferedImage会导致显示损坏。

7
我正在使用这里描述的JScrollNavigator组件,以便在我嵌入在JScrollPane中的大型“画布式”CAD组件上提供导航窗口。
我尝试调整JScrollNavigator以绘制画布的缩略图,以向用户提供一些附加上下文。但是,这样做会导致我的应用程序主框架渲染损坏。具体而言,调用位于视口组件上(即我的主画布)的paint(Graphics)方法,传递由BufferedImage创建的Graphics对象会导致后续显示发生损坏。如果我注释掉这行代码,一切正常。
以下是JScrollNavigator的重写paintComponent方法:
@Override
protected void paintComponent(Graphics g) {
    Component view = jScrollPane.getViewport().getView();
    BufferedImage img = new BufferedImage(view.getWidth(), view.getHeight(), BufferedImage.TYPE_INT_ARGB);
    Graphics2D g2d = img.createGraphics();

    // Paint JScrollPane view to off-screen image and then scale.
    // It is this action that causes the display corruption!
    view.paint(g2d);
    g2d.drawImage(img, 0, 0, null);
    Image scaled = img.getScaledInstance(getWidth(), getHeight(), 0);

    super.paintComponent(g);
    g.drawImage(scaled, 0, 0, null);
}

有人能提供损坏的原因吗?我本以为在离屏图像上绘制不会对现有的绘制操作产生影响。

编辑

提供一些额外的细节: JScrollNavigator 是一个子面板,位于 JSplitPane 的左侧。与导航器相关联的 JScrollPane 位于右侧。 "corruption" 导致拆分器不再呈现,滚动条不可见(它们呈白色)。如果我调整 JFrame 的大小,则 JMenu 部分也变为白色。如果我试图使用导航器或与滚动条交互,则它们变为可见,但分隔符仍然是白色的。就好像各种组件的不透明设置受到了视口视图渲染到离屏图像的影响一样。

此外,如果我将 JScrollNavigator 显示在完全独立的 JDialog 中,一切都正常工作。

编辑 2

我可以通过以下方式 持续地 复现问题:

JMenuBar 添加到 mFrame 中:

JMenuBar bar = new JMenuBar();
bar.add(new JMenu("File"));
mFrame.setJMenuBar(bar);

JScrollNavigatormain() 方法中,将以下内容替换:
jsp.setViewportView(textArea);

使用以下方式:

jsp.setViewportView(new JPanel() {
  {
    setBackground(Color.GREEN);
    setBorder(BorderFactory.createLineBorder(Color.BLACK, 5));
  }
});

确保将JScrollNavigator作为一个面板嵌入到mFrame中,而不是作为一个独立的JDialog出现:

mFrame.add(jsp, BorderLayout.CENTER);
mFrame.add(nav, BorderLayout.NORTH);

现在应用程序运行时,JMenuBar不再可见;将视图(即绿色的JPanel带有粗黑边框)绘制到BufferedImage.createGraphics()返回的Graphics2D上实际上似乎是从JFrame的左上角开始呈现在屏幕上,因此遮挡了其他组件。这只发生在将JPanel用作视口视图而不是其他组件(如JTextAreaJTable等)的情况下。
编辑3:
看起来这个人也遇到了同样的问题(尽管没有发布解决方案):http://www.javaworld.com/community/node/2894/ 编辑4:
以下是导致Edit 2中所描述的可重现错误的mainpaintComponent方法:
public static void main(String[] args) {
    JScrollPane jsp = new JScrollPane();
    jsp.setViewportView(new JPanel() {
        {
            setBackground(Color.GREEN);
            setBorder(BorderFactory.createLineBorder(Color.BLACK, 5));
        }
    });

    JScrollNavigator nav = new JScrollNavigator();
    nav.setJScrollPane(jsp);

    JFrame mFrame = new JFrame();

    JMenuBar bar = new JMenuBar();
    bar.add(new JMenu("File"));
    mFrame.setJMenuBar(bar);

    mFrame.setTitle("JScrollNavigator Test");

    mFrame.setSize(800, 600);

    mFrame.setLayout(new GridLayout(1, 2));

    mFrame.add(jsp);
    mFrame.add(nav);
    Dimension screenDim = Toolkit.getDefaultToolkit().getScreenSize();
    mFrame.setLocation((screenDim.width - mFrame.getSize().width) / 2, (screenDim.height - mFrame.getSize().height) / 2);

    mFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    mFrame.setVisible(true);
}

@Override
protected void paintComponent(Graphics g) {
    super.paintComponent(g);

    Component view = jScrollPane.getViewport().getView();

    if (img == null) {
        GraphicsConfiguration gfConf = GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice().getDefaultConfiguration();
        img = new BufferedImage(view.getWidth(), view.getHeight(), BufferedImage.TYPE_INT_ARGB);
    }

    Graphics2D g2d = img.createGraphics();
    view.paint(g2d);

    Image scaled = img.getScaledInstance(getWidth(), getHeight(), 0);

    g.drawImage(scaled, 0, 0, null);
}

编辑5

似乎其他人也无法重现确切的问题。我建议大家运行在这里粘贴的代码。当我第一次运行此示例时,我看到以下内容:

损坏的图像1

既未绘制JScrollNavigator也未绘制JMenuBar;这些框架区域是透明的。

调整大小后,我看到以下内容:

损坏的图像2

JMenuBar仍未被绘制,并且似乎JPanel曾经在(0,0)处呈现过(JMenuBar应该在此处)。view.paint调用paintComponent中的是直接原因。


“腐败”是什么意思?我刚刚运行了这个示例,它对我来说完美无缺。我只需要在重写的paintComponent方法的开头调用super.paintComponent(g);即可。 - Xeon
2
我已经在这里提交了代码。 - Xeon
3个回答

9
总结:原始的 JScrollNavigator 使用 Swing 的 opacity 属性在相邻的 JScrollPane 中缩放组件的缩略图上呈现方便的绿色 NavBox。由于它扩展了 JPanel,(共享) UI 代理的使用与可滚动组件的使用冲突。在上面的第5个编辑中看到的图像是相关渲染工件的典型示例,也在此处显示。解决方案是让NavBoxJScrollNavigator和可滚动组件扩展JComponent,如下面的第二个附录所建议的那样。然后,每个组件都可以单独管理其自己的属性。

enter image description here

我在我的平台Mac OS X,Java 1.6上看到您发布的代码没有任何异常渲染效果。很抱歉,我没有发现任何明显的可移植性违规。

image one

一些可能与主题无关,但或许有用的观察。
  • Even if you use setSize(), appropriately in this case, you should still pack() the enclosing Window.

    f.pack();
    f.setSize(300, 200);
    
  • For convenience, add() forwards the component to the content pane.

    f.add(nav, BorderLayout.WEST);
    
  • Prefer StringBuilder to StringBuffer.

  • Consider ComponentAdapter in place of ComponentListener.

补充说明:如此处所建议,我使用RenderingHints而不是getScaledInstance()可以得到更灵活的结果,如下所示。添加一些图标可以更容易地看到对图像和文本的差异影响。

image two

editPane.insertIcon(UIManager.getIcon("OptionPane.errorIcon"));
editPane.insertIcon(UIManager.getIcon("OptionPane.warningIcon"));
...
@Override
protected void paintComponent(Graphics g) {
    super.paintComponent(g);
    Component view = jScrollPane.getViewport().getView();
    BufferedImage img = new BufferedImage(view.getWidth(),
        view.getHeight(), BufferedImage.TYPE_INT_ARGB);
    Graphics2D off = img.createGraphics();
    off.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
        RenderingHints.VALUE_ANTIALIAS_ON);
    off.setRenderingHint(RenderingHints.KEY_INTERPOLATION,
        RenderingHints.VALUE_INTERPOLATION_BICUBIC);
    view.paint(off);
    Graphics2D on = (Graphics2D)g;
    on.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
        RenderingHints.VALUE_ANTIALIAS_ON);
    on.setRenderingHint(RenderingHints.KEY_INTERPOLATION,
        RenderingHints.VALUE_INTERPOLATION_BICUBIC);
    on.drawImage(img, 0, 0, getWidth(), getHeight(), null);
}

第二附录:看起来JPanel UI代理不太合作。一个解决方法是扩展JComponent,以便您可以控制不透明度。稍微多做一点工作就可以管理backgroundColorNavBoxJScrollNavigator也可以采用类似的处理方式。

enter image description here

jsp.setViewportView(new JComponent() {

    {
        setBackground(Color.red);
        setBorder(BorderFactory.createLineBorder(Color.BLACK, 16));
    }

    @Override
    protected void paintComponent(Graphics g) {
        super.paintComponent(g);
        g.setColor(getBackground());
        g.fillRect(0, 0, getWidth(), getHeight());
    }

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

谢谢 - 说实话,当我导入代码时,我已经对JScrollNavigator进行了更改。我注意到只有在将JScrollNavigator作为应用程序框架的子面板嵌入时才会出现损坏问题;如果我将导航器作为单独的JDialog,则似乎可以正常工作。我会继续深入研究。 - Adamski
@Adamski:这个答案提醒我重采样伪影问题,在这里引用的文章中提到了。 - trashgod
谢谢;我尝试改用GridLayout,但它仍然看起来不好 - JMenuBar被覆盖了。我已经发布了我使用的确切main和paintComponent方法。 - Adamski
我猜这一定是一个Windows特定的问题 - 我让两个同事在Fedora和Windows 7上运行代码。两者都遇到了相同的重绘问题,只有当JPanel作为视口视图时才会出现,而不是JTextArea(在那里一切正常)。 - Adamski
顺便问一下,“..see the disparate effect on images and test.” 应该改为 “..see the disparate effect on images and text.” 吗?(s->x) - Andrew Thompson
显示剩余10条评论

2

我不确定你所说的“corruption”是什么意思,但是我注意到如果你指定Image.SCALE_SMOOTH作为重新缩放提示,重采样后的图像会更加清晰:

Image scaled = img.getScaledInstance(getWidth(), getHeight(), Image.SCALE_SMOOTH);

也许这就是你在寻找的内容...

啊,_这个_工件。还要考虑这个有关重采样工件的答案;+1。 - trashgod

2
我能够重现您的问题并为您提供所需的结果。问题在于,当您再次重绘时,图像的绘制尚未完成,因此只有部分图像被绘制。要解决这个问题,请将以下字段添加到您的JScrollNavigator类中(作为锁定):
/** Lock to prevent trying to repaint too many times */
private boolean blockRepaint = false;

当我们重新绘制组件时,这个锁将被激活。直到我们成功绘制面板后,它才会被释放 - 然后可以执行另一个绘制操作。

paintComponent需要更改以遵守锁并在绘制导航面板时使用ImageObserver

@Override
protected void paintComponent(final Graphics g) {
    super.paintComponent(g);
    if(!blockRepaint){
        final Component view = (Component)jScrollPane.getViewport().getView();
        BufferedImage img = new BufferedImage(view.getWidth(), view.getHeight(), BufferedImage.TYPE_INT_ARGB);
        Graphics2D g2d = img.createGraphics();
        g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);

        // Paint JScrollPane view to off-screen image and then scale.
        // It is this action that causes the display corruption!
        view.paint(g2d);
        ImageObserver io = new ImageObserver() {

            @Override
            public boolean imageUpdate(Image img, int infoflags, int x, int y,int width, int height) {
                boolean result = true;
                g.drawImage(img, 0, 0, null);
                if((infoflags & ImageObserver.FRAMEBITS) == ImageObserver.FRAMEBITS){
                    blockRepaint = false;
                    result = false;
                }

                return result;
            }
        };

        Image scaled = img.getScaledInstance(getWidth(), getHeight(), 0);
        blockRepaint = g.drawImage(scaled, 0, 0, io);
    }
}

我尝试将您的paintComponent替换到我粘贴的示例中,但是我看到相同的结果。我不太明白缩放如何成为问题:即使我注释掉缩放代码,问题仍然存在;绘制到离屏图像会导致此问题。 - Adamski
我假设你将锁作为一个字段添加了(否则代码无法运行)。如果是这样,那么在ImageObserver上返回的标志可能不同。您指示出问题的那一行似乎只是因为当您注释掉它时,会阻止其他步骤执行任何操作 - 它是g.drawImage(...) 行需要花费一些时间来完成,因为它正在更新UI。我现在有用的代码在这里:http://pastebin.com/MccHaaFj。我在`paintComponent`中添加了一些打印语句,以便您可以看到传递的确切标志(并根据需要进行调整) - Nick Rippe
@NickRippe:我在Ubuntu/OpenJDK上无法使其工作,但我从未尝试过像这样重新排序渲染。我的解决方案是通过扩展JComponent来避免使用PanelUI;我欢迎您提供任何关键见解。 - trashgod
@trashgod - 我刚刚遇到了一些问题,无法确定实际的问题所在。我以为找到了问题所在,但是现在回头看,似乎只是在修复另一个渲染问题。:P 哎呀!我已经花了足够的时间处理这个问题了 - 你的答案提供了一个不错的变通方法(我只是希望能够修复问题)。 - Nick Rippe
@NickRippe:谢谢。我刚刚注意到BasicPanelUI是共享的,这可能是一个因素。 - trashgod

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接