понедельник, 10 октября 2016 г.

VAnavigation — Навигация в JavaFX

Как оказалось, JavaFX не обладает удобным инструментом, чтобы реализовать навигацию между окнами. Но это решается не сложно. Преследуемая цель:


  • минимум программного кода чтобы открыть новый экран
  • возможность перемещаться по экранам назад
  • передать данные между окнами

Вариантов реализовать подобный инструмент несколько. Почему выбран именно этот — об этом ниже.

Для тех, кто привык воспринимать через схемы:

Каждый экран должен реализовывать интерфейс Controller (Controller.java).


public interface Controller {
    Node getView();
    void setView(Node view);

    void Show();
}

Изучение JavaFX, начинается с рассказа о том, что бывает файл разметки FXML и сопровождающий его класс контроллера на языке Java. Реализация нашего интерфейса, позволит задать (setView) и получить(getView) корневой элемент разметки файла FXML. Метод Show — отобразит содержимое fxml файла.

Далее, базовый для всех наших экранов класс BaseController (BaseController.java)

public class BaseController implements Controller {

    private Node view;

    @Override
    public Node getView() {
        return view;
    }

    @Override
    public void setView (Node view){
        this.view = view;
    }

    @Override
    public void Show() {
        PreShowing();
        Main.getNavigation().Show(this);
        PostShowing();
    }

    public void PreShowing()
    {
    }

    public void PostShowing()
    {
    }
}
Этот класс и берет на себя реализацию интерфейса Controller. Методы PreShowing() и PostShowing() дадут нам возможность сделать что-либо на нашем экране перед открытием или после открытия.

Каждый контроллер экрана, необходимо наследовать от BaseController:

public class View1 extends BaseController implements Initializable {
...    
}

А теперь о том как работает навигация — файл Navigation.java:

public class Navigation {

    private final Stage stage;
    private final Scene scene;

    private List<Controller> controllers = new ArrayList<>();


    public Navigation(Stage stage)
    {
        this.stage = stage;
        scene = new Scene(new Pane());
        stage.setScene(scene);
    }

    public Controller load(String sUrl)
    {
        try {

            //loads the fxml file
            FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource(sUrl));
            Node root = fxmlLoader.load();

            Controller controller = fxmlLoader.getController();
            controller.setView(root);

            return controller;

        } catch(Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    public void Show(Controller controller)
    {
        try {
            scene.setRoot((Parent) controller.getView());
            controllers.add(controller);

            System.out.println("Add to history: " + controller.toString() + ". Total scenes: " + controllers.size());
        }
        catch(Exception e) {
            e.printStackTrace();
        }
    }

    public void GoBack()
    {
        if (controllers.size() > 1)
        {
            controllers.remove(controllers.get(controllers.size() - 1));
            scene.setRoot((Parent) controllers.get(controllers.size() - 1).getView());
        }

        System.out.println("GoBack: " + controllers.get(controllers.size() - 1).toString() + ". Total scenes: " + controllers.size());
    }


    public void ClearHistory()
    {
        while (controllers.size() > 1)
            controllers.remove(0);

        System.out.println("ClearHistory. Total scenes: " + controllers.size());
    }
}
При создании объекта Navigation, указывается Stage — это то что принято называть окном. В JavaFX каждому Stage может быть задана Scene — сцена — область внутри Stage. В этой области будет рисоваться содержимое файла разметки fxml. В нашем случае, переключение между экранами происходит на одной сцене — будем подменять то, что должно отображаться на сцене (в окне).

Чтобы возвращаться назад, нам понадобиться история переходов по окнам. Используем для этого список:

private List<Controller> controllers = new ArrayList<>();
Когда нам нужно будет сменить экран, нам останется сделать 2 шага:


  1. вызвать метод load, чтобы загрузить файл разметки FXML
  2. вызвать метод Show, который заменит содержимое сцены и добавит контроллер в список для истории.

Можно использовать класс приложения, чтобы всякий раз иметь возможность получать доступ к объекту навигации. (Main.java)

public class Main extends Application {

    private static Navigation navigation;
    public static Navigation getNavigation()
    {
        return navigation;
    }

    @Override
    public void start(Stage primaryStage) throws Exception{
        navigation = new Navigation(primaryStage);

        primaryStage.setTitle("VA navigation");
        primaryStage.show();

        //navigate to first view
        Main.getNavigation().load(View1.URL_FXML).Show();
    }

    public static void main(String[] args) {
        launch(args);
    }
}
И вот теперь в любом месте достаточно написать


Main.getNavigation().load(View1.URL_FXML).Show();
и произойдет смена экрана.

Передача параметров между контроллерами.


Допустим необходимо чтобы контроллер View2 передал строку текста контроллеру View3. Для этого, объявляем в контроллере View3:

private String parameterFromView2;

public String getParameterFromView2() {
    return parameterFromView2;
}

public void setParameterFromView2(String parameterFromView2) {
    this.parameterFromView2 = parameterFromView2;
}
И когда будем вызвать View3 из View2 немного изменим вызов:

View3 view3 = (View3)Main.getNavigation().load(View3.URL_FXML);
view3.setParameterFromView2(txtParameter.getText());
view3.Show();
Очень часто, получаемые в контроллерах параметры должны быть использованы для тех или иных действий перед отображением на экране. Именно для этого, в базовом классе, предусмотрена функция:

@Override
public void PreShowing() {
    super.PreShowing();
    lblParameter.setText(getParameterFromView2());
}


Почему именно так.

Почему именно смена содержимого сцены? Потому что опробованные 2 других способа имеют недостатки.

1 способ. Если при навигации, мы бы использовали принцип: один Stage — один контроллер, то конечно бы у нас получилось все хорошо. Но что если экранов много? Это можно сравнить с просмотром некоторого сайта — когда каждая ссылка открывается в новом окне. Но при таком подходе обнаружились проблемы при отображении окон в полноэкранном режиме. Временами новое окно отображалось просто белым и причину этого, к сожалению, так и не удалось найти.

2 способ. Что если менять сцены в одном окне? Этот способ, хоть и работает, но имеет одну неприятную особенность — при смене сцены происходит некрасивое переключение — быстрое, но не приятное. Насколько удалось заметить, в этот момент окно просто исчезает на мгновение и потом появляется уже с новой сценой.

Готовый файлы можно скачать отсюда (VAnavigation)



2 комментария: