JavaFX:AnimationTimer和MenuBar之间的交互

3

我正在开发一款软件,该软件从摄像头获取图像并实时显示在JavaFX的ImageView中。我有一个线程获取最后一张图像(在这种情况下是一个BufferedImage),以及一个AnimationTimer将其分配给ImageView。我选择使用AnimationTimer是因为它似乎比每次获取新图像时填充平台运行的Runnable更好。刷新效果很好,帧率也不错。

然而,我注意到当AnimationTimer正在运行时,我的软件菜单栏无法正常显示。当我悬停在某些菜单项上时,部分菜单项消失。以下图片对此进行了说明: enter image description here

左侧是菜单的正常外观,右侧是AnimationTimer运行时的外观。你可以看到,“Save”菜单项已经消失,并且我的实时图像背景被显示出来。而且,当我打开一个新窗口(在新的Scene中)时,当我悬停在任何类型的Node(如按钮、复选框等)上时,背景会变黑。我通过在初始化Scene时将深度缓冲布尔设置为true来解决了这个问题。然而,我不知道如何修复这个菜单栏错误,并且我认为这些错误表明我正在做的可能不正确。

我认为JavaFX应用程序线程被新图像占据,导致其他元素(例如菜单项)绘制所需时间过长,从而导致这种情况发生。

问题:

  1. 这个bug真的是从那里产生的吗?
  2. 有没有一种方法可以改进我的代码,例如使用与AnimationTimer不同的东西?

以下是重现此问题的代码片段。在start函数中更改两个字符串以进行图像路径的更改。这些图像应该相对较大(几MB)。

单击“Start”按钮以启动动画计时器。然后尝试打开“File”菜单,并悬停在菜单项上。这个bug不会出现在所有情况下,请反复移动鼠标上下,它应该在某些时候出现。

import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.logging.Level;
import java.util.logging.Logger;
import javafx.animation.AnimationTimer;
import javafx.application.Application;
import javafx.embed.swing.SwingFXUtils;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Menu;
import javafx.scene.control.MenuBar;
import javafx.scene.control.MenuItem;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.Pane;
import javafx.stage.Stage;
import javax.imageio.ImageIO;

public class ImageRefresher extends Application {

    @Override
    public void start(Stage primaryStage) {

        //Here change the 2 Strings to a path to an image on your HDD
        //The bug appears more easily with large images (>3-4MB)
        String pathToImage1 = "/path/to/your/first/image";
        String pathToImage2 = "/path/to/your/second/image";

        try {
            //Image content (contains buffered image, see below)
            ImageContent image = new ImageContent(pathToImage1);

            //If this line is commented, the bug does not appear
            image.setImage(ImageIO.read(new File(pathToImage2)));

            //JavaFX class containing nodes (see below)
            MainWindow window = new MainWindow(image);

            Scene scene = new Scene(window.getPane(), 300, 250);
            primaryStage.setTitle("Menu refresh");
            primaryStage.setScene(scene);
            primaryStage.show();
        } catch (IOException ex) {
            Logger.getLogger(ImageRefresher.class.getName()).log(Level.SEVERE, null, ex);
        }

    }

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

    public class MainWindow {

        private BorderPane pane;
        private MenuBar menuBar;
        private ImageView displayImage;
        private Button startRefreshingButton;

        private ImageContent imageContent;
        private AnimationTimer animationTimer;

        public MainWindow(ImageContent imageContent) {
            this.imageContent = imageContent;

            //Builds the window's components
            buildGraphic();

            //The image is reset at each frame
            animationTimer = new AnimationTimer() {
                @Override
                public void handle(long now) {
                    displayImage.setImage(imageContent.getDisplayableImage());
                }
            };
        }

