# LerverUI
**Repository Path**: lifay/LerverUI
## Basic Information
- **Project Name**: LerverUI
- **Description**: kotlin + javaFX自定义UI框架和组件
- **Primary Language**: Kotlin
- **License**: Apache-2.0
- **Default Branch**: master
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 1
- **Forks**: 1
- **Created**: 2022-10-09
- **Last Updated**: 2025-04-30
## Categories & Tags
**Categories**: Uncategorized
**Tags**: Javafx, Kotlin
## README
[TOC]
## LerverUI简介
LerverUI是一个基于javaFX框架封装了部分UI组件、常用操作的框架,语言采用 [Kotlin](https://book.kotlincn.net/text/getting-started.html)。
---
部分功能(模态窗口提示、弹出通知、样式)引用和借鉴了[easyfx](https://github.com/xizi110/easyfx)。
JavaFX的样式UI组件引用了[atlantafx](https://github.com/mkpaz/atlantafx)。
---
## 模块引入
```
cn.lifay.LerverUI
Core
1.54
lifay-public
http://www.lifay.cn:8081/repository/maven-public/
```
## 下载源码编译
```
1. 需要jdk17和kotlin环境
2. Parent模块clean install
```
## 模块总览
### 启动应用方式(2选1)
#### 快速启动
```
class DemoApplication : BaseApplication() {
override fun start(primaryStage: Stage?) {
val fxmlLoader = FXMLLoader(DemoApplication::class.java.getResource("formTest.fxml"))
val load = fxmlLoader.load()
val scene = Scene(load)
primaryStage!!.title = "Hello World111"
primaryStage.scene = scene
primaryStage.setOnCloseRequest {
println("GlobeStartUp.launch close window...")
}
primaryStage.show()
}
}
```
---
#### DB方式启动(sqlite):脚本自动升级、升级信息窗口
```
//函数体内为程序初始界面
class DemoApplication : InitDbApplication() {
override fun addAppStage(): Stage {
val pane = VBox(42.0)
pane.children.add(Button("dasdsadasd"))
val stage = Stage()
stage.title = "首页"
stage.centerOnScreen()
stage.setOnCloseRequest {
println("CommonDbDemo close window...")
}
stage.scene = Scene(pane)
return stage
}
}
```
### 启动程序
```
fun main() {
Application.launch(DemoApplication::class.java)
}
```
### 配置管理
#### 应用配置
##### 配置文件路径
- 默认
`程序目录中的lerver.yml`
- 自定义配置文件路径
```
LerverConfig.SetConfigPath(LerverResource.USER_DIR + "xxx.yml")
//启动程序代码
...
```
##### 日志
- log.prefix 日志名称,默认:client 输出:client.log
- log.dir 日志目录路径,默认:LerverResource.USER_DIR + "logs"
##### 主题
- theme 主题名:默认白色 THEME_DARK-黑色
##### 数据库
继承InitDbApplication时指定
- dbName db文件名称
- isShowStage 升级界面后是否自动跳转到业务界面
```
//伪代码
InitDbApplication(
val dbName: String = "db.db",
val isShowStage: Boolean = true,
)
```
#### 业务配置
##### 自定义配置管理
```
//是否存在
ContainsKey(key: String): Boolean
//获取配置,配置默认值(可选)
ReadProperties("key", "defaultValue")
//按多层路径获取配置,配置默认值(可选)
ReadProperties("path.key")
//新增或更新配置
WriteProperties("key", "value")
```
##### 文件选择路径(内置)
记录文件、目录选择及打开等操作的目录信息
`COMMON_CHOOSER_PATH: 用于缓存打开文件选择框时的目录路径,默认key`
使用示例
```
//
val directoryChooser = DirectoryChooser().apply {
title = "选择输出目录"
initialDirectory = ChooserExtension.getInitFile("可空,也可自定义")
}
val directory = directoryChooser.showDialog(index.rootPane.scene.window)
//...业务代码
//更新路径
ChooserExtension.updateInitChooserPath("/path/...", "可空,也可自定义")
```
### 事件总线
#### 事件类型
##### 订阅
订阅注册:会被触发执行的匿名函数
DemoId: 创建继承BaseEventBusId的枚举,也可用字符串代替
```
//DefaultEvent为内置无参事件传输DTO
EventBus.subscribe(DemoId.RELOAD_UI, DefaultEvent::class) {
platformRun {
tableView.refresh()
}
}
```
##### 发布,订阅
#### 参数类型
##### 无参
```
//DefaultEvent为内置无参事件传输DTO
EventBus.subscribe(DemoId.RELOAD_UI, DefaultEvent::class) {
platformRun {
tableView.refresh()
}
}
//发布
EventBus.publish(DefaultEvent(DemoId.RELOAD_UI))
```
##### 单文本参数
```
//TextEvent为内置事件传输DTO,包含一个String类型参数
EventBus.subscribe(DemoId.CHAT, TextEvent::class) {
platformRun {
user1.appendText("${it.text}\n")
}
}
//发布
EventBus.publish(TextEvent(DemoId.CHAT, sendText.text))
```
##### 对象参数
```
//TextEvent为内置事件传输DTO,包含一个String类型参数
EventBus.subscribe>>(
DemoId.INFO
) {
it.body?.let {
val itemEventListBody = it as EventListBody
}
}
//发布
EventBus.publish(
BodyEvent(
DemoId.INFO,
EventListBody(hashCode(), datas.toList())
)
)
```
##### 改成同步执行(默认异步执行)
```
EventBus.publish(DefaultEvent(DemoId.RELOAD_UI),false)
```
### ui
#### 视图
##### 基础视图
- 新建一个CommonDemoView(控制器视图)继承BaseView
```kotlin
class CommonDemoView : BaseView() {
//注意var
@FXML
var rootPane = AnchorPane()
override fun rootPane(): AnchorPane {
return rootPane
}
}
```
- 创建控制器视图实例示例代码
```kotlin
//CommonDemoView映射demo.fxml
val view = BaseView.createView(XXXApplication::class.java.getResource("demo.fxml"))
val scene = Scene(view.ROOT_PANE)
primaryStage.title = "Hello World"
primaryStage.scene = scene
primaryStage.show()
```
##### 扩展视图
1. 普通表单
`BaseFormUI(这里选择直接实例化,也可创建一个类继承和实现BaseFormUI)`
```kotlin
val baseFormUI = BaseFormUI("测试基础表单") {
//设置默认填充内容
defaultEntity(UserData(11, "11", SelectTypeEnum.A, true, "男"))
//定义和添加元素
val nameElement =
TextElement("名称:", UserData::name, isTextArea = true, primary = false, initValue = "初始值") {
isDisable = false
isEditable = true
}
addElements(
TextElement("ID:", UserData::id, true),
nameElement,
SelectElement("类型:", UserData::type, SelectTypeEnum.values().toList()),
CheckElement("是否未成年:", UserData::child),
RadioElement("性别:", UserData::sex, listOf("男", "女")) {
//ui操作
}
)
//添加按钮和操作
addCustomButtons(
BaseButton(Button("测试").styleInfo()) {
showNotification("测试获取name内容:${nameElement.getElementValue()}")
},
clearBtn()
)
//表单初始化前操作
beforeFormInitCall {
showNotification("还没初始化")
}
//表单初始化后操作
afterFormInitCall {
showNotification("已经初始化完毕")
}
//窗口关闭操作
setOnCloseRequest {
println("窗口已关闭")
}
}
//展示窗口
baseFormUI.show()
```

2.定义数据表单(可扩展新增、编辑数据功能)
`DataFormUI(这里选择创建一个类继承和实现DataFormUI,也可直接实例化并实现)`
```kotlin
//注意变量需要可修改,定义为var
data class UserData(
var id: Int,
var name: String,
var type: SelectTypeEnum?,
var child: Boolean,
var sex: String,
)
class UserDataForm(t: UserData? = null, isUpdate: Boolean = false) :
DataFormUI(_isUpdate = isUpdate, buildFormUI = {
if (t != null) {
defaultEntity(t)
}
val id = TextElement("ID:", UserData::id, true)
id.fillValue = 666
val name = TextElement("名称:", UserData::name, isTextArea = true, primary = false, initValue = "初始值")
val type = SelectElement("类型:", UserData::type, SelectTypeEnum.values().toList())
val child = CheckElement("是否未成年:", UserData::child)
val sex = RadioElement("性别:", UserData::sex, listOf("男", "女", "中间"))
addElements(id, name, type, child, sex)
addCustomButtons(BaseButton(Button("测试自定义按钮").styleWarn()) {
println(it)
})
}) {
//保存操作
override fun saveDataFunc(entity: UserData): Boolean {
if (entity!!.name!!.isBlank()) {
throw LerverUIException("名称不能为空!")
}
UserDatas.add(entity)
return true
}
//更新操作
override fun updateDataFunc(entity: UserData): Boolean {
UserDatas.update(entity)
return true
}
}
```

3. 定义CURD增删查改界面(分页查询、新增、查看明细、修改、删除)
`CurdUI(这里选择创建一个类继承和实现CurdUI,也可直接实例化并实现)`
```kotlin
class UserManage : CurdUI("用户管理", buildElements = {
val id = TextElement("ID:", UserData::id, true)
val name = TextElement("名称:", UserData::name, isTextArea = true, primary = false, initValue = "初始值")
val type = SelectElement("类型:", UserData::type, SelectTypeEnum.values().toList())
val child = CheckElement("是否未成年:", UserData::child)
val sex = RadioElement("性别:", UserData::sex, listOf("男", "女", "中间"))
addElements(id, name, type, child, sex)
addCustomButtons(BaseButton>(Button("测试自定义按钮").styleWarn()) {
println(it)
})
}) {
//分页实现,可根据搜索关键字进行筛选,返回:1-数据总数量 2-根据页码和每页数量的查询逻辑
override fun dbPageInit(keyword: String): Pair, ((UserDatas) -> ColumnDeclaring)?> {
return Pair(DbManage.userDatas) {
it.name like DbManage.formatLikeKeyword(keyword)
}
}
//更新操作
override fun updateDataFunc(entity: UserData): Boolean {
return true
}
//保存操作
override fun saveDataFunc(entity: UserData): Boolean {
if (entity!!.name!!.isBlank()) {
throw LerverUIException("名称不能为空!")
}
return true
}
//删除操作
override fun delDataFunc(entity: UserData): Boolean {
return true
}
}
```

#### 控件加强
##### 树
##### 表格
### 日志组件
### 消息
#### 通知
#### 提示
- 常用弹出提示



```
alertInfo("信息打印")
alertWarn("警告打印")
alertError(
"错误打印","头部信息", "异常详细信息fun tableText(actionEvent: ActionEvent) {\n" +
" tableView.items[0].text = \"33333\"\n" +
" }\n"
)
```
### 扩展sdk方法
- 为窗体指定图标(图标默认路径:/icon.png,即将icon.png放在resources目录下,会自动引入)
```
//为stage指定图标(如果是通过框架创建的stage,一般已经默认设置了)
GlobalResource.loadIcon(stage)
//自定义图标路径
GlobalResource.setGlobalIconImage(imgPath)
```
- 主线程执行(简化Platform.runLater)
```
platformRun{
tudo()
}
```
- 校验参数
```
if(!checkParam("名称",name)){
return
}
```
- 异步协程执行
```
asyncTask {
//后台执行
}
```
- 异步延迟执行耗时操作 时间:毫秒
```
asyncDelayTask(500) {
platformRun { closeFunc() }
}
```
- 异步执行耗时操作,同时有加载图标提示
```
asyncTaskLoading(getWindow(), "保存中") {
try {
//后台操作
} catch (e: Exception) {
e.printStackTrace()
showErrMessage("保存失败:" + e.message)
} finally {
//结束操作
}
}
```
- 为Stage绑定快捷键:ESC关闭窗口
```
stage.bindEscKey()
```
- 快速复制文本到粘贴板
```
copyToClipboard("文字")
```
3.新建一个CommonDemoView(控制器视图)继承BaseView
```
class CommonDemoView : BaseView() {
@FXML
var rootPane = AnchorPane()
override fun rootPane(): AnchorPane {
return rootPane
}
}
```
4.创建控制器视图实例示例代码
```
val view = BaseView.createView(XXXApplication::class.java.getResource("demo.fxml"))
val scene = Scene(view.ROOT_PANE)
primaryStage.title = "Hello World"
primaryStage.scene = scene
primaryStage.show()
```
---
---
---
## 视图容器
> 视图=Controller,BaseView 内置封装了便捷功能


1.视图类定义(1)
```kotlin
//简单使用:定义测试View 继承BaseView 传入fxml资源
class BaseViewDemoView1 : BaseView(BaseViewDemoView1::class.java.getResource("baseView.fxml")) {
@FXML
private val rootPane = AnchorPane()
/**
* 注册根容器
*/
override fun rootPane(): AnchorPane {
return rootPane
}
}
//初始化
val baseView = BaseViewDemoView1()
primaryStage.title = "Hello World"
primaryStage.scene = Scene(baseView.ROOT_PANE)
primaryStage.show()
```
2.视图类定义(2)
```kotlin
//定义测试View 继承BaseView
class BaseViewDemoView2 : BaseView() {
//定义最顶级pane,注意rootPane不能为val
@FXML
var rootPane = AnchorPane()
//返回rootPane
override fun rootPane(): AnchorPane {
return this.rootPane
}
//可选
override fun initialize(p0: URL?, p1: ResourceBundle?) {
super.initialize(p0, p1)
}
//封装功能:
showNotification("右上角系统通知")
showMessage("应用提示")
}
//(1)外部实例化
val view = BaseView.createView(BaseViewDemo::class.java.getResource("baseViewDemo.fxml"))
val scene = Scene(view.getRoot())
primaryStage.title = "Hello World"
primaryStage.scene = scene
primaryStage.show()
//(2)BaseView内部实例化
val view = createView(BaseViewDemo::class.java.getResource("baseViewDemo.fxml"))
val scene = Scene(view.getRoot())
primaryStage.title = "Hello World"
primaryStage.scene = scene
primaryStage.show()
//(3)直接创建Stage窗口
val stage = createViewStage(
"窗口标题",
BaseViewDemo::class.java.getResource("baseViewDemo.fxml"),
closeFunc = {},
initFunc = {})
stage.show()
```
---
## 消息总线
> 消息总线一般是为了解决跨界面无耦合回调执行操作,并且可以多界面同时触发
```
使用方法:
1.为事件定义枚举类ID,实现EventBusId
enum class DemoId : EventBusId {
RELOAD_UI,
CHAT,
}
2.订阅注册:将来会被触发执行的匿名函数
//DefaultEvent为内置无参事件传输DTO
EventBus.subscribe(DemoId.RELOAD_UI, DefaultEvent::class) {
platformRun {
tableView.refresh()
}
}
//TextEvent为内置事件传输DTO,包含一个String类型参数
EventBus.subscribe(DemoId.CHAT, TextEvent::class) {
platformRun {
user1.appendText("${it.text}\n")
}
}
//其他的BodyEvent是传递实体参数
3.发布消息
EventBus.publish(DefaultEvent(DemoId.RELOAD_UI))
EventBus.publish(TextEvent(DemoId.CHAT, sendText.text))
```
## 表单视图
> 自动绑定实体类、组合表单元素和基础操作功能
1.定义普通表单 BaseFormUI(这里选择直接实例化,也可创建一个类继承和实现BaseFormUI)

```kotlin
val baseFormUI = BaseFormUI("测试基础表单") {
//设置默认填充内容
defaultEntity(UserData(11, "11", SelectTypeEnum.A, true, "男"))
//定义和添加元素
val nameElement =
TextElement("名称:", UserData::name, isTextArea = true, primary = false, initValue = "初始值") {
isDisable = false
isEditable = true
}
addElements(
TextElement("ID:", UserData::id, true),
nameElement,
SelectElement("类型:", UserData::type, SelectTypeEnum.values().toList()),
CheckElement("是否未成年:", UserData::child),
RadioElement("性别:", UserData::sex, listOf("男", "女")) {
//ui操作
}
)
//添加按钮和操作
addCustomButtons(
BaseButton(Button("测试").styleInfo()) {
showNotification("测试获取name内容:${nameElement.getElementValue()}")
},
clearBtn()
)
//表单初始化前操作
beforeFormInitCall {
showNotification("还没初始化")
}
//表单初始化后操作
afterFormInitCall {
showNotification("已经初始化完毕")
}
//窗口关闭操作
setOnCloseRequest {
println("窗口已关闭")
}
}
//展示窗口
baseFormUI.show()
```
2.定义数据表单 DataFormUI(这里选择创建一个类继承和实现DataFormUI,也可直接实例化并实现)

```kotlin
//注意变量需要可修改,定义为var
data class UserData(
var id: Int,
var name: String,
var type: SelectTypeEnum?,
var child: Boolean,
var sex: String,
)
class UserDataForm(t: UserData? = null, isUpdate: Boolean = false) :
DataFormUI(_isUpdate = isUpdate, buildFormUI = {
if (t != null) {
defaultEntity(t)
}
val id = TextElement("ID:", UserData::id, true)
id.fillValue = 666
val name = TextElement("名称:", UserData::name, isTextArea = true, primary = false, initValue = "初始值")
val type = SelectElement("类型:", UserData::type, SelectTypeEnum.values().toList())
val child = CheckElement("是否未成年:", UserData::child)
val sex = RadioElement("性别:", UserData::sex, listOf("男", "女", "中间"))
addElements(id, name, type, child, sex)
addCustomButtons(BaseButton(Button("测试自定义按钮").styleWarn()) {
println(it)
})
}) {
//保存操作
override fun saveDataFunc(entity: UserData): Boolean {
if (entity!!.name!!.isBlank()) {
throw LerverUIException("名称不能为空!")
}
UserDatas.add(entity)
return true
}
//更新操作
override fun updateDataFunc(entity: UserData): Boolean {
UserDatas.update(entity)
return true
}
}
```
3.定义CURD增删查改界面 CurdUI(这里选择创建一个类继承和实现CurdUI,也可直接实例化并实现)

```kotlin
class UserManage : CurdUI("用户管理", buildElements = {
val id = TextElement("ID:", UserData::id, true)
val name = TextElement("名称:", UserData::name, isTextArea = true, primary = false, initValue = "初始值")
val type = SelectElement("类型:", UserData::type, SelectTypeEnum.values().toList())
val child = CheckElement("是否未成年:", UserData::child)
val sex = RadioElement("性别:", UserData::sex, listOf("男", "女", "中间"))
addElements(id, name, type, child, sex)
addCustomButtons(BaseButton>(Button("测试自定义按钮").styleWarn()) {
println(it)
})
}) {
//分页实现,返回:1-数据总数量 2-根据页码和每页数量的查询逻辑
override fun pageDataFunc(pageIndex: Int, pageCount: Int): Pair> {
return Pair(
DbManage.userDatas.totalRecordsInAllPages, DbManage.userDatas.drop(pageIndex * pageCount)
.take(pageCount).toList()
)
}
//更新操作
override fun updateDataFunc(entity: UserData): Boolean {
return true
}
//保存操作
override fun saveDataFunc(entity: UserData): Boolean {
if (entity!!.name!!.isBlank()) {
throw LerverUIException("名称不能为空!")
}
return true
}
//删除操作
override fun delDataFunc(entity: UserData): Boolean {
return true
}
}
```
## 树视图
> 考虑到树有很多应用场景,业务过程中产生很多冗余代码
- 根据数据集自动构建树节点,也可一键动态刷新
- 动态添加
- 动态删除
- 动态修改
注册和定义数据源
```kotlin
//(分List数据、Tree数据两种数据源)
//Register-为treeView实例注册,示例中数据源为List模式,所以需要传入id和parentId的属性引用,函数体内是获取数据源的逻辑代码
treeView.apply {
root = rootTreeItem
isShowRoot = true
Register(TreeTestVO::id, TreeTestVO::parentId, true) {
listOf(test1, test2, test3)
}
}
//刷新树节点,并可选择添加过滤逻辑
treeView.RefreshTree(filterFunc = {
it.name.contains(s2)
})
//根据实体类的id查找所在TreeItem实例,并更新实体
val treeItem = treeView.GetItemByBusiId("add1")
treeItem?.UpdateItem(TreeTestVO("修改测试222", "5", "修改测试222", SimpleStringProperty("修改测试22")))
//为treeItem添加子元素
val selectedItem = treeView.selectionModel.selectedItem
selectedItem.AddChildren(
TreeTestVO(
"根节点下节点1",
"6",
"根节点下节点1",
SimpleStringProperty("根节点下节点1")
)
)
selectedItem.AddChildrenList(
listOf(
TreeTestVO(
"根节点下节点2",
"6",
"根节点下节点2",
SimpleStringProperty("根节点下节点2")
)
)
)
//删除当前TreeItem节点
rootTreeItem.children[0].DeleteThis()
//删除当前TreeItem的子节点
rootTreeItem.DeleteChildItem { it.id == "4" }
```
CheckBox树视图定义,cellFactory = CheckBoxTreeCell.forTreeView(),Register指定checkBox=true
```
@FXML
var treeTree = TreeView()
override fun initialize(p0: URL?, p1: ResourceBundle?) {
val rootTreeItem = CheckBoxTreeItem(
TreeNodeDTO("0", "-1", "根节点", "ss", ArrayList(), GloabaUtil.TYPE_ROOT, 0)
)
treeTree.apply {
root = rootTreeItem
Styles.toggleStyleClass(this, Styles.DENSE)
cellFactory = CheckBoxTreeCell.forTreeView()
Register(TreeNodeDTO::id, TreeNodeDTO::children, true, true) {
val treeNodeDTOs = tudo()
treeNodeDTOs ?: emptyList()
}
}
}
```