如何在不闪烁的情况下调整Swing JWindow的大小?

24
我正在尝试基于JWindow创建自定义UI,以选择要共享的屏幕区域。我已经扩展了JWindow并添加了代码,使其可调整大小,并使用AWTUtilities.setWindowShape() '剪切' 窗口中心。

运行代码时,当窗口向负x和y方向(即向上和向左)调整大小时,会出现闪烁。看起来是在更新组件之前调整大小并绘制窗口。下面是代码的简化版本。运行后,可以使用顶部面板将窗口向上和向左调整大小。窗口的背景设置为绿色,以清楚地显示不希望显示的像素。

编辑:通过ComponentListener改进了代码以正确地形状化窗口,并在底部添加了虚拟组件以进一步说明闪烁(还更新了截图)。

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Rectangle;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.geom.Area;

import javax.swing.JPanel;
import javax.swing.JWindow;
import javax.swing.border.CompoundBorder;
import javax.swing.border.EmptyBorder;
import javax.swing.border.EtchedBorder;
import javax.swing.border.LineBorder;

import com.sun.awt.AWTUtilities;

public class FlickerWindow extends JWindow implements MouseListener, MouseMotionListener{

    JPanel controlPanel;
    JPanel outlinePanel;
    int mouseX, mouseY;
    Rectangle windowRect;
    Rectangle cutoutRect;
    Area windowArea;

    public static void main(String[] args) {
        FlickerWindow fw = new FlickerWindow();
    }

    public FlickerWindow() {
        super();
        setLayout(new BorderLayout());
        setBounds(500, 500, 200, 200);
        setBackground(Color.GREEN);

        controlPanel = new JPanel();
        controlPanel.setBackground(Color.GRAY);
        controlPanel.setBorder(new EtchedBorder(EtchedBorder.LOWERED));
        controlPanel.addMouseListener(this);
        controlPanel.addMouseMotionListener(this);

        outlinePanel = new JPanel();
        outlinePanel.setBackground(Color.BLUE);
        outlinePanel.setBorder(new CompoundBorder(new EmptyBorder(2,2,2,2), new LineBorder(Color.RED, 1)));

        add(outlinePanel, BorderLayout.CENTER);
        add(controlPanel, BorderLayout.NORTH);
        add(new JButton("Dummy button"), BorderLayout.SOUTH);
        setVisible(true);
        setShape();

        addComponentListener(new ComponentAdapter() {           
            @Override
            public void componentResized(ComponentEvent e) {
                setShape();
            }});
    }


    public void paint(Graphics g) {
        // un-comment or breakpoint here to see window updates more clearly
        //try {Thread.sleep(10);} catch (Exception e) {}
        super.paint(g);
    }

    public void setShape() {
        Rectangle bounds = getBounds();
        Rectangle outlineBounds = outlinePanel.getBounds();
        Area newShape = new Area (new Rectangle(0, 0, bounds.width, bounds.height));
        newShape.subtract(new Area(new Rectangle(3, outlineBounds.y + 3, outlineBounds.width - 6, outlineBounds.height - 6)));
        setSize(bounds.width, bounds.height);
        AWTUtilities.setWindowShape(this, newShape);
    }

    public void mouseDragged(MouseEvent e) {
        int dx = e.getXOnScreen() - mouseX;
        int dy = e.getYOnScreen() - mouseY;

        Rectangle newBounds = getBounds();
        newBounds.translate(dx, dy);
        newBounds.width -= dx;
        newBounds.height -= dy;

        mouseX = e.getXOnScreen();
        mouseY = e.getYOnScreen();

        setBounds(newBounds);
    }

    public void mousePressed(MouseEvent e) {
        mouseX = e.getXOnScreen();
        mouseY = e.getYOnScreen();
    }

    public void mouseMoved(MouseEvent e) {}
    public void mouseClicked(MouseEvent e) {}
    public void mouseReleased(MouseEvent e) {}
    public void mouseEntered(MouseEvent e) {}
    public void mouseExited(MouseEvent e) {}
}

重写的paint()方法可以用作断点,或者可以取消注释Thread.sleep()以提供更清晰的更新视图。

我的问题似乎源于setBounds()方法在布局之前将窗口绘制到屏幕上。


窗口在调整大小之前,应该是这个样子:

alt text


窗口在调整大小时变大(向上和向左),这是在重写的paint()方法中看到的断点:

alt text


窗口在调整大小时变小(向下和向右),如在重写的paint()方法中看到的断点:

alt text


这些屏幕截图是在鼠标拖动时拍摄的,但即使对于较为温和的鼠标拖动,闪烁也会变得非常令人讨厌。在调整大小到更大的屏幕截图上,绿色区域显示了在任何绘画/布局之前绘制的新背景,似乎发生在底层的ComponentPeer或本地窗口管理器中。在“调整大小到较小”的屏幕截图上,蓝色区域显示了JPanel的背景被推入视图,但现在已经过时。这在Linux(Ubuntu)和Windows XP下发生。
是否有人找到了一种方法,在对屏幕进行任何更改之前将Window或JWindow调整大小到后备缓冲区,从而避免这种闪烁效应?也许有一个java.awt....系统属性可以设置以避免这种情况,但我找不到。

编辑 #2: 将调用 AWTUtilities.setWindowShape() 的语句注释掉(如果需要,可以取消注释 paint() 中的 Thread.sleep(10) 行),然后激烈地拖动顶部面板,以清晰地看到闪烁的性质。

编辑 #3: 有人能在Windows 7或Mac OSX上测试这种行为吗?


