JavaFX17-学习手册-七-

176 阅读1小时+

JavaFX17 学习手册(七)

原文:Learn JavaFX 17

协议:CC BY-NC-SA 4.0

十三、了解TableView

在本章中,您将学习:

  • 什么是TableView

  • 如何创建一个TableView

  • 关于将列添加到TableView

  • 关于用数据填充TableView

  • 关于在TableView中显示、隐藏和重新排序列

  • 关于排序和编辑TableView中的数据

  • 关于在TableView中添加和删除行

  • 关于在TableView中调整列的大小

  • 关于用 CSS 样式化一个TableView

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

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

本章中一些较长的列表是以缩略方式显示的。要获得完整的列表,请查阅该书的下载区。

什么是TableView

TableView是一个强大的控件,以表格形式显示和编辑数据模型中的数据。一个TableView由行和列组成。单元格是行和列的交集。单元格包含数据值。列的标题描述了它们包含的数据类型。列可以嵌套。调整列数据的大小和排序具有内置支持。图 13-1 显示了一个有四列的TableView,这四列有标题文本 Id、名字、姓氏和出生日期。它有五行,每行包含一个人的数据。例如,第四行第三列中的单元格包含姓氏 Boyd。

img/336502_2_En_13_Fig1_HTML.jpg

图 13-1

显示人员名单的

是一个强大但不简单的控件。您需要编写几行代码来使用最简单的向用户显示一些有意义的数据的TableView。与TableView一起工作的有几个类。当我讨论TableView的不同特性时,我将详细讨论这些类:

  • TableView

  • TableColumn

  • TableRow

  • TableCell

  • TablePosition

  • TableView.TableViewFocusModel

  • TableView.TableViewSelectionModel

TableView类代表一个TableView控件。TableColumn类表示TableView中的一列。通常,一个TableView包含多个TableColumn实例。一个TableColumn由单元格组成,这些单元格是TableCell类的实例。一个TableColumn使用两个属性来填充单元格并在其中呈现值。它使用单元格值工厂从项目列表中提取其单元格的值。它使用单元格工厂来呈现单元格中的数据。您必须为一个TableColumn指定一个单元格值工厂来查看其中的一些数据。一个TableColumn使用默认的单元格工厂,它知道如何呈现文本和图形节点。

TableRow类继承自IndexedCell类。一个TableRow的实例代表一个TableView中的一行。除非您想为行提供一个定制的实现,否则您几乎不会在应用程序中使用这个类。通常,您自定义单元格,而不是行。

TableCell类的一个实例代表了TableView中的一个单元格。单元格是高度可定制的。它们显示 TableView 的基础数据模型中的数据。它们能够显示数据和图形。

TableColumnTableRowTableCell类包含一个tableView属性,该属性保存对包含它们的TableView的引用。当TableColumn不属于某个TableView时,tableView属性包含null

一个TablePosition代表一个单元格的位置。它的getRow()getColumn()方法分别返回单元格所属的行和列的索引。

TableViewFocusModel类是TableView类的内部静态类。它代表了TableView管理行和单元格焦点的焦点模型。

TableViewSelectionModel类是TableView类的内部静态类。它代表了TableView管理行和单元格选择的选择模型。

ListViewTreeView控件一样,TableView是虚拟化的。它创建了刚好足够显示可视内容的单元格。当您滚动浏览内容时,单元格会被回收。这有助于将场景图形中的节点数量保持在最小。假设在一个TableView中有 10 列 1000 行,一次只能看到 10 行。低效的方法是创建 10,000 个单元,每个单元代表一条数据。TableView只创建了 100 个单元格,所以它可以显示十行十列。当您滚动内容时,同样的 100 个单元格将被循环显示其他可见的行。虚拟化使得将TableView与大型数据模型结合使用成为可能,并且在查看数据块时不会影响性能。

对于本章中的例子,我将使用 MVC 第十一章中的Person类。Person级在com.jdojo.mvc.model包里。在我开始详细讨论TableView控件之前,我将介绍一个PersonTableUtil类,如清单 13-1 所示。我将在给出的例子中多次重用它。它有静态方法来返回一个可见的 persona 列表和一个TableColumn类的实例来表示一个TableView中的列。

// PersonTableUtil.java
package com.jdojo.control;

import com.jdojo.mvc.model.Person;
import java.time.LocalDate;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.control.TableColumn;
import javafx.scene.control.cell.PropertyValueFactory;

public class PersonTableUtil {
    /* Returns an observable list of persons */
    public static ObservableList<Person> getPersonList() {
      Person p1 =
            new Person("Ashwin", "Sharan", LocalDate.of(2012, 10, 11));
      Person p2 =
            new Person("Advik", "Sharan", LocalDate.of(2012, 10, 11));
      Person p3 =
            new Person("Layne", "Estes", LocalDate.of(2011, 12, 16));
      Person p4 =
            new Person("Mason", "Boyd", LocalDate.of(2003, 4, 20));
      Person p5 =
            new Person("Babalu", "Sharan", LocalDate.of(1980, 1, 10));
      return FXCollections.<Person>observableArrayList(p1, p2, p3, p4, p5);
    }

    /* Returns Person Id TableColumn */
    public static TableColumn<Person, Integer> getIdColumn() {
        TableColumn<Person, Integer> personIdCol = new TableColumn<>("Id");
        personIdCol.setCellValueFactory(
               new PropertyValueFactory<>("personId"));
        return personIdCol;
    }

    /* Returns First Name TableColumn */
    public static TableColumn<Person, String> getFirstNameColumn() {
      TableColumn<Person, String> fNameCol =
            new TableColumn<>("First Name");
      fNameCol.setCellValueFactory(new PropertyValueFactory<>("firstName"));
      return fNameCol;
    }

    /* Returns Last Name TableColumn */
    public static TableColumn<Person, String> getLastNameColumn() {
        TableColumn<Person, String> lastNameCol =
               new TableColumn<>("Last Name");
        lastNameCol.setCellValueFactory(
               new PropertyValueFactory<>("lastName"));
        return lastNameCol;
    }

    /* Returns Birth Date TableColumn */
    public static TableColumn<Person, LocalDate> getBirthDateColumn() {
        TableColumn<Person, LocalDate> bDateCol =
            new TableColumn<>("Birth Date");
        bDateCol.setCellValueFactory(
               new PropertyValueFactory<>("birthDate"));
        return bDateCol;
    }
}

Listing 13-1A PersonTableUtil Utility Class

后续部分将带您完成在TableView中显示和编辑数据的步骤。

创建一个TableView

在下面的例子中,您将使用TableView类来创建一个TableView控件。TableView是一个参数化的类,它接受TableView包含的项目类型。或者,您可以将模型传递给提供数据的构造器。构造器创建了一个没有模型的TableView。下面的语句创建了一个TableView,它将使用Person类的对象作为它的项目:

TableView<Person> table = new TableView<>();

当你将前面的TableView添加到场景中时,它会显示一个占位符,如图 13-2 所示。占位符让您知道您需要向TableView添加列。在TableView数据中必须至少有一个可见的叶列。

img/336502_2_En_13_Fig2_HTML.jpg

图 13-2

没有显示占位符的列和数据的TableView

您可以使用另一个TableView类的构造器来指定模型。它接受一个可观察的项目列表。以下语句传递一个可观察的Person对象列表作为TableView的初始数据:

TableView<Person> table = new TableView<>(PersonTableUtil.getPersonList());

将列添加到TableView

TableColumn类的一个实例代表了TableView中的一列。一个TableColumn负责显示和编辑其单元格中的数据。一个TableColumn有一个可以显示标题文本和/或图形的标题。您可以为一个TableColumn创建一个上下文菜单,当用户在列标题中单击鼠标右键时,就会显示这个菜单。使用contextMenu属性设置一个上下文菜单。

TableColumn<S, T>类是一个泛型类。S参数为项目类型,与TableView的参数类型相同。T参数是该列所有单元格中的数据类型。例如,TableColumn<Person, Integer>的一个实例可以用来表示一个显示一个人的 ID 的列,它是int类型;一个TableColumn<Person, String>的实例可以用来表示一个显示一个人的名字的列,它是String类型的。以下代码片段创建了一个TableColumn with名字作为其标题文本:

TableColumn<Person, String> fNameCol = new TableColumn<>("First Name");

TableColumn需要知道如何从模型中获取其单元格的值(或数据)。要填充单元格,您需要设置TableColumncellValueFactory属性。如果TableView的模型包含基于 JavaFX 属性的类的对象,您可以使用PropertyValueFactory类的对象作为单元格值工厂,该工厂接受属性名。它从模型中读取属性值,并填充列中的所有单元格,如下面的代码所示:

// Use the firstName property of Person object to populate the column cells
PropertyValueFactory<Person, String> fNameCellValueFactory =
        new PropertyValueFactory<>("firstName");
fNameCol.setCellValueFactory(fNameCellValueFactory);

您需要为TableView中的每一列创建一个TableColumn对象,并设置其单元格值工厂属性。下一节将解释如果您的 item 类不基于 JavaFX 属性,或者您希望用计算值填充单元格,该怎么办。

设置TableView的最后一步是将TableColumns添加到它的列列表中。一个TableView将它的列的引用存储在一个ObservableList<TableColumn>中,?? 的引用可以使用TableViewgetColumns()方法获得:

// Add the First Name column to the TableView
table.getColumns().add(fNameCol);

这就是使用最简单形式的TableView所要做的一切,毕竟它并不那么“简单”!清单 13-2 中的程序展示了如何创建一个带有模型的TableView并向其中添加列。它使用PersonTableUtil类来获取人员和列的列表。程序显示如图 13-3 所示的窗口。

img/336502_2_En_13_Fig3_HTML.jpg

图 13-3

带有一个显示四列五行的TableView的窗口

// SimplestTableView.java
package com.jdojo.control;

import com.jdojo.mvc.model.Person;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.TableView;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

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

        @Override
        public void start(Stage stage) {
                // Create a TableView with a list of persons
                TableView<Person> table =
                         new TableView<>(PersonTableUtil.getPersonList());

                // Add columns to the TableView
                table.getColumns().addAll(
                         PersonTableUtil.getIdColumn(),
                         PersonTableUtil.getFirstNameColumn(),
                         PersonTableUtil.getLastNameColumn(),
                         PersonTableUtil.getBirthDateColumn());

                VBox root = new VBox(table);
                root.setStyle("""
                         -fx-padding: 10;
                   -fx-border-style: solid inside;
                   -fx-border-width: 2;
                   -fx-border-insets: 5;
                   -fx-border-radius: 5;
                   -fx-border-color: blue;""");

                Scene scene = new Scene(root);
                stage.setScene(scene);
                stage.setTitle("Simplest TableView");
                stage.show();
        }
}

Listing 13-2Using TableView in Its Simplest Form

TableView支持列嵌套。例如,您可以在名称列中嵌套两列,第一列和最后一列。一个TableColumn将嵌套列的列表存储在一个可观察列表中,该列表的引用可以使用TableColumn类的getColumns()方法获得。最里面的嵌套列被称为叶列。您需要为叶列添加单元格值工厂。嵌套列仅提供视觉效果。下面的代码片段创建了一个TableView,并添加了一个 Id 列和两个叶列,第一个和最后一个,它们嵌套在 Name 列中。结果TableView如图 13-4 所示。注意,您将最顶端的列添加到了TableView,而不是嵌套的列。TableView负责为最顶端的列添加所有嵌套的列。对列嵌套的级别没有限制。

img/336502_2_En_13_Fig4_HTML.jpg

图 13-4

具有嵌套列的TableView

// Create a TableView with data
TableView<Person> table = new TableView<>(PersonTableUtil.getPersonList());

// Create leaf columns - Id, First and Last
TableColumn<Person, String> idCol = new TableColumn<>("Id");
idCol.setCellValueFactory(new PropertyValueFactory<>("personId"));

TableColumn<Person, String> fNameCol = new TableColumn<>("First");
fNameCol.setCellValueFactory(new PropertyValueFactory<>("firstName"));

TableColumn<Person, String> lNameCol = new TableColumn<>("Last");
lNameCol.setCellValueFactory(new PropertyValueFactory<>("lastName"));

// Create Name column and nest First and Last columns in it
TableColumn<Person, String> nameCol = new TableColumn<>("Name");
nameCol.getColumns().addAll(fNameCol, lNameCol);

// Add columns to the TableView
table.getColumns().addAll(idCol, nameCol);

TableView类中的以下方法提供了关于可见叶列的信息:

TableColumn<S,?> getVisibleLeafColumn(int columnIndex)
ObservableList<TableColumn<S,?>> getVisibleLeafColumns()
int getVisibleLeafIndex(TableColumn<S,?> column)

getVisibleLeafColumn()方法返回指定列索引的列引用。只对可见的叶列计算列索引,索引从零开始。getVisibleLeafColumns()方法返回所有可见叶列的可见列表。getVisibleLeafIndex()方法返回可见叶列的指定列索引的列引用。

定制TableView占位符

TableView当占位符没有任何可见的叶列或内容时,显示占位符。考虑下面的代码片段,它创建了一个TableView并向其中添加了列:

TableView<Person> table = new TableView<>();
table.getColumns().addAll(PersonTableUtil.getIdColumn(),
                   PersonTableUtil.getFirstNameColumn(),
                   PersonTableUtil.getLastNameColumn(),
                   PersonTableUtil.getBirthDateColumn());

图 13-5 显示了前面TableView的结果。显示列和占位符,表示TableView没有数据。

img/336502_2_En_13_Fig5_HTML.jpg

图 13-5

有列但没有数据的TableView控件

您可以使用TableViewplaceholder属性替换内置占位符。属性的值是Node类的一个实例。以下语句设置了一个带有作为占位符的generic消息的Label:

table.setPlaceholder(new Label("No visible columns and/or data exist."));

您可以设置一个自定义占位符来通知用户导致TableView中不显示数据的具体情况。以下语句使用绑定来随着条件的变化而改变占位符:

table.placeholderProperty().bind(
    new When(new SimpleIntegerProperty(0)
                 .isEqualTo(table.getVisibleLeafColumns().size()))
            .then(new When(new SimpleIntegerProperty(0)
                              .isEqualTo(table.getItems().size()))
                      .then(new Label("No columns and data exist."))
                      .otherwise(new Label("No columns exist.")))
            .otherwise(new When(new SimpleIntegerProperty(0)
                           .isEqualTo(table.getItems().size()))
                           .then(new Label("No data exist."))
                           .otherwise((Label)null)));

用数据填充表格列

TableView行中的单元格包含与一个项目相关的数据,比如一个人、一本书等等。一行中某些单元格的数据可能直接来自该项目的属性,也可能是计算出来的。

TableView有一个ObservableList<S>类型的items属性。通用类型STableView的通用类型相同。它是TableView的数据模型。项目列表中的每个元素代表TableView中的一行。向条目列表添加新条目会向TableView添加新行。从项目列表中删除项目会从TableView中删除相应的行。

Tip

更新项目列表中的项目是否会更新TableView中的相应数据取决于该列的单元格值工厂是如何设置的。我将在本节讨论这两种类型的例子。

下面的代码片段创建了一个TableView,其中一行代表一个Person对象。它添加两行数据:

TableView<Person> table = new TableView<>();

Person p1 = new Person("John", "Jacobs", null);
Person p2 = new Person("Donna", "Duncan", null);
table.getItems().addAll(p1, p2);

TableView添加项目是没有用的,除非你向它添加列。除了其他一些东西,一个TableColumn对象定义了

  • 列的标题文本和图形

  • 用于填充列中单元格的单元格值工厂

TableColumn类让你完全控制如何填充一列中的单元格。TableColumn类的cellValueFactory属性负责填充列的单元格。单元格值工厂是Callback类的对象,它接收一个TableColumn.CellDataFeatures对象并返回一个ObservableValue

CellDataFeatures类是TableColumn类的静态内部类,它包装了TableViewTableColumn的引用,以及正在填充列单元格的行的项目。使用CellDataFeatures类的getTableView()getTableColumn()getValue()方法分别获取TableViewTableColumn的引用和该行的项目。

TableView需要单元格的值时,它调用单元格所属列的单元格值工厂对象的call()方法。call()方法应该返回一个ObservableValue对象的引用,该对象被监控是否有任何变化。返回的ObservableValue对象可以包含任何类型的对象。如果它包含一个节点,则该节点在单元格中显示为图形。否则调用对象的toString()方法,返回的字符串显示在单元格中。

以下代码片段使用匿名类创建了一个单元格值工厂。工厂返回对Person类的firstName属性的引用。注意,JavaFX 属性是一个ObservableValue

import static javafx.scene.control.TableColumn.CellDataFeatures;
...
// Create a String column with the header "First Name" for Person object
TableColumn<Person, String> fNameCol = new TableColumn<>("First Name");

// Create a cell value factory object
Callback<CellDataFeatures<Person, String>, ObservableValue<String>> fNameCellFactory =
  new Callback<CellDataFeatures<Person, String>, ObservableValue<String>>() {
    @Override
    public ObservableValue<String> call(CellDataFeatures<Person,
            String> cellData) {
      Person p = cellData.getValue();
      return p.firstNameProperty();
}};

// Set the cell value factory
fNameCol.setCellValueFactory(fNameCellFactory);

使用 lambda 表达式创建和设置单元格值工厂非常方便。前面的代码片段可以编写如下:

TableColumn<Person, String> fNameCol = new TableColumn<>("First Name");
fNameCol.setCellValueFactory(cellData ->
    cellData.getValue().firstNameProperty());

当 JavaFX 属性为列中的单元格提供值时,如果使用PropertyValueFactory类的对象,创建单元格值工厂会更容易。您需要将 JavaFX 属性的名称传递给它的构造器。下面的代码片段与前面显示的代码具有相同的功能。您将采用这种方法在PersonTableUtil类的实用方法中创建TableColumn对象。

TableColumn<Person, String> fNameCol = new TableColumn<>("First Name");
fNameCol.setCellValueFactory(new PropertyValueFactory<>("firstName"));

Tip

使用 JavaFX 属性作为单元格的值有一个很大的优势。TableView保持属性和单元格中的值同步。更改模型中的属性值会自动更新单元中的值。

TableColumn还支持 POJO (Plain Old Java Object)作为TableView中的条目。缺点是当模型更新时,单元值不会自动更新。您使用相同的PropertyValueFactory类来创建单元格值工厂。该类将使用您传递的属性名称查找公共 getter 和 setter 方法。如果只找到 getter 方法,该单元格将是只读的。对于一个xxx属性,它尝试使用 JavaBeans 命名约定寻找getXxx()setXxx()方法。如果xxx的类型是 boolean,它也会寻找isXxx()方法。如果找不到 getter 或 setter 方法,则会引发运行时异常。以下代码片段创建了一个标题为“年龄类别”的列:

TableColumn<Person, Person.AgeCategory> ageCategoryCol =
    new TableColumn<>("Age Category");
ageCategoryCol.setCellValueFactory(new PropertyValueFactory<>("ageCategory"));

表示项目类型为Person,列类型为Person.AgeCategory。它将ageCategory作为属性名传递给PropertyValueFactory类的构造器。首先,这个类将在Person类中寻找一个ageCategory属性。Person类没有这个属性。因此,它将尝试使用Person类作为该属性的 POJO。然后它会在Person类中寻找getAgeCategory()setAgeCategory()方法。它只找到 getter 方法getAgeCategory(),因此它将使该列成为只读的。

列单元格中的值不一定来自 JavaFX 或 POJO 属性。它们可以用一些逻辑来计算。在这种情况下,您需要创建一个定制的单元格值工厂,并返回一个包装计算值的ReadOnlyXxxWrapper对象。以下代码片段创建了一个年龄列,该列以年为单位显示计算出的年龄:

TableColumn<Person, String> ageCol = new TableColumn<>("Age");
ageCol.setCellValueFactory(cellData -> {
        Person p = cellData.getValue();
        LocalDate dob = p.getBirthDate();
        String ageInYear = "Unknown";
        if (dob != null) {
                long years = YEARS.between(dob, LocalDate.now());
                if (years == 0) {
                        ageInYear = "< 1 year";
                } else if (years == 1) {
                        ageInYear = years + " year";
                } else {
                        ageInYear = years + " years";
                }

        }
        return new ReadOnlyStringWrapper(ageInYear);
});

这就完成了在TableView中为一列单元格设置单元格值工厂的不同方式。清单 13-3 中的程序为 JavaFX 属性、POJO 属性和计算值创建单元格值工厂。显示如图 13-6 所示的窗口。

img/336502_2_En_13_Fig6_HTML.jpg

图 13-6

一个TableView包含 JavaFX 属性、POJO 属性和计算值的列

// TableViewDataTest.java
// ...find in the book's download area

Listing 13-3Setting Cell Value Factories for Columns

TableView中的单元格可以显示文本和图形。如果单元格值工厂返回一个Node类的实例,它可能是一个ImageView,单元格将它显示为图形。否则,它显示从对象的toString()方法返回的字符串。可以在单元格中显示其他控件和容器。然而,TableView并不意味着,这种用途是不鼓励的。有时,在单元格中使用特定类型的控件(例如复选框)来显示或编辑布尔值可以提供更好的用户体验。我将很快介绍这种单元格的定制。

使用地图作为TableView中的项目

有时,TableView的一行中的数据可能不会映射到域对象,例如,您可能希望在TableView中显示动态查询的结果集。物品清单包括一份可观察的Map清单。列表中的Map包含该行中所有列的值。您可以定义一个自定义的单元格值工厂来从Map中提取数据。MapValueFactory级就是为此目的专门设计的。它是单元格值工厂的一个实现,从一个指定键的Map中读取数据。

下面的代码片段创建了一个MapTableView。它创建一个 Id 列,并将MapValueFactory类的一个实例设置为它的单元格值工厂,将idColumnKey指定为包含 Id 列值的键。它创建一个Map,并使用idColumnKey填充 Id 列。您需要对所有的列和行重复这些步骤。

TableView<Map> table = new TableView<>();

// Define the column, its cell value factory and add it to the TableView
String idColumnKey = "id";
TableColumn<Map, Integer> idCol = new TableColumn<>("Id");
idCol.setCellValueFactory(new MapValueFactory<>(idColumnKey));
table.getColumns().add(idCol);

// Create and populate a Map an item
Map row1 = new HashMap();
row1.put(idColumnKey, 1);

// Add the Map to the TableView items list

table.getItems().add(row1);

清单 13-4 中的程序展示了如何使用MapValueFactory作为TableView中列的单元格值工厂。它显示由PersonTableUtil类中的getPersonList()方法返回的人的数据。

// TableViewMapDataTest.java
// ...find in the book's download area

Listing 13-4Using MapValueFactory As a Cell Value Factory for Cells in a TableView

显示和隐藏列

默认情况下,TableView中的所有列都是可见的。TableColumn类有一个visible属性来设置列的可见性。如果关闭父列(具有嵌套列的列)的可见性,其所有嵌套列也将不可见:

TableColumn<Person, String> idCol = new TableColumn<>("Id");

// Make the Id column invisible
idCol.setVisible(false);
...
// Make the Id column visible
idCol.setVisible(true);

有时,您可能希望让用户控制列的可见性。TableView类有一个tableMenuButtonVisible属性。如果设置为true,标题区会显示一个菜单按钮:

// Create a TableView
TableView<Person> table = create the TableView here...

// Make the table menu button visible
table.setTableMenuButtonVisible(true);

单击菜单按钮会显示所有叶列的列表。列显示为单选菜单项,可用于切换它们的可见性。图 13-7 显示了一个有四列的TableView。它的tableMenuButtonVisible属性被设置为 true。该图显示了一个菜单,其中所有列的名称都带有复选标记。单击菜单按钮时会显示菜单。列名旁边的复选标记表示这些列可见。单击列名可切换其可见性。

img/336502_2_En_13_Fig7_HTML.jpg

图 13-7

一个带有菜单按钮的TableView来切换列的可见性

TableView中的列进行重新排序

您可以用两种方式重新排列TableView中的列:

  • 通过将列拖放到不同的位置

  • 通过改变它们在由TableView类的getColumns()方法返回的可观察列表中的位置

默认情况下,第一个选项可用。用户需要在新位置拖放一列。当列被重新排序时,它在列列表中的位置会发生变化。第二个选项将直接在列列表中对列进行重新排序。

没有简单的方法来禁用默认的列重新排序特性。如果您想禁用这个特性,您需要向由TableViewgetColumns()方法返回的ObservableList添加一个ChangeListener。当报告更改时,重置列,使它们再次处于原始顺序。

要启用或禁用列重新排序特性,请对列使用setReorderable()方法:

table.getColumns().forEach(c -> {
      boolean b = ...; // determine whether column is reorderable

      c.setReorderable(b);
});

对表格视图中的数据进行排序

TableView内置了对列中数据排序的支持。默认情况下,它允许用户通过单击列标题对数据进行排序。它还支持以编程方式对数据进行排序。您还可以对TableView中的一列或所有列禁用排序。

按用户排序数据

默认情况下,可以对TableView中所有列的数据进行排序。用户可以通过单击列标题对列中的数据进行排序。第一次单击按升序对数据进行排序。第二次单击按降序对数据进行排序。第三次单击会从排序顺序列表中删除该列。

默认情况下,启用单列排序。也就是说,如果你点击一列,那么TableView中的记录只根据被点击列中的数据进行排序。要启用多列排序,您需要在单击要排序的列的标题时按住 Shift 键。

TableView在已排序列的标题中显示视觉线索,以指示排序类型和排序顺序。默认情况下,列标题中会显示一个指示排序类型的三角形。对于升序排序类型,它指向上;对于降序排序类型,它指向下。列的排序顺序由点或数字表示。点用于排序顺序列表中的前三列。从第四列开始使用数字。例如,排序顺序列表中的第一列显示一个点,第二列显示两个点,第三列显示三个点,第四列显示数字 4,第五列显示数字 5,依此类推。

图 13-8 显示了一个有四列的TableView。列标题显示了排序类型和排序顺序。姓氏的排序类型是降序,其他的排序类型是升序。姓氏、名字、出生日期和 Id 的排序顺序分别为 1、2、3 和 4。请注意,点用于前三列中的排序顺序,数字 4 用于 Id 列,因为它是排序顺序列表中的第四列。这种排序是通过按以下顺序单击列标题来实现的:姓氏(两次)、名字、出生日期和 Id。

img/336502_2_En_13_Fig8_HTML.jpg

图 13-8

显示排序类型和排序顺序的列标题

以编程方式排序数据

可以通过编程方式对列中的数据进行排序。TableViewTableColumn类为排序提供了一个非常强大的 API。排序 API 由两个类中的几个属性和方法组成。分拣的每个部分和每个阶段都是可定制的。以下部分通过示例描述了 API。

使列可排序

TableColumnsortable属性决定了该列是否可排序。默认情况下,它被设置为 true。将其设置为 false 可禁用列的排序:

// Disable sorting for fNameCol column
fNameCol.setSortable(false);

指定列的排序类型

一个TableColumn有一个排序类型,可以是升序也可以是降序。它是通过sortType属性指定的。TableColumn.SortType枚举的ASCENDINGDESCENDING常量分别代表列的升序和降序排序类型。sortType属性的默认值是TableColumn.SortType.ASCENDINGDESCENDING常量设置如下:

// Set the sort type for fNameCol column to descending
fNameCol.setSortType(TableColumn.SortType.DESCENDING);

为列指定比较器

一个TableColumn使用一个Comparator来排序它的数据。您可以使用comparator属性为TableColumn指定Comparator。被比较的两个单元格中的对象被传入comparator。A TableColumn使用默认的Comparator,用常量TableColumn.DEFAULT_COMPARATOR表示。默认比较器使用以下规则比较两个单元格中的数据:

  • 它检查null值。首先对null值进行排序。如果两个单元格都有null,则认为它们相等。

  • 如果被比较的第一个值是Comparable接口的实例,它调用第一个对象的compareTo()方法,将第二个对象作为参数传递给该方法。

  • 如果前面两个条件都不成立,它将两个对象转换成字符串,调用它们的toString()方法,并使用一个Comparator来比较两个String值。

大多数情况下,默认比较器就足够了。下面的代码片段为String列使用了一个自定义比较器,该比较器只比较单元格数据的第一个字符:

TableColumn<Person, String> fNameCol = new TableColumn<>("First Name");
...
// Set a custom comparator
fNameCol.setComparator((String n1, String n2) -> {
        if (n1 == null && n2 == null) {
                return 0;
        }

        if (n1 == null) {
                return -1;
        }

        if (n2 == null) {
                return 1;
        }

        String c1 = n1.isEmpty()? n1:String.valueOf(n1.charAt(0));
        String c2 = n2.isEmpty()? n2:String.valueOf(n2.charAt(0));
        return c1.compareTo(c2);
});

指定列的排序节点

TableColumn类包含一个sortNode属性,该属性指定一个节点在列标题中显示关于列的当前排序类型和排序顺序的可视线索。当排序类型为升序时,节点旋转 180 度。当列不是排序的一部分时,节点不可见。默认情况下,它是null,而TableColumn提供了一个三角形作为排序节点。

指定列的排序顺序

TableView类包含几个用于排序的属性。要对列进行排序,您需要将它们添加到TableView的排序顺序列表中。sortOrder属性指定了排序顺序。它是TableColumn的一只ObservableList。列表中TableColumn的顺序指定了列在排序中的顺序。根据列表中的第一列对行进行排序。如果列中两行的值相等,则排序顺序列表中的第二列用于确定两行的排序顺序,依此类推。

下面的代码片段将两列添加到一个TableView中,并指定它们的排序顺序。请注意,这两列都将按升序排序,这是默认的排序类型。如果您想按降序对它们进行排序,请按如下方式设置它们的sortType属性:

// Create a TableView with data
TableView<Person> table = new TableView<>(PersonTableUtil.getPersonList());

TableColumn<Person, String> lNameCol = PersonTableUtil.getLastNameColumn();
TableColumn<Person, String> fNameCol = PersonTableUtil.getFirstNameColumn();

// Add columns to the TableView
table.getColumns().addAll(lNameCol, fNameCol );

// Add columns to the sort order to sort by last name followed by first name
table.getSortOrder().addAll(lNameCol, fNameCol);

监视TableViewsortOrder属性的变化。如果修改了,TableView会立即根据新的排序顺序进行排序。向排序顺序列表中添加列并不保证该列包含在排序中。该列也必须是可排序的,才能包含在排序中。还监视TableColumnsortType属性的变化。在排序顺序列表中更改列的排序类型,会立即重新排序TableView数据。

获取TableView的比较器

TableView包含一个只读的comparator属性,它是一个基于当前排序顺序列表的Comparator。您很少需要在代码中使用这个Comparator。如果将两个TableView项传递给Comparatorcompare()方法,它将返回一个负整数、零或正整数,分别表示第一项小于、等于或大于第二项。

回想一下,TableColumn也有一个comparator属性,用于指定如何确定TableColumn单元格中值的顺序。TableViewcomparator属性组合了其排序顺序列表中所有TableColumnscomparator属性。

指定排序策略

一个TableView有一个排序策略来指定如何执行排序。它是一个Callback对象。TableView作为参数传递给call()方法。如果排序成功,该方法返回true。如果排序失败,则返回falsenull

TableView类包含一个DEFAULT_SORT_POLICY常量,用作TableView的默认排序策略。它使用comparator属性对TableView的条目列表进行排序。指定一个排序策略来完全控制排序算法。排序策略Callback对象的call()方法将对TableView的项目进行排序。

举个简单的例子,将排序策略设置为null将禁用排序,因为当用户或程序请求排序时,将不执行排序:

TableView<Person> table = ...

// Disable sorting for the TableView
table.setSortPolicy(null);

有时,出于性能原因,暂时禁用排序是有用的。假设您有一个包含大量条目的已排序的TableView,并且您想要对排序顺序列表进行一些更改。排序顺序列表中的每次更改都会触发对项目的排序。在这种情况下,您可以通过将排序策略设置为null来禁用排序,进行所有更改,并通过恢复原始排序策略来启用排序。排序策略的变化会触发立即排序。这项技术将只对项目排序一次:

TableView<Person> table = ...
...
// Store the current sort policy
Callback<TableView<Person>, Boolean> currentSortPolicy =
    table.getSortPolicy();

// Disable the sorting
table.setSortPolicy(null)

// Make all changes that might need or trigger sorting

...

// Restore the sort policy that will sort the data once immediately
table.setSortPolicy(currentSortPolicy);

手动排序数据

TableView包含一个sort()方法,该方法使用当前排序顺序列表对TableView中的项目进行排序。在向一个TableView添加了许多项目之后,您可以调用这个方法来对项目进行排序。当列的排序类型、排序顺序或排序策略发生变化时,会自动调用此方法。

处理排序事件

TableView在收到排序请求时,在将排序算法应用于项目之前,触发一个SortEvent。添加一个SortEvent监听器,在实际排序之前执行任何操作:

TableView<Person> table = ...
table.setOnSort(e -> {/* Code to handle the sort event */});

如果SortEvent被消耗,分类中止。如果您想禁用对一个TableView的排序,按如下方式消耗SortEvent:

// Disable sorting for the TableView
table.setOnSort(e -> e.consume());

禁用表视图的排序

有几种方法可以禁用对TableView的排序:

  • TableColumn设置sortable属性只会禁用该列的排序。如果将TableView中所有列的sortable属性设置为 false,那么TableView的排序将被禁用。

  • 您可以为TableViewnull设置分类策略。

  • 你可以用SortEventTableView

  • 从技术上来说,可以覆盖TableView类的sort()方法,并为该方法提供一个空体,但不推荐这样做。

对一个TableView部分或完全禁用排序的最好方法是对它的部分或全部列禁用排序。

自定义单元格中的数据呈现

TableColumn中的单元格是TableCell类的一个实例,它显示单元格中的数据。一个TableCell是一个Labeled控件,它能够显示文本和/或图形。

您可以为TableColumn指定一个细胞工厂。单元工厂的工作是呈现单元中的数据。TableColumn类包含一个cellFactory属性,它是一个Callback对象。它的call()方法在单元格所属的TableColumn的引用中传递。该方法返回一个TableCell的实例。TableCellupdateItem()方法被覆盖以提供单元格数据的自定义呈现。

如果未指定cellFactory属性,TableColumn将使用默认的单元格工厂。默认单元工厂根据数据类型显示单元数据。如果单元格数据包含一个节点,则数据显示在单元格的graphic属性中。否则,调用单元格数据的toString()方法,返回的字符串显示在单元格的text属性中。

到目前为止,您已经使用了一系列的Person对象作为示例中的数据模型,用于在TableView中显示数据。出生日期列的格式为 yyyy-mm-dd,这是由LocalDate类的toString()方法返回的默认 ISO 日期格式。如果要将出生日期格式化为 mm/dd/yyyy 格式,可以通过为出生日期列设置自定义单元格工厂来实现:

TableColumn<Person, LocalDate> birthDateCol = ...;
birthDateCol.setCellFactory (col -> {
    TableCell<Person, LocalDate> cell =
        new TableCell<Person, LocalDate>() {
          @Override
          public void updateItem(LocalDate item, boolean empty) {
              super.updateItem(item, empty);

              // Cleanup the cell before populating it
              this.setText(null);
              this.setGraphic(null);

              if (!empty) {
                  // Format the birth date in mm/dd/yyyy format
                  String formattedDob =
                      DateTimeFormatter.ofPattern("MM/dd/yyyy").
                         format(item);
                  this.setText(formattedDob);
              }
          }

    };
    return cell;
});

您还可以使用前面的技术在单元格中显示图像。在updateItem()方法中,为图像创建一个ImageView对象,并使用TableCellsetGraphic()方法显示它。TableCell包含tableColumntableRowtableView属性,分别存储其TableColumnTableRowTableView的引用。这些属性对于访问数据模型中表示单元格行的项目非常有用。

如果将前面代码片段中的if语句替换为以下代码,则出生日期列将显示出生日期和年龄类别,例如 10/11/2012(婴儿):

if (!empty) {
    String formattedDob =
         DateTimeFormatter.ofPattern("MM/dd/yyyy").format(item);

    if (this.getTableRow() != null ) {
        // Get the Person item for this cell
        int rowIndex = this.getTableRow().getIndex();
        Person p = this.getTableView().getItems().get(rowIndex);
        String ageCategory = p.getAgeCategory().toString();

        // Display birth date and age category together
        this.setText(formattedDob + " (" + ageCategory + ")" );
    }
}

下面是以不同方式呈现单元格数据的TableCell的子类。例如,CheckBoxTableCell在复选框中显示单元格数据,而ProgressBarTableCell使用进度条显示数字:

  • CheckBoxTableCell

  • ChoiceBoxTableCell

  • ComboBoxTableCell

  • ProgressBarTableCell

  • TextFieldTableCell

下面的代码片段创建了一个标签为 Baby?并设置一个单元格工厂来显示一个CheckBoxTableCell中的值。CheckBoxTableCell类的forTableColumn(TableColumn<S, Boolean> col)方法返回一个用作单元格工厂的Callback对象:

// Create a "Baby?" column
TableColumn<Person, Boolean> babyCol = new TableColumn<>("Baby?");
babyCol.setCellValueFactory(cellData -> {
        Person p = cellData.getValue();
        Boolean v =  (p.getAgeCategory() == Person.AgeCategory.BABY);
        return new ReadOnlyBooleanWrapper(v);
});

// Set a cell factory that will use a CheckBox to render the value
babyCol.setCellFactory(CheckBoxTableCell.<Person>forTableColumn(babyCol));

请浏览 API 文档了解TableCell的其他子类以及如何使用它们。例如,您可以在列的单元格中显示一个带有选项列表的组合框。用户可以选择其中一个选项作为单元格数据。

清单 13-5 有一个完整的程序来展示如何使用定制的细胞工厂。显示如图 13-9 所示的窗口。该程序使用单元格工厂将出生日期格式化为 mm/dd/yyyy 格式,并使用复选框显示一个人是否是婴儿。

img/336502_2_En_13_Fig9_HTML.jpg

图 13-9

使用自定义单元格工厂格式化单元格中的数据并在复选框中显示单元格数据

// TableViewCellFactoryTest.java

// ...find in the book's download area

Listing 13-5Using a Custom Cell Factory for a TableColumn

选择TableView中的单元格和行

TableView具有由其属性selectionModel表示的选择模型。选择模型是TableViewSelectionModel类的一个实例,它是TableView类的一个内部静态类。选择模型支持单元格级和行级选择。它还支持两种选择模式:单个和多个。在单一选择模式下,一次只能选择一个单元格或一行。在多选模式下,可以选择多个单元格或行。默认情况下,单行选择处于启用状态。您可以启用多行选择,如下所示:

TableView<Person> table = ...

// Turn on multiple-selection mode for the TableView
TableViewSelectionModel<Person> tsm = table.getSelectionModel();
tsm.setSelectionMode(SelectionMode.MULTIPLE);

通过将选择模型的cellSelectionEnabled属性设置为 true,可以启用单元格级别的选择,如下面的代码片段所示。当该属性设置为 true 时,TableView被置于单元格级选择模式,并且您不能选择整行。如果启用了多重选择模式,您仍然可以选择一行中的所有单元格。但是,行本身并没有被报告为选中,因为TableView处于单元格级别的选择模式。默认情况下,单元格级别的选择模式为 false。

// Enable cell-level selection
tsm.setCellSelectionEnabled(true);

选择模型提供关于所选单元格和行的信息。如果指定的rowIndex处的行被选中,isSelected(int rowIndex)方法返回true。使用isSelected(int rowIndex, TableColumn<S,?> column)方法了解指定的rowIndex和列中的单元格是否被选中。选择模型提供了几种方法来选择单元格和行,并获得所选单元格和行的报告:

  • selectAll()方法选择所有单元格或行。

  • select()方法被重载。它选择一行、一项的一行和一个单元格。

  • 如果没有选择,isEmpty()方法返回true。否则,它返回false

  • getSelectedCells()方法返回一个只读的ObservableList<TablePosition>,它是当前选中单元格的列表。当TableView中的选择改变时,列表也会改变。

  • getSelectedIndices()方法返回一个只读的ObservableList<Integer>,它是当前选择的索引的列表。当TableView中的选择改变时,列表也会改变。如果启用了行级选择,则列表中的项目是选定行的行索引。如果启用了单元格级别的选择,则列表中的一项是选择了一个或多个单元格的行的行索引。

  • getSelectedItems()方法返回一个只读的ObservableList<S>,其中STableView的通用类型。该列表包含已选择相应行或单元格的所有项目。

  • clearAndSelect()方法被重载。它允许您在选择一行或一个单元格之前清除所有选择。

  • clearSelection()方法被重载。它允许您清除对一行、一个单元格或整个TableView的选择。

TableView中的单元格或行选择发生变化时,通常需要做出一些改变或采取一些行动。例如,TableView可以作为主-详细数据视图中的主列表。当用户在主列表中选择一行时,您希望刷新详细视图中的数据。如果您对处理选择更改事件感兴趣,您需要将一个ListChangeListener添加到前面列出的方法返回的一个ObservableList中,该方法报告选定的单元格或行。下面的代码片段将一个ListChangeListener添加到由getSelectedIndices()方法返回的ObservableList中,以跟踪TableView中的行选择更改:

TableView<Person> table = ...
TableViewSelectionModel<Person> tsm = table.getSelectionModel();
ObservableList<Integer> list = tsm.getSelectedIndices();

// Add a ListChangeListener
list.addListener((ListChangeListener.Change<? extends Integer> change) -> {
        System.out.println("Row selection has changed");
});

编辑TableView中的数据

可以编辑TableView中的单元格。可编辑单元格在编辑和非编辑模式之间切换。在编辑模式下,用户可以修改单元格数据。要使单元格进入编辑模式,TableViewTableColumnTableCell必须是可编辑的。它们都有一个editable属性,可以使用setEditable(true)方法将其设置为 true。默认情况下,TableColumnTableCell是可编辑的。要使单元格在TableView中可编辑,您需要使TableView可编辑:

TableView<Person> table = ...
table.setEditable(true);

TableColumn类支持三种类型的事件:

  • onEditStart

  • onEditCommit

  • onEditCancel

当列中的单元格进入编辑模式时,触发onEditStart事件。当用户成功提交编辑时,例如通过按下TextField中的回车键,触发onEditCommit事件。当用户取消编辑时,例如通过在TextField中按下 Esc 键,触发onEditCancel事件。

事件由一个TableColumn.CellEditEvent类的对象表示。事件对象封装了单元格中的旧值和新值,TableViewTableColumnTablePosition的项目列表中的 row 对象(表示正在进行编辑的单元格位置)以及TableView的引用。使用CellEditEvent类的方法获得这些值。

使TableView可编辑并不能让您编辑它的单元格数据。在编辑单元格中的数据之前,您需要做更多的准备工作。单元格编辑功能是通过TableCell类的专门实现提供的。JavaFX 库提供了其中的一些实现。将列的单元格工厂设置为使用以下TableCell实现之一来编辑单元格数据:

  • CheckBoxTableCell

  • ChoiceBoxTableCell

  • ComboBoxTableCell

  • TextFieldTableCell

使用复选框编辑数据

一个CheckBoxTableCell在单元格内呈现一个复选框。通常,它用于表示列中的布尔值。该类提供了一种使用Callback对象将其他类型的值映射到布尔值的方法。如果值为真,则选中该复选框。否则,它将被取消选中。双向绑定用于绑定复选框的选中属性和底层ObservableValue。如果用户更改选择,则基础数据会更新,反之亦然。

Person类中没有布尔属性。您必须通过提供单元格值工厂来创建布尔列,如下面的代码所示。如果一个Person是婴儿,单元格值工厂返回true。否则返回false

TableColumn<Person, Boolean> babyCol = new TableColumn<>("Baby?");
babyCol.setCellValueFactory(cellData -> {
        Person p = cellData.getValue();
        Boolean v =  (p.getAgeCategory() == Person.AgeCategory.BABY);
        return new ReadOnlyBooleanWrapper(v);
});

让细胞工厂使用CheckBoxTableCell很容易。使用forTableColumn()静态方法获取列的单元格工厂:

// Set a CheckBoxTableCell to display the value
babyCol.setCellFactory(CheckBoxTableCell.<Person>forTableColumn(babyCol));

一个CheckBoxTableCell不会触发单元格编辑事件。复选框的 selected 属性被绑定到代表单元格中数据的ObservableValue。如果您对跟踪选择更改事件感兴趣,您需要为单元格的数据添加一个ChangeListener

使用选择框编辑数据

一个ChoiceBoxTableCell呈现一个选择框,在单元格内有一个指定的值列表。列表中值的类型必须与TableColumn的类型相匹配。当单元格未被编辑时,ChoiceBoxTableCell中的数据显示在Label中。编辑单元格时使用ChoiceBox

Person类没有性别属性。您想要向TableView<Person>添加一个性别列,可以使用选择框对其进行编辑。下面的代码片段创建了TableColumn并设置了一个单元格值工厂,它将所有单元格设置为一个空字符串。如果有的话,您可以设置单元格值工厂来使用Person类的性别属性。

// Gender is a String, editable, ComboBox column
TableColumn<Person, String> genderCol = new TableColumn<>("Gender");

// Use an appropriate cell value factory.
// For now, set all cells to an empty string
genderCol.setCellValueFactory(cellData -> new ReadOnlyStringWrapper(""));

您可以使用ChoiceBoxTableCell类的forTableColumn()静态方法创建一个单元格工厂,它使用一个选择框来编辑单元格中的数据。您需要指定要在选择框中显示的项目列表:

// Set a cell factory, so it can be edited using a ChoiceBox
genderCol.setCellFactory(
        ChoiceBoxBoxTableCell.<Person, String>forTableColumn(
               "Male", "Female")
);

当在选择框中选择一个项目时,该项目被设置为基础数据模型。例如,如果列基于域对象中的属性,则选定项将被设置为属性。您可以设置一个当用户选择一个项目时触发的onEditCommit事件处理程序。下面的代码片段为性别列添加了这样一个处理程序,它在标准输出中打印一条消息:

// Add an onEditCommit handler
genderCol.setOnEditCommit(e -> {
        int row = e.getTablePosition().getRow();
        Person person = e.getRowValue();
        System.out.println("Gender changed (" + person.getFirstName() +
               " " + person.getLastName() + ")" + " at row " + (row + 1) +
           ". New value = " + e.getNewValue());
});

单击选定的单元格会将该单元格置于编辑模式。双击未选中的单元格会将该单元格置于编辑模式。将焦点切换到另一个单元格或从列表中选择一项会将编辑单元格置于非编辑模式,当前值显示在Label中。

使用组合框编辑数据

一个ComboBoxTableCell呈现一个组合框,在单元格内有一个指定的值列表。它的工作原理类似于一个ChoiceBoxTableCell。请参考“使用选择框编辑数据”一节了解更多详细信息。

使用文本字段编辑数据

当单元格被编辑时,TextFieldTableCell在单元格内呈现一个TextField,用户可以在其中修改数据。当单元格未被编辑时,它在Label中呈现单元格数据。

单击选中的单元格或双击未选中的单元格会将单元格置于编辑模式,在TextField中显示单元格数据。一旦单元格处于编辑模式,您需要单击TextField(再单击一次!)将插入符号放在TextField中,以便您可以进行更改。注意,编辑一个单元格至少需要三次点击,这对于那些必须编辑大量数据的用户来说是一件痛苦的事情。让我们期待TableView API 的设计者在未来的版本中让数据编辑变得不那么麻烦。

如果您正在编辑单元格数据,请按 Esc 键取消编辑,这将使单元格返回到非编辑模式,并恢复到单元格中的旧数据。如果TableColumn是基于Writable ObservableValue的,按下回车键将数据提交到底层数据模型。

如果您正在使用TextFieldTableCell编辑一个单元格,将焦点移动到另一个单元格,例如,通过单击另一个单元格,取消编辑并将旧值放回单元格中。这不是用户所期望的。目前,这个问题没有简单的解决方法。您必须创建一个TableCell的子类并添加一个焦点改变监听器,这样您就可以在TextField失去焦点时提交数据。

使用TextFieldTableCell类的forTableColumn()静态方法获得一个使用TextField编辑单元格数据的单元格工厂。下面的代码片段展示了如何为 First Name String列执行此操作:

TableColumn<Person, String> fNameCol = new TableColumn<>("First Name");
fNameCol.setCellFactory(TextFieldTableCell.<Person>forTableColumn());

有时,您需要使用TextField来编辑非字符串数据,例如日期。日期可以表示为模型中的LocalDate类的对象。您可能希望在TextField中将它显示为格式化字符串。当用户编辑日期时,您希望将数据作为LocalDate提交给模型。TextFieldTableCell类通过StringConverter支持这种对象到字符串的转换,反之亦然。下面的代码片段为带有StringConverter的出生日期列设置了一个单元格工厂,它将字符串转换为LocalDate,反之亦然。列类型为LocalDate。默认情况下,LocalDateStringConverter采用 mm/dd/yyyy 的日期格式。

TableColumn<Person, LocalDate> birthDateCol = new TableColumn<>("Birth Date");
LocalDateStringConverter converter = new LocalDateStringConverter();
birthDateCol.setCellFactory(
    TextFieldTableCell.<Person, LocalDate>forTableColumn(converter));

清单 13-6 中的程序展示了如何使用不同类型的控件编辑TableView中的数据。TableView包含 Id、名字、姓氏、出生日期、婴儿和性别列。Id 列不可编辑。名、姓和出生日期列使用TextFieldTableCell,因此可以使用TextField进行编辑。Baby 列是不可编辑的计算字段,不受数据模型支持。它使用CheckBoxTableCell来呈现它的值。性别列是可编辑的计算字段。它不受数据模型的支持。它使用一个ComboBoxTableCell在编辑模式下向用户显示一个值列表(男性和女性)。当用户选择一个值时,该值不会保存到数据模型中。它呆在牢房里。添加了一个onEditCommit事件处理程序,在标准输出中打印性别选择。程序显示如图 13-10 所示的窗口,可以看到您已经为所有人选择了一个性别值。正在编辑第五行的出生日期值。

img/336502_2_En_13_Fig10_HTML.jpg

图 13-10

单元格处于编辑模式的TableView

// TableViewEditing.java
// ...find in the book's download area

Listing 13-6Editing Data in a TableView

使用任何控件编辑 TableCell 中的数据

在上一节中,我讨论了如何使用不同的控件编辑TableView单元格中的数据,例如,TextFieldCheckBoxChoiceBox。你可以子类化TableCell来使用任何控件来编辑单元格数据。例如,您可能希望使用DatePicker在日期列的单元格中选择日期,或者使用RadioButtons从多个选项中进行选择。可能性是无穷的。

您需要覆盖TableCell类的四个方法:

  • startEdit()

  • commitEdit()

  • cancelEdit()

  • updateItem()

单元格从非编辑模式转换到编辑模式的startEdit()方法。通常,您可以在带有当前数据的单元格的graphic属性中设置您选择的控件。

当用户操作(例如,按下TextField中的 Enter 键)表明用户已经完成了对单元格数据的修改,并且数据需要保存在底层数据模型中时,就会调用commitEdit()方法。通常,您不需要覆盖这个方法,因为如果TableColumn是基于Writable ObservableValue的,那么修改后的数据将被提交给数据模型。

当用户动作(例如,在TextField中按下 Esc 键)表明用户想要取消编辑过程时,调用cancelEdit()方法。当编辑过程取消时,单元格返回到非编辑模式。您需要重写此方法,并将单元格数据恢复为它们的旧值。

当单元格需要再次呈现时,调用updateItem()方法。根据编辑模式的不同,您需要适当地设置单元格的文本和图形属性。

现在让我们开发一个继承自TableCell类的DatePickerTableCell类。当你想用一个DatePicker控件编辑一个TableColumn的单元格时,你可以使用DatePickerTableCell的实例。TableColumn必须是LocalDate的。清单 13-7 有DatePickerTableCell类的完整代码。

// DatePickerTableCell.java
// ...find in the book's download area

Listing 13-7The DatePickerTableCell Class to Allow Editing Table Cells Using a DatePicker Control

DatePickerTableCell类支持StringConverterDatePicker的可编辑属性值。您可以将它们传递给构造器或forTableColumn()方法。当第一次调用startEdit()方法时,它会创建一个DatePicker控件。添加了一个ChangeListener,它在输入或选择新日期时提交数据。提供了几个版本的静态方法来返回单元工厂。以下代码片段显示了如何使用DatePickerTableCell类:

TableColumn<Person, LocalDate> birthDateCol = ...

// Set a cell factory for birthDateCol. The date format is mm/dd/yyyy
// and the DatePicker is editable.
birthDateCol.setCellFactory(DatePickerTableCell.<Person>forTableColumn());

// Set a cell factory for birthDateCol. The date format is "Month day, year"
// and and the DatePicker is non-editable
StringConverter converter = new LocalDateStringConverter("MMMM dd, yyyy");
birthDateCol.setCellFactory(DatePickerTableCell.<Person>forTableColumn(
    converter, false));

