JavaFX:如何更改焦点遍历策略?

Posted

技术标签:

【中文标题】JavaFX:如何更改焦点遍历策略?【英文标题】:JavaFX: How to change the focus traversal policy? 【发布时间】:2013-02-20 17:55:41 【问题描述】:

在 JavaFX 中是否可以像在 AWT 中那样更改 焦点遍历策略

因为我的两个HBoxes 的遍历顺序是错误的。

【问题讨论】:

你可以通过调用node.requestFocus()方法为javafx中的任意节点请求焦点 是的,我知道,但这不是一个很好的解决方案。 请明确您的问题并提供sscce。 最好将控件分组到“遍历组”中并设置选项卡索引。如果此遍​​历组中的某个控件具有焦点,则按下 [Tab]-Key 会强制具有下一个更高索引的控件成为焦点。类似于按 [Tab] + [Shift] 和下一个较低的索引。 @gontard,提出问题,以便有答案。 Sonja 在问,是否可以自定义焦点遍历方式。 【参考方案1】:

最简单的解决方案是编辑 FXML 文件并适当地重新排序容器。例如,我当前的应用程序有一个注册对话框,可以在其中输入序列号。为此目的有 5 个文本字段。为了使焦点正确地从一个文本字段传递到另一个文本字段,我必须以这种方式列出它们:

<TextField fx:id="tfSerial1" layoutX="180.0" layoutY="166.0" prefWidth="55.0" />
<TextField fx:id="tfSerial2" layoutX="257.0" layoutY="166.0" prefWidth="55.0" />
<TextField fx:id="tfSerial3" layoutX="335.0" layoutY="166.0" prefWidth="55.0" />
<TextField fx:id="tfSerial4" layoutX="412.0" layoutY="166.0" prefWidth="55.0" />
<TextField fx:id="tfSerial5" layoutX="488.0" layoutY="166.0" prefWidth="55.0" />

【讨论】:

是的,这是最简单的方法。我遇到过同样的问题。这意味着在我们设计 UI Scene Builder 时,会按附加顺序重新生成相关的 FXML 标签。一旦我们重新排列它们正确的顺序,它就可以正常工作了。 如果 SB 能自动为我们做这件事就好了。 这绝对是最简单的方法。谢谢。【参考方案2】:

Bluehair 的回答是对的,但你甚至可以在 JavaFX Scene Builder 中做到这一点。

您在左栏中有层次结构面板。场景中有你所有的组件。它们的顺序代表焦点遍历顺序,它响应它们在 FXML 文件中的顺序。

我在这个网页上找到了这个提示:www.wobblycogs.co.uk

【讨论】:

【参考方案3】:

通常情况下,导航是按容器顺序、子项顺序或按箭头键完成的。您可以更改节点的顺序 - 这将是您在这种情况下的最佳解决方案。

JFX 中有一个关于遍历引擎策略替换的后门:

你可以继承内部类 com.sun.javafx.scene.traversal.TraversalEngine

engine = new TraversalEngine(this, false) 
            @Override public void trav(Node owner, Direction dir) 
                // do whatever you want
            
        ;

并使用

setImpl_traversalEngine(engine); 

调用以应用该引擎。

您可以观察 OpenJFX 的代码,了解它是如何工作的,以及您可以做什么。

要非常小心:它是一个内部 API,并且很可能在不久的将来发生变化。所以不要依赖这个(无论如何你不能依赖这个官方)。

示例实现:

public void start(Stage stage) throws Exception 
    final VBox vb = new VBox();

    final Button button1 = new Button("Button 1");
    final Button button2 = new Button("Button 2");
    final Button button3 = new Button("Button 3");

    TraversalEngine engine = new TraversalEngine(vb, false) 
        @Override
        public void trav(Node node, Direction drctn) 
            int index = vb.getChildren().indexOf(node);

            switch (drctn) 
                case DOWN:
                case RIGHT:
                case NEXT:
                    index++;
                    break;
                case LEFT:
                case PREVIOUS:
                case UP:
                    index--;
            

            if (index < 0) 
                index = vb.getChildren().size() - 1;
            
            index %= vb.getChildren().size();

            System.out.println("Select <" + index + ">");

            vb.getChildren().get(index).requestFocus();
        
    ;

    vb.setImpl_traversalEngine(engine);

    vb.getChildren().addAll(button1, button2, button3);
    Scene scene = new Scene(vb);
    stage.setScene(scene);
    stage.show();

一般情况下需要强大的分析技能;)

【讨论】:

fx8 中的内部 api 似乎发生了很大变化,不再可能(可能需要通过实现自定义算法进行调整,用它配置 ParentFocusTraversalEngine)【参考方案4】:

我们为此使用JavaFX event filters,例如:

cancelButton.addEventFilter(KeyEvent.KEY_PRESSED, new EventHandler<KeyEvent>() 
    @Override
    public void handle(KeyEvent event) 
        if (event.getCode() == KeyCode.TAB && event.isShiftDown()) 
            event.consume();
            getDetailsPane().requestFocus();
        
    
);

event.consume() 会抑制默认的焦点遍历,否则会在调用requestFocus() 时造成麻烦。

【讨论】:

这是最简单最好的。谢谢。 在我看来,这是唯一可持续的答案。我们不调用内部 API 并且在 FXML 中重新排列节点并不总是可能的,例如如果我们想使用 Java 动态添加节点。 即使对于 GirdPane 中的控件以及使用 Java 代码动态生成的控件也是最合适的答案【参考方案5】:

这是适应内部 api 更改的公认答案(发生在 fx-8 的某个时间点,我当前的版本是 8u60b5)。显然原来的免责声明仍然适用:它是 internal api,随时开放更改,恕不另行通知!

变化(与接受的答案相比)

Parent 需要一个 ParentTraversalEngine 类型的 TraversalEngine nav 不再是 TraversalEngine(也不是 ParentTE)的方法,而只是 TopLevelTraversalEngine 的方法 导航实现委托给称为算法的策略 实际焦点转移(似乎是?)由 TopLevelTE 处理,算法只找到并返回新目标

示例代码的简单翻译:

/**
 * Requirement: configure focus traversal
 * old question with old hack (using internal api):
 * http://***.com/q/15238928/203657
 * 
 * New question (closed as duplicate by ... me ..)
 * http://***.com/q/30094080/203657
 * Old hack doesn't work, change of internal api
 * rewritten to new internal (sic!) api
 * 
 */
public class FocusTraversal extends Application 

    private Parent getContent() 
        final VBox vb = new VBox();

        final Button button1 = new Button("Button 1");
        final Button button2 = new Button("Button 2");
        final Button button3 = new Button("Button 3");

        Algorithm algo = new Algorithm() 

            @Override
            public Node select(Node node, Direction dir,
                    TraversalContext context) 
                Node next = trav(node, dir);
                return next;
            

            /**
             * Just for fun: implemented to invers reaction
             */
            private Node trav(Node node, Direction drctn) 
                int index = vb.getChildren().indexOf(node);

                switch (drctn) 
                    case DOWN:
                    case RIGHT:
                    case NEXT:
                    case NEXT_IN_LINE:    
                        index--;
                        break;
                    case LEFT:
                    case PREVIOUS:
                    case UP:
                        index++;
                

                if (index < 0) 
                    index = vb.getChildren().size() - 1;
                
                index %= vb.getChildren().size();

                System.out.println("Select <" + index + ">");

                return vb.getChildren().get(index);
            

            @Override
            public Node selectFirst(TraversalContext context) 
                return vb.getChildren().get(0);
            

            @Override
            public Node selectLast(TraversalContext context) 
                return vb.getChildren().get(vb.getChildren().size() - 1);
            

        ;
        ParentTraversalEngine engine = new ParentTraversalEngine(vb, algo);
        // internal api in fx8
        // vb.setImpl_traversalEngine(engine);
        // internal api since fx9
        ParentHelper.setTraversalEngine(vb, engine);
        vb.getChildren().addAll(button1, button2, button3);
        return vb;
    

    @Override
    public void start(Stage primaryStage) throws Exception 
        primaryStage.setScene(new Scene(getContent()));
        primaryStage.show();
    

    public static void main(String[] args) 
        launch(args);
    

【讨论】:

查看 ContainerTabOrder 以获取完整算法的示例(需要在子级中递归)。但这对我来说太复杂了,我使用了一种解决方法来忽略选项卡策略中的按钮,在没有 java 代码的 FXML 中:设置focusTraversable="false"。它将继续到下一个可聚焦的元素。 从 Java 9 开始,所有这些内部 API 都没有被导出,并且这个后门不再可用。 如果您被允许修改封装,它们仍然可用 - 这与以前版本中的脏度大致相同:) 嗯 .. 里程有所不同,但 .. 添加出口/打开并不是什么大问题,IMO :) 不讨论自定义 fx 分发 - 只需根据需要使用 --add-exports / --add-opens 修改构建/运行时路径 ...【参考方案6】:

在场景构建器中,转到视图菜单并选择显示文档。左侧将是当前 fxml 文档中的所有对象。在列表中向上或向下拖动控件以重新排序选项卡索引。选择隐藏文档以使用其他工具,因为文档窗格占用空间。

【讨论】:

【参考方案7】:

我使用 Eventfilter 作为解决方案并结合引用字段 ID,因此您只需将字段命名为 (field1,field2,field3,field4),以便您可以将字段放置在您想要的位置:

mainScene.addEventFilter(KeyEvent.KEY_PRESSED, (event) -> 
        if(event.getCode().equals(KeyCode.TAB))
            event.consume();
            final Node node =  mainScene.lookup("#field"+focusNumber);
            if(node!=null)
                node.requestFocus();
            
            focusNumber ++;
            if(focusNumber>11)
              focusNumber=1;
            
        
    ); 

【讨论】:

这是我的通用解决方案的核心。所有 Node 对象都有一个 id,每当我创建一个可聚焦元素时,我也会设置 id。然后,我创建了一个简单的列表,其中包含我想要包含在焦点循环中的元素的 ID 和节点对象的 Map。然后我将获取关键事件的目标 (ke.getTarget()) 获取它的 id,在 id 列表中找到该 id 的索引,根据需要获取上一个/下一个 id 并在 Map 中查找并调用 requestFocus ()关于那个。可能有更好的方法来优化这一点,这很容易理解 - 我认为:)【参考方案8】:

您可以使用 SceneBuilder 轻松完成此操作。使用 scenebuilder 打开 fxml 文件,然后转到 Document 选项卡下的 hierarchy。通过拖动输入控件将输入控件放置到您想要的顺序。记得在属性中检查焦点遍历。然后,当您按下标签栏时,焦点遍历策略将很好地工作。

【讨论】:

【参考方案9】:

如上所述,您可以使用NodeName.requestFocus();此外,请确保在为根布局实例化并添加所有节点后请求此焦点,因为这样做时,焦点将发生变化。

【讨论】:

【参考方案10】:

我遇到了一个问题,其中 JavaFX 滑块正在捕获我希望通过我的方法 keyEventHandler(它处理场景的键事件)处理的左右箭头键击。对我有用的是将以下行添加到初始化 Slider 的代码中:

slider.setOnKeyPressed(keyEventHandler);

添加

keyEvent.consume();

keyEventHandler的末尾。

【讨论】:

【参考方案11】:

受 Patrick Eckert 的回答启发的通用解决方案。

当我创建 UI 时,例如添加 TextField,我会这样设置:

List<String> displayOrder;
Map<String, Node> cycle;

TextField tf = new TextField();
tf.setId("meTF");
cycle.put("meTF", tf);
displayOrder.add("meTF");
getChildren().add(tf);

然后在 UI 元素(通常是布局)上添加:

ui.addEventFilter(KeyEvent.KEY_PRESSED, (ke) ->

    if(!ke.getCode().equals(KeyCode.TAB) || !(ke.getTarget() instanceof Node))
        return;
            
    int i = displayOrder.indexOf(((Node)ke.getTarget()).getId());
            
    if(i < 0) // can't find it
        return;
            
    if(ke.isShiftDown())
        i = (i == 0 ? displayOrder.size() - 1 : i - 1);
    else
        i = ++i % displayOrder.size();
            
    cycle.get(displayOrder.get(i)).requestFocus();
            
    ke.consume();
);

仅供参考,我认为仅在您实际上要调用请求焦点时才使用该事件很重要。不太可能以这种方式无意中破坏某些东西......

如果有人能想出进一步优化的方法,我将不胜感激:)

【讨论】:

以上是关于JavaFX:如何更改焦点遍历策略?的主要内容,如果未能解决你的问题,请参考以下文章

动态访问/遍历/操作从控制器类外部的 FXML 创建的 JavaFX 节点

有没有办法改变TableView的焦点可遍历行为?

Javafx 。如何获得下一个元素非焦点丢失

遍历 ImageButtons 给予焦点 Android

第27天:表单获取焦点和数组声明遍历

如何遍历字典并更改值?