Статьи

JavaFX Совет 20: много чего показать?

Кажется, что есть два типа приложений JavaFX: первый использует граф сцены с узлами и CSS-стилем, а второй использует один холст. Тем не менее, совершенно законно смешивать эти два подхода. Особенно, когда ваше приложение должно показывать много подробной информации, где вы легко могли бы создать тысячи и тысячи узлов. Несмотря на то, что общая производительность JavaFX просто фантастическая, вы, скорее всего, поставите свою систему на колени, когда для всех этих узлов требуется стилизация (особенно, когда стилизация требуется снова и снова из-за динамического характера вашей визуализации).

Для меня это было прозрение, когда я понял, что единственный способ гарантировать высокую производительность в FlexGanttFX — это использовать ListView с каждой ячейкой, содержащей холст. К сожалению, код этого фреймворка слишком сложен, чтобы делиться им с вами в небольшом блоге, поэтому я написал небольшой пример, иллюстрирующий основные понятия. На рисунке ниже показан результат при запуске примера. Данные, отображаемые в ListView, охватывают годы моей жизни со случайно сгенерированными значениями для каждого дня каждого года.

bildschirmfoto-2015-06-15-эм-19-25-50

Самый важный класс называется CanvasCell . Это специализированная ячейка списка, содержащая метку и холст. Метка используется для отображения года, холст — для рисования диаграммы.

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
import java.util.Collections;
import java.util.List;
 
import javafx.geometry.Pos;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.control.ContentDisplay;
import javafx.scene.control.Label;
import javafx.scene.control.ListCell;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.paint.CycleMethod;
import javafx.scene.paint.LinearGradient;
import javafx.scene.paint.Stop;
 
public class CanvasCell extends ListCell<YearEntry> {
 
    private Label yearLabel;
    private ResizableCanvas canvas;
 
    public CanvasCell() {
        /*
         * Important, otherwise we will keep seeing a horizontal scrollbar.
         */
        setStyle("-fx-padding: 0px;");
 
        yearLabel = new Label();
        yearLabel
          .setStyle("-fx-padding: 10px; -fx-font-size: 1.2em; -fx-font-weight: bold;");
        StackPane.setAlignment(yearLabel, Pos.TOP_LEFT);
 
        /*
         * Create a resizable canvas and bind its width and height to the width
         * and height of the table cell.
         */
        canvas = new ResizableCanvas();
        canvas.widthProperty().bind(widthProperty());
        canvas.heightProperty().bind(heightProperty());
 
        StackPane pane = new StackPane();
        pane.getChildren().addAll(yearLabel, canvas);
 
        setGraphic(pane);
        setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
    }
 
    @Override
    protected void updateItem(YearEntry entry, boolean empty) {
        if (empty || entry == null) {
            yearLabel.setText("");
            canvas.setData(Collections.emptyList());
            canvas.draw();
        } else {
            yearLabel.setText(Integer.toString(entry.getYear()));
            canvas.setData(entry.getValues());
            canvas.draw();
        }
    }
 
    /*
     * Canvas is normally not resizable but by overriding isResizable() and
     * binding its width and height to the width and height of the cell it will
     * automatically resize.
     */
    class ResizableCanvas extends Canvas {
 
        private List<Double> data = Collections.emptyList();
 
        public ResizableCanvas() {
 
            /*
             * Make sure the canvas draws its content again when its size
             * changes.
             */
            widthProperty().addListener(it -> draw());
            heightProperty().addListener(it -> draw());
        }
 
        @Override
        public boolean isResizable() {
            return true;
        }
 
        @Override
        public double prefWidth(double height) {
            return getWidth();
        }
 
        @Override
        public double prefHeight(double width) {
            return getHeight();
        }
 
        public void setData(List<Double> data) {
            this.data = data;
        }
 
        /*
         * Draw a chart based on the data provided by the model.
         */
        private void draw() {
            GraphicsContext gc = getGraphicsContext2D();
            gc.clearRect(0, 0, getWidth(), getHeight());
 
            Stop[] stops = new Stop[] { new Stop(0, Color.SKYBLUE),
                    new Stop(1, Color.SKYBLUE.darker().darker()) };
            LinearGradient gradient = new LinearGradient(0, 0, 0, 300, false,
                    CycleMethod.NO_CYCLE, stops);
 
            gc.setFill(gradient);
 
            double availableHeight = getHeight() * .8;
            double counter = 0;
            for (Double value : data) {
                double x = getWidth() / 365 * counter;
                double barHeight = availableHeight * value / 100;
                double barWidth = getWidth() / 365 + 1;
                gc.fillRect(x, getHeight() - barHeight, barWidth, barHeight);
                counter++;
            }
        }
    }
}

Для данных мы используем очень простой класс, который хранит год и список значений.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import java.util.ArrayList;
import java.util.List;
 
 
/**
 * Just some fake model object.
 */
public class YearEntry {
 
    private int year;
 
    public YearEntry(int year) {
        this.year = year;
    }
 
    public int getYear() {
        return year;
    }
 
    private List<Double> values = new ArrayList<>();
 
    /**
     * Stores the values shown in the chart.
     */
    public List<Double> getValues() {
        return values;
    }
}

И следующий список показывает основной класс.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.Scene;
import javafx.scene.control.ListView;
import javafx.stage.Stage;
 
public class CanvasApp extends Application {
 
    @Override
    public void start(Stage stage) throws Exception {
 
        /*
         * Create some random data for my life span.
         */
        ObservableList<YearEntry> data =
            FXCollections.observableArrayList();
        for (int year = 1969; year < 2015; year++) {
            YearEntry entry = new YearEntry(year);
            for (int day = 0; day < 365; day++) {
                entry.getValues().add(Math.random() * 100);
            }
            data.add(entry);
        }
 
        ListView<YearEntry> listView = new ListView<>(data);
        listView.setCellFactory(param -> new CanvasCell());
        listView.setFixedCellSize(200);
 
        Scene scene = new Scene(listView);
 
        stage.setTitle("Canvas Cell");
        stage.setScene(scene);
        stage.setWidth(600);
        stage.setHeight(600);
        stage.show();
    }
 
    public static void main(String[] args) {
        launch(args);
    }
}
Ссылка: JavaFX Совет 20: много чего показать? Используйте холст! от нашего партнера JCG Дирка Леммермана в блоге Pixel Perfect .