清单 13-8 中的程序使用DatePickerTableCell来编辑出生日期列单元格中的数据。运行应用程序,然后双击出生日期列中的单元格。该单元格将显示一个DatePicker控件。您不能编辑DatePicker中的日期,因为它是不可编辑的。您需要从弹出日历中选择一个日期。

// CustomTableCellTest.java
// ...find in the book's download area

Listing 13-8Using DatePickerTableCell to Edit a Date in Cells

TableView中添加和删除行

TableView中添加和删除行很容易。注意,TableView中的每一行都由项目列表中的一个项目支持。添加一行就像在项目列表中添加一个项目一样简单。当您向项目列表中添加项目时,在TableView中会出现一个新行,其索引与项目列表中已添加项目的索引相同。如果TableView已排序,则在添加新行后可能需要重新排序。增加一行后,调用TableViewsort()方法对行进行重新排序。

您可以通过从项目列表中移除项目来删除行。应用程序为用户提供了一种指示应该删除哪些行的方法。通常,用户选择一行或多行来删除。其他选项是为每一行添加一个删除按钮,或者为每一行提供一个删除复选框。单击删除按钮应该会删除该行。选中某行的“删除”复选框表示该行被标记为删除。

清单 13-9 中的程序展示了如何在TableView中添加和删除行。它显示一个包含三个部分的窗口:

  • 顶部的Add Person表单有三个用于添加个人详细信息的字段和一个 add 按钮。输入一个人的详细信息,然后单击 Add 按钮向TableView添加一条记录。代码中跳过了错误检查。

  • 中间有两个按钮。一个按钮用于恢复TableView中的默认行。另一个按钮删除选定的行。

  • 在底部,显示一个带有一些行的TableView。启用多行选择。用鼠标按住 Ctrl 或 Shift 键选择多行。

// TableViewAddDeleteRows.java
// ...find in the book's download area

Listing 13-9Adding and Deleting Rows in a TableView

代码中的大部分逻辑很简单。deleteSelectedRows()方法实现了删除所选行的逻辑。从项目列表中删除项目时,选择模型不会删除其索引。假设选择了第一行。如果从“项目”列表中删除第一个项目,将选择第二行,即第一行。为了确保不会发生这种情况,在将行从项目列表中移除之前,请清除该行的选择。您从最后到第一(从高索引到低索引)删除行,因为当您从列表中删除一个项目时,被删除项目之后的所有项目将具有不同的索引。假设您选择了索引 1 和索引 2 处的行。删除索引 1 处的行首先会将索引 2 的索引更改为 1。从最后到第一个执行删除可以解决这个问题。

表格视图中滚动

当行或列超出可用空间时,自动提供垂直和水平滚动条。用户可以使用滚动条滚动到特定的行或列。有时,您需要滚动的编程支持。例如,当您将一行追加到一个TableView中时,您可能希望通过将该行滚动到视图中来让用户看到它。TableView类包含四种方法,可以用来滚动到特定的行或列:

  • scrollTo(int rowIndex)

  • scrollTo(S item)

  • scrollToColumn(TableColumn<S,?> column)

  • scrollToColumnIndex(int columnIndex)

scrollTo()方法将带有指定索引或项目的行滚动到视图中。scrollToColumn()scrollToColumnIndex(方法分别滚动到指定的列和columnIndex

当请求使用上述滚动方法之一滚动到一行或一列时,TableView触发一个ScrollToEventScrollToEvent类包含一个getScrollTarget()方法,根据滚动类型返回行索引或列引用:

TableView<Person> table = ...

// Add a ScrollToEvent for row scrolling
table.setOnScrollTo(e -> {
        int rowIndex = e.getScrollTarget();
        System.out.println("Scrolled to row " + rowIndex);
});

// Add a ScrollToEvent for column scrolling
table.setOnScrollToColumn(e -> {
        TableColumn<Person, ?> column = e.getScrollTarget();
        System.out.println("Scrolled to column " + column.getText());
});

Tip

当用户滚动行和列时,不会触发ScrollToEvent。当您调用TableView类的四个滚动相关方法之一时,它被触发。

调整表格列的大小

用户是否可以调整一个TableColumn的大小是由它的resizable属性决定的。默认情况下,TableColumn是可调整大小的。如何调整TableView中一列的大小由TableViewcolumnResizePolicy属性指定。该属性是一个Callback对象。它的call()方法接受ResizeFeatures类的一个对象,该对象是TableView类的一个静态内部类。ResizeFeatures对象封装了调整列大小的增量、TableColumnTableView。如果成功地按增量调整了列的大小,call()方法将返回true。否则,返回false

TableView类提供了两个内置的调整大小策略作为常量:

  • CONSTRAINED_RESIZE_POLICY

  • UNCONSTRAINED_RESIZE_POLICY

CONSTRAINED_RESIZE_POLICY确保所有可见叶列的宽度之和等于TableView的宽度。调整列的大小会调整调整后的列右侧所有列的宽度。当列宽增加时,最右边一列的宽度会减少到其最小宽度。如果增加的宽度仍未得到补偿,最右边第二列的宽度将减少到其最小宽度,依此类推。当右边的所有列都达到其最小宽度时,列宽就不能再增加了。当调整列的大小以减小其宽度时,相同的规则适用于相反的方向。

当一列的宽度增加时,UNCONSTRAINED_RESIZE_POLICY将所有列向右移动宽度增加的量。当宽度减小时,右边的列向左移动相同的量。如果某列有嵌套列,则调整该列的大小会在直接子列之间均匀分布增量。这是TableView的默认列调整策略:

TableView<Person> table = ...;

// Set the column resize policy to constrained resize policy
table.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY);

您还可以创建自定义的列大小调整策略。以下代码片段将作为模板。您需要编写消耗 delta 的逻辑,delta 是列的新旧宽度之差:

TableView<Person> table = new TableView<>(PersonTableUtil.getPersonList());
table.setColumnResizePolicy(resizeFeatures -> {
    boolean consumedDelta = false; double delta = resizeFeatures.getDelta();
    TableColumn<Person, ?> column = resizeFeatures.getColumn();
    TableView<Person> tableView = resizeFeatures.getTable();

    // Adjust the delta here...

    return consumedDelta;
});

您可以通过设置一个不起任何作用的简单回调来禁用列大小调整。它的call()简单地返回true,表明它已经消耗了增量:

// Disable column resizing
table.setColumnResizePolicy(resizeFeatures -> true);

用 CSS 设计一个TableView

您可以样式化一个TableView及其所有部分,例如,列标题、单元格、占位符等等。将 CSS 应用到TableView非常复杂,范围也很广。这一部分简要概述了TableView的 CSS 样式。一个TableView的默认 CSS 样式类名是table-view。单元格、行和列标题的默认 CSS 样式类分别是table-celltable-row-cellcolumn-header:

/* Set the font for the cells */
.table-row-cell {
        -fx-font-size: 10pt;
        -fx-font-family: Arial;
}

/* Set the font size and text color for column headers */
.table-view .column-header .label{
        -fx-font-size: 10pt;
        -fx-text-fill: blue;
}

TableView支持以下 CSS 伪类:

  • cell-selection

  • row-selection

当单元格级别的选择被启用时,cell-selection伪类被应用,而row-selection伪类被应用于行级别的选择。当列调整策略为CONSTRAINED_RESIZE_POLICY时,应用constrained-resize伪类。

默认情况下,TableView中的交替行被高亮显示。下面的代码删除了替代行的突出显示。它为所有行设置白色背景色:

.table-row-cell {
    -fx-background-color: white;
}

.table-row-cell .table-cell {
        -fx-border-width: 0.25px;
        -fx-border-color: transparent gray gray transparent;
}

TableView显示空行以填充其可用高度。下面的代码删除空行。事实上,这使它们看起来像被移走了:

.table-row-cell:empty {
        -fx-background-color: transparent;
}

.table-row-cell:empty .table-cell {
        -fx-border-width: 0px;
}

TableView包含几个可以单独设计风格的子结构:

  • column-resize-line

  • column-overlay

  • placeholder

  • column-header-background

column-resize-line substructure是一个Region,当用户试图调整列大小时显示。column-overlay substructure是一个Region,显示为正在移动的列的覆盖图。placeholder substructure是一个StackPane,当TableView没有列或数据时显示,如以下代码所示:

/* Make the text in the placeholder red and bold */
.table-view .placeholder .label {
        -fx-text-fill: red;
        -fx-font-weight: bold;
}

column-header-background子结构是一个StackPane,是列标题后面的区域。它包含几个子结构。它的填充子结构是一个Region,是最右边的列和标题区域中TableView右边缘之间的区域。它的 show-hide-columns-button 子结构是一个StackPane,是显示菜单按钮的区域,用来显示要显示和隐藏的列的列表。请参考modena.css文件和 JavaFX CSS 参考指南以获得可以被样式化的TableView属性的完整列表。下面的代码将填充背景设置为白色:

/* Set the filler background to white*/
.table-view .column-header-background .filler {
        -fx-background-color: white;
}

摘要

TableView是一个用于以表格形式显示和编辑数据的控件。一个TableView由行和列组成。行和列的交叉点称为单元格。单元格包含数据值。列的标题描述了它们包含的数据类型。列可以嵌套。调整列数据的大小和排序具有内置支持。以下类别用于使用TableView控件:TableViewTableColumnTableRowTableCellTablePositionTableView.TableViewFocusModelTableView.TableViewSelectionModelTableView类代表一个TableView控件。TableColumn类表示TableView中的一列。通常,一个TableView包含多个TableColumn实例。一个TableColumn由单元格组成,这些单元格是TableCell类的实例。一个TableColumn负责显示和编辑其单元格中的数据。一个TableColumn有一个可以显示标题文本和/或图形的标题。您可以为TableColumn创建一个上下文菜单,当用户在列标题中单击鼠标右键时,就会显示这个菜单。使用contextMenu属性设置一个上下文菜单。

TableRow类继承自IndexedCell类。一个TableRow的实例代表一个TableView中的一行。除非您想为行提供一个定制的实现,否则您几乎从不在应用程序中使用这个类。通常,您自定义单元格,而不是行。

TableCell类的一个实例代表了TableView中的一个单元格。单元格是高度可定制的。它们为TableView显示来自底层数据模型的数据。它们能够显示数据和图形。TableView行中的单元格包含与一个项目相关的数据,比如一个人、一本书等等。一行中某些单元格的数据可能直接来自该项目的属性,也可能是计算出来的。

TableView有一个ObservableList<S>类型的items属性。通用类型STableView的通用类型相同。它是TableView的数据模型。项目列表中的每个元素代表TableView中的一行。向条目列表添加新条目会向TableView添加新行。从项目列表中删除项目会从TableView中删除相应的行。

TableColumnTableRowTableCell类包含一个tableView属性,该属性保存对包含它们的TableView的引用。当TableColumn不属于某个TableView时,tableView属性包含null

一个TablePosition代表一个单元格的位置。它的getRow()getColumn()方法分别返回单元格所属的行和列的索引。

TableViewFocusModel类是TableView类的内部静态类。它代表了TableView管理行和单元格焦点的焦点模型。

TableViewSelectionModel类是TableView类的内部静态类。它代表了TableView管理行和单元格选择的选择模型。

默认情况下,TableView中的所有列都是可见的。TableColumn类有一个visible属性来设置列的可见性。如果关闭父列(具有嵌套列的列)的可见性,其所有嵌套列都将不可见。

有两种方法可以重新排列TableView中的列:将列拖放到不同的位置,或者改变它们在由TableView类的getColumns()方法返回的可观察列表中的位置。默认情况下,第一个选项可用。

TableView内置了对列中数据排序的支持。默认情况下,它允许用户通过单击列标题对数据进行排序。它还支持以编程方式对数据进行排序。您还可以对TableView中的一列或所有列禁用排序。

TableView支持多层次定制。它允许您定制列的呈现,例如,您可以使用复选框、组合框或TextField在列中显示数据。你也可以使用 CSS 样式化一个TableView

下一章将讨论 2D 形状以及如何将它们添加到场景中。

Tip

本书省略了上一版的TreeViewTreeTableView章节。这些控件的处理非常类似于表格视图,并且章节非常大,所以为了将这个版本保持在一个合理的范围内,在附录中有一个对树控件的简明介绍。

十四、理解 2D 形状

在本章中,您将学习:

  • 什么是 2D 图形,它们在 JavaFX 中是如何表示的

  • 如何画 2D 图形

  • 如何使用Path类绘制复杂形状

  • 如何使用可缩放矢量图形(SVG)绘制形状

  • 如何组合形状以构建另一个形状

  • 如何为形状使用笔画

  • 如何使用级联样式表(CSS)设置形状样式

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

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

什么是 2D 形状?

任何能在二维平面上画出的形状都叫做 2D 形状。JavaFX 提供了各种节点来绘制不同类型的形状(线条、圆形、矩形等)。).您可以将形状添加到场景图。

形状可以是二维的,也可以是三维的。在这一章,我将讨论 2D 形状。第十六章讨论 3D 形状。

所有形状类都在javafx.scene.shape包中。表示 2D 形状的类继承自抽象的Shape类,如图 14-1 所示。

img/336502_2_En_14_Fig1_HTML.png

图 14-1

表示 2D 形状的类的类图

形状有大小和位置,这是由它们的属性定义的。例如,widthheight属性定义矩形的大小,radius属性定义圆的大小,xy属性定义矩形左上角的位置,centerXcenterY属性定义圆心,等等。

在布局过程中,父形状不会调整形状的大小。只有当形状的与大小相关的属性改变时,形状的大小才会改变。您可能会发现类似“JavaFX 形状是不可调整的”这样的短语这意味着在布局过程中,形状不可被其父对象改变大小。它们只能通过更改属性来调整大小。

形状有内部和描边。定义形状内部和笔画的属性在Shape类中声明。属性指定了填充 ?? 内部的颜色。默认填充为Color.BLACKstroke属性指定轮廓线条的颜色,默认为null,除了LinePolylinePath默认为strokestrokeWidth属性指定轮廓的宽度,默认为 1.0px。Shape类包含其他与笔画相关的属性,我将在“理解形状的笔画”一节中讨论这些属性

Shape类包含一个smooth属性,默认为真。其 true 值指示应该使用抗锯齿提示来呈现形状。如果设置为 false,将不使用抗锯齿提示,这可能会导致形状的边缘不清晰。

清单 14-1 中的程序创建了两个圆。第一个圆有浅灰色填充,没有描边,这是默认设置。第二个圆圈有黄色填充和 2.0 像素宽的黑色描边。图 14-2 显示了两个圆。

img/336502_2_En_14_Fig2_HTML.jpg

图 14-2

具有不同填充和描边的两个圆

// ShapeTest.java
package com.jdojo.shape;

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.layout.HBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.stage.Stage;

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

        @Override
        public void start(Stage stage) {
            // Create a circle with a light gray fill and no stroke
            Circle c1 = new Circle(40, 40, 40);
            c1.setFill(Color.LIGHTGRAY);

            // Create a circle with an yellow fill and a black stroke
            // of 2.0px
            Circle c2 = new Circle(40, 40, 40);
            c2.setFill(Color.YELLOW);
            c2.setStroke(Color.BLACK);
            c2.setStrokeWidth(2.0);

            HBox root = new HBox(c1, c2);
            root.setSpacing(10);
            root.setStyle("""
               -fx-padding: 10;
               -fx-border-style: solid inside;
               -fx-border-width: 2;
               -fx-border-insets: 5;
               -fx-border-radius: 5;
               -fx-border-color: blue;""");

            Scene scene = new Scene(root);
            stage.setScene(scene);
            stage.setTitle("Using Shapes");
            stage.show();
        }
}

Listing 14-1Using fill and stroke Properties of the Shape Class

画 2D 图形

以下部分详细描述了如何使用代表 2D 形状的 JavaFX 类来绘制这些形状。

画线

Line类的一个实例代表一个线节点。一辆Line没有内饰。默认情况下,它的fill属性设置为null。设置fill没有效果。默认strokeColor.BLACK,默认strokeWidth为 1.0。Line类包含四个 double 属性:

  • startX

  • startY

  • endX

  • endY

Line表示(startX, startY)(endX, endY)点之间的线段。Line类有一个无参数构造器,它将所有四个属性默认为零,得到一条从(0,0)到(0,0)的线,表示一个点。另一个构造器接受startXstartYendXendY的值。创建了Line之后,可以通过改变四个属性中的任何一个来改变它的位置和长度。

清单 14-2 中的程序创建一些Line并设置它们的strokestrokeWidth属性。第一个Line将显示为一个点。图 14-3 为线条。

img/336502_2_En_14_Fig3_HTML.jpg

图 14-3

使用线节点

// LineTest.java
package com.jdojo.shape;

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.layout.HBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.Line;
import javafx.stage.Stage;

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

        @Override

        public void start(Stage stage) {
            // It will be just a point at (0, 0)
            Line line1 = new Line();

            Line line2 = new Line(0, 0, 50, 0);
            line2.setStrokeWidth(1.0);

            Line line3 = new Line(0, 50, 50, 0);
            line3.setStrokeWidth(2.0);
            line3.setStroke(Color.RED);

            Line line4 = new Line(0, 0, 50, 50);
            line4.setStrokeWidth(5.0);
            line4.setStroke(Color.BLUE);

            HBox root = new HBox(line1, line2, line3, line4);
            root.setSpacing(10);
            root.setStyle("""
               -fx-padding: 10;
               -fx-border-style: solid inside;
               -fx-border-width: 2;
               -fx-border-insets: 5;
               -fx-border-radius: 5;
               -fx-border-color: blue;""");

            Scene scene = new Scene(root);
            stage.setScene(scene);
            stage.setTitle("Using Lines");
            stage.show();
        }
}

Listing 14-2Using the Line Class to Create Line Nodes

绘制矩形

Rectangle类的一个实例表示一个矩形节点。该类使用六个属性来定义矩形:

  • x

  • y

  • width

  • height

  • arcWidth

  • arcHeight

