【问题标题】:Focus owner temporary changes to null焦点所有者临时更改为 null
【发布时间】:2011-09-10 14:13:36
【问题描述】:

我是 Swing 开发的新手,希望我的问题不是愚蠢的。

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

当焦点位于其中一个面板或其子面板内时,我当前的 UI 逻辑正在对控件进行一些更改。然而,获得中级 null 打破了这个逻辑。

我在 Google 中搜索了有关此问题的信息,但没有找到任何相关信息。

问题是,这种行为是否是设计使然,以及是否有办法解决中间空值。

这是重现上述行为的最小应用程序:

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,(已跳过)]
permanentFocusOwner 更改为:null
permanentFocusOwner 更改自:null
PermanentFocusOwner 改为:javax.swing.JButton[,41,18,41x26, (skipped)]


(可选部分,关于代码意图)
我的目标是制作看起来像列表视图的东西,其中条目在获得焦点时展开并显示更多信息(并在失去焦点时折叠回来)。展开的视图包含一些额外的按钮。

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

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

【问题讨论】:

  • +1 表示sscce,但有关目标的更多详细信息可能会为更好的解决方案提供信息。
  • @trashgod:添加了一些关于代码意图的细节

标签: java swing focus


【解决方案1】:

KeyboardFocusManager 正在为大多数属性触发两个事件(根据 bean 规范,它不应该 - 从未发现原因,只是猜测焦点的异步性质可能是原因)

   firePropertyChange(someProperty, oldValue, null)
   firePropertyChange(someProperty, null, newValue)

根据 newVaue 做事,等待第二个

【讨论】:

  • 我不禁猜测这是由于窗口获得焦点的方式跨平台变化的结果。
【解决方案2】:

作为解决方法,将最后一个“真正的”先前焦点所有者作为成员存储在您的事件处理程序中。

if ((e.getOldValue() != null) && (e.getNewValue() == null))
 prev_owner = e.getOldValue();

然后,当您的焦点实际落在目标上时,您将拥有该对象的句柄。仅在真正的组件实际获得焦点时(即当getNewValue() 为非空时)处理突出显示的更改。

(该行为似乎与The AWT Focus Subsystem中描述的一致,因为前一个组件首先失去焦点,然后目标组件得到它。它不是原子的,所以有一段时间实际上什么都没有有重点。但我不是专家,所以这可能会有所不同。)

【讨论】:

    【解决方案3】:

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

    另一种选择,我有时使用JSplitPane:在左侧,我将(可聚焦的)展开按钮放在面板的JTableOutline 或垂直Box 中;在右边,我放了展开的视图。

    【讨论】:

    • 感谢您的建议!虽然这会导致不同的 UI,但它绝对是一个不错的设计。 (而且我之前没有听说过Outline。)
    • 顺便问一下,有没有一种简单的方法可以将 JPanel/Box 子项绑定到模型,就像 JTable 的项目可以绑定到 TableModel 一样?
    • 我通常在JPanel 构造函数中给按钮一个ActionactionPerformed() 方法查询应用程序的数据模型并更新展开的视图。 Action 可以从按钮、菜单、工具栏或焦点侦听器调用。
    • @trashgod 我在考虑这个问题,非常棒的建议,但被我引用了......,然后我以我所知道的最古老的想法结束,请参阅我的帖子,之前已投票 +1,跨度>
    • @trashgod:我目前正在为我的模型对象使用SwingPropertyChangeSupport。它似乎封装了EventListenersList。好吧,在 Swing 中为 每个 控件创建一个抽象模型会非常好,但我不反对手动构建一些东西——至少这会让我理解 TableModel 和其他现有模型有效。 (我有 .NET/WPF/MVVM 的经验,这似乎大致相同。)
    【解决方案4】:
    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 ()

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

    【讨论】:

    • 非常感谢您的代码!但是,我看到一些关于我想要实现的问题。首先,扩展是通过按钮激活来切换的。这与用户使用鼠标获得焦点相同,但如果焦点是通过键盘设置焦点(例如,通过 Tab 键),则不会激活按钮。其次,面板应在焦点离开时立即折叠。
    • 如果没有 MouseListener 那么 ButtonModel 对我来说适用于 javax.swing.Timer (用于延迟显示,或者是否有多个动作,或者来自最近的 JComponents 的可能并发)和 KeyBinding (不是TAB,但F1,F2或F5)可以解决这个问题,但我仍然认为将JWindow实现为Container而不是跳转JPanel会更好
    • 我不确定我是否理解您提到的确切 MouseListener。为了使 ButtonModel 工作,我们需要确保按钮处于按下状态,只要焦点位于按钮或展开面板中的任何项目上——有没有简单的方法呢?现在,我只是像示例中一样跟踪焦点,另外忽略对 null 的更改。 UI 不是我定义的,所以跳板似乎在所难免。
    • 再试一次:您的代码在鼠标悬停时折叠/取消折叠内容,而不是在焦点上。我最初的问题是检测焦点何时离开控件及其所有子控件;已经彻底解决了这个问题,我将能够按照您建议的方式切换内容。
    • that the button is in pressed state as long as the focus is on the button 辛苦 - 如果是滚动,则显示容器,立即删除 ButtonModel 并放置 MouseMotionListener,然后如果光标移出 JButton 和容器的 getBounds,则启动 Timer(延迟 500-750 毫秒)用于隐藏 Container 并添加 ButtonModel,如果 Cursor 返回就停止 Timer,只定义 Timer#setrepeat(false)
    猜你喜欢
    • 2014-03-05
    • 2014-07-23
    • 1970-01-01
    • 2017-03-30
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-04-08
    相关资源
    最近更新 更多