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。open
和 close
闭包分别执行读取和写入操作。
为了有趣一点,并且只稍微复杂一点,我们在应用程序中添加了一些更漂亮、更友好的图片。JavaFX 的默认表情符号字体渲染在某些平台上有点草率,而且使用漂亮的彩色图片并没有多少工作量。这是通过使用 https://github.com/pavlobu/emoji-text-flow-javafx 中的图标来实现的。应用程序在没有它们的情况下也能完美运行(以及大约 20 行用于 cellFactory
和 cellValueFactory
定义的代码可以省略),但使用更友好的图片会更漂亮。我们把它们缩小到原来的 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 的声明式构建器风格。
以下是应用程序的使用示例:
更多信息
此应用程序的代码可以在以下位置找到
这是一个 Groovy 3 和 JDK 8 应用程序,但如果您想查看使用最新的 Groovy 和 JDK 版本从 CSV 文件中反序列化类和记录(以及 Groovy 的模拟记录)的示例,请参阅这篇 博客文章。