JavaFX: printing node across multiple pages

I tried to contact the new JavaFX Print API 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);
    }
  }
}

      

Now my question is, how do I set up or use a printer job to print the contents of a scene that is (obviously) too large for a single page across multiple pages? I tried to set page ranges like this

job.getJobSettings().setPageRanges(new PageRange(1, 5));

      

or

job.getJobSettings().setPageRanges(new PageRange(1, 1), new PageRange(2, 2));

      

or change the page range between calls to printPage such as

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 works. Always only the left half of the content is displayed on the printed document every time I call printPage. To be clear: I don't want to scale the node that is printed to fit one page, I want to keep the size of the node and completely print it across multiple pages. It was possible in Swing. Isn't this not possible in JavaFX?

+3


source to share


1 answer


Ok, so basically I used transforms to position the node in the way that I want for each page that needs to be printed.

First, the NodePrinter class, which 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 to be printed and defined in the world coordinate system (i.e. in the node's content window).

test application picture

And this is the output when the printer scale is set to 3

, printed in the xps file:

the resulting xps file

+4


source







All Articles