Software Engineer and NASA contractor in the Washington D.C. area. Specializes in Java, the NetBeans Platform, D3.JS and ANSI C. Sean is a DZone MVB and is not an employee of DZone and has posted 9 posts at DZone. You can read more from them at their website. View Full User Profile

How to Integrate JavaFX into a NetBeans Platform Wizard (Part 2)

04.29.2013
| 5776 views |
  • submit to reddit

Previously we had demonstrated how a developer can take a JavaFX GUI form and FXML developed using Scene Builder and replace a NetBeans Platform Wizard visual panel with minimal effort.  Another approach is to build the panel with JavaFX JFXPanel classes, effectively injecting ready to use JavaFX components as needed.  The trick is to using this approach is to use a specific design pattern that hides the Swing Interop pattern from the Wizard developer

To fully understand and follow this tutorial you will need to start with part one, of which the link is below:

http://netbeans.dzone.com/articles/how-integrate-javafx-netbeans-part3

which itself builds upon the following previous entries:

http://netbeans.dzone.com/articles/javafx-fxml-meets-netbeans 

http://netbeans.dzone.com/articles/how-embed-javafx-chart-visual

Now let's extend the wizard from the previous tutorial to have a second page in the wizard dialog.  We want this second page to use information from the first page.  The information will come from one of our JavaFX controls and ideally interact with a JavaFX component on the second page.  This time we will work with a JavaFX component that is part of a prebuilt class that extends the JFXPanel class. 

Assuming you have your NetBeans Platform project setup where the previous tutorial left off we will update the wizard to take the HTML content in our HTML editor component on the first page and render it with a custom Browser Panel (using WebView).  Something like the image below:



The custom Browser Panel will extend the JFXPanel class, encapsulating fields and providing an API of sorts to inject the HTML content as necessary.

  1. Provide access to HTML Editor in JavaFX GUI. Update your FXML file and controller class by adding an annotated HTMLEditor object and link it to your FXML via an fx:id string.  Follow the existing example for the TextField and Button that exist.  Provide a String accessor method for accessing the HTMLEditor object's html content.  Below is what your controller class might look like:

    import java.io.File;
    import java.net.URL;
    import java.util.ResourceBundle;
    import javafx.embed.swing.JFXPanel;
    import javafx.event.ActionEvent;
    import javafx.fxml.FXML;
    import javafx.fxml.Initializable;
    import javafx.scene.control.Button;
    import javafx.scene.control.TextField;
    import javafx.scene.web.HTMLEditor;
    import javafx.stage.FileChooser;
    
    /**
     *
     * @author SPhillips (King of Australia)
     */
    public class WizPanelController extends JFXPanel implements Initializable {
        @FXML
        private HTMLEditor htmledit; //fx:id="htmledit"
        @FXML //  fx:id="browseButton"
        private Button browseButton; // Value injected by FXMLLoader
        @FXML //  fx:id="pathText"
        private TextField pathText; //Field that Path is stored in
        private String filePath = ""; //some value to pass to the next Wizard panel
        
        // Handler for Button[fx:id="browseButton"] onAction
        public void handleButtonAction(ActionEvent event) {
            FileChooser fileChooser = new FileChooser();
            fileChooser.setTitle("Select File");
            //Show open file dialog
            File file = fileChooser.showOpenDialog(null);
            if(file!=null) {
                setFilePath(file.getPath());
                pathText.setText(filePath);
            }
        }
        @Override // This method is called by the FXMLLoader when initialization is complete
        public void initialize(URL fxmlFileLocation, ResourceBundle resources) {
            assert browseButton != null : "fx:id=\"browseButton\" was not injected: check your FXML file 'WizPanel.fxml'.";
            assert pathText != null : "fx:id=\"pathText\" was not injected: check your FXML file 'WizPanel.fxml'.";
            assert htmledit != null : "fx:id=\"htmlEdit\" was not injected: check your FXML file 'WizPanel.fxml'.";
            // initialize your logic here: all @FXML variables will have been injected
        }
        @Override //This method is used by Wizard Framework to generate list of steps
        public String getName() {
            return "FXML Panel";
        }    
        /**
         * @return the filePath
         */
        public String getFilePath() {
            return filePath;
        }
        /**
         * @param filePath the filePath to set
         */
        public void setFilePath(String filePath) {
            this.filePath = filePath;
        }
        public String getHtmlContent() {
            return htmledit.getHtmlText();
        }
    }
    The update to the FXML file is very small, simply add an fx:id parameter to the existing HTMLEditor.  Make sure the fx:id string matches the variable name in your controller class.  For example:

    <HTMLEditor fx:id="htmledit" prefHeight="272.0" prefWidth="572.0" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0" />
  2. Update first wizard panel classes to pass html content. Nothing out of the ordinary here.  It will look the same as a standard Swing based NetBeans Platform wizard:

        @Override
        public void storeSettings(WizardDescriptor wiz) {
            // use wiz.putProperty to remember current panel state
            wiz.putProperty("content", getComponent().getHtmlContent());        
        }
    In fact you may have already done so after following the previous tutorial. 

  3. Create BrowserJFXPanel. Technically you can lay this out manually within the Visual Panel class's constructor but it is a better practice to create reusable JFXPanel based components.  This abstracts and hides the Swing Interop pattern from the Swing developer.  If done right the component should have an adequate API for interaction. 

    In this case we want to use the WebView component to render the HTML content typed in the previous wizard page.  Lets create a component that does exactly this, providing a method for the user to set the URL or the HTML Content String.  The following relevant code would give you this, itself borrowing heavily from the Swing/JavaFX interop example that comes with the NetBeans IDE. 

    public class BrowserJFXPanel extends JFXPanel {
     
        private String defaultPage = "http://www.oracle.com/us/index.html";
        private WebView view;
        private WebEngine eng;
        private TextField locationUrl = null;
        private Button loadButton;
        private Pane browserPane = null;
        
        public BrowserJFXPanel() {
            super();
            // create JavaFX scene
            Platform.setImplicitExit(false);
            Platform.runLater(new Runnable() {
                @Override
                public void run() {
                    createScene();
                }
            });
        }
    
        private void createScene() {
            view =  new WebView();
            eng = view.getEngine();
            loadButton = new Button("Load");        
            loadButton.setDefaultButton(true);
            locationUrl = new TextField(defaultPage); //Gotta have something to start with
            EventHandler<ActionEvent> loadAction = new EventHandler<ActionEvent>() {
                @Override public void handle(ActionEvent event) {
                    setLocationUrl(locationUrl.getText().startsWith("http://") ? locationUrl.getText()
                           : "http://" + locationUrl.getText());
                }
            };  
            loadButton.setOnAction(loadAction);
            locationUrl.setOnAction(loadAction);
            eng.locationProperty().addListener(new ChangeListener<String>() {
                @Override public void changed(ObservableValue<? extends String> observable, String oldValue, String newValue) {
                    locationUrl.setText(newValue);
                }
            }); 
            
            GridPane grid = new GridPane();
            grid.setPadding(new Insets(5));
            grid.setVgap(5);
            grid.setHgap(5);
            GridPane.setConstraints(loadButton, 0, 0);
            GridPane.setConstraints(locationUrl, 1, 0, 1, 1, HPos.CENTER, VPos.CENTER, Priority.ALWAYS, Priority.SOMETIMES);
            GridPane.setConstraints(view, 0, 1, 2, 1, HPos.CENTER, VPos.CENTER, Priority.ALWAYS, Priority.ALWAYS);
            grid.getChildren().addAll(loadButton,locationUrl, view);
            browserPane = grid;
            this.setScene(new Scene(browserPane));        
        }    
        
        public void setLocationUrl(String url) {
            //eng.getLoadWorker().progressProperty().addListener(handler);
            final String newUrl = url;
            Platform.runLater(new Runnable() {
                @Override
                public void run() {        
                    locationUrl.setText(newUrl);
                    //eng.getLoadWorker().
                    eng.load(newUrl);
                }
            });
        }
        public void setContent(String contentString) {
             final String newcontentString = contentString;
            Platform.runLater(new Runnable() {
                @Override
                public void run() {        
                    eng.loadContent(newcontentString);
                    locationUrl.setText("User Content");
                }
            });
        }
    That was the relevant code.  You can fill out the remainder of the class information and imports easily enough.  The entire class is an example of the Swing Interop pattern, taken care of mostly via the constructor.
    Also notice that both of the set methods use the Platform.runLater() pattern, which might be obvious if you have been following along.  Now that we have a component with an API, lets add it to our Swing panel.

  4. Add BrowserJFXPanel to VisualPanel2 class. You should be able to easily add your JFXPanel components to a JScrollPane at a BorderLayout.Center position.  Below is how your code might look:

    import com.javafx.browser.BrowserJFXPanel;
    import java.awt.BorderLayout;
    import javax.swing.JPanel;
    import javax.swing.JScrollPane;
    
    public final class JfxwizVisualPanel2 extends JPanel {
        private JScrollPane scrollPane = new JScrollPane(); 
        public BrowserJFXPanel browserPanel;
        /**
         * Creates new form JfxwizVisualPanel2
         */
        public JfxwizVisualPanel2() {
            initComponents();
            setLayout(new BorderLayout());
            browserPanel = new BrowserJFXPanel();
            scrollPane.getViewport().add(browserPanel);
            add(scrollPane,BorderLayout.CENTER);        
        }
    
        @Override
        public String getName() {
            return "Review in JavaFX";
        }
    
        /**
         * This method is called from within the constructor to initialize the form.
         * WARNING: Do NOT modify this code. The content of this method is always
         * regenerated by the Form Editor.
         */
        // <editor-fold defaultstate="collapsed" desc="Generated Code">                          
        private void initComponents() {
    
            javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this);
            this.setLayout(layout);
            layout.setHorizontalGroup(
                layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
                .addGap(0, 400, Short.MAX_VALUE)
            );
            layout.setVerticalGroup(
                layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
                .addGap(0, 300, Short.MAX_VALUE)
            );
        }// </editor-fold>                        
        // Variables declaration - do not modify                     
        // End of variables declaration                   
    }
    Not much here really... just another Swing component to add to a layout.

  5. Set BrowserJFXPanel content. Finally you can use the HTML content captured using the HTMLEditor component from the previous wizard dialog page.  As with the standard NetBeans Platform Wizard pattern, the information is retrieved using the readSettings() method override.  However this time you simply set the browserPanel's content string using the method interface exposed in previous steps.  Your code would look like this:

        @Override
        public void readSettings(WizardDescriptor wiz) {
            // use wiz.getProperty to retrieve previous panel state
            content = (String) wiz.getProperty("content");  
            component.browserPanel.setContent(content);
        }
The result if done correctly can render the content created using your FXML GUI in your new BrowserJFXPanel component like the following:



And what's more... because you designed your component so well you can enter any web url you want and render that instead.  Hopefully you were already wondering why I bothered laying out a location toolbar and load button for this.  The trick to this tutorial is that the browser component provided an interface for automatically setting either the URL or the actual content string automatically.The idea is that you can develop these reusable components for elsewhere in your NetBeans Platform application, like say as part of a custom TopComponent or maybe as a embedded component in a Visual Library scene...  (hint hint ... stay tuned...)

Hey let's use our new BrowserJFXPanel component to check out what awesome blogs there are out there:





Published at DZone with permission of Sean Phillips, author and DZone MVB.

(Note: Opinions expressed in this article and its replies are the opinions of their respective authors and not those of DZone, Inc.)