Qt WebEngine을 활용한 웹페이지 PDF 변환

들어가며

바로 이전 게시물의 주제가 파이썬을 이용해 "웹페이지 PDF변환, 인쇄" 예제 입니다.

이번 예제는 같은 주제를 C++로 구현해 보았습니다.

실행시 웹브라우저(X)가 아니라 만든 앱을 이용해 아래와 같이 웹페이지를 표시하고, 이를 PDF 저장 or 인쇄가 가능합니다.

[네이버 페이지 표시]

[PDF로 저장된 모습]

[웹페이지 인쇄 화면]


개발환경 및 배경기술

이 예제의 개발 환경입니다.

  • Qt 5.15.2 (MS Visual Studio 2019 64bit)
  • Qt Creator 4.14.1
  • Qt WebEngine (Qt 설치시 별도로 설치)

[Qt 개발환경]

이 예제는 QtWebEngine을 이용해 웹페이지를 위젯에 띄우고, 해당 페이지를 PDF저장 or 인쇄하는 목적으로 제작되었습니다.

QtWebEngine은 웹페이지 표시, 인쇄, 북마크, 탐색, 스크립트 지원 등 웹과 관련된 대부분의 기능을 지원하는 클래스들의 집합으로 구성되어 있습니다.

이를 이용해 웹브라우저를 만드는 것도 가능합니다.

Qt6가 2020년 12월에 릴리즈 되었지만 Webengine 등 모듈은 6.2 버전에 업데이트될 계획이므로 아직은 5.15.2 버전을 쓰고 있습니다.

크로미엄 기반의 QtWebEngine에 대한 자세한 내용은 Qt 사이트 or 이전 게시물을 참조하기 바랍니다.


소스코드

Qt Creator로 제작한 Widget Base 프로젝트이며, Qt Designer 를 사용하지 않고, 동적할당을 통해 컨트롤을 추가하는 형태로 제작되었습니다.

전체 코드는 main.cpp, widget.h, widget.cpp 3개의 파일로 구성되어 있으며 먼저 Qt WebEngine을 사용하기 위해 *.pro 파일에 WebEngine을 추가합니다.

[Qt *.pro 프로젝트 파일]

widget.h 소스코드

#ifndef WIDGET_H
#define WIDGET_H

#include <QWidget>

class QPushButton;
class QLineEdit;
class QWebEngineView;
class QHBoxLayout;
class QVBoxLayout;
class QPrinter;

class Widget : public QWidget
{
    Q_OBJECT

public:
    Widget(QWidget *parent = nullptr);
    ~Widget();

public slots:
    void onClickMove();
    void onClickPdf();
    void onClickPrn();
    bool eventFilter(QObject *object, QEvent *e);

private:
    void initUI();
    void printDocument(QPrinter *printer);

private:
    QPushButton *pBtnMove, *pBtnPdf, *pBtnPrn;
    QLineEdit *pLineEdit;
    QWebEngineView *pView;
    QHBoxLayout *pHbox;
    QVBoxLayout *pVbox;

    const QString strUrl;
};
#endif // WIDGET_H

QWidget에서 상속받은 widget 클래스의 선언부입니다.

먼저 헤더파일의 의존성을 낮추기 위해 필요한 컨트롤(버튼, 라인에디트 등) 클래스를 전방선언합니다.

C++에서 전방선언된 클래스의 포인터를 사용하는 경우에는 헤더파일을 Include 하지 않아도 됩니다.

21번 라인은 시그널(버튼 클릭 등) 발생시 호출되는 슬롯 함수들의 선언부입니다.

자세한 코드는 아래 *.cpp 구현부에서 설명하겠습니다.

 

widget.cpp 소스코드

#include "widget.h"
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QPushButton>
#include <QLineEdit>
#include <QtWebEngineWidgets/QWebEngineView>
#include <QtWebEngineWidgets/QWebEnginePage>
#include <QFileDialog>
#include <QPrinter>
#include <QPrintDialog>
#include <QEvent>
#include <QEventLoop>
#include <QKeyEvent>

Widget::Widget(QWidget *parent)
    : QWidget(parent),
      pBtnMove(nullptr),
      pBtnPdf(nullptr),
      pBtnPrn(nullptr),
      pLineEdit(nullptr),
      pView(nullptr),
      pHbox(nullptr),
      pVbox(nullptr),
      strUrl("https://google.com")
{
    // create controls
    pBtnMove = new QPushButton("Move");
    pBtnPdf = new QPushButton("PDF");
    pBtnPrn = new QPushButton("Print");
    pLineEdit = new QLineEdit(strUrl);
    pLineEdit->installEventFilter(this);
    pView = new QWebEngineView();
    pHbox = new QHBoxLayout();
    pVbox = new QVBoxLayout();

    // signal,slot
    connect(pBtnMove, &QPushButton::clicked, this, &Widget::onClickMove);
    connect(pBtnPdf, &QPushButton::clicked, this, &Widget::onClickPdf);
    connect(pBtnPrn, &QPushButton::clicked, this, &Widget::onClickPrn);


    // initialize UI
    initUI();
}

Widget::~Widget()
{
    if(pBtnMove)
        delete pBtnMove;
    if(pBtnPdf)
        delete pBtnPdf;
    if(pBtnPrn)
        delete pBtnPrn;
    if(pLineEdit)
        delete pLineEdit;
    if(pView)
        delete pView;
    if(pHbox)
        delete pHbox;
    if(pVbox)
        delete pVbox;
}

void Widget::initUI()
{
    this->setWindowTitle("Ocean Coding School");
    this->resize(800,600);

    pView->load(strUrl);

    pHbox->addWidget(pLineEdit);
    pHbox->addWidget(pBtnMove);
    pHbox->addWidget(pBtnPdf);
    pHbox->addWidget(pBtnPrn);

    pVbox->addLayout(pHbox);
    pVbox->addWidget(pView);

    this->setLayout(pVbox);
}

bool Widget::eventFilter(QObject *object, QEvent *e)
{
    if (object == pLineEdit && e->type() == QEvent::KeyPress)
    {
        QKeyEvent* ke = static_cast<QKeyEvent *>(e);
        if(ke->key() == Qt::Key_Return)
        {
            onClickMove();
            return true;
        }
        else
            return false;
    }
    return false;
}

void Widget::onClickMove()
{
    QString url = pLineEdit->text();

    if (!url.contains("http"))
    {
        url.insert(0, "http://");
        pLineEdit->setText(url);
    }
    pView->load(QUrl(url));
}

void Widget::onClickPdf()
{
    QWebEnginePage* pPage = pView->page();

    QString path = QFileDialog::getSaveFileName(this, "PDF Save", "", "PDF File (*.pdf)");
    if(!path.isEmpty())
        pPage->printToPdf(path);
}

void Widget::onClickPrn()
{
    QPrinter prn = QPrinter();
    prn.setResolution(QPrinter::HighResolution);
    prn.setPageSize(QPrinter::A4);

    QPrintDialog dlg(&prn, pView);
    if (dlg.exec() == QDialog::Accepted)
        printDocument(&prn);
}

void Widget::printDocument(QPrinter *printer)
{
    QEventLoop loop;
    bool result;
    auto printPreview = [&](bool success) { result = success; loop.quit(); };
    auto pPage = pView->page();
    pPage->print(printer, std::move(printPreview));
    loop.exec();
    if (!result) {
        QPainter painter;
        if (painter.begin(printer)) {
            QFont font = painter.font();
            font.setPixelSize(20);
            painter.setFont(font);
            painter.drawText(QPointF(10,25),
                             QStringLiteral("Could not generate print preview."));

            painter.end();
        }
    }
}

