不可调整大小的JFrame打包错误

4

JFramepack()方法似乎并不总是在窗口不可调整大小时起作用。请自行尝试(可能需要多次重试):

import javax.swing.*;
import java.awt.*;

public class FramePackBug {
    public static void main(String[] args) {
        SwingUtilities.invokeLater(new Runnable() {
            public void run() {
                for (int i = 0; i < 20; i++) {
                    JFrame window = new JFrame("Buggy");
                    window.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                    window.setResizable(false);
                    window.setBackground(Color.RED);
                    JPanel jp = new JPanel() {
                        public void paintComponent(Graphics g) {
                            g.setColor(Color.GREEN);
                            g.fillRect(0, 0, 200, 100);
                        }
                    };
                    jp.setPreferredSize(new Dimension(200, 100));
                    window.add(jp);
                    window.pack();
                    window.setLocation((i % 5) * 250, (i / 5) * 150);
                    window.setVisible(true);
                }
            }
        });
    }
}

您可以通过在将其设置为不可调整大小后直接使框架可见来解决此问题。
为什么会这样呢?

@mins:是的。在那种情况下,所有窗口都是错误的。 - AyCe
1+, 这有点奇怪。当我在Windows 7上使用JDK7进行测试时,第一个框架总是正确的,但之后大多数都是错误的。我只能得到3-4个正确大小的窗口。我甚至将您的代码更改为使用Timer每500毫秒创建一个窗口。我还将代码更改为使用JPanel而无需覆盖paintComponent()方法。 - camickr
非可调整大小的框架边框比可调整大小的框架边框细1像素是一个常见的混淆源(至少在Windows上使用默认LaF),人们错误地调用setResizable(false),并想知道为什么他们的内容区域总是偏移2个像素。但在这种情况下,调用顺序是清晰和正确的。有时一些窗口出现问题通常表示某些线程问题,但这也可能不是这种情况。这确实很奇怪,期待看到答案。 - Marco13
@camickr 是的,我也观察到了这一点。原来的代码要大得多,但我将其大大减少,发现对invokeLater的不同调用、延迟等都没有任何作用。 - AyCe
super.paintComponent()? - trashgod
显示剩余3条评论
3个回答

3

作为参考,这是Mac OS X的一个变种外观。请注意,

  • 一个像素的差异表明可能有一个专门用于焦点指示器的空间;但是,正如@Marco13在评论中所说,偶尔出现的情况表明存在(不明显的)线程问题。

  • 由于外观似乎与平台相关,标签显示了操作系统和版本系统属性。

  • 该示例覆盖了getPreferredSize()以建立封闭面板的几何形状,建议参见此处

  • 如果实现通过填充每个像素来遵守不透明度属性,则对super.paintComponent()的调用不是严格必要的。

  • @AyCe的Windows截图中看到的不规则位置与@Marco13提出的线程问题一致。

Mac OS X: image Mac OS X

Windows 7 (@AyCe): image Windows

Ubuntu 14: image Ubuntu

import javax.swing.*;
import java.awt.*;

public class FramePackBug {

    public static void main(String[] args) {
        SwingUtilities.invokeLater(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 20; i++) {
                    JFrame window = new JFrame("Buggy");
                    window.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                    window.setResizable(false);
                    window.setBackground(Color.RED);
                    JPanel jp = new JPanel() {
                        @Override
                        public void paintComponent(Graphics g) {
                            super.paintComponent(g);
                            g.setColor(Color.GREEN);
                            g.fillRect(0, 0, getWidth(), getHeight());
                        }

                        @Override
                        public Dimension getPreferredSize() {
                            return new Dimension(200, 100);
                        }
                    };
                    window.add(jp);
                    window.add(new JLabel(System.getProperty("os.name") + ", "
                        + System.getProperty("os.version")), BorderLayout.NORTH);
                    window.pack();
                    window.setLocation((i % 5) * (window.getWidth() + 5),
                        (i / 5) * (window.getHeight() + 5) + 20);
                    window.setVisible(true);
                }
            }
        });
    }
}

使用您的代码,我得到了这个。此外,请注意以这种方式无法看到红色边框,因为面板始终填充整个空间(但仍大于指定大小)。 - AyCe
为了参考,我添加了你的屏幕截图和Ubuntu的一个屏幕截图;抱歉,我无法解释在Windows上看到的效果。 - trashgod

1

编辑:请查看下方更新内容

