[ネタ] QWebViewでGUIアプリケーションを作る
- 2015/02/28
- C++
CocoaTwit
以前から何度かつぶやいていた自作Twitterクライアント, CocoaTwit-QWebViewを公開しました.
A Simple and Cute Twitter Clientをコンセプトに, シンプルなデザインかつ細かな拡張のしやすいTwitterクライアントを目指しています.
また, 僕が今まで使ってきたTwitterクライアントの手が届かなかったところを改善して実装していこうと思っています.
ちなみにCocoaTwitという名前に特別な意味は無いです. ココアさんかわいい.
バイナリ配布は今のところ予定していませんが, 気になるLinux使いの方はビルドしてみてください.
(Macでもイケると思ったけど, twitppがApple Clangでビルドできなかった)
この記事で紹介する手法はネタです
正月くらいからこのCocoaTwitをタイトル通りの手法で開発していました.
しかし, もっとよさ気な方法を見つけたため設計を大幅変更することにしました1.
この時点でQWebView版CocoaTwitも本記事もある程度出来上がっており, 消してしまうのがもったいなかったためネタ記事として公開することにしました.
**こんなブッ飛んだこともできるんだな〜**って感じで読んでもらえると良いかと思います.
C++でGUI is つらい
昨年からtwitppを書いていますが, これはC++でTwitterクライアントが作りたかったためです.
それに伴い以前からよさ気なGUIライブラリを探していたのですが, なかなか良いものが見つかりません.
C++には有名なQtをはじめwxWidgetsやFLTK, gtkmmなどのGUIライブラリが存在2しますが, これらの中から
- クセの少なく
- マクロを多用していなくて
- 他のC++コードとイイ感じに組み合わせられる
ものに絞っていくと, うーん…
悩んだ末, 最近よく耳にしていて個人的に使ってみたかったライブラリでもあったQt5を使うことにしました.
Qtの微妙だった点
STLと非互換なライブラリ
Qtは独自の文字列型であるQString
をはじめQVector
やQList
などのSTLに代わるクラスを提供しており, そしてGUI周りのクラスがそれらに依存しています.
そのため, 今回のような非Qtなライブラリと組み合わせて使おうとすると独自型とのキャストが多発し, あまり気分の良いものではありません.
CocoaTwitでは特にstd::string
とQString
の相互変換が多発しました.
僕はこんな感じに対処しましたが, もっと良い物があれば教えてもらえると嬉しいです.
// std::string -> QString
std::string foo("foo");
QString bar = QString::fromStdString(foo);
// QString -> std::string
QString hoge = "hoge";
std::string fuga(hoge.toUtf8().constData());
マクロの多用
Qtの代表的な機能であるSignal/Slotなどの実現には魔術(マクロ等)が多用されています.
別にただ単にマクロが使われている分には良いのですが, 実際に副作用的なものに遭遇したため許せませんでした.
特にQ_OBJECT
マクロが曲者です.
まず, Q_OBJECT
が書かれたクラス宣言のあるヘッダファイルでboost/asio.hpp
をincludeするとビルドが通らなくなることがある現象3に遭遇し1ヶ月悩みました.
そして, 2015年の目標に書いたようにtemplateを使ったプログラムに力を入れたかったのですが, Q_OBJECT
の使われたクラス(Qt関連のすべてのクラス)ではClass Templateが使えない4と知って本当にショックでした.
命名規則の違い
QtはcamelCaseなクラス/関数を提供しています.
しかし, C++標準ライブラリやBoostに合わせてsnake_caseでコードを書いてきた僕にとっては物凄い違和感に苦しめられました.
命名規則は大体はその言語の標準ライブラリに合わせるのがBestかなと思っているのですが, どうしてこう外部C++のライブラリはcamelCaseのものが多いのか…
まぁコンパイラから見れば関係ないんだろうけどさぁ…
複雑なUIを組みたい
camelCaseな書き方を我慢するとしても, マクロの副作用に気をつけたとしても, Class Templateを諦めたとしても, 実際にQtでTwitterクライアント開発を始めて行くと大きな壁にぶつかりました. Widgetの拡張です.
単純にQPushButton
等の用意されたWidgetを配置していく程度のアプリケーションなら何の問題もないのですが, 今回作成したいのはTwitterクライアントです.
例えばタイムライン表示を実装するとします. TweetDeckほど複雑な表示でなくとも, 最低限アイコンとscreen_name, tweet本文を独立して表示できるListViewが欲しいわけです.
QtにこのようなWidgetは用意されていないので, ちょっと弄る必要があります.
QtのListViewであるQListView
を拡張するには, 必要に応じてQAbstractListModel
等を継承した独自のModelを作成し, またQStyledItemDelegate
を継承したクラスを適用させてQListView
のItemをPaintするときの動作を上書きしていくようです. 詳しくはこちら.
これらの情報を元に, 年末にいろいろ弄ってみました.
しかし, 僕のQt力が足りないことや, 雑に書いたのもありますがどうも不安定で, コードが予想以上に複雑に…
ここから表示する情報を増やしたり, Tweetの行数に合わせて高さを可変させるようにしたり, デザインよくしたりと考えていくともう…_(:3 」∠)_
それっぽいのできた pic.twitter.com/7jZU0Iv95o
— 不正競争防止法 (@myon\_\_\_) December 31, 2014
ちなみに, QListView
とQStandardItemModel
を利用すると, アイコン付きのListは簡単に作れました.
しかし, 画面端での文字折り返しがどうしてもうまくいかず諦めました. (標準Widgetでは不可能…?)
QListView* l = new QListView();
QStandardItemModel* model = new QStandardItemModel();
l->setModel(model);
l->setEditTriggers(QAbstractItemView::NoEditTriggers);
l->setIconSize(QSize(64, 64));
model->insertRow(0, new QStandardItem(QIcon("icon.png"), "myon!!"));
QtでUserstream流せた∩(>◡<\*)∩ pic.twitter.com/bgbyyh6r7d
— 不正競争防止法 (@myon\_\_\_) November 14, 2014
HTMLでUIを書くという選択肢
さて, 長くなりましたが本題です.
なんとかいい感じに, 思い通りのUIを設計できる方法がないかと探っていると, たまたま開いたQtのドキュメントでQWebView
というクラスがあるのを見つけます.
このクラスは名前の通りWebドキュメントを表示したり編集したりするためのものです.
また, QWebView
に関連したクラスであるQWebFrame
に, addToJavaScriptWindowObject()
という何とも怪しいメンバ関数があるのを見つけます.
このメンバ関数は, なんとQObject
を継承しているObjectをQWebView
などで表示しているページ内のJavaScript Objectとして追加できるという物凄いものでした.
そして最後に, 上記のメンバ関数の利用例を示したThe Qt WebKit Bridgeというドキュメントにたどり着きます.
このページには, QWebView
内で動くJavaScriptとC++なコードを結びつけてアプリケーションを作る手法のあれこれが記されています.
あっ, これってつまり… GUI周りをすべてHTMLで実現できるってことじゃね!!??
ということで, 上記手法を用いたGUIアプリケーション作成方法について紹介していこうと思います.
Hello World!
とりあえずQWebView
を使ってみます.
$ qmake
$ make
$ ./QWebView_example
これだけです. 簡単ですね.
ちなみに, main.cc
の8行目のR"( ... )"
はC++11からのRaw String Literal5と呼ばれるものです.
C++からDOM
QWebView
の中でJavaScriptを動かすって方法もありますが, あくまで今回のメインはC++です.
DOM要素を扱うQWebElement
を使うことでいろいろできます.
Sample: Tosainu / QWebView_dom.pro
要素を取得する
JavaScriptでいうdocument.getElementById
等に相当するようなものです.
例えばlist
クラスのついた<ul>
を取得するにはこんな感じに書きます.
auto list = webview->page()->mainFrame()->findFirstElement("ul.list");
そのリストの最初の子要素を取得するならこんな感じ.
auto item = webview->page()->mainFrame()->findFirstElement("ul.list li");
要素を挿入する
取得した要素に新しい要素を追加してみます.
以下のようなHTMLで, (a)の位置に挿入するにはQWebElement::prependInside()
, (b)の位置に挿入するにはQWebElement::appendInside()
を使います.
<ul class="list">
<!-- (a) -->
<li>もともと存在する要素</li>
<!-- (b) -->
</ul>
auto list = webview->page()->mainFrame()->findFirstElement("ul.list");
list.prependInside("<li>" + QTime::currentTime().toString() + "</li>");
list.appendInside("<li>" + QTime::currentTime().toString() + "</li>");
ここで注意したいのが, QWebFrame::findFirstElement()
が返す値はポインタではないということです.
周りと同じようにアロー演算子(->
)を使ったりするとビルドできません.
取得した要素を削除する
取得した要素を削除するにはQWebElement::removeFromDocument()
を使います.
auto item = webview->page()->mainFrame()->findFirstElement("ul.list li");
item.removeFromDocument();
クラスを追加/削除する
要素のクラスを追加するにはQWebElement::addClass()
, 削除するにはQWebElement::removeClass()
を使います. そのままですね.
auto list = webview->page()->mainFrame()->findFirstElement("ul.list");
list.addClass("hide");
list.removeClass("hide");
Signal/Slot
QtといったらSignal/Slotでしょう.
QWebFrame::addToJavaScriptWindowObject()
の力を借りながらQWebView
内のHTMLとC++のコードを繋いでみます.
HTML -> C++
表示しているHTMLからQObject
を継承したJsObj
クラスのSlotであるhogeSlot()
を呼び出してみます.
Sample: Tosainu / QWebView_html-to-cpp.pro
auto jo = new JsObj();
webview->page()->mainFrame()->addToJavaScriptWindowObject("jsobj", jo);
このように記述すると, jo
がjsobj
という名前のJavaScript contextとしてページに放り込まれます.
例えばページ内の<button>
からhogeSlot()
を呼び出すには, onclick=""
にこのように記述してやります.
<button onclick="jsobj.hogeSlot();">Click!</button>
上に挙げたサンプルプログラムでは, ページに配置されたClick!ボタンをクリックすると, こんな感じで**QMessageBox::information()**が表示されたと思います.
C++ -> HTML(JavaScript)
今度はその逆, C++上のSignalが発火したらQWebView
内のJavaScript関数が実行されるようにしてみます.
Sample: Tosainu / QWebView_cpp-to-html.pro
同様にjsobj
を追加したうえで, このような文法でJavaScriptの関数fugaSlot
をC++上のhogeSignal
にconnectします.
var fugaSlot = function() {
location.href = 'http://myon.info/';
};
jsobj.hogeSignal.connect(fugaSlot);
サンプルプログラムでは, 下部のQPushButtonをクリックするとhogeSignal
が発火するようにしてありますので, ボタンをクリックするとhttp://myon.info/に飛んだと思います.
値の受け渡し
関数が相互に呼び出せるようになったので, 今度は値のやり取りをしてみます.
Sample: Tosainu / QWebView_exchange-value.pro
やり取りする相手がJavaScriptですが, なにか特別な書き方が必要というわけではありません.
サンプルでは文字列の受け渡ししかしていませんが, The Qt WebKit Bridge | Qt 5.4によると以下のような型が扱えると紹介されています. (面倒で全て試せてない)
形式 | Qtでの型 |
---|---|
数値 | int, short, float, double等 |
文字列 | QString |
日付/時刻 | QDate, QTime, QDateTime |
正規表現 | QRegExp |
配列 | QVariantList, QStringList, QObjectList, QList等 |
JSON | QVariantMap等 |
その他 | QVariant, QObject, QWidget, QImage, QPixmap等 |
またSignal/SlotのOverloadも可能なほか, 以下のように呼び出すSlotを直接指定して呼び出すこともできるようです.
QString
を受け取るSlotに数値を渡したところ綺麗に変換(std::stoi()
みたいな)されたのはおもしろかったです.
myQObject['myOverloadedSlot(int)']("10");
myQObject['myOverloadedSlot(QString)'](10);
まとめ
僕と似たような意見を持っている方を見つけたのでリンクを貼っておきます.
Why all GUI toolkits suck? - C++ Forum
とりあえず, 当分の間QtってるC++書きたくない.
Footnotes
-
ただし作者は当分Qtってるコードを書きたくない模様 ↩
-
https://svn.boost.org/trac/boost/ticket/10688 includeする場所をソースファイルに変えると解決する ↩
-
C++11: Syntax and Feature - 2.8.4.3 生文字列リテラル(Raw String Literal) ↩