2

我的主屏幕 UI 上有一个进度指示器,由各种选项卡和服务共享。每个 TabController 都有自己的 Service 实例。在我的 MainController 类中,对于每个选项卡,我已将每个服务的进度属性绑定到 ProgressIndicator。

@FXML
Region veil;
@FXML
ProgressIndicator progressDial;

  progressDial.progressProperty().bind(tabController.commandService.progressProperty());
    veil.visibleProperty().bind(tabController.commandService.runningProperty());
    progressDial.visibleProperty().bind(tabController.commandService.runningProperty());
    tabController.commandService.messageProperty().addListener(new ChangeListener<String>() {
        @Override
        public void changed(ObservableValue<? extends String> ov, String t, String newValue) {
            addCommentary(newValue);
        }
    });

但是我看到在第一个服务使用它之后,没有出现用于执行后续服务或任务的进度表盘。我想知道我是否在滥用 ProgressIndicator,因为每个服务可能同时运行。我猜在第一次完成后进度没有重置。我该如何重置它?进度属性是只读的。

ReadOnlyDoubleProperty progressProperty() 获取表示进度的 ReadOnlyDoubleProperty。

并且调用 updateProgress(0) 不会使表盘重新出现。

我尝试使用 ProgressIndicator 作为全局显式重置它

mainController.progressDial.setProgress(0);

但这失败了

java.lang.RuntimeException: A bound value cannot be set. at javafx.beans.property.DoublePropertyBase.set(DoublePropertyBase.java:159)

我可能弄错了,但我认为这是 JavaFX UI 控件设计中的错误。将进度更新为 0 应该重置进度指示器。

4

1 回答 1

13

我的回答中有一些文字,因为从您的问题中我并不清楚您的实例出了什么问题。希望答案中的解释或示例代码是有用的。

我可能弄错了,但我认为这是 JavaFX UI 控件设计中的错误。将进度更新为 0 应该重置进度指示器。

你有点误会了。您已将指示器的进度绑定到任务的进度。任务已完成,进度为 1。现在,如果您想将相同的指标重新用于另一个任务或让它衡量其他任务的进度,您必须先停止它衡量原始任务的进度。要取消与原始任务的进度指示器的关联,请取消绑定它的进度。一旦进度指示器的进度不再绑定到原始任务的进度,您可以自由地将指示器设置为您想要的任何值,或者将其绑定到其他值。

同样,您一次只能将进度指示器的进度绑定到一件事(除非您双向绑定指示器,您不能对任务进度执行此操作,因为任务进度是只读的并且双向绑定到多个任务进度无论如何,值都是不正确的,因为每个任务都处于不同的进度点)。

使表盘重新出现。

从你的描述中我不确定为什么表盘会首先消失,以便它需要重新出现。通常,当进度指示器的进度达到 1 时,它仍然保持可见,报告完全完成的进度,它不会自动消失。您可能会将指标的可见性设置为 false 或将其不透明度修改为零。这两个属性都与指标衡量的实际进度无关。或者,您可能正在从显示的场景中移除指示器。如果您在任务完成后修改可见性并将指示器设置为不可见,并且您希望随后再次看到它以测量另一个任务的进度,那么您需要确保它在场景中,不透明度 > 0 并且可见性设置为真。

一条建议

您只能运行一次任务,因此在完成后,如果它已经取得了一些进展,将其进度设置回零并没有多大意义。

属性类型

进度指示器的进度属性是一个普通DoubleProperty的,而不是一个ReadOnlyDoubleProperty,所以它可以直接设置(只要它没有绑定到另一个值)。

任务的进度属性是只读的,必须通过updateProgress进行更改。任务的进度属性可能是只读的,因此可以通过updateProgress例程中的特殊代码确保对它的更新是线程安全的。


示例代码

考虑以下代码(我相信)可以实现您尝试做的事情的意图。该代码模拟运行铁人三项,其中铁人三项的每个阶段(游泳、自行车、跑步)都是一个单独的任务。在进行铁人三项比赛时,进度指示器会显示铁人三项比赛每个阶段的进度。当铁人三项完成时,进度指示器会逐渐消失,直到开始新的铁人三项。抱歉,样本太长了,我发现很难想出更简洁的东西。

铁人三项赛跑监视器

import javafx.animation.FadeTransition;
import javafx.application.Application;
import javafx.beans.*;
import javafx.beans.property.*;
import javafx.beans.value.*;
import javafx.concurrent.Task;
import javafx.event.*;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.control.ProgressIndicator;
import javafx.scene.layout.*;
import javafx.stage.Stage;
import javafx.util.Duration;