xy属性是矩形左上角在节点局部坐标系中的 x 和 y 坐标。widthheight属性分别是矩形的宽度和高度。指定相同的宽度和高度来绘制正方形。

默认情况下,矩形的角是尖锐的。通过指定arcWidtharcHeight属性,矩形可以有圆角。你可以把一个椭圆的一个象限定位在四个角上,使它们变圆。arcWidtharcHeight属性是椭圆的水平和垂直直径。默认情况下,它们的值为零,这使得矩形具有尖角。图 14-4 显示了两个矩形——一个带尖角,一个带圆角。显示椭圆是为了说明圆角矩形的arcWidtharcHeight属性之间的关系。

img/336502_2_En_14_Fig4_HTML.png

图 14-4

带尖角和圆角的矩形

Rectangle类包含几个构造器。它们将各种属性作为参数。xywidthheightarcWidtharcHeight属性的默认值为零。构造器是

  • Rectangle()

  • Rectangle(double width, double height)

  • Rectangle(double x, double y, double width, double height)

  • Rectangle(double width, double height, Paint fill)

当您将一个Rectangle添加到大多数布局窗格中时,您将看不到为其指定xy属性值的效果,因为它们将子元素放置在(0,0)处。A Pane使用这些属性。清单 14-3 中的程序将两个矩形添加到一个Pane中。第一个矩形使用 x 和 y 属性的默认值零。第二个矩形为x属性指定 120,为y属性指定 20。图 14-5 显示了Pane内两个矩形的位置。请注意,第二个矩形(右侧)的左上角位于(120,20)。

img/336502_2_En_14_Fig5_HTML.jpg

图 14-5

窗格内的矩形,它使用 x 和 y 属性来定位它们

// RectangleTest.java
package com.jdojo.shape;

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;

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

        @Override
        public void start(Stage stage) {
            // x=0, y=0, width=100, height=50, fill=LIGHTGRAY, stroke=null
            Rectangle rect1 = new Rectangle(100, 50, Color.LIGHTGRAY);

            // x=120, y=20, width=100, height=50, fill=WHITE, stroke=BLACK
            Rectangle rect2 = new Rectangle(120, 20, 100, 50);
            rect2.setFill(Color.WHITE);
            rect2.setStroke(Color.BLACK);
            rect2.setArcWidth(10);
            rect2.setArcHeight(10);

            Pane root = new Pane();
            root.getChildren().addAll(rect1, rect2);
            Scene scene = new Scene(root);
            stage.setScene(scene);
            stage.setTitle("Using Rectangles");
            stage.show();
        }
}

Listing 14-3Using the Rectangle Class to Create Rectangle Nodes

画圆

Circle类的一个实例代表一个圆形节点。该类使用三个属性来定义圆:

  • centerX

  • centerY

  • radius

centerXcenterY属性是圆心在节点局部坐标系中的 x 和 y 坐标。radius属性是圆的半径。这些属性的默认值为零。

Circle类包含几个构造器:

  • Circle()

  • Circle(double radius)

  • Circle(double centerX, double centerY, double radius)

  • Circle(double centerX, double centerY, double radius, Paint fill)

  • Circle(double radius, Paint fill)

清单 14-4 中的程序给一个HBox增加了两个圆。注意HBox没有使用圆的centerXcenterY属性。将它们添加到一个Pane来观察效果。图 14-6 显示了两个圆。

img/336502_2_En_14_Fig6_HTML.jpg

图 14-6

使用圆形节点

// CircleTest.java
package com.jdojo.shape;

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.layout.HBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.stage.Stage;

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

        @Override
        public void start(Stage stage) {
            // centerX=0, centerY=0, radius=40, fill=LIGHTGRAY,
            // stroke=null
            Circle c1 = new Circle(0, 0, 40);
            c1.setFill(Color.LIGHTGRAY);

            // centerX=10, centerY=10, radius=40\. fill=YELLOW,
            // stroke=BLACK
            Circle c2 = new Circle(10, 10, 40, Color.YELLOW);
            c2.setStroke(Color.BLACK);
            c2.setStrokeWidth(2.0);

            HBox root = new HBox(c1, c2);
            root.setSpacing(10);
            root.setStyle("""
               -fx-padding: 10;
               -fx-border-style: solid inside;
               -fx-border-width: 2;
               -fx-border-insets: 5;
               -fx-border-radius: 5;
               -fx-border-color: blue;""");

            Scene scene = new Scene(root);
            stage.setScene(scene);
            stage.setTitle("Using Circle");
            stage.show();
        }
}

Listing 14-4Using the Circle Class to Create Circle Nodes

绘制椭圆

Ellipse类的一个实例表示一个椭圆节点。该类使用四个属性来定义椭圆:

  • centerX

  • centerY

  • radiusX

  • radiusY

centerXcenterY属性是圆心在节点局部坐标系中的 x 和 y 坐标。radiusXradiusY是椭圆在水平和垂直方向上的半径。这些属性的默认值为零。当radiusXradiusY相同时,圆是椭圆的特例。

Ellipse类包含几个构造器:

  • Ellipse()

  • Ellipse(double radiusX, double radiusY)

  • Ellipse(double centerX, double centerY, double radiusX, double radiusY)

清单 14-5 中的程序创建了Ellipse类的三个实例。第三个实例画了一个圆,因为程序为radiusXradiusY属性设置了相同的值。图 14-7 显示了三个椭圆。

img/336502_2_En_14_Fig7_HTML.jpg

图 14-7

使用椭圆节点

// EllipseTest.java
package com.jdojo.shape;

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.layout.HBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.Ellipse;
import javafx.stage.Stage;

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

        @Override
        public void start(Stage stage) {
            Ellipse e1 = new Ellipse(50, 30);
            e1.setFill(Color.LIGHTGRAY);

            Ellipse e2 = new Ellipse(60, 30);
            e2.setFill(Color.YELLOW);
            e2.setStroke(Color.BLACK);
            e2.setStrokeWidth(2.0);

            // Draw a circle using the Ellipse class (radiusX=radiusY=30)
            Ellipse e3 = new Ellipse(30, 30);
            e3.setFill(Color.YELLOW);
            e3.setStroke(Color.BLACK);
            e3.setStrokeWidth(2.0);

            HBox root = new HBox(e1, e2, e3);
            root.setSpacing(10);
            root.setStyle("""
               -fx-padding: 10;
               -fx-border-style: solid inside;
               -fx-border-width: 2;
               -fx-border-insets: 5;
               -fx-border-radius: 5;
               -fx-border-color: blue;""");

            Scene scene = new Scene(root);
            stage.setScene(scene);
            stage.setTitle("Using Ellipses");
            stage.show();
        }

}

Listing 14-5Using the Ellipse Class to Create Ellipse Nodes

绘制多边形

Polygon类的一个实例代表一个多边形节点。该类不定义任何公共属性。它允许您使用定义多边形顶点的(x,y)坐标数组来绘制多边形。使用Polygon类,你可以绘制任何类型的几何形状,这些几何形状是使用连接线创建的(三角形、五边形、六边形、平行四边形等)。).

Polygon类包含两个构造器:

  • Polygon()

  • Polygon(double... points)

无参数构造器创建一个空多边形。您需要添加形状顶点的(x,y)坐标。多边形将从第一个顶点到第二个顶点、从第二个顶点到第三个顶点等等绘制一条线。最后,通过从最后一个顶点到第一个顶点画一条线来闭合形状。

Polygon类将顶点的坐标存储在一个ObservableList<Double>中。您可以使用getPoints()方法获得可观察列表的引用。注意,它将坐标存储在一个列表Double中,这个列表只是一个数字。您的工作是成对传递数字,因此它们可以用作顶点的(x,y)坐标。如果传递奇数个数字,则不会创建任何形状。下面的代码片段创建了两个三角形——一个在构造器中传递顶点的坐标,另一个稍后将它们添加到可观察列表中。两个三角形在几何上是相同的:

// Create an empty triangle and add vertices later
Polygon triangle1 = new Polygon();
triangle1.getPoints().addAll(50.0, 0.0,
                      0.0, 100.0,
                      100.0, 100.0);

// Create a triangle with vertices
Polygon triangle2 = new Polygon(50.0, 0.0,
                        0.0, 100.0,

                        100.0, 100.0);

清单 14-6 中的程序使用Polygon类创建了一个三角形、一个平行四边形和一个六边形,如图 14-8 所示。

img/336502_2_En_14_Fig8_HTML.jpg

图 14-8

使用多边形节点

// PolygonTest.java
package com.jdojo.shape;

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.layout.HBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.Polygon;
import javafx.stage.Stage;

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

        @Override
        public void start(Stage stage) {
            Polygon triangle1 = new Polygon();
            triangle1.getPoints().addAll(50.0, 0.0,
                0.0, 50.0,
                100.0, 50.0);
            triangle1.setFill(Color.WHITE);
            triangle1.setStroke(Color.RED);

            Polygon parallelogram = new Polygon();
            parallelogram.getPoints().addAll(
               30.0, 0.0,
               130.0, 0.0,
               100.00, 50.0,
               0.0, 50.0);
            parallelogram.setFill(Color.YELLOW);
            parallelogram.setStroke(Color.BLACK);

            Polygon hexagon = new Polygon(
                100.0, 0.0,
                120.0, 20.0,
                120.0, 40.0,
                100.0, 60.0,
                80.0, 40.0,
                80.0, 20.0);
            hexagon.setFill(Color.WHITE);
            hexagon.setStroke(Color.BLACK);

            HBox root = new HBox(triangle1, parallelogram, hexagon);
            root.setSpacing(10);
            root.setStyle("""
               -fx-padding: 10;
               -fx-border-style: solid inside;
               -fx-border-width: 2;
               -fx-border-insets: 5;
               -fx-border-radius: 5;
               -fx-border-color: blue;""");

            Scene scene = new Scene(root);
            stage.setScene(scene);
            stage.setTitle("Using Polygons");
            stage.show();
        }

}

Listing 14-6Using the Polygon Class to Create a Triangle, a Parallelogram, and a Hexagon

绘制多段线

折线类似于多边形,只是它不在最后一点和第一点之间绘制直线。也就是说,折线是一个开放的多边形。然而,fill颜色用于填充整个形状,就好像该形状是闭合的一样。

Polyline类的一个实例代表一个折线节点。该类不定义任何公共属性。它允许您使用定义折线顶点的(x,y)坐标数组来绘制折线。使用Polyline类,你可以绘制任何类型的几何形状,这些几何形状是使用连接线创建的(三角形、五边形、六边形、平行四边形等)。).

Polyline类包含两个构造器:

  • Polyline()

  • Polyline(double... points)

无参数构造器创建一条空折线。您需要添加形状顶点的(x,y)坐标。多边形将从第一个顶点到第二个顶点、从第二个顶点到第三个顶点等等绘制一条线。与Polygon不同,该形状不会自动闭合。如果要闭合形状,需要添加第一个顶点的坐标作为最后一对数字。

如果您想稍后添加顶点的坐标,请将它们添加到由Polyline类的getPoints()方法返回的ObservableList<Double>中。下面的代码片段使用不同的方法创建了两个具有相同几何属性的三角形。请注意,为了闭合三角形,第一对和最后一对数字是相同的:

// Create an empty triangle and add vertices later
Polygon triangle1 = new Polygon();
triangle1.getPoints().addAll(
    50.0, 0.0,
    0.0, 100.0,
    100.0, 100.0,
    50.0, 0.0);

// Create a triangle with vertices
Polygon triangle2 = new Polygon(
    50.0, 0.0,
    0.0, 100.0,
    100.0, 100.0,
    50.0, 0.0);

清单 14-7 中的程序使用Polyline类创建了一个三角形、一个开放的平行四边形和一个六边形,如图 14-9 所示。

img/336502_2_En_14_Fig9_HTML.jpg

图 14-9

使用折线节点

// PolylineTest.java
package com.jdojo.shape;

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.layout.HBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.Polyline;
import javafx.stage.Stage;

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

        @Override
        public void start(Stage stage) {
            Polyline triangle1 = new Polyline();
            triangle1.getPoints().addAll(
                50.0, 0.0,
                0.0, 50.0,
                100.0, 50.0,
                50.0, 0.0);
            triangle1.setFill(Color.WHITE);
            triangle1.setStroke(Color.RED);

            // Create an open parallelogram

            Polyline parallelogram = new Polyline();
            parallelogram.getPoints().addAll(
                30.0, 0.0,
                130.0, 0.0,
                100.00, 50.0,
                0.0, 50.0);
            parallelogram.setFill(Color.YELLOW);
            parallelogram.setStroke(Color.BLACK);

            Polyline hexagon = new Polyline(
               100.0, 0.0,
               120.0, 20.0,
               120.0, 40.0,
               100.0, 60.0,
               80.0, 40.0,
               80.0, 20.0,
               100.0, 0.0);
            hexagon.setFill(Color.WHITE);
            hexagon.setStroke(Color.BLACK);

            HBox root = new HBox(triangle1, parallelogram, hexagon);
            root.setSpacing(10);
            root.setStyle("""
               -fx-padding: 10;
               -fx-border-style: solid inside;
               -fx-border-width: 2;
               -fx-border-insets: 5;
               -fx-border-radius: 5;
               -fx-border-color: blue;""");

            Scene scene = new Scene(root);
            stage.setScene(scene);
            stage.setTitle("Using Polylines");
            stage.show();
        }
}

Listing 14-7Using the Polyline Class to Create a Triangle, an Open Parallelogram, and a Hexagon

画弧线

Arc类的一个实例代表一个椭圆的一部分。该类使用七个属性来定义椭圆:

  • centerX

  • centerY

  • radiusX

  • radiusY

  • startAngle

  • length

  • type

前四个属性定义了一个椭圆。关于如何定义椭圆,请参考“绘制椭圆”一节。最后三个属性定义了椭圆的一个扇区,即Arc节点。startAngle属性指定从 x 轴正方向逆时针测量的部分的起始角度,单位为度。它定义了弧的起点。length是一个角度,以度为单位,从开始角度逆时针测量,以定义扇形的结束。如果length属性设置为 360 度,则Arc是一个完整的椭圆。图 14-10 说明了这些特性。

img/336502_2_En_14_Fig10_HTML.png

图 14-10

定义弧的属性

type 属性指定了关闭Arc的方式。它是ArcType枚举中定义的OPENCHORDROUND常量之一:

  • ArcType.OPEN不关闭圆弧。

  • ArcType.CHORD通过用直线连接起点和终点来闭合圆弧。

  • ArcType.ROUND通过将起点和终点连接到椭圆的中心来闭合圆弧。

图 14-11 显示了弧的三种闭合类型。一个Arc的默认类型是ArcType.OPEN。如果不对Arc应用描边,那么ArcType.OPENArcType.CHORD看起来是一样的。

img/336502_2_En_14_Fig11_HTML.png

图 14-11

弧的闭合类型

Arc类包含两个构造器:

  • Arc()

  • Arc(double centerX, double centerY, double radiusX, double radiusY, double startAngle, double length)

清单 14-8 中的程序展示了如何创建Arc节点。产生的窗口如图 14-12 所示。

img/336502_2_En_14_Fig12_HTML.jpg

图 14-12

使用弧形节点

// ArcTest.java
package com.jdojo.shape;

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.layout.HBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.Arc;
import javafx.scene.shape.ArcType;
import javafx.stage.Stage;

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

        @Override
        public void start(Stage stage) {
            // An OPEN arc with a fill
            Arc arc1 = new Arc(0, 0, 50, 100, 0, 90);
            arc1.setFill(Color.LIGHTGRAY);

            // An OPEN arc with no fill and a stroke

            Arc arc2 = new Arc(0, 0, 50, 100, 0, 90);
            arc2.setFill(Color.TRANSPARENT);
            arc2.setStroke(Color.BLACK);

            // A CHORD arc with no fill and a stroke
            Arc arc3 = new Arc(0, 0, 50, 100, 0, 90);
            arc3.setFill(Color.TRANSPARENT);
            arc3.setStroke(Color.BLACK);
            arc3.setType(ArcType.CHORD);

            // A ROUND arc with no fill and a stroke
            Arc arc4 = new Arc(0, 0, 50, 100, 0, 90);
            arc4.setFill(Color.TRANSPARENT);
            arc4.setStroke(Color.BLACK);
            arc4.setType(ArcType.ROUND);

            // A ROUND arc with a gray fill and a stroke
            Arc arc5 = new Arc(0, 0, 50, 100, 0, 90);
            arc5.setFill(Color.GRAY);
            arc5.setStroke(Color.BLACK);
            arc5.setType(ArcType.ROUND);

            HBox root = new HBox(arc1, arc2, arc3, arc4, arc5);
            root.setSpacing(10);
            root.setStyle("""
               -fx-padding: 10;
               -fx-border-style: solid inside;
               -fx-border-width: 2;
               -fx-border-insets: 5;
               -fx-border-radius: 5;
               -fx-border-color: blue;""");

            Scene scene = new Scene(root);
            stage.setScene(scene);
            stage.setTitle("Using Arcs");
            stage.show();
        }
}

Listing 14-8Using the Arc Class to Create Arcs, Which Are Sectors of Ellipses

绘制二次曲线

贝塞尔曲线在计算机图形学中用于绘制平滑曲线。QuadCurve类的一个实例表示使用指定的贝塞尔控制点与两个指定点相交的二次贝塞尔曲线段。QuadCurve class包含六个属性来指定三个点:

  • startX

  • startY

  • controlX

  • controlY

  • endX

  • endY

QuadCurve类包含两个构造器:

  • QuadCurve()

  • QuadCurve(double startX, double startY, double controlX, double controlY, double endX, double endY)

清单 14-9 中的程序绘制了同一个二次贝塞尔曲线两次——一次用笔画和透明填充,一次没有笔画和浅灰色填充。图 14-13 显示了两条曲线。

