На самом деле, 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;}
Теперь наш компонент может находиться в произвольном месте окна, и его размеры соответствуют размерам контента который он содержит. Важные моменты на этом этапе разработки:
-
setManaged(false) — не просто открепляет элемент от управление расположением его в сетке, он полностью открепляет его от управления его отрисовкой по этому. установка prefSize, minSize, maxSize (пишу упрощенно что бы не перечислять конкретные методы setPrefWidth(), setPrefHeight() и так далее). Единственное что остается это вызвать метод resize(double width, double height)
-
Для координат x и y выбран тип SimpleDoubleProperty данные типы данных observable и могут иметь обработчики в случае их изменения. Это просто и удобно использовать что бы синхронизировать любые изменения координат с обложкой данного компонента
-
Сделаны методы getXProperty и getYProperty для возможности вне компонента добавлять обработчики изменения данных параметров
-
Получение размеров компонентов внутри контейнера довольно весомая задача по этому в реальном компоненте, я вычисляю лишь высоту первой строки и просто умножаю на количество дочерних компонентов и добавляю 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/