jTabbedPane将焦点转移到下一个选项卡。

6

问题描述

我正在编写一个应用程序,用于将纸张上的数据手动复制到数据库中。该应用程序有很多小部件,用户可以在其中输入数据。为了使界面看起来整洁一些,我决定使用选项卡窗格,将输入字段分成逻辑单元。

该应用程序最重要的特性是它应该可以仅通过键盘使用。因此,您应该能够使用 CTRL+PgUp/PgDown 快捷键切换选项卡。但是,为了额外的方便,当用户将焦点从当前选项卡的最后一个小部件转移出去时,我希望立即激活下一个选项卡。

因此,如果用户将焦点放在最后一个文本字段上,并按下 Tab 键,我希望激活下一个选项卡,并将焦点放在其中的第一个小部件上。

为了解决这个问题,我将 jTabbedPane 标记为 focusCycleRootProvider 并添加了自定义的 FocusTraversalPolicy。我的当前问题是:当我以编程方式激活下一个选项卡(使用 setSelectedIndex)时,在 getComponentAfter 方法中会执行第二次 getComponentAfter 方法。这破坏了我的当前逻辑。我似乎找不到防止这种情况发生的方法。有什么想法吗?

在下面的示例中,您将看到一个 ArrayIndexOutOfBoundsException。这是因为 getComponentAfter 在第一个选项卡和第二个选项卡上分别调用了两次。但是两次使用相同的小部件作为参数。这意味着第二次时,for 循环将无法找到匹配的组件,因此计数器 i 将与第二个选项卡中的组件数量 +1 一样大。这会导致异常。

测试可执行文件

/*
 * TestFrame.java
 *
 * Created on Apr 18, 2011, 4:37:52 PM
 */

package testrun;

import java.awt.Component;
import java.awt.event.FocusAdapter;
import java.awt.event.FocusEvent;

/**
 *
 * @author malbert
 */
public class TestFrame extends javax.swing.JFrame {

    /** Creates new form TestFrame */
    public TestFrame() {
        initComponents();
        jTabbedPane1.setFocusTraversalPolicyProvider(true);
        jTabbedPane1.setFocusTraversalPolicy(new EasyTabberFocusTraversalPolicy(jTabbedPane1));

        jTabbedPane1.addFocusListener(new FocusAdapter() {

            @Override
            public void focusGained(FocusEvent e) {
                super.focusGained(e);
                jTabbedPane1.setSelectedIndex(0);
                Component ca = jTabbedPane1.getFocusTraversalPolicy().getFirstComponent(jTabbedPane1);
                if (ca != null) {
                    ca.requestFocusInWindow();
                }
            }
        });
    }

