官术网_书友最值得收藏!

Initializing the user interface

While the FXML defines the structure of the user interface, we do need some Java code to initialize various elements, respond to actions, and so forth. This class, referred to as the controller, is simply a class that extends javafx.fxml.Initializable:

    public class Controller implements Initializable { 
      @FXML 
      private TableView<ProcessHandle> processList; 
      @Override 
      public void initialize(URL url, ResourceBundle rb) { 
      } 
    } 

The initialize() method comes from the interface, and is used by the JavaFX runtime to initialize the controller when it is created in the call to FXMLLoader.load() from the preceding Application class. Note the @FXML annotation on the instance variable processList. When JavaFX initializes the controller, before the initialize() method is called, the system looks for FXML elements that specify an fx:id attribute, and assigns that reference to the appropriate instance variable in the controller. To complete this connection, we must make one more change to our FXML file:

    <TableView fx:id="processList" BorderPane.alignment="CENTER">
...

The change can also be made in Scene Builder as seen in this screenshot:

The value of the fx:id attribute must match the name of an instance variable that has been annotated with @FXML. When initialize is called, processList will have a valid reference to TableView that we can manipulate in our Java code.

The value of fx:id can be set via Scene Builder as well. To set the value, click on the control in the form editor, then expand the Code section in the accordion on the right. In the fx:id field, type in the name of the desired variable name.

The final piece of the puzzle is specifying the controller for the FXML file. In the XML source, you can set this via the fx:controller attribute on the root element of the user interface:

    <BorderPane  xmlns="http://javafx.com/javafx/8.0.60"
xmlns:fx="http://javafx.com/fxml/1"
fx:controller=
"com.steeplesoft.procman.Controller">

This can also be set via Scene Builder. In the Document section of the accordion on the left, expand the Controller section and enter the desired fully-qualified class name in the Controller class field:

With those pieces in place, we can begin the work of initializing TableView, which gets us back to our primary interest, the process handling APIs. Our starting point is ProcessHandles.allProcesses(). From the Javadoc, you learn that this method returns a snapshot of all processes visible to the current process. From each ProcessHandle in the stream, we can get information about the process ID, its state, children, parents, and so on. Each ProcessHandle also has a nested object, Info, that contains a snapshot of information about the process. Since not all information is available across the various supported platforms and it is limited by the privileges of the current process, the properties on the Info object are the Optional<T> instances, indicating that the values may or may not be set. It's probably worth the time to take a quick look at what Optional<T> is.

The Javadoc describes Optional<T> as a container object which may or may not contain a non-null value. Inspired by Scala and Haskell, Optional<T> was introduced in Java 8 to allow API authors to provide a more null-safe interface. Prior to Java 8, a method on ProcessHandle.Info may be defined like this:

    public String command(); 

To consume the API, the developer would likely write something like this:

    String command = processHandle.info().command(); 
    if (command == null) { 
      command = "<unknown>"; 
    } 

If the developer fails to check for null explicitly, NullPointerException is almost certain to occur at some point. By using Optional<T>, the API author signals to the user that the return value may be null and should be handled carefully. The updated code, then, may look like this:

    String command = processHandle.info().command() 
     .orElse("<unknown>"); 

Now, in one concise line, we can get the value, if it is present, or a default if it is not. The ProcessHandle.Info API makes extensive use of this construct as we'll see later.

What else does Optional afford us as developers? There are a number of instance methods that can help clarify null-handling code:

  • filter(Predicate<? super T> predicate): With this method, we filter the contents of Optional. Rather than using an if...else block, we can pass the filter() method a Predicate and do the test inline. A Predicate is a @FunctionalInterface that takes an input and returns a Boolean. For example, some uses of the JavaFX Dialog may return Optional<ButtonType>. If we wanted to do something only if the user clicked a specific button, say, OK, we could filter Optional like this:
        alert.showAndWait() 
         .filter(b -> b instanceof ButtonType.OK) 
  • map(Function<? super T,? extends U> mapper): The map function allows us to pass the contents of Optional to a function, which will perform some processing on it, and return it. The return from the function, though, will be wrapped in an Optional:
        Optional<String> opts = Optional.of("hello"); 
        Optional<String> upper = opts.map(s ->  
         s.toUpperCase()); 
        Optional<Optional<String>> upper2 =  
         opts.map(s -> Optional.of(s.toUpperCase())); 

