JavaFX:跨多个页面打印节点

时间:2014-11-06 17:27:59

标签: java printing javafx-8

我尝试与JDK 8中引入的JavaFX新打印API取得联系。

考虑以下测试程序:

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);
    }
  }
}

我现在的问题是:如何配置或使用打印机作业来打印多个页面中显然对于一个页面来说太大的场景内容? 我试图像这样设置页面范围

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

或者

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

或更改printPage调用之间的页面范围,如下所示

job.getJobSettings().setPageRanges(new PageRange(1, 1));
boolean success = job.printPage(node);
job.getJobSettings().setPageRanges(new PageRange(2, 2));
success &= job.printPage(node);

但似乎没有任何效果。每次调用printPage时,始终只能在打印文档上看到内容的左半部分。要明确:我不想缩小打印的节点以适应一个页面,我想保持节点的大小并将其完全打印在多个页面上。这在Swing中是可能的。在JavaFX中不再可能吗?

1 个答案:

答案 0 :(得分:4)

好的,基本上我使用转换来按照我想要的每个要打印的页面的方式定位节点。

首先,执行实际打印的NodePrinter类:

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;
    }
  }
}

以下是使用此类打印一些简单节点的示例应用程序:

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;
  }
}

黑色边框矩形描述了要打印的区域,并在世界坐标系中定义(即节点内容的视口)。

test application picture

这是将打印机的比例设置为3,打印到xps文件中的结果:

the resulting xps file