        private void buildGraphic() {
            pane = new BorderPane();
            menuBar = new MenuBar();

            Menu menu = new Menu("File");
            menu.getItems().addAll(new MenuItem("Save"),
                    new MenuItem("Open"),
                    new MenuItem("Close"));
            menuBar.getMenus().add(menu);

            displayImage = new ImageView();

            startRefreshingButton = new Button("Start");
            startRefreshingButton.setOnAction((event) -> {
                animationTimer.start();
            });

            pane.setTop(menuBar);
            pane.setCenter(displayImage);
            pane.setBottom(startRefreshingButton);
        }

        public Pane getPane() {
            return pane;
        }
    }

    public class ImageContent {

        private BufferedImage imageContent;

        //Initializes bufferedimage with the path specified
        public ImageContent(String pathToImage) throws IOException {
            imageContent = ImageIO.read(new File(pathToImage));
        }

        public void setImage(BufferedImage newImage) {
            imageContent = newImage;
        }

        //Function called by the animation timer to
        //get a JavaFX image from a bufferedimage
        public Image getDisplayableImage() {
            return SwingFXUtils.toFXImage(imageContent, null);
        }
    }
}
2个回答

3
我猜问题在于每帧都重新绘制图像,导致菜单弹出窗口与图像重叠。这似乎是一个错误,而且您还要求FX应用程序线程执行比您需要的更多的工作。
理想情况下,您应该找到一种方法来检查是否真的有一个新图像,并仅在确实有一个新文件时更新图像(考虑使用java.nio.file.Path表示文件并调用Files.getLastModifiedTime(path))。
另一种避免向FX应用程序线程发送过多Platform.runLater(...)调用的方法,请参见限制javafx gui更新

有趣的阅读,尽管使用这种策略似乎仍然存在错误。 - Koln
我有一个来自摄像头的实时视频流,因此我需要至少10-12帧每秒才能与其进行适当的交互。我尝试使用Observable模式,并在获取新图像时触发更新。在更新时,我使用您描述的策略调用Platform.runLater()。问题仍然存在。我认为我显示的图像太大了(2560 * 2160像素)。 - Koln
请在 https://javafx-jira.kenai.com 上报告一个错误。请包含一个完整的示例以演示问题。 - James_D

1
最终我没有在jira上提交任何问题,因为我已经解决了我的问题。问题出在我调用 SwingFXUtils.toFXImage(imageContent, null) 的方式上。我在每一帧上都返回了这个函数的结果,虽然我不确定细节,但这可能会每次都创建一个新对象。避免这种情况的简单方法是将 WritableImage 作为参数传递,并将 ImageViewImageProperty 值绑定到它上面。
如果我采取上面发布的MCVE,代码可能如下(未经测试,可能存在更简洁的解决方案):
    public class MainWindow {

        private BorderPane pane;
        private MenuBar menuBar;
        private ImageView displayImage;
        private Button startRefreshingButton;

        private ImageContent imageContent;
        private AnimationTimer animationTimer;

        // Here's the value to bind
        private ObservableValue<WritableImage> imageProperty;

        public MainWindow(ImageContent imageContent) {
            //initialization stuff 

            this.imageProperty = new ObservableValue<>(imageContent.getWritableImage());
            displayImage.imageProperty().bind(imageProperty);

            //The image is reset at each frame
            animationTimer = new AnimationTimer() {
                @Override
                public void handle(long now) {
                    SwingFXUtils.toFXImage(imageContent.getBufferedImage(), image.getWritableImage());
                }
            };
        }
    }


    public class ImageContent {

        private BufferedImage imageContent;
        private WritableImage writableImage;

        //Initializes bufferedimage with the path specified
        public ImageContent(String pathToImage) throws IOException {
            imageContent = ImageIO.read(new File(pathToImage));

            //Get the width and height values from your image
            int width = imageContent.getWidth();
            int height = imageContent.getHeight();
            writableImage = new WritableImage(width, height);
        }

        public void setImage(BufferedImage newImage) {
            imageContent = newImage;
        }

        public WritableImage getWritableImage() {
            return writableImage;
        }

        public BufferedImage getBufferedImage() {
            return imageContent;
        }
    }

然而,现在似乎需要占用大量内存,我会调查一下。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接