Note the double wrapping in Optional for upper2. If Function returns Optional, it will be wrapped in another Optional, giving us this odd double wrap, which is less than desirable. Fortunately, we have an alternative.

  • flatMap(Function<? super T,Optional<U>> mapper): The flatMap function combines two functional ideas--maps and flatten. If the result of Function is an Optional object, rather than double wrapping the value, it is flattened to a single Optional object. Revisiting the preceding example, we get this:
        Optional<String> upper3 = opts.flatMap(s ->      
         Optional.of(s.toUpperCase())); 

Note that upper3, unlike upper2, is a single Optional:

  • get(): This returns the wrapped value, if present. If there is no value, a NoSuchElementException error is thrown.
  • ifPresent(Consumer<? super T> action): If the Optional object contains a value, it is passed to the Consumer. If there is no value present, nothing happens.
  • ifPresentOrElse(Consumer<? super T> action, Runnable emptyAction): Like ifPresent(), this will pass the value to the Consumer if there is one present. If no value is present, the Runnable emptyAction is executed.
  • isPresent(): This simply returns true if the Optional object contains a value.
  • or(Supplier<Optional<T>> supplier): If the Optional object has a value, the Optional is described. If there is no value present, an Optional object produced by the Supplier is returned.
  • orElse(T other): If the Optional object contains a value, it is returned. If there is no value, other is returned.
  • orElseGet(Supplier<? extends T> supplier): This works just like orElse() mentioned earlier, but, if no value is present, the result of the Supplier is returned.
  • orElseThrow(Supplier<? extends X> exceptionSupplier): If there is a value present, it is returned. If there is no value, the Exception provided by the Supplier is thrown.

Optional also has several static methods that facilitate the creation of the Optional instances, some of which are as follows:

  • empty(): This returns an empty Optional object.
  • of(T value): This returns an Optional object describing the non-null value. If the value is null, a NullPointerException is thrown.
  • ofNullable(T value): This returns an Optional object describing the value. If the value is null, an empty Optional is returned.

With that very brief introduction to Optional<T> under our belts, let's see how its presence affects our application.

Returning our attention to the initialize() method, then, our first step is to get the list of processes to display. The streams API makes this extremely simple:

    ProcessHandle.allProcesses() 
     .collect(Collectors.toList()); 

The allProcesses() method returns Stream<ProcessHandle>, which allows us to apply the new stream operations to our problem. In this case, we just want to create a List of all of the ProcessHandle instances, so we call collect(), which is a stream operation that takes in a Collector. There are a number of options from which we could choose, but we want a List, so we use Collectors.toList(), which will collect each item in the stream and eventually return a List when the stream terminates. Note that the parameterized type of List will match that of Stream, which is ProcessHandle in this case.

This one line, then, gets us a List<ProcessHandle> of every process on the system that the current process can see, but that only gets us halfway. The TableView API doesn't accept a List<T>. It only supports ObservableList<T>, but what is that? Its Javadoc defines it very simply--A list that allows listeners to track changes when they occur. To put it another way, when this list changes, TableView will be told about it automatically and will redraw itself. Once we associate TableView with this list, all we have to worry about is the data, and the control will handle the rest. Creating ObservableList is pretty straightforward:

    @FXML 
    private TableView<ProcessHandle> processView; 
    final private ObservableList<ProcessHandle> processList =  
      FXCollections.observableArrayList(); 
    // ... 
    processView.setItems(processList);      
    processList.setAll(ProcessHandle.allProcesses() 
     .collect(Collectors.toList())); 

In our case, the TableView instance is injected by the runtime (included here for clarity), and we create the ObservableList via FXCollections.observableArrayList(). In initialize(), we set the ObservableList on the TableView via setItems(), then populate the ObservableList via setAll(). With that, our TableView has all the data it needs to render itself. Almost. It has the data to render, but how does it do it? Where does each field of ProcessHandle.Info go? To answer that, we have to define the columns on the table, and tell each column where to get its data.

