GroovyFX 探险

作者: Paul King
发布日期: 2022-12-12 02:22PM


这篇博客介绍了 GroovyFX 版本的 最初用 JavaFX 编写的 ToDo 应用程序。首先,我们从一个 ToDoCategory 枚举开始,它包含我们的 ToDo 类别

enum ToDoCategory {
    EXERCISE("🚴"),
    WORK("📊"),
    RELAX("🧘"),
    TV("📺"),
    READ("📚"),
    EVENT("🎭"),
    CODE("💻"),
    COFFEE("☕️"),
    EAT("🍽"),
    SHOP("🛒"),
    SLEEP("😴")

    final String emoji

    ToDoCategory(String emoji) {
        this.emoji = emoji
    }
}

我们将有一个 ToDoItem 类,它包含 todo 任务、前面提到的类别和截止日期。

@Canonical
@JsonIncludeProperties(['task', 'category', 'date'])
@FXBindable
class ToDoItem {
    final String task
    final ToDoCategory category
    final LocalDate date
}

它用 @JsonIncludeProperties 注释,以便轻松地将对象序列化/反序列化为 JSON 格式,以提供简单的持久化,以及 @FXBindable 注释,它消除了定义 JavaFX 属性所需的样板代码。

接下来,我们将定义一些辅助变量

var file = 'todolist.json' as File
var mapper = new ObjectMapper().registerModule(new JavaTimeModule())
var open = { mapper.readValue(it, new TypeReference<List<ToDoItem>>() {}) }
var init = file.exists() ? open(file) : []
var items = FXCollections.observableList(init)
var close = { mapper.writeValue(file, items) }
var table, task, category, date, images = [:]
var urls = ToDoCategory.values().collectEntries {
    [it, "emoji/${Integer.toHexString(it.emoji.codePointAt(0))}.png"]
}

在这里,mapper 使用 Jackson 库 将我们的顶级域对象(ToDo 列表)序列化和反序列化为 JSON。openclose 闭包分别执行读取和写入操作。

为了有趣一点,并且只稍微复杂一点,我们在应用程序中添加了一些更漂亮、更友好的图片。JavaFX 的默认表情符号字体渲染在某些平台上有点草率,而且使用漂亮的彩色图片并没有多少工作量。这是通过使用 https://github.com/pavlobu/emoji-text-flow-javafx 中的图标来实现的。应用程序在没有它们的情况下也能完美运行(以及大约 20 行用于 cellFactorycellValueFactory 定义的代码可以省略),但使用更友好的图片会更漂亮。我们把它们缩小到原来的 1/3,但如果我们愿意,当然可以把它们做得更大。

我们的应用程序将有一个组合框用于选择 ToDo 项的类别。我们将为组合框创建一个工厂,以便每个选择都是一个包含图形和文本组件的标签。

def graphicLabelFactory = {
    new ListCell<ToDoCategory>() {
        void updateItem(ToDoCategory cat, boolean empty) {
            super.updateItem(cat, empty)
            if (!empty) {
                graphic = new Label(cat.name()).tap {
                    graphic = new ImageView(images[cat])
                }
            }
        }
    }
}

在显示我们的 ToDo 列表时,我们将使用表格视图。因此,让我们为表格单元格创建一个工厂,它将使用漂亮的图片作为居中的图形。

def graphicCellFactory = {
    new TableCell<ToDoItem, ToDoItem>() {
        void updateItem(ToDoItem item, boolean empty) {
            graphic = empty ? null : new ImageView(images[item.category])
            alignment = Pos.CENTER
        }
    }
}

最后,在这些定义都到位后,我们可以定义我们的 GroovyFX 应用程序,用于操作我们的 ToDo 列表

start {
    stage(title: 'GroovyFX ToDo Demo', show: true, onCloseRequest: close) {
        urls.each { k, v -> images[k] = image(url: v, width: 24, height: 24) }
        scene {
            gridPane(hgap: 10, vgap: 10, padding: 20) {
                columnConstraints(minWidth: 80, halignment: 'right')
                columnConstraints(prefWidth: 250)

                label('Task:', row: 1, column: 0)
                task = textField(row: 1, column: 1, hgrow: 'always')

                label('Category:', row: 2, column: 0)
                category = comboBox(items: ToDoCategory.values().toList(),
                        cellFactory: graphicLabelFactory, row: 2, column: 1)

                label('Date:', row: 3, column: 0)
                date = datePicker(row: 3, column: 1)

                table = tableView(items: items, row: 4, columnSpan: REMAINING,
                        onMouseClicked: {
                            var item = items[table.selectionModel.selectedIndex.value]
                            task.text = item.task
                            category.value = item.category
                            date.value = item.date
                        }) {
                    tableColumn(property: 'task', text: 'Task', prefWidth: 200)
                    tableColumn(property: 'category', text: 'Category', prefWidth: 80,
                            cellValueFactory: { new ReadOnlyObjectWrapper(it.value) },
                            cellFactory: graphicCellFactory)
                    tableColumn(property: 'date', text: 'Date', prefWidth: 90, type: Date)
                }

                hbox(row: 5, columnSpan: REMAINING, alignment: CENTER, spacing: 10) {
                    button('Add', onAction: {
                        if (task.text && category.value && date.value) {
                            items << new ToDoItem(task.text, category.value, date.value)
                        }
                    })
                    button('Update', onAction: {
                        if (task.text && category.value && date.value &&
                                !table.selectionModel.empty) {
                            items[table.selectionModel.selectedIndex.value] =
                                    new ToDoItem(task.text, category.value, date.value)
                        }
                    })
                    button('Remove', onAction: {
                        if (!table.selectionModel.empty)
                            items.removeAt(table.selectionModel.selectedIndex.value)
                    })
                }
            }
        }
    }
}

我们可以通过将此应用程序的 GUI 部分放在 fxml 文件中,在一定程度上将应用程序逻辑和显示逻辑分离。然而,为了我们的目的,我们将把整个应用程序放在一个源文件中,并使用 Groovy 的声明式构建器风格。

以下是应用程序的使用示例: TodoScreenshot

更多信息

此应用程序的代码可以在以下位置找到

这是一个 Groovy 3 和 JDK 8 应用程序,但如果您想查看使用最新的 Groovy 和 JDK 版本从 CSV 文件中反序列化类和记录(以及 Groovy 的模拟记录)的示例,请参阅这篇 博客文章