JavaFX17-学习手册-二-

78 阅读58分钟

JavaFX17 学习手册(二)

原文:Learn JavaFX 17

协议:CC BY-NC-SA 4.0

三、可观察的集合

在本章中,您将学习:

  • JavaFX 中有哪些可观察的集合

  • 如何观察可观察集合的失效和变化

  • 如何使用可观察集合作为属性

本章的例子在com.jdojo.collections包中。为了让它们工作,您必须在module-info.java文件中添加相应的一行:

...
opens com.jdojo.collections to javafx.graphics, javafx.base;
...

什么是可观测集合?

JavaFX 中的可观察集合是 Java 中集合的扩展。Java 中的集合框架有ListSetMap接口。JavaFX 添加了以下三种类型的可观察集合,可以观察到它们内容的变化:

  • 可观察的列表

  • 可观察的集合

  • 可观察的地图

JavaFX 通过三个新接口支持这些类型的集合:

  • ObservableList

  • ObservableSet

  • ObservableMap

这些接口从java.util包中的ListSetMap继承而来。除了从 Java 集合接口继承之外,JavaFX 集合接口还继承了Observable接口。所有 JavaFX 可观察集合接口和类都在javafx.collections包中。图 3-1 显示了ObservableListObservableSetObservableMap接口的部分类图。

img/336502_2_En_3_Fig1_HTML.jpg

图 3-1

JavaFX 中可观察集合接口的部分类图

JavaFX 中的可观察集合有两个额外的特性:

  • 它们支持失效通知,因为它们是从Observable接口继承的。

  • 它们支持更改通知。您可以向它们注册更改侦听器,当它们的内容发生更改时会得到通知。

javafx.collections.FXCollections类是一个使用 JavaFX 集合的实用程序类。它由所有静态方法组成。

JavaFX 不公开可观察列表、集合和映射的实现类。您需要使用FXCollections类中的一个工厂方法来创建ObservableListObservableSetObservableMap接口的对象。

Tip

简单地说,JavaFX 中的可观察集合是一个列表、集合或映射,可以观察到它的失效和内容变化。

理解观察列表

一个ObservableList是一个java.util.List和一个具有变更通知特性的Observable。图 3-2 显示了ObservableList接口的类图。

img/336502_2_En_3_Fig2_HTML.jpg

图 3-2

ObservableList接口的类图

Tip

图中缺少方法filtered()sorted()。您可以使用它们来过滤和排序列表元素。有关详细信息,请参见 API 文档。

ObservableList接口中的The addListener()removeListener()方法允许您分别添加和移除ListChangeListener s。其他方法对列表执行操作,这会影响多个元素。

如果您想在ObservableList中发生变化时收到通知,您需要添加一个ListChangeListener接口,当列表中发生变化时会调用该接口的onChanged()方法。Change类是ListChangeListener接口的静态内部类。一个Change对象包含一个ObservableList中变化的报告。它被传递给ListChangeListeneronChanged()方法。我将在本节的后面详细讨论列表更改侦听器。

您可以使用从Observable接口继承的以下两种方法在ObservableList中添加或移除失效监听器:

  • void addListener(InvalidationListener listener)

  • void removeListener(InvalidationListener listener)

注意,ObservableList包含了List接口的所有方法,因为它从List接口继承了这些方法。

Tip

JavaFX 库提供了两个名为FilteredListSortedList的类,它们在javafx.collections.transformation包中。一个FilteredList是一个ObservableList,它使用一个指定的Predicate过滤它的内容。A SortedList对其内容进行排序。我不会在本章讨论这些类。所有关于可观察列表的讨论也适用于这些类的对象。

创建一个可观察列表

您需要使用FXCollections类的以下工厂方法之一来创建一个ObservableList:

  • <E> ObservableList<E> emptyObservableList()

  • <E> ObservableList<E> observableArrayList()

  • <E> ObservableList<E> observableArrayList(Collection<? extends E> col)

  • <E> ObservableList<E> observableArrayList(E... items)

  • <E> ObservableList<E> observableList(List<E> list)

  • <E> ObservableList<E> observableArrayList(Callback<E, Observable[]> extractor)

  • <E> ObservableList<E> observableList(List<E> list, Callback<E, Observable[]> extractor)

emptyObservableList()方法创建一个空的、不可修改的ObservableList。通常,当您需要一个ObservableList作为参数传递给一个方法,并且您没有任何元素要传递给那个列表时,就使用这个方法。您可以创建一个空的StringObservableList,如下所示:

ObservableList<String> emptyList = FXCollections.emptyObservableList();

observableArrayList()方法创建一个由ArrayList支持的ObservableList。该方法的其他变体创建一个ObservableList,其初始元素可以在一个Collection中指定为一个项目列表或一个List

前面列表中的最后两个方法创建了一个ObservableList,可以观察它的元素是否有更新。他们接受一个提取器,它是Callback<E, Observable[]>接口的一个实例。一个提取器用于获取Observable值的列表,以观察更新。我将在“观察 ObservableList 的更新”一节中介绍这两种方法的使用。

清单 3-1 展示了如何创建可观察列表以及如何使用ObservableList接口的一些方法来操作列表。最后,它展示了如何使用FXCollections类的concat()方法来连接两个可观察列表的元素。

// ObservableListTest.java
package com.jdojo.collections;

import javafx.collections.FXCollections;
import javafx.collections.ObservableList;

public class ObservableListTest {
        public static void main(String[] args) {
            // Create a list with some elements
            ObservableList<String> list =
                    FXCollections.observableArrayList("one", "two");
            System.out.println("After creating list: " + list);

            // Add some more elements to the list

            list.addAll("three", "four");
            System.out.println("After adding elements: " + list);

            // You have four elements. Remove the middle two
            // from index 1 (inclusive) to index 3 (exclusive)
            list.remove(1, 3);
            System.out.println("After removing elements: " + list);

            // Retain only the element "one"
            list.retainAll("one");
            System.out.println("After retaining \"one\": " + list);

            // Create another ObservableList
            ObservableList<String> list2 =
                FXCollections.<String>observableArrayList(
                      "1", "2", "3");

            // Set list2 to list
            list.setAll(list2);
            System.out.println("After setting list2 to list: " +
                     list);

            // Create another list
            ObservableList<String> list3 =
                FXCollections.<String>observableArrayList(
                       "ten", "twenty", "thirty");

            // Concatenate elements of list2 and list3
            ObservableList<String> list4 =
                     FXCollections.concat(list2, list3);
            System.out.println("list2 is " + list2);
            System.out.println("list3 is " + list3);
            System.out.println(
                     "After concatenating list2 and list3:" + list4);
        }

}
After creating list: [one, two]
After adding elements: [one, two, three, four]
After removing elements: [one, four]
After retaining "one": [one]
After setting list2 to list: [1, 2, 3]
list2 is [1, 2, 3]
list3 is [ten, twenty, thirty]
After concatenating list2 and list3:[1, 2, 3, ten, twenty, thirty]

Listing 3-1Creating and Manipulating Observable Lists

观察一个可观察列表的无效

您可以像添加任何一个Observable一样添加失效监听器到一个ObservableList。清单 3-2 展示了如何使用带有ObservableList的失效监听器。

Tip

ObservableList的情况下,失效监听器被通知列表中的每一个变化,而不管变化的类型。

// ListInvalidationTest.java
package com.jdojo.collections;

import javafx.beans.Observable;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;

public class ListInvalidationTest {
        public static void main(String[] args) {
                // Create a list with some elements
                ObservableList<String> list =
                    FXCollections.observableArrayList("one", "two");

                // Add an InvalidationListener to the list
                list.addListener(ListInvalidationTest::invalidated);

                System.out.println("Before adding three.");
                list.add("three");
                System.out.println("After adding three.");

                System.out.println("Before adding four and five.");
                list.addAll("four", "five");
                System.out.println("Before adding four and five.");

                System.out.println("Before replacing one with one.");
                list.set(0, "one");
                System.out.println("After replacing one with one.");
        }

        public static void invalidated(Observable list) {
                System.out.println("List is invalid.");
        }
}
Before adding three.
List is invalid.
After adding three.
Before adding four and five.
List is invalid.
Before adding four and five

.
Before replacing one with one.
List is invalid.
After replacing one with one.

Listing 3-2Testing Invalidation Notifications for an ObservableList

观察可观察列表的变化

观察ObservableList的变化有点棘手。列表可以有多种变化。有些变化可能是排他性的,而有些变化可能与其他变化一起发生。列表中的元素可以被置换、更新、替换、添加和删除。学习这个话题你需要耐心,因为我会零零碎碎的讲。

您可以使用其addListener()方法向ObservableList添加一个变更监听器,该方法采用了一个ListChangeListener接口的实例。每次列表发生变化时,监听器的changed()方法都会被调用。下面的代码片段展示了如何向StringObservableList添加一个变更监听器。onChanged()方法简单;当它被通知更改时,它在标准输出上打印一条消息:

// Create an observable list
ObservableList<String> list = FXCollections.observableArrayList();

// Add a change listener to the list
list.addListener(new ListChangeListener<String>() {
        @Override
        public void onChanged(ListChangeListener.Change<? extends String>
                  change) {
            System.out.println("List has changed.");
        }

});

清单 3-3 包含了展示如何检测ObservableList中的变化的完整程序。它使用带有方法引用的 lambda 表达式(Java 8 的特性)来添加更改监听器。在添加了一个更改侦听器之后,它操纵列表四次,每次都通知侦听器,从下面的输出可以明显看出。

// SimpleListChangeTest.java
package com.jdojo.collections;

import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;

public class SimpleListChangeTest {
        public static void main(String[] args) {
            // Create an observable list
            ObservableList<String> list =
                    FXCollections.observableArrayList();

            // Add a change listener to the list
            list.addListener(SimpleListChangeTest::onChanged);

            // Manipulate the elements of the list
            list.add("one");
            list.add("two");
            FXCollections.sort(list);
            list.clear();
        }

        public static void onChanged(
                   ListChangeListener.Change<? extends String> change) {
            System.out.println("List has changed");
        }

}
List has changed.
List has changed.
List has changed.
List has changed.

Listing 3-3Detecting Changes in an ObservableList

了解 ListChangeListener。改变

有时,您可能想要更详细地分析列表的更改,而不仅仅是知道列表已经更改。传递给onChanged()方法的ListChangeListener.Change对象包含一个对列表执行的更改的报告。您需要使用其方法的组合来了解变更的细节。表 3-1 列出了ListChangeListener.Change类中的方法及其类别。

表 3-1

ListChangeListener.Change类中的方法

|

方法

|

种类

| | --- | --- | | ObservableList<E> getList() | 一般 | | boolean next()``void reset() | 光标移动 | | boolean wasAdded()``boolean wasRemoved()``boolean wasReplaced()``boolean wasPermutated()``boolean wasUpdated() | 更改类型 | | int getFrom()``int getTo() | 受影响范围 | | int getAddedSize()``List<E> getAddedSubList() | 添加 | | List<E> getRemoved()``int getRemovedSize() | 搬迁 | | int getPermutation(int oldIndex) | 排列 |

getList()方法在更改后返回源列表。一个ListChangeListener.Change对象可以报告多个块中的变化。这可能一开始并不明显。考虑以下代码片段:

ObservableList<String> list = FXCollections.observableArrayList();

// Add a change listener here...

list.addAll("one", "two", "three");
list.removeAll("one", "three");

在这段代码中,变更监听器将被通知两次:一次是针对addAll()方法调用,一次是针对removeAll()方法调用。ListChangeListener.Change对象报告受影响的索引范围。在第二个更改中,您删除了属于两个不同索引范围的两个元素。注意,在两个被移除的元素之间有一个元素"two"。在第二种情况下,Change对象将包含两个变更的报告。第一个变化将包含索引 0 处的元素"one"已被移除的信息。现在,列表只包含两个元素,元素"two"的索引为 0,元素"three"的索引为 1。第二个变化将包含索引 1 处的元素"three"已被移除的信息。

一个Change对象包含一个指向报告中特定变更的光标。next()reset()方法用于控制光标。当调用onChanged()方法时,光标指向报告中的第一个变更。第一次调用next()方法会将光标移动到报告中的第一个变更处。在试图读取变更的细节之前,您必须通过调用next()方法将光标指向变更。如果next()方法将光标移动到一个有效的变更,它将返回true。否则返回falsereset()方法在第一次改变前移动光标。通常,在 while 循环中调用next()方法,如以下代码片段所示:

ObservableList<String> list = FXCollections.observableArrayList();
...
// Add a change listener to the list
list.addListener(new ListChangeListener<String>() {
    @Override
    public void onChanged(ListChangeListener.Change<? extends String>
             change) {
        while(change.next()) {
            // Process the current change here...
        }
    }

});

在变更类型类别中,方法报告特定类型的变更是否已经发生。如果添加了元素,wasAdded()方法返回true。如果元素被移除,wasRemoved()方法返回true。如果元素被替换,wasReplaced()方法返回true。您可以将替换看作是在相同的索引处删除后添加。如果wasReplaced()返回true,则wasRemoved()wasAdded()也返回true。如果列表的元素被置换(即重新排序)但没有被删除、添加或更新,则wasPermutated()方法返回true。如果列表的元素被更新,wasUpdated()方法返回true

并非列表的所有五种类型的更改都是排他的。某些变更可能会在同一个变更通知中同时发生。置换和更新这两种类型的改变是互斥的。如果您对处理所有类型的更改感兴趣,那么您在onChanged()方法中的代码应该如下所示:

public void onChanged(ListChangeListener.Change change) {
        while (change.next()) {
                if (change.wasPermutated()) {
                        // Handle permutations
                }
                else if (change.wasUpdated()) {
                        // Handle updates
                }
                else if (change.wasReplaced()) {
                        // Handle replacements
                }
                else {
                        if (change.wasRemoved()) {
                                // Handle removals
                        }
                        else if (change.wasAdded()) {
                                // Handle additions
                        }
                }
        }
}

在受影响的范围类型类别中,getFrom()getTo()方法报告受变更影响的索引范围。getFrom()方法返回开始索引,getTo()方法返回结束索引加 1。如果wasPermutated()方法返回true,则该范围包括被置换的元素。如果wasUpdated()方法返回true,则该范围包括被更新的元素。如果wasAdded()方法返回true,则该范围包括添加的元素。如果wasRemoved()方法返回truewasAdded()方法返回false,那么getFrom()getTo()方法返回相同的数字——移除的元素在列表中的位置的索引。

getAddedSize()方法返回添加的元素数量。getAddedSubList()方法返回一个包含添加元素的列表。getRemovedSize()方法返回移除的元素数量。getRemoved()方法返回一个不可变的被移除或替换元素的列表。getPermutation(int oldIndex)方法返回排列后元素的新索引。例如,如果在置换过程中,索引 2 处的元素移动到索引 5 处,getPermutation(2)将返回5

关于ListChangeListener.Change类的方法的讨论到此结束。但是,您还没有完成这个课程!我仍然需要讨论如何在实际情况下使用这些方法,例如,当列表的元素被更新时。我将在下一节介绍如何处理列表元素的更新。我将用一个涵盖所有讨论内容的例子来结束这个主题。

观察可观察列表的更新

在“创建一个可观察列表一节中,我已经列出了下面两个创建ObservableListFXCollections类的方法:

  • <E> ObservableList<E> observableArrayList(Callback<E, Observable[]> extractor)

  • <E> ObservableList<E> observableList(List<E> list, Callback<E, Observable[]> extractor)

如果您希望在列表元素更新时得到通知,您需要使用以下方法之一创建列表。这两种方法有一个共同点:它们接受一个Callback<E,Observable[]>对象作为参数。Callback<P,R>接口在javafx.util包中。其定义如下:

public interface Callback<P,R> {
        R call(P param)
}

Callback<P,R>接口用于 API 在以后合适的时间需要进一步动作的情况。第一个泛型类型参数指定传递给call()方法的参数的类型,第二个指定call()方法的返回类型。

如果您注意到Callback<E,Observable[]>中类型参数的声明,第一个类型参数是E,它是列表元素的类型。第二个参数是一个Observable数组。当您向列表中添加一个元素时,会调用Callback对象的call()方法。添加的元素作为参数传递给call()方法。你应该从call()方法返回一个Observable的数组。如果返回的Observable数组中的任何元素发生变化,监听器将被通知列表元素的“更新”变化,因为call()方法已经为该列表返回了Observable数组。

让我们看看为什么需要一个Callback对象和一个Observable数组来检测列表元素的更新。列表存储其元素的引用。它的元素可以在程序的任何地方使用它们的引用来更新。列表不知道它的元素正在从其他地方被更新。它需要知道Observable对象的列表,其中任何一个对象的改变都可能被认为是对其元素的更新。Callback对象的call()方法满足了这一要求。列表将每个元素传递给call()方法。call()方法返回一个Observable数组。该列表监视Observable数组元素的任何变化。当它检测到一个变化时,它通知它的变化监听器,它的与Observable数组相关的元素已经被更新。这个参数被命名为提取器的原因是它为一个列表元素提取一个数组Observable

清单 3-4 展示了如何创建一个ObservableList,当它的元素被更新时,它可以通知它的变化监听器。

// ListUpdateTest.java
package com.jdojo.collections;

import java.util.List;
import javafx.beans.Observable;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.util.Callback;

public class ListUpdateTest {
        public static void main(String[] args) {
            // Create an extractor for IntegerProperty.
            Callback<IntegerProperty, Observable[]> extractor =
                    (IntegerProperty p) -> {
                   // Print a message to know when it is called
                   System.out.println("The extractor is called for " + p);
                   // Wrap the parameter in an Observable[] and return it
                   return new Observable[]{p};
               };
            // Create an empty observable list with a callback to
            // extract the observable values for each element of the list
            ObservableList<IntegerProperty> list =
                FXCollections.observableArrayList(extractor);

            // Add two elements to the list

            System.out.println("Before adding two elements...");
            IntegerProperty p1 = new SimpleIntegerProperty(10);
            IntegerProperty p2 = new SimpleIntegerProperty(20);
            list.addAll(p1, p2); // Will call the call() method of the
                          // extractor - once for p1 and once for p2.
            System.out.println("After adding two elements...");

            // Add a change listener to the list
            list.addListener(ListUpdateTest::onChanged);

            // Update p1 from 10 to 100, which will trigger
            // an update change for the list
            p1.set(100);
        }

        public static void onChanged(
               ListChangeListener.Change<? extends IntegerProperty>
                    change) {
            System.out.println("List is " + change.getList());

            // Work on only updates to the list
            while (change.next()) {
                if (change.wasUpdated()) {
                    // Print the details of the update
                    System.out.println("An update is detected.");

                    int start = change.getFrom();
                    int end = change.getTo();
                    System.out.println("Updated range: [" + start +
                               ", " + end + "]");

                    List<? extends IntegerProperty> updatedElementsList;
                    updatedElementsList =
                               change.getList().subList(start, end);

                    System.out.println("Updated elements: " +
                                updatedElementsList);
                }
            }
        }

}
Before adding two elements...
The extractor is called for IntegerProperty [value: 10]
The extractor is called for IntegerProperty [value: 20]
After adding two elements...
List is [IntegerProperty [value: 100], IntegerProperty [value: 20]]
An update is detected.
Updated range: [0, 1]
Updated elements: [IntegerProperty [value: 100]]

Listing 3-4Observing a List for Updates of Its Elements

ListUpdateTest类的main()方法创建一个提取器,它是Callback<IntegerProperty, Observable[]>接口的一个对象。call()方法接受一个IntegerProperty参数,并将其包装在一个Observable数组中返回。它还打印传递给它的对象。

提取器用于创建一个ObservableList。两个IntegerProperty对象被添加到列表中。当添加对象时,提取器的call()方法被调用,添加的对象作为它的参数。从输出中可以明显看出这一点。call()方法返回被添加的对象。这意味着列表将监视对象(IntegerProperty)的任何变化,并通知它的变化监听器。

列表中会添加一个更改监听器。它只处理列表的更新。最后,您将列表中第一个元素的值从 10 更改为 100,以触发更新更改通知。

观察可观察列表变化的完整示例

本节提供了一个完整的例子,展示了如何处理对ObservableList的不同种类的更改。

我们的起点是一个Person类,如清单 3-5 所示。在这里,您将使用Person对象中的ObservableListPerson类有两个属性:firstNamelastName。两种属性都是StringProperty类型。它的compareTo()方法被实现来按照先名后姓的升序对Person对象进行排序。它的toString()方法打印名字、空格和姓氏。

// Person.java
package com.jdojo.collections;

import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;

public class Person implements Comparable<Person> {
        private StringProperty firstName = new SimpleStringProperty();
        private StringProperty lastName = new SimpleStringProperty();

        public Person() {
                this.setFirstName("Unknown");
                this.setLastName("Unknown");
        }

        public Person(String firstName, String lastName) {
                this.setFirstName(firstName);
                this.setLastName(lastName);
        }

        // Complete listing part of the example sources download for
        // the book
        ...
}

Listing 3-5A Person Class with Two Properties Named firstName and lastName

清单 3-6 中所示的PersonListChangeListener类是一个变更监听器类。它实现了ListChangeListener接口的onChanged()方法,为Person对象的ObservableList处理所有类型的变更通知。

// PersonListChangeListener.java
// Listing part of the example sources download for the book

Listing 3-6A Change Listener for an ObservableList of Person Objects

清单 3-7 中所示的ListChangeTest类是一个测试类。它创建了一个带有提取器的ObservableList。提取器返回一个Person对象的firstNamelastName属性的数组。这意味着当这些属性中的一个被改变时,作为列表元素的一个Person对象被认为是更新的,并且一个更新通知将被发送给所有的改变监听器。它将更改侦听器添加到列表中。最后,它对列表进行了几种更改,以触发更改通知。更改通知的详细信息打印在标准输出上。

这就完成了关于为ObservableList编写变更监听器的最复杂的讨论之一。JavaFX 的设计者没有把它变得更复杂,你难道不庆幸吗?

// ListChangeTest.java
// Listing part of the example sources download for the book
Before adding Li Na: []
Change Type: Added
Added Size: 1
Added Range: [0, 1]
Added List: [Li Na]
After adding Li Na: [Li Na]

Before adding Vivi Gin and Li He: [Li Na]
Change Type: Added

Added Size: 2
Added Range: [1, 3]
Added List: [Vivi Gin, Li He]
After adding Vivi Gin and Li He: [Li Na, Vivi Gin, Li He]

Before sorting the list:[Li Na, Vivi Gin, Li He]
Change Type: Permutated
Permutated Range: [0, 3]
index[0] moved to index[1]
index[1] moved to index[2]
index[2] moved to index[0]
After sorting the list:[Li He, Li Na, Vivi Gin]

Before updating Li Na: [Li He, Li Na, Vivi Gin]
Change Type: Updated
Updated Range : [1, 2]
Updated elements are: [Li Smith]
After updating Li Smith: [Li He, Li Smith, Vivi Gin]

Before replacing Li He with Simon Ng: [Li He, Li Smith, Vivi Gin]
Change Type: Replaced
Change Type: Removed
Removed Size: 1
Removed Range: [0, 1]
Removed List: [Li He]
Change Type: Added

Added Size: 1
Added Range: [0, 1]
Added List: [Simon Ng]
After replacing Li He with Simon Ng: [Simon Ng, Li Smith, Vivi Gin]

Before setAll(): [Simon Ng, Li Smith, Vivi Gin]
Change Type: Replaced
Change Type: Removed
Removed Size: 3
Removed Range: [0, 3]
Removed List: [Simon Ng, Li Smith, Vivi Gin]
Change Type: Added
Added Size: 3
Added Range: [0, 3]
Added List: [Lia Li, Liz Na, Li Ho]
After setAll(): [Lia Li, Liz Na, Li Ho]

Before removeAll(): [Lia Li, Liz Na, Li Ho]
Change Type: Removed
Removed Size: 1
Removed Range: [0, 0]
Removed List: [Lia Li]
Change Type: Removed

Removed Size: 1
Removed Range: [1, 1]
Removed List: [Li Ho]
After removeAll(): [Liz Na]

Listing 3-7Testing an ObservableList of Person Objects for All Types of Changes

了解可观察设置

如果您在学习了ObservableList和 list change listeners 之后还活着,那么学习ObservableSet将会很容易!图 3-3 显示了ObservableSet接口的类图。

img/336502_2_En_3_Fig3_HTML.jpg

图 3-3

ObservableSet接口的类图

它继承自SetObservable接口。它支持失效和变更通知,并且从Observable接口继承了失效通知支持的方法。它添加了以下两种方法来支持更改通知:

  • void addListener(SetChangeListener<? super E> listener)

  • void removeListener(SetChangeListener<? super E> listener)

SetChangeListener接口的一个实例监听ObservableSet中的变化。它声明了一个名为Change的静态内部类,表示一个ObservableSet中的变化报告。

Note

集合是一个无序的集合。本节显示了输出中几个集合的元素。您可能会得到不同的输出,以不同于示例中所示的顺序显示集合的元素。

创建一个可观察集合

您需要使用FXCollections类的以下工厂方法之一来创建一个ObservableSet:

  • <E> ObservableSet<E> observableSet(E... elements)

  • <E> ObservableSet<E> observableSet(Set<E> set)

  • <E> ObservableSet<E> emptyObservableSet()

由于使用可观察集合与使用可观察列表没有太大的不同,我们不进一步研究这个主题。您可以参考 API 文档和com.jdojo.collections包中的示例类来了解更多关于可观察集的信息。

理解观察图

图 3-4 显示了ObservableMap接口的类图。它继承自MapObservable接口。它支持失效和更改通知。它从Observable接口继承了无效通知支持的方法,并增加了以下两个方法来支持变更通知:

img/336502_2_En_3_Fig4_HTML.jpg

图 3-4

ObservableMap接口的类图

  • void addListener(MapChangeListener<? super K, ? super V> listener)

  • void removeListener(MapChangeListener<? super K, ? super V> listener)

MapChangeListener接口的一个实例监听ObservableMap中的变化。它声明了一个名为Change的静态内部类,表示一个ObservableMap中的变化报告。

创建一个可观察地图

您需要使用FXCollections类的以下工厂方法之一来创建一个ObservableMap:

  • <K,V> ObservableMap<K, V> observableHashMap()

  • <K,V> ObservableMap<K, V> observableMap(Map<K, V> map)

  • <K,V> ObservableMap<K,V> emptyObservableMap()

第一种方法创建一个由HashMap支持的空的可观察地图。第二种方法创建一个由指定地图支持的ObservableMap。在ObservableMap上执行的突变被报告给监听器。直接在支持映射上执行的突变不会报告给监听器。第三种方法创建一个空的不可修改的可观察图。清单 3-8 展示了如何创建ObservableMap s。

// ObservableMapTest.java
package com.jdojo.collections;

import java.util.HashMap;
import java.util.Map;
import javafx.collections.FXCollections;
import javafx.collections.ObservableMap;

public class ObservableMapTest {
        public static void main(String[] args) {
            ObservableMap<String, Integer> map1 =
                    FXCollections.observableHashMap();

            map1.put("one", 1);
            map1.put("two", 2);
            System.out.println("Map 1: " + map1);

            Map<String, Integer> backingMap = new HashMap<>();
            backingMap.put("ten", 10);
            backingMap.put("twenty", 20);

            ObservableMap<String, Integer> map2 =
                    FXCollections.observableMap(backingMap);
            System.out.println("Map 2: " + map2);
        }

}
Map 1: {two=2, one=1}
Map 2: {ten=10, twenty=20}

Listing 3-8Creating ObservableMaps

因为使用可观察的地图与使用可观察的列表和集合没有太大的不同,所以我们不进一步研究这个主题。您可以参考 API 文档和com.jdojo.collections包中的示例类来了解更多关于可观察地图的信息。

JavaFX 集合的属性和绑定

可以将ObservableListObservableSetObservableMap集合公开为Property对象。它们还支持使用高级和低级绑定 API 的绑定。代表单一值的属性对象在第二章中讨论过。在继续本节之前,请确保您已经阅读了该章。

了解 ObservableList 属性和绑定

图 3-5 显示了ListProperty类的部分类图。ListProperty类实现了ObservableValu e 和ObservableList接口。它是一个可观察的值,因为它包含了一个ObservableList的参考。实现ObservableList接口使得它的所有方法对一个ListProperty对象可用。在ListProperty上调用ObservableList的方法与在被包装的ObservableList上调用它们具有相同的效果。

img/336502_2_En_3_Fig5_HTML.jpg

图 3-5

ListProperty类的部分类图

您可以使用SimpleListProperty类的以下构造器之一来创建ListProperty的实例:

  • SimpleListProperty()

  • SimpleListProperty(ObservableList<E> initialValue)

  • SimpleListProperty(Object bean, String name)

  • SimpleListProperty(Object bean, String name, ObservableList<E> initialValue)

使用ListProperty类的一个常见错误是在使用之前没有将ObservableList传递给它的构造器。在对其执行有意义的操作之前,ListProperty必须有对ObservableList的引用。如果不使用ObservableList来创建ListProperty对象,可以使用它的set()方法来设置ObservableList的引用。以下代码片段会生成一个异常:

ListProperty<String> lp = new SimpleListProperty<String>();

// No ObservableList to work with. Generates an exception.
lp.add("Hello");
Exception in thread "main" java.lang.UnsupportedOperationException
        at java.util.AbstractList.add(AbstractList.java:148)
        at java.util.AbstractList.add(AbstractList.java:108)
        at javafx.beans.binding.ListExpression.add(ListExpression.java:262)

Tip

在包装了null引用的ListProperty上执行的操作被视为在不可变的空ObservableList上执行的操作。

下面的代码片段展示了如何在使用之前创建和初始化一个ListProperty:

ObservableList<String> list1 = FXCollections.observableArrayList();
ListProperty<String> lp1 = new SimpleListProperty<String>(list1);
lp1.add("Hello");

ListProperty<String> lp2 = new SimpleListProperty<String>();
lp2.set(FXCollections.observableArrayList());
lp2.add("Hello");

观察列表属性的变化

您可以将三种类型的监听器附加到一个ListProperty:

  • 一个InvalidationListener

  • ChangeListener

  • ListChangeListener

当包装在ListProperty中的ObservableList的引用发生变化或者ObservableList的内容发生变化时,所有三个监听器都会得到通知。当列表的内容改变时,ChangeListenerschanged()方法接收对相同列表的引用作为新旧值。如果ObservableList的包装引用被一个新的替换,这个方法接收旧列表和新列表的引用。要处理列表更改事件,请参考本章中的“观察一个可观察列表的更改”一节。

清单 3-9 中的程序展示了如何处理对一个ListProperty的所有三种类型的改变。列表更改监听器以简单通用的方式处理列表内容的更改。具体如何处理一个ObservableList的内容变化事件,请参见本章“观察一个观察列表的变化”一节。

// ListPropertyTest.java
// Listing part of the example sources download for the book

Before addAll()
List property is invalid.
List Property has changed. Old List: [one, two, three], New List: [one, two, three]
Action taken on the list: Added. Removed: [], Added: [one, two, three]
After addAll()

Before set()
List property is invalid.
List Property has changed. Old List: [one, two, three], New List: [two, three]
Action taken on the list: Replaced. Removed: [one, two, three], Added: [two, three]
After set()

Before remove()
List property is invalid

.
List Property has changed. Old List: [three], New List: [three]
Action taken on the list: Removed. Removed: [two], Added: []
After remove()

Listing 3-9Adding Invalidation, Change, and List Change Listeners to a ListProperty

绑定列表属性大小属性

一个ListProperty公开了两个属性,sizeempty,它们分别属于类型ReadOnlyIntegerPropertyReadOnlyBooleanProperty。您可以使用sizeProperty()emptyProperty()方法访问它们。sizeempty属性对于 GUI 应用程序中的绑定非常有用。例如,GUI 应用程序中的模型可能由一个ListProperty支持,您可以将这些属性绑定到屏幕上标签的 text 属性。当模型中的数据发生变化时,标签会通过绑定自动更新。sizeempty属性在ListExpression类中声明。

清单 3-10 中的程序展示了如何使用sizeempty属性。它使用ListExpression类的asString()方法将包装的ObservableList内容转换为String

// ListBindingTest.java
package com.jdojo.collections;

import javafx.beans.property.ListProperty;
import javafx.beans.property.SimpleListProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections;

public class ListBindingTest {
        public static void main(String[] args) {
            ListProperty<String> lp =
                        new SimpleListProperty<>(FXCollections.observableArrayList());

            // Bind the size and empty properties of the ListProperty
            // to create a description of the list
            StringProperty initStr = new SimpleStringProperty("Size: " );
            StringProperty desc = new SimpleStringProperty();
            desc.bind(initStr.concat(lp.sizeProperty())
                             .concat(", Empty: ")
                             .concat(lp.emptyProperty())
                             .concat(", List: ")
                             .concat(lp.asString()));

            System.out.println("Before addAll(): " + desc.get());
            lp.addAll("John", "Jacobs");
            System.out.println("After addAll(): " + desc.get());
        }

}
Before addAll(): Size: 0, Empty: true, List: []
After addAll(): Size: 2, Empty: false, List: [John, Jacobs]

Listing 3-10Using the size and empty Properties of a ListProperty Object

绑定到列表属性和内容

支持列表属性高级绑定的方法在ListExpressionBindings类中。低级绑定可以通过子类化ListBinding类来创建。一个ListProperty支持两种类型的绑定:

  • 绑定它所包装的ObservableList的引用

  • 绑定它所包装的ObservableList的内容

bind()bindBidirectional()方法用于创建第一种绑定。清单 3-11 中的程序展示了如何使用这些方法。如下面的输出所示,注意两个列表属性在绑定后都引用了同一个ObservableList

// BindingListReference.java
package com.jdojo.collections;

import javafx.beans.property.ListProperty;
import javafx.beans.property.SimpleListProperty;
import javafx.collections.FXCollections;

public class BindingListReference {

        public static void main(String[] args) {
            ListProperty<String> lp1 =
                new SimpleListProperty<>(
                         FXCollections.observableArrayList());
            ListProperty<String> lp2 =
                new SimpleListProperty<>(
                         FXCollections.observableArrayList());

            lp1.bind(lp2);

            print("Before addAll():", lp1, lp2);
            lp1.addAll("One", "Two");
            print("After addAll():", lp1, lp2);

            // Change the reference of the ObservableList in lp2
            lp2.set(FXCollections.observableArrayList("1", "2"));
            print("After lp2.set():", lp1, lp2);

            // Cannot do the following as lp1 is a bound property
            // lp1.set(FXCollections.observableArrayList("1", "2"));
            // Unbind lp1
            lp1.unbind();
            print("After unbind():", lp1, lp2);

            // Bind lp1 and lp2 bidirectionally

            lp1.bindBidirectional(lp2);
            print("After bindBidirectional():", lp1, lp2);

            lp1.set(FXCollections.observableArrayList("X", "Y"));
            print("After lp1.set():", lp1, lp2);
        }

        public static void print(String msg, ListProperty<String> lp1,
                   ListProperty<String> lp2) {
            System.out.println(msg);
            System.out.println("lp1: " + lp1.get() + ", lp2: " +
                    lp2.get() + ", lp1.get() == lp2.get(): " +
                    (lp1.get() == lp2.get()));
            System.out.println("---------------------------");
        }
}
Before addAll():
lp1: [], lp2: [], lp1.get() == lp2.get(): true
---------------------------
After addAll():
lp1: [One, Two], lp2: [One, Two], lp1.get() == lp2.get(): true
---------------------------
After lp2.set():
lp1: [1, 2], lp2: [1, 2], lp1.get() == lp2.get(): true
---------------------------
After unbind():
lp1: [1, 2], lp2: [1, 2], lp1.get() == lp2.get(): true
---------------------------
After bindBidirectional():
lp1: [1, 2], lp2: [1, 2], lp1.get() == lp2.get(): true

---------------------------
After lp1.set():
lp1: [X, Y], lp2: [X, Y], lp1.get() == lp2.get(): true
---------------------------

Listing 3-11Binding the References of List Properties

通过bindContent()bindContentBidirectional()方法,您可以分别在一个方向和两个方向上将包装在ListProperty中的ObservableList的内容绑定到另一个ObservableList的内容。确保使用相应的方法unbindContent()unbindContentBidirectional()来解除两个可观察列表的内容绑定。

Tip

您还可以使用Bindings类的方法来为可观察列表的引用和内容创建绑定。

允许更改一个内容已经绑定到另一个ObservableListListProperty的内容,但这并不可取。在这种情况下,绑定的ListProperty将不会与其目标列表同步。清单 3-12 展示了这两种内容绑定的例子。

// BindingListContent.java
package com.jdojo.collections;

import javafx.beans.property.ListProperty;
import javafx.beans.property.SimpleListProperty;
import javafx.collections.FXCollections;

public class BindingListContent {

        public static void main(String[] args) {
            ListProperty<String> lp1 =
                new SimpleListProperty<>(
                         FXCollections.observableArrayList());
            ListProperty<String> lp2 =
                new SimpleListProperty<>(
                         FXCollections.observableArrayList());

            // Bind the content of lp1 to the content of lp2
            lp1.bindContent(lp2);

            /* At this point, you can change the content of lp1\. However,
             * that will defeat the purpose of content binding, because
             * the content of lp1 is no longer in sync with the content of
             * lp2.
             * Do not do this:
             * lp1.addAll("X", "Y");
             */
            print("Before lp2.addAll():", lp1, lp2);
            lp2.addAll("1", "2");
            print("After lp2.addAll():", lp1, lp2);

            lp1.unbindContent(lp2);
            print("After lp1.unbindContent(lp2):", lp1, lp2);

            // Bind lp1 and lp2 contents bidirectionally

            lp1.bindContentBidirectional(lp2);

            print("Before lp1.addAll():", lp1, lp2);
            lp1.addAll("3", "4");
            print("After lp1.addAll():", lp1, lp2);

            print("Before lp2.addAll():", lp1, lp2);
            lp2.addAll("5", "6");
            print("After lp2.addAll():", lp1, lp2);
        }

        public static void print(String msg, ListProperty<String> lp1,
                   ListProperty<String> lp2) {
            System.out.println(msg + " lp1: " + lp1.get() +
                    ", lp2: " + lp2.get());
        }

}
Before lp2.addAll(): lp1: [], lp2: []
After lp2.addAll(): lp1: [1, 2], lp2: [1, 2]
After lp1.unbindContent(lp2): lp1: [1, 2], lp2: [1, 2]
Before lp1.addAll(): lp1: [1, 2], lp2: [1, 2]
After lp1.addAll(): lp1: [1, 2, 3, 4], lp2: [1, 2, 3, 4]
Before lp2.addAll(): lp1: [1, 2, 3, 4], lp2: [1, 2, 3, 4]
After lp2.addAll(): lp1: [1, 2, 3, 4, 5, 6], lp2: [1, 2, 3, 4, 5, 6]

Listing 3-12Binding Contents of List Properties

绑定到列表的元素

提供了如此多有用的特性,以至于我可以继续讨论这个话题至少 50 多页!我将用另外一个例子来结束这个话题。

可以使用ListExpression类的以下方法之一绑定到包装在ListProperty中的ObservableList的特定元素:

  • ObjectBinding<E> valueAt(int index)

  • ObjectBinding<E> valueAt(ObservableIntegerValue index)

该方法的第一个版本为列表中特定索引处的元素创建一个ObjectBinding。该方法的第二个版本将一个索引作为参数,它是一个可以随时间变化的ObservableIntegerValue。当valueAt()方法中的绑定索引在列表范围之外时,ObjectBinding包含null

让我们使用该方法的第二个版本来创建一个绑定,它将绑定到列表的最后一个元素。在这里,您可以利用ListPropertysize属性来创建绑定表达式。清单 3-13 中的程序展示了如何使用valueAt()方法。

// BindingToListElements.java
// Listing part of the example sources download for the book
List:[], Last Value: null
List:[John], Last Value: John
List:[John, Donna, Geshan], Last Value: Geshan
List:[John, Donna], Last Value: Donna
List:[], Last Value: null

Listing 3-13Binding to the Elements of a List

了解 ObservableSet 属性和绑定

一个SetProperty对象包装了一个ObservableSet。和SetProperty一起工作和和ListProperty一起工作非常相似。我不打算重复前面几节中讨论的关于ObservableList的属性和绑定的内容。同样的讨论也适用于ObservableSet的属性和绑定。以下是使用SetProperty时需要记住的要点:

  • SetProperty类的类图类似于图 3-5 中ListProperty类的类图。您需要将所有名称中的单词“List”替换为“Set”。

  • SetExpressionBindings类包含支持设置属性的高级绑定的方法。你需要子类化SetBinding类来创建底层绑定。

  • ListProperty一样,SetProperty公开了sizeempty属性。

  • ListProperty一样,SetProperty支持引用和它所包装的ObservableSet内容的绑定。

  • ListProperty一样,SetProperty支持三种类型的通知:失效通知、更改通知和设置更改通知。

  • 与列表不同,集合是项目的无序集合。它的元素没有索引。它不支持绑定到其特定元素。因此,SetExpression类不像ListExpression类那样包含类似于valueAt()的方法。

您可以使用SimpleSetProperty类的以下构造器之一来创建SetProperty的实例:

  • SimpleSetProperty()

  • SimpleSetProperty(ObservableSet<E> initialValue)

  • SimpleSetProperty(Object bean, String name)

  • SimpleSetProperty(Object bean, String name, ObservableSet<E> initialValue)

下面的代码片段创建了一个SetProperty的实例,并向属性包装的ObservableSet添加了两个元素。最后,它使用get()方法从属性对象中获取ObservableSet的引用:

// Create a SetProperty object
SetProperty<String> sp = new SimpleSetProperty<String>(FXCollections.observableSet());

// Add two elements to the wrapped ObservableSet
sp.add("one");
sp.add("two");

// Get the wrapped set from the sp property

ObservableSet<String> set = sp.get();

清单 3-14 中的程序演示了如何绑定SetProperty对象。

// SetBindingTest.java
// Listing part of the example sources download for the book
Before sp1.add(): Size: 0, Empty: true, Set: []
After sp1.add(): Size: 2, Empty: false, Set: [Jacobs, John]
Called sp1.bindContent(sp2)...
Before sp2.add(): sp1: [], sp2: []
After sp2.add(): sp1: [1], sp2: [1]
After sp1.unbindContent(sp2): sp1: [1], sp2: [1]
Before sp2.add(): sp1: [1], sp2: [1]
After sp2.add(): sp1: [1, 2], sp2: [2, 1]

Listing 3-14Using Properties and Bindings for Observable Sets

理解 ObservableMap 属性和绑定

一个MapProperty对象包装了一个ObservableMap。和MapProperty一起工作和和ListProperty一起工作非常相似。我不打算重复前面几节中讨论的关于ObservableList的属性和绑定的内容。同样的讨论也适用于ObservableMap的属性和绑定。以下是使用MapProperty时需要记住的要点:

  • MapProperty类的类图类似于图 3-5 中ListProperty类的类图。您需要将所有名称中的单词“List”替换为单词“map”,将泛型类型参数<E>替换为<K, V>,其中 K 和 V 分别代表 Map 中条目的键类型和值类型。

  • MapExpressionBindings类包含支持地图属性高级绑定的方法。你需要子类化MapBinding类来创建底层绑定。

  • ListProperty一样,MapProperty公开了sizeempty属性。

  • ListProperty一样,MapProperty支持引用和它所包装的ObservableMap内容的绑定。

  • ListProperty一样,MapProperty支持三种类型的通知:无效通知、更改通知和地图更改通知。

  • MapProperty支持使用其valueAt()方法绑定到特定键值。

使用下列SimpleMapProperty类的构造器之一创建MapProperty的实例:

  • SimpleMapProperty()

  • SimpleMapProperty(Object bean, String name)

  • SimpleMapProperty(Object bean, String name, ObservableMap<K,V> initialValue)

  • SimpleMapProperty(ObservableMap<K,V> initialValue)

下面的代码片段创建了一个MapProperty的实例,并添加了两个条目。最后,它使用get()方法获得被包装的ObservableMap的引用:

// Create a MapProperty object
MapProperty<String, Double> mp =
        new SimpleMapProperty<String, Double>(FXCollections.observableHashMap());

// Add two entries to the wrapped ObservableMap
mp.put("Ken", 8190.20);
mp.put("Jim", 8990.90);

// Get the wrapped map from the mp property
ObservableMap<String, Double> map = mp.get();

清单 3-15 中的程序展示了如何绑定MapProperty对象。它显示了两个地图之间的内容绑定。您还可以在两个地图属性之间使用单向和双向简单绑定来绑定它们包装的地图的引用。

// MapBindingTest.java
// Listing part of the example sources download for the book
Ken Salary: null
Before mp1.put(): Size: 0, Empty: true, Map: {}, Ken Salary: null
After mp1.put(): Size: 3, Empty: false, Map: {Jim=9800.8, Lee=6000.2, Ken=7890.9}, Ken Salary: 7890.9
Called mp1.bindContent(mp2)...
Before mp2.put(): Size: 0, Empty: true, Map: {}, Ken Salary: null
After mp2.put(): Size: 2, Empty: false, Map: {Cindy=7800.2, Ken=7500.9}, Ken Salary: 7500.9

Listing 3-15Using Properties and Bindings for Observable Maps

摘要

JavaFX 通过添加对可观察列表、集合和映射(称为可观察集合)的支持,扩展了 Java 中的集合框架。可观察集合是一个列表、集合或映射,可以观察到它的失效和内容变化。javafx.collections包中的ObservableListObservableSetObservableMap接口的实例代表 JavaFX 中可观察到的接口。您可以向这些可观察集合的实例添加失效和更改侦听器。

FXCollections类是一个使用 JavaFX 集合的实用程序类。它由所有静态方法组成。JavaFX 不公开可观察列表、集合和映射的实现类。您需要使用FXCollections类中的一个工厂方法来创建ObservableListObservableSetObservableMap接口的对象。

JavaFX 库提供了两个名为FilteredListSortedList的类,它们在javafx.collections.transformation包中。一个FilteredList是一个ObservableList,它使用一个指定的Predicate过滤它的内容。A SortedList对其内容进行排序。

下一章将讨论如何在 JavaFX 应用程序中创建和定制 stages。

四、管理舞台

在本章中,您将学习:

  • 如何获取屏幕的详细信息,如数量、分辨率和尺寸

  • JavaFX 中的舞台是什么,以及如何设置舞台的边界和样式

  • 如何移动未装饰的舞台

  • 如何设置舞台的形态和不透明度

  • 如何调整舞台的大小以及如何在全屏模式下显示舞台

本章的例子在com.jdojo.stage包中。为了让它们工作,您必须在module-info.java文件中添加相应的一行:

...
opens com.jdojo.stage to javafx.graphics, javafx.base;
...

了解屏幕的细节

javafx.stage包中的Screen类用于获取细节,例如,每英寸点数(DPI)设置和用户屏幕(或显示器)的尺寸。如果多个屏幕连接到一台计算机,其中一个屏幕被称为主屏幕,其他屏幕被称为非主屏幕。您可以使用Screen类的静态getPrimary()方法,用下面的代码获取主监视器的Screen对象的引用:

// Get the reference to the primary screen
Screen primaryScreen = Screen.getPrimary();

静态的getScreens()方法返回一个Screen对象的ObservableList:

ObservableList<Screen> screenList = Screen.getScreens();

您可以使用Screen类的getDpi()方法获得 DPI 中屏幕的分辨率,如下所示:

Screen primaryScreen = Screen.getPrimary();
double dpi = primaryScreen.getDpi();

您可以使用getBounds()getVisualBounds()方法分别获得边界和可视边界。这两个方法都返回一个Rectangle2D对象,该对象封装了一个矩形的左上角和右下角的(x,y)坐标、宽度和高度。getMinX()getMinY()方法分别返回矩形左上角的 x 和 y 坐标。getMaxX()getMaxY()方法分别返回矩形右下角的 x 和 y 坐标。getWidth()getHeight()方法分别返回矩形的宽度和高度。

屏幕的边界覆盖了屏幕上可用的区域。可视边界表示在考虑本机窗口系统使用的区域(如任务栏和菜单)后,屏幕上可供使用的区域。通常,但不是必须的,屏幕的可视边界表示比其边界更小的区域。

如果桌面跨越多个屏幕,非主屏幕的边界相对于主屏幕。例如,如果桌面跨越两个屏幕,主屏幕左上角的(x,y)坐标为(0,0),宽度为 1600,则第二个屏幕左上角的坐标为(1600,0)。

清单 4-1 中的程序在有两个屏幕的 Windows 桌面上运行时打印屏幕细节。您可能会得到不同的输出。请注意一个屏幕的边界和可视边界的高度差异,而另一个屏幕则没有。主屏幕在底部显示一个任务栏,从可视边界中去掉部分高度。非主屏幕不显示任务栏,因此它的边界和可视边界是相同的。

Tip

虽然在 API 文档中没有提到Screen类,但是在 JavaFX 启动器启动之前,您不能使用这个类。也就是说,您无法在非 JavaFX 应用程序中获得屏幕描述。这就是为什么您要在 JavaFX 应用程序类的start()方法中编写代码的原因。不需要在 JavaFX 应用程序线程上使用Screen类。您也可以在您的类的init()方法中编写相同的代码。

// ScreenDetailsApp.java
package com.jdojo.stage;

import javafx.application.Application;
import javafx.application.Platform;
import javafx.collections.ObservableList;
import javafx.geometry.Rectangle2D;
import javafx.stage.Screen;
import javafx.stage.Stage;

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

       public void start(Stage stage) {
               ObservableList<Screen> screenList = Screen.getScreens();
               System.out.println("Screens Count: " + screenList.size());

               // Print the details of all screens
               for(Screen screen: screenList) {
                      print(screen);
               }

               Platform.exit();
       }

       public void print(Screen s) {
               System.out.println("DPI: " + s.getDpi());

               System.out.print("Screen Bounds: ");
               Rectangle2D bounds = s.getBounds();
               print(bounds);

               System.out.print("Screen Visual Bounds: ");
               Rectangle2D visualBounds = s.getVisualBounds();
               print(visualBounds);
               System.out.println("-----------------------");
       }

       public void print(Rectangle2D r) {
               System.out.format("minX=%.2f, minY=%.2f, width=%.2f,
                        height=%.2f%n",
                        r.getMinX(), r.getMinY(),
                        r.getWidth(), r.getHeight());
       }
}
Screens Count: 2
DPI: 96.0
Screen Bounds: minX=0.00, minY=0.00, width=1680.00, height=1050.00
Screen Visual Bounds: minX=0.00, minY=0.00, width=1680.00, height=1022.00
-----------------------
DPI: 96.0
Screen Bounds: minX = 1680.00, minY=0.00, width= 1680.00, height=1050.00
Screen Visual Bounds: minX = 1680.00, minY=0.00, width= 1680.00, height=1050.0
-----------------------

Listing 4-1Accessing Screen Details

什么是舞台?

JavaFX 中的舞台是承载场景的顶级容器,场景由可视元素组成。javafx.stage包中的Stage类表示 JavaFX 应用程序中的一个舞台。初级舞台由平台创建,并传递给Application类的start(Stage s)方法。您可以根据需要创建其他舞台。

Tip

JavaFX 应用程序中的舞台是顶级容器。这并不一定意味着它总是显示为一个单独的窗口。然而,对于本书的目的来说,一个舞台对应一个窗口,除非另有说明。

图 4-1 显示了从Window类继承而来的Stage类的类图。Window类是几个窗口行容器类的超类。它包含所有类型窗口共有的基本功能(例如,显示和隐藏窗口的方法;设置 x、y、宽度和高度属性。设置窗口的不透明度;等等。).Window类定义了xywidthheightopacity属性。它有show()hide()方法分别显示和隐藏一个窗口。Window类的setScene()方法为窗口设置场景。Stage类定义了一个close()方法,与调用Window类的hide()方法效果相同。

img/336502_2_En_4_Fig1_HTML.png

图 4-1

Stage类的类图

必须在 JavaFX 应用程序线程上创建和修改一个Stage对象。回想一下在 JavaFX 应用程序线程上调用了Application类的start()方法,并且创建了一个主Stage并将其传递给该方法。注意,通过start()方法的初级舞台没有显示出来。需要调用show()方法来展示。

需要讨论使用舞台的几个方面。在接下来的部分中,我将从基础到高级逐一处理它们。

显示初级舞台

让我们从最简单的 JavaFX 应用程序开始,如清单 4-2 所示。start()方法没有代码。当您运行应用程序时,您看不到窗口,也看不到控制台上的输出。应用程序将永远运行。您需要使用特定于系统的键来取消应用程序。如果你用的是 Windows,用你最喜欢的组合键 Ctrl + Alt + Del 来激活任务管理器!如果使用命令提示符,请使用 Ctrl + C。

// EverRunningApp.java
package com.jdojo.stage;

import javafx.application.Application;
import javafx.stage.Stage;

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

       @Override
       public void start(Stage stage) {
               // Do not write any code here
       }
}

Listing 4-2An Ever-Running JavaFX Application

要确定清单 4-2 中的程序有什么问题,您需要理解 JavaFX 应用程序启动器是做什么的。回想一下,当调用Platform.exit()方法或关闭最后一个显示的舞台时,JavaFX 应用程序线程被终止。当所有非守护进程线程死亡时,JVM 终止。JavaFX 应用程序线程是非守护进程线程。当 JavaFX 应用程序线程终止时,Application.launch()方法返回。在前面的示例中,无法终止 JavaFX 应用程序线程。这就是应用程序永远运行的原因。

start()方法中使用Platform.exit()方法可以解决这个问题。清单 4-3 中显示了start()方法的修改代码。当你运行程序时,它不做任何有意义的事情就退出了。

// ShortLivedApp.java
package com.jdojo.stage;

import javafx.application.Application;
import javafx.application.Platform;
import javafx.stage.Stage;

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

       @Override
       public void start(Stage stage) {
               Platform.exit(); // Exit the application
       }
}

Listing 4-3A Short-Lived JavaFX Application

让我们通过关闭初级舞台来尝试修复一直运行的程序。调用start()方法时只有一个舞台,关闭它应该会终止 JavaFX 应用程序线程。让我们用下面的代码修改EverRunningAppstart()方法:

@Override
public void start(Stage stage) {
       stage.close(); // Close the only stage you have
}

即使有了这个用于start()方法的代码,EverRunningApp也会永远运行。如果舞台没有显示,则close()方法不会关闭舞台。初级舞台从未显示。因此,向start()方法添加一个stage.close()调用没有任何好处。下面的代码适用于start()方法。但是,这将导致舞台显示和关闭时屏幕闪烁:

@Override
public void start(Stage stage) {
       stage.show();  // First show the stage
       stage.close(); // Now close it
}

Tip

Stage类的close()方法与调用Window类的hide()方法具有相同的效果。JavaFX API 文档没有提到试图关闭一个不显示的窗口没有任何效果。

设定舞台的界限

舞台的边界由四个属性组成:xywidthheightxy属性决定舞台左上角的位置。widthheight属性决定了它的大小。在本节中,您将学习如何在屏幕上定位舞台并调整其大小。您可以使用这些属性的 getters 和 setters 来获取和设置它们的值。

让我们从一个简单的例子开始,如清单 4-4 所示。程序在显示前设置初级舞台的标题。当您运行这段代码时,您会看到一个带有标题栏、边框和空白区域的窗口。如果打开了其他应用程序,您可以透过舞台的透明区域看到它们的内容。窗口的位置和大小由平台决定。

Tip

当舞台没有场景并且其位置和大小没有明确设置时,其位置和大小由平台确定和设置。

// BlankStage.java
package com.jdojo.stage;

import javafx.application.Application;
import javafx.stage.Stage;

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

       @Override
       public void start(Stage stage) {
               stage.setTitle("Blank Stage");
               stage.show();
       }
}

Listing 4-4Displaying a Stage with No Scene and with the Platform Default Position and Size

让我们稍微修改一下逻辑。在这里,您将为舞台设置一个空场景,而不设置场景的大小。修改后的start()方法如下所示:

import javafx.scene.Group;
import javafx.scene.Scene;
...
@Override
public void start(Stage stage) {
       stage.setTitle("Stage with an Empty Scene");
       Scene scene = new Scene(new Group());
       stage.setScene(scene);
       stage.show();
}

请注意,您已经设置了一个没有子节点的Group作为场景的根节点,因为没有根节点就无法创建场景。当您使用前面的代码作为其start()方法运行清单 4-4 中的程序时,载物台的位置和大小由平台决定。这一次,内容区域将具有白色背景,因为场景的默认背景颜色是白色。

我们再修改一下逻辑。这里,我们给场景添加一个按钮。修改后的start()方法如下:

import javafx.scene.control.Button;
...
@Override
public void start(Stage stage) {
       stage.setTitle("Stage with a Button in the Scene");
       Group root = new Group(new Button("Hello"));
       Scene scene = new Scene(root);
       stage.setScene(scene);
       stage.show();
}

当您使用前面的代码作为其start()方法运行清单 4-4 中的程序时,舞台的位置和大小由场景的计算大小决定。舞台的内容区域足够宽,可以显示标题栏菜单或场景的内容,以较大者为准。舞台的内容区域足够高,可以显示场景的内容,在本例中只有一个按钮。载物台位于屏幕中央,如图 4-2 所示。

img/336502_2_En_4_Fig2_HTML.png

图 4-2

具有包含按钮的场景的舞台,其中场景的大小未指定

让我们通过在场景中添加一个按钮,并将场景的宽度和高度分别设置为 300 和 100,为逻辑添加另一个扭曲,如下所示:

@Override
public void start(Stage stage) {
       stage.setTitle("Stage with a Sized Scene");
       Group root = new Group(new Button("Hello"));
       Scene scene = new Scene(root, 300, 100);
       stage.setScene(scene);
       stage.show();
}

当您使用前面的代码作为其start()方法运行清单 4-4 中的程序时,舞台的位置和大小由场景的指定大小决定。舞台的内容区域与场景的指定大小相同。舞台的宽度包括两侧的边框,舞台的高度包括标题栏和下边框的高度。载物台位于屏幕中央,如图 4-3 所示。

img/336502_2_En_4_Fig3_HTML.jpg

图 4-3

具有特定大小场景的舞台

让我们在逻辑上再加一个转折。您将使用以下代码设置场景和舞台的大小:

@Override
public void start(Stage stage) {
       stage.setTitle("A Sized Stage with a Sized Scene");
       Group root = new Group(new Button("Hello"));
       Scene scene = new Scene(root, 300, 100);
       stage.setScene(scene);
       stage.setWidth(400);
       stage.setHeight(100);
       stage.show();
}

当您使用前面的代码作为其start()方法运行清单 4-4 中的程序时,舞台的位置和大小由舞台的指定大小决定。舞台在屏幕上居中,然后看起来如图 4-4 所示。

img/336502_2_En_4_Fig4_HTML.jpg

图 4-4

大小合适的舞台和大小合适的场景

Tip

舞台的默认居中方式是在屏幕上水平居中。舞台左上角的 y 坐标是屏幕高度的三分之一减去舞台高度。这是在Window类的centerOnScreen()方法中使用的逻辑。

让我回顾一下定位和调整舞台大小的规则。如果没有指定舞台的界限,并且

  • 它没有场景,它的边界由平台决定。

  • 它有一个没有可视节点的场景,它的边界由平台决定。在这种情况下,不指定场景的大小。

  • 它有一个带有视觉节点的场景,它的边界由场景中的视觉节点决定。在这种情况下,不指定场景的大小,舞台在屏幕上居中。

  • 它有一个场景,场景的大小是指定的,它的边界由场景的指定大小决定。舞台在屏幕中央。

如果您指定了舞台的大小,但没有指定其位置,则舞台将根据设置的大小调整大小,并在屏幕上居中,而不考虑是否存在场景以及场景的大小。如果您指定载物台的位置(x,y 坐标),它会相应地定位。

Tip

如果您想要设置舞台的宽度和高度以适合其场景的内容,请使用Window类的sizeToScene()方法。如果您希望在运行时修改场景后将舞台的大小与其场景的大小同步,则方法非常有用。使用Window类的centerOnScreen()方法使舞台在屏幕上居中。

如果您希望舞台在屏幕上水平和垂直居中,请使用以下逻辑:

Rectangle2D bounds = Screen.getPrimary().getVisualBounds();
double x = bounds.getMinX() + (bounds.getWidth() - stage.getWidth())/2.0;
double y = bounds.getMinY() + (bounds.getHeight() - stage.getHeight())/2.0;
stage.setX(x);
stage.setY(y);

使用前面的代码片段时要小心。它利用了舞台的大小。舞台的大小直到第一次演出时才知道。在显示舞台之前使用前面的逻辑不会真正使舞台在屏幕上居中。JavaFX 应用程序的以下start()方法将无法正常工作:

@Override
public void start(Stage stage) {
       stage.setTitle("A Truly Centered Stage");
       Group root = new Group(new Button("Hello"));
       Scene scene = new Scene(root);
       stage.setScene(scene);

       // Wrong!!!! Use the logic shown below after the stage.show() call
       // At this point, stage width and height are not known. They are NaN.
       Rectangle2D bounds = Screen.getPrimary().getVisualBounds();
       double x = bounds.getMinX() + (bounds.getWidth() –
                 stage.getWidth())/2.0;
       double y = bounds.getMinY() + (bounds.getHeight() –
                 stage.getHeight())/2.0;
       stage.setX(x);
       stage.setY(y);

       stage.show();
}

初始化舞台的样式

舞台的区域可以分为两部分:内容区和装饰区。内容区域显示其场景的可视内容。通常,装饰由标题栏和边框组成。标题栏的存在及其内容根据平台提供的装饰类型而有所不同。一些装饰品提供了额外的功能,而不仅仅是美观。例如,标题栏可用于将舞台拖动到不同的位置;标题栏中的按钮可用于最小化、最大化、恢复和关闭舞台;或者可以使用边框来调整舞台的大小。

在 JavaFX 中,舞台的样式属性决定了它的背景颜色和装饰。根据样式,在 JavaFX 中可以有以下五种类型的舞台:

  • 盛饰建筑的

  • 未加装饰的

  • 透明的

  • 统一的

  • 效用

一个装饰的舞台有纯白色的背景和平台装饰。一个没有装饰的舞台有纯白色的背景,没有任何装饰。一个透明的舞台,背景透明,没有任何装饰。统一舞台有平台装饰,客户区与装饰之间无边框;客户区背景与装饰相统一。要看统一舞台风格的效果,场景要用Color.TRANSPARENT填充。统一风格是有条件的特征。一个实用舞台有纯白色背景和最少的平台装饰。

Tip

一个舞台的风格仅仅决定了它的装饰。背景颜色由其场景背景控制,默认情况下是纯白色。如果你把一个舞台的风格设置为TRANSPARENT,你会得到一个纯白背景的舞台,这就是场景的背景。为了得到一个真正透明的舞台,你需要使用setFill()方法将场景的背景色设置为null

您可以使用Stage类的initStyle(StageStyle style)方法来设置舞台的样式。一个舞台的风格必须在第一次展示之前设定好。在舞台显示后,第二次设置它会引发运行时异常。默认情况下,舞台是装饰的。

舞台的五种样式在StageStyle枚举中定义为五个常量:

  • StageStyle.DECORATED

  • StageStyle.UNDECORATED

  • StageStyle.TRANSPARENT

  • StageStyle.UNIFIED

  • StageStyle.UTILITY

清单 4-5 展示了如何在舞台上使用这五种风格。在start()方法中,您一次只需要取消一条语句的注释,这将初始化 stage 的样式。您将使用一个VBox来显示两个控件:一个Label和一个ButtonLabel展示舞台的风格。提供Button是为了关闭舞台,因为不是所有的样式都提供带有关闭按钮的标题栏。图 4-5 显示了使用四种风格的舞台。背景中窗口的内容可以通过透明的舞台看到。这就是当您使用透明样式时,您会看到更多已添加到舞台的内容的原因。

img/336502_2_En_4_Fig5_HTML.jpg

图 4-5

使用不同风格的舞台

// StageStyleApp.java
package com.jdojo.stage;

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
import javafx.stage.StageStyle;
import static javafx.stage.StageStyle.DECORATED;
import static javafx.stage.StageStyle.UNDECORATED;
import static javafx.stage.StageStyle.TRANSPARENT;
import static javafx.stage.StageStyle.UNIFIED;
import static javafx.stage.StageStyle.UTILITY;

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

       @Override
       public void start(Stage stage) {
               // A label to display the style type
               Label styleLabel = new Label("Stage Style");

               // A button to close the stage
               Button closeButton = new Button("Close");
               closeButton.setOnAction(e -> stage.close());

               VBox root = new VBox();
               root.getChildren().addAll(styleLabel, closeButton);
               Scene scene = new Scene(root, 100, 70);
               stage.setScene(scene);

               // The title of the stage is not visible for all styles.
               stage.setTitle("The Style of a Stage");

               /* Uncomment one of the following statements at a time */
               this.show(stage, styleLabel, DECORATED);
               //this.show(stage, styleLabel, UNDECORATED);
               //this.show(stage, styleLabel, TRANSPARENT);
               //this.show(stage, styleLabel, UNIFIED);
               //this.show(stage, styleLabel, UTILITY);
       }

       private void show(Stage stage, Label styleLabel, StageStyle style) {
               // Set the text for the label to match the style
               styleLabel.setText(style.toString());

               // Set the style
               stage.initStyle(style);

               // For a transparent style, set the scene fill to null.
               // Otherwise, the content area will have the default white
               // background of the scene.
               if (style == TRANSPARENT) {
                      stage.getScene().setFill(null);
                      stage.getScene().getRoot().setStyle(
                             "-fx-background-color: transparent");
               } else if(style == UNIFIED) {
                      stage.getScene().setFill(Color.TRANSPARENT);
               }

               // Show the stage
               stage.show();
       }
}

Listing 4-5Using Different Styles for a Stage

移动未装饰的舞台

您可以通过拖动其标题栏将舞台移动到不同的位置。在未装饰或透明的舞台中,标题栏是不可用的。您需要编写几行代码,让用户通过在场景区域拖动鼠标来移动这种舞台。清单 4-6 展示了如何编写代码来支持舞台的拖动。如果将舞台更改为透明,则需要通过仅在消息标签上拖动鼠标来拖动舞台,因为透明区域不会响应鼠标事件。

这个例子使用鼠标事件处理。我将在第九章详细介绍事件处理。这里简单介绍一下,以完成关于使用不同风格的舞台的讨论。

// DraggingStage.java
package com.jdojo.stage;

import javafx.application.Application;

import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.VBox;

import javafx.stage.Stage;
import javafx.stage.StageStyle;

public class DraggingStage extends Application {
       private Stage stage;
       private double dragOffsetX;
       private double dragOffsetY;

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

       @Override
       public void start(Stage stage) {
               // Store the stage reference in the instance variable to
               // use it in the mouse pressed event handler later.
               this.stage = stage;

               Label msgLabel = new Label(
                        "Press the mouse button and drag.");
               Button closeButton = new Button("Close");
               closeButton.setOnAction(e -> stage.close());

               VBox root = new VBox();
               root.getChildren().addAll(msgLabel, closeButton);

               Scene scene = new Scene(root, 300, 200);

               // Set mouse pressed and dragged even handlers for the
               // scene
               scene.setOnMousePressed(e -> handleMousePressed(e));
               scene.setOnMouseDragged(e -> handleMouseDragged(e));

               stage.setScene(scene);
               stage.setTitle("Moving a Stage");
               stage.initStyle(StageStyle.UNDECORATED);
               stage.show();
       }

       protected void handleMousePressed(MouseEvent e) {
               // Store the mouse x and y coordinates with respect to the
               // stage in the reference variables to use them in the
               // drag event
               this.dragOffsetX = e.getScreenX() - stage.getX();
               this.dragOffsetY = e.getScreenY() - stage.getY();
       }

       protected void handleMouseDragged(MouseEvent e) {
               // Move the stage by the drag amount
               stage.setX(e.getScreenX() - this.dragOffsetX);
               stage.setY(e.getScreenY() - this.dragOffsetY);
       }
}

Listing 4-6Dragging a Stage

以下代码片段将鼠标按下和鼠标拖动事件处理程序添加到场景中:

scene.setOnMousePressed(e -> handleMousePressed(e));
scene.setOnMouseDragged(e -> handleMouseDragged(e));

当你在场景中(除了按钮区域)按下鼠标,就会调用handleMousePressed()方法。MouseEvent对象的getScreenX()getScreenY()方法返回鼠标相对于屏幕左上角的 x 和 y 坐标。图 4-6 显示了坐标系的示意图。它显示舞台周围有一条细边框。但是,当您运行示例代码时,您将看不到任何边框。此处显示是为了区分屏幕区域和舞台区域。您将鼠标相对于舞台左上角的xy坐标存储在实例变量中。

img/336502_2_En_4_Fig6_HTML.png

图 4-6

计算鼠标相对于载物台的坐标

拖动鼠标时,会调用handleMouseDragged()方法。方法使用鼠标按下时的位置和拖动时的位置来计算和设置舞台的位置。

初始化舞台的模态

在 GUI 应用程序中,你可以有两种类型的窗口:模态窗口和非模态窗口。当显示模式窗口时,用户不能使用应用程序中的其他窗口,直到模式窗口被关闭。如果一个应用程序显示多个非模态窗口,用户可以随时在它们之间切换。

JavaFX 为舞台提供了三种类型的模态:

  • 没有人

  • 窗口模式

  • 应用模型

舞台的模态由javafx.stage包的Modality枚举中的以下三个常量之一定义:

  • NONE

  • WINDOW_MODAL

  • APPLICATION_MODAL

您可以使用Stage类的initModality(Modality m)方法设置舞台的形态,如下所示:

// Create a Stage object and set its modality
Stage stage = new Stage();
stage.initModality(Modality.WINDOW_MODAL);

/* More code goes here.*/

// Show the stage
stage.show();

Tip

必须在显示之前设置舞台的形态。在显示舞台后设置它的模态会引发运行时异常。为主要舞台设置通道也会引发运行时异常。

一个Stage可以有一个所有者。一辆Stage的拥有者是另一辆Window。您可以使用Stage类的initOwner(Window owner)方法来设置Stage的所有者。必须在舞台显示之前设置Stage的所有者。一辆Stage的车主可能是null,在这种情况下,据说Stage没有车主。设置一个Stage的所有者会创建一个所有者拥有的关系。例如,如果所有者被最小化或隐藏,则Stage被最小化或隐藏。

Stage的默认设备是NONE。当显示带有模态NONEStage时,它不会阻挡应用程序中的任何其他窗口。它的行为就像一个无模式窗口。

带有WINDOW_MODAL模态的Stage阻塞其所有者层级中的所有窗口。假设有四个舞台:s1、s2、s3 和 s4。舞台 s1 和 s4 具有设置为NONE的模态,并且没有所有者;s1 是 s2 的所有者;s2 是 s3 的所有者。将显示所有四个舞台。如果 s3 的主机设置为WINDOW_MODAL,您可以使用 s3 或 s4,但不能使用 s2 和 s1。所有者-所有者关系被定义为 s1 到 s2 到 s3。当显示 s3 时,它会阻止 s2 和 s1,这两个节点位于其所有者层次结构中。因为 s4 不在 s3 的所有者层次结构中,所以您仍然可以使用 s4。

Tip

对于没有所有者的舞台,WINDOW_MODAL的形态具有与形态被设置为NONE相同的效果。

如果显示的Stage的模态设置为APPLICATION_MODAL,您必须使用Stage并将其关闭,然后才能使用应用程序中的任何其他窗口。继续上一段中显示四个舞台的相同示例,如果您将 s4 的模态设置为APPLICATION_MODAL,焦点将被设置为 s4,您必须先将其关闭,然后才能处理其他舞台。请注意,一个APPLICATION_MODAL舞台阻塞了同一应用程序中的所有其他窗口,而不管所有者拥有的关系如何。

清单 4-7 显示了如何为一个舞台使用不同的模态。它用六个按钮显示初级舞台。每个按钮打开一个具有指定主机和所有者的次级舞台。按钮的文本告诉您它们将打开哪种次级舞台。当显示次要舞台时,尝试单击主要舞台。当第二舞台的模态阻塞第一舞台时,您将无法使用第一舞台;单击主要舞台会将焦点设置回次要舞台。

// StageModalityApp.java
package com.jdojo.stage;

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import javafx.stage.Modality;
import static javafx.stage.Modality.NONE;
import static javafx.stage.Modality.WINDOW_MODAL;
import static javafx.stage.Modality.APPLICATION_MODAL;
import javafx.stage.Window;

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

       @Override
       public void start(Stage stage) {
               /* Buttons to display each kind of modal stage */
               Button ownedNoneButton = new Button("Owned None");
               ownedNoneButton.setOnAction(e -> showDialog(stage, NONE));

               Button nonOwnedNoneButton = new Button("Non-owned None");
               nonOwnedNoneButton.setOnAction(e ->
                        showDialog(null, NONE));

               Button ownedWinButton = new Button("Owned Window Modal");
               ownedWinButton.setOnAction(e ->
                        showDialog(stage, WINDOW_MODAL));

               Button nonOwnedWinButton =
                        new Button("Non-owned Window Modal");
               nonOwnedWinButton.setOnAction(e ->
                        showDialog(null, WINDOW_MODAL));

               Button ownedAppButton =
                        new Button("Owned Application Modal");
               ownedAppButton.setOnAction(e ->
                        showDialog(stage, APPLICATION_MODAL));

               Button nonOwnedAppButton =
                        new Button("Non-owned Application Modal");
               nonOwnedAppButton.setOnAction(e ->
                        showDialog(null, APPLICATION_MODAL));

               VBox root = new VBox();
               root.getChildren().addAll(
                        ownedNoneButton, nonOwnedNoneButton,
                        ownedWinButton, nonOwnedWinButton,
                        ownedAppButton, nonOwnedAppButton);
               Scene scene = new Scene(root, 300, 200);
               stage.setScene(scene);
               stage.setTitle("The Primary Stage");
               stage.show();
       }

       private void showDialog(Window owner, Modality modality) {
               // Create a Stage with specified owner and modality
               Stage stage = new Stage();
               stage.initOwner(owner);
               stage.initModality(modality);

               Label modalityLabel = new Label(modality.toString());
               Button closeButton = new Button("Close");
               closeButton.setOnAction(e -> stage.close());

               VBox root = new VBox();
               root.getChildren().addAll(modalityLabel, closeButton);
               Scene scene = new Scene(root, 200, 100);
               stage.setScene(scene);
               stage.setTitle("A Dialog Box");
               stage.show();
       }
}

