================
ruby-qmlという、QML / Qt Quickを使ってUIを記述し、Rubyで書いたロジックと組み合わせてGUIアプリを作ることを可能にするgemを作りました (gem名は'qml') 。
https://github.com/seanchas116/ruby-qml で公開しています。
今のところ、Ruby 2.0以降・OS X/Linuxに対応しています。
[追記] Qtは 5.2 以降に対応しています。
[さらに追記] Ruby 1.9.3に対応しました。
[2014-10-14] QML.application do ~
をQML.run do ~
に修正しました。
何ができるか?
ruby-qmlでできるのは、
- クロスプラットフォームのUI記述言語であるQMLを使ってQt QuickのGUIを書く
- Rubyでアプリのロジック部を書く
- それらを組み合わせてアプリを作る
さらに、
- Qt + C++で書いたコードをRubyから使う
ことです。
これによって、 RubyとQMLのみでアプリを作る ことや、 UI部分をQMLで記述し、ロジック部分をRubyとC++で分担する ことが可能になり、Qt QuickやRubyを使った開発の幅が広がることを目指しています。
Hello, World!
import QtQuick 2.2
import QtQuick.Controls 1.1
ApplicationWindow {
visible: true
width: 200
height: 100
title: "Hello, world!"
}
QML.run do |app|
app.load_path Pathname(__FILE__) + '../main.qml'
end
$ ruby hello.rb
なぜ作ったか?
Qtについて
Qtは、クラスプラットフォームでのGUIアプリ開発によく使われるフレームワーク(ライブラリ)です。
各環境のネイティブGUI開発環境と違い、プラットフォーム間の違いを吸収し、同じソースコードで様々なプラットフォーム用のアプリをビルドすることができるのが売りです。
Qtは、デスクトップ環境(Windows, Mac, X11など)でのアプリ開発に使われることが多いのですが、最近ではiOSやAndroidなどのモバイルプラットフォームへの対応も進められています。
QMLとQt Quickについて
Qtの開発言語には伝統的にはC++が使われてきて、GUI部品もC++で配置・実装する仕組み(Qt Widgets)でした。
しかし、 バージョン4.7から、QMLという専用の言語とJavaScriptを使ってUI記述を行う仕組み(Qt Quick)が導入され、
ビューはQML+JavaScript、ロジックはC++といった開発言語の組み合わせが使われるようになりました。
QMLはUI記述に特化した言語で、HTMLやCSSの要領でUIオブジェクトの配置を記述し、JavaScriptでロジックを記述します。
データバインディングの一種として、オブジェクトのプロパティの値を連動させる プロパティバインディング もQMLの有名な機能です。
C++で書いたオブジェクト(QObject)を、QMLのオブジェクトと同様に扱うことも可能になっており、C++で書いたロジックをQML内でシームレスに使うこともできます。
QMLの例
import QtQuick 2.3
import QtQuick.Controls 1.1
import QtQuick.Layouts 1.1
ApplicationWindow {
visible: true
width: 200
height: 100
ColumnLayout {
anchors.fill: parent
anchors.margins: 10
Label {
text: "Text input: " + textField.text
}
TextField {
id: textField
}
Button {
text: "Click me"
onClicked: {
console.log("clicked!!")
}
}
}
}
C++以外の言語でもロジックを書きたい
Qt Quickが導入されたことで、Qtではビューの記述を直感的に行うことが可能になりました。
一方、ロジックの記述はC++が必要なのは変わりませんでした。
Qt C++ライブラリのバインディング
C++は、Qtのような資産が数多くある言語なのですが、複雑で書きにくいことでも有名です。
そこで、QtがC++のみで開発されていた時代から、QtのC++以外の言語からのバインディングが多く開発されてきました。
例えば、qtrubyというruby向けのQtバインディングです(Qt4版のみでQt5版はまだありません)。
しかし、C++は静的な言語で、メソッド呼出などがコンパイル時に解決されてしまい、リフレクション機能もありません。
(リフレクション機能は現在C++標準化委員会で議論が行われているようですが)
結果的に、C++のバインディングは、専用のツールで生成することになり、開発やメンテナンスが大変です。
QML / JavaScriptなら楽にバインディングが書ける
一方、Qt Quickで導入されたQML / JavaScriptは動的な言語でリフレクションも可能なため、
バインディングが比較的書きやすいという特徴があります。
そこで、QML用のバインディングを書いて、C++で書かれたロジックを他の言語で置換するといったことが容易になります。
例えば、go-qml (QMLのGoバインディング)などが開発されています。
RubyでもQt Quickのロジックが書きたい
そこで、Ruby用のQt Quickバインディング(ラッパ)であるruby-qmlの開発をはじめることにしました。
Qt QuickアプリのロジックをRubyのようなスクリプト言語で書けるようになれば、気楽にGUIアプリを開発できるようになったり、
RubyスクリプトにGUIを付けることが容易になります。
似たフレームワーク
ruby-qmlと似たフレームワークとしては、
-
Kivy
- QMLと似たようなUI記述言語を使ってPythonでGUIアプリ開発をするフレームワーク
-
node-webkit
- HTML上のJavaScriptからNode.jsを使える
- HTML+JSでネイティブアプリ開発
- ビューをHTMLで書くのである意味似ている
などがあります。
ruby-qmlの利点は
- 実績のあるQtをベースにUIが作れる
- C++との組み合わせが容易 (後述)
などで、欠点は
- UI側であるQMLとロジック側のRubyで言語が合わせられない (QML側はJavaScript)
などです。
ruby-qmlの機能
QMLのロードとアプリケーションのスタート
QML.run
メソッドのブロック内に、アプリケーションの初期化処理を記述します。
load_path
でQMLファイルのパスを指定してロードします。
QML.run do |app|
app.load_path Pathname(__FILE__) + '../main.qml'
end
RubyのオブジェクトをQML内で使う
RubyのオブジェクトをQML内でインスタンス化して、
プロパティバインディングやJSなどを使ってQMLのUI要素と連携させることができます。
QML::Access
モジュールをincludeしたクラスは、QML内でオブジェクトとして使えるようになり、
Ruby側で、QMLのようなプロパティやシグナルを宣言することもできるようになります。
これによって、QMLからメソッド呼出やプロパティバインディング、シグナルの接続を行うことができます。
#register_to_qml
メソッドをクラス内で呼び出すと、クラスがQMLの型名として登録されるので、
QML内でインスタンス化することができるようになります。
例 (テキスト入力欄に数値を入れるとリアルタイムにFizzBuzz結果が表示される)
class FizzBuzz
include QML::Access
register_to_qml under: "Example", version: "1.0"
property :input, '0'
property :result , ''
on_changed :input do
i = input.to_i
self.result = case
when i % 3 == 0 && i % 5 == 0
"FizzBuzz"
when i % 3 == 0
"Fizz"
when i % 5 == 0
"Buzz"
else
i.to_s
end
end
def quit
puts "quitting..."
QML.application.quit
end
end
QML.run do |app|
app.load_path Pathname(__FILE__) + '../main.qml'
end
import QtQuick 2.3
import QtQuick.Controls 1.1
import QtQuick.Layouts 1.1
import Example 1.0
ApplicationWindow {
visible: true
width: 200
height: 100
title: "FizzBuzz"
ColumnLayout {
anchors.fill: parent
TextField {
id: textField
}
Label {
y: 100
id: text
text: fizzbuzz.result
}
Button {
text: 'Quit'
onClicked: fizzbuzz.quit()
}
}
FizzBuzz {
id: fizzbuzz
input: textField.text
}
}
QML・C++のオブジェクトをRuby内で使う
逆に、QMLのオブジェクトやC++のオブジェクト(QObject派生)をRuby内で使うこともできます。
Item {
property var hoge
function foo() {
console.log("foo")
}
}
class Foo : public QObject {
Q_OBJECT
Q_PROPERTY(int hoge MEMBER m_hoge)
public:
Q_INVOKABLE void foo();
private:
int m_hoge = 0;
};
item.hoge = 10
item.hoge #=> 10
item.foo()
Ruby側で用意したListModelをQML内で使う
QMLのListViewとRubyのデータをバインドするためのListModelを用意することができます。
QML::Data::ArrayModel
は内部で配列を使用しており、
Ruby側で要素の追加や削除、変更を行うと、QMLのListViewに自動的に反映されます。
QML::Data::ListModel
を継承することで様々なListModelを作ることができ、
ListModelの要素をSQLなどから用意したり、遅延的にデータを読み込むことも可能です。
そこで、QML::Data::QueryModel
という、SQLなどからデータを取得するListModelも用意してあります。
class TodoController
include QML::Access
register_to_qml under: "Example", version: "1.0"
property :model, QML::Data::ArrayModel.new(:title, :description, :due_date)
def add(title, description, due_date)
item = {
title: title,
description: description,
due_date: due_date
}
p item
model << item
end
end
ListView {
model: todo.model
delegate: Text {
text: "Title: " + title + ", Description: " + description + ", Due date: " + due_date
}
}
TodoController {
id: todo
}
値の変換
数値、文字列、日時、配列、連想配列などは自動的に変換されます。
QML、C++側のQObjectは自動的にRubyオブジェクトにラップされ、
include Access
したRubyのオブジェクトも自動的にQObjectにラップされます。
QtのC++プラグインのロード
ruby-qmlでは、C++で書いたQtのプラグインをロードして、Ruby内で使うこともできます。
これによって、処理の一部ををC++で記述することが容易になっています。
ruby-qmlの実装でも、一部にC++のプラグインで書かれている部分があります。
応用例 (Sequelと組み合わせて簡単なTodo一覧アプリ)
応用例として、ORMであるSequelと組み合わせて簡単なTodo一覧アプリを作りました。
(この例はGitHubにもあります。)
ApplicationWindow {
visible: true
title: "Todo with Sequel"
FontLoader {
source: "../assets/fonts/fontawesome-webfont.ttf"
}
id: window
property int margin: 10
width: layout.implicitWidth + 2 * margin
height: layout.implicitHeight + 2 * margin
RowLayout {
id: layout
anchors.fill: parent
anchors.margins: window.margin
ColumnLayout {
TextField {
placeholderText: "Title"
id: titleField
}
TextField {
placeholderText: "Description"
id: descriptionField
}
Calendar {
id: calendar
}
Button {
text: "Add"
onClicked: todo.add()
}
}
ColumnLayout {
RowLayout {
Label { text: "Sort by" }
ComboBox {
id: orderComboBox
model: ListModel {
ListElement { text: "Title"; column: "title" }
ListElement { text: "Description"; column: "description" }
ListElement { text: "Due Date"; column: "due_date" }
}
property string currentColumn: model.get(currentIndex).column
}
}
ListView {
model: todo.model
spacing: 10
Layout.fillWidth: true
Layout.fillHeight: true
Layout.alignment: Qt.AlignTop
Layout.minimumWidth: 300
delegate: ColumnLayout {
Text {
font.bold: true
text: title
}
Text {
text: description
}
RowLayout {
Text {
font.family: "FontAwesome"
text: "\uf073"
}
Text {
text: Qt.formatDate(due_date)
}
}
}
}
}
}
TodoController {
id: todo
title: titleField.text
description: descriptionField.text
due_date: calendar.selectedDate
order_by: orderComboBox.currentColumn
}
}
require 'qml'
require 'sequel'
module Examples
module Todo
VERSION = '0.1'
DB = Sequel.sqlite
DB.create_table :todos do
primary_key :id
String :title
String :description
Date :due_date
end
class SequelModel < QML::Data::QueryModel
attr_accessor :dataset
def initialize(dataset)
@dataset = dataset
super(*dataset.columns)
end
def query_count
@dataset.count
end
def query(offset, count)
@dataset.offset(offset).limit(count).all
end
end
class TodoController
include QML::Access
register_to_qml
def initialize
super
@todo_dataset = DB[:todos]
self.model = SequelModel.new(@todo_dataset)
end
property :title, ''
property :description, ''
property :due_date, ''
property :order_by, ''
property :model
def add
@todo_dataset.insert(title: title, description: description, due_date: due_date)
model.update
end
on_changed :order_by do
model.dataset = @todo_dataset.order(order_by.to_sym)
model.update
end
end
end
end
QML.run do |app|
app.load_path Pathname(__FILE__) + '../main.qml'
end