Graph Printing Example

The Graph Printing example demonstrates how to print or export to PDF 2D and 3D graphs.

The printing functionality is implemented in the GraphPrinter class whose slots are invoked from QML, passing an image obtained from QuickItem.grabToImage() . The image is scaled and painted onto either a QPrinter or a QPdfWriter, which inherit QPaintDevice.

Graph Printing example

Download this example

# Copyright (C) 2026 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause

import sys
from pathlib import Path

from PySide6.QtCore import QCoreApplication, QSize, Qt
from PySide6.QtGui import QGuiApplication, QShortcut, QKeySequence
from PySide6.QtQuick import QQuickView

from graphprinter import GraphPrinter


if __name__ == "__main__":
    app = QGuiApplication(sys.argv)

    viewer = QQuickView()
    viewer.setTitle("Graph Printing")
    graphPrinter = GraphPrinter()
    viewer.rootContext().setContextProperty("graphPrinter", graphPrinter)
    viewer.setMinimumSize(QSize(1280, 720))
    viewer.engine().addImportPath(Path(__file__).parent)
    viewer.loadFromModule("GraphPrintingExample", "Main")
    window = viewer.rootObject()
    if not window:
        sys.exit(-1)
    quitKey = QKeySequence(QKeySequence.StandardKey.Quit)
    if not quitKey.isEmpty():
        quitShortcut = QShortcut(quitKey, window)
        quitShortcut.activated.connect(app.quit)
        quitShortcut.setContext(Qt.ShortcutContext.ApplicationShortcut)
    viewer.setResizeMode(QQuickView.ResizeMode.SizeRootObjectToView)
    viewer.setColor(Qt.GlobalColor.white)
    viewer.show()

    ex = QCoreApplication.exec()
    del viewer
    sys.exit(ex)
# Copyright (C) 2026 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause

from PySide6.QtCore import QObject, QMarginsF, QUrl, Qt, Property, Slot
from PySide6.QtGui import (QDesktopServices, QImage, QPageSize, QPainter, QPaintDevice,
                           QPdfWriter)
from PySide6.QtQml import QmlElement
from PySide6.QtPrintSupport import QPrinter, QPrinterInfo

QML_IMPORT_NAME = "GraphPrintingExample"
QML_IMPORT_MAJOR_VERSION = 1


@QmlElement
class GraphPrinter(QObject):

    def __init__(self, parent=None):
        super().__init__(parent)

    @Property(int, constant=True)
    def maxTextureSize(self):
        return 4096  # Use 4096 as the minimum

    def paintImage(self, image: QImage, device: QPaintDevice):
        painter = QPainter(device)
        viewportSize = painter.viewport().size()
        imageSize = image.size()
        print(f"Scaling {imageSize.width()}x{imageSize.height()} to "
              f"{viewportSize.width()}x{viewportSize.height()}.")
        finalImage = image.scaled(viewportSize, Qt.AspectRatioMode.KeepAspectRatio)
        painter.setRenderHint(QPainter.RenderHint.LosslessImageRendering)
        painter.drawImage(finalImage.rect(), finalImage)
        painter.end()

    def _generatePDF(self, fileName: str, image: QImage):
        writer = QPdfWriter(fileName)
        writer.setResolution(90)
        writer.setTitle("Graph")
        writer.setPageSize(QPageSize(image.size()))
        writer.setPageMargins(QMarginsF(0, 0, 0, 0))
        writer.newPage()
        self.paintImage(image, writer)

    @Slot(QUrl, QImage, result=str)
    def generatePDF(self, path: QUrl, image: QImage):
        fileName = path.toLocalFile()
        self._generatePDF(fileName, image)

        QDesktopServices.openUrl(path)

        return fileName

    @Slot(QImage, str, result=str)
    def print(self, image: QImage, printerName: str):
        printInfo = QPrinterInfo.printerInfo(printerName)
        if printInfo.isNull():
            return f"{printerName} is not a valid printer"

        printer = QPrinter(printInfo, QPrinter.PrinterMode.HighResolution)
        printer.setOutputFormat(QPrinter.OutputFormat.NativeFormat)
        self.paintImage(image, printer)

        return f"Printed to {printerName}"

    @Slot(result="QStringList")
    def getPrinters(self):
        return QPrinterInfo.availablePrinterNames()