import java.util.Random;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Triathlon extends Application {

  private final Random random = new Random();
  private final ExecutorService exec = Executors.newSingleThreadExecutor();

  @Override public void start(Stage stage) throws Exception {
    final TaskMonitor taskMonitor = new TaskMonitor();

    final ProgressIndicator progressIndicator = new ProgressIndicator();
    progressIndicator.progressProperty().bind(
        taskMonitor.currentTaskProgressProperty()
    );

    final Label currentRaceStage = new Label();
    currentRaceStage.textProperty().bind(
        taskMonitor.currentTaskNameProperty()
    );

    createMainLayout(
        stage,
        createStartRaceButton(
            exec,
            taskMonitor
        ),
        createRaceProgressView(
            taskMonitor,
            progressIndicator,
            currentRaceStage
        )
    );
  }

  @Override public void stop() throws Exception {
    exec.shutdownNow();
  }

  private Button createStartRaceButton(final ExecutorService exec, final TaskMonitor taskMonitor) {
    final Button startButton = new Button("Start Race");
    startButton.disableProperty().bind(taskMonitor.idleProperty().not());
    startButton.setOnAction(new EventHandler<ActionEvent>() {
      @Override
      public void handle(ActionEvent actionEvent) {
        runRace(exec, taskMonitor);
      }
    });
    return startButton;
  }

  private HBox createRaceProgressView(final TaskMonitor taskMonitor, ProgressIndicator progressIndicator, Label currentRaceStage) {
    final HBox raceProgress = new HBox(10);
    raceProgress.getChildren().setAll(
      currentRaceStage,
      progressIndicator
    );
    raceProgress.setOpacity(0);
    raceProgress.setAlignment(Pos.CENTER);

    final FadeTransition fade = new FadeTransition(Duration.seconds(0.75), raceProgress);
    fade.setToValue(0);

    taskMonitor.idleProperty().addListener(new InvalidationListener() {
      @Override
      public void invalidated(Observable observable) {
        if (taskMonitor.idleProperty().get()) {
          fade.playFromStart();
        } else {
          fade.stop();
          raceProgress.setOpacity(1);
        }
      }
    });

    return raceProgress;
  }

  private void createMainLayout(Stage stage, Button startButton, HBox raceProgress) {
    final VBox layout = new VBox(10);
    layout.getChildren().setAll(
      raceProgress,
      startButton
    );
    layout.setAlignment(Pos.CENTER);
    layout.setStyle("-fx-background-color: cornsilk; -fx-padding: 10px;");
    stage.setScene(new Scene(layout, 200, 130));
    stage.show();
  }


  private void runRace(ExecutorService exec, TaskMonitor taskMonitor) {
    StageTask swimTask = new StageTask("Swim", 30,   40);
    StageTask bikeTask = new StageTask("Bike", 210, 230);
    StageTask runTask  = new StageTask("Run",  120, 140);

    taskMonitor.monitor(swimTask, bikeTask, runTask);

    exec.execute(swimTask);
    exec.execute(bikeTask);
    exec.execute(runTask);
  }

  class TaskMonitor {
    final private ReadOnlyObjectWrapper<StageTask> currentTask = new ReadOnlyObjectWrapper<>();
    final private ReadOnlyStringWrapper currentTaskName        = new ReadOnlyStringWrapper();
    final private ReadOnlyDoubleWrapper currentTaskProgress    = new ReadOnlyDoubleWrapper();
    final private ReadOnlyBooleanWrapper idle                  = new ReadOnlyBooleanWrapper(true);

    public void monitor(final StageTask task) {
      task.stateProperty().addListener(new ChangeListener<Task.State>() {
        @Override
        public void changed(ObservableValue<? extends Task.State> observableValue, Task.State oldState, Task.State state) {
          switch (state) {
            case RUNNING:
              currentTask.set(task);
              currentTaskProgress.unbind();
              currentTaskProgress.set(task.progressProperty().get());
              currentTaskProgress.bind(task.progressProperty());
              currentTaskName.set(task.nameProperty().get());
              idle.set(false);
              break;

            case SUCCEEDED:
            case CANCELLED:
            case FAILED:
              task.stateProperty().removeListener(this);
              idle.set(true);
              break;
          }
        }
      });
    }

    public void monitor(final StageTask... tasks) {
      for (StageTask task: tasks) {
        monitor(task);
      }
    }

    public ReadOnlyObjectProperty<StageTask> currentTaskProperty() {
      return currentTask.getReadOnlyProperty();
    }

    public ReadOnlyStringProperty currentTaskNameProperty() {
      return currentTaskName.getReadOnlyProperty();
    }

    public ReadOnlyDoubleProperty currentTaskProgressProperty() {
      return currentTaskProgress.getReadOnlyProperty();
    }

    public ReadOnlyBooleanProperty idleProperty() {
      return idle.getReadOnlyProperty();
    }
  }

  class StageTask extends Task<Duration> {
    final private ReadOnlyStringWrapper name;
    final private int minMinutesElapsed;
    final private int maxMinutesElapsed;

    public StageTask(String name, int minMinutesElapsed, int maxMinutesElapsed) {
      this.name = new ReadOnlyStringWrapper(name);
      this.minMinutesElapsed = minMinutesElapsed;
      this.maxMinutesElapsed = maxMinutesElapsed;
    }

    @Override protected Duration call() throws Exception {
      Duration duration = timeInRange(
        minMinutesElapsed, maxMinutesElapsed
      );

      for (int i = 0; i < 25; i++) {
        updateProgress(i, 25);
        Thread.sleep((int) (duration.toMinutes()));
      }
      updateProgress(25, 25);

      return duration;
    }

    private Duration timeInRange(int min, int max) {
      return Duration.minutes(
        random.nextDouble() * (max - min) + min
      );
    }

    public ReadOnlyStringProperty nameProperty() {
      return name.getReadOnlyProperty();
    }
  }

  public static void main(String[] args) {
    Application.launch(Triathlon.class);
  }
}