img/336502_2_En_14_Fig13_HTML.jpg

图 14-13

使用二次贝塞尔曲线

// QuadCurveTest.java
package com.jdojo.shape;

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.layout.HBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.QuadCurve;
import javafx.stage.Stage;

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

        @Override
        public void start(Stage stage) {
            QuadCurve qc1 = new QuadCurve(0, 100, 20, 0, 150, 100);
            qc1.setFill(Color.TRANSPARENT);
            qc1.setStroke(Color.BLACK);

            QuadCurve qc2 = new QuadCurve(0, 100, 20, 0, 150, 100);
            qc2.setFill(Color.LIGHTGRAY);

            HBox root = new HBox(qc1, qc2);
            root.setSpacing(10);
            root.setStyle("""
               -fx-padding: 10;
               -fx-border-style: solid inside;
               -fx-border-width: 2;
               -fx-border-insets: 5;
               -fx-border-radius: 5;
               -fx-border-color: blue;""");

            Scene scene = new Scene(root);
            stage.setScene(scene);
            stage.setTitle("Using QuadCurves");
            stage.show();
        }
}

Listing 14-9Using the QuadCurve Class to Draw Quadratic BezierCurve

绘制三次曲线

CubicCurve类的一个实例使用两个指定的贝塞尔控制点表示与两个指定点相交的三次贝塞尔曲线段。关于贝塞尔曲线的详细解释和演示,请参考 http://en.wikipedia.org/wiki/Bezier_curves 的维基百科文章。CubicCurve class包含八个属性来指定四个点:

  • startX

  • startY

  • controlX1

  • controlY1

  • controlX2

  • controlY2

  • endX

  • endY

CubicCurve类包含两个构造器:

  • CubicCurve()

  • CubicCurve(double startX, double startY, double controlX1, double controlY1, double controlX2, double controlY2, double endX, double endY)

清单 14-10 中的程序绘制同一个三次贝塞尔曲线两次——一次用笔画和透明填充,一次没有笔画和浅灰色填充。图 14-14 显示了两条曲线。

img/336502_2_En_14_Fig14_HTML.jpg

图 14-14

使用三次贝塞尔曲线

// CubicCurveTest.java
package com.jdojo.shape;

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.layout.HBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.CubicCurve;
import javafx.stage.Stage;

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

        @Override
        public void start(Stage stage) {
            CubicCurve cc1 = new CubicCurve(0, 50, 20, 0, 50, 80, 50, 0);
            cc1.setFill(Color.TRANSPARENT);
            cc1.setStroke(Color.BLACK);

            CubicCurve cc2 = new CubicCurve(0, 50, 20, 0, 50, 80, 50, 0);
            cc2.setFill(Color.LIGHTGRAY);

            HBox root = new HBox(cc1, cc2);
            root.setSpacing(10);
            root.setStyle("""
               -fx-padding: 10;
               -fx-border-style: solid inside;

               -fx-border-width: 2;
               -fx-border-insets: 5;
               -fx-border-radius: 5;
               -fx-border-color: blue;""");

            Scene scene = new Scene(root);
            stage.setScene(scene);
            stage.setTitle("Using CubicCurves");
            stage.show();
        }
}

Listing 14-10Using the CubicCurve Class to Draw a Cubic Bezier Curve

使用 Path 类构建复杂形状

我在前面的章节中讨论了几个形状类。它们被用来画简单的形状。对于复杂的形状不方便使用它们。您可以使用Path类绘制复杂的形状。Path类的一个实例定义了形状的路径(轮廓)。路径由一个或多个子路径组成。子路径由一个或多个路径元素组成。每个子路径都有一个起点和一个终点。

path 元素是PathElement抽象类的一个实例。PathElement类的以下子类代表特定类型的路径元素:

  • MoveTo

  • LineTo

  • HLineTo

  • VLineTo

  • ArcTo

  • QuadCurveTo

  • CubicCurveTo

  • ClosePath

在您看到一个例子之前,让我们概述一下使用Path类创建一个形状的过程。这个过程类似于用铅笔在纸上画一个形状。首先,你把铅笔放在纸上。你可以重述一遍,“你把铅笔移到纸上的一点。”不管你想画什么形状,移动铅笔到一个点必须是第一步。现在,您开始移动铅笔来绘制路径元素(例如,水平线)。当前路径元素的起点与前一个路径元素的终点相同。根据需要绘制尽可能多的路径元素(例如,垂直线、弧线和二次贝塞尔曲线)。最后,您可以在开始的同一点或其他地方结束最后一个路径元素。

定义PathElement的坐标可以是绝对的,也可以是相对的。默认情况下,坐标是绝对的。它是由PathElement类的absolute属性指定的。如果为 true(这是默认值),则坐标是绝对的。如果为假,则坐标是相对的。绝对坐标是相对于节点的局部坐标系测量的。将前一个PathElement的终点作为原点,测量相对坐标。

Path类包含三个构造器:

  • Path()

  • Path(Collection<? extends PathElement> elements)

  • Path(PathElement... elements)

无参数构造器创建一个空形状。另外两个构造器将路径元素列表作为参数。一个Path在一个ObservableList<PathElement>中存储路径元素。您可以使用getElements()方法获取列表的引用。您可以修改路径元素列表来修改形状。下面的代码片段展示了使用Path类创建形状的两种方法:

// Pass the path elements to the constructor
Path shape1 = new Path(pathElement1, pathElement2, pathElement3);

// Create an empty path and add path elements to the elements list
Path shape2 = new Path();

shape2.getElements().addAll(pathElement1, pathElement2, pathElement3);

Tip

可以同时将一个PathElement实例作为路径元素添加到Path对象中。一个Path对它所有的路径元素使用相同的fillstroke

MoveTo 路径元素

一个MoveTo路径元素用于将指定的 x 和 y 坐标作为当前点。它具有将铅笔提起并放置在纸上指定点的效果。一个Path对象的第一个路径元素必须是一个MoveTo元素,并且不能使用相对坐标。MoveTo类定义了两个double属性,它们是点的 x 和 y 坐标:

  • x

  • y

MoveTo类包含两个构造器。无参数构造器将当前点设置为(0.0,0.0)。另一个构造器将当前点的 x 和 y 坐标作为参数:

// Create a MoveTo path element to move the current point to (0.0, 0.0)
MoveTo mt1 = new MoveTo();

// Create a MoveTo path element to move the current point to (10.0, 10.0)
MoveTo mt2 = new MoveTo(10.0, 10.0);

Tip

路径必须以MoveTo路径元素开始。一个路径中可以有多个MoveTo路径元素。后续的MoveTo元素表示新子路径的起点。

LineTo 路径元素

一个LineTo路径元素从当前点到指定点画一条直线。它包含两个double属性,分别是线条末端的 x 和 y 坐标:

  • x

  • y

LineTo类包含两个构造器。无参数构造器将行尾设置为(0.0,0.0)。另一个构造器将行尾的 x 和 y 坐标作为参数:

// Create a LineTo path element with its end at (0.0, 0.0)
LineTo lt1 = new LineTo();

// Create a LineTo path element with its end at (10.0, 10.0)
LineTo lt2 = new LineTo(10.0, 10.0);

有了MoveToLineTo路径元素的知识,您可以构建仅由线条组成的形状。下面的代码片段创建了一个如图 14-15 所示的三角形。图中显示了三角形及其路径元素。箭头显示了图纸的流向。注意,绘图从(0.0)开始,使用第一个MoveTo路径元素。

img/336502_2_En_14_Fig15_HTML.png

图 14-15

使用 MoveTo 和 LineTo 路径元素创建三角形

Path triangle = new Path(
    new MoveTo(0, 0),
    new LineTo(0, 50),
    new LineTo(50, 50),
    new LineTo(0, 0));

ClosePath path 元素通过从当前点到路径的起点画一条直线来闭合一条路径。如果路径中存在多个MoveTo路径元素,一个ClosePath会从当前点到最后一个MoveTo标识的点绘制一条直线。你可以用一个ClosePath为之前的三角形例子重写路径:

Path triangle = new Path(
    new MoveTo(0, 0),
   new LineTo(0, 50),
   new LineTo(50, 50),
   new ClosePath());

清单 14-11 中的程序创建了两个Path节点:一个三角形和一个带有两个倒三角形的节点,给它一个星形的外观,如图 14-16 所示。在第二个形状中,每个三角形都被创建为一个子路径,每个子路径都以一个MoveTo元素开始。注意ClosePath元素的两种用法。每个ClosePath关闭其子路径。

img/336502_2_En_14_Fig16_HTML.jpg

图 14-16

基于路径元素的形状

// PathTest.java
package com.jdojo.shape;

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.layout.HBox;
import javafx.scene.shape.ClosePath;
import javafx.scene.shape.LineTo;
import javafx.scene.shape.MoveTo;
import javafx.scene.shape.Path;
import javafx.stage.Stage;

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

        @Override
        public void start(Stage stage) {
           Path triangle = new Path(
              new MoveTo(0, 0),
              new LineTo(0, 50),
              new LineTo(50, 50),
              new ClosePath());

            Path star = new Path();
            star.getElements().addAll(
               new MoveTo(30, 0),
               new LineTo(0, 30),
               new LineTo(60, 30),
               new ClosePath(),/* new LineTo(30, 0), */
               new MoveTo(0, 10),
               new LineTo(60, 10),
               new LineTo(30, 40),
               new ClosePath() /*new LineTo(0, 10)*/);

            HBox root = new HBox(triangle, star);
            root.setSpacing(10);
            root.setStyle("""
               -fx-padding: 10;
               -fx-border-style: solid inside;
               -fx-border-width: 2;
               -fx-border-insets: 5;
               -fx-border-radius: 5;
               -fx-border-color: blue;""");

            Scene scene = new Scene(root);
            stage.setScene(scene);
            stage.setTitle("Using Paths");
            stage.show();
        }

}

Listing 14-11Using the Path Class to Create a Triangle and a Star

lineto 和 lineto 路径元素

HLineTo path 元素从当前点到指定的 x 坐标画一条水平线。直线终点的 y 坐标与当前点的 y 坐标相同。HLineTo类的x属性指定了终点的 x 坐标:

// Create an horizontal line from the current point (x, y) to (50, y)
HLineTo hlt = new HLineTo(50);

VLineTo path 元素从当前点到指定的 y 坐标画一条垂直线。直线终点的 x 坐标与当前点的 x 坐标相同。VLineTo类的y属性指定了终点的 y 坐标:

// Create a vertical line from the current point (x, y) to (x, 50)
VLineTo vlt = new VLineTo(50);

Tip

LineTo路径元素是HLineToVLineTo的通用版本。

下面的代码片段创建了与上一节中讨论的相同的三角形。这一次,您使用HLineToVLineTo路径元素来绘制三角形的底边和高边,而不是使用LineTo路径元素:

Path triangle = new Path(
   new MoveTo(0, 0),
   new VLineTo(50),
   new HLineTo(50),
   new ClosePath());

ArcTo 路径元素

一个ArcTo路径元素定义了一段连接当前点和指定点的椭圆。它包含以下属性:

  • radiusX

  • radiusY

  • x

  • y

  • XAxisRotation

  • largeArcFlag

  • sweepFlag

radiusXradiusY属性指定椭圆的水平和垂直半径。xy属性指定圆弧终点的 x 和 y 坐标。请注意,圆弧的起点是路径的当前点。

XAxisRotation属性指定椭圆 x 轴的旋转角度。请注意,旋转是针对从中获得圆弧的椭圆的 x 轴,而不是节点坐标系的 x 轴。正值逆时针旋转 x 轴。

largeArcFlagsweepFlag属性是布尔类型,默认情况下,它们被设置为 false。它们的用途需要详细的解释。两个椭圆可以通过两个给定点,如图 14-17 所示,给我们四条弧线来连接这两点。

img/336502_2_En_14_Fig17_HTML.png

图 14-17

largeArcFlag 和 sweepFlag 属性对 ArcTo path 元素的影响

图 14-17 分别显示了标记为StartEnd的起点和终点。椭圆上的两点可以穿过较大的弧或较小的弧。如果largeArcFlag为真,则使用较大的圆弧。否则,使用较小的圆弧。

当决定使用较大或较小的圆弧时,您仍然有两个选择:将使用两个可能椭圆中的哪个椭圆?这由sweepFlag属性决定。尝试使用两个选定的圆弧(两个较大的圆弧或两个较小的圆弧)绘制从起点到终点的圆弧。对于一个圆弧,遍历将是顺时针方向,而对于另一个圆弧,遍历将是逆时针方向。如果sweepFlag为真,则使用顺时针遍历的椭圆。如果sweepFlag为假,则使用逆时针遍历的椭圆。表 14-1 显示了根据这两个属性将使用哪个椭圆的哪种类型的圆弧。

表 14-1

基于 largeArcFlag 和 sweepFlag 属性选择弧段和椭圆

|

长焦

|

扫雷日

|

弧型

|

椭圆

| | --- | --- | --- | --- | | true | true | 更大的 | 椭圆-2 | | true | false | 更大的 | 椭圆-1 | | false | true | 较小的 | 椭圆-1 | | false | false | 较小的 | 椭圆-2 |

清单 14-12 中的程序使用一个ArcTo路径元素来构建一个Path对象。该程序允许用户改变ArcTo路径元素的属性。运行程序并更改largeArcFlagsweepFlag和其他属性,看看它们如何影响ArcTo路径元素。

// ArcToTest.java
package com.jdojo.shape;

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.CheckBox;
import javafx.scene.control.Label;
import javafx.scene.control.Slider;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.GridPane;
import javafx.scene.shape.ArcTo;
import javafx.scene.shape.HLineTo;
import javafx.scene.shape.MoveTo;
import javafx.scene.shape.Path;
import javafx.scene.shape.VLineTo;
import javafx.stage.Stage;

public class ArcToTest extends Application {
        private ArcTo arcTo;

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

        @Override
        public void start(Stage stage) {
            // Create the ArcTo path element
            arcTo = new ArcTo();

            // Use the arcTo element to build a Path
            Path path = new Path(
               new MoveTo(0, 0),
               new VLineTo(100),
               new HLineTo(100),
               new VLineTo(50),
               arcTo);

            BorderPane root = new BorderPane();
            root.setTop(this.getTopPane());
            root.setCenter(path);
            root.setStyle("""
               -fx-padding: 10;
               -fx-border-style: solid inside;

               -fx-border-width: 2;
               -fx-border-insets: 5;
               -fx-border-radius: 5;
               -fx-border-color: blue;""");

            Scene scene = new Scene(root);
            stage.setScene(scene);
            stage.setTitle("Using ArcTo Path Elements");
            stage.show();
        }

        private GridPane getTopPane() {
            CheckBox largeArcFlagCbx = new CheckBox("largeArcFlag");
            CheckBox sweepFlagCbx = new CheckBox("sweepFlag");
            Slider xRotationSlider = new Slider(0, 360, 0);
            xRotationSlider.setPrefWidth(300);
            xRotationSlider.setBlockIncrement(30);
            xRotationSlider.setShowTickMarks(true);
            xRotationSlider.setShowTickLabels(true);

            Slider radiusXSlider = new Slider(100, 300, 100);
            radiusXSlider.setBlockIncrement(10);
            radiusXSlider.setShowTickMarks(true);
            radiusXSlider.setShowTickLabels(true);

            Slider radiusYSlider = new Slider(100, 300, 100);
            radiusYSlider.setBlockIncrement(10);
            radiusYSlider.setShowTickMarks(true);
            radiusYSlider.setShowTickLabels(true);

            // Bind ArcTo properties to the control data

            arcTo.largeArcFlagProperty().bind(
                    largeArcFlagCbx.selectedProperty());
            arcTo.sweepFlagProperty().bind(
                    sweepFlagCbx.selectedProperty());
            arcTo.XaxisRotationProperty().bind(
                    xRotationSlider.valueProperty());
            arcTo.radiusXProperty().bind(
                    radiusXSlider.valueProperty());
            arcTo.radiusYProperty().bind(
                    radiusYSlider.valueProperty());

            GridPane pane = new GridPane();
            pane.setHgap(5);
            pane.setVgap(10);
            pane.addRow(0, largeArcFlagCbx, sweepFlagCbx);
            pane.addRow(1, new Label("XAxisRotation"), xRotationSlider);
            pane.addRow(2, new Label("radiusX"), radiusXSlider);
            pane.addRow(3, new Label("radiusY"), radiusYSlider);

            return pane;
        }
}

Listing 14-12Using ArcTo Path Elements

QuadCurveTo 路径元素

QuadCurveTo类的一个实例使用指定的控制点(controlX,controlY)从当前点到指定的终点(x,y)绘制一条二次贝塞尔曲线。它包含四个属性来指定结束点和控制点。

  • x

  • y

  • controlX

  • controlY

xy属性指定终点的 x 和 y 坐标。controlXcontrolY属性指定控制点的 x 和 y 坐标。

QuadCurveTo类包含两个构造器:

  • QuadCurveTo()

  • QuadCurveTo(double controlX, double controlY, double x, double y)

下面的代码片段使用了一个带有(10,100)控制点和(0,0)结束点的QuadCurveTo。图 14-18 显示了结果路径。

img/336502_2_En_14_Fig18_HTML.png

图 14-18

使用 QuadCurveTo 路径元素

Path path = new Path(
   new MoveTo(0, 0),
   new VLineTo(100),
   new HLineTo(100),
   new VLineTo(50),
   new QuadCurveTo(10, 100, 0, 0));

三次曲线到路径元素

CubicCurveTo类的实例使用指定的控制点(controlX1,controlY1)和(controlX2,controlY2)绘制从当前点到指定终点(x,y)的三次贝塞尔曲线。它包含六个属性来指定结束点和控制点:

  • x

  • y

  • controlX1

  • controlY1

  • controlX2

  • controlY2

