4

In a JavaFX dialog, I would like to show a list of files with their icons and file names.

It was easy to find how to get an icon for a file extension:

File file = File.createTempFile("icon", ".doc");  
FileSystemView view = FileSystemView.getFileSystemView();      
java.swing.Icon icon = view.getSystemIcon(file);      
file.delete();

But, how can I draw that Swing Icon in a JavaFX ListView?

private static class AttachmentListCell extends ListCell<String> {
    @Override
    public void updateItem(String fileName, boolean empty) {
        if (item != null) {

            // Get file Icon for fileName as shown above.
            java.swing.Icon icon = 

            // Transform Icon to something that can be 
            // added to the box, maybe an ImageView.
            javafx.scene.image.ImageView image = ???

            // Label for file name
            Label label = new Label(item);

            HBox box = new HBox();
            box.getChildren().addAll(image, label);
            setGraphic(box);
        }
    }
}
jewelsea
  • 150,031
  • 14
  • 366
  • 406
wimix
  • 574
  • 1
  • 5
  • 16

2 Answers2

7

Here is an example for a list view with file icons:

enter image description here

import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.HashMap;

import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.embed.swing.SwingFXUtils;
import javafx.scene.Scene;
import javafx.scene.control.ListCell;
import javafx.scene.control.ListView;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import javafx.util.Callback;

import javax.swing.filechooser.FileSystemView;

public class ListViewCellFactory2 extends Application {

    ListView<String> list = new ListView<String>();
    ObservableList<String> data = FXCollections.observableArrayList(
            "a.msg", "a1.msg", "b.txt", "c.pdf", 
            "d.html", "e.png", "f.zip",
            "g.docx", "h.xlsx", "i.pptx");

    @Override
    public void start(Stage stage) {
        VBox box = new VBox();
        Scene scene = new Scene(box, 200, 200);
        stage.setScene(scene);
        stage.setTitle("ListViewSample");
        box.getChildren().addAll(list);
        VBox.setVgrow(list, Priority.ALWAYS);

        list.setItems(data);

        list.setCellFactory(new Callback<ListView<String>, ListCell<String>>() {
            @Override
            public ListCell<String> call(ListView<String> list) {
                return new AttachmentListCell();
            }
        });

        stage.show();
    }

    private static class AttachmentListCell extends ListCell<String> {
        @Override
        public void updateItem(String item, boolean empty) {
            super.updateItem(item, empty);
            if (empty) {
                setGraphic(null);
                setText(null);
            } else {
                Image fxImage = getFileIcon(item);
                ImageView imageView = new ImageView(fxImage);
                setGraphic(imageView);
                setText(item);
            }
        }
    }

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


    static HashMap<String, Image> mapOfFileExtToSmallIcon = new HashMap<String, Image>();

    private static String getFileExt(String fname) {
        String ext = ".";
        int p = fname.lastIndexOf('.');
        if (p >= 0) {
            ext = fname.substring(p);
        }
        return ext.toLowerCase();
    }

    private static javax.swing.Icon getJSwingIconFromFileSystem(File file) {

        // Windows {
        FileSystemView view = FileSystemView.getFileSystemView();
        javax.swing.Icon icon = view.getSystemIcon(file);
        // }

        // OS X {
        //final javax.swing.JFileChooser fc = new javax.swing.JFileChooser();
        //javax.swing.Icon icon = fc.getUI().getFileView(fc).getIcon(file);
        // }

        return icon;
    }

    private static Image getFileIcon(String fname) {
        final String ext = getFileExt(fname);

        Image fileIcon = mapOfFileExtToSmallIcon.get(ext);
        if (fileIcon == null) {

            javax.swing.Icon jswingIcon = null; 

            File file = new File(fname);
            if (file.exists()) {
                jswingIcon = getJSwingIconFromFileSystem(file);
            }
            else {
                File tempFile = null;
                try {
                    tempFile = File.createTempFile("icon", ext);
                    jswingIcon = getJSwingIconFromFileSystem(tempFile);
                }
                catch (IOException ignored) {
                    // Cannot create temporary file. 
                }
                finally {
                    if (tempFile != null) tempFile.delete();
                }
            }

            if (jswingIcon != null) {
                fileIcon = jswingIconToImage(jswingIcon);
                mapOfFileExtToSmallIcon.put(ext, fileIcon);
            }
        }

        return fileIcon;
    }

    private static Image jswingIconToImage(javax.swing.Icon jswingIcon) {
        BufferedImage bufferedImage = new BufferedImage(jswingIcon.getIconWidth(), jswingIcon.getIconHeight(),
                BufferedImage.TYPE_INT_ARGB);
        jswingIcon.paintIcon(null, bufferedImage.getGraphics(), 0, 0);
        return SwingFXUtils.toFXImage(bufferedImage, null);
    }

}

EDIT: On a HDPI monitor, the file icons are clipped.

enter image description here

  1. What has to be done to have the icons entirely displayed in the row?
  2. How can I retrieve the scale factor from Java?
wimix
  • 574
  • 1
  • 5
  • 16
  • 2
    Although the attached solution works, for Java 8, it might be a good idea to use SwingUtilities.invokeLater and Platform.runLater to ensure that swing code is executed on the swing thread and JavaFX code is executed on the JavaFX thread - such a change *might* make the solution more reliable. – jewelsea Jan 21 '15 at 22:36
6

I came up with this code, which seems to work for converting the icon returned by getSystemIcon to a format which JavaFX can understand. It does this by using a combo of SwingUtilities.invokeLater with Platform.runLater to try to mitigate any potential threading issues between the two projects. The javax.swing.Icon is painted to java.awt.BufferedImage which is converted by SwingFXUtils to a JavaFX Image.