module GraphPrintingExample
Main 1.0 Main.qml
Graph2D 1.0 Graph2D.qml
Graph3D 1.0 Graph3D.qml
// Copyright (C) 2026 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause

import QtCore
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls.Fusion
import QtQuick.Dialogs

Rectangle {
    id: mainView
    width: 1280
    height: 720
    color: Application.styleHints.colorScheme === Qt.Dark ? "darkgray" : "lightgray"

    property var item: stackLayout.itemAt(stackLayout.currentIndex)
    property var outputsize: Qt.size(linegraph.width * 4, linegraph.height * 4)

    RowLayout {
        id: rowLayout
        anchors.fill: parent
        anchors.leftMargin: 5
        anchors.rightMargin: 5
        anchors.topMargin: 5
        anchors.bottomMargin: 5
        spacing: 5

        GroupBox {
            id: groupBox
            Layout.alignment: Qt.AlignLeft | Qt.AlignTop
            title: qsTr("Printing and exporting")

            ColumnLayout {
                id: buttonLayout
                spacing: 0
                uniformCellSizes: true
                Button {
                    id: captureButton
                    text: qsTr("Save to PDF")
                    flat: true

                    icon.source: pressed ? "documents_fill.svg" : "documents.svg"
                    icon.height: 36
                    icon.width: 36

                    onPressed: dialog.open()
                }

                Button {
                    id: printButton
                    text: qsTr("Send to printer")
                    flat: true

                    icon.source: pressed ? "print_fill.svg" : "print.svg"
                    icon.height: 36
                    icon.width: 36

                    onPressed: printerDialog.open()
                }
            }
        }

        Item {
            id: tabGroup
            Layout.fillHeight: true
            Layout.fillWidth: true

            TabBar {
                id: tabBar
                anchors.left: parent.left
                anchors.right: parent.right

                TabButton {
                    text: "2D Graph"
                    implicitHeight: 48
                    icon.source: checked ? "flatten_square_fill.svg" : "flatten.svg"
                    icon.height: 36
                    icon.width: 36
                }

                TabButton {
                    text: "3D Graph"
                    implicitHeight: 48
                    icon.source: checked ? "box_left_fill.svg" : "box_left.svg"
                    icon.height: 36
                    icon.width: 36
                }
            }
            Frame {
                id: tabFrame
                anchors.left: parent.left
                anchors.right: parent.right
                anchors.top: tabBar.bottom
                anchors.bottom: parent.bottom

                StackLayout {
                    id: stackLayout

                    anchors.fill: parent
                    currentIndex: tabBar.currentIndex

                    Graph2D {
                        id: linegraph
                    }

                    Graph3D {
                        id: bargraph
                    }
                }
            }
        }
    }

    MessageDialog {
        id: message
        onButtonClicked: mainView.cleanAfterPrint()
    }

    FileDialog {
        id: dialog
        currentFolder: StandardPaths.writableLocation(StandardPaths.PicturesLocation)
        nameFilters: ["PDF files (*.pdf)"]
        defaultSuffix: "pdf"

        fileMode: FileDialog.SaveFile
        onAccepted: {
            mainView.prepareForPrint()

            mainView.item.grabToImage(function (result) {
                message.title = "Save PDF"
                message.text = "PDF saved to " + graphPrinter.generatePDF(
                            dialog.selectedFile, result.image)
                message.open()
            }, mainView.outputsize)
        }
    }

    Dialog {
        id: printerDialog
        anchors.centerIn: parent
        contentHeight: printerListView.height
        contentWidth: printerListView.width

        title: qsTr("Available Printers")
        modal: true

        onOpened: {
            printerModel.clear()
            var printers = graphPrinter.getPrinters()
            printers.forEach((x, i) => printerModel.append({
                                                               "name": x
                                                           }))
        }

        onAccepted: {
            var selectedPrinter = printerModel.get(printerListView.currentIndex)
            mainView.prepareForPrint()
            mainView.item.grabToImage(function (result) {
                message.title = "Print"
                message.text = graphPrinter.print(result.image,
                                                  selectedPrinter.name)
                message.open()
            }, mainView.outputsize)
        }

        onClosed: {
            mainView.cleanAfterPrint()
        }

        Component {
            id: printerDelegate
            Rectangle {
                width: 198
                height: 25
                color: "transparent"
                border.color: mainView.item.theme.grid.mainColor
                clip: true

                Text {
                    padding: 5
                    text: qsTr("<b>%1</b>").arg(name)
                    color: mainView.item.theme.labelTextColor
                }

                MouseArea {
                    anchors.fill: parent
                    onClicked: printerListView.currentIndex = index
                }
            }
        }

        contentItem: Rectangle {
            id: printerItem
            height: printerListView.height
            width: printerListView.width
            color: mainView.item.theme.plotAreaBackgroundColor

            ListView {
                id: printerListView
                height: 100
                width: 200
                clip: true

                model: printerModel
                delegate: printerDelegate
                highlight: Rectangle {
                    color: mainView.item.theme.grid.subColor
                }
            }
        }

        footer: DialogButtonBox {
            Button {
                text: "Print"
                DialogButtonBox.buttonRole: DialogButtonBox.AcceptRole
            }
            Button {
                text: "Cancel"
                DialogButtonBox.buttonRole: DialogButtonBox.RejectRole
            }
        }
    }

    ListModel {
        id: printerModel
    }

    function prepareForPrint() {
        if (stackLayout.currentIndex === 1) {
            var newsize = Qt.size(bargraph.width * 4, bargraph.height * 4)

            // check that we do not exceed maximum texture size
            if (newsize.width * Screen.devicePixelRatio > graphPrinter.maxTextureSize ) {
                // scale to 25% under max texture size to be on the safe side; some GPUs seem
                // to glitch when using the abosulute max
                var ratio = (newsize.width * Screen.devicePixelRatio * 1.25)
                        / graphPrinter.maxTextureSize
                newsize.width /= ratio
                newsize.height /= ratio
            }
            outputsize.width = Math.round(newsize.width)
            outputsize.height = Math.round(newsize.height)

            // resize the bar graph to match the PDF output size
            item.width = outputsize.width
            item.height = outputsize.height
        } else {
            outputsize = Qt.size(linegraph.width * 4, linegraph.height * 4)
        }
    }

    function cleanAfterPrint() {
        if (stackLayout.currentIndex === 1) {
            // resize the bar graph back to the actual visual size
            item.width = stackLayout.width
            item.height = stackLayout.height
        }
    }
}
// Copyright (C) 2026 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause

import QtQuick
import QtGraphs

Rectangle {
    id: graphContainer
    width: 1280
    height: 720
    property alias theme: lines.theme

    color: "white"

    GraphsView {
        id: lines
        anchors.fill: parent
        anchors.margins: 16
        theme: GraphsTheme {
            grid.mainColor: "darkgrey"
            grid.subColor: "lightgrey"
            labelTextColor: "black"
            plotAreaBackgroundColor: "white"
            backgroundColor: "white"
            colorScheme: Qt.Light
        }
        axisX: ValueAxis {
            max: 5
            tickInterval: 1
            subTickCount: 9
            labelDecimals: 1
        }
        axisY: ValueAxis {
            max: 10
            tickInterval: 1
            subTickCount: 4
            labelDecimals: 1
        }

        component Marker : Rectangle {
            width: 8
            height: 8
            color: "#ffffff"
            radius: width * 0.5
            border.width: 4
            border.color: "#000000"
        }

        LineSeries {
            id: lineSeries1
            width: 4
            pointDelegate: Marker { }
            color: "black"
            XYPoint { x: 0; y: 0 }
            XYPoint { x: 1; y: 2.1 }
            XYPoint { x: 2; y: 3.3 }
            XYPoint { x: 3; y: 2.1 }
            XYPoint { x: 4; y: 4.9 }
            XYPoint { x: 5; y: 3.0 }
        }

        LineSeries {
            id: lineSeries2
            width: 4
            pointDelegate: Marker { }
            color: "black"
            XYPoint { x: 0; y: 5.0 }
            XYPoint { x: 1; y: 3.3 }
            XYPoint { x: 2; y: 7.1 }
            XYPoint { x: 3; y: 7.5 }
            XYPoint { x: 4; y: 6.1 }
            XYPoint { x: 5; y: 3.2 }
        }
    }
}
// Copyright (C) 2026 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause

import QtQuick
import QtGraphs

Item {
    id: graphContainer
    width: 1280
    height: 720
    property alias theme: bars.theme

    Bars3D {
        id: bars
        anchors.fill: parent
        msaaSamples: 8
        cameraPreset: Graphs3D.CameraPreset.IsometricLeftHigh

        theme: GraphsTheme {
            backgroundColor: "white"
            plotAreaBackgroundVisible: false
            grid.mainColor: "black"
            labelFont.pointSize: 20
            labelBackgroundVisible: false
            colorScheme: Qt.Light
        }

        Bar3DSeries {
            id: series
            itemLabelFormat: "Expenses, @colLabel, @rowLabel: -@valueLabel"
            baseGradient: gradient
            colorStyle: GraphsTheme.ColorStyle.RangeGradient

            ItemModelBarDataProxy {
                id: barProxy
                itemModel: ListModel {
                    ListElement{ coords: "0,0"; data: "4.75"; }
                    ListElement{ coords: "1,0"; data: "3.00"; }
                    ListElement{ coords: "0,1"; data: "3.55"; }
                    ListElement{ coords: "1,1"; data: "3.03"; }
                    ListElement{ coords: "0,2"; data: "3.37"; }
                    ListElement{ coords: "1,2"; data: "2.98"; }
                    ListElement{ coords: "0,3"; data: "5.34"; }
                    ListElement{ coords: "1,3"; data: "4.54"; }
                    ListElement{ coords: "0,4"; data: "6.01"; }
                    ListElement{ coords: "1,4"; data: "5.83"; }
                }
                rowRole: "coords"
                columnRole: "coords"
                valueRole: "data"
                rowRolePattern: /(\d),(\d)/
                columnRolePattern: /(\d),(\d)/
                rowRoleReplace: "\\1"
                columnRoleReplace: "\\2"
            }

            Gradient {
                id: gradient
                GradientStop { position: 1.0; color: "#5000FF" }
                GradientStop { position: 0.0; color: "#2000FF" }
            }
        }
    }
}
// Copyright (C) 2026 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause

import QtCore
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls.Fusion
import QtQuick.Dialogs

