diff --git a/CMakeLists.txt b/CMakeLists.txt index 6f75501d..c71a32e8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -12,7 +12,7 @@ project(QtGraphs ) find_package(Qt6 ${PROJECT_VERSION} CONFIG REQUIRED COMPONENTS BuildInternals) -find_package(Qt6 ${PROJECT_VERSION} CONFIG OPTIONAL_COMPONENTS Core Quick Gui Widgets QuickTest QuickWidgets Test Quick3D QuickShapesPrivate) +find_package(Qt6 ${PROJECT_VERSION} CONFIG OPTIONAL_COMPONENTS Core Quick Gui Widgets QuickTest QuickWidgets Test Quick3D QuickShapesPrivate PrintSupport) macro(assertTargets) foreach(qtTarget IN ITEMS ${ARGN}) diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index 497037e6..8d9a8a2f 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -11,4 +11,8 @@ if(QT_FEATURE_graphs_2d) add_subdirectory(graphs/2d) endif() +if(QT_FEATURE_graphs_2d AND QT_FEATURE_graphs_3d AND TARGET Qt6::PrintSupport) + add_subdirectory(graphs/graphprinting) +endif() + qt_examples_build_end() diff --git a/examples/graphs/graphprinting/CMakeLists.txt b/examples/graphs/graphprinting/CMakeLists.txt new file mode 100644 index 00000000..08676bff --- /dev/null +++ b/examples/graphs/graphprinting/CMakeLists.txt @@ -0,0 +1,57 @@ +# Copyright (C) 2024 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +cmake_minimum_required(VERSION 3.16) +project(graphprinting LANGUAGES CXX) + +set(CMAKE_INCLUDE_CURRENT_DIR ON) + +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTORCC ON) +set(CMAKE_AUTOUIC ON) + +if(NOT DEFINED INSTALL_EXAMPLESDIR) + set(INSTALL_EXAMPLESDIR "examples") +endif() + +set(INSTALL_EXAMPLEDIR "${INSTALL_EXAMPLESDIR}") + +find_package(Qt6 COMPONENTS Core) +find_package(Qt6 COMPONENTS Gui) +find_package(Qt6 COMPONENTS Quick) +find_package(Qt6 COMPONENTS Quick3D) +find_package(Qt6 COMPONENTS Graphs) + +qt_add_executable(graphprinting + main.cpp +) +set_target_properties(graphprinting PROPERTIES + WIN32_EXECUTABLE TRUE + MACOSX_BUNDLE TRUE +) + +target_link_libraries(graphprinting PRIVATE + Qt::Core + Qt::Gui + Qt::Quick + Qt::Quick3D + Qt::Graphs + Qt::PrintSupport +) + +qt6_add_qml_module(graphprinting + URI GraphPrintingExample + NO_RESOURCE_TARGET_PATH + SOURCES + graphprinter.h graphprinter.cpp + QML_FILES + qml/graphprinting/main.qml + qml/graphprinting/Graph2D.qml + qml/graphprinting/Graph3D.qml +) + +install(TARGETS graphprinting + RUNTIME DESTINATION "${INSTALL_EXAMPLEDIR}" + BUNDLE DESTINATION "${INSTALL_EXAMPLEDIR}" + LIBRARY DESTINATION "${INSTALL_EXAMPLEDIR}" +) diff --git a/examples/graphs/graphprinting/doc/images/graphprinting-example.png b/examples/graphs/graphprinting/doc/images/graphprinting-example.png new file mode 100644 index 00000000..97aeef8f Binary files /dev/null and b/examples/graphs/graphprinting/doc/images/graphprinting-example.png differ diff --git a/examples/graphs/graphprinting/doc/src/graphprinting.qdoc b/examples/graphs/graphprinting/doc/src/graphprinting.qdoc new file mode 100644 index 00000000..f04d5a72 --- /dev/null +++ b/examples/graphs/graphprinting/doc/src/graphprinting.qdoc @@ -0,0 +1,113 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GFDL-1.3-no-invariants-only + +/*! + \example graphprinting + \meta tags {Graphs, GraphsView, LineSeries, Bars3D} + \examplecategory {Data Visualization} + \title Graph Printing + \ingroup qtgraphs_qmlexamples + \ingroup qtgraphs_qmlexamples_3D + \brief Printing a 2D or 3D Graph to a PDF. + + The \e {Graph Printing} example demonstrates how to print or export to PDF + 2D and 3D graphs. + + \image graphprinting-example.png + + \include examples-run.qdocinc + + \section1 GraphPrinter class + + The printing functionality is implemented in the \c GraphPrinter class. The + class exposes these functions: + + \list + + \li The \c generatePDF function, which works as follows. + + \list + \li Sets up the output PDF file. + + The function instantiates \l QPdfWriter with a "graph.pdf" file + pointed in the specified folder. The function also specifies the + options for the exported PDF file: title, resolution, page size, + and margins. + + \snippet graphprinting/graphprinter.cpp 0 + \li Sets up image processing. + + The function creates a \l QPainter referring to the previously + created \l QPdfWriter. + + To ensure the graph is printed correctly, it is scaled to the + painter's viewport size with the original aspect ratio. + + The painter's rendering hint is set to lossless image rendering. + After this, the function draws the image to the PDF file. + + \snippet graphprinting/graphprinter.cpp 1 + \endlist + \li The \c print function, which works like the \c generatePDF function, + but creates a \l QPainter referring a \QPrinter instance: + + \snippet graphprinting/graphprinter.cpp 2 + + \li The \c getPrinters function returns a list of available printers. + + \snippet graphprinting/graphprinter.cpp 3 + \endlist + + \section1 Application setup + + In addition to the application setup code, the \c {main.cpp} file contains + code that creates a new instance of the \l {GraphPrinter class} and makes + it reachable from the QML code. + + \snippet graphprinting/main.cpp 0 + + \section1 Setting up the layout and image capture + + The 2D and 3D graphs are laid out in a Stacklayout. Users can navigate it + with a TabBar. + + \snippet graphprinting/qml/graphprinting/main.qml 0 + + The FolderDialog component is used to select a folder for saving an + exported file. This component has no visual representation in the + application layout, but its API is accessible from the current QML file. + + The Button invokes a folder dialog. + + \snippet graphprinting/qml/graphprinting/main.qml 1.1 + \dots + \snippet graphprinting/qml/graphprinting/main.qml 1.2 + + A custom printing dialog is created for selecting a printer. + The Dialog retrieves the list of available printers and displays them in a + list view. + + \snippet graphprinting/qml/graphprinting/main.qml 2.1 + \dots + \snippet graphprinting/qml/graphprinting/main.qml 2.2 + + The \uicontrol {Save to PDF} button and \uicontrol {Print} button in the + printing dialog run the following code: + + \list + \li Capture an image using the \c grabToImage method. + The current graph is the Stacklayout's item at the current index. + \li In the \c grabToImage parameters, we specify the callback as the + \c generatePDF or \c print function in the \c GraphPrinter class. + + For the size, the code makes the image render at a 7282 by 4096 + resolution. For 3D graphs, the item must also be expanded + for the duration of printing. + \endlist + + \snippet graphprinting/qml/graphprinting/main.qml 3.1 + \dots + \snippet graphprinting/qml/graphprinting/main.qml 3.2 + + +*/ diff --git a/examples/graphs/graphprinting/graphprinter.cpp b/examples/graphs/graphprinting/graphprinter.cpp new file mode 100644 index 00000000..79aa4cb1 --- /dev/null +++ b/examples/graphs/graphprinting/graphprinter.cpp @@ -0,0 +1,62 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause +#include "graphprinter.h" +#include +#include + +GraphPrinter::GraphPrinter(QObject *parent) + : QObject(parent) +{} + +GraphPrinter::~GraphPrinter() {} + +void GraphPrinter::generatePDF(const QUrl &path, const QImage &image) +{ + //! [0] + const QFile file = QFile(path.toLocalFile() + QStringLiteral("/graph.pdf")); + + QPdfWriter writer(file.fileName()); + writer.setResolution(90); + writer.setTitle("Graph"); + writer.setPageSize(QPageSize(image.size())); + writer.setPageMargins(QMarginsF(0, 0, 0, 0)); + writer.newPage(); + //! [0] + + //! [1] + QPainter painter(&writer); + const QImage finalImage = image.scaled(painter.viewport().size(), Qt::KeepAspectRatio); + painter.setRenderHint(QPainter::LosslessImageRendering); + painter.drawImage(finalImage.rect(), finalImage); + //! [1] + + qInfo("printed PDF to %ls", qUtf16Printable(file.fileName())); +} + +//! [2] +void GraphPrinter::print(const QImage &image, const QString printerName) +{ + QPrinterInfo printInfo = QPrinterInfo::printerInfo(printerName); + if (printInfo.isNull()) { + qWarning("%ls is not a valid printer", qUtf16Printable(printerName)); + return; + } + + QPrinter printer(printInfo, QPrinter::HighResolution); + printer.setOutputFormat(QPrinter::NativeFormat); + + QPainter painter(&printer); + const QImage finalImage = image.scaled(painter.viewport().size(), Qt::KeepAspectRatio); + painter.setRenderHint(QPainter::LosslessImageRendering); + painter.drawImage(finalImage.rect(), finalImage); + + qInfo("printed image with %ls", qUtf16Printable(printerName)); +} +//! [2] + +//! [3] +QStringList GraphPrinter::getPrinters() +{ + return QPrinterInfo::availablePrinterNames(); +} +//! [3] diff --git a/examples/graphs/graphprinting/graphprinter.h b/examples/graphs/graphprinting/graphprinter.h new file mode 100644 index 00000000..e319ab3c --- /dev/null +++ b/examples/graphs/graphprinting/graphprinter.h @@ -0,0 +1,24 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +#ifndef GRAPHPRINTER_H +#define GRAPHPRINTER_H + +#include +#include +#include +#include + +class GraphPrinter : public QObject { + Q_OBJECT + QML_ELEMENT +public: + explicit GraphPrinter(QObject *parent = 0); + ~GraphPrinter() override; + + Q_INVOKABLE void generatePDF(const QUrl &path, const QImage &image); + Q_INVOKABLE void print(const QImage &image, const QString printerName); + Q_INVOKABLE QStringList getPrinters(); +}; + +#endif // GRAPHPRINTER_H diff --git a/examples/graphs/graphprinting/main.cpp b/examples/graphs/graphprinting/main.cpp new file mode 100644 index 00000000..d407b129 --- /dev/null +++ b/examples/graphs/graphprinting/main.cpp @@ -0,0 +1,38 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +#include +#include +#include +#include +#include "graphprinter.h" + +int main(int argc, char *argv[]) { + QGuiApplication app(argc, argv); + + QQuickView viewer; + +// The following are needed to make examples run without having to install the +// module in desktop environments. +#ifdef Q_OS_WIN + QString extraImportPath(QStringLiteral("%1/../../../../%2")); +#else + QString extraImportPath(QStringLiteral("%1/../../../%2")); +#endif + viewer.engine()->addImportPath(extraImportPath.arg( + QGuiApplication::applicationDirPath(), QString::fromLatin1("qml"))); + viewer.setTitle(QStringLiteral("Graph Printing")); + + //! [0] + GraphPrinter graphPrinter; + viewer.rootContext()->setContextProperty("graphPrinter", &graphPrinter); + //! [0] + + viewer.setMinimumSize({1280, 720}); + viewer.setSource(QUrl("qrc:/qml/graphprinting/main.qml")); + viewer.setResizeMode(QQuickView::SizeRootObjectToView); + viewer.setColor("black"); + viewer.show(); + + return app.exec(); +} diff --git a/examples/graphs/graphprinting/qml/graphprinting/Graph2D.qml b/examples/graphs/graphprinting/qml/graphprinting/Graph2D.qml new file mode 100644 index 00000000..94415318 --- /dev/null +++ b/examples/graphs/graphprinting/qml/graphprinting/Graph2D.qml @@ -0,0 +1,80 @@ +// Copyright (C) 2024 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 + + color: "white" + + GraphsView { + anchors.fill: parent + anchors.margins: 16 + theme: GraphsTheme { + grid.mainColor: "darkgrey" + grid.subColor: "lightgrey" + labelTextColor: "black" + plotAreaBackgroundColor: "white" + backgroundColor: "white" + } + axisX: ValueAxis { + max: 5 + tickInterval: 1 + subTickCount: 9 + labelDecimals: 1 + } + axisY: ValueAxis { + max: 10 + tickInterval: 1 + subTickCount: 4 + labelDecimals: 1 + } + //! [linegraph] + + //! [linemarker] + component Marker : Rectangle { + width: 8 + height: 8 + color: "#ffffff" + radius: width * 0.5 + border.width: 4 + border.color: "#000000" + } + //! [linemarker] + + //! [lineseries1] + 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 } + } + //! [lineseries1] + + //! [lineseries2] + 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 } + } + //! [lineseries2] + } + +} diff --git a/examples/graphs/graphprinting/qml/graphprinting/Graph3D.qml b/examples/graphs/graphprinting/qml/graphprinting/Graph3D.qml new file mode 100644 index 00000000..deef4696 --- /dev/null +++ b/examples/graphs/graphprinting/qml/graphprinting/Graph3D.qml @@ -0,0 +1,63 @@ +// Copyright (C) 2024 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 + + Bars3D { + anchors.fill: parent + msaaSamples: 8 + cameraPreset: Graphs3D.CameraPreset.IsometricLeftHigh + + theme: GraphsTheme { + backgroundColor: "white" + plotAreaBackgroundVisible: false + grid.mainColor: "black" + labelFont.pointSize: 20 + labelBackgroundVisible: false + } + + Bar3DSeries { + id: series + itemLabelFormat: "Expenses, @colLabel, @rowLabel: -@valueLabel" + baseGradient: gradient + colorStyle: GraphsTheme.ColorStyle.RangeGradient + + ItemModelBarDataProxy { + id: barProxy + itemModel: ListModel { + ListElement{ coords: "0,0"; data: "20.0/10.0/4.75"; } + ListElement{ coords: "1,0"; data: "21.1/10.3/3.00"; } + ListElement{ coords: "0,1"; data: "20.2/11.2/3.55"; } + ListElement{ coords: "1,1"; data: "21.3/11.5/3.03"; } + ListElement{ coords: "0,2"; data: "20.2/12.3/3.37"; } + ListElement{ coords: "1,2"; data: "21.1/12.4/2.98"; } + ListElement{ coords: "0,3"; data: "20.7/13.3/5.34"; } + ListElement{ coords: "1,3"; data: "21.5/13.2/4.54"; } + ListElement{ coords: "0,4"; data: "20.6/15.0/6.01"; } + ListElement{ coords: "1,4"; data: "21.3/14.6/5.83"; } + } + rowRole: "coords" + columnRole: "coords" + valueRole: "data" + rowRolePattern: /(\d),\d/ + columnRolePattern: /(\d),(\d)/ + valueRolePattern: /^([^\/]*)\/([^\/]*)\/(.*)$/ + rowRoleReplace: "\\1" + columnRoleReplace: "\\2" + valueRoleReplace: "\\3" + } + + Gradient { + id: gradient + GradientStop { position: 1.0; color: "#5000FF" } + GradientStop { position: 0.0; color: "#2000FF" } + } + } + } +} diff --git a/examples/graphs/graphprinting/qml/graphprinting/main.qml b/examples/graphs/graphprinting/qml/graphprinting/main.qml new file mode 100644 index 00000000..fc5a46bd --- /dev/null +++ b/examples/graphs/graphprinting/qml/graphprinting/main.qml @@ -0,0 +1,190 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls.Fusion +import QtQuick.Dialogs + +Item { + id: mainView + width: 1280 + height: 720 + + //! [0] + TabBar { + id: tabBar + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + + TabButton { + text: "2D Graph" + implicitHeight: 50 + } + + TabButton { + text: "3D Graph" + implicitHeight: 50 + } + } + + StackLayout { + id: stackLayout + anchors.top: tabBar.bottom + anchors.bottom: parent.bottom + width: parent.width + currentIndex: tabBar.currentIndex + + Graph2D {} + + Graph3D {} + } + //! [0] + + RowLayout { + id: rowLayout + anchors.bottom: parent.bottom + anchors.left: parent.left + + //! [1.2] + Button { + id: setFolderButton + onClicked: dialog.open() + text: "Set save location" + Layout.margins: 5 + } + //! [1.2] + //! [3.1] + Button { + id: captureButton + text: "Save to PDF" + Layout.margins: 5 + property var item: stackLayout.itemAt(stackLayout.currentIndex) + + onPressed: { + if (stackLayout.currentIndex === 1) { + item.width = 7282 + item.height = 4096 + } + item.grabToImage(function(result) { + graphPrinter.generatePDF(dialog.currentFolder, result.image) + }, Qt.size(7282, 4096)) + } + + onReleased: { + if (stackLayout.currentIndex === 1) { + item.width = mainView.width + item.height = mainView.height + } + } + } + //! [3.1] + + Button { + id: printButton + text: "Send to printer" + Layout.margins: 5 + onPressed: printerDialog.open() + } + } + + //! [1.1] + FolderDialog { + id: dialog + onAccepted: console.log("Saving to " + currentFolder) + } + //! [1.1] + + //! [2.1] + Dialog { + id: printerDialog + x: parent.width * 0.5 - width * 0.5 + y: parent.height * 0.5; + contentHeight: 200 + contentWidth: 200 + + title: qsTr("Available printers") + modal: true + + property var item: stackLayout.itemAt(stackLayout.currentIndex) + + onOpened: { + printerModel.clear() + var printers = graphPrinter.getPrinters() + printers.forEach((x,i) => + printerModel.append({"name": x})) + } + //! [2.1] + + //! [3.2] + onAccepted: { + var selectedPrinter = printerModel.get(printerListView.currentIndex) + if (stackLayout.currentIndex === 1) { + item.width = 7282 + item.height = 4096 + } + item.grabToImage(function(result) { + graphPrinter.print(result.image, selectedPrinter.name) + }, Qt.size(7282, 4096)) + } + onClosed: { + if (stackLayout.currentIndex === 1) { + item.width = mainView.width + item.height = mainView.height + } + } + //! [3.2] + + Component { + id : printerDelegate + Rectangle { + width: 200; height: 40 + color: "transparent" + border.color: "black" + Text { + padding: 5 + text: qsTr("%1").arg(name) + color: "white" + } + + MouseArea { + anchors.fill: parent + onClicked: printerListView.currentIndex = index + } + } + } + + //! [2.2] + contentItem: Item { + id: printerItem + height: 200 + width: parent.width + ListView { + id: printerListView + height: 200 + width: 200 + clip: true + + model: printerModel + delegate: printerDelegate + highlight: Rectangle {color: "darkgrey"} + } + } + //! [2.2] + + footer: DialogButtonBox { + Button { + text: "Print" + DialogButtonBox.buttonRole: DialogButtonBox.AcceptRole + } + standardButtons: Dialog.Cancel + } + } + + ListModel { + id: printerModel + } +} + +