我正在使用 JavaFX 开发一个 Kotlin 应用程序,并且我已经制作了“自定义 ListView”的包装器。操作大致如下:
ListView 显示应用程序启动时加载的客户端列表。在众多选项中,用户有两个相关的选项:启用和禁用所选客户端。禁用的客户端通过外部文件中的特殊 css 样式突出显示。
我遇到的问题是,当我禁用客户端时,CSS 样式不仅适用于该客户端,而且还适用于额外的空行之一,并且当我按下按钮重新启用相同的客户,我必须做两次。 (一次取消空行的样式,一次取消正确行的样式)。
该应用程序非常大,并从数据库中获取每个客户端的数据,但我简化了代码以在此处显示它。
测试应用程序.kt:
class TestApplication : Application() {
override fun start(stage: Stage) {
val fxmlLoader = FXMLLoader(TestApplication::class.java.getResource("test-view.fxml"))
val scene = Scene(fxmlLoader.load())
stage.title = "Test application"
stage.scene = scene
scene.stylesheets.add(this::class.java.getResource("custom.css").toExternalForm())
stage.show()
}
}
fun main() {
Application.launch(TestApplication::class.java)
}
文件 TestController.kt:
class TestController: Initializable {
private lateinit var objListViewWrapper: ListViewWrapper
// This MutableList is sent to a model (not included here) to update the customers database table:
private var listCustomers: MutableList<MutableMap<String, String>> = getCustomers()
@FXML
lateinit var lvCustomers: ListView<CustomerData>
override fun initialize(p0: URL?, p1: ResourceBundle?) {
objListViewWrapper = ListViewWrapper(lvCustomers, "id", "text")
generateListView()
}
private fun generateListView(){
objListViewWrapper.clearListView()
objListViewWrapper.populateListView(listCustomers)
}
private fun updateListViewContent(){
listCustomers.forEach { customer ->
val id = customer.getValue("id")
val active = customer.getValue("active").toInt()
lvCustomers.items.forEach { lvCustomer ->
if(lvCustomer.getId() == id){
lvCustomer.setActive(active)
}
}
}
objListViewWrapper.clearSelection()
}
private fun getCustomers(): MutableList<MutableMap<String, String>>{
var li: MutableList<MutableMap<String, String>> = mutableListOf()
val listNames = listOf(
"John Doe", "Jan Jansen",
"Osman Whitfield", "Jane Smith",
"Joe Bloggs", "Michalina Cottrell"
)
listNames.forEachIndexed { index, name ->
val mp: MutableMap<String, String> = mutableMapOf()
mp["id"] = (1 + index).toString()
mp["text"] = name.uppercase()
mp["active"] = "1"
li.add(mp)
}
return li
}
@FXML
fun disableCustomer(){
val selectedItem = objListViewWrapper.getSelectedItem()
if(selectedItem == null){
println("Error: You must select a customer.")
return
}
val idCustomer = selectedItem.getId()
listCustomers.forEach { mutableMap ->
if(idCustomer == mutableMap.getValue("id")){
mutableMap["active"] = "0"
}
}
updateListViewContent()
}
@FXML
fun enableCustomer(){
val selectedItem = objListViewWrapper.getSelectedItem()
if(selectedItem == null){
println("Error: You must select a customer.")
return
}
val idCustomer = selectedItem.getId()
listCustomers.forEach { mutableMap ->
if(idCustomer == mutableMap.getValue("id")){
mutableMap["active"] = "1"
}
}
updateListViewContent()
}
}
文件ListViewWrapper.kt:
class CustomerData(id: String, text: String, active: Int) {
var id: SimpleStringProperty = SimpleStringProperty(id)
var text: SimpleStringProperty = SimpleStringProperty(text)
var active: SimpleIntegerProperty = SimpleIntegerProperty(active)
fun getId(): String {
return this.id.get()
}
fun setId(id: String) {
this.id.set(id)
}
fun idProperty(): SimpleStringProperty {
return id
}
fun getText(): String {
return this.text.get()
}
fun setText(text: String) {
this.text.set(text)
}
fun textProperty(): SimpleStringProperty {
return text
}
fun getActive(): Int {
return this.active.get()
}
fun setActive(active: Int) {
this.active.set(active)
}
fun activeProperty(): SimpleIntegerProperty {
return active
}
}
class ListViewWrapper(listView: ListView<CustomerData>, idField: String = "id", textField: String = "text") {
private var listView = listView
private var idField = idField
private var textField = textField
private var obsList: ObservableList<CustomerData> = FXCollections.observableArrayList()
private val cssDisabledRow = "row-disabled"
init {
listView.setCellFactory { lv ->
object : ListCell<CustomerData?>() {
override fun updateItem(obj: CustomerData?, empty: Boolean) {
super.updateItem(obj, empty)
if (empty || obj == null) {
text = null
style = null
graphic = null
} else if(obj.getActive() == 0) {
text = obj.getText()
if(!styleClass.contains(cssDisabledRow)){
styleClass.add(cssDisabledRow)
}
} else {
text = obj.getText()
if(styleClass.contains(cssDisabledRow)){
styleClass.remove(cssDisabledRow)
}
}
}
}
}
}
fun clearListView(){
obsList.clear()
}
fun clearSelection(){
listView.selectionModel.clearSelection()
}
fun populateListView(li: MutableList<MutableMap<String, String>>){
if(li.isEmpty()){
println("The list of items is empty.")
return
}
li.forEach {
val id = it.getValue(idField)
val text = it.getValue(textField)
val active = it.getValue("active").toInt()
obsList.add(CustomerData(id, text, active))
}
listView.items.setAll(obsList)
}
fun getSelectedItem(): CustomerData? {
return listView.selectionModel.selectedItem
}
}
文件 test-view.fxml:
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.ListView?>
<?import javafx.scene.layout.AnchorPane?>
<?import javafx.scene.layout.HBox?>
<AnchorPane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="350.0" prefWidth="400.0" xmlns="http://javafx.com/javafx/16" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.example.testproject.TestController">
<padding>
<Insets bottom="4.0" left="4.0" right="4.0" top="4.0" />
</padding>
<children>
<ListView fx:id="lvCustomers" layoutX="48.0" layoutY="42.0" prefHeight="200.0" prefWidth="200.0" AnchorPane.bottomAnchor="36.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0" />
<HBox alignment="CENTER" layoutX="14.0" layoutY="250.0" prefHeight="32.0" spacing="4.0" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0">
<children>
<Button fx:id="btnDisable" mnemonicParsing="false" onAction="#disableCustomer" prefWidth="100.0" text="Disable" />
<Button fx:id="btnEnable" mnemonicParsing="false" onAction="#enableCustomer" prefWidth="100.0" text="Enable" />
</children>
</HBox>
</children>
</AnchorPane>
文件custom.css:
.list-cell:filled:hover {
-fx-background-color: #0093ff;
-fx-text-fill: white;
}
.row-disabled{
-fx-background-color: #c2c2c2 !important;
-fx-text-fill: red;
}
.list-cell:filled:selected:focused, .list-cell:filled:selected {
-fx-background-color: linear-gradient(#328BDB 0%, #207BCF 25%, #1973C9 75%, #0A65BF 100%);
-fx-text-fill: white;
}
截图:
disabled rows in dark gray so that they are more noticeable here
编辑:
感谢jewelsea的评论我已经意识到如何使用 ListView。 ListView 不需要重新生成,它只需要更新“active”属性。我已经编辑了代码并进行了修改以使其正常工作。我知道代码很长,对于那些试图找到问题的人来说可能很麻烦,但我认为这会消除新手的任何疑虑。
最佳答案
我将回答我的问题并借此机会澄清我是如何解决它的:
正如我所说,问题在于我正在重新生成包含客户数据的“ObservableList”,而不是仅仅更新“active”属性。
在代码的前面,所做的是使用generateListView()方法从ListView中删除项目并在每次禁用或启用客户端时再次重新填充它。为了解决这个问题,我创建了 updateListViewContent() 方法。如您所见,此方法仅更新“active”属性,并采用 listCustomers 内分配的值。
private fun updateListViewContent(){
listCustomers.forEach { customer ->
val id = customer.getValue("id")
val active = customer.getValue("active").toInt()
lvCustomers.items.forEach { lvCustomer ->
if(lvCustomer.getId() == id){
lvCustomer.setActive(active)
}
}
}
objListViewWrapper.clearSelection()
}
listCustomers 是包含每个客户数据的映射的列表,lvCustomers 是包含 CustomerData 类型的项目的 ListView。如果已禁用或启用的客户端 ID 与 ListView 中存在的 ID 之一匹配,则更新 active 属性。
可以通过单击按钮直接更改事件属性。此示例实际上并不需要 listCustomers。这是我在原始代码中用来存储额外数据的变量,这些数据不属于此ListView,并且以“逐步”方式呈现的表单中收集。
关于Kotlin 与 JavaFX - ListView css 类也适用于空行,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/70715790/