Разработка своего компонента для JavaFX

от автора

На самом деле, JavaFX есть механизм кастомизации уже существующих компонентов (StackPane, Pane, HBox, VBox, ScrollPane и т.д.) с помощью css. css javafx поддерживает псевдоклассы (after, before, hover, focused, presed и так далее). В целом все гибко и можем довольно легко взять любой компонент и превратить его в свое родное.

Но возьмем к примеру задачу реализовать всплывающее окно подсказки. Я столкнулся с такой ситуацией и сначала подумал найти готовое решение но к сожалению не нашел что то более менее подходящей моей цели компонент, который был бы довольно простым для отображения и внутри производил бы расчетов размеров (Bounds). По этому было принято решение реализовать свой компонент тем более компонент редактора написан свой с рендером строк и частей.

В первую очередь мы должны создать класс который унаследован от javafx.scene.control.Control

import javafx.scene.control.Controlpublic class Autocomplit extends Control {    private AutocomplitSkin aSkin;    public Autocomplit() {         aSkin = new AutocomplitSkin(this);         setSkin(aSkin);    }}

В этом коде мы просто создаем свой класс, и устанавливаем Skin компонента. Тем самым логика отображения находится в классе AutocomplitSkin а основная логика компонента находиться в самом классе Control

public class AutocomplitSkin extends SkinBase<Autocomplit>  {    private VBox contentBox;      public AutocomplitSkin(Autocomplit control) {      super(control);      contentBox = new VBox();      Text oneString = new Text("one string");      Text twoString = new Text("two string");            contentBox.getChildren().addAll(oneString, twoString);      getChildren().add(contentBox);    }}

В целом, если мы уже вызовем new Autocomplit() в своем коде и добавим в какой либо компонент он у нас уже отобразиться в интерфейсе. Но будет отрисован с помощью layout встроенный в JavaFX. Но нам нужна планка подсказки которая плавает относительно координат каретки по этому сразу модифицируем код и contentBox открепим от layout’а и вынесем логику из конструктора в отдельный метод в классе обложки (AutocomplitSkin)

// AutocomplitSkin.....private void setupContent() {  Text oneString = new Text("one string");  Text twoString = new Text("two string");  contentBox.getChildren().addAll(oneString, twoString);  getChildren().add(contentBox);}

В классе самого Autocomplit тоже вынесем в отдельный метод логику связанную с математикой размеров компонента

// Autocomplit.....private SimpleDoubleProperty xCoordinate = new SimpleDoubleProperty();private SimpleDoubleProperty yCoordinate = new SimpleDoubleProperty();private void setupComponent() {   setManaged(false);   resize(calculateWidth, calculateHeight); // resize(double width, double height)   setLayoutX(getXPosition());   setLayoutY(getYPosition());}public double getXPosition() { return xCoordinate.get(); }public double getYPosition() { return yCoordinate.get(); }public void setXPosition(double x) { xCoordinate.set(x); }public void setYPosition(double y) { yCoordinate.set(y); }public SimpleDoubleProperty() { return xCoordinate; }public SimpleDuobleProperty() { return yCoordinate; }public void addHandlerX() {  xCoordinate.addListener((e) -> {    aSkin.setX(xCoordinate);  });}public void addHandlerY() {  yCoordinate.addListener((e) -> {    aSkin.setY(yCoordinate);  })}public double calculateWidth() {  // логика расчета ширины компонента  Double result = 0.0;  for (int i = 0; i < aSkin.getContentBox().getChildren().size(); i++) {    Bounds stringBound = aSkin.getContetBox().getChildren().get(i).getBoundsInParent();    if (result < stringBound.width()) {      result = stringBound.getWidth();    }  }  return result;}public double calculateHeight() {  // логика расчета высоты такой же цикл как и в calculateWidth  // единственное отличие собираем сумму высоты\  Double result = 0.0;  for (int i = 0; i < aSkin.getContentBox().getChildren().size(); i++) {    Bounds stringBound = aSkin.getContetBox().getChildren().get(i).getBoundsInParent();    result += stringBound.getHeight();  }  return result;}

