在条件下重置ChangeListener中的复选框选择



在我的JavaFX应用程序中,我在TreeView中使用Checkboxes来更改节点的可见性。

选中
  • 复选框=某些节点可见
  • 取消选中复选框=某些节点不可见

但是,在特殊情况下,应该提示用户确认他们的选择,因为激活特定复选框时可能会出现问题。打开一个对话框窗口;是";以及";否";。如果用户选择";是";,节点可见,一切正常。但是如果用户选择";否";,应再次取消选中该复选框。

我的想法是检查ChangeListener中的条件(在这种情况下,在对话框窗口中按"否"),如果为true,则将所选值设置为false。

但不管出于什么原因,它都没有起作用。之后,我发现它可以使用TreeViewrefresh()方法。

问题

  1. 为什么没有refresh()方法就不能工作,为什么setSelected()被忽略
  2. 我应该使用refresh()方法吗
  3. 是否有更好的变通方法来更改选择状态

最小可重复示例

使用refresh()行将显示所需行为:单击后复选框保持未选中状态,因为5>4(5>4模拟例如在对话框窗口中按"否")。

import javafx.application.Application;
import javafx.beans.value.ChangeListener;
import javafx.scene.Scene;
import javafx.scene.control.CheckBoxTreeItem;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import org.controlsfx.control.CheckTreeView;
public class HelloApplication extends Application {
enum Names { TEST1, TEST2, TEST3, TEST4 }
private final CheckTreeView<String> checkTreeView = new CheckTreeView<>();
@Override
public void start(Stage stage) {
VBox vBox = new VBox();
Scene scene = new Scene(vBox, 500, 500);
setTreeView();
vBox.getChildren().add(checkTreeView);
stage.setScene(scene);
stage.show();
}
public static void main(String[] args) {
launch();
}

public void setTreeView() {
CheckBoxTreeItem<String> rootItem = new CheckBoxTreeItem<>("Root");
rootItem.setExpanded(true);
for (Names name : Names.values()) {
CheckBoxTreeItem<String> item = new CheckBoxTreeItem<>(name.toString(), null);
item.selectedProperty().addListener(this.onSelectionChanged(item));
rootItem.getChildren().add(item);
}
this.checkTreeView.setRoot(rootItem);
}
private ChangeListener<Boolean> onSelectionChanged(CheckBoxTreeItem<String> item) {
return (observableValue, previousChoice, newChoice) -> {
if (newChoice) { // if checkbox is selected
// if anything happens... for example press a "no" button in a dialog window
if (5 > 4) {
System.out.println("reset checkbox status");
item.setSelected(previousChoice);
}
}
// it works with refresh:
// this.checkTreeView.refresh();
};
}
}

编辑:

用解决@VGR问题

Platform.runLater(() -> item.setSelected(previousChoice));

适用于最小可复制的示例,但这似乎不是最好的方法。正如评论中已经讨论过的,Platform.runLater()

大部分时间工作

无法保证,因为确切的时间未指定,如果挂起事件中的某些内容也使用runlater,则会中断f.i。

需要始终有效的解决方案/变通方法。。。

我们在OP示例中看到的实际上是双向绑定属性的标准行为,简化为一个简单的(没有ui,只有属性)示例。

场景:

  • 有两个布尔属性,表示数据(checkBoxTreeItem)和ui(checkBox)所选属性
  • 它们的连接方式与checkBoxTreeCell中的连接方式相同,即ui双向绑定到数据
  • 数据的监听器在接收到从false的改变时将数据恢复为false->真的

只是为了表明(尽管也不建议)在侦听器中恢复发送方的状态是有效的:我们将数据属性设置为true。最后,数据和ui都是假的,正如我们所希望的那样

before data.set(true)
state of data/ui: false / false
enter listener to ui: true
enter listener to data: true
enter listener to ui: false
enter listener to data: false
... returning
listener to data - after reset false
after data.set(true) - state of data/ui: false / false

对于OP上下文的真实模拟:设置ui属性(用户单击checkBox)-最后,数据属性被还原,而ui属性则不被还原。从技术上讲,这是在双向绑定中完成的,它在更新时忽略传入的更改。

before ui.set(true)
state of data/ui: false / false
enter listener to data: true
enter listener to data: false
... returning
listener to data - after reset false
enter listener to ui: true
after ui.set(true) - state of data/ui: false / true