xy属性指定终点的 x 和 y 坐标。controlX1controlY1属性指定第一个控制点的 x 和 y 坐标。controlX2controlY2属性指定第二个控制点的 x 和 y 坐标。

CubicCurveTo类包含两个构造器:

  • CubicCurveTo()

  • CubicCurveTo(double controlX1, double controlY1, double controlX2, double controlY2, double x, double y)

下面的代码片段使用了一个CubicCurveTo,将(10,100)和(40,80)作为控制点,将(0,0)作为结束点。图 14-19 显示了结果路径。

img/336502_2_En_14_Fig19_HTML.png

图 14-19

使用 QuadCurveTo 路径元素

Path path = new Path(
   new MoveTo(0, 0),
   new VLineTo(100),
   new HLineTo(100),
   new VLineTo(50),
   new CubicCurveTo(10, 100, 40, 80, 0, 0));

ClosePath 路径元素

ClosePath path 元素关闭当前子路径。注意一个Path可能由多个子路径组成,因此,一个Path中可能有多个ClosePath元素。一个ClosePath元素从当前点到当前子路径的起始点画一条直线,并结束子路径。一个ClosePath元素后面可能跟着一个MoveTo元素,在这种情况下,MoveTo元素是下一个子路径的起点。如果一个ClosePath元素后面是一个路径元素而不是一个MoveTo元素,那么下一个子路径从被ClosePath元素关闭的子路径的起始点开始。

下面的代码片段创建了一个Path对象,它使用了两个子路径。每个子路径画一个三角形。使用ClosePath元素关闭子路径。图 14-20 显示了最终的形状。

img/336502_2_En_14_Fig20_HTML.png

图 14-20

使用两个子路径和一个 ClosePath 元素的形状

Path p1 = new Path(
   new MoveTo(50, 0),
   new LineTo(0, 50),
   new LineTo(100, 50),
   new ClosePath(),
   new MoveTo(90, 15),
   new LineTo(40, 65),
   new LineTo(140, 65),
   new ClosePath());
p1.setFill(Color.LIGHTGRAY);

路径的填充规则

一个Path可以用来画非常复杂的形状。有时,很难确定一个点是在形状内部还是外部。Path类包含一个fillRule属性,用于确定一个点是否在一个形状内。它的值可以是FillRule枚举:NON_ZEROEVEN_ODD的常量之一。如果某个点在形状内部,它将使用填充颜色进行渲染。图 14-21 显示了由一个Path和两个三角形公共区域中的一个点创建的两个三角形。我将讨论该点是否在形状内部。

img/336502_2_En_14_Fig21_HTML.png

图 14-21

由两个三角形子路径组成的形状

笔划的方向是决定一个点是否在形状内部的重要因素。图 14-21 中的形状可以用不同方向的笔画出来。图 14-22 显示了其中的两个。在形状-1 中,两个三角形都使用逆时针笔划。在形状-2 中,一个三角形使用逆时针笔划,另一个使用顺时针笔划。

img/336502_2_En_14_Fig22_HTML.png

图 14-22

由两个使用不同描边方向的三角形子路径组成的形状

Path的填充规则从该点到无限远绘制光线,因此它们可以与所有路径段相交。在NON_ZERO填充规则中,如果逆时针和顺时针方向上与射线相交的路径段数量相等,则该点在形状之外。否则,点在形状内部。你可以用一个从零开始的计数器来理解这个规则。对于逆时针方向与路径段相交的每条射线,计数器加 1。对于每一条沿顺时针方向与路径段相交的光线,从计数器中减去 1。最后,如果计数器不为零,则该点在内部;否则,重点就在外面。图 14-23 显示了应用NON_ZERO填充规则时,由两个三角形子路径组成的相同两条路径及其计数器值。从该点画出的射线用虚线表示。第一个形状中的点得分为 6(非零值),并且位于路径内部。第二个形状中的点得分为零,它在路径之外。

img/336502_2_En_14_Fig23_HTML.png

图 14-23

将非零填充规则应用于两个三角形子路径

NON_ZERO填充规则一样,EVEN_ODD填充规则也从一个点向无限远的所有方向绘制光线,因此所有路径段都相交。它计算光线和路径段之间的相交数。如果数字是奇数,则该点在路径内。否则,该点在路径之外。如果将图 14-23 中所示的两个图形的fillRule属性设置为EVEN_ODD,那么这两个图形的点都在路径之外,因为在这两种情况下,光线和路径段之间的交点数量都是六(偶数)。一个PathfillRule属性的默认值是FillRule.NON_ZERO

清单 14-13 中的程序是本节讨论的例子的一个实现。它绘制了四个路径:前两个(从左数)使用NON_ZERO填充规则,后两个使用EVEN_ODD填充规则。图 14-24 显示了路径。第一个和第三个路径使用逆时针笔划绘制两个三角形子路径。第二个和第四个路径是使用逆时针笔划绘制的,对于一个三角形使用顺时针笔划。

img/336502_2_En_14_Fig24_HTML.jpg

图 14-24

使用不同填充规则的路径

// PathFillRule.java
package com.jdojo.shape;

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.layout.HBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.FillRule;
import javafx.scene.shape.LineTo;
import javafx.scene.shape.MoveTo;
import javafx.scene.shape.Path;
import javafx.scene.shape.PathElement;
import javafx.stage.Stage;

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

        @Override
        public void start(Stage stage) {
            // Both triangles use a counterclockwise stroke
            PathElement[] pathElements1 = {
               new MoveTo(50, 0),
               new LineTo(0, 50),
               new LineTo(100, 50),
               new LineTo(50, 0),
               new MoveTo(90, 15),
               new LineTo(40, 65),
               new LineTo(140, 65),
               new LineTo(90, 15)};

            // One triangle uses a clockwise stroke and
            // another uses a counterclockwise stroke
            PathElement[] pathElements2 = {
               new MoveTo(50, 0),
               new LineTo(0, 50),
               new LineTo(100, 50),
               new LineTo(50, 0),
               new MoveTo(90, 15),
               new LineTo(140, 65),
               new LineTo(40, 65),
               new LineTo(90, 15)};

            /* Using the NON-ZERO fill rule by default */
            Path p1 = new Path(pathElements1);
            p1.setFill(Color.LIGHTGRAY);

            Path p2 = new Path(pathElements2);
            p2.setFill(Color.LIGHTGRAY);

            /* Using the EVEN_ODD fill rule */
            Path p3 = new Path(pathElements1);
            p3.setFill(Color.LIGHTGRAY);
            p3.setFillRule(FillRule.EVEN_ODD);

            Path p4 = new Path(pathElements2);
            p4.setFill(Color.LIGHTGRAY);

            p4.setFillRule(FillRule.EVEN_ODD);

            HBox root = new HBox(p1, p2, p3, p4);
            root.setSpacing(10);
            root.setStyle("""
               -fx-padding: 10;
               -fx-border-style: solid inside;
               -fx-border-width: 2;
               -fx-border-insets: 5;
               -fx-border-radius: 5;
               -fx-border-color: blue;""");

            Scene scene = new Scene(root);
            stage.setScene(scene);
            stage.setTitle("Using Fill Rules for Paths");
            stage.show();
        }
}

Listing 14-13Using Fill Rules for Paths

绘制可缩放矢量图形

SVGPath类的一个实例从编码字符串中的路径数据绘制一个形状。你可以在 www.w3.org/TR/SVG 找到 SVG 规范。在 www.w3.org/TR/SVG/paths.html 可以找到构造字符串格式路径数据的详细规则。JavaFX 部分支持 SVG 规范。

SVGPath类包含一个无参数构造器来创建它的对象:

// Create a SVGPath object
SVGPath sp = new SVGPath();

SVGPath类包含两个属性:

  • content

  • fillRule

属性定义了 SVG path的编码字符串。fillRule属性指定形状内部的填充规则,可以是FillRule.NON_ZEROFillRule.EVEN_ODDfillRule属性的默认值是FillRule.NON_ZERO。有关填充规则的更多详细信息,请参考“路径的填充规则”一节。PathSVGPath的填充规则相同。

下面的代码片段将“M50,0 L0,50 L100,50 Z”编码字符串设置为SVGPath对象绘制三角形的内容,如图 14-25 所示:

img/336502_2_En_14_Fig25_HTML.jpg

图 14-25

使用 SVGPath 的三角形

SVGPath sp2 = new SVGPath();
sp2.setContent("M50, 0 L0, 50 L100, 50 Z");
sp2.setFill(Color.LIGHTGRAY);
sp2.setStroke(Color.BLACK);

SVGPath的内容是遵循一些规则的编码字符串:

  • 该字符串由一系列命令组成。

  • 每个命令名的长度正好是一个字母。

  • 命令后面是它的参数。

  • 命令的参数值由逗号或空格分隔。例如,“M50,0 L0,50 L100,50 Z”和“M50 0 L0 50 L100 50 Z”代表相同的路径。为了可读性,您将使用逗号来分隔两个值。

  • 您不需要在命令字符前后添加空格。比如“M50 0 L0 50 L100 50 Z”可以改写为“M50 0L0 50L100 50Z”。

让我们考虑一下上一个例子中使用的 SVG 内容:

M50, 0 L0, 50 L100, 50 Z

内容由四个命令组成:

  • M50, 0

  • L0, 50

  • L100, 50

  • Z

将 SVG 路径命令与Path API 比较,第一个命令是“MoveTo (50,0)”;第二个命令是“LineTo(0,50)”;第三个命令是“LineTo(100,50)”;第四个命令是“ClosePath”。

Tip

SVGPath内容中的命令名是代表Path对象中路径元素的类的第一个字母。例如,Path API 中的绝对MoveTo变成了SVGPath内容中的M,绝对LineTo变成了L,以此类推。

命令的参数是坐标,可以是绝对的,也可以是相对的。当命令名为大写时(如M),其参数被认为是绝对的。当命令名为小写(例如 m)时,其参数被认为是相对的。“关闭路径”命令是Zz。因为“closepath”命令不带任何参数,所以大写和小写版本的行为是相同的。

考虑两个SVG路径的内容:

  • M50, 0 L0, 50 L100, 50 Z

  • M50, 0 l0, 50 l100, 50 Z

第一条路径使用绝对坐标。第二条路径使用绝对和相对坐标。像Path一样,SVGPath必须以“moveTo”命令开始,该命令必须使用绝对坐标。如果SVGPath以相对“移动到”命令开始(如"m 50, 0",其参数被视为绝对坐标。在前面的 SVG 路径中,可以用"m50, 0"作为字符串的开头,结果是一样的。

前面的两个 SVG 路径将绘制两个不同的三角形,如图 14-26 所示,尽管两者使用相同的参数。第一条路径在左边绘制三角形,第二条路径在右边绘制三角形。第二条路径中的命令解释如下:

img/336502_2_En_14_Fig26_HTML.jpg

图 14-26

在 SVG 路径中使用绝对和相对坐标

  • 移动到(50,0)。

  • 从当前点(50,0)到(50,50)画一条线。终点(50,50)是通过将当前点的 x 和 y 坐标添加到相对的“lineto”命令(l)参数中得出的。终点变成(50,50)。

  • 从当前点(50,50)到(150,100)画一条线。同样,终点的坐标是通过将当前点的 x 和 y 坐标(50,50)与命令参数“l100,50”相加得出的(“l100,50”中的第一个字符是小写的 L,而不是数字 1)。

  • 然后关闭路径(Z)。

表 14-2 列出了SVGPath对象内容中使用的命令。它还列出了在Path API 中使用的等价类。下表列出了使用绝对坐标的命令。命令的相对版本使用小写字母。参数列中的加号(+)表示可以使用多个参数。

表 14-2

SVG 路径命令列表

|

命令

|

参数

|

命令名称

|

路径 API 类

| | --- | --- | --- | --- | | M | (x, y)+ | moveto | MoveTo | | L | (x,y)+ | lineto | LineTo | | H | x+ | lineto | HLineTo | | V | y+ | lineto | VLineTo | | A | (rx,ry,x 轴旋转,大圆弧标志,扫描标志,x,y)+ | arcto | ArcTo | | Q | (x1,y1,x,y)+ | Quadratic Bezier curveto | QuadCurveTo | | T | (x,y)+ | Shorthand/smooth quadratic Bezier curveto | QuadCurveTo | | C | (x1,y1,x2,y2,x,y)+ | curveto | CubicCurveTo | | S | (x2,y2,x,y)+ | Shorthand/smooth curveto | CubicCurveTo | | Z | 没有人 | closePath | ClosePath |

“移动到”命令

“moveTo”命令M在指定的(x,y)坐标开始一个新子路径。它后面可能是一对或多对坐标。第一对坐标被认为是该点的 x 和 y 坐标,该命令将使其成为当前点。每个额外的对都被视为“lineto”命令的一个参数。如果“moveTo”命令是相对的,则“lineto”命令也是相对的。如果“moveTo”命令是绝对的,则“lineto”命令将是绝对的。例如,以下两个 SVG 路径是相同的:

M50, 0 L0, 50 L100, 50 Z
M50, 0, 0, 50, 100, 50 Z

“lineto”命令

有三个“行到”命令:LHV。它们被用来画直线。

命令L用于从当前点到指定的(x,y)点画一条直线。如果指定多对(x,y)坐标,它将绘制一条多段线。(x,y)坐标的最后一对成为新的当前点。下面的 SVG 路径将绘制相同的三角形。第一个使用两个L命令,第二个只使用一个:

  • M50, 0 L0, 50 L100, 50 L50, 0

  • M50, 0 L0, 50, 100, 50, 50, 0

HV命令用于从当前点绘制水平线和垂直线。命令H从当前点(cx,cy)到(x,cy)画一条水平线。命令V画一条从当前点(cx,cy)到(cx,y)的垂直线。您可以向它们传递多个参数。最后一个参数值定义了当前点。比如“M0,0H200,100 V50Z”会画一条从(0,0)到(200,0),从(200,0)到(100,0)的线。第二个命令将(100,0)作为当前点。第三个命令将绘制一条从(100,0)到(100,50)的垂直线。z 命令将绘制一条从(100,50)到(0,0)的直线。下面的代码片段绘制了一个 SVG 路径,如图 14-27 所示:

img/336502_2_En_14_Fig27_HTML.jpg

图 14-27

对“lineto”命令使用多个参数

SVGPath p1 = new SVGPath();
p1.setContent("M0, 0H-50, 50, 0 V-50, 50, 0, -25 L25, 0");
p1.setFill(Color.LIGHTGRAY);
p1.setStroke(Color.BLACK);

“arcto”命令

“arcto”命令A从当前点到指定的(x,y)点画一个椭圆弧。它使用 rx 和 ry 作为沿 x 轴和 y 轴的半径。x 轴旋转是椭圆 x 轴的旋转角度,以度为单位。大弧标志和扫描标志是用于从四个可能的弧中选择一个弧的标志。使用 0 和 1 作为标志值,其中 1 表示真,0 表示假。有关其所有参数的详细说明,请参考“ArcTo 路径元素”一节。您可以传递多个弧参数,在这种情况下,一个弧的终点将成为下一个弧的当前点。下面的代码片段用弧线绘制了两条 SVG 路径。第一个路径为“arcTo”命令使用一个参数,第二个路径使用两个参数。图 14-28 显示了路径。

img/336502_2_En_14_Fig28_HTML.jpg

图 14-28

使用“arcTo”命令绘制椭圆弧路径

SVGPath p1 = new SVGPath();

// rx=150, ry=50, x-axis-rotation=0, large-arc-flag=0,
// sweep-flag 0, x=-50, y=50
p1.setContent("M0, 0 A150, 50, 0, 0, 0, -50, 50 Z");
p1.setFill(Color.LIGHTGRAY);
p1.setStroke(Color.BLACK);

// Use multiple arcs in one "arcTo" command
SVGPath p2 = new SVGPath();

// rx1=150, ry1=50, x-axis-rotation1=0, large-arc-flag1=0,
// sweep-flag1=0, x1=-50, y1=50
// rx2=150, ry2=10, x-axis-rotation2=0, large-arc-flag2=0,
// sweep-flag2=0, x2=10, y2=10
p2.setContent("M0, 0 A150 50 0 0 0 -50 50, 150 10 0 0 0 10 10 Z");
p2.setFill(Color.LIGHTGRAY);
p2.setStroke(Color.BLACK);

“二次贝塞尔曲线”命令

命令QT都用于绘制二次贝塞尔曲线。

命令Q使用指定的(x1,y1)作为控制点,从当前点到指定的(x,y)点绘制一条二次贝塞尔曲线。

命令T使用一个控制点绘制一条从当前点到指定(x,y)点的二次贝塞尔曲线,该控制点是前一个命令的控制点的反射。如果没有先前的命令或先前的命令不是QqTt,则当前点被用作控制点。

命令Q将控制点作为参数,而命令T采用控制点。以下代码片段使用命令QT绘制二次贝塞尔曲线,如图 14-29 所示:

img/336502_2_En_14_Fig29_HTML.jpg

图 14-29

使用 Q 和 T 命令绘制二次贝塞尔曲线

SVGPath p1 = new SVGPath();
p1.setContent("M0, 50 Q50, 0, 100, 50");
p1.setFill(Color.LIGHTGRAY);
p1.setStroke(Color.BLACK);

SVGPath p2 = new SVGPath();
p2.setContent("M0, 50 Q50, 0, 100, 50 T200, 50");
p2.setFill(Color.LIGHTGRAY);
p2.setStroke(Color.BLACK);

“三次贝塞尔曲线”命令

命令CS用于绘制三次贝塞尔曲线。

命令C使用指定的控制点(x1,y1)和(x2,y2)绘制从当前点到指定点(x,y)的三次贝塞尔曲线。

命令S从当前点到指定点(x,y)绘制一条三次贝塞尔曲线。它假设第一个控制点是前一个命令上第二个控制点的反射。如果没有先前的命令或先前的命令不是 C、C、S或 s,则当前点用作第一个控制点。指定点(x2,y2)是第二个控制点。多组坐标画出一个凝聚体。

下面这段代码使用命令CS绘制三次贝塞尔曲线,如图 14-30 所示。第二条路径使用命令S将前一个命令C的第二个控制点的反射作为其第一个控制点:

img/336502_2_En_14_Fig30_HTML.jpg

图 14-30

使用 C 和 S 命令绘制三次贝塞尔曲线

SVGPath p1 = new SVGPath();
p1.setContent("M0, 0 C0, -100, 100, 100, 100, 0");
p1.setFill(Color.LIGHTGRAY);
p1.setStroke(Color.BLACK);

SVGPath p2 = new SVGPath();
p2.setContent("M0, 0 C0, -100, 100, 100, 100, 0 S200 100 200, 0");
p2.setFill(Color.LIGHTGRAY);
p2.setStroke(Color.BLACK);

“closepath”命令

“闭合路径”命令Zz从当前点到当前子路径的起点画一条直线,并结束子路径。该命令的大写和小写版本工作方式相同。

组合形状

Shape类提供了三个静态方法,让您执行形状的并集、交集和差集:

  • union(Shape shape1, Shape shape2)

  • intersect(Shape shape1, Shape shape2)

  • subtract(Shape shape1, Shape shape2)

这些方法返回一个新的Shape实例。它们对输入形状的区域进行操作。如果形状没有填充和描边,则其面积为零。新形状有一个描边和一个填充。union()方法组合两个形状的面积。intersect()方法使用形状之间的公共区域来创建新的形状。subtract()方法通过从第一个形状中减去指定的第二个形状来创建新的形状。

清单 14-14 中的程序使用并集、交集和减法运算来组合两个圆。图 14-31 显示了最终的形状。

img/336502_2_En_14_Fig31_HTML.jpg

图 14-31

由两个圆组合而成的形状

// CombiningShapesTest.java
package com.jdojo.shape;

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.layout.HBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Shape;
import javafx.stage.Stage;

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

        @Override
        public void start(Stage stage) {
            Circle c1 = new Circle (0, 0, 20);
            Circle c2 = new Circle (15, 0, 20);

            Shape union = Shape.union(c1, c2);
            union.setStroke(Color.BLACK);
            union.setFill(Color.LIGHTGRAY);

            Shape intersection = Shape.intersect(c1, c2);
            intersection.setStroke(Color.BLACK);
            intersection.setFill(Color.LIGHTGRAY);

            Shape subtraction = Shape.subtract(c1, c2);
            subtraction.setStroke(Color.BLACK);
            subtraction.setFill(Color.LIGHTGRAY);

            HBox root = new HBox(union, intersection, subtraction);
            root.setSpacing(20);
            root.setStyle("""
               -fx-padding: 10;
               -fx-border-style: solid inside;
               -fx-border-width: 2;
               -fx-border-insets: 5;
               -fx-border-radius: 5;
               -fx-border-color: blue;""");

            Scene scene = new Scene(root);
            stage.setScene(scene);
            stage.setTitle("Combining Shapes");
            stage.show();
        }

}