    /** This method is called from within the constructor to
     * initialize the form.
     * WARNING: Do NOT modify this code. The content of this method is
     * always regenerated by the Form Editor.
     */
    @SuppressWarnings("unchecked")
    // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents
    private void initComponents() {

        jTabbedPane1 = new javax.swing.JTabbedPane();
        jPanel1 = new javax.swing.JPanel();
        jTextField2 = new javax.swing.JTextField();
        jTextField3 = new javax.swing.JTextField();
        jPanel2 = new javax.swing.JPanel();
        jButton1 = new javax.swing.JButton();
        jButton2 = new javax.swing.JButton();
        jTextField4 = new javax.swing.JTextField();
        jTextField1 = new javax.swing.JTextField();

        setDefaultCloseOperation(javax.swing.WindowConstants.EXIT_ON_CLOSE);

        jTextField2.setText("jTextField2");

        jTextField3.setText("jTextField3");

        javax.swing.GroupLayout jPanel1Layout = new javax.swing.GroupLayout(jPanel1);
        jPanel1.setLayout(jPanel1Layout);
        jPanel1Layout.setHorizontalGroup(
            jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
            .addGroup(jPanel1Layout.createSequentialGroup()
                .addContainerGap()
                .addGroup(jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
                    .addComponent(jTextField2, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
                    .addComponent(jTextField3, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE))
                .addContainerGap(289, Short.MAX_VALUE))
        );
        jPanel1Layout.setVerticalGroup(
            jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
            .addGroup(jPanel1Layout.createSequentialGroup()
                .addContainerGap()
                .addComponent(jTextField2, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                .addComponent(jTextField3, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
                .addContainerGap(168, Short.MAX_VALUE))
        );

        jTabbedPane1.addTab("tab1", jPanel1);

        jButton1.setText("jButton1");

        jButton2.setText("jButton2");

        jTextField4.setText("jTextField4");

        javax.swing.GroupLayout jPanel2Layout = new javax.swing.GroupLayout(jPanel2);
        jPanel2.setLayout(jPanel2Layout);
        jPanel2Layout.setHorizontalGroup(
            jPanel2Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
            .addGroup(jPanel2Layout.createSequentialGroup()
                .addContainerGap()
                .addGroup(jPanel2Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
                    .addGroup(jPanel2Layout.createSequentialGroup()
                        .addComponent(jButton1)
                        .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                        .addComponent(jButton2))
                    .addComponent(jTextField4, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE))
                .addContainerGap(165, Short.MAX_VALUE))
        );
        jPanel2Layout.setVerticalGroup(
            jPanel2Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
            .addGroup(jPanel2Layout.createSequentialGroup()
                .addContainerGap()
                .addGroup(jPanel2Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
                    .addComponent(jButton1)
                    .addComponent(jButton2))
                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                .addComponent(jTextField4, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
                .addContainerGap(162, Short.MAX_VALUE))
        );

        jTabbedPane1.addTab("tab2", jPanel2);

        jTextField1.setText("jTextField1");

        javax.swing.GroupLayout layout = new javax.swing.GroupLayout(getContentPane());
        getContentPane().setLayout(layout);
        layout.setHorizontalGroup(
            layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
            .addGroup(layout.createSequentialGroup()
                .addContainerGap()
                .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
                    .addComponent(jTabbedPane1, javax.swing.GroupLayout.DEFAULT_SIZE, 376, Short.MAX_VALUE)
                    .addComponent(jTextField1, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE))
                .addContainerGap())
        );
        layout.setVerticalGroup(
            layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
            .addGroup(layout.createSequentialGroup()
                .addContainerGap()
                .addComponent(jTextField1, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                .addComponent(jTabbedPane1, javax.swing.GroupLayout.DEFAULT_SIZE, 251, Short.MAX_VALUE)
                .addContainerGap())
        );

        pack();
    }// </editor-fold>//GEN-END:initComponents

    /**
    * @param args the command line arguments
    */
    public static void main(String args[]) {
        java.awt.EventQueue.invokeLater(new Runnable() {
            public void run() {
                new TestFrame().setVisible(true);
            }
        });
    }

    // Variables declaration - do not modify//GEN-BEGIN:variables
    private javax.swing.JButton jButton1;
    private javax.swing.JButton jButton2;
    private javax.swing.JPanel jPanel1;
    private javax.swing.JPanel jPanel2;
    private javax.swing.JTabbedPane jTabbedPane1;
    private javax.swing.JTextField jTextField1;
    private javax.swing.JTextField jTextField2;
    private javax.swing.JTextField jTextField3;
    private javax.swing.JTextField jTextField4;
    // End of variables declaration//GEN-END:variables

}

Traversal Policy

package testrun;

import java.awt.Component;
import java.awt.Container;
import javax.swing.JTabbedPane;
import javax.swing.LayoutFocusTraversalPolicy;

/**
 *
 * @author malbert
 */
public class EasyTabberFocusTraversalPolicy extends LayoutFocusTraversalPolicy {

    private final JTabbedPane container;
    private int currentTab = 0;

    public EasyTabberFocusTraversalPolicy(JTabbedPane container) {
        this.container = container;
    }

    @Override
    public Component getComponentAfter(Container aContainer, Component aComponent) {
        System.out.println("after " + aComponent);
        Component comp = container.getComponentAt(currentTab);
        if (Container.class.isInstance(comp)) {
            Component[] components = ((Container) comp).getComponents();
            int i = 0;
            for (i = 0; i < components.length; i++) {
                if (!components[i].isEnabled() || !components[i].isFocusable()) {
                    continue;
                }
                if (components[i].equals(aComponent)) {
                    break;
                }
            }
            if (i == components.length - 1) {
                // we reached the end. Go to the next tab!
                currentTab = currentTab + 1;
                Component fc = firstComponentInCurrentTab();
                activateTab(currentTab);
                return fc;
            } else {
                return components[i + 1];
            }
        } else {
            return comp;
        }
    }

    @Override
    public Component getComponentBefore(Container aContainer, Component aComponent) {
        System.out.println("before");
        return super.getComponentBefore(aContainer, aComponent);
    }

    @Override
    public Component getFirstComponent(Container aContainer) {
        System.out.println("first");
        return firstComponentInCurrentTab();
    }

    @Override
    public Component getLastComponent(Container aContainer) {
        System.out.println("last");
        return lastComponentInCurrentTab();
    }

    private Component firstComponentInCurrentTab() {
        Component comp = container.getComponentAt(currentTab);
        if (comp instanceof Container) {
            Component[] components = ((Container) comp).getComponents();
            if (components.length == 0) {
                return null;
            }
            return components[0];
        } else {
            return comp;
        }
    }

    private Component lastComponentInCurrentTab() {
        Component comp = container.getComponentAt(currentTab);
        if (comp instanceof Container) {
            Component[] components = ((Container) comp).getComponents();
            if (components.length == 0) {
                return null;
            }
            return components[components.length - 1];
        } else {
            return comp;
        }
    }

    private void activateTab(int index) {
        // wrap around
        if (index < 0) {
            index = container.getTabCount() - 1;
        } else if (index > container.getTabCount() - 1) {
            index = 0;
        }
        currentTab = index;
        container.setSelectedIndex(index);
    }
}

1
EasyTabberFocusTraversalPolicy 降级为默认访问权限,并将其放在 TestFrame 源代码的末尾,以使 SSCCE 更容易编译和运行(这是我的建议)。 - Andrew Thompson
我曾经在Delphi的VCL中遇到了完全相同的问题,由于VCL没有暴露足够的内容来做你们在Swing中扩展LayoutFocusTraversalPolicy所做的高级操作,因此我创建了一个(抱歉:完全全局的)事件嗅探器,它捕获制表符,然后查看焦点组件是否是多选项卡页面控件中第一个/最后一个可聚焦组件,如果是,则强制将上一页/下一页上的最后一个/第一个可聚焦组件聚焦。这个方法效果很好,代码紧凑,只包含一个事件处理程序。在Swing中,不能使用事件监听器完成这个操作吗? - TheBlastOne
我必须承认,对于嵌套页面控件,需要进行一些微调才能正常工作。 - TheBlastOne
1个回答

2

FTP是一种让人头疼的协议,它真正让人难受的地方在于其操作方式;-)

不太确定什么导致了NPE,只是对你的代码进行了简化(如果有一个方法可以访问最后一个元素,最好使用它),以便更好地测试:

@Override
public Component getComponentAfter(Container aContainer, Component aComponent) {
    System.out.println("after " + aComponent);
    Component last = lastComponentInCurrentTab();
    if (aComponent == last) {
      // we reached the end. Go to the next tab!
      currentTab = currentTab + 1;
      Component fc = firstComponentInCurrentTab();
      activateTab(currentTab);
      return fc;

    }
    return super.getComponentAfter(aContainer, aComponent);
}

AIOOB已经消失,第二个选项卡已显示...但没有焦点。这种情况发生的原因是(仅猜测,因为我在其他设置中也看到了类似的问题,但没有记住所有的肮脏细节 ;)),在返回第二个选项卡的第一个组件时,它还没有资格获得焦点,因此它进一步转移到选项卡外的文本。编辑:不得不修正我的猜测-经过一番挖掘,问题似乎是错误地覆盖了getFirst/LastComponent。它们必须返回所有选项卡的第一个/最后一个,即第一个始终是第一个选项卡的第一个,最后一个始终是最后一个选项卡的最后一个。以下是第一个的代码片段:
@Override
public Component getFirstComponent(Container aContainer) {
    System.out.println("first");
    return firstComponentInTab(0) ;//firstComponentInCurrentTab();
}

private Component firstComponentInCurrentTab() {
    int tabIndex = currentTab;
    return firstComponentInTab(tabIndex);
}

private Component firstComponentInTab(int tabIndex) {
    Component comp = container.getComponentAt(tabIndex);
    LOG.info("comp: " + comp.getName());
    if (comp instanceof Container) {
        Component[] components = ((Container) comp).getComponents();
        if (components.length == 0) {
            return null;
        }
        return components[0];
    } else {
        return comp;
    }
}

然后向前切换看起来很好。最后也需要类似的操作,但清理工作留给OP处理 :-)
顺便说一句:好主意!
编辑2:请注意-清理工作并不容易。 FTP必须处理其子代的完整层次结构,因此仅检查直接子代将很快破坏(例如对于JComboBox等复合组件)。

现在标签已经显示了。但是它的行为有点奇怪。当我不停地按TAB键时,焦点会在容器外的文本字段和容器之间跳转,而不是进入容器。但这可能是焦点监听器的问题。不幸的是,我今天必须离开办公室。明天我会再次深入研究... - exhuma
谢谢。这个有效。这给了我一个可以开始工作的起点 :) 非常感谢。现在我得消化一下,为什么它现在有效,而昨天不行... ;) - exhuma

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