Java 焦点所有者临时更改为null

Java 焦点所有者临时更改为null,java,swing,focus,Java,Swing,Focus,我对Swing开发还很陌生,希望我的问题不是一个愚蠢的问题 我有以下问题。我正在使用KeyboardFocusManager跟踪焦点,监听属性permanentFocusOwner的更改。但是,当焦点从一个控件更改为另一个控件时,我将permanentFocusOwner属性中间更改为null 当焦点位于其中一个面板或其子面板内时,我当前的UI逻辑正在对控件进行一些更改。但是,获取中间值null会破坏此逻辑 我在谷歌上搜索了关于这个问题的信息,没有找到任何相关信息 问题是,这种行为是否出于设计,

我对Swing开发还很陌生,希望我的问题不是一个愚蠢的问题

我有以下问题。我正在使用
KeyboardFocusManager
跟踪焦点,监听属性
permanentFocusOwner
的更改。但是,当焦点从一个控件更改为另一个控件时,我将
permanentFocusOwner
属性中间更改为
null

当焦点位于其中一个面板或其子面板内时,我当前的UI逻辑正在对控件进行一些更改。但是,获取中间值
null
会破坏此逻辑

我在谷歌上搜索了关于这个问题的信息,没有找到任何相关信息

问题是,这种行为是否出于设计,以及是否有某种方法可以绕过中间空值

以下是再现上述行为的最小应用程序:

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

public class FocusNullTest extends JFrame {

    public static void main(String[] args) {
        EventQueue.invokeLater(new Runnable() {
            public void run() {
                FocusNullTest self = new FocusNullTest();
                self.setVisible(true);
            }
        });
    }

    public FocusNullTest() {
        setSize(150, 100);
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        Container contentPane = getContentPane(); 
        contentPane.setLayout(new BoxLayout(contentPane, BoxLayout.X_AXIS));

        contentPane.add(new JButton("1"));
        contentPane.add(new JButton("2"));

        KeyboardFocusManager focusManager =
            KeyboardFocusManager.getCurrentKeyboardFocusManager();
        focusManager.addPropertyChangeListener(
                "permanentFocusOwner",
                new PropertyChangeListener() {
                    @Override
                    public void propertyChange(PropertyChangeEvent e) {
                        System.out.println("permanentFocusOwner changed from: "
                                           + e.getOldValue());
                        System.out.println("permanentFocusOwner changed to  : "
                                           + e.getNewValue());
                    }
                });
    }
}
日志输出为:

(程序启动,焦点自动设置为按钮1)
permanentFocusOwner已从更改为:null
permanentFocusOwner更改为:javax.swing.JButton[,0,18,41x26,(跳过)]
(点击按钮2)
permanentFocusOwner已从:javax.swing.JButton[,0,18,41x26,(跳过)]
永久焦点所有者更改为:null
permanentFocusOwner已从:null更改为 permanentFocusOwner更改为:javax.swing.JButton[,41,18,41x26,(跳过)]


(可选部分,关于代码意图)
我的目标是使某些内容看起来像一个列表视图,其中条目在获得焦点时展开并显示更多信息(在失去焦点时折叠)。展开的视图包含一些附加按钮

JList
似乎不是合适的控件,因为(1)它不允许单击按钮,(2)它的条目具有恒定的高度,而我希望条目在焦点上动态展开
JTable
及其编辑模式似乎也不是一个合适的解决方案,至少因为条目大小不变


因此,我使用带有垂直框布局的plain
JPanel
作为容器,订阅模型更改并手动更新视觉效果。问题是,当我单击按钮时,包含的列表项会失去焦点。如果焦点暂时不会更改为
null
,我可以检测到焦点仍然停留在列表项中。

KeyboardFocusManager正在为大多数属性触发两个事件(从beans规范开始,它不应该-永远也找不到原因,只是猜测焦点的异步性质可能就是原因)


要根据newVaue进行操作,请等待第二个

作为解决方法,将上一个“真正的”焦点所有者存储为事件处理程序中的成员