Listing 14-14Combining Shapes to Create New Shapes

了解形状的笔画

描边是画出形状轮廓的过程。有时,形状的轮廓也称为笔画。Shape类包含几个定义形状笔画外观的属性:

  • stroke

  • strokeWidth

  • strokeType

  • strokeLineCap

  • strokeLineJoin

  • strokeMiterLimit

  • strokeDashOffset

stroke属性指定笔画的颜色。对于除了LinePathPolyline之外的所有形状,默认的stroke被设置为null,它们的默认笔画为Color.BLACK

strokeWidth属性指定笔画的宽度。默认为 1.0px。

描边是沿着形状的边界绘制的。strokeType属性指定边界上笔画宽度的分布。它的值是StrokeType枚举的三个常量CENTEREDINSIDEOUTSIDE之一。默认值为CENTEREDCENTERED描边类型将描边宽度的一半画在边界外,另一半画在边界内。INSIDE笔划类型在边界内绘制笔划。OUTSIDE描边在边界外绘制描边。形状的描边宽度包含在其布局边界内。

清单 14-15 中的程序创建了四个矩形,如图 14-32 所示。所有矩形都有相同的widthheight ( 50px 和 50px)。从左边数起,第一个矩形没有描边,其布局边界为 50px X 50px。第二个矩形使用宽度为 4px 的笔画和INSIDE笔画类型。INSIDE笔画类型绘制在宽度和高度边界内,矩形的布局边界为 50px X 50px。第三个矩形使用 4px 的描边宽度和默认的CENTERED描边类型。笔画在边界内绘制 2px,在边界外绘制 2px。2px 外部笔划被添加到所有四个尺寸中,使得布局边界为 54px X 54px。第四个矩形使用 4px 描边宽度和OUTSIDE描边类型。整个笔画宽度超出了矩形的宽度和高度,使布局为 58px X 58px。

img/336502_2_En_14_Fig32_HTML.jpg

图 14-32

使用不同类型笔画的矩形

// StrokeTypeTest.java
package com.jdojo.shape;

import javafx.application.Application;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.layout.HBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.scene.shape.StrokeType;
import javafx.stage.Stage;

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

        @Override
        public void start(Stage stage) {
            Rectangle r1 = new Rectangle(50, 50);
            r1.setFill(Color.LIGHTGRAY);

            Rectangle r2 = new Rectangle(50, 50);
            r2.setFill(Color.LIGHTGRAY);
            r2.setStroke(Color.BLACK);
            r2.setStrokeWidth(4);
            r2.setStrokeType(StrokeType.INSIDE);

            Rectangle r3 = new Rectangle(50, 50);
            r3.setFill(Color.LIGHTGRAY);
            r3.setStroke(Color.BLACK);
            r3.setStrokeWidth(4);

            Rectangle r4 = new Rectangle(50, 50);
            r4.setFill(Color.LIGHTGRAY);
            r4.setStroke(Color.BLACK);
            r4.setStrokeWidth(4);
            r4.setStrokeType(StrokeType.OUTSIDE);

            HBox root = new HBox(r1, r2, r3, r4);
            root.setAlignment(Pos.CENTER);
            root.setSpacing(10);
            root.setStyle("""
               -fx-padding: 10;
               -fx-border-style: solid inside;
               -fx-border-width: 2;
               -fx-border-insets: 5;
               -fx-border-radius: 5;
               -fx-border-color: blue;""");

            Scene scene = new Scene(root);
            stage.setScene(scene);
            stage.setTitle("Using Different Stroke Types for Shapes");
            stage.show();
        }

}

Listing 14-15Effects of Applying Different Stroke Types on a Rectangle

strokeLineCap属性为未闭合的子路径和虚线段指定笔画的结束修饰。它的值是StrokeLineCap枚举的常量之一:BUTTSQUAREROUND。默认为BUTTBUTT线帽不在子路径的末端添加装饰;笔画恰好在起点和终点开始和结束。SQUARE线帽延伸末端一半的行程宽度。ROUND线帽在末端增加一个圆帽。圆帽使用的半径等于笔划宽度的一半。图 14-33 显示三条线,为未闭合的子路径。所有线条的宽度都是 100 像素,使用 10 像素的笔画宽度。图中显示了他们使用的strokeLineCap。使用BUTT线帽的线条的布局边界宽度保持为 100 像素。但是,对于其他两行,布局边界的宽度增加到 110 像素,两端增加 10 像素。

img/336502_2_En_14_Fig33_HTML.png

图 14-33

笔画的不同线帽样式

注意,strokeLineCap属性应用于未闭合的子路径的线段末端。图 14-34 显示了由未闭合子路径创建的三个三角形。他们使用不同的笔画线帽。使用 SVG 路径数据“M50,0L0,50 M0,50 L100,50 M100,50 L50,0”来绘制三角形。填充设置为null,笔画宽度设置为 10px。

img/336502_2_En_14_Fig34_HTML.png

图 14-34

使用未闭合子路径的三角形使用不同的描边线帽

属性指定子路径的两个连续路径元素如何连接。它的值是StrokeLineJoin枚举的常量之一:BEVELMITERROUND。默认为斜接。BEVEL线连接通过一条直线连接路径元素的外角。MITER线连接延伸两个路径元素的外边缘,直到它们相遇。ROUND线条连接通过将两个路径元素的角圆化一半的笔画宽度来连接它们。图 14-35 显示了用 SVG 路径数据“M50,0L0,50 L100,50 Z”创建的三个三角形。填充颜色为空,描边宽度为 10px。如图所示,三角形使用不同的线连接。

img/336502_2_En_14_Fig35_HTML.png

图 14-35

使用不同线条连接类型的三角形

一个MITER线连接通过延伸它们的外边缘来连接两个路径元素。如果路径元素以较小的角度相交,连接的长度可能会变得很大。您可以使用strokeMiterLimit属性来限制连接的长度。它指定斜接长度和描边宽度的比率。斜接长度是连接的最内侧点和最外侧点之间的距离。如果两个路径元素不能通过在此范围内扩展它们的外边缘而相遇,则使用一个BEVEL连接。默认值为 10.0。也就是说,默认情况下,斜接长度可能高达笔画宽度的十倍。

下面的代码片段创建了两个三角形,如图 14-36 所示。默认情况下,两者都使用MITER线连接。第一个三角形使用 2.0 作为斜接限制。第二个三角形使用默认的斜接限制,即 10.0。笔画宽度为 10px。第一个三角形尝试通过将两条线延伸到 20px 来连接角,20px 是通过将 10px 描边宽度乘以 2.0 的斜接限制来计算的。在 20px 内不能使用MITER连接来连接拐角,所以使用了BEVEL连接。

img/336502_2_En_14_Fig36_HTML.jpg

图 14-36

使用不同笔划斜接限制的三角形

SVGPath t1 = new SVGPath();
t1.setContent("M50, 0L0, 50 L100, 50 Z");
t1.setStrokeWidth(10);
t1.setFill(null);
t1.setStroke(Color.BLACK);
t1.setStrokeMiterLimit(2.0);

SVGPath t2 = new SVGPath();
t2.setContent("M50, 0L0, 50 L100, 50 Z");
t2.setStrokeWidth(10);
t2.setFill(null);
t2.setStroke(Color.BLACK);

默认情况下,描边绘制实心轮廓。也可以有虚线轮廓。您需要提供一个虚线模式和虚线偏移量。虚线图案是存储在ObservableList<Double>中的double的数组。您可以使用Shape类的getStrokeDashArray()方法来获取列表的引用。列表的元素指定了虚线和间隙的模式。第一个元素是虚线长度、第二个间隙、第三个虚线长度、第四个间隙等等。急骤的图案被重复以画出轮廓。strokeDashOffset属性指定笔画开始处的虚线图案的偏移量。

下面的代码片段创建了两个Polygon实例,如图 14-37 所示。两者都使用相同的虚线模式,但虚线偏移不同。第一个使用 0.0 的虚线偏移,这是默认值。第一个矩形的笔画以 15.0px 的虚线开始,这是虚线图案的第一个元素,可以在从(0,0)到(100,0)绘制的虚线中看到。第二个Polygon使用虚线偏移量 20.0,这意味着笔画将在虚线图案内 20.0px 处开始。前两个元素 15.0 和 3.0 在虚线偏移 20.0 内。因此,第二个Polygon的笔画从第三个元素开始,这是一个 5.0px 的破折号。

img/336502_2_En_14_Fig37_HTML.jpg

图 14-37

两个多边形的轮廓使用虚线图案

Polygon p1 = new Polygon(0, 0, 100, 0, 100, 50, 0, 50, 0, 0);
p1.setFill(null);
p1.setStroke(Color.BLACK);
p1.getStrokeDashArray().addAll(15.0, 5.0, 5.0, 5.0);

Polygon p2 = new Polygon(0, 0, 100, 0, 100, 50, 0, 50, 0, 0);
p2.setFill(null);
p2.setStroke(Color.BLACK);
p2.getStrokeDashArray().addAll(15.0, 5.0, 5.0, 5.0);
p2.setStrokeDashOffset(20.0);

使用 CSS 设计形状样式

所有形状都没有默认的样式类名。如果您想使用 CSS 将样式应用于形状,您需要向它们添加样式类名。所有形状都可以使用下列 CSS 属性:

  • -fx-fill

  • -fx-smooth

  • -fx-stroke

  • -fx-stroke-type

  • -fx-stroke-dash-array

  • -fx-stroke-dash-offset

  • -fx-stroke-line-cap

  • -fx-stroke-line-join

  • -fx-stroke-miter-limit

  • -fx-stroke-width

所有 CSS 属性都对应于Shape类中的属性,我在上一节已经详细讨论过了。Rectangle支持两个额外的 CSS 属性来指定圆角矩形的弧宽和高度:

  • -fx-arc-height

  • -fx-arc-width

以下代码片段创建了一个Rectangle,并添加了矩形作为其样式类名:

Rectangle r1 = new Rectangle(200, 50);
r1.getStyleClass().add("rectangle");

下面的样式将产生一个如图 14-38 所示的矩形:

img/336502_2_En_14_Fig38_HTML.jpg

图 14-38

将 CSS 样式应用于矩形

.rectangle {
        -fx-fill: lightgray;
        -fx-stroke: black;
        -fx-stroke-width: 4;
        -fx-stroke-dash-array: 15 5 5 10;
        -fx-stroke-dash-offset: 20;
        -fx-stroke-line-cap: round;
        -fx-stroke-line-join: bevel;
}

摘要

任何能在二维平面上画出的形状都叫做 2D 形状。JavaFX 提供了各种节点来绘制不同类型的形状(线条、圆形、矩形等)。).您可以将形状添加到场景图。所有形状类都在javafx.scene.shape包中。代表 2D 形状的类继承自抽象的Shape类。形状可以有定义形状轮廓的笔划。形状可以有填充。

Line类的一个实例代表一个线节点。一辆Line没有内饰。默认情况下,它的fill属性设置为null。设置fill没有效果。默认strokeColor.BLACK,默认strokeWidth为 1.0。

Rectangle类的一个实例表示一个矩形节点。该类使用六个属性来定义矩形:xywidthheightarcWidtharcHeightxy属性是矩形左上角在节点局部坐标系中的 x 和 y 坐标。widthheight属性分别是矩形的宽度和高度。指定相同的宽度和高度来绘制正方形。默认情况下,矩形的角是尖锐的。通过指定arcWidtharcHeight属性,矩形可以有圆角。

Circle类的一个实例代表一个圆形节点。该类使用三个属性来定义圆:centerXcenterYradiuscenterXcenterY属性是圆心在节点本地坐标系中的 x 和 y 坐标。radius属性是圆的半径。这些属性的默认值为零。

Ellipse类的一个实例表示一个椭圆节点。该类使用四个属性来定义椭圆:centerXcenterYradiusXradiusYcenterXcenterY属性是圆心在节点的局部坐标系中的 x 和 y 坐标。radiusXradiusY是椭圆在水平和垂直方向上的半径。这些属性的默认值为零。当radiusXradiusY相同时,圆是椭圆的特例。

Polygon类的一个实例代表一个多边形节点。该类不定义任何公共属性。它允许您使用定义多边形顶点的(x,y)坐标数组来绘制多边形。使用Polygon类,你可以绘制任何类型的几何形状,这些几何形状是使用连接线创建的(三角形、五边形、六边形、平行四边形等)。).

折线类似于多边形,只是它不在最后一点和第一点之间绘制直线。也就是说,折线是一个开放的多边形。然而,fill颜色用于填充整个形状,就好像该形状是闭合的一样。Polyline类的一个实例代表一个折线节点。

Arc类的一个实例代表一个椭圆的一部分。该类使用七个属性来定义椭圆:centerXcenterYradiusXradiusYstartAnglelengthtype。前四个属性定义了一个椭圆。最后三个属性定义了椭圆的一个扇区,即Arc节点。startAngle属性指定从 x 轴正方向逆时针测量的截面起始角度,单位为度。它定义了弧的起点。length是一个角度,以度为单位,从开始角度逆时针测量,以定义扇形的结束。如果length属性设置为 360 度,则Arc是一个完整的椭圆。

贝塞尔曲线在计算机图形学中用于绘制平滑曲线。QuadCurve类的一个实例表示使用指定的贝塞尔控制点与两个指定点相交的二次贝塞尔曲线段。

CubicCurve类的一个实例使用两个指定的贝塞尔控制点表示与两个指定点相交的三次贝塞尔曲线段。

您可以使用Path类绘制复杂的形状。Path类的一个实例定义了形状的路径(轮廓)。路径由一个或多个子路径组成。子路径由一个或多个路径元素组成。每个子路径都有一个起点和一个终点。路径元素是PathElement抽象类的一个实例。PathElement类的几个子类代表特定类型的路径元素;这些级别是MoveToLineToHLineToVLineToArcToQuadCurveToCubicCurveToClosePath

JavaFX 部分支持 SVG 规范。SVGPath类的一个实例从编码字符串中的路径数据绘制一个形状。

JavaFX 允许您通过组合多个形状来创建一个形状。Shape类提供了三个名为union()intersect()subtract()的静态方法,允许您对作为参数传递给这些方法的两个形状执行并集、交集和差集操作。这些方法返回一个新的Shape实例。它们对输入形状的区域进行操作。如果形状没有填充和描边,则其面积为零。新形状有一个描边和一个填充。union()方法组合两个形状的面积。intersect()方法使用形状之间的公共区域来创建新的形状。subtract()方法通过从第一个形状中减去指定的第二个形状来创建新的形状。

描边是画出形状轮廓的过程。有时,形状的轮廓也称为笔画。Shape类包含几个属性,如strokestrokeWidth等,用于定义形状笔画的外观。

JavaFX 允许你用 CSS 设计 2D 形状。

下一章将讨论如何处理文本绘制。