So the Swing Icon => JavaFX image portion of the code you were asking about seems to work. The only thing is, when I tested the application on OS X 10.7 running Oracle Java8u20, every file extension type I tried gave the exact same icon. So it would seem that your method for getting an icon from the system via the swing FileSystemView isn't really supported on OS X (as far as I can tell). I guess you could try it on another OS and see if it works for you there (presuming that supporting this icon lookup feature on OS X is not necessary for your task).

So googling around brought up the following question:

And in that, Sam Barnum recommends a FileView works much better than a FileSystemView - and so it did for me. I switched your code to use a FileView and after that started getting different icons for different file types on OS X. The icons were still really small 16x16 icons (I wouldn't know how to retrieve the hi-res retina icons for instance), but at least the icons which were retrieved appeared correct and file type specific.

import javafx.application.Application;
import javafx.application.Platform;
import javafx.embed.swing.SwingFXUtils;
import javafx.scene.Scene;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;

import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;

public class FileIconViewer extends Application {

    @Override
    public void start(Stage stage) throws IOException {
        Runnable fetchIcon = () -> {
            File file = null;
            try {
                file = File.createTempFile("icon", ".png");

                // commented code always returns the same icon on OS X...
                // FileSystemView view = FileSystemView.getFileSystemView();
                // javax.swing.Icon icon = view.getSystemIcon(file);

                // following code returns different icons for different types on OS X...
                final javax.swing.JFileChooser fc = new javax.swing.JFileChooser();
                javax.swing.Icon icon = fc.getUI().getFileView(fc).getIcon(file);

                BufferedImage bufferedImage = new BufferedImage(
                    icon.getIconWidth(), 
                    icon.getIconHeight(), 
                    BufferedImage.TYPE_INT_ARGB
                );
                icon.paintIcon(null, bufferedImage.getGraphics(), 0, 0);

                Platform.runLater(() -> {
                    Image fxImage = SwingFXUtils.toFXImage(
                        bufferedImage, null
                    );
                    ImageView imageView = new ImageView(fxImage);
                    stage.setScene(
                        new Scene(
                            new StackPane(imageView), 
                            200, 200
                        )
                    );
                    stage.show();
                });
            } catch (IOException e) {
                e.printStackTrace();
                Platform.exit();
            } finally {
                if (file != null) {
                    file.delete();
                }
            }
        };

        javax.swing.SwingUtilities.invokeLater(fetchIcon);
    }

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

Note: there is an existing request in the JavaFX issue tracker to add this functionality to JavaFX (it is currently not scheduled for implementation, though you could log into the issue tracker and vote for the issue, comment on it, link it back to this StackOverflow question, etc):

Community
  • 1
  • 1
jewelsea
  • 150,031
  • 14
  • 366
  • 406
  • Thank you, jewelsea, for this detailed answer. When I run your FileIconViewer test app on Windows, the correct file icons are shown only when using FileSystemView class. With JFileChooser, always the same icon is displayed. Luckily, I only need the solution on Windows. So for me, your answer pushed me a large step forward. I voted for the RT. -- But I encounter another problem with my AttachmentListCell class: the list view selection does not change on mouse clicks. It changes only by cursor keys. Can you imagine why? Kind regards. – wimix Jan 20 '15 at 22:23
  • The `AttachmentListCell` class as you originally posted mixes Swing/AWT and JavaFX code on the same thread - doing this is not recommended as it can lead to unpredictable results. I don't know whether or not that is the root cause of your selection issues though. You could create an [MCVE](http://stackoverflow.com/help/mcve) and post it in a new question about your selection issues if you wish. – jewelsea Jan 20 '15 at 23:21
  • 1
    The AttachmentListCell.updateItem has to call super.updateItem. This solves the problem. – wimix Jan 21 '15 at 22:16
  • @jewelsea Is it really necessary that those methods are done separately in `invokeLater()` and `runLater()`? Is there a case where both frameworks (JavaFX and Swing) use different threads? – konsolebox Oct 11 '16 at 16:32
  • @konsolebox Swing and JavaFX do use different threads. That is why the `invokeLater()` and `runLater()` calls are necessary - to run the code for the given framework on the appropriate thread for that framework. Running the GUI code for a given framework on the wrong thread may result in race conditions leading to unpredictable errors or outcomes. – jewelsea Oct 11 '16 at 19:09
  • @jewelsea Thanks for the reply. Yes I know the idea. I'm just thinking if the implementation is always the case even in JDK 8 and 9 - that they aren't merged to use a single thread. – konsolebox Oct 12 '16 at 05:31
  • @konsolebox there is a flag `-Djavafx.embed.singleThread=true `, which merges the Swing EDT thread and the JavaFX application thread. With this flag enabled, you are correct that the switching calls for `invokeLater` and `runLater` are no longer required. See a [discussion in Oracle forums](https://community.oracle.com/thread/3613808) for more info. As far as I know, the merged thread feature is experimental, undocumented and not recommended for use in production code. For more info contact the [openjfx dev mailing list](http://mail.openjdk.java.net/mailman/listinfo/openjfx-dev). – jewelsea Oct 12 '16 at 18:49
  • @jewelsea Thanks. I just need a hint about its current state as it adds factor to my decision whether to use some stuff in Swing or just have a pure JavaFX setup - where in this case if I so choose not to use any Swing, I'd use internal icons which will be shown based on mime-type instead. – konsolebox Oct 12 '16 at 19:28