To do that, we need to create several TableColumn<S,T> instances. The TableColumn is responsible for displaying not only its column heading (as appropriate), but also the value of each cell. However, you have to tell it how to display the cell. That is done via a cell value factory. Under Java 7, that API would get us code like this:

    TableColumn<ProcessHandle, String> commandCol =  
     new TableColumn<>("Command"); 
    commandCol.setCellValueFactory(new  
      Callback<TableColumn.CellDataFeatures<ProcessHandle, String>,  
       ObservableValue<String>>() { 
         public ObservableValue<String> call( 
          TableColumn.CellDataFeatures<ProcessHandle,  
           String> p) { 
             return new SimpleObjectProperty(p.getValue()
.info() .command() .map(Controller::afterLast) .orElse("<unknown>")); } }
);

I'll go ahead and say it for you: that's really ugly. Fortunately, we can put lambdas and type inference to work for us, to make that a lot more pleasant to read:

    TableColumn<ProcessHandle, String> commandCol =  
     new TableColumn<>("Command"); 
    commandCol.setCellValueFactory(data ->  
     new SimpleObjectProperty(data.getValue().info().command() 
      .map(Controller::afterLast) 
      .orElse("<unknown>"))); 

That's fourteen lines of code replaced by six. Much prettier. Now, we just have to do that five more times, once for each column. As improved as the preceding code may be, there's still quite a bit of repeated code. Again, Java 8 functional interfaces can help us clean the code up a bit more. For each column, we want to specify the header, a width, and what to extract from ProcessHandle.Info. We can encapsulate that with this method:

    private <T> TableColumn<ProcessHandle, T>  
      createTableColumn(String header, int width,  
       Function<ProcessHandle, T> function) { 
         TableColumn<ProcessHandle, T> column = 
          new TableColumn<>(header); 
 
         column.setMinWidth(width); 
         column.setCellValueFactory(data ->  
          new SimpleObjectProperty<T>( 
           function.apply(data.getValue()))); 
           return column; 
    } 

The Function<T,R> interface is FunctionalInterface, which represents a function that takes in one type, T, and returns another, R. In our case, we're defining this method as one that takes as parameters a String, an int, and a function that takes in ProcessHandle and returns a generic type. That may be hard to picture, but with this method defined, we can replace the preceding code and the others like it with calls to this method. The same preceding code can now be condensed to this:

    createTableColumn("Command", 250,  
      p -> p.info().command() 
      .map(Controller::afterLast) 
      .orElse("<unknown>")) 

Now we just need to add these columns to the control, which we can do with this:

    processView.getColumns().setAll( 
      createTableColumn("Command", 250,  
      p -> p.info().command() 
       .map(Controller::afterLast) 
       .orElse("<unknown>")), 
      createTableColumn("PID", 75, p -> p.getPid()), 
      createTableColumn("Status", 150,  
       p -> p.isAlive() ? "Running" : "Not Running"), 
      createTableColumn("Owner", 150,  
       p -> p.info().user() 
        .map(Controller::afterLast) 
        .orElse("<unknown>")), 
      createTableColumn("Arguments", 75,  
       p -> p.info().arguments().stream() 
        .map(i -> i.toString()) 
        .collect(Collectors.joining(", ")))); 

Note that every method we're using on ProcessHandle.Info returns the Optional<T> we looked at in the preceding code. Since it does this, we have a very nice and clean API to get the information we want (or a reasonable default) without the specter of a NullPointerException in production.

If we run the application now, we should get something like this:

It's looking good so far, but it's not quite ready yet. We want to be able to start new processes as well as kill existing ones. Both of those will require menus, so we'll add those next.

主站蜘蛛池模板: 郁南县| 新河县| 东兴市| 绥滨县| 浦东新区| 黑河市| 子洲县| 禹州市| 双辽市| 繁昌县| 寿阳县| 旺苍县| 天长市| 定安县| 铅山县| 巴林左旗| 若尔盖县| 中超| 资源县| 隆尧县| 濮阳县| 香河县| 乌拉特中旗| 瓦房店市| 始兴县| 墨脱县| 宁国市| 漳州市| 来凤县| 西林县| 安岳县| 灵武市| 泽州县| 满洲里市| 托克逊县| 专栏| 安宁市| 方山县| 玉溪市| 阳信县| 东阳市|