为TornadoFx的封装的常用控件与工具,基于JFoenix,借鉴KFoenix
这个开源库原本我也不想开源出来的,毕竟花费了自己很久的时间,但是想到TornadoFx国内实在没有多少人用,便是想开源出来了,国内TornadoFx资料较少,有些组件并没有,只能靠自己来造。
在这个前端为主的时代,可能只有我这种人还在独自坚持研究这种小众的技术吧(至少国内是这样的情况)
希望你在使用之前可以根据自己的实际情况给予打赏,算是对我的鼓励,你的打赏是我后期维护并积极更新的动力!谢谢
TornadoFX交流群:1071184701
TornadoFx中文文档 目前还在翻译中
本库包含了之前的IconText和DialogBuilder
-
IconText 5000+个Material Design字体图标库
-
DialogBuilder 基于Jfoenix的对话框生成器
最新版本1.7
PS:2.0版本进行了包名结构优化,出现报错,请重新导包即可,且2.0移除了
IconText
图标组件库,使用了RemixIcon
图标库
- CommonControl
1. 添加仓库
由于jar包是上传在jitpack仓库中,所以得在项目的pom.xml添加仓库
<repositories>
<repository>
<id>jitpack.io</id>
<url>https://jitpack.io</url>
</repository>
</repositories>
2.添加依赖
<dependency>
<groupId>com.github.Stars-One</groupId>
<artifactId>common-controls</artifactId>
<version>1.6</version>
</dependency>
allprojects {
repositories {
...
maven { url 'https://jitpack.io' }
}
}
dependencies {
implementation 'com.github.Stars-One:common-controls:1.6'
}
注意:考虑到查找的方便,1.6版本之后控件的名字发生了变化,控件名多个前缀x,且采用了驼峰命名法,如jfxbutton
变为了xJfxButton
**,其他依次类推
控件主要分为以下几个大类:
- 对话框
- 检测更新
- 常用方法
- 常用控件
- 下载框架(另有独立版,不过暂未发布,敬请期待)
对话框提供了整合了之前的DiglogBuilder,并新增加了加载对话框和自定义对话框内容,参考了Kf项目,我把有些对话框整合成了Kotlin中的DSL方式调用,有些对话框就没有
jfxbutton("测试消息") {
action {
jfxdialog(currentStage, "标题", "内容")
}
}
jfxbutton("测试确认对话框") {
action {
DialogBuilder(currentStage)
.setTitle(title)
.setMessage("hello")
.setNegativeBtn("取消"){ println("点击了取消按钮")}
.setPositiveBtn("确定") { println("点击了确定按钮")}
.create()
}
}
PS:提供了改变颜色,在对应的setBtn方法添加颜色即可,颜色支持十六进制和文字,如
jfxbutton("测试确认对话框") {
action {
DialogBuilder(currentStage)
.setTitle(title)
.setMessage("hello")
.setNegativeBtn("取消","red"){ println("点击了取消按钮")}
.setPositiveBtn("确定","#fafafa") { println("点击了确定按钮")}
.create()
}
}
弹出对话框,让用户输入内容
jfxbutton("测试输入对话框") {
action {
//text即为用户输入的内容
jfxdialog(currentStage, "带输入框的对话框", "输入整数内容", { text -> println(text) })
}
}
此对话框用来提示对应的网址或者是文件的位置,用户点击之后可以跳转浏览器或者是打开资源管理器并定位到文件
jfxbutton("测试输出对话框") {
action {
//文件
jfxdialog(currentStage, "带链接的输入框", "输出目录为","D:\\text.txt"
,false)
//网址
jfxdialog(currentStage, "带链接的输入框", "输出目录为","stars-one.site"
,true)
}
}
jfxbutton("下载"){
action{
DownloadDialogView(currentStage,"下载地址","D:\\test.txt")
.show()
}
}
PS:第三个参数路径可不填,则自动截取下载地址的末尾的作为文件名,并下载到与jar的同目录文件中
jfxbutton("测试加载对话框") {
action {
loadingDialog(currentStage,"标题","内容"){alert ->
runAsync {
//这里做网络请求或者其他耗时的操作
for (i in 0..3) {
Thread.sleep(200)
println(i)
}
} ui{
//调用close或者hideWithAnimation关闭对话框
alert.hideWithAnimation()
}
}
}
}
jfxbutton("关闭程序对话框") {
action {
stopDialog(currentStage,"标题","点击确定结束当前程序")
}
}
在右下角弹窗,且过3s后自动消失(3s是默认数值),目前还没有实现类似队列那种弹窗,新的弹窗会将之前的覆盖掉
button {
action {
showDialogPopup(currentStage, "提示", "复制成功",3.0)
}
}
效果如下:
button("单选按钮对话框") {
action {
showDialogRadio(currentStage,"选择选项", listOf("txt","md")){label, index ->
println("选中下标$index 内容:$label")
}
}
}
这里单独抽出来讲解使用,详见第2部分
若是你不满足已有的对话框,你可以按照你的喜欢,自定义对话框的内容
//这里你可以自定义布局
val content = HBox(Text("自定义内容"))
val alert = DialogBuilder(currentStage)
.setCustomContext(content)
.setNegativeBtn("确定")
.create()
XMessage
版本2.1.1添加的组件
使用:
class TestView : View("My View") {
var message by singleAssign<XMessage>()
override val root = vbox {
setPrefSize(700.0, 400.0)
button("测试1") {
action{
message.create("HELLO", AlertLevel.DANGER)
}
}
button("测试2") {
action{
message.create("HELLO222", AlertLevel.DANGER)
}
}
//这个必须放在最后
message = XMessage.bindingContainer(this)
}
}
默认用法是要提供一个顶级的根布局,其必须是StackPane,但目前我改造了下,可以使用任意Pane,但是需要把初始化的方法放在最后面
PS:
bindingContainer()
绑定的布局一定不要涉及节点的变换操作(如添加,删除等),会导致样式出问题!
基于上面的对话框,实现了自动检测更新的框架
我这里是使用了博客园和蓝奏云作为更新的平台,博客园用来发布新版本的信息,蓝奏云则用来上传更新文件
使用之前,你需要在博客园发布一篇随笔,并以下面的表格形式,这里给出对应的md格式
|版本名|版本号 |更新时间 |更新内容 |更新地址|
|-- |-- |-- |-- |-- |
|v1.1 |2 |2020-7-10 |1.更新xx\n2.更新xx | |
|v1.0 |1 |2020-7-10 |1.更新xx\n2.更新xx | |
更新内容一栏中,通过\n
来实现换行
更新地址,你可以使用蓝奏云的地址或者是其他的地址,如果使用蓝奏云的话,一定要是整个文件夹的地址
更新版本时,你需要将博客园上随笔的内容进行更新,并在新版本的信息放在表格第一行,并保证版本号比之前的要高,否则框架无法检测到新版本的升级(使用蓝奏云的话,则在之前的文件夹上传文件即可,框架会自动下载第一个的文件,即为最新版本的文件)
jfxbutton("检测更新") {
action {
//最重要的是版本号,记得是int类型的,版本名无关紧要,只是让之后的升级提示对话框显示可以好看点
TornadoFxUtil.checkVersion(currentStage, "https://www.cnblogs.com/stars-one/p/13284015.html", "当前版本号", "当前版本名")
}
}
位于TornadoFxUtils.kt文件中
- 下载文件
downloadFile
- 下载图片
downloadImage
- 复制文字到粘贴板
copyTextToClipboard
- 自动补全网址
completeUrl
- 检测版本更新
checkVersion
- 打开网址
openUrl
- 打开文件所在文件夹
openFileDir
- 打开文件(使用默认关联应用程序)
openFile
- 获取当前显示器的宽度
getScreenWidth
- 获取当前显示器的高度
getScreenHeight
- 处理文件名(替换掉文件名中非法的字符)
handleFileName
- 打开指定jar文件
openApp
- 重启当前jar文件
restartApp
- 获取当前jar文件的根目录
getCurrentJarPath
- 判断是否是window平台
isWin
- 判断是否是Linux平台
isLinux
- 判断是否是Mac平台
isMac
- 获取当前设备主板SN序列号
getDeviceSn
- 执行cmd命令(支持window,linux和macOs)
execCmd
- 设置页面快捷键
addShortcut
以下的控件其实本质上都是一个方法,使用了TornadoFx内置的DSL语法进行书写,使用的时候和TornadoFx编写布局的代码是一样的
控件 | 控件名称 | 例子 |
---|---|---|
xUrlLink | 可选择的超链接文本 | urlLink("博客地址","stars-one.site") |
xImageView | 生成指定宽高的图片,正方形的图片可省略高度参数 | imageview("xx.jpg",50,50) |
xIconItem | 生成带图标的右键菜单(ContextMenu),每个控件都有setContextMenu方法 | |
xSelectText | 可选择的文本框 | selectText("内容") |
xFfxButton | 指定宽高的扁平按钮,正方形可省略高度参数 | jfxbutton("xx.jpg",50,50) |
xCircleJfxbutton | 圆形图标扁平按钮(鼠标滑过会有阴影),传递一个node参数 | circlejfxbutton(imageview("xx.jpg",50)) |
xChooseFile | 文件输入+选择 | xChooseFile(viewModel.mdFilePath, "md,markdown", "markdown文件") |
xChooseFileDirectory | 文件夹输入+选择 | xChooseFileDirectory("下载目录", dirPath) |
xIconButton | 圆形按钮 | xIconButton(xImageView("/x5.jpg",30)) |
showToast | 显示Toast | |
remixIconLabel | 显示字体图标,点击查看使用说明 | remixIconLabel("home-4-fill", c("red"),29) |
remixIconText | 显示字体图标,点击查看使用说明 | remixIconText("home-4-fill", c("red"),29) |
remixIconButton | 圆形图标按钮 | remixIconButton("home-4-fill") |
remixIconButtonWithBorder | 圆形图标按钮(带边框) | remixIconButtonWithBorder("home-4-fill") |
xSwitch | 开关组件 | xSwitch("功能开关") |
xCircleImageView | 圆形头像框 | xCircleImageView("/my.jpg") |
xTag | 标签 | xTag("免费") |
xCountDownBtn | 倒计时按钮 | xCountDownBtn("发送验证码", 90, {}) |
xHighLightTextFlow | 关键字高亮文本textflow | xHighLightTextFlow("我的555", "的") |
对应的HttpDownloader类
构造方法需要传入一个下载地址和保存文件路径,之后调用startDownload
方法即可下载
HttpDownloader(downloadUrl, file).startDownload(object : HttpDownloader.OnDownloading {
override fun onProgress(progress: Double, percent: Int, speed: String) {
//回调的几个参数,progress是进度,percent是百分比,speed是下载速度
//你可以使用在下载之前将相关得到控件绑定一个观察者,之后将这里的相关参数设置即可
}
override fun onFinish() {
}
override fun onError(e: IOException) {
}
})
仿造Android中的RecyclerView用法写的一个控件,主要实现将一个List数据以列表的方式显示出来
旧版本(及命名为FxRecyclerView)使用方法详见Tornadofx控件库(2)——FxRecyclerView | Stars-One的杂货小窝
XRecyclerView为使用MVVM模式重构的版本,使用起来更为的方便
这个没啥好说的,就是一个存数据的bean类,如一个Person
类,根据自己的情况与需求创建
data class Person(var name: String,var age:String)
这个就是列表中的每一项View,需要继承tornadofx中的View,我这里就是显示Person的name和age属性,比较简单演示一下
PS:我自己封装了一个抽象类ItemViewBase,可以直接实现此类即可
class MyItemView : ItemViewBase<Person, MyItemView>(null,null){
var nameText by singleAssign<TextField>()
var ageText by singleAssign<TextField>()
/**
* 设置输入的改变,当用户输入内容时候,同时改变obList中的beanList数据
*
*/
override fun inputChange() {
val name = nameText.text
val age = ageText.text
val copy = bean.copy()
copy.name = name
copy.age = age
obList.update(bean, copy)
}
/**
* 初次设置数值,直接设置相关控件对象的数值
*
* @param beanT
*/
override fun bindData(beanT: Person) {
nameText.text = beanT.name
ageText.text = beanT.age
}
override val root = hbox {
nameText = textfield() {
}
ageText = textfield {
}
//这里必须要调用此方法,将当前所有输入控件传入,进行设置数值变化监听
setDataChange(nameText,ageText)
}
}
RvDataObservableList里面包含了beanList和itemViewList,后续只需要调用其相关的方法即可实现对列表数据进行的增删改查,且会同步itemVIew进行修改(即界面会同步根据数据改变)
PS:建议把此类RvDataObservableList声明为全局变量
//Person是bean的实体类,MyItemView则是itemview的类
val dataList = RvDataObservableList<Person,MyItemView>()
val adapter = object : RvAdapter<Person, MyItemView>(dataList) {
override fun onBindData(itemView: MyItemView, bean: Person, position: Int) {
//这里bindData是ItemViewBase中定义的方法
itemView.bindData(dataList,position)
}
//itemView的单击事件
override fun onClick(itemView: MyItemView, position: Int) {
println("单击$position")
}
//itemView的右击事件
override fun onRightClick(itemView: MyItemView, position: Int) {
println("右击$position")
}
override fun onCreateView(): MyItemView {
return MyItemView()
}
}
class RvTestView : View("My View") {
val dataList = RvDataObservableList<Person,MyItemView>()
override val root = vbox {
setPrefSize(1000.0, 400.0)
val adapter = object : RvAdapter<Person, MyItemView>(dataList) {
override fun onBindData(itemView: MyItemView, bean: Person, position: Int) {
itemView.bindData(dataList,position)
}
override fun onClick(itemView: MyItemView, position: Int) {
println("单击$position")
}
override fun onRightClick(itemView: MyItemView, position: Int) {
println("右击$position")
}
override fun onCreateView(): MyItemView {
return MyItemView()
}
}
this+=XRecyclerView<Person, MyItemView>().setRvAdapter(adapter)
}
}
XRecyclerView:
方法名 | 参数说明 | 方法说明 |
---|---|---|
setWidth(double) | double类型的数值 | 设置宽度 |
setHegiht(double) | double类型的数值 | 设置高度 |
setIsShowHorizontalBar(String) | 显示方式,never(不显示) always(一直显示) asneed(自动根据需要显示) | 设置是否显示水平滚动条 |
RvDataObservableList:
注意:下面涉及到对beanList的修改,都会同步对界面进行修改
方法名 | 参数说明 | 方法说明 |
---|---|---|
getItemView(bean: beanT) | bean对象 | 根据bean对象获取itemView |
getItemView(index: Int) | double类型的数值 | 设置高度 |
add(beanT) | 添加一个数据 | |
add(index,beanT) | 指定坐标插入新数据 | |
addAll(list: List) | 添加多个新数据 | |
addAll(vararg bean: beanT) | 添加多个新数据 | |
update(index: Int, bean: beanT) | 更新指定坐标数据 | |
update(bean: beanT,newBean:beanT) | 把某个对象更新为新对象newBean | |
remove(bean: beanT) | 指定坐标插入新数据 | |
remove(index: Int) | 移除指定下标 | |
remove(vararg indexList:Int) | 移除指定的多个下标 | |
remove(indexList: List) | 移除指定的多个下标 | |
fun remove(from: Int, to: Int) | 移除连续的多个指定下标( 区间为[from,to) ) | |
removeLast() | 移除最后一个数据 | |
clear | 清除所有数据 |
在多数情况下,软件会有设置项的存储,需要本地存储下用户的设置
这里便是自己琢磨了下,写了个配置存储的功能(GlobalDataConfig
类),但此类并不是能直接使用,所以下面介绍下使用方法
下面以一个开关设置来讲解使用
class Constants {
companion object {
const val FLAG_OPEN_OPTION = "flag_open_option"
}
}
class GlobalData {
companion object {
//某项功能开关
val openFlag = GlobalDataConfig(Constants.FLAG_OPEN_OPTION, false)
}
}
这里说明下,GlobalDataConfig
的构造函数为key和defaultValue,之后会以键值对的形式存储在本地的一个properties文件中
defaultValue
即默认值,之后有个方法,主要是初始化会调用(从本地文件读取数值,若是没有数值,则取默认数值)
注意:目前框架只支持4种类型,
string
,double
,int
,boolean
经过上面的步骤,现在我们就可以使用GlobalData.openFlag
来进行使用了,这里补充说明下GlobalDataConfig
中含有的方法
//当前数值
GlobalData.openFlag.currentValue
//更改数值(更改会自动同步更新本地配置文件里的保存的数值)
GlobalData.openFlag.setValue()
//恢复默认值
GlobalData.openFlag.resetValue()
对于TornadoFx应用,一般推荐我们将TextField与ViewModel中的属性进行绑定,这里也是新增了工具类去快速实现使用
有四个类型的方法,各自获取不同类型的Property属性
- GlobalDataConfigUtil.getSimpleBooleanProperty
- GlobalDataConfigUtil.getSimpleStringProperty
- GlobalDataConfigUtil.getSimpleDoubleProperty
- GlobalDataConfigUtil.getSimpleIntegerProperty
具体的使用示例代码如下(包含四种类型的使用)
点击展开代码
package com.starsone.controls.test
import com.starsone.controls.utils.GlobalDataConfig
import com.starsone.controls.utils.GlobalDataConfigUtil
import tornadofx.*
class GlobalDataTestView : View("My View") {
val viewModel by inject<GlobalDataTestViewModel>()
override val root = vbox {
prefWidth = 800.0
prefHeight = 500.0
checkbox("开启某功能", viewModel.booleanFlag)
hbox {
text("string绑定")
textfield(viewModel.stringFlag)
}
hbox{
text("int绑定")
//注意,这里如果数字数值过大,会出现逗号
textfield(viewModel.intFlag)
}
hbox{
text("double绑定")
textfield(viewModel.doubleFlag)
}
}
}
class GlobalDataTestViewModel : ViewModel() {
val booleanFlag = GlobalDataConfigUtil.getSimpleBooleanProperty(GlobalData.booleanFlag)
val stringFlag = GlobalDataConfigUtil.getSimpleStringProperty(GlobalData.stringFlag)
val doubleFlag = GlobalDataConfigUtil.getSimpleDoubleProperty(GlobalData.doubleFlag)
val intFlag = GlobalDataConfigUtil.getSimpleIntegerProperty(GlobalData.intFlag)
}
class Constants {
companion object {
const val BOOLEAN_FLAG = "Boolean_FLAG"
const val STRING_FLAG = "String_FLAG"
const val INT_FLAG = "Int_FLAG"
const val DOUBLE_FLAG = "Double_FLAG"
}
}
/**
* 主要是仿写不可变的常量
*/
class GlobalData {
companion object {
val booleanFlag = GlobalDataConfig(Constants.BOOLEAN_FLAG, false)
val doubleFlag = GlobalDataConfig(Constants.DOUBLE_FLAG, 0.0)
val stringFlag = GlobalDataConfig(Constants.STRING_FLAG, "")
val intFlag = GlobalDataConfig(Constants.INT_FLAG, 1)
}
}
此功能仅在window平台上可用!!!
使用:
val monitor = SystemClipboardMonitor()
monitor.addClipboardListener(object :GlobalClipBoardListener{
override fun onCopy(text: String?, clipboard: Clipboard?, contents: Transferable?) {
//这里可以加上相关的判断来测试内容是否是符合自己的定义的条件才触发对应的操作
println("已监听到方法...")
println(text)
}
})
考虑到会有设置的选项,就定义了两个开关方法,可以在需要的时候进行开关的设置(默认是剪切板的监听就是开启的)
//开启监听
monitor.stopListen()
//停止监听
monitor.startListen()
不过测试的时候,发现还是会有些小问题...
此组件是2.0.2以上版本新增的组件,库增加500KB左右的大小吧,应该还能接受,有2000+个图标
效果:
使用步骤:
hbox{
//1.先进行初始化
RemixIconData.init(resources)
//2.使用,传入图标名称即可
remixIconLabel("home-4-fill", c("red"),29)
remixIconText("home-4-fill", c("green"),20)
}
PS: 从
2.0.9
版本开始,可不用手动调用RemixIconData.init()
进行初始化!!
resources
是TornadoFx提供的一个内置对象
上述我初始化是直接在页面初始化,实际上,推荐在App里进行初始化更好,如下:
- remixIconLabel() 本质上是Label组件
- remixIconText 本质上是Text组件
可以通过Remix Icon - Open source icon library来找到对应的图标名称,目前使用的Remix Icon版本为3.2.0
点开你需要用的图标即可看到名称,如下图所示
位于ViewExtend.kt文件中
Button.setActionHank()
按钮防抖actionButton.addShortcut
按钮设置快捷键Pane.setMargin()
pane设置margin
位于ExtendFun.kt文件中
Long.toDateString
long类型转时间字符串Long.toUnitString
字节转为对应的单位Long.toUnitStringNew
字节转为对应的单位Long.toUnitStringNewWithUnit
字节转为对应的单位,得到Pair类型结果的数值和单位分开Long.fillZero
前置补0Int.fillZero
前置补0Date.toDateString
date类型转时间字符串Double.toFix
double保留几位小数String.parseJsonToList
json字符串转List对象String.parseJsonToObject
json字符串转Object对象
代码位于ShortCutUtils中
createShortCut()
创建快捷方式setAppStartup()
设置快捷方式为开机启动
目前支持get post请求,可传json,且支持打印输出网络请求日志
//开启网络请求日志输出
NetworkUtil.isDebug = true
//get请求示例
val url = "http://127.0.0.1:8099/userlogin"
val data = hashMapOf("username" to "hello")
//这里可以指定的实体类,可方便之后将数据转为实体类对象
// 我这里是传了String,就是不进行实体类转换处理,直接返回json数据
NetworkUtil.get(url, data, hashMapOf(), object : NetworkUtil.RespCallBack<String>() {
override fun onResponse(call: Call, data: String) {
//数据
}
override fun onFailure(call: Call, e: Exception) {
e.printStackTrace()
}
})
QRCodeUtil
生成二维码的工具,支持创建带logo,底部文本的二维码
比较核心的就两个方法,如下面代码所示,其他的方法是带Swing关键字,就是生成Swing包中的Image对象
getQRcodeFxImg()
方法就是直接生成Fx的Image对象,可以JavaFx中直接使用
/**
* 初始化设置
*
* @param qrcodeSize 二维码尺寸,默认为320(即320*320)
* @param logoSize logo图标尺寸,默认为80(即80*80)
* @param bottomTextSize 底部文字大小,默认20px
* @param qrcodeType 二维码图片格式,默认为png
*/
fun initConfig(qrcodeSize: Int = 320, logoSize: Int = 80, bottomTextSize: Int = 20, qrcodeType: String = "PNG")
/**
* 生成二维码图片
*
* @param data 二维码文本内容
* @param logoPath 图标图片的路径
* @param bottomText 底部文字
* @return fx的img对象
*/
fun getQRcodeFxImg(data: String?, logoPath: String?=null, bottomText: String?=null): WritableImage
//得到的swing的image对象
val buImg = QRCodeUtil.getQRcodeFxImg("这是测试文本")
val buImg1 = QRCodeUtil.getQRcodeFxImg("这是测试文本", null, "底部文字")
val buImg2 = QRCodeUtil.getQRcodeFxImg("这是测试文本", "/x5.jpg", "底部文字")
val list = listOf(buImg, buImg1, buImg2)
hbox(20.0) {
list.forEach {
imageview(it) {
fitWidth = 200.0
fitHeight = 200.0
}
}
}