Qt在Android平台上实现html转PDF的功能
程序员文章站
2022-04-09 14:58:46
Qt for Android Qt for Android enables you to run Qt 5 applications Android devices. All Qt modules (essential and add-on) are supported except Qt WebE ......
qt for android
qt for android enables you to run qt 5 applications android devices. all qt modules (essential and add-on) are supported except qt webengine, qt serial port, and the platform-specific ones (qt mac extras, qt windows extras, and qt x11 extras).
在windows或者linux平台上可以用qtwebengine模块实现网页预览和打印成pdf文档,用起来很方便,生成的文档质量也比较好,但在android平台上qtwebengine模块不能用,想要显示网页可以用qtwebview模块,不支持打印成pdf。尝试用qtextdocument和qprinter将html转为pdf,发现qtextdocument不支持css样式,生成的pdf文档排版是错的。
查看qtwebview在android平台上的实现,可以发现其用的就是android的webview控件实现的网页显示。尝试在android平台上实现html生成pdf,找到了这篇文章,验证后可行。需要依赖第三方库dexmaker,可以用谷歌实现的 implementation 'com.google.dexmaker:dexmaker:1.2',库文件名为dexmaker-1.2.jar。
修改qt源码,在android平台上实现html转pdf的功能
- 修改$qtsrc/qtwebview/src/jar/src/org/qtproject/qt5/android/view/qtandroidwebviewcontroller.java文件
/**************************************************************************** ** ** copyright (c) 2015 the qt company ltd. ** contact: http://www.qt.io/licensing/ ** ** this file is part of the qtwebview module of the qt toolkit. ** ** $qt_begin_license:lgpl3$ ** commercial license usage ** licensees holding valid commercial qt licenses may use this file in ** accordance with the commercial license agreement provided with the ** software or, alternatively, in accordance with the terms contained in ** a written agreement between you and the qt company. for licensing terms ** and conditions see http://www.qt.io/terms-conditions. for further ** information use the contact form at http://www.qt.io/contact-us. ** ** gnu lesser general public license usage ** alternatively, this file may be used under the terms of the gnu lesser ** general public license version 3 as published by the free software ** foundation and appearing in the file license.lgplv3 included in the ** packaging of this file. please review the following information to ** ensure the gnu lesser general public license version 3 requirements ** will be met: https://www.gnu.org/licenses/lgpl.html. ** ** gnu general public license usage ** alternatively, this file may be used under the terms of the gnu ** general public license version 2.0 or later as published by the free ** software foundation and appearing in the file license.gpl included in ** the packaging of this file. please review the following information to ** ensure the gnu general public license version 2.0 requirements will be ** met: http://www.gnu.org/licenses/gpl-2.0.html. ** ** $qt_end_license$ ** ****************************************************************************/ package org.qtproject.qt5.android.view; import android.content.pm.packagemanager; import android.view.view; import android.webkit.geolocationpermissions; import android.webkit.urlutil; import android.webkit.valuecallback; import android.annotation.suppresslint; import android.content.context; import android.os.bundle; import android.os.cancellationsignal; import android.os.parcelfiledescriptor; import android.print.pagerange; import android.print.printattributes; import android.print.printdocumentadapter; import android.webkit.webview; import android.webkit.webviewclient; import android.webkit.webchromeclient; import java.lang.runnable; import android.app.activity; import android.content.intent; import android.net.uri; import java.lang.string; import android.webkit.websettings; import android.webkit.websettings.pluginstate; import android.graphics.bitmap; import java.util.concurrent.semaphore; import java.io.file; import java.io.ioexception; import java.lang.reflect.invocationhandler; import java.lang.reflect.method; import android.os.build; import java.util.concurrent.timeunit; import com.google.dexmaker.stock.proxybuilder; public class qtandroidwebviewcontroller { private final activity m_activity; private final long m_id; private boolean busy; private boolean m_haslocationpermission; private webview m_webview = null; private static final string tag = "qtandroidwebviewcontroller"; private final int init_state = 0; private final int started_state = 1; private final int loading_state = 2; private final int finished_state = 3; private volatile int m_loadingstate = init_state; private volatile int m_progress = 0; private volatile int m_framecount = 0; // api 11 methods private method m_webviewonresume = null; private method m_webviewonpause = null; private method m_websettingssetdisplayzoomcontrols = null; // api 19 methods private method m_webviewevaluatejavascript = null; // native callbacks private native void c_onpagefinished(long id, string url); private native void c_onpagestarted(long id, string url, bitmap icon); private native void c_onprogresschanged(long id, int newprogress); private native void c_onreceivedicon(long id, bitmap icon); private native void c_onreceivedtitle(long id, string title); private native void c_onrunjavascriptresult(long id, long callbackid, string result); private native void c_onreceivederror(long id, int errorcode, string description, string url); private native void c_onpdfprintingfinished(long id, boolean succeed); // we need to block the ui thread in some cases, if it takes to long we should timeout before // anr kicks in... usually the hard limit is set to 10s and if exceed that then we're in trouble. // in general we should not let input events be delayed for more then 500ms (if we're spending more // then 200ms somethings off...). private final long blocking_timeout = 250; private void resetloadingstate(final int state) { m_progress = 0; m_framecount = 0; m_loadingstate = state; } private class html2pdf { private file file; private file dexcachefile; private printdocumentadapter printadapter; private pagerange[] ranges; private parcelfiledescriptor descriptor; private void printtopdf(webview webview, string filename) { if (webview != null) { file = new file(filename); dexcachefile = webview.getcontext().getdir("dex", 0); if (!dexcachefile.exists()) { dexcachefile.mkdir(); } try { if (file.exists()) { file.delete(); } file.createnewfile(); descriptor = parcelfiledescriptor.open(file, parcelfiledescriptor.mode_read_write); printattributes attributes = new printattributes.builder() .setmediasize(printattributes.mediasize.iso_a4) .setresolution(new printattributes.resolution("id", context.print_service, 300, 300)) .setcolormode(printattributes.color_mode_color) .setminmargins(printattributes.margins.no_margins) .build(); ranges = new pagerange[]{pagerange.all_pages}; printadapter = webview.createprintdocumentadapter(); printadapter.onstart(); printadapter.onlayout(attributes, attributes, new cancellationsignal(), getlayoutresultcallback(new invocationhandler() { @override public object invoke(object proxy, method method, object[] args) throws throwable { if (method.getname().equals("onlayoutfinished")) { onlayoutsuccess(); } else { descriptor.close(); c_onpdfprintingfinished(m_id, false); busy = false; } return null; } }, dexcachefile.getabsolutefile()), new bundle()); } catch (ioexception e) { if (descriptor != null) { try { descriptor.close(); } catch (ioexception ex) { ex.printstacktrace(); } } c_onpdfprintingfinished(m_id, false); e.printstacktrace(); busy = false; } } } private void onlayoutsuccess() throws ioexception { printdocumentadapter.writeresultcallback callback = getwriteresultcallback(new invocationhandler() { @override public object invoke(object o, method method, object[] objects) throws throwable { if (method.getname().equals("onwritefinished")) { c_onpdfprintingfinished(m_id, true); } else { c_onpdfprintingfinished(m_id, false); } busy = false; if (descriptor != null) { try { descriptor.close(); } catch (ioexception ex) { ex.printstacktrace(); } } return null; } }, dexcachefile.getabsolutefile()); printadapter.onwrite(ranges, descriptor, new cancellationsignal(), callback); } @suppresslint("newapi") private printdocumentadapter.layoutresultcallback getlayoutresultcallback(invocationhandler invocationhandler, file dexcachedir) throws ioexception { return proxybuilder.forclass(printdocumentadapter.layoutresultcallback.class) .dexcache(dexcachedir) .handler(invocationhandler) .build(); } @suppresslint("newapi") private printdocumentadapter.writeresultcallback getwriteresultcallback(invocationhandler invocationhandler, file dexcachedir) throws ioexception { return proxybuilder.forclass(printdocumentadapter.writeresultcallback.class) .dexcache(dexcachedir) .handler(invocationhandler) .build(); } } private class qtandroidwebviewclient extends webviewclient { qtandroidwebviewclient() { super(); } @override public boolean shouldoverrideurlloading(webview view, string url) { // handle http: and http:, etc., as usual if (urlutil.isvalidurl(url)) return false; // try to handle geo:, tel:, mailto: and other schemes try { intent intent = new intent(intent.action_view, uri.parse(url)); view.getcontext().startactivity(intent); return true; } catch (exception e) { e.printstacktrace(); } return false; } @override public void onloadresource(webview view, string url) { super.onloadresource(view, url); } @override public void onpagefinished(webview view, string url) { super.onpagefinished(view, url); m_loadingstate = finished_state; if (m_progress != 100) // onprogresschanged() will notify qt if we didn't finish here. return; m_framecount = 0; c_onpagefinished(m_id, url); } @override public void onpagestarted(webview view, string url, bitmap favicon) { super.onpagestarted(view, url, favicon); if (++m_framecount == 1) { // only call onpagestarted for the first frame. m_loadingstate = loading_state; c_onpagestarted(m_id, url, favicon); } } @override public void onreceivederror(webview view, int errorcode, string description, string url) { super.onreceivederror(view, errorcode, description, url); resetloadingstate(init_state); c_onreceivederror(m_id, errorcode, description, url); } } private class qtandroidwebchromeclient extends webchromeclient { qtandroidwebchromeclient() { super(); } @override public void onprogresschanged(webview view, int newprogress) { super.onprogresschanged(view, newprogress); m_progress = newprogress; c_onprogresschanged(m_id, newprogress); if (m_loadingstate == finished_state && m_progress == 100) { // did we finish? m_framecount = 0; c_onpagefinished(m_id, view.geturl()); } } @override public void onreceivedicon(webview view, bitmap icon) { super.onreceivedicon(view, icon); c_onreceivedicon(m_id, icon); } @override public void onreceivedtitle(webview view, string title) { super.onreceivedtitle(view, title); c_onreceivedtitle(m_id, title); } @override public void ongeolocationpermissionsshowprompt(string origin, geolocationpermissions.callback callback) { callback.invoke(origin, m_haslocationpermission, false); } } public qtandroidwebviewcontroller(final activity activity, final long id) { m_activity = activity; m_id = id; final semaphore sem = new semaphore(0); m_activity.runonuithread(new runnable() { @override public void run() { m_webview = new webview(m_activity); m_haslocationpermission = haslocationpermission(m_webview); websettings websettings = m_webview.getsettings(); if (build.version.sdk_int > 10) { try { m_webviewonresume = m_webview.getclass().getmethod("onresume"); m_webviewonpause = m_webview.getclass().getmethod("onpause"); m_websettingssetdisplayzoomcontrols = websettings.getclass().getmethod("setdisplayzoomcontrols", boolean.class); if (build.version.sdk_int > 18) { m_webviewevaluatejavascript = m_webview.getclass().getmethod("evaluatejavascript", string.class, valuecallback.class); } } catch (exception e) { /* do nothing */ e.printstacktrace(); } } //allowing access to location without actual access_fine_location may throw security exception websettings.setgeolocationenabled(m_haslocationpermission); websettings.setjavascriptenabled(true); if (m_websettingssetdisplayzoomcontrols != null) { try { m_websettingssetdisplayzoomcontrols.invoke(websettings, false); } catch (exception e) { e.printstacktrace(); } } websettings.setbuiltinzoomcontrols(true); websettings.setpluginstate(pluginstate.on); m_webview.setwebviewclient((webviewclient)new qtandroidwebviewclient()); m_webview.setwebchromeclient((webchromeclient)new qtandroidwebchromeclient()); sem.release(); } }); try { sem.acquire(); } catch (exception e) { e.printstacktrace(); } } public void loadurl(final string url) { if (url == null) { return; } resetloadingstate(started_state); c_onpagestarted(m_id, url, null); m_activity.runonuithread(new runnable() { @override public void run() { m_webview.loadurl(url); } }); } public void loaddata(final string data, final string mimetype, final string encoding) { if (data == null) return; resetloadingstate(started_state); c_onpagestarted(m_id, null, null); m_activity.runonuithread(new runnable() { @override public void run() { m_webview.loaddata(data, mimetype, encoding); } }); } public void loaddatawithbaseurl(final string baseurl, final string data, final string mimetype, final string encoding, final string historyurl) { if (data == null) return; resetloadingstate(started_state); c_onpagestarted(m_id, null, null); m_activity.runonuithread(new runnable() { @override public void run() { m_webview.loaddatawithbaseurl(baseurl, data, mimetype, encoding, historyurl); } }); } public void goback() { m_activity.runonuithread(new runnable() { @override public void run() { m_webview.goback(); } }); } public boolean cangoback() { final boolean[] back = {false}; final semaphore sem = new semaphore(0); m_activity.runonuithread(new runnable() { @override public void run() { back[0] = m_webview.cangoback(); sem.release(); } }); try { sem.tryacquire(blocking_timeout, timeunit.milliseconds); } catch (exception e) { e.printstacktrace(); } return back[0]; } public void goforward() { m_activity.runonuithread(new runnable() { @override public void run() { m_webview.goforward(); } }); } public boolean cangoforward() { final boolean[] forward = {false}; final semaphore sem = new semaphore(0); m_activity.runonuithread(new runnable() { @override public void run() { forward[0] = m_webview.cangoforward(); sem.release(); } }); try { sem.tryacquire(blocking_timeout, timeunit.milliseconds); } catch (exception e) { e.printstacktrace(); } return forward[0]; } public void stoploading() { m_activity.runonuithread(new runnable() { @override public void run() { m_webview.stoploading(); } }); } public void reload() { m_activity.runonuithread(new runnable() { @override public void run() { m_webview.reload(); } }); } public string gettitle() { final string[] title = {""}; final semaphore sem = new semaphore(0); m_activity.runonuithread(new runnable() { @override public void run() { title[0] = m_webview.gettitle(); sem.release(); } }); try { sem.tryacquire(blocking_timeout, timeunit.milliseconds); } catch (exception e) { e.printstacktrace(); } return title[0]; } public int getprogress() { return m_progress; } public boolean isloading() { return m_loadingstate == loading_state || m_loadingstate == started_state || (m_progress > 0 && m_progress < 100); } public void runjavascript(final string script, final long callbackid) { if (script == null) return; if (build.version.sdk_int < 19 || m_webviewevaluatejavascript == null) return; m_activity.runonuithread(new runnable() { @override public void run() { try { m_webviewevaluatejavascript.invoke(m_webview, script, callbackid == -1 ? null : new valuecallback<string>() { @override public void onreceivevalue(string result) { c_onrunjavascriptresult(m_id, callbackid, result); } }); } catch (exception e) { e.printstacktrace(); } } }); } public string geturl() { final string[] url = {""}; final semaphore sem = new semaphore(0); m_activity.runonuithread(new runnable() { @override public void run() { url[0] = m_webview.geturl(); sem.release(); } }); try { sem.tryacquire(blocking_timeout, timeunit.milliseconds); } catch (exception e) { e.printstacktrace(); } return url[0]; } public webview getwebview() { return m_webview; } public void onpause() { if (m_webviewonpause == null) return; m_activity.runonuithread(new runnable() { @override public void run() { try { m_webviewonpause.invoke(m_webview); } catch (exception e) { e.printstacktrace(); } } }); } public void onresume() { if (m_webviewonresume == null) return; m_activity.runonuithread(new runnable() { @override public void run() { try { m_webviewonresume.invoke(m_webview); } catch (exception e) { e.printstacktrace(); } } }); } private static boolean haslocationpermission(view view) { final string name = view.getcontext().getpackagename(); final packagemanager pm = view.getcontext().getpackagemanager(); return pm.checkpermission("android.permission.access_fine_location", name) == packagemanager.permission_granted; } public void destroy() { m_activity.runonuithread(new runnable() { @override public void run() { m_webview.destroy(); } }); } public void printtopdf(final string filename){ if(!busy){ busy = true; m_activity.runonuithread(new runnable() { @override public void run() { html2pdf html2pdf = new html2pdf(); html2pdf.printtopdf(m_webview, filename); } }); }else{ c_onpdfprintingfinished(m_id,false); } } }
- 主要修改:
- 增加了 void printtopdf(final string filename)打印接口
- 增加了 native void c_onpdfprintingfinished(long id, boolean succeed)作为打印完成的回调
- 增加了内部类html2pdf实现打印成pdf
- 修改$qtsrc/qtwebview/src/plugins/android/qandroidwebview_p.h
- 增加槽函数 void printtopdf(const qstring &filename) q_decl_override;
- 修改$qtsrc/qtwebview/src/plugins/android/qandroidwebview.cpp
- 实现槽函数
void qandroidwebviewprivate::printtopdf(const qstring &filename) { const qjniobjectprivate &filenamestring = qjniobjectprivate::fromstring(filename); m_viewcontroller.callmethod<void>("printtopdf","(ljava/lang/string;)v",filenamestring.object()); }
- 实现java代码中打印完成的回调
static void c_onpdfprintingfinished(jnienv *env, jobject thiz, jlong id, jboolean succeed) { q_unused(env) q_unused(thiz) const webviews &wv = (*g_webviews); qandroidwebviewprivate *wc = wv[id]; if (!wc) return; q_emit wc->pdfprintingfinished(succeed); }
- 修改jniexport jint jni_onload(javavm* vm, void* /*reserved*/),注册c_onpdfprintingfinished回调函数。
jninativemethod methods[]数组里增加一项 {"c_onpdfprintingfinished","(jz)v",reinterpret_cast<void *>(c_onpdfprintingfinished)}
- 修改$qtsrc/qtwebview/src/webview/qabstractwebview_p.h(以下增加的所有的c++代码、函数、信号等都用#if android宏条件编译)
- 增加信号void pdfprintingfinished(bool succeed);
- 修改$qtsrc/qtwebview/src/webview/qquickwebview_p.h
- 增加公开槽函数 void printtopdf(const qstring &filename) q_decl_override;
- 增加信号 void pdfprintingfinished(bool succeed);
- 增加私有槽函数 void onpdfprintingfinished(bool succeed);
- 修改$qtsrc/qtwebview/src/webview/qquickwebview.cpp
- 构造函数里关联槽函数和信号
#if android connect(m_webview, &qwebview::pdfprintingfinished, this, &qquickwebview::onpdfprintingfinished); #endif
- 实现槽函数printtopdf
#if android void qquickwebview::printtopdf(const qstring &filename) { m_webview->printtopdf(filename); } #endif
- 实现槽函数onpdfprintingfinished
#if android void qquickwebview::onpdfprintingfinished(bool succeed) { q_emit pdfprintingfinished(succeed); } #endif
- 修改$qtsrc/qtwebview/src/webview/qwebviewinterface_p.h
- 增加纯虚函数 virtual void printtopdf(const qstring &filename) = 0;
- 修改$qtsrc/qtwebview/src/webview/qwebviewfactory.cpp
- qnullwebview类增加
void printtopdf(const qstring &filename) override {q_unused(filename); }
- 修改$qtsrc/qtwebview/src/webview/qwebview_p.h
- 增加公开槽函数 void printtopdf(const qstring &filename) q_decl_override;
- 增加信号 void pdfprintingfinished(bool succeed);
- 增加私有槽函数 void onpdfprintingfinished(bool succeed);
- 修改$qtsrc/qtwebview/src/webview/qwebview.cpp
- 构造函数里关联槽函数和信号
#if android connect(d, &qabstractwebview::pdfprintingfinished, this, &qwebview::onpdfprintingfinished); #endif
- 实现槽函数printtopdf
#if android void qwebview::printtopdf(const qstring &filename) { d->printtopdf(filename); } #endif
- 实现槽函数onpdfprintingfinished
#if android void qwebview::onpdfprintingfinished(bool succeed) { q_emit pdfprintingfinished(succeed); } #endif
- 在$qtsrc/qtwebview/src/jar目录下新建lib目录,将dexmaker-1.2.jar文件拷贝到该目录下
- 修改$qtsrc/qtwebview/src/jar/jar.pro
target = qtandroidwebview load(qt_build_paths) config += java destdir = $$module_base_outdir/jar javaclasspath += $$pwd/src \ $$pwd/lib/dexmaker-1.2.jar javasources += $$pwd/src/org/qtproject/qt5/android/view/qtandroidwebviewcontroller.java # install target.path = $$[qt_install_prefix]/jar target.files += $$pwd/lib/dexmaker-1.2.jar installs += target
- 修改$qtsrc/qtwebview/src/webview/webview.pro
…… qmake_docs = \
$$pwd/doc/qtwebview.qdocconf
android_bundled_jar_dependencies = \
jar/qtandroidwebview.jar \
jar/dexmaker-1.2.jar
android_permissions = \
android.permission.access_fine_location
android_lib_dependencies = \
plugins/webview/libqtwebview_android.so
headers += $$public_headers $$private_headers
load(qt_module) - 修改$qtsrc/qtwebview/src/imports/plugins.qmltypes
- 增加信号
signal { name: "pdfprintingfinished" revision: 1 parameter { name: "succeed"; type: "bool" } }
- 增加方法
method { name: "printtopdf" revision: 1 parameter { name: "filename"; type: "string" } }
-
配置和编译
-
./configure -extprefix $qtinstall/android_arm64_v8a -xplatform android-clang -release -nomake tests -nomake examples -opensource -confirm-license -recheck-all -android-ndk $ndkpath -android-sdk $androidsdkpath -android-ndk-host linux-x86_64 -android-arch arm64-v8a
-android-arch支持armeabi, armeabi-v7a, arm64-v8a, x86, x86_64,一次只能编译一个架构,注意不同架构要修改安装目录
- make -j8
- make install
- 上述软件版本
- qt:5.13.2
- ndk:r20b(20.1.5948944)
- android-buildtoolsversion:29.0.2
- 使用示例
import qtquick 2.12 import qtquick.window 2.12 import qtwebview 1.1 import qtquick.controls 2.12 window { id: window visible: true width: 1080 height: 1920 title: qstr("hello world") webview{ id:webview anchors.bottom: printbtn.top anchors.right: parent.right anchors.left: parent.left anchors.top: parent.top anchors.bottommargin: 0 url:"http://www.qq.com" onpdfprintingfinished: { printbtn.text = "打印" + (succeed?"成功":"失败") printbtn.enabled = true } } button { id:printbtn text: "打印" anchors.bottom: parent.bottom anchors.bottommargin: 0 onclicked: { printbtn.enabled = false webview.printtopdf("/sdcard/aaa.pdf") } } }
下期预告:在android平台上实现串口读写的功能