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

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

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

【问题讨论】:

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

标签: java javafx focus javafx-2 javafx-8


【解决方案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://stackoverflow.com/q/15238928/203657
     * 
     * New question (closed as duplicate by ... me ..)
     * http://stackoverflow.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();
            });
            

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

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

            【讨论】:

              猜你喜欢
              • 2013-03-01
              • 1970-01-01
              • 2014-10-04
              • 1970-01-01
              • 2017-12-17
              • 1970-01-01
              • 1970-01-01
              • 2012-06-21
              • 2012-01-23
              相关资源
              最近更新 更多