Reputation: 6574
I tried to get in touch with the new printing API of JavaFX that was introduced in JDK 8.
Consider the following test program:
import javafx.application.Application;
import javafx.print.PrinterJob;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.ToolBar;
import javafx.scene.layout.BorderPane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
public class SimplePrintingTest extends Application {
private PrinterJob job = PrinterJob.createPrinterJob();
public static void main(String[] args) {
launch(args);
}
@Override
public void start(Stage primaryStage) {
BorderPane pane = new BorderPane();
final Rectangle rect = new Rectangle(0, 0, 1000, 1000);
pane.setCenter(rect);
final ToolBar value = new ToolBar();
final Button print = new Button("print");
final Button dialog = new Button("print dialog");
final Button pageLayout = new Button("page layout settings");
value.getItems().add(print);
value.getItems().add(dialog);
value.getItems().add(pageLayout);
print.setOnAction(event -> print(pane));
dialog.setOnAction(event -> showPrintDialog(primaryStage));
pageLayout.setOnAction(event -> showPageSetupDialog(primaryStage));
pane.setTop(value);
Scene scene = new Scene(pane, 1200, 1024, Color.GRAY);
primaryStage.setScene(scene);
primaryStage.show();
}
public void print(Node node) {
if (job != null) {
// -- ???
boolean success = job.printPage(node);
if (success) {
job.endJob();
job = PrinterJob.createPrinterJob();
}
}
}
public void showPageSetupDialog(Stage stage) {
if (job != null) {
job.showPageSetupDialog(stage);
}
}
public void showPrintDialog(Stage stage) {
if (job != null) {
job.showPrintDialog(stage);
}
}
}
my question is now: How do I configure or use the printer job to print the content of the scene that is (obviously) too big for one page across multiple pages? I have tried to set the page ranges like this
job.getJobSettings().setPageRanges(new PageRange(1, 5));
or this
job.getJobSettings().setPageRanges(new PageRange(1, 1), new PageRange(2, 2));
or change the page range between to printPage calls like this
job.getJobSettings().setPageRanges(new PageRange(1, 1));
boolean success = job.printPage(node);
job.getJobSettings().setPageRanges(new PageRange(2, 2));
success &= job.printPage(node);
but nothing seems to work. Always only the left half of the content is visible on the printed document for each time I call printPage. To be clear: I don't want to scale down the node that is printed to fit onto one page, I want to keep the size of the node and print it completely over mutiple pages. This was possible in Swing. Is it not possible anymore in JavaFX?
Upvotes: 4
Views: 8307
Reputation: 6574
Okay so basically I used transformations to position the node in a way I want for each page that is to be printed.
First, the class NodePrinter that does the actual printing:
import javafx.print.PageLayout;
import javafx.print.PrinterJob;
import javafx.scene.Node;
import javafx.scene.shape.Rectangle;
import javafx.scene.transform.Scale;
import javafx.scene.transform.Transform;
import javafx.scene.transform.Translate;
import javafx.stage.Window;
import java.util.ArrayList;
import java.util.List;
/**
* Prints any given area of a node to multiple pages
*/
public class NodePrinter {
private static final double SCREEN_TO_PRINT_DPI = 72d / 96d;
private double scale = 1.0f;
/**
* This rectangle determines the portion to print in the world coordinate system.
*/
private Rectangle printRectangle;
/**
* Prints the given node.
* @param job The printer job which has the configurations for the page layout etc. and does the actual printing.
* @param showPrintDialog Whether or not the print dialog needs to be shown prior to printing.
* @param node The content to print.
* @return <code>true</code> if everything was printed, <code>false</code> otherwise
*/
public boolean print(PrinterJob job, boolean showPrintDialog, Node node) {
// bring up the print dialog in which the user can choose the printer etc.
Window window = node.getScene() != null ? node.getScene().getWindow() : null;
if (!showPrintDialog || job.showPrintDialog(window)) {
PageLayout pageLayout = job.getJobSettings().getPageLayout();
double pageWidth = pageLayout.getPrintableWidth();
double pageHeight = pageLayout.getPrintableHeight();
PrintInfo printInfo = getPrintInfo(pageLayout);
double printRectX = this.printRectangle.getX();
double printRectY = this.printRectangle.getY();
double printRectWith = this.printRectangle.getWidth();
double printRectHeight = this.printRectangle.getHeight();
// the following is suboptimal in many ways but needed for the sake of demonstration.
// there need to be transformations made on the node so we store them and restore them later.
// this is bad when the node is embedded somewhere in the scene graph because the size changes
// will trigger updates and at least lead to "flickering".
// in a real world application there should be another way to construct a node object
// specifically for printing.
// store old transformations and clip of the node
Node oldClip = node.getClip();
List<Transform> oldTransforms = new ArrayList<>(node.getTransforms());
// set the printingRectangle bounds as clip
node.setClip(new javafx.scene.shape.Rectangle(printRectX, printRectY,
printRectWith, printRectHeight));
int columns = printInfo.getColumnCount();
int rows = printInfo.getRowCount();
// by adjusting the scale, you can force the contents to be printed one page for example
double localScale = printInfo.getScale();
node.getTransforms().add(new Scale(localScale, localScale));
// move to 0,0
node.getTransforms().add(new Translate(-printRectX, -printRectY));
// the transform that moves the node to fit the current printed page in the grid
Translate gridTransform = new Translate();
node.getTransforms().add(gridTransform);
// for each page, move the node into position by adjusting the transform
// and call the print page method of the PrinterJob
boolean success = true;
for (int row = 0; row < rows; row++) {
for (int col = 0; col < columns; col++) {
gridTransform.setX(-col * pageWidth / localScale);
gridTransform.setY(-row * pageHeight / localScale);
success &= job.printPage(pageLayout, node);
}
}
// restore the original transformation and clip values
node.getTransforms().clear();
node.getTransforms().addAll(oldTransforms);
node.setClip(oldClip);
return success;
}
return false;
}
/**
* Returns a scale factor to apply for printing.
* A value of <code>0.72</code> makes <code>96</code> units in the world coordinate system appear exactly one inch long.
* The default value is <code>1.0</code>.
*/
public double getScale() {
return scale;
}
/**
* Sets a scale factor to apply for printing.
* A value of <code>0.72</code> makes <code>96</code> units in the world coordinate system appear exactly one inch long.
* The default value is <code>1.0</code>.
*/
public void setScale(final double scale) {
this.scale = scale;
}
/**
* Returns the rectangle that will be printed.
* This rectangle determines the portion of the node to print in the world coordinate system.
* @return a rectangle in the world coordinate system that defines the area of the contents of the
* node to print.
*/
public Rectangle getPrintRectangle() {
return printRectangle;
}
/**
* Sets the rectangle that will be printed.
* This rectangle determines the portion of the node to print in the world coordinate system.
* @param printRectangle a rectangle in the world coordinate system that defines the area of the contents of the
* node to print.
*/
public void setPrintRectangle(final Rectangle printRectangle) {
this.printRectangle = printRectangle;
}
/**
* Determines the scale and the number of rows and columns needed to print the determined contents of the component
* @param pageLayout the {@link javafx.print.PageLayout} that defines the printable area of a page.
* @return a PrintInfo instance that encapsulates the computed values for scale, number of rows and columns.
*/
public PrintInfo getPrintInfo(final PageLayout pageLayout) {
double contentWidth = pageLayout.getPrintableWidth();
double contentHeight = pageLayout.getPrintableHeight();
double localScale = getScale() * SCREEN_TO_PRINT_DPI;
final Rectangle printRect = getPrintRectangle();
final double width = printRect.getWidth() * localScale;
final double height = printRect.getHeight() * localScale;
// calculate how many pages we need dependent on the size of the content and the page.
int cCount = (int) Math.ceil((width) / contentWidth);
int rCount = (int) Math.ceil((height) / contentHeight);
return new PrintInfo(localScale, rCount, cCount);
}
/**
* Encapsulates information for printing with a specific {@link javafx.print.PageLayout},
* i.e. the scale dependent on the screen DPI as well as the number of rows and columns for poster printing.
*/
public static class PrintInfo {
final double scale;
final int rowCount;
final int columnCount;
/**
* Constructs a new PrintInfo instance.
* @param scale The scale of the content.
* @param rowCount The number of rows that are needed to print the content completely with the {@link javafx.print.PageLayout}.
* @param columnCount The number of columns that are needed to print the content completely with the {@link javafx.print.PageLayout}.
*/
public PrintInfo(final double scale, final int rowCount, final int columnCount) {
this.scale = scale;
this.rowCount = rowCount;
this.columnCount = columnCount;
}
public double getScale() {
return scale;
}
public int getRowCount() {
return rowCount;
}
public int getColumnCount() {
return columnCount;
}
}
}
Here is an example application that uses this class to print some simple nodes:
import javafx.application.Application;
import javafx.event.ActionEvent;
import javafx.print.PrinterJob;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.ToolBar;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
public class PrintTest extends Application {
private NodePrinter printer = new NodePrinter();
private Node nodeToPrint;
private Rectangle printRectangle;
private PrinterJob job;
public static void main(String[] args) {
launch(args);
}
@Override
public void start(Stage primaryStage) {
job = PrinterJob.createPrinterJob();
BorderPane root = new BorderPane();
Group pane = new Group();
pane.getChildren().addAll(getNodeToPrint(), getPrintRectangle());
Button printButton = new Button("Print!");
printButton.setOnAction(this::print);
root.setTop(new ToolBar(printButton));
root.setCenter(pane);
Scene scene = new Scene(root, 1800, 700, Color.GRAY);
primaryStage.setScene(scene);
primaryStage.show();
}
private void print(final ActionEvent actionEvent) {
printer.setScale(3);
printer.setPrintRectangle(getPrintRectangle());
boolean success = printer.print(job, true, getNodeToPrint());
if (success) {
job.endJob();
}
}
private Rectangle getPrintRectangle() {
if (printRectangle == null) {
printRectangle = new Rectangle(600, 500, null);
printRectangle.setStroke(Color.BLACK);
}
return printRectangle;
}
private Node getNodeToPrint() {
if (nodeToPrint == null) {
Group group = new Group();
group.getChildren().addAll(
new Rectangle(200, 100, Color.RED),
new Rectangle(200,100, 200, 100),
new Rectangle(400, 200, 200, 100),
new Rectangle(600, 300, 200, 100),
new Rectangle(800, 400, 200, 100)
);
nodeToPrint = group;
}
return nodeToPrint;
}
}
The black bordered rectangle describes the area that is to printed and is defined in the world coordinate system (that is, viewport of the node content).
And this is the outcome with the scale of the printer set to 3
, printed into a xps file:
Upvotes: 5