[1~13 라인]

필요한 헤더 파일 불러오기.

 

[15~44 라인] 

Widget 클래스 생성자 함수.

멤버 초기화 리스트를 통해 포인터 변수들을 초기화, 기본 홈주소 문자열을 구글로 설정.

Url을 입력하는 라인에디트 컨트롤은 주소변경후 엔터키 이벤트를 즉시 처리하기 위해 이벤트 필터 설정. (QLineEdit SubClassing에 비해 간단)

[QLineEdit 컨트롤]

이어서 필요한 컨트롤을 동적 생성하고 버튼의 경우는 클릭 시그널 발생시 호출되는 슬롯함수들을 함수포인터로 연결.


[46~62 라인]

Widget 클래스 소멸자 함수, 종료시 Heap에 동적할당된 객체 삭제.


[64~80 라인]

생성자에서 호출되는 UI 초기화 용도 함수.

레이아웃박스를 이용해 생성자에서 만들어진 컨트롤을 배치.


[82~96 라인]

URL 주소를 적는 라인 에디트컨트롤의 엔터키 KeyPressEvent 를 감지해 변경된 주소로 바로 이동.


[98~108 라인]

이동 버튼 클릭시 호출되는 슬롯함수.

입력된 주소에 "http" 문자열이 없는 경우 붙여서 웹 페이지 이동처리.

예) naver.com 입력 -> http://naver.com 으로 변경


[110~117 라인]

PDF 버튼 클릭시 호출되는 슬롯함수.

QtWebEngineView 클래스의 페이지 객체 (QtWebEnginePage) 를 생성.

QFileDialog 클래스를 통해 PDF 파일 저장 경로를 얻은 후, QtWebEnginePage의 printToPdf() 함수를 통해 PDF 파일 생성, 저장.


[119~128 라인]

프린트 버튼 클릭시 호출되는 슬롯함수.

QPrinter 객체를 생성하고 해상도 및 인쇄 사이즈 설정.

QPrintDialog 객체를 모달로 띄우고 실제 인쇄가 처리될 printDocument() 호출


[130~150 라인]

실제 인쇄 작업이 이루어지는 함수.

QtWebEnginePageprint(프린터, 콜백함수) 함수는 2개의 전달인자를 받아서 처리하도록 되어 있으며, 2번째 전달인자 사용할 람다함수, printPreview() 선언.

이는 인쇄시 인쇄 작업의 성공여부를 판단.

자세한 내용은 아래 Qt 문서를 참조

[출처 Qt : QWebEngine, print 함수 설명]

인쇄 이벤트 루프를 실행하고, 실패시 오류 메시지 출력.

 

main.cpp 소스코드

#include "widget.h"
#include <QApplication>

int main(int argc, char *argv[])
{
    //QApplication::setAttribute(Qt::AA_EnableHighDpiScaling, true);
    QApplication a(argc, argv);
    Widget w;
    w.show();
    return a.exec();
}

메인 함수, QApplication 객체를 생성해 앱 이벤트 루프 처리.

Widget 객체를 띄우고 앱 실행.

두 객체는 서로 연결이나 연관이 없어보이지만 Qt의 QApplication의 전역변수를 QWidget이 참조하는 형태로 구성됨.

더 세부적으로 설명하면 QApplication은 Singleton 클래스이며, qApp라는 전역 객체를 가짐.

QWidget은 QWidget.cpp를 열어보면 아래와 같이 qApp를 참조

if (Q_UNLIKELY(!qApp)) {
        qFatal("QWidget: Must construct a QApplication before a QWidget");
        return;
    }

전형적인 싱글톤 패턴이며 MFC의 CWinApp 의 theApp와 유사.

 

이상으로 모든 설명을 마칩니다.

감사합니다.

댓글

이 블로그의 인기 게시물

Qt Designer 설치하기

PyQt5 기반 동영상 플레이어앱 만들기