Rectangle {
    id: mainView
    width: 1280
    height: 720
    color: Application.styleHints.colorScheme === Qt.Dark ? "darkgray" : "lightgray"

    property var item: stackLayout.itemAt(stackLayout.currentIndex)
    property var outputsize: Qt.size(linegraph.width * 4, linegraph.height * 4)

    RowLayout {
        id: rowLayout
        anchors.fill: parent
        anchors.leftMargin: 5
        anchors.rightMargin: 5
        anchors.topMargin: 5
        anchors.bottomMargin: 5
        spacing: 5

        GroupBox {
            id: groupBox
            Layout.alignment: Qt.AlignLeft | Qt.AlignTop
            title: qsTr("Printing and exporting")

            ColumnLayout {
                id: buttonLayout
                spacing: 0
                uniformCellSizes: true
                Button {
                    id: captureButton
                    text: qsTr("Save to PDF")
                    flat: true

                    icon.source: pressed ? "documents_fill.svg" : "documents.svg"
                    icon.height: 36
                    icon.width: 36

                    onPressed: dialog.open()
                }

                Button {
                    id: printButton
                    text: qsTr("Send to printer")
                    flat: true

                    icon.source: pressed ? "print_fill.svg" : "print.svg"
                    icon.height: 36
                    icon.width: 36

                    onPressed: printerDialog.open()
                }
            }
        }

        Item {
            id: tabGroup
            Layout.fillHeight: true
            Layout.fillWidth: true

            TabBar {
                id: tabBar
                anchors.left: parent.left
                anchors.right: parent.right

                TabButton {
                    text: "2D Graph"
                    implicitHeight: 48
                    icon.source: checked ? "flatten_square_fill.svg" : "flatten.svg"
                    icon.height: 36
                    icon.width: 36
                }

                TabButton {
                    text: "3D Graph"
                    implicitHeight: 48
                    icon.source: checked ? "box_left_fill.svg" : "box_left.svg"
                    icon.height: 36
                    icon.width: 36
                }
            }
            Frame {
                id: tabFrame
                anchors.left: parent.left
                anchors.right: parent.right
                anchors.top: tabBar.bottom
                anchors.bottom: parent.bottom

                StackLayout {
                    id: stackLayout

                    anchors.fill: parent
                    currentIndex: tabBar.currentIndex

                    Graph2D {
                        id: linegraph
                    }

                    Graph3D {
                        id: bargraph
                    }
                }
            }
        }
    }

    MessageDialog {
        id: message
        onButtonClicked: mainView.cleanAfterPrint()
    }

    FileDialog {
        id: dialog
        currentFolder: StandardPaths.writableLocation(StandardPaths.PicturesLocation)
        nameFilters: ["PDF files (*.pdf)"]
        defaultSuffix: "pdf"

        fileMode: FileDialog.SaveFile
        onAccepted: {
            mainView.prepareForPrint()

            mainView.item.grabToImage(function (result) {
                message.title = "Save PDF"
                message.text = "PDF saved to " + graphPrinter.generatePDF(
                            dialog.selectedFile, result.image)
                message.open()
            }, mainView.outputsize)
        }
    }

    Dialog {
        id: printerDialog
        anchors.centerIn: parent
        contentHeight: printerListView.height
        contentWidth: printerListView.width

        title: qsTr("Available Printers")
        modal: true

        onOpened: {
            printerModel.clear()
            var printers = graphPrinter.getPrinters()
            printers.forEach((x, i) => printerModel.append({
                                                               "name": x
                                                           }))
        }

        onAccepted: {
            var selectedPrinter = printerModel.get(printerListView.currentIndex)
            mainView.prepareForPrint()
            mainView.item.grabToImage(function (result) {
                message.title = "Print"
                message.text = graphPrinter.print(result.image,
                                                  selectedPrinter.name)
                message.open()
            }, mainView.outputsize)
        }

        onClosed: {
            mainView.cleanAfterPrint()
        }

        Component {
            id: printerDelegate
            Rectangle {
                width: 198
                height: 25
                color: "transparent"
                border.color: mainView.item.theme.grid.mainColor
                clip: true

                Text {
                    padding: 5
                    text: qsTr("<b>%1</b>").arg(name)
                    color: mainView.item.theme.labelTextColor
                }

                MouseArea {
                    anchors.fill: parent
                    onClicked: printerListView.currentIndex = index
                }
            }
        }

        contentItem: Rectangle {
            id: printerItem
            height: printerListView.height
            width: printerListView.width
            color: mainView.item.theme.plotAreaBackgroundColor

            ListView {
                id: printerListView
                height: 100
                width: 200
                clip: true

                model: printerModel
                delegate: printerDelegate
                highlight: Rectangle {
                    color: mainView.item.theme.grid.subColor
                }
            }
        }

        footer: DialogButtonBox {
            Button {
                text: "Print"
                DialogButtonBox.buttonRole: DialogButtonBox.AcceptRole
            }
            Button {
                text: "Cancel"
                DialogButtonBox.buttonRole: DialogButtonBox.RejectRole
            }
        }
    }

    ListModel {
        id: printerModel
    }

    function prepareForPrint() {
        if (stackLayout.currentIndex === 1) {
            var newsize = Qt.size(bargraph.width * 4, bargraph.height * 4)

            // check that we do not exceed maximum texture size
            if (newsize.width * Screen.devicePixelRatio > graphPrinter.maxTextureSize ) {
                // scale to 25% under max texture size to be on the safe side; some GPUs seem
                // to glitch when using the abosulute max
                var ratio = (newsize.width * Screen.devicePixelRatio * 1.25)
                        / graphPrinter.maxTextureSize
                newsize.width /= ratio
                newsize.height /= ratio
            }
            outputsize.width = Math.round(newsize.width)
            outputsize.height = Math.round(newsize.height)

            // resize the bar graph to match the PDF output size
            item.width = outputsize.width
            item.height = outputsize.height
        } else {
            outputsize = Qt.size(linegraph.width * 4, linegraph.height * 4)
        }
    }

    function cleanAfterPrint() {
        if (stackLayout.currentIndex === 1) {
            // resize the bar graph back to the actual visual size
            item.width = stackLayout.width
            item.height = stackLayout.height
        }
    }
}