if ((e.getOldValue() != null) && (e.getNewValue() == null))
 prev_owner = e.getOldValue();
然后,当你聚焦到目标上时,你会有一个对象的句柄。仅当实际组件实际接收焦点时(即
getNewValue()
为非空时),才处理高亮显示更改

(该行为似乎与中所述一致,即前一个组件先失去焦点,然后目标组件获得焦点。它不是原子的,因此有一段时间没有任何东西真正具有焦点。但我不是专家,因此这可能会有所不同。)

我的目标是使某些东西看起来像一个列表视图,其中条目在获得焦点时展开并显示更多信息

另一种选择是,我有时使用a:在左边,我将(可聚焦)展开按钮放在a或垂直面板上;在右边,我放置了展开视图

My goal is to make something looking like a list view, where the entries 
expand and display more information when they get focus (and collapse back 
when they lose it). The expanded view contains some additional buttons.

ButtonModel可以通过使用JButton实现这一点,非常好的输出是通过使用JToggleButton实现的,或者仍然有保留JPanel+MouseListener()的原始想法


+1,但关于目标的更多细节可能会提供更好的解决方案。@垃圾神:添加了一些关于代码意图的细节。我不禁猜测这是窗口如何获得焦点的跨平台变化的结果。谢谢您的建议!虽然这将导致一个不同的用户界面,但它绝对是一个不错的设计。(我以前没有听说过
Outline
)顺便问一下,有没有一种简单的方法可以将JPanel/Box子项绑定到模型,就像JTable的项可以绑定到TableModel一样?我通常在
JPanel
构造函数中给按钮一个。
actionPerformed()
方法查询应用程序的数据模型并更新扩展视图。
操作
可以从按钮、菜单、工具栏或焦点侦听器中调用。@垃圾神我想了一些,非常好的建议,但调用了我…,然后我以我所知道的最古老的想法结束,请参阅我的帖子,以前的投票+1,@trashgood:我现在正在使用我的模型对象。它似乎包含了
事件列表列表
。好吧,为每个控件准备一个抽象模型会很好,但我并不反对手动构建一些东西——至少这会让我了解
TableModel
和其他现有模型是如何工作的。(我有使用.NET/WPF/MVVM的经验,这似乎大致相同。)非常感谢您的代码!然而,我看到了一些关于我想要实现什么的问题。首先,通过按钮激活切换扩展。这与用户使用鼠标进行对焦相同,但如果通过键盘(例如,通过选项卡)设置对焦,则不会激活该按钮。其次,当焦点离开面板时,面板应该立即折叠。如果没有鼠标侦听器,则ButtonModel可以使用javax.swing.Timer(用于延迟显示,或者如果有多个操作,或者可能来自最近的JComponents的一致性)为我工作。键绑定(不是TAB,而是F1、F2或F5)可以解决这一问题,但我仍然认为最好将JWindow实现为容器,而不是跳转到JPanelI。我不确定我到底理解哪个鼠标
My goal is to make something looking like a list view, where the entries 
expand and display more information when they get focus (and collapse back 
when they lose it). The expanded view contains some additional buttons.
import java.awt.*;
import java.awt.event.*;
import java.awt.font.*;
import java.awt.image.BufferedImage;
import javax.swing.*;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;

public class CollapsablePanelTest {

    public static void main(String[] args) {
        CollapsablePanel cp = new CollapsablePanel("test", buildPanel());
        JFrame f = new JFrame();
        f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        f.getContentPane().add(new JScrollPane(cp));
        f.setSize(360, 300);
        f.setLocation(200, 100);
        f.setVisible(true);
    }

