RubyとQML/Qt Quickでデスクトップ用GUIアプリを書けるgem「ruby-qml」を作った

================

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!

```qml:main.qml
import QtQuick 2.2
import QtQuick.Controls 1.1

ApplicationWindow {
    visible: true
    width: 200
    height: 100
    title: "Hello, world!"
}
```

```ruby:hello.rb
QML.run do |app|
  app.load_path Pathname(__FILE__) + '../main.qml'
end
```

```
$ ruby hello.rb
```

なぜ作ったか？
----------------


### Qtについて

[Qt](http://qt-project.org/)は、クラスプラットフォームでの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の例

```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](https://github.com/go-qml/qml) (QMLのGoバインディング)などが開発されています。

### RubyでもQt Quickのロジックが書きたい

そこで、Ruby用のQt Quickバインディング（ラッパ）であるruby-qmlの開発をはじめることにしました。
Qt QuickアプリのロジックをRubyのようなスクリプト言語で書けるようになれば、気楽にGUIアプリを開発できるようになったり、
RubyスクリプトにGUIを付けることが容易になります。

### 似たフレームワーク

ruby-qmlと似たフレームワークとしては、

- [Kivy](http://kivy.org/)
  - QMLと似たようなUI記述言語を使ってPythonでGUIアプリ開発をするフレームワーク
- [node-webkit](https://github.com/rogerwang/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ファイルのパスを指定してロードします。

```ruby
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結果が表示される)

![capture.png](https://qiita-image-store.s3.amazonaws.com/0/36226/8eeb8a3f-b69e-637b-38f9-c33297402c6d.png "capture.png")


```ruby
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
```

```qml:main.qml
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内で使うこともできます。

```qml
Item {
  property var hoge
  function foo() {
    console.log("foo")
  }
}
```

```c++
class Foo : public QObject {
  Q_OBJECT
  Q_PROPERTY(int hoge MEMBER m_hoge)
public:
  Q_INVOKABLE void foo();
private:
  int m_hoge = 0;
};
```

```ruby
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も用意してあります。


```ruby
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
```

```qml
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](http://sequel.jeremyevans.net/)と組み合わせて簡単なTodo一覧アプリを作りました。

(この例は[GitHub](https://github.com/seanchas116/ruby-qml/tree/master/examples/todo_sequel)にもあります。)

![capture.png](https://qiita-image-store.s3.amazonaws.com/0/36226/ac25cb77-8633-5777-cbee-41038fde2b56.png "capture.png")


```qml:main.qml
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
    }
}
```

```ruby:todo_sequel.rb
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
```