Introduction by Example: JavaFX 8 Printing
I‘ve not blogged in awhile, and I miss sharing with others about all things JavaFX (My day job and family are likely excuses). For those who are new to this blog, I am the author of JavaFX 2 Introduction by Example (JIBE), co-author of Java 7 Recipes, and technical reviewer of the Pro JavaFX 2 books from Apress publishing. For those who already know me, I’d like to thank you for supporting me and the other authors by purchasing these books. More importantly, my hope is to reach out to Java enthusiasts and share ideas.
The book JavaFX 2 Introduction by Example, was published in Nov. 2011 and many more APIs were added since then. During the writing of the book, I was working on the early editions of JavaFX 2.0 up until the announcement at JavaOne Oct. 2011. It was pretty crazy trying to update the book based on API changes as things were almost set in stone. I thought it was amazing how it even got out the door. However, I was pretty pleased. Some of you who have read the beginning of the book (JIBE) understand that the chapters of JIBE are also found in the book Java 7 Recipes (actually it is originally taken from Java 7 recipes). This little fact explains why the book, JavaFX 2 Introduction by Example, is reminiscent of recipe or cookbook style technical books. My intent was to help the reader get introduced quickly without a lot of tech blather. Instead of trying to convince people about the JavaFX platform, I’d rather demonstrate things with useful examples. I find it counter productive discussing deep philosophical debates regarding why one particular technology is superior to the other (cheesy 80′s Highlander reference).
After the release of JavaFX 2.0, there came subsequent versions such as JavaFX 2.1, 2.2 and the upcoming release of JavaFX 8 (January 2014). In this blog entry, I will provide a recipe for the JavaFX 8′s Printing API. Similar to my book (JIBE), I will follow the same pattern as before where I present a problem, solution, code, and a “How it Works” section.
Declaimer: In this blog you will encounter Java Functional Interfaces using Lambda expressions. I will not be discussing them here, but will refer you to Oracle’s tutorials on Project Lambda .
Prerequisite software:
JDK 8 – https://jdk8.java.net/download.html
Problem
You want to create a JavaFX application that prints out a visited website.
Solution
Use the JavaFX 8 PrintJob and Printer APIs to print any JavaFX scene graph node. Also, use the WebView and WebEngine APIs to display a Website or Web page.
Instructions
Assuming you’ve compiled and have run the application, follow the instruction below:
- Enter website address or url into the text field.
- Hit the enter key
- After the page is loaded, click on the “Print” button
- Go to the printer to get the printed web page
Code
package org.carlfx; import javafx.application.Application; import javafx.beans.property.BooleanProperty; import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.value.ChangeListener; import javafx.concurrent.Worker.State; import javafx.print.*; import javafx.scene.Node; import javafx.scene.Scene; import javafx.scene.control.Button; import javafx.scene.control.TextField; import javafx.scene.layout.BorderPane; import javafx.scene.layout.HBox; import javafx.scene.transform.Scale; import javafx.scene.web.WebEngine; import javafx.scene.web.WebView; import javafx.stage.Stage; /** * Demo to use JavaFX 8 Printer API. * * @author cdea */ public class PrintDemo extends Application { @Override public void start(Stage primaryStage) { final TextField urlTextField = new TextField(); final Button printButton = new Button("Print"); final WebView webPage = new WebView(); final WebEngine webEngine = webPage.getEngine(); HBox hbox = new HBox(); hbox.getChildren().addAll(urlTextField, printButton); BorderPane borderPane = new BorderPane(); borderPane.setTop(hbox); borderPane.setCenter(webPage); Scene scene = new Scene(borderPane, 300, 250); primaryStage.setTitle("Print Demo"); primaryStage.setScene(scene); // print button pressed, page loaded final BooleanProperty printButtonClickedProperty = new SimpleBooleanProperty(false); final BooleanProperty pageLoadedProperty = new SimpleBooleanProperty(false); // when the a page is loaded and the button was pressed call the print() method. final BooleanProperty printActionProperty = new SimpleBooleanProperty(false); printActionProperty.bind(pageLoadedProperty.and(printButtonClickedProperty)); // WebEngine updates flag when finished loading web page. webEngine.getLoadWorker() .stateProperty() .addListener( (ChangeListener) (obsValue, oldState, newState) -> { if (newState == State.SUCCEEDED) { pageLoadedProperty.set(true); } }); // When user enters a url and hits the enter key. urlTextField.setOnAction( aEvent -> { pageLoadedProperty.set(false); printButtonClickedProperty.set(false); webEngine.load(urlTextField.getText()); }); // When the user clicks the print button the webview node is printed printButton.setOnAction( aEvent -> { printButtonClickedProperty.set(true); }); // Once the print action hears a true go print the WebView node. printActionProperty.addListener( (ChangeListener) (obsValue, oldState, newState) -> { if (newState) { print(webPage); } }); primaryStage.show(); } /** Scales the node based on the standard letter, portrait paper to be printed. * @param node The scene node to be printed. */ public void print(final Node node) { Printer printer = Printer.getDefaultPrinter(); PageLayout pageLayout = printer.createPageLayout(Paper.NA_LETTER, PageOrientation.PORTRAIT, Printer.MarginType.DEFAULT); double scaleX = pageLayout.getPrintableWidth() / node.getBoundsInParent().getWidth(); double scaleY = pageLayout.getPrintableHeight() / node.getBoundsInParent().getHeight(); node.getTransforms().add(new Scale(scaleX, scaleY)); PrinterJob job = PrinterJob.createPrinterJob(); if (job != null) { boolean success = job.printPage(node); if (success) { job.endJob(); } } } /** * The main() method is ignored in correctly deployed JavaFX application. * main() serves only as fallback in case the application can not be * launched through deployment artifacts, e.g., in IDEs with limited FX * support. NetBeans ignores main(). * * @param args the command line arguments */ public static void main(String[] args) { launch(args); } }
How it Works
The code begins by creating a TextField, a Button, and a WebView control to be placed into a BorderPane. When using the BorderPane layout, you will be able to place controls into the following regions: Top, Right, Left, Bottom, and Center.
Similar to a web browser, the text field allows the user to enter a website url. Once the url is entered, the user will hit the enter key to load the web page into the WebView node. When placing controls on any of the side regions, the BorderPane layout will take on the preferred height of any controls that are added. The center region will allow a node to take up the available space minus the remaining space taken up by the width and height of the bordering side regions. In other words, if the side regions contain no nodes (empty), a node in the center region has the opportunity to take all the available width and height space provided by its parent (Scene). Since the WebView node will occupy the center region, it will take all the available width and height (minus top region) when the web page is fully loaded. You’ll also notice scroll bars allowing the user to view pages larger than the current view port.
After laying out all the components for the UI, you will need to wire things up. Here you will simply create three boolean property
(javafx.beans.property.SimpleBooleanProperty) instances. The first property variable printButtonClickedProperty is a flag indicating when the print button was clicked. The second property pageLoadedProperty is a flag indicating that the web page was finished loading. Lastly, you will want to note the printActionProperty which binds the printButtonClickedProperty and the pageLoadedProperty by using the fluent API. As they evaluate, the printActionProperty will be true if both the printLoadedProperty and the printLoadedProperty are true values.
// print button pressed, page loaded final BooleanProperty printButtonClickedProperty = new SimpleBooleanProperty(false); final BooleanProperty pageLoadedProperty = new SimpleBooleanProperty(false); // when the a page is loaded and the button was pressed call the print() method. final BooleanProperty printActionProperty = new SimpleBooleanProperty(false); printActionProperty.bind(pageLoadedProperty.and(printButtonClickedProperty));
Continuing the wiring up of the UI, I took an event driven approach where the handler code will respond to events and property changes. Starting with the WebView node, I attached handler code to the statePropery instance (ChangeListener) in order that the pageLoadedProperty will be set to true once the web page is loaded successfully.
// WebEngine updates flag when finished loading web page. webEngine.getLoadWorker() .stateProperty() .addListener( (ChangeListener) (obsValue, oldState, newState) -> { if (newState == State.SUCCEEDED) { pageLoadedProperty.set(true); } });
Next, you will see the text field’s ‘setOnAction‘ method containing handler code that resets the pageLoadedProperty and printButtonClickedProperty objects. Also, the code will initiate the loading of the page via the WebView‘s WebEngine load() method.
// When user enters a url and hits the enter key. urlTextField.setOnAction( aEvent -> { pageLoadedProperty.set(false); printButtonClickedProperty.set(false); webEngine.load(urlTextField.getText()); });
After the TextField control’s action code is wired up, the print button will also need handler code to set the printButtonClickedProperty flag to true. Lastly, the printActionProperty property will need a ChangeListener to respond when its state evaluates to true. When this evaluates to true, my print() method is invoked.
// When the user clicks the print button the webview node is printed printButton.setOnAction( aEvent -> { printButtonClickedProperty.set(true); }); // Once the print action hears a true go print the WebView node. printActionProperty.addListener( (ChangeListener) (obsValue, oldState, newState) -> { if (newState) { print(webPage); } });
Finally, the print() method takes a JavaFX Node object to be printed. The Printer object has a method which returns the default printer your computer is set to. Before actually printing, we can derive a default page layout to scale the node before printing the node. If you don’t do this, only part of the web page will be printed. With the default printer obtained, the createPrinterJob() method is invoked to return a PrinterJob instance that does the actual printing. To print a JavaFX displayable type node, you simply invoke the PrinterJob object’s printPage() method by passing in the Node instance as a parameter.
/** Scales the node based on the standard letter, portrait paper to be printed. * @param node The scene node to be printed. */ public void print(final Node node) { Printer printer = Printer.getDefaultPrinter(); PageLayout pageLayout = printer.createPageLayout(Paper.NA_LETTER, PageOrientation.PORTRAIT, Printer.MarginType.DEFAULT); double scaleX = pageLayout.getPrintableWidth() / node.getBoundsInParent().getWidth(); double scaleY = pageLayout.getPrintableHeight() / node.getBoundsInParent().getHeight(); node.getTransforms().add(new Scale(scaleX, scaleY)); PrinterJob job = PrinterJob.createPrinterJob(); if (job != null) { boolean success = job.printPage(node); if (success) { job.endJob(); } } }
In conclusion, I find that the APIs are simpler to use compared to Java Swing/AWT APIs. I would like to mention that there are many features you can play around with since this blog entry only scratches the surface on the APIs currently available.
NOTE: JavaFX 8 printer API is still in the early stages and there are still outstanding issues (Jira issues).