由兩篇文章組成的系列文章主要闡述如何在嵌入式 Linux 智慧型裝置的應用程式中增加 Web 支援。第 1 部分介紹了如何裝置上提供常規 Web 功能的支援。本文是第2部分,將重點介紹如何讓在嵌入式裝置上啟動並執行 Web 程式能支援裝置本身特有的功能。本文分別以四種應用情境為例,介紹如何通過修改瀏覽器核心代碼來實現裝置本地應用和 Web 結合的功能。
Web 與本地應用的關聯
雖然在嵌入式 Linux 智慧型裝置中採用 Web 支援已經解決了很多問題,但是還有一些和裝置相關的特殊功能是 Web 支援不能提供的。比如廣告機中的音視頻播放功能,條碼掃描機的模式識別功能,還有與某種外設的通訊等。這些並不是 HTML 和瀏覽器的標準所包含的,而是需要本地應用的支援。既然我們希望使用 Web 和 B/S 等技術來實現我們的應用,那麼這些本地應用功能也應該由 Web 來控制。比如說廣告機的視頻播放,實際的播放是由本地應用實現的,但是什麼時候在什麼位置播放什麼視頻應該由 Web 來決定。並且廣告頁面內容的編輯也應該在網頁的 HTML 中體現,而不需要另外一套播放控制機制。
但是想要由 Web 來控制本地應用存在一個問題,這些本地應用的調用沒有一種統一的機制。有的可能通過驅動,有的可能是通過 I2C、串口的通訊口,有的可能是第三方提供的庫,還有的可能是與其他進程的通訊。可以說,除了他們大多用 C/C++ 語言進行開發之外,幾乎沒有什麼共同點。
那麼現在我們要解決的問題就是,當 QWebView 渲染一個網頁的時候,如何讓我們在網頁裡編寫的一些特定的 HTML 能和我們的 C/C++ 代碼關聯起來。幸運的是,Qt 封裝的 WebKit 提供了多種方法使我們可以很好實現這個關聯。接下來,我們會以幾種應用情境為例來討論 Web 和本地應用關聯的幾種實現方法。
截取 request 的方法
首先我們介紹第一種應用情境:某嵌入式智慧型裝置需要實現下面的功能,使用者點擊網頁上“更新”的連結,裝置就會下載指定的 Firmware 並且進行更新。
為了實現這個功能,用戶端的瀏覽器需要在使用者點擊了某個特定的 Link 之後,啟動系統的更新過程。包括擷取最新 Firmware 的地址,進行下載,最後更新裝置。Firmware 的更新過程和裝置硬體相關,標準瀏覽器不能實現這個功能,因此我們必須“截獲”使用者的這個請求,然後使用本地代碼來完成整個更新過程。
為了實現截獲使用者的這個 HTML request,我們先分析一下 QWebView 的結構。
圖 1. QWebView 的結構圖
QWebView 使用 QWebPage 來實現頁面,QWebPage 使用 QWebFrame 來實現頁面元素。當頁面發出一個 Navigation 的 request 時,QWebPage 會來進行處理。這個時候有一個函數會被調用:
bool QWebPage::acceptNavigationRequest ( QWebFrame *frame, const QWebNetworkRequest &request, QWebPage::NavigationType type )
這個函數會在發生 Navigation Request 的時候擷取到觸發事件的頁面元素、request 內容和類型。如果函數如果返回 false,瀏覽器將忽略這個 request。
我們可以從 QWebPage 派生一個子類,重寫 acceptNavigationRequest,在發現特定 request 內容的時候,做出自己的處理。假設目標地址是 http://xxxx.com/update/Firmware.bin,實現如下:
清單 6. acceptNavigationRequest 函數的定義和實現
class QMyWebPage : public QwebPage { protected: bool acceptNavigationRequest ( QWebFrame *frame, const QWebNetworkRequest &request, QWebPage::NavigationType type ); ... ... }; QMyWebPage::acceptNavigationRequest ( QWebFrame *frame, const QWebNetworkRequest &request, QWebPage::NavigationType type ) { if( type == QWebPage::NavigationTypeFormSubmitted ) { QString str = url = request.url().path(); // 如果是特定的目標 if( str == “http://xxxx.com/update/Firmware.bin” ) { // 從 link 中擷取 Firmware 地址 get Firmware addr from path // 下載 Firmware download Firmware // 更新裝置 Firmware update Firmware // 返回 false 讓瀏覽器不再處理這個 request return false; } } return QWebPage::acceptNavigationRequest ( frame, request , type ); } |
上面實現部分中擷取、下載和更新 Firmware 部分用說明性文字來表示,不是真實的實現代碼,使用者可以根據自身的需求改寫這部分本地代碼。
除了實現具體功能之外,我們還需要讓 QMyWebPage 被 QWebView 使用。這是通過 QWebView 的 setPage 調用實現的,可以在構造 QWebView 執行個體的時候加入:
QWebView* Webview = new QWebView ( this );
QMyWebPage* page = new QMyWebPage ();Webview -> setPage ( page ); // 讓 WebView 使用我們的 QwebPage
至此,我們實現了當頁面發生了點擊 http://xxxx.com/update/Firmware.bin 的時候,截取了這個 request,並讓我們的本地代碼能被適時的調用運行。
acceptNavigationRequest 還可以被用在另外一種場合,某些網站會根據裝置的 mac 地址決定是否提供下載服務,讓裝置在請求下載連結的時候,要求其在頭資訊裡提供 mac 地址。我們注意到 acceptNavigationRequest 的參數裡有 QWebNetworkRequest 的變數,這個類實際上就包含了頭資訊,雖然在這個變數在這裡是一個不可更改的引用,但是我們可以保留這個資訊,複製一份,在頭資訊裡加入 mac 資訊,然後讓 QWebView 主動進行一次下載請求,從而實現在頭資訊裡添加自訂內容的功能。
在頁面中執行自訂的 JavaScript 的方法
接著我們介紹另外一種應用情境:手持條碼機對準貨物的條碼,按鍵掃描之後,該貨物的資訊立刻在條碼機上顯示出來。
這個功能是一個很典型的網頁查詢應用,我們可以假設條碼是被手工輸入到網頁上的編輯框,然後 submit 一個請求,伺服器返回該條碼錶示的貨物資訊。所以,如果在按鍵掃描之後,條碼號能被填入網頁上的編輯框並且觸發一個 submit,這個功能就可以實現。
Qt 封裝的 WebKit 可以在已載入的頁面中插入執行使用者自定的 JavaScript,這是通過 QWebFrame 的 evaluateJavaScript 介面來實現。
QVariant QWebFrame::evaluateJavaScript ( const QString& scriptSource );
下面我們通過幾個例子來示範如何執行 JavaScript。
假設我們的頁面中有一個編輯框,名稱為“code”,它的旁邊還有一個按鈕名稱為“query”。掃描機對準條碼之後,使用者按下一個按鍵,觸發了 Qt 程式表單 form 中的一個訊息響應函數,在訊息響應函數中通過如下的語句可以設定編輯框中的內容:
清單 7. 設定編輯框內容的代碼實現
QWebFrame *frame = form.WebView->page()->mainFrame(); QString code = getScanCode (); // 調用掃描條碼的功能,需要自己實現 QString js = QString ("document.getElementById('code').value =\"%1\";" ).arg(code) ); frame->evaluateJavaScript ( js ); |
接下來可以用下面語句來實現觸發 query 按鈕:
清單 8. 觸發 query 按鈕的代碼實現
QWebFrame *frame = form.WebView->page()->mainFrame(); QString js = QString ( "document.getElementById('query').submit();" ); frame -> evaluateJavaScript ( js ); |
除了可以設定網頁上編輯框內容外,我們還可以通過下面的語句擷取編輯框中的內容:
清單 9. 擷取編輯框內容的代碼實現
QWebFrame *frame = form.WebView->page()->mainFrame(); QString s1 = frame->evaluateJavaScript ("document.getElementById ('code').name" ); |
這樣就解決了條碼機的貨物查詢功能所碰到的問題。我們可以讓頁面隨時運行我們自訂的 JavaScript,這個功能將發揮非常大的作用。它實際上解決了在由裝置進行主動觸發的的應用模式下,本地代碼和網頁進行配合的問題。但是這個方法只能用於特定的網頁,因為我們自己插入的 JavaScript 必須與網頁上運行環境匹配。
自訂 JavaScript 擴充的方法
接著我們介紹第三種應用情境:我們先考慮這樣一個問題,如果頁面的 JavaScript 代碼中需要得到本地應用的支援怎麼辦?比如一個機頂盒軟體需要配置本網(這種應用原本都是編寫本地應用程式實現的,但是既然我們討論採用 Web 方法來代替原有開發模式,就需要考慮如何在 Web 上實現)。首先,頁面需要擷取當前網路設定方式,IP 位址、子網路遮罩、DNS 等。在網頁上的編輯框、下拉框等控制項內顯示,使用者做了一些配置之後,點擊網頁上的“確定”按鈕,這些配置資訊就生效了。
從頁面的角度來說,這些都需要用 JavaScript 代碼來實現,那麼我們就需要讓 JavaScript 代碼能和本地代碼關聯起來。Qt 支援自訂的 JavaScript 擴充,也就是說使用者可以自己在 Qt 中定義一個對象,編譯到 WebKit 中。頁面中的 JavaScript 指令碼可以直接產生這個對象並且調用其方法。Qt 在目錄 \src\3rdparty\WebKit\WebCore\bridge\ 中提供了一個 demo。測試代碼在檔案 testqtbindings.cpp 中。我們可以參考他的方法的編寫自訂的類:
清單 10. 自訂類的實現代碼
class MyObject : public QObject { Q_OBJECT // 定義屬性和函數的關聯 Q_PROPERTY ( QString ip READ ip WRITE setIp ) public: MyObject (){} QString ip () { // 以字串方式返回 IP 位址的實現 }; void setIp( QString ) { // 設定 IP 位址的實現 }; }; // 通過如下的代碼來產生對象執行個體: MyObject* myObject = new MyObject; |
然後用下面的方法實現對象 myObject 和 JavaScript 中的對象 myInterface 的關聯:
清單 11. C++ 對象和 JavaScript 對象的關聯代碼
Global* global = new Global (); RefPtr<Interpreter> interp = new Interpreter ( global ); ExecState* exec = interp->globalExec (); // 實現 C++ 對象和 JavaScript 對象的關聯 global->put ( exec, Identifier("myInterface" ), Instance::createRuntimeObject ( Instance::QtLanguage, (void*)myObject) ); |
將 MyObject 的定義在 QWebFrame.h 中聲明,並且將清單 11 中的代碼加入到 QWebFrame 的建構函式中(QWebFrame.cpp)。QWebFrame.h 和 QWebFrame.cpp 兩個檔案在目錄 src\3rdparty\WebKit\WebKit\qt\Api 下。重新編譯 WebKit 模組之後,在網頁中就可以使用 myInterface 來調用對象 myObject 的方法了。調用的 JavaScript 代碼如下:
需要擷取 ip 的時候:
str = myInterface.ip;
需要設定 ip 的時候:
myInterface.Ip = str;
上面的代碼只是 ip 地址擷取和設定樣本,其他類似掩碼、dns 之類可以使用類似的方法。
任何嵌入式智慧型裝置的用 C/C++ 實現的本地功能,都可以通過上述方法讓 JavaScript 來進行調用。這種擴充能讓瀏覽器來解釋執行任何我們想要的功能,幾乎讓 Web 和本地代碼的結合完全掃除了障礙。將實現具體功能的本地代碼封裝成為庫和模組,然後由 Web 來進行上層架構和耦合,這將大大降低嵌入式 Linux 智慧型裝置的軟體開發難度。特別是對於經常要更新功能的應用,以前需要重新整理 Firmware,而現在只要更新遠程伺服器上的網頁就可以了。
編寫 WebKit plugin 的方法
除了自訂 JavaScript 對象之外,有時候我們還會用到自訂的網頁元素。在 PC 上,最典型的網頁元素就是 FLASH。FLASH 並不是 HTML 標準,但是可以用外掛程式的方式讓瀏覽器對這些特定的標籤進行解釋。
最後一種應用情境:以廣告機為例,之前我們提到過廣告機的視頻播放功能。不同平台播放視頻的方式都是不同的,而網頁也沒有像定義圖片一樣定義視頻播放的標準,因此廣告機作為區別於 PC 的嵌入式裝置,其視頻播放功能必須用本地代碼來實現。當我們用網頁方式來組織廣告機的螢幕,視頻部分就應該像圖片、文字一樣,成為網頁中的一個部分,可以通過 HTML 的定義來控制。HTML 提供了標籤“object”,來方便實現一些特別的對象。比如:
< object data="yahtzee.gif" type="image/gif" title="A Yahtzee animation" width=200 height=100 > |
如果我們的瀏覽器支援我們用類似方法在網頁中插入一個自訂對象,那麼這個問題就可以得到解決。
Qt 支援在 WebKit 中添加自訂的外掛程式。在檔案 FrameLoaderClientQt.cpp 中的函數 FrameLoaderClientQt::createPlugin 中,可以找到如下的程式碼片段:
清單 12. FrameLoaderClientQt.cpp 程式碼片段
if ( mimeType == "application/x-qt-plugin" || mimeType == "application/x-qt-styled-widget" ) { object = m_WebFrame->page()->createPlugin( classid, qurl, params, values ); |
通過上面的代碼,可以看出如果 HTML 中一個 object 將自己的 type 設定為 application/x-qt-plugin 或者是 application/x-qt-styled-widget,Qt 則會識別並要求 QWebPage 來建立外掛程式,其方式就是調用 QWebPage 的 createPlugin 函數,函數定義如下:
QObject *QWebPage::createPlugin ( const QString &classid, const QUrl &url, const QStringList ¶mNames, const QStringList ¶mValues ); |
我們設計以下的 HTML 來標識我們的對象:
<object type="application/x-qt-plugin" classid="VideoPlayer" width=800 height=600 file="kfc.avi" ></object> |
我們設定了在網頁中插入一個 VideoPlaye 的對象,並且設定了寬高、要播放的檔案等參數。因為我們設定了這個對象的 type 為 application/x-qt-plugin,所以當瀏覽器碰到這段 HTML 程式碼時,會調用到 QWebPage 的 createPlugin 功能。這個函數被要求返回一個表單。而這個表單會被當成一個標準網頁對象,和編輯框、下拉框等一樣被嵌入到 Web 頁面中。
我們先從 QWebPage 中派生自己的對象,實現 createPlugin:
清單 13. createPlugin 函數的實現
class QMyWebPage : public QWebPage { protected: QObject *createPlugin ( const QString &classid, const QUrl &url, const QStringList ¶mNames, const QStringList ¶mValues ); ... ... }; QObject* QMyWebPage::createPlugin ( const QString &classid, const QUrl &url, const QStringList ¶mNames, const QStringList ¶mValues ) { if ( classid == "VideoPlayer" ) { // 在這裡建立一個自訂的帶視頻播放功能的表單, VideoWindow* window = new VideoWindow(); // 配置參數如 width=800 等,會在參數 paramNames 和 paramValues 中傳過來 window->setGeometry( ........ ) ; window->setSourceFile( ...... ) ; return window; // 返回建立的表單 } ... } |
與截取 request 的方法一樣,我們要讓自己 QMyWebPage 被使用:
QWebView* Webview = new QWebView ( this ); QMyWebPage* page = new QMyWebPage (); Webview->setPage ( page ); // 讓 WebView 使用我們的 QMyWebPage |
注意載入頁面之前要開啟外掛程式使能的選項,方法如下:
QWebSettings* setting = Webview->settings (); setting->setAttribute ( QWebSettings::PluginsEnabled, true ); |
至此,我們建立了自己的網頁元素:類型為 VideoPlayer 的 object。網頁可以像使用標準網頁元素一樣,靈活的使用嵌入式平台自己特有的功能。當然,不一定非要把這個網頁元素用 application/x-qt-plugin 或者是 application/x-qt-styled-widget 來定義,Qt 也支援 type 不是這兩者或者以動態連結程式庫的方式來使用外掛程式,這樣就可以支援類似 FLASH 之類非 Qt 自訂的 object,關於這方面更多資訊可以參考 Qt 的文檔。