Статьи

Новый пользовательский элемент управления: TaskProgressView

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

проблемно-монитор

Если вы уже знакомы с классом javafx.concurrent.Task, вы быстро поймете, что элемент управления отображает значение его свойств title, message и progress. Но он также показывает значок, который не покрывается Task API. Я добавил необязательную графическую фабрику (обратный вызов), которая будет вызываться для каждой задачи для поиска графического узла, который будет размещен слева от ячейки представления списка, представляющей задачу.

Видео, показывающее элемент управления в действии, можно найти здесь:

Контроль

Поскольку этот элемент управления довольно прост, я подумал, что имеет смысл опубликовать весь исходный код для него, чтобы его можно было использовать для изучения другими. Следующий листинг показывает код самого элемента управления. Как и ожидалось, он расширяет класс Control и предоставляет наблюдаемый список для отслеживаемых задач и свойство объекта для графической фабрики (обратный вызов).

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
124
125
126
127
128
package org.controlsfx.control;
 
import impl.org.controlsfx.skin.TaskProgressViewSkin;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.concurrent.Task;
import javafx.concurrent.WorkerStateEvent;
import javafx.event.EventHandler;
import javafx.scene.Node;
import javafx.scene.control.Control;
import javafx.scene.control.Skin;
import javafx.util.Callback;
 
/**
 * The task progress view is used to visualize the progress of long running
 * tasks. These tasks are created via the {@link Task} class. This view
 * manages a list of such tasks and displays each one of them with their
 * name, progress, and update messages.<p>
 * An optional graphic factory can be set to place a graphic in each row.
 * This allows the user to more easily distinguish between different types
 * of tasks.
 *
 * <h3>Screenshots</h3>
 * The picture below shows the default appearance of the task progress view
 * control:
 * <center><img src="task-monitor.png" /></center>
 *
 * <h3>Code Sample</h3>
 *
 * <pre>
 * TaskProgressView<MyTask> view = new TaskProgressView<>();
 * view.setGraphicFactory(task -> return new ImageView("db-access.png"));
 * view.getTasks().add(new MyTask());
 * </pre>
 */
public class TaskProgressView<T extends Task<?>> extends Control {
 
    /**
     * Constructs a new task progress view.
     */
    public TaskProgressView() {
        getStyleClass().add("task-progress-view");
 
        EventHandler<WorkerStateEvent> taskHandler = evt -> {
            if (evt.getEventType().equals(
                    WorkerStateEvent.WORKER_STATE_SUCCEEDED)
                    || evt.getEventType().equals(
                            WorkerStateEvent.WORKER_STATE_CANCELLED)
                    || evt.getEventType().equals(
                            WorkerStateEvent.WORKER_STATE_FAILED)) {
                getTasks().remove(evt.getSource());
            }
        };
 
        getTasks().addListener(new ListChangeListener<Task<?>>() {
            @Override
            public void onChanged(Change<? extends Task<?>> c) {
                while (c.next()) {
                    if (c.wasAdded()) {
                        for (Task<?> task : c.getAddedSubList()) {
                            task.addEventHandler(WorkerStateEvent.ANY,
                                    taskHandler);
                        }
                    } else if (c.wasRemoved()) {
                        for (Task<?> task : c.getAddedSubList()) {
                            task.removeEventHandler(WorkerStateEvent.ANY,
                                    taskHandler);
                        }
                    }
                }
            }
        });
    }
 
    @Override
    protected Skin<?> createDefaultSkin() {
        return new TaskProgressViewSkin<>(this);
    }
 
    private final ObservableList<T> tasks = FXCollections
            .observableArrayList();
 
    /**
     * Returns the list of tasks currently monitored by this view.
     *
     * @return the monitored tasks
     */
    public final ObservableList<T> getTasks() {
        return tasks;
    }
 
    private ObjectProperty<Callback<T, Node>> graphicFactory;
 
    /**
     * Returns the property used to store an optional callback for creating
     * custom graphics for each task.
     *
     * @return the graphic factory property
     */
    public final ObjectProperty<Callback<T, Node>> graphicFactoryProperty() {
        if (graphicFactory == null) {
            graphicFactory = new SimpleObjectProperty<Callback<T, Node>>(
                    this, "graphicFactory");
        }
 
        return graphicFactory;
    }
 
    /**
     * Returns the value of {@link #graphicFactoryProperty()}.
     *
     * @return the optional graphic factory
     */
    public final Callback<T, Node> getGraphicFactory() {
        return graphicFactory == null ? null : graphicFactory.get();
    }
 