附加问题的更新

假设每个阶段都不是铁人三项,而是一个独立的项目(如奥运会)。所以游泳、骑自行车、跑步等都是 SportService 的实例。它们同时执行。体育场电子记分牌上有一个进度指示器表盘,所有 SportServices 游泳、自行车、跑步等都共享它。它为我提供了大致的总体进度 - 虽然我意识到这是模糊的,但它是对一切进展情况的总结,而没有看到细节每个事件的。

使用创建多个并行任务中定义的机制并行运行事件。为您的奥运会整体进度创建一个进度指示器,并使用低级绑定 API 将其绑定到所有任务的进度总和的进度。

ObservableList<Service> services = FXCollections.observableArrayList();

. . .  add services to list.

// extract the progress property for each of the added services.
final ReadOnlyDoubleProperty[] taskProgressList = new ReadOnlyDoubleProperty[services.size()];
for (int i = 0; i < taskProgressList.length; i++) {
  taskProgressList[i] = services.get(i).progressProperty();
}

// calculate the average progress of all services.
DoubleBinding overallProgress =  Bindings.createDoubleBinding(new Callable<Double>() {
  @Override public Double call() throws Exception {
    double value = 0;

    for (int i = 0; i < taskProgressList.length; i++) {
      value += taskProgressList[i].get();
    }

    value /= taskProgressList.length;

    return value;
  }
}, taskProgressList);

// bind the overall progress to our indicator
ProgressIndicator overallProgressIndicator = new ProgressIndicator();
overallProgressIndicator.progressProperty().bind(overallProgress);

这是另一个示例,它演示了整体进度的使用DoubleBinding

进度总结

import java.io.*;
import java.net.URL;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.DoubleBinding;
import javafx.beans.property.*;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.concurrent.*;
import javafx.event.EventHandler;
import javafx.scene.*;
import javafx.scene.control.*;
import javafx.scene.layout.*;
import javafx.stage.Stage;

public class FirstLineSequentialVsParallelService extends Application {
  private static final String[] URLs = {
    "http://www.google.com", 
    "http://www.yahoo.com", 
    "http://www.microsoft.com", 
    "http://www.oracle.com" 
  };

  private ExecutorService sequentialFirstLineExecutor;
  private ExecutorService parallelFirstLineExecutor;

  @Override public void init() throws Exception {
    sequentialFirstLineExecutor = Executors.newFixedThreadPool(
      1, 
      new FirstLineThreadFactory("sequential")
    );  

    parallelFirstLineExecutor = Executors.newFixedThreadPool(
      URLs.length, 
      new FirstLineThreadFactory("parallel")
    );  
  }

  @Override
  public void stop() throws Exception {
    parallelFirstLineExecutor.shutdown();
    parallelFirstLineExecutor.awaitTermination(3, TimeUnit.SECONDS);

    sequentialFirstLineExecutor.shutdown();
    sequentialFirstLineExecutor.awaitTermination(3, TimeUnit.SECONDS);
  }

  public static void main(String[] args) { launch(args); }
  @Override public void start(Stage stage) throws Exception {
    final VBox messages = new VBox();
    messages.setStyle("-fx-background-color: cornsilk; -fx-padding: 10;");

    messages.getChildren().addAll(
      new Label("Parallel Execution"), 
      new Label("------------------")
    );
    DoubleBinding parallelProgress = fetchFirstLines(messages, parallelFirstLineExecutor);
    ProgressMonitoredLabel parallelProgressSummary = new ProgressMonitoredLabel("Parallel Execution Summary");
    parallelProgressSummary.progress.progressProperty().bind(parallelProgress);
    messages.getChildren().add(parallelProgressSummary);

    messages.getChildren().addAll(
      new Label("Sequential Execution"), 
      new Label("--------------------")
    );
    DoubleBinding  sequentialProgress = fetchFirstLines(messages, sequentialFirstLineExecutor);
    ProgressMonitoredLabel sequentialProgressSummary = new ProgressMonitoredLabel("Sequential Execution Summary");
    sequentialProgressSummary.progress.progressProperty().bind(sequentialProgress);
    messages.getChildren().add(sequentialProgressSummary);

    messages.setStyle("-fx-font-family: monospace;");

    stage.setScene(new Scene(messages, 600, 650));
    stage.show();
  }

  private DoubleBinding fetchFirstLines(final VBox monitoredLabels, ExecutorService executorService) {
    ObservableList<Service> services = FXCollections.observableArrayList();
    for (final String url: URLs) {
      final FirstLineService service = new FirstLineService();
      service.setExecutor(executorService);
      service.setUrl(url);

      final ProgressMonitoredLabel monitoredLabel = new ProgressMonitoredLabel(url);
      monitoredLabels.getChildren().add(monitoredLabel);
      monitoredLabel.progress.progressProperty().bind(service.progressProperty());

      service.setOnSucceeded(new EventHandler<WorkerStateEvent>() {
        @Override public void handle(WorkerStateEvent t) {
          monitoredLabel.addStrings(
            service.getMessage(),
            service.getValue()
          );
        }
      });
      service.start();

      services.add(service);
    }

    final ReadOnlyDoubleProperty[] taskProgressList = new ReadOnlyDoubleProperty[services.size()];
    for (int i = 0; i < taskProgressList.length; i++) {
      taskProgressList[i] = services.get(i).progressProperty();
    }

    return Bindings.createDoubleBinding(new Callable<Double>() {
      @Override public Double call() throws Exception {
        double value = 0;

        for (int i = 0; i < taskProgressList.length; i++) {
          value += taskProgressList[i].get();
        }

        value /= taskProgressList.length;

        return value;
      }
    }, taskProgressList);
  }

  public class ProgressMonitoredLabel extends HBox {
    final ProgressBar progress;
    final VBox labels;

    public ProgressMonitoredLabel(String initialString) {
      super(20);

      progress = new ProgressBar();
      labels   = new VBox();
      labels.getChildren().addAll(new Label(initialString), new Label());

      progress.setPrefWidth(100);
      progress.setMinWidth(ProgressBar.USE_PREF_SIZE);
      HBox.setHgrow(labels, Priority.ALWAYS);
      setMinHeight(60);

      getChildren().addAll(progress, labels);
    }

    public void addStrings(String... strings) {
      for (String string: strings) {
        labels.getChildren().add(
          labels.getChildren().size() - 1,
          new Label(string)
        );
      }
    }
  }

  public static class FirstLineService extends Service<String> {
    private StringProperty url = new SimpleStringProperty(this, "url");
    public final void setUrl(String value) { url.set(value); }
    public final String getUrl() { return url.get(); }
    public final StringProperty urlProperty() { return url; }
    protected Task createTask() {
      final String _url = getUrl();
      return new Task<String>() {
        { updateProgress(0, 100); }
        protected String call() throws Exception {
          updateMessage("Called on thread: " + Thread.currentThread().getName());
          URL u = new URL(_url);
          BufferedReader in = new BufferedReader(
                  new InputStreamReader(u.openStream()));
          String result = in.readLine();
          in.close();

          // pause just so that it really takes some time to run the task 
          // so that parallel execution behaviour can be observed.
          for (int i = 0; i < 100; i++) {
            updateProgress(i, 100);
            Thread.sleep(50); 
          }

          return result;
        }
     };
    }
  }

  static class FirstLineThreadFactory implements ThreadFactory {
    static final AtomicInteger poolNumber = new AtomicInteger(1);
    private final String type;

    public FirstLineThreadFactory(String type) {
      this.type = type;
    }

    @Override public Thread newThread(Runnable runnable) {
      Thread thread = new Thread(runnable, "LineService-" + poolNumber.getAndIncrement() + "-thread-" + type);
      thread.setDaemon(true);

      return thread;
    }
  }  
}
于 2013-05-04T08:14:24.673 回答