java - 有没有一种方法可以更改 Java 属性而不向其监听器触发值更改事件?

标签 java javafx properties undo-redo

我正在尝试做什么

我正在寻找一种更改属性的方法,而无需调用监听器的更改方法。

更具体地说,我正在尝试实现撤消/重做功能。我实现它的方式如下,在一个带有 BooleanProperty 的示例中和 JavaFX CheckBox .

  1. selectedProperty CheckBox的单击鼠标即可更改。
  2. 一个BooleanProperty (实际上是 JavaFX SimpleBooleanProperty )已更改,因为它双向绑定(bind)到 selectedProperty
  3. ChangeListener BooleanProperty的注册它并添加 Command在应用程序的 undoStackCommand存储属性、旧值和新值。
  4. 用户点击撤消按钮
  5. 通过按钮,应用程序获取最后的 Command从堆栈中调用它是 undo()方法。
  6. undo()方法改变BooleanProperty回来。
  7. ChangeListener再次注册此更改并创建一个新的 Command
  8. 创建无限循环

我的黑客解决方案

我这样做的方法是传递 ChangeListenerCommand目的。然后是undo()方法首先删除 ChangeListener ,更改 BooleanProperty然后添加ChangeListener再次。
通过ChangeListener感觉是错误的和hacky的到Command (在我的第 3 步的实际实现中, ChangeListenerCommand 之间实际上还有一些类,现在都需要了解 ChangeListener )

我的问题

这真的是这样做的方法吗?有没有办法在第 6 步中更改属性并告诉它通知它的监听器?或者至少是为了吸引听众?

最佳答案

正如您所描述的,没有支持绕过监听器的方法。您只需将此逻辑构建到您的撤消/重做机制中即可。这个想法基本上是在执行撤消/重做时设置一个标志,如果是这样,则不将更改添加到堆栈中。

这是一个非常简单的示例:请注意,这不是生产质量 - 例如,在文本控件中键入内容将添加到每个字符更改的堆栈中(在每次更改时保留当前文本的副本)。在实际代码中,您应该将这些更改合并在一起。

import java.util.Deque;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map;

import javafx.beans.property.Property;
import javafx.beans.value.ChangeListener;

public class UndoManager {

    private boolean performingUndoRedo = false ;
    private Deque<Command<?>> undoStack = new LinkedList<>();
    private Deque<Command<?>> redoStack = new LinkedList<>();


    private Map<Property<?>, ChangeListener<?>> listeners = new HashMap<>();

    public <T> void register(Property<T> property) {
        // don't register properties multiple times:
        if (listeners.containsKey(property)) {
            return ;
        }
        // FIXME: should coalesce (some) changes on the same property, so, e.g. typing in a text
        // control does not result in a separate command for each character
        ChangeListener<? super T> listener = (obs, oldValue, newValue) -> {
            if (! performingUndoRedo) {
                Command<T> cmd = new Command<>(property, oldValue, newValue) ;
                undoStack.addFirst(cmd);
            }
        };
        property.addListener(listener);
        listeners.put(property, listener);
    }

    public <T> void unregister(Property<T> property) {
        listeners.remove(property);
    }

    public void undo() {
        if (undoStack.isEmpty()) {
            return ;
        }
        Command<?> command = undoStack.pop();
        performingUndoRedo = true ;
        command.undo();
        redoStack.addFirst(command);
        performingUndoRedo = false ;
    }

    public void redo() {
        if (redoStack.isEmpty()) {
            return ;
        }
        Command<?> command = redoStack.pop();
        performingUndoRedo = true ;
        command.redo();
        undoStack.addFirst(command);
        performingUndoRedo = false ;
    }



    private static class Command<T> {
        private final Property<T> property ;
        private final T oldValue ;
        private final T newValue ;

        public Command(Property<T> property, T oldValue, T newValue) {
            super();
            this.property = property;
            this.oldValue = oldValue;
            this.newValue = newValue;
        }