抱歉,这还不是最终解决方案(目前还未确定),但我正在调查此问题,并想分享一些第一手信息,也许对其他人有所帮助,以便进行进一步的分析:

这种奇怪的行为是由窗口的 getInsets() 方法引起的。当重写此方法时,您会发现它返回的插入值在大多数情况下是 3,其中左、右和底部是 3,但当出现错误时,它们变成了 4

import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Insets;

import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;

public class FramePackBug {
    public static void main(String[] args) {

        SwingUtilities.invokeLater(new Runnable() {
            public void run() {
                for (int i = 0; i < 20; i++) {
                    final int ii = i;
                    JFrame window = new JFrame("Buggy")
                    {
                        @Override
                        public Insets getInsets()
                        {
                            Insets insets = super.getInsets();
                            if (insets.left == 4)
                            {
                                System.out.printf(
                                    "Wrong insets in %d : %s\n", ii, insets);
                            }
                            return insets;
                        }
                    };
                    window.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                    window.setResizable(false);
                    window.setBackground(Color.RED);
                    JPanel jp = new JPanel() {
                        public void paintComponent(Graphics g) {
                            g.setColor(Color.GREEN);
                            g.fillRect(0, 0, 200, 100);
                        }
                    };
                    jp.setPreferredSize(new Dimension(200, 100));
                    window.add(jp);
                    window.pack();
                    window.setLocation((i % 5) * 250, (i / 5) * 150);
                    window.setVisible(true);
                }
            }
        });
    }
}

我已经扫描了底层实现,最终委托给WWindowPeer和一些本地方法,这些方法根据窗口样式(即窗口是否可调整大小)进行一些奇怪的计算,并且我已经确定了一些错误的可能原因,但仍然没有最终结论。(当然,这似乎是随机发生的,不会使调试变得更容易...)

一个副注:仅仅调用pack()方法两次就能让框架总是正确显示,但是当然,只有在找到奇怪行为的根本原因之前,这最多只能被认为是一种hacky workaround。


更新

我深挖了一层,但仍然没有找到最终解决方案。

sun.awt.windows.WFramePeer类的初始化方法实现如下:

void initialize() {
    super.initialize();

    Frame target = (Frame)this.target;

    if (target.getTitle() != null) {
        setTitle(target.getTitle());
    }
    setResizable(target.isResizable());
    setState(target.getExtendedState());
}

“super”调用进入“sun.awt.windows.WWindowPeer”,然后执行以下操作:
void initialize() {
    super.initialize();

    updateInsets(insets_);
    ...
}

updateInsets调用结束于一个本地方法。updateInsets的本地实现进行了一些可疑的调用,查询窗口的“style”,并根据WS_THICKFRAME属性调整插入物。

if (style & WS_THICKFRAME) {
    m_insets.left = m_insets.right =
        ::GetSystemMetrics(SM_CXSIZEFRAME);

我可以确定的是,这些updateInsets调用的结果是错误的。它们对于不可调整大小的框架来说太大了,最终导致了错误。我不能确定的是:在哪里(有时)更新了错误的插入以正确反映不可调整大小的框架的大小(对于某些框架)。

一个可能的解决方案?

从更新后的代码片段中可以看到,WFramePeer的初始化最终会调用

    setResizable(target.isResizable());
< p > setResizable再次委托给本地方法,并且在 setResizable方法的本地实现中,再次出现了styleWS_THICKFRAME

因此,将sun.awt.windows.WFramePeer#initialize()方法更改为首先设置"resizable"属性,然后向上传递调用(这将导致调用updateInsets)解决了我的问题:

void initialize() {

    Frame target = (Frame)this.target;
    setResizable(target.isResizable());
    super.initialize();

    if (target.getTitle() != null) {
        setTitle(target.getTitle());
    }
    setState(target.getExtendedState());
}

通过这个修改,插图的计算是正确的,并且框架 总是 能够正确地显示。


比我做的还要好,等待1秒后再调用pack();和setVisible(true);。至少我们可以通过在setResizable(false)之后但在pack()之前调用setVisible(true)来修复它,使其不那么hacky。 - AyCe
感谢您对此事的关注。我之前看到过建议使用pack()两次的解决方法,但没有参考资料。我想知道这种异常位置是否与这里所见的效果有关。 - trashgod

0

我不确定这是否正确,但您可以尝试在pack方法之后放置setResizable()方法,因为setResizeable(false)可能会禁用pack函数。


这个已经在第一条评论中提出过了,但实际上这样做会“禁用”pack方法。 - AyCe

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