    public static JPanel buildPanel() {
        GridBagConstraints gbc = new GridBagConstraints();
        gbc.insets = new Insets(2, 1, 2, 1);
        gbc.weightx = 1.0;
        gbc.weighty = 1.0;
        JPanel p1 = new JPanel(new GridBagLayout());
        p1.setBackground(Color.blue);
        gbc.gridwidth = GridBagConstraints.RELATIVE;
        p1.add(new JButton("button 1"), gbc);
        gbc.gridwidth = GridBagConstraints.REMAINDER;
        p1.add(new JButton("button 2"), gbc);
        gbc.gridwidth = GridBagConstraints.RELATIVE;
        p1.add(new JButton("button 3"), gbc);
        gbc.gridwidth = GridBagConstraints.REMAINDER;
        p1.add(new JButton("button 4"), gbc);
        return p1;
    }

    private CollapsablePanelTest() {
    }
}

class CollapsablePanel extends JPanel {

    private static final long serialVersionUID = 1L;
    private boolean selected;
    private JPanel contentPanel_;
    private HeaderPanel headerPanel_;

    private class HeaderPanel extends JButton /*JToggleButton //implements MouseListener*/ {

        private static final long serialVersionUID = 1L;
        private String __text;
        private Font __font;
        private BufferedImage open, closed;
        private final int OFFSET = 30, PAD = 5;

        public HeaderPanel(String text) {
            //addMouseListener(this);
            __text = text;
            setText(__text);
            __font = new Font("sans-serif", Font.PLAIN, 12);
            // setRequestFocusEnabled(true);
            setPreferredSize(new Dimension(200, 30));
            int w = getWidth();
            int h = getHeight();
            /*try {
            open = ImageIO.read(new File("images/arrow_down_mini.png"));
            closed = ImageIO.read(new File("images/arrow_right_mini.png"));
            } catch (IOException e) {
            e.printStackTrace();
            }*/
            getModel().addChangeListener(new ChangeListener() {

                @Override
                public void stateChanged(ChangeEvent e) {
                    ButtonModel model = (ButtonModel) e.getSource();
                    if (model.isRollover()) {
                        toggleSelection();
                    } else if (model.isPressed()) {
                        toggleSelection();//for JToggleButton
                    }
                }
            });
        }

        /*@Override
        protected void paintComponent(Graphics g) {
        super.paintComponent(g);
        Graphics2D g2 = (Graphics2D) g;
        g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
        int h = getHeight();
        ///if (selected)
        //g2.drawImage(open, PAD, 0, h, h, this);
        //else
        //g2.drawImage(closed, PAD, 0, h, h, this);
        // Uncomment once you have your own images
        g2.setFont(font);
        FontRenderContext frc = g2.getFontRenderContext();
        LineMetrics lm = font.getLineMetrics(__text, frc);
        float height = lm.getAscent() + lm.getDescent();
        float x = OFFSET;
        float y = (h + height) / 2 - lm.getDescent();
        g2.drawString(__text, x, y);
        }
        @Override
        public void mouseClicked(MouseEvent e) {
        toggleSelection();
        }

        @Override
        public void mouseEntered(MouseEvent e) {
        }

        @Override
        public void mouseExited(MouseEvent e) {
        }

        @Override
        public void mousePressed(MouseEvent e) {
        }

        @Override
        public void mouseReleased(MouseEvent e) {
        }*/
    }

    public CollapsablePanel(String text, JPanel panel) {
        super(new GridBagLayout());
        GridBagConstraints gbc = new GridBagConstraints();
        gbc.insets = new Insets(1, 3, 0, 3);
        gbc.weightx = 1.0;
        gbc.fill = GridBagConstraints.HORIZONTAL;
        gbc.gridwidth = GridBagConstraints.REMAINDER;
        selected = false;
        headerPanel_ = new HeaderPanel(text);
        setBackground(Color.orange);
        contentPanel_ = panel;
        add(headerPanel_, gbc);
        add(contentPanel_, gbc);
        contentPanel_.setVisible(false);
        JLabel padding = new JLabel();
        gbc.weighty = 1.0;
        add(padding, gbc);
    }

    public void toggleSelection() {
        selected = !selected;
        if (contentPanel_.isShowing()) {
            contentPanel_.setVisible(false);
        } else {
            contentPanel_.setVisible(true);
        }
        validate();
        headerPanel_.repaint();
    }
}