        private void undo() {
            property.setValue(oldValue);
        }

        private void redo() {
            property.setValue(newValue);
        }

        @Override 
        public String toString() {
            return "property: "+property+", from: "+oldValue+", to: "+newValue ;
        }
    }
}

这是一个快速测试工具:

import javafx.application.Application;
import javafx.beans.value.ChangeListener;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.CheckBox;
import javafx.scene.control.ComboBox;
import javafx.scene.control.ListCell;
import javafx.scene.control.TextArea;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;

public class UndoExample extends Application {

    @Override
    public void start(Stage stage) throws Exception {
        ComboBox<Color> textColor = new ComboBox<Color>();
        textColor.getItems().addAll(Color.BLACK, Color.RED, Color.DARKGREEN, Color.BLUE);
        textColor.setValue(Color.BLACK);
        textColor.setCellFactory(lv -> new ColorCell());
        textColor.setButtonCell(new ColorCell());
        CheckBox italic = new CheckBox("Italic");
        TextArea text = new TextArea();
        updateStyle(text, textColor.getValue(), italic.isSelected());

        ChangeListener<Object> listener = (obs, oldValue, newValue) -> 
            updateStyle(text, textColor.getValue(), italic.isSelected());
        textColor.valueProperty().addListener(listener);
        italic.selectedProperty().addListener(listener);

        UndoManager undoMgr = new UndoManager();
        undoMgr.register(textColor.valueProperty());
        undoMgr.register(italic.selectedProperty());
        undoMgr.register(text.textProperty());

        Button undo = new Button("Undo");
        Button redo = new Button("Redo");
        undo.setOnAction(e -> undoMgr.undo());
        redo.setOnAction(e -> undoMgr.redo());

        HBox controls = new HBox(textColor, italic, undo, redo);
        controls.setSpacing(5);

        BorderPane root = new BorderPane(text);
        root.setTop(controls);

        Scene scene = new Scene(root);
        stage.setScene(scene);
        stage.show();
    }

    private void updateStyle(TextArea text, Color textColor, boolean italic) {
        StringBuilder style = new StringBuilder()
                .append("-fx-text-fill: ")
                .append(hexString(textColor))
                .append(";")
                .append("-fx-font: ");
        if (italic) {
            style.append("italic ");
        }
        style.append("13pt sans-serif ;");
        text.setStyle(style.toString());
    }

    private String hexString(Color color) {
        int r = (int) (color.getRed() * 255) ;
        int g = (int) (color.getGreen() * 255) ;
        int b = (int) (color.getBlue() * 255) ;
        return String.format("#%02x%02x%02x", r, g, b);
    }

    private static class ColorCell extends ListCell<Color> {
        private Rectangle rect = new Rectangle(25, 25);
        @Override
        protected void updateItem(Color color, boolean empty) {
            super.updateItem(color, empty);
            if (empty || color==null) {
                setGraphic(null);
            } else {
                rect.setFill(color);
                setGraphic(rect);
            }
        }       
    }

    public static void main(String[] args) {
        Application.launch(args);
    }

}

关于java - 有没有一种方法可以更改 Java 属性而不向其监听器触发值更改事件?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/61478539/

相关文章:

java - 如何在 JPanel 上按名称或 id 查找元素?

java - 模块化 Maven 项目中的域类应该基于接口(interface)吗?

java - 一起使用 Platform.exit() 和 System.exit(int)

c# - 在 C# 中是否可以使用同名的公共(public) getter 和私有(private) setter?

iphone - 如何向 UIImageView 的子类添加自定义动画属性?

java - 定义常量标识符的最佳方法?

java - Spring 启动: Disable fetch of PersistentBag programatically

java - 使 JavaFX 应用程序线程等待另一个线程完成

java - @FXML 注释和 FXMLLoader 类未解析为 Java 11 和 JavaFX 11 中的类型

properties - Swift 类 : Property not initialized at super. init 调用中出现错误