Теперь наш компонент может находиться в произвольном месте окна, и его размеры соответствуют размерам контента который он содержит. Важные моменты на этом этапе разработки:

  1. setManaged(false) — не просто открепляет элемент от управление расположением его в сетке, он полностью открепляет его от управления его отрисовкой по этому. установка prefSize, minSize, maxSize (пишу упрощенно что бы не перечислять конкретные методы setPrefWidth(), setPrefHeight() и так далее). Единственное что остается это вызвать метод resize(double width, double height)

  2. Для координат x и y выбран тип SimpleDoubleProperty данные типы данных observable и могут иметь обработчики в случае их изменения. Это просто и удобно использовать что бы синхронизировать любые изменения координат с обложкой данного компонента

  3. Сделаны методы getXProperty и getYProperty для возможности вне компонента добавлять обработчики изменения данных параметров

  4. Получение размеров компонентов внутри контейнера довольно весомая задача по этому в реальном компоненте, я вычисляю лишь высоту первой строки и просто умножаю на количество дочерних компонентов и добавляю 5 пикселей для отступов.

  5. Для вычисления максимальной ширины используем кэш, который пересчитываем лишь при изменение количества подсказок или же их размеров. getChildren() возвращает как раз ObservableList на который просто можно повесить слушатель и обновлять размеры при необходимости. Единственное важно вычислить момент отрисован ли данный элемент или нет.

Когда у нас есть уже свободно плавающий элемент в интерфейсе, пора задуматься о словаре подсказок что собственно отображать. По этому добавляем в сам элемент ObservableList<String> текстов подсказок

// Autocomplit.....private ObservableList<String> words = FXCollections.observableArrayList();public void addWord(String word) { words.add(word); }public void addAllWord(String[] words) {   words.removeAll();  words.addAll(words);}public ObservableList<String> getWorsProperty() { return words; }public void handleChange() {  words.addListener((e, oldW, newW) {    aSkin.setWords(words);  })}

Заострю внимание на использование ObservableList это помогает нам в синхронизации данных из компонента в его обложку. Нам остается добавить в скин так же параметр ObservableList<String> words

// AutocomplitSkinprivate ObservableList<String> words = FXCollections.observableArrayList();....public void setObservableList(ObservableList<String> words) { this.words = words; }

И в качестве заключения нам необходимо изменить метод setupContent

private void setupContent() {  contentBox.getChildren().clear();  contentBox.getStyleClass.add("autocomplit-vbox");    getStyleClass.add("autocomplit-main-control");    words.forEach((v) -> {     Text tmp = new Text(v);    tmp.getStyleClass.add("autocomplit-string");    contentBox.getChildren().add(tmp);  });  getChildren().add(contentBox);}

Метод setupContent вызываем в конструкторе, и при изменение словаря с помощью listener на данное изменение.

Я описал и привел пример в качестве основы создания компонента, в этой статье была упрощена логика от рисовки в качестве примера, в реальном компоненте интсанцируется не просто Text а 3 отдельных планки (Pane), у каждой высчитывается максимальная ширина и строиться таблица, в первом столбце иконка, второе название подсказки и третье короткое описание. При наведение на короткое описание и клике в этой области, открывается вторая всплывающая панель с подробным описанием.

Так же и словарь в реальном компоненте это не просто ObservableList<String> вместо String использован класс WordDto который содержит полностью название, описание, и параметры и их позиции что бы после добавления автоподсказки каретку редактора установить в позицию параметров и добавить названия параметров в редактор.

Логику показа и скрытия элемента тоже не описывал в этой статье, так как она простая если поиск находит по вхождению последнего набираемого слова в индексах словаря (который сохраняется в локальный проект, с помощью SqlLite) то просто ставим setVisible(true) при выборе подсказки ставим false.

Логика индексация вынесена в другой поток и минимально пересекается с логикой отображения подсказок, даже не пришлось реализовывать методы synchronize, так как есть первично собранный словарь нативных функций php и предустановленных классов. После готовности индексов проекта поиск начинает работать уже и по ним.

Логика выделения и выбора подсказки вынесена в отдельный сервис, и отслеживание нажатия клавиатуры и мыши происходит в компоненте самого редактора, он тоже кастомный, в прошлой статье писал что там очень много логики связанной с вычислением позиций и рендера строк, подсветки, курсора и так далее.

В качестве заключения, разработка собственной Idea оказалась довольно амбициозной задачей, с начала была мысль использовать готовые решения и просто прикрутить xdebug, seach engine, code анализатор. Но когда начал понят что это не так и уже 3 месяц приходиться большую часть реализовывать саму. Но к счастью все получается и приносит свое удовольствие и удовлетворение. Возможно те кто занимается профессионально java скажет что это все очевидно и просто, но я занимаюсь java всего 3 месяца…

ссылка на оригинал статьи https://habr.com/ru/articles/1043416/