示例:

public class BidiBindingUpdating {
public static void main(String[] args) {
BooleanProperty data = new SimpleBooleanProperty();
BooleanProperty ui = new SimpleBooleanProperty();
ui.bindBidirectional(data);
// listener to item: revert to false
data.addListener((src, ov, nv) -> {
System.out.println("enter listener to data: " + nv);
if (!nv) {
System.out.println(" ... returning");
return;
}
data.set(ov);
System.out.println("listener to data - after reset " + data.get());
});
// listener to check: logging only
ui.addListener((src, ov, nv) -> {
System.out.println("enter listener to ui: " + nv);
});
// set item directly:
System.out.println("before data.set(true)");
System.out.println("    state of data/ui: " + data.get() + " / " + ui.get());
data.set(true);
System.out.println("after data.set(true) - state of data/ui: " + data.get() + " / " + ui.get());
// set bound property:
System.out.println("nbefore ui.set(true)");
System.out.println("    state of data/ui: " + data.get() + " / " + ui.get());
ui.set(true);
System.out.println("after ui.set(true) - state of data/ui: " + data.get() + " / " + ui.get());
}
}

您过早地重置了复选框项目。它尚未完成事件处理。

您想等到它完成后再重置它。请稍后使用Platform.run确保在处理完所有挂起的事件后重置它:

Platform.runLater(() -> item.setSelected(previousChoice));

我相信你已经从之前的回答和本文中的评论中获得了关于这个问题的所有信息。因此,我不再重复解释问题的原因。

我可以提出一个替代的解决方法,类似于@VGR使用Platform.runLater的答案。尽管Platform.runLater在大多数问题上起着主密钥的作用,但由于其不可预测性和线程原因,大多数情况下不建议使用它。

@VGR答案中提到的问题的关键原因:

您过早地重置了复选框项目。它还没有完成事件处理尚未完成。你想等到它完成后再做重置。

和@kelopatra评论:

我认为罪魁祸首是监听器

由于上述原因,如果我们在事件处理结束时运行所需的代码(我的意思是在所有布局子项完成后),我们似乎可以解决这个问题。

JavaFX有一个非常强大的类AnimationTimer。您可以通过这个类的API,但简单地说,它将允许您在每个帧的末尾运行所需的代码。与Platform.run相反,稍后,我们将在这里知道它何时运行。该类的实际使用实际上在Scene->addPostLayoutPulseListener API。

解决方案:

创建一个接受Runnable并在当前场景脉冲结束时仅运行该代码一次的实用程序类。当执行此代码时,场景中的布局已经完成,因此您不会有任何问题。

import javafx.animation.AnimationTimer;
import java.util.concurrent.atomic.AtomicInteger;
/**
* Utility to run the provided runnable in each scene pulse.
*/
public final class PulseUtil extends AnimationTimer {
/** Specifies the count of current pulse. */
private static final int CURRENT_PULSE = 0;
/** Specifies the count of the timer once it is started. */
private final AtomicInteger count = new AtomicInteger(0);
/** Specifies the order at which the runnable need to be executed. */
private final int order;
/** Runnable to execute in each frame. */
private final Runnable callback;
/**
* Constructor.
*
* @param aCallBack runnable to execute
* @param aOrder order at which the runnable need to be executed
*/
private PulseUtil(final Runnable aCallBack, final int aOrder) {
callback = aCallBack;
order = aOrder;
}
/**
* Executes the provided runnable at the end of the current pulse.
*
* @param callback runnable to execute
*/
public static void doEndOfPulse(final Runnable callback) {
new PulseUtil(callback, CURRENT_PULSE).start();
}
@Override
public void handle(final long now) {
if (count.getAndIncrement() == order) {
try {
callback.run();
} finally {
stop();
}
}
}
}

您将在change监听器中调用此方法:

private ChangeListener<Boolean> onSelectionChanged(CheckBoxTreeItem<String> item) {
return (observableValue, previousChoice, newChoice) -> {
if (newChoice) { // if checkbox is selected
// if anything happens... for example press a "no" button in a dialog window
if (5 > 4) {
System.out.println("reset checkbox status");
PulseUtil.doEndOfPulse(()->item.setSelected(previousChoice));
}
}
};
}

最新更新