Listing 4-7Using Different Modalities for a Stage

设置舞台的不透明度

舞台的不透明度决定了您透过舞台可以看到的程度。您可以使用Window类的setOpacity(double opacity)方法设置舞台的不透明度。使用getOpacity()方法获得当前舞台的不透明度。

不透明度值的范围从 0.0 到 1.0。不透明度为 0.0 表示舞台完全半透明;不透明度为 1.0 表示舞台完全不透明。不透明度会影响舞台的整个区域,包括其装饰。并非所有 JavaFX 运行时平台都需要支持不透明性。在不支持不透明度的 JavaFX 平台上设置不透明度没有任何效果。以下代码片段将舞台的不透明度设置为半透明:

Stage stage = new Stage();
stage.setOpacity(0.5); // A half-translucent stage

调整舞台大小

您可以通过使用其setResizable(boolean resizable)方法来设置用户是否可以调整舞台的大小。注意,对setResizable()方法的调用是对实现的一个提示,使 stage 可调整大小。默认情况下,舞台是可调整大小的。有时,您可能希望将调整舞台大小的使用限制在一定的宽度和高度范围内。Stage类的setMinWidth()setMinHeight()setMaxWidth()setMaxHeight()方法允许您设置用户可以调整舞台大小的范围。

Tip

Stage对象上调用setResizable(false)方法会阻止用户调整舞台的大小。您仍然可以通过编程方式调整舞台的大小。

经常需要打开一个占据整个屏幕空间的窗口。为此,您需要将窗口的位置和大小设置为屏幕的可视边界。清单 4-8 提供了说明这一点的程序。它会打开一个空舞台,占据屏幕的整个可视区域。

// MaximizedStage.java
package com.jdojo.stage;

import javafx.application.Application;
import javafx.geometry.Rectangle2D;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.stage.Screen;
import javafx.stage.Stage;

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

       @Override
       public void start(Stage stage) {
               stage.setScene(new Scene(new Group()));
               stage.setTitle("A Maximized Stage");

               // Set the position and size of the stage equal to the
               // position and size of the screen
               Rectangle2D visualBounds =
                        Screen.getPrimary().getVisualBounds();
               stage.setX(visualBounds.getMinX());
               stage.setY(visualBounds.getMinY());
               stage.setWidth(visualBounds.getWidth());
               stage.setHeight(visualBounds.getHeight());

               // Show the stage
               stage.show();
       }
}

Listing 4-8Opening a Stage to Take Up the Entire Available Visual Screen Space

以全屏模式显示舞台

Stage类有一个fullScreen属性,它指定舞台是否应该以全屏模式显示。全屏模式的实现取决于平台和配置文件。如果平台不支持全屏模式,JavaFX 运行时将通过显示最大化和未修饰的舞台来模拟它。一个 stage 可以通过调用setFullScreen(true)方法进入全屏模式。当舞台进入全屏模式时,会显示一条关于如何退出全屏模式的简短消息:您需要按 ESC 键退出全屏模式。您可以通过调用setFullScreen(false)方法以编程方式退出全屏模式。使用isFullScreen()方法检查载物台是否处于全屏模式。

展示一个舞台并等待它关闭

您通常希望显示一个对话框并暂停进一步的处理,直到它被关闭。例如,您可能希望向用户显示一个消息框,其中包含单击“是”和“否”按钮的选项,并且您希望根据用户单击的按钮执行不同的操作。在这种情况下,当消息框显示给用户时,程序必须等待它关闭,然后才能执行下一个逻辑序列。考虑以下伪代码:

Option userSelection = messageBox("Close", "Do you want to exit?", YESNO);
if (userSelection == YES) {
       stage.close();
}

在这段伪代码中,当调用messageBox()方法时,程序需要等待执行后续的if语句,直到消息框被解除。

Window类的show()方法立即返回,使得在前面的例子中打开一个对话框没有用。您需要使用showAndWait()方法,该方法显示舞台并等待它关闭,然后返回给调用者。showAndWait()方法暂时停止处理当前事件,并开始一个嵌套的事件循环来处理其他事件。

Tip

必须在 JavaFX 应用程序线程上调用showAndWait()方法。不应在主要舞台调用它,否则将引发运行时异常。

您可以使用showAndWait()方法打开多个舞台。每次调用方法都会启动一个新的嵌套事件循环。当此方法调用后创建的所有嵌套事件循环都已终止时,对该方法的特定调用将返回给调用方。

这个规则在开始时可能会令人困惑。让我们看一个例子来详细解释这一点。假设您有三个舞台:s1、s2 和 s3。使用调用s1.showAndWait()打开舞台 s1。从 s1 中的代码开始,使用调用s2.showAndWait()打开舞台 s2。此时,有两个嵌套的事件循环:一个由s1.showAndWait()创建,另一个由s2.showAndWait()创建。对s1.showAndWait()的调用将只在 s1 和 s2 都关闭后返回,而不管它们关闭的顺序。s2 关闭后,s2.showAndWait()调用将返回。

清单 4-9 包含一个程序,它允许你使用多个舞台进行showAndWait()方法调用。使用Open按钮打开初级舞台。点击Open按钮使用showAndWait()方法打开第二舞台。第二级有两个按钮——Say HelloOpen——分别在控制台上打印信息和打开另一个第二级。在调用showAndWait()方法前后,控制台上会显示一条消息。您需要打开多个次级舞台,通过单击Say Hello按钮打印消息,按照您想要的任何顺序关闭它们,然后在控制台上查看输出。

// ShowAndWaitApp.java
package com.jdojo.stage;

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

public class ShowAndWaitApp extends Application {
       protected static int counter = 0;
       protected Stage lastOpenStage;

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

       @Override
       public void start(Stage stage) {
               VBox root = new VBox();
               Button openButton = new Button("Open");
               openButton.setOnAction(e -> open(++counter));
               root.getChildren().add(openButton);
               Scene scene = new Scene(root, 400, 400);
               stage.setScene(scene);
               stage.setTitle("The Primary Stage");
               stage.show();

               this.lastOpenStage = stage;
       }

       private void open(int stageNumber) {
               Stage stage = new Stage();
               stage.setTitle("#" + stageNumber);

               Button sayHelloButton = new Button("Say Hello");
               sayHelloButton.setOnAction(
                  e -> System.out.println(
                        "Hello from #" + stageNumber));

               Button openButton = new Button("Open");
               openButton.setOnAction(e -> open(++counter));

               VBox root = new VBox();
               root.getChildren().addAll(sayHelloButton, openButton);
               Scene scene = new Scene(root, 200, 200);
               stage.setScene(scene);
               stage.setX(this.lastOpenStage.getX() + 50);
               stage.setY(this.lastOpenStage.getY() + 50);
               this.lastOpenStage = stage;

               System.out.println("Before stage.showAndWait(): " +
                        stageNumber);

               // Show the stage and wait for it to close
               stage.showAndWait();

               System.out.println("After stage.showAndWait(): " +
                        stageNumber);
       }
}

Listing 4-9Playing with the showAndWait() Call

Tip

JavaFX 不提供可用作对话框的内置窗口(消息框或提示窗口)。您可以通过为舞台设置适当的模态并使用showAndWait()方法显示来开发一个。

摘要

javafx.stage包中的Screen类用于获取与运行程序的机器挂钩的用户屏幕的详细信息,例如 DPI 设置和尺寸。如果有多个屏幕,其中一个屏幕称为主屏幕,其他的为非主屏幕。您可以使用Screen类的静态getPrimary()方法获取主监视器的Screen对象的引用。

JavaFX 中的舞台是承载场景的顶级容器,场景由可视元素组成。javafx.stage包中的Stage类表示 JavaFX 应用程序中的一个舞台。初级舞台由平台创建,并传递给Application类的start(Stage s)方法。您可以根据需要创建其他舞台。

一个舞台有包含其位置和大小的界限。舞台的边界由其四个属性定义:xywidthheightxy属性决定舞台左上角的位置。widthheight属性决定了它的大小。

舞台的区域可以分为两部分:内容区和装饰区。内容区域显示其场景的可视内容。通常,装饰由标题栏和边框组成。标题栏的存在及其内容根据平台提供的装饰类型而有所不同。JavaFX 中有五种类型的 stages:修饰的、未修饰的、透明的、统一的和实用的。

JavaFX 允许您拥有两种类型的窗口:模态窗口和非模态窗口。当显示模式窗口时,用户不能使用应用程序中的其他窗口,直到模式窗口被关闭。如果一个应用程序显示多个非模态窗口,用户可以随时在它们之间切换。JavaFX 为舞台定义了三种类型的模态:无模态、窗口模态和应用程序模态。“无”作为其模态的舞台是无模式窗口。将窗口模式作为其模式的舞台会阻止其所有者层次结构中的所有窗口。将应用程序模式作为其模式的舞台会阻塞应用程序中的所有其他窗口。

舞台的不透明度决定了您透过舞台可以看到的程度。您可以使用其setOpacity(double opacity)方法设置舞台的不透明度。不透明度值的范围从 0.0 到 1.0。不透明度为 0.0 表示舞台完全半透明;不透明度为 1.0 表示舞台完全不透明。不透明度会影响舞台的整个区域,包括其装饰。

您可以通过使用 stage 的setResizable(boolean resizable)方法来设置用户是否可以调整 stage 大小的提示。Stage类的setMinWidth()setMinHeight()setMaxWidth()setMaxHeight()方法允许您设置用户可以调整舞台大小的范围。一个 stage 可以通过调用它的setFullScreen(true)方法进入全屏模式。

你可以使用Stage类的show()showAndWait()方法来显示一个舞台。show()方法显示舞台并返回,而showAndWait()方法显示舞台并阻塞,直到舞台关闭。

下一章将向你展示如何创建场景和使用场景图。