你想不想在瀏覽器上運行你的Qt Quick程序呢?在Qt 5.12之前,唯一的方法是使用Qt WebGL Streaming技術把界面鏡像到瀏覽器上。但該方法有不少缺陷,下文會說。前不久隨着Qt 5.12的推出,有一個模塊正式進入Qt大家庭,那就是Qt for WebAssembly。簡單地講,就是使用WebAssembly技術,將Qt庫編譯為wasm字節碼,然后直接在瀏覽器上運行。聽着是不是很黑科技?本文將向大家展示如何使用Qt for WebAssembly(以下簡稱QtWasm)來開發程序,同時也針對目前該模塊的幾個問題提出自己的解決方法。
The English version of this post is:
為啥要在瀏覽器上運行Qt程序?
在講具體技術前,我們首先討論下該技術的目的,或者優勢。沒有實用價值的黑科技不是好科技。
首先得講一下WebAssembly技術的優勢。Wasm官網提了幾個好處,大家可以去詳細了解下,在我看來其中毫無疑問最大的優勢就是速度。wasm是一種新的字節碼技術,可以在瀏覽器本地獲得近乎二進制機器碼的運行速度,比js要快多了。對於那些對性能有極高要求的Web應用,例如Web游戲、圖形圖像處理應用,恨不得榨干整個機器的資源,wasm這類技術方向無疑是必走的。而wasm目前已經獲得了Chrome、Firefox、Edge以及Safari這四大瀏覽器的支持,所以wasm未來可期。
再來講講QtWasm。Qt是一套成熟的C++框架,從基本邏輯到界面、網絡都可以幫我們快速開發應用。通過將Qt搬到WebAssembly平台,我們可以在瀏覽器上運行我們的Qt程序。WebAssembly的高性能使得像Qt這么復雜的C++框架也能夠順暢運行。Qt之前的口號是:Write once, compile & run everywhere。而QtWasm使得該口號可以變為:Compile once, run everywhere。是不是很熟悉?對,那正是Java的口號啊!QtWasm解決了Qt程序二進制的跨平台問題,而Web的模式解決了應用的分發、升級問題,所以QtWasm同樣未來可期。
所以總結起來,我們為啥要在瀏覽器上跑Qt 程序?
- 借由Qt和WebAssembly技術,快速構建高效、豐富的Web應用,非常適合Qt程序員向Web端拓展;
- 解決了傳統桌面Qt程序分發、安裝、升級問題,而且是一次編譯各平台各瀏覽器都能運行。
現有的瀏覽器跑Qt Quick程序的方法
目前我還沒發現有什么方法可以在瀏覽器跑Qt Widgets的,所以這里只說Qt Quick:
- Qt WebGL Streaming。官方介紹在這里:Qt Quick WebGL release in Qt 5.12。該方法的本質是將Qt Quick的OpenGL ES命令通過WebGL Streaming技術發送到瀏覽器,實現界面鏡像的效果。既然是鏡像,也就意味着程序的所有運行其實都還在本地,瀏覽器只是一個遠程桌面。該方法筆者測試下來覺得非常適合於局域網內嵌入式開發板上的Qt Quick程序的遠程控制,但也僅限於此。非局域網的情況下會非常卡。
- 將QML編譯成JavaScript,然后在瀏覽器跑。典型的是qmlweb。Qt官方是沒有這類項目的。這種方法其實只是用了QML的語法,並不是真正的Qt Quick程序,所以也就不可能使用Qt框架的種種強大的功能。
和這兩類方法比較,QtWasm有着本質區別,可以說是唯一的、真正在瀏覽器跑的Qt解決方案。
配置編譯環境
講了QtWasm的優勢,那我們開始嘗試開發吧。首先就是配置編譯環境。
目前QtWasm並沒有以二進制的方式包含在Qt安裝包里面,需要我們自己編譯。筆者在MacOS和Ubuntu for Windows Sub-system Linux(簡稱WSL)都編譯成功過。下面以WSL為例:
- 首先是安裝WSL。這個網上教程很多,給大家參考一個:How to install Windows 10’s Linux Subsystem on your PC 。大家一定要裝Ubuntu 18.04這個版本。舊版本可能會出問題(因為軟件源里的各種庫都過時了)。
- 然后是安裝emscripten。安裝過程參考emscripten官方教程:Emscripten download and install。這個過程特別需要一個好網,你懂的。確保安裝成功的標志是能通過下面命令打印出em++的版本號:
em++ --version
。如果報錯了,那說明安裝沒成功(很可能某個組件下載出錯了)。總之這個環節是最麻煩的。 - 下載Qt 5.12.0源代碼。注意要下載Linux newline ending的源代碼,可以用下面的命令下載:
wget https://download.qt.io/official_releases/qt/5.12/5.12.0/single/qt-everywhere-src-5.12.0.tar.xz
。 - 接下去就可以參照Qt官方教程來編譯QtWasm了:Getting Started With Qt for WebAssembly。編譯過程視機器性能不同,可能會花費30分鍾到2小時不等。但只要你做好了上面三步,基本上能夠編譯成功。
編譯完成后,我們就可以用來開發QtWasm了。一般開發過程是:
- 使用Qt for Desktop正常新建工程(注意,要用qmake組織,而不是cmake或者qbs),開發、編譯和測試代碼;
- 轉到WSL,使用QtWasm的qmake以及make編譯程序。編譯會生成wasm、js和html文件。
- 使用Python http模塊搭建簡單Web服務器。如果是Python2,使用命令:
python -m SimpleHTTPServer 8080
;如果是Python3,則使用命令:python -m http.server 8080
。你也可以用其他Web服務器。 - 使用支持Wasm的瀏覽器打開html文件,如果一切順利的話,你應該能看到你的Qt程序跑在瀏覽器中了。
解決編譯慢的問題
能運行起來第一個QtWasm應該有些激動吧?但別高興太早。當你試着修改源碼運行其他功能時,你馬上就會發現一個問題:編譯實在太慢了!巨慢無比,簡直不能忍。筆者試過i5的MacBook Pro和i7的Windows Linux sub-system,都很慢。
冷靜下來想想,這倒不是emscripten的鍋,實在是Qt這個庫太大了,而WebAssembly又要求靜態鏈接,所以……
是可忍孰不可忍,必須解決它。查遍官方文檔,無果,納悶難道這幫人沒遇到過這個問題嗎?自己動手豐衣足食。忽然想到Qt QML其實是支持網絡加載的,比如Loader
就可以將source
設置為網絡上的一個qml文件。
所以解決思路就是:將QML界面獨立出來,而將C++在內的做成一個加載器,通過Loader
動態加載我們的QML界面。由於加載器部分邏輯明確,無需頻繁修改,所以只要編譯一次就好了(慢就慢點吧……);而我們的QML界面可以隨意修改,只要刷新下頁面,就能夠加載我們的新界面。說起來這比C++編譯還快啊!
所以老實講,筆者並沒有解決Qt庫編譯慢的問題,而是將QML界面獨立出來通過Qt自帶的Loader
動態加載。本質上我們是將Qt的QML引擎編譯成了WebAssembly跑在了瀏覽器上。
Image
無法加載圖片怎么辦?
真正要動手寫點嚴肅的程序的時候,小伙伴們肯定還會發現,我的Image
怎么沒法顯示圖片了?
由於我們的QML界面是動態加載的,而Loader
是沒法加載qrc這些資源文件的。那讀者可能會想,我可以把圖片放在服務器上,Image
是支持加載網絡圖片的呀?是的,你之前寫的Image
都是可以的,但是QtWasm不行。原因是網頁圖片加載情況下Image
只能以異步的方式,也就是說Image
會試圖創建新線程來執行后台加載;但目前QtWasm不支持多線程,所以就加載失敗了。
那怎么辦?解決辦法是:在我們的項目開發時就盡可能准備好圖片素材,然后將他們都放進加載器的qrc里一起編譯。然后在我們動態加載的QML界面里,就正常使用"qrc:/"開頭的路徑加載圖片就好了。
讀者可能會追問:一開始就准備好所有圖片怎么可能呢?那也沒辦法,一次准備不行,那就分階段准備多次,總之得將圖片編譯進加載器里,這是筆者目前想到的最好辦法了。雖不完美,但能湊合着用。
我的中文怎么成方塊了?
這又是個惱人的問題,困擾筆者很久。后來在QtWasm的官方wiki上找到了原因:
Applications do not have access to system fonts. Font files must be distributed with the application, for example in Qt resources. Qt for WebAssembly itself embeds one such font.
翻譯過來就是說,QtWasm是沒法訪問系統字體的,字體文件必須一起編譯進程序中。
英文字體還好,中文字體動則三四十兆,也要編譯進wasm中?好吧,大小我忍了,但是……編譯通不過啊!編譯了半天最后出錯什么的最傷人了。
中文字體之所以大是因為漢字實在是多。但我們一個程序一般用到的漢字只是其中很小的一個子集。所以我們解決該問題的主要思路是:只抽取我們要用的漢字集合生成新的字體,然后使用該字體再編譯我們的程序。
如何抽取字體?我使用了一個小工具叫FontZip。是一個jar包,所以要先安裝java環境。我試驗了一下,目前好像只支持從一個ttf格式的字體文件中抽取,可保存為otf或者ttf格式,這兩個格式都是Qt支持的。
例如我使用一個免費的中文字體:WenQuanYi Micro Hei Mono。能用FontZip抽取成功,而且目前用下來都沒問題。
將抽取出來的字體加入我們的Qt Quick工程qrc中,然后在main.qml中加入下面代碼:
FontLoader{ id: chineseFont source: "qrc:/fonts/fontzipmin.otf" }
然后在用到的地方使用font.family: chineseFont.name
即可:
Label{ id: label font{family: chineseFont.name} text: "你好" }
這里面特別需要注意的是,直接寫中文字符串只支持像main.qml這樣參與編譯的QML文件!如果是通過Loader
動態加載的QML文件,必須使用Unicode Codepoint才行,這也是筆者遇到的一個大坑。例如顯示"你好我們"
需要像下面這么寫:
Label{ id: label anchors.centerIn: parent text: "\u4f60\u597d\u6211\u4eec" }
推薦大家安裝VS Code上的兩個插件:
- Unicode code point of current character。這個插件可以把中文轉成Unicode codepoint。但目前只能一個個轉,略麻煩,但好處是可以直接在編代碼時就地轉換好。
- unicodeToChinese。如果哪天你忘了轉出來的Unicode codepoint原來是什么漢字了,還能用這個插件轉回去。
當然,將漢字轉成Unicode codepoint,大家也可以用http://unicode.scarfboy.com/ 這個網站。能夠一次性將一大段中文轉成codepoint。
示例
延續本專欄以往教程的優良傳統,文末必須配上筆者實際驗證過的示例程序。這次示例程序也上傳到了Github:QtWasmLoader。源代碼我上傳到了src分支,而編譯出來的加載器和用於動態加載的QML文件我放在了master分支上。
我打開了Github Pages功能,大家可以直接瀏覽器打開:https://cjmdaixi.github.io/QtWasmLoader/ 就能看到效果:
大家看到我跑的是我另一個Qt Quick工程DarkSwitch。
大家參考、克隆我的工程的時候要注意,我在Loader
里寫入的是我這個工程在Github Pages上的URL。如果你要跑自己的程序,那需要將下面的URL設置成你的工程的Github Pages URL:
Loader{ id: mainView anchors.fill: parent source: "你的Github Pages URL/UI/MainView.qml" onLoaded: { loadingItem.visible = false; } }
為了方便大家開發,我已經把3500個常用漢字抽出來了,就是font目錄下的Simplified-Chinese3500.otf文件。所以一般開發測試情況下,大家不需要自己抽取漢字了。
有問題歡迎大家去Github上留issue。
https://zhuanlan.zhihu.com/p/53077277