2
@willjcroz:+1,问题表述得非常清晰。并且请各位别忘了,在没有给问题点赞的情况下也不要点赞任何回答。 - SyntaxT3rr0r
1
我也想知道相同问题的答案 - 多年来一直讨厌这个问题,但从未找到过一个不成熟的解决方法。在Windows 7上进行了测试,尽管我没有像你那样完全相同的效果,但我得到了非常相似的东西。调整大小工件可以看到每个Java应用程序,不仅仅是你的例子。奇怪的是,移动窗口还行,只有调整大小不好(可能是因为Windows 7自己处理移动窗口并且不触发Java中的调整大小 - 与我记得的Windows XP不同)。 - Domchi
@Domchi:感谢你确认它在Windows 7上发生了:-)。在XP上移动窗口似乎很顺畅,没有重绘。也许你指的是XP上的窗口“修复”效果不好。像Vista/Win7、OSX和Linux上的Compiz等组合式窗口管理器可以处理先前被遮挡部分的“修复”,而无需请求重绘,这就是我认为XP可能存在问题的地方。 - willjcroz
1
@willjcroz:是的,你说得对 - 我指的是Win7缓存当前窗口图像,因此如果EDT由于任何原因而卡住,您仍然可以移动窗口并查看内部,而不是在移动窗口时立即出现灰色矩形,就像在XP上一样。 - Domchi
3个回答

3
我承认这不是一个特别有用的答案,但是了解Swing的确切含义可能会有所帮助。
看到了吗,Swing做了所有的工作,除了从操作系统获得一点绘图空间。所有的绘图、小部件等都是Java代码。问题不在于它运行得太慢,而在于它没有得到2D图形卡加速和操作系统渲染技巧的好处。
还记得DirectDraw吗?现在所有东西都有了它,窗口操作非常流畅。但是如果你拿到一台没有它的电脑(比如没有驱动程序的XP安装),你就会注意到恰恰是这种类型的减速。
对于Swing来说,因为它管理了所有自己的空间,操作系统无法进行任何这些渲染技巧来帮助你。
也许有人会提出一些优化方法,在你的计算机上解决你的问题,但我担心这实际上并不能解决根本问题——Swing很慢,而且无法变得更快。
你应该考虑使用本地工具包。AWT还可以,但缺少很多小部件等。它是本地的,内置的,所以如果你只需要这些,它应该足够快。我喜欢SWT,这是Eclipse、Vuze(等等)使用的工具包。它结合了AWT的本地性和Swing的易用性和功能,并且当然可以在任何地方运行。
编辑:阅读了您的一些评论后,很明显您绝对理解窗口管理发生的方式,我不想显得居高临下。不仅如此,你更关心调整大小,而我的评论与之无关。我仍然建议使用SWT,因为它是本地代码,速度更快,但这是一个不同的答案。

谢谢您的想法,不用担心,我并没有感到受到傲慢对待。是的,SWT看起来像是未来某个项目解决这个问题的一种方式。但由于这个项目有一个紧急的截止日期,并且这个UI方面是在需要快速加载时间的小应用程序中,所以SWT现在并不能为我提供解决方案。 - willjcroz
同时,问题似乎实际上存在于Swing的JWindow底层的AWT层中。具体来说,本地对等体ComponentPeer及其相应的本地代码没有提供任何方法来要求子组件在更新到屏幕设备之前将自身绘制到提供的缓冲区中。因此,这似乎是所有顶级AWT和(重量级)Swing组件的一个问题。 - willjcroz

1

另一种方法可能是等待用户完成拖动/调整大小后再进行绘制。也许可以使用边界框来向用户显示窗口的大小,直到他们的调整事件完成为止。

我注意到很多窗口应用程序要么采用这种方法,要么处理闪烁问题。我尝试过所有的应用程序都有同样的问题(非Java),因此问题可能只存在于图形子系统而不是Swing本身。


是的,我目前正在考虑使用边界框方法,唯一的问题是用户需要精确选择屏幕上的确切区域,所以在两个状态之间改变界面是不可取的。至于其他应用程序,您是否尝试在Eclipse中调整大小?尽管它具有复杂的布局,但对我来说没有任何闪烁,卡顿是有的,但没有闪烁!这让我认为我学习和使用SWT可能会更好运。 - willjcroz
2
是的,我想抖动/闪烁对我来说是一回事。无论如何,看到任何重绘都是不可取的。我在写评论时调整了我的IDE大小 - 所以我想我们是在同一页上。我在工作中使用一些SWT / RCP应用程序,尽管它提供了一个相当不错的框架,但在可扩展性方面还有很多需要改进的地方。它使用的事件模型也不太友好。你有试过检查NetBeans平台吗?我认为那是一个值得研究的基于Swing的框架。 - Lam Chau
PS:我使用一台旧机器测试你的程序并尝试修复(2.0单核带Windows XP)-- 当我进行任何调整大小操作时,仍然会出现闪烁/卡顿。 - Lam Chau

1

我刚刚尝试了你的示例。当我调整窗口大小时,确实看到了一些小闪烁。我尝试将从setBounds()开始并以AWTUtilities.setWindowShape(this, newShape);结束的代码片段替换为

setSize(newBounds.width, newBounds.height);

然后发现闪烁消失了。所以,我建议您使用这个解决方案,除非您有特殊原因要使用setBounds


感谢您的输入。为了获得正确的调整大小行为(此示例表示向左上方调整大小的用例),我需要同时移动窗口并调整其大小(JWindow的左上角原点需要更改),因此使用了setBounds() - willjcroz
1
@AlexR:好的,但是在许多其他平台上,AWTUtilities.setWindowShape(this, newShape)无法正常工作,我已经尝试过了,包括Fedora Linux/Ubuntu和OpenJDK。 - user285594

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