    /**
     * Sets the value of {@link #graphicFactoryProperty()}.
     *
     * @param factory an optional graphic factory
     */
    public final void setGraphicFactory(Callback<T, Node> factory) {
        graphicFactoryProperty().set(factory);
    }

Кожа

Как и следовало ожидать, для отображения заданий скин использует ListView с настраиваемой фабрикой ячеек.

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
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
package impl.org.controlsfx.skin;
 
import javafx.beans.binding.Bindings;
import javafx.concurrent.Task;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.Button;
import javafx.scene.control.ContentDisplay;
import javafx.scene.control.Label;
import javafx.scene.control.ListCell;
import javafx.scene.control.ListView;
import javafx.scene.control.ProgressBar;
import javafx.scene.control.SkinBase;
import javafx.scene.control.Tooltip;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.VBox;
import javafx.util.Callback;
 
import org.controlsfx.control.TaskProgressView;
 
import com.sun.javafx.css.StyleManager;
 
public class TaskProgressViewSkin<T extends Task<?>> extends
        SkinBase<TaskProgressView<T>> {
 
    static {
        StyleManager.getInstance().addUserAgentStylesheet(
                TaskProgressView.class
                        .getResource("taskprogressview.css").toExternalForm()); //$NON-NLS-1$
    }
 
    public TaskProgressViewSkin(TaskProgressView<T> monitor) {
        super(monitor);
 
        BorderPane borderPane = new BorderPane();
        borderPane.getStyleClass().add("box");
 
        // list view
        ListView<T> listView = new ListView<>();
        listView.setPrefSize(500, 400);
        listView.setPlaceholder(new Label("No tasks running"));
        listView.setCellFactory(param -> new TaskCell());
        listView.setFocusTraversable(false);
 
        Bindings.bindContent(listView.getItems(), monitor.getTasks());
        borderPane.setCenter(listView);
 
        getChildren().add(listView);
    }
 
    class TaskCell extends ListCell<T> {
        private ProgressBar progressBar;
        private Label titleText;
        private Label messageText;
        private Button cancelButton;
 
        private T task;
        private BorderPane borderPane;
 
        public TaskCell() {
            titleText = new Label();
            titleText.getStyleClass().add("task-title");
 
            messageText = new Label();
            messageText.getStyleClass().add("task-message");
 
            progressBar = new ProgressBar();
            progressBar.setMaxWidth(Double.MAX_VALUE);
            progressBar.setMaxHeight(8);
            progressBar.getStyleClass().add("task-progress-bar");
 
            cancelButton = new Button("Cancel");
            cancelButton.getStyleClass().add("task-cancel-button");
            cancelButton.setTooltip(new Tooltip("Cancel Task"));
            cancelButton.setOnAction(evt -> {
                if (task != null) {
                    task.cancel();
                }
            });
 
            VBox vbox = new VBox();
            vbox.setSpacing(4);
            vbox.getChildren().add(titleText);
            vbox.getChildren().add(progressBar);
            vbox.getChildren().add(messageText);
 
            BorderPane.setAlignment(cancelButton, Pos.CENTER);
            BorderPane.setMargin(cancelButton, new Insets(0, 0, 0, 4));
 
            borderPane = new BorderPane();
            borderPane.setCenter(vbox);
            borderPane.setRight(cancelButton);
            setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
        }
 
        @Override
        public void updateIndex(int index) {
            super.updateIndex(index);
 
            /*
             * I have no idea why this is necessary but it won't work without
             * it. Shouldn't the updateItem method be enough?
             */
            if (index == -1) {
                setGraphic(null);
                getStyleClass().setAll("task-list-cell-empty");
            }
        }
 
        @Override
        protected void updateItem(T task, boolean empty) {
            super.updateItem(task, empty);
 
            this.task = task;
 
            if (empty || task == null) {
                getStyleClass().setAll("task-list-cell-empty");
                setGraphic(null);
            } else if (task != null) {
                getStyleClass().setAll("task-list-cell");
                progressBar.progressProperty().bind(task.progressProperty());
                titleText.textProperty().bind(task.titleProperty());
                messageText.textProperty().bind(task.messageProperty());
                cancelButton.disableProperty().bind(
                        Bindings.not(task.runningProperty()));
 
                Callback<T, Node> factory = getSkinnable().getGraphicFactory();
                if (factory != null) {
                    Node graphic = factory.call(task);
                    if (graphic != null) {
                        BorderPane.setAlignment(graphic, Pos.CENTER);
                        BorderPane.setMargin(graphic, new Insets(0, 4, 0, 0));
                        borderPane.setLeft(graphic);
                    }
                } else {
                    /*
                     * Really needed. The application might have used a graphic
                     * factory before and then disabled it. In this case the border
                     * pane might still have an old graphic in the left position.
                     */
                    borderPane.setLeft(null);
                }
 
                setGraphic(borderPane);
            }
        }
    }
}

CSS

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

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
43
44
45
46
47
48
49
50
51
52
.task-progress-view  {
       -fx-background-color: white;
}
 
.task-progress-view > * > .label {
    -fx-text-fill: gray;
    -fx-font-size: 18.0;
    -fx-alignment: center;
    -fx-padding: 10.0 0.0 5.0 0.0;
}
 
.task-progress-view > * > .list-view  {
    -fx-border-color: transparent;
    -fx-background-color: transparent;
}
 
.task-title {
    -fx-font-weight: bold;
}
 
.task-progress-bar .bar {
    -fx-padding: 6px;
    -fx-background-radius: 0;
    -fx-border-radius: 0;
}
 
.task-progress-bar .track {
    -fx-background-radius: 0;
}
 
.task-message {
}
 
.task-list-cell {
    -fx-background-color: transparent;
    -fx-padding: 4 10 8 10;
    -fx-border-color: transparent transparent linear-gradient(from 0.0% 0.0% to 100.0% 100.0%, transparent, rgba(0.0,0.0,0.0,0.2), transparent) transparent;
}
 
.task-list-cell-empty {
    -fx-background-color: transparent;
    -fx-border-color: transparent;
}
 
.task-cancel-button {
    -fx-base: red;
    -fx-font-size: .75em;
    -fx-font-weight: bold;
    -fx-padding: 4px;
    -fx-border-radius: 0;
    -fx-background-radius: 0;
}
Ссылка: Новый пользовательский элемент управления: TaskProgressView от нашего партнера JCG Дирка Леммермана в блоге Pixel Perfect .