Ubuntu Weekly Recipe

第424回GUIプログラムをPython/Ruby/ECMAScriptで書く

先週の記事に対するお詫び
前回の記事において、Ubuntuプロジェクトでのパッケージの取り扱いについて、筆者の事実誤認による誤った記述がありました。主要な誤りは2点で、パッケージの分類基準と、Feature Freezeの説明です。他にも説明の誤りがあり、全面的に内容を修正しました。読者の皆様にはご迷惑をおかけしたことをお詫びいたします。

Ubuntuのデスクトップ環境は、ウェブブラウザなどユーザーが必要とするソフトウェアがデフォルトでインストールされているため、そのままでも十分に便利です。主要なシステム設定もマウスで行えるため、Unixライクな環境にありがちな「真っ黒な画面にコマンドをひたすら入力する」という、すなわちコマンドラインによる操作をしなくても一通りのことができます。

しかし一度困難に陥ると、真っ黒な画面での操作すなわちコマンドラインによる操作を要求されます。簡単なコマンドだったら苦になりませんが、オプションがたくさんあるコマンドは記憶を要求されるため面倒です。このような場合、うまくラップするようなシェルスクリプトを書くのが典型です。

このようなシェルスクリプトを第3者に渡す必要がある場合、作者である自分しか使い方を理解していないため、ドキュメント整備やヘルプの整備などの労力が発生します。こういう時、⁠あぁ、画面に案内を提示して、それをユーザーが操作する類のインタラクティブなインターフェイスだったらいいのに」と考えてしまいます。

本連載の第422回ではwhiptailとシェルスクリプトとの組み合わせで、インタラクティブなツールの作り方を紹介しました。この方法は手軽ですが、実現可能な処理がシェルスクリプトで書ける範囲となります。シェルスクリプトでも結構な処理が記述できますが、シェルスクリプトが苦手とするような処理を書く場合、この方法は使えません。せっかく凝ったことをするなら、自分がよく使っている言語で書きたいものです。

Gtk+3を利用したGUIプログラム

多くのユーザーが使用している言語の代表例は、Python 3、Ruby、ECMAScriptです。今回はそれらスクリプト言語で簡単なGUIをプログラムします。GUIツールキットとしてGtk+3を利用し、ウィンドウを描画します。

なお、実行環境はUbuntuデスクトップを前提としますので、Ubuntu PhoneやSnappy Ubuntu Coreではまた別の作法が必要であることに注意してください。

Python 3の場合

Python 3を使った場合のプログラムは、次のとおりです。

#!/usr/bin/env python3

import gi

gi.require_version('Gtk', '3.0')
from gi.repository import Gtk

from gi.repository import GLib
import signal

class Sample(Gtk.Window):
    def __init__(self):
        Gtk.Window.__init__(self)
        self.set_title('Gtk+3 sample')
        self.set_border_width(20)

        vbox = Gtk.Box()
        vbox.set_orientation(Gtk.Orientation.VERTICAL)
        vbox.set_spacing(10)
        self.add(vbox)

        topbox = Gtk.Box()
        topbox.set_spacing(10)
        vbox.pack_start(topbox, True, True, 0)

        button = Gtk.Button()
        button.set_label('swap')
        button.connect('clicked', self.on_click_swap)
        topbox.pack_start(button, True, True, 0)

        button = Gtk.Button()
        button.set_label('close')
        button.set_use_underline(True)
        button.connect('clicked', self.on_click_close)
        topbox.pack_start(button, True, True, 0)

        bottombox = Gtk.Box()
        bottombox.set_spacing(10)
        vbox.pack_start(bottombox, True, True, 0)

        self.entry = Gtk.Entry()
        self.entry.set_text('left')
        bottombox.pack_start(self.entry, True, True, 0)

        self.label = Gtk.Label()
        self.label.set_text('right')
        bottombox.pack_start(self.label, True, True, 0)

        self.connect('delete-event', Gtk.main_quit)

        GLib.unix_signal_add(GLib.PRIORITY_HIGH, signal.SIGINT, \
                             self.handle_unix_signal, None)

    def handle_unix_signal(self, user_data):
        self.on_click_close( None)

    def on_click_swap(self, button):
        label = self.label.get_text()
        self.label.set_text(self.entry.get_text())
        self.entry.set_text(label)

    def on_click_close(self, button):
        Gtk.main_quit()

win = Sample()
win.show_all()

Gtk.main()

コードの解説は後述します。python3.pyというファイルにこのプログラムを保存し、権限を付与して実行すると、以下のウィンドウが開きます。

$ gedit python3.py
$ chmod a+x python3.py
$ ./python3.py
図1 Python 3で書いたGtk+3アプリケーション
画像

Rubyの場合

Rubyを使った場合のプログラムは、次のとおりです。

#!/usr/bin/env ruby

require 'gtk3'

class Sample < Gtk::Window
    def initialize()
        super
        self.title = 'Gtk+3 sample'
        self.border_width = 20

        vbox = Gtk::Box.new(Gtk::Orientation::VERTICAL, 10)
        self.add(vbox)

        topbox = Gtk::Box.new(Gtk::Orientation::HORIZONTAL, 10)
        vbox.pack_start(topbox, :expand => true, :fill => true, :padding => 0)

        button = Gtk::Button.new(:label => 'swap')
        button.signal_connect('clicked') { |obj|
            label = @label.text
            @label.set_text(@entry.text)
            @entry.set_text(label)
        }
        topbox.pack_start(button, :expand => true, :fill => true, :padding => 0)

        button = Gtk::Button.new(:label => 'close')
        button.signal_connect('clicked') { |obj|
            Gtk::main_quit()
        }
        topbox.pack_start(button, :expand => true, :fill => true, :padding => 0)

        bottombox = Gtk::Box.new(Gtk::Orientation::HORIZONTAL, 10)
        vbox.pack_start(bottombox, :expand => true, :fill => true, :padding => 0)

        @entry = Gtk::Entry.new()
        @entry.set_text('left')
        bottombox.pack_start(@entry, :expand => true, :fill => true, :padding => 0)
        @label = Gtk::Label.new('result')
        @label.set_text('right')
        bottombox.pack_start(@label, :expand => true, :fill => true, :padding => 0)

        Signal.trap('INT') { |signo|
            Gtk.main_quit()
        }

        self.signal_connect('delete_event') {
            Gtk.main_quit()
        }
    end
end

win = Sample.new()
win.show_all()
Gtk::main()

このプログラムを実行するために、⁠ruby-gtk3」パッケージをインストールしてください。ruby.rbというファイルに保存し、権限を付与して実行すると、Python 3のサンプルとほぼ同じウィンドウが開きます。

$ sudo apt-get install ruby-gtk3
$ gedit ruby.rb
$ chmod a+x ruby.rb
$ ./ruby.rb

ECMAScriptの場合

ECMAScriptは、JavaScriptとして言及される雑多な言語実装の、共通文法となるべきものです。

ECMAScriptを使った場合のプログラムは、次のとおりです。

#!/usr/bin/gjs

const Gtk = imports.gi.Gtk;
const Lang = imports.lang;

const Sample = new Lang.Class ({
    Name: 'Gtk+3-Sample',
    Extends: Gtk.Window,
    _init: function() {
        let params = {
            title: 'Gtk+3 sample',
            border_width: 20,
        };
        this.parent(params);

        params = {
            orientation: Gtk.Orientation.VERTICAL,
        };
        let vbox = new Gtk.Box(params);
        this.add(vbox);

        params = {
            orientation: Gtk.Orientation.HORIZONTAL,
            spacing: 10,
        };
        let topbox = new Gtk.Box(params);
        vbox.pack_start(topbox, true, true, 0);

        params = {
            label: 'swap',
        };
        this.button = new Gtk.Button(params);
        this.button.connect("clicked", Lang.bind(this, this.on_click_swap));
        topbox.pack_start(this.button, true, true, 0);

        params = {
            label: 'close',
        };
        let button = new Gtk.Button(params);
        button.connect('clicked', Lang.bind(this, this.on_click_close));
        topbox.pack_start(button, true, true, 0);

        params = {
            orientation: Gtk.Orientation.HORIZONTAL,
            spacing: 10,
        };
        let bottombox = new Gtk.Box(params);
        vbox.pack_start(bottombox, true, true, 0);

        this.entry = new Gtk.Entry();
        this.entry.text = 'left';
        bottombox.pack_start(this.entry, true, true, 0);

        params = {
            label:  'right',
        };
        this.label = new Gtk.Label(params);
        bottombox.pack_start(this.label, true, true, 0);
    },

    on_click_swap: function() {
        let label = this.label.label;
        this.label.label = this.entry.text;
        this.entry.text = label;
    },

    on_click_close: function() {
        Gtk.main_quit();
    },
});

Gtk.init(null);
var win = new Sample();
win.show_all();
Gtk.main();

このプログラムを実行するために、⁠gjs」パッケージをインストールしてください。ecmascript.jsというファイルに保存し、権限を付与して実行すると、他のサンプルとほぼ同じウィンドウが開きます。

$ sudo apt-get install gjs
$ gedit ecmascript.js
$ chmod a+x ecmascript.js
$ ./ecmascript.js

なお、gjsはGNOMEプロジェクトの成果物です。GNOME Shellというデスクトップシェルを構成するスクリプトの実行環境として開発されています。ECMAScriptを処理するエンジンとしてMozillaプロジェクトのSpiderMonkeyを利用しています。

また、今回のプログラムでは、GNOMEプロジェクトが提供するLangというラッパースクリプトを利用し、Javaのようなクラス志向のプログラムとして読めるようにしました。ECMAScriptらしくプロトタイプチェーン操作を記述することも考えましたが、GNOME Shell向けスクリプトの作法に従いました。その他の作法や便利な抽象は、他のサンプルとの比較を容易とするためにあえて除外しました。GNOME Shell向けスクリプトとして評価するには原始的であることに注意してください。

共通処理について

Python 3/Ruby/ECMAScriptで記述されたプログラムを示しましたが、これらのコードを比較すると、次の2つの共通点を見出せます。

  1. windowの機能を実装したオブジェクトを作成し、Gtk.main()を呼ぶ。
  2. そのオブジェクトは初期化の過程で、レイアウトを決定されてウィジェットやイベントハンドラを付与される。

この2つの共通点について説明します。

ウィジェットの配置

まずは2番目についてです。今回のサンプルプログラムで使うBoxは、window内のレイアウトを決めるためのものです。ButtonやEntryはユーザーが操作するGUIパーツで、ウィジェットと呼ばれます。サンプルでは、次の流れで上下2段のレイアウトを作成し、合計4つのウィジェットを配置しています。

  1. vboxとして垂直ボックスを生成し、ウィンドウ内に配置。
  2. topboxとして水平ボックスを生成し、vboxに配置。
  3. buttonとしてボタンを生成し、topboxに配置。
  4. buttonとしてもうひとつボタンを生成し、topboxに配置。
  5. bottomboxとして水平ボックスを生成し、vboxに配置。
  6. entryとしてテキストエントリーを生成し、bottomboxに配置。
  7. labelとしてテキストラベルを生成し、bottomboxに配置。

2つのボタンには、クリック時の処理を記述した処理を登録しました。このような処理のことをイベントハンドラと呼びます。Gtk+3の作法では、connect()やsignal_connect()と言ったメソッドを使い、イベントハンドラとイベントの関連付けを行います[1]⁠。

Gtk.main()でのループ処理

共通点の1番目で言及しているGtk.main()は、イベントループを回す関数です。プログラムが実行されここに到達すると、ループして終了しなくなります。ループ内では、ユーザーからの入力やデバイスへの出力、他のプロセスへの通信といったイベントを捕捉し、対応するハンドラを実行します。イベントループはこの処理を、システムに過度の負荷をかけることを避けつつ延々と行っています。

今回のサンプルで実装したイベントハンドラは、このイベントループ内で呼ばれます。サンプル内のイベントループはひとつなので、どう操作しようがイベントハンドラを同時に実行することはできない点に留意してください。すなわち、あるイベントハンドラがイベントループに処理をなかなか戻さないようにコード修正した場合、その間他のイベントを捕捉できませんので、アプリケーションの実行が止まったように見えるという問題が発生します。

ループに突入するとプログラムは終了しなくなりますが、それだと非常に不便です。今回のサンプルではプログラムを終了するためのチュートリアルとして、closeボタンを実装しています。またPython 3とRubyのサンプルでは、Unixシグナルに対するハンドラを設定しました。サンプルを実行した端末上でキーボードのCtrlキーとCキーを同時押しすると、プログラムがSIGINTを捕捉してループを抜けます。gjsの場合は、プログラムしなくてもUnixシグナルを捕捉して終了するようです。

Qt5を利用したGUIプログラム

前章で確認した共通の処理は、他のプログラミング言語やGUIツールキットにも見出すことができます。

そこで、Python 3のQt5バインディングを利用してプログラムを書いてみました。

#!/usr/bin/env python3

from PyQt5.QtWidgets import QApplication, QWidget, QHBoxLayout, QVBoxLayout
from PyQt5.QtWidgets import QToolButton, QGroupBox, QLineEdit, QLabel

from gi.repository import GLib
import signal

class Sample(QWidget):
    def __init__(self, parent=None):
        super(Sample, self).__init__(parent)

        self.setWindowTitle("PyQt5 sample")

        layout = QVBoxLayout()
        self.setLayout(layout)

        top_grp = QGroupBox(self)
        top_layout = QHBoxLayout()
        top_grp.setLayout(top_layout)
        layout.addWidget(top_grp)

        buttom_grp = QGroupBox(self)
        buttom_layout = QHBoxLayout()
        buttom_grp.setLayout(buttom_layout)
        layout.addWidget(buttom_grp)

        button = QToolButton(top_grp)
        button.setText('swap')
        top_layout.addWidget(button)
        button.clicked.connect(self.on_click_swap)

        close = QToolButton(top_grp)
        close.setText('close')
        top_layout.addWidget(close)
        close.clicked.connect(app.quit)

        self.entry = QLineEdit(buttom_grp)
        self.entry.setText('left')
        buttom_layout.addWidget(self.entry)

        self.label = QLabel(buttom_grp)
        self.label.setText('right')
        buttom_layout.addWidget(self.label)

        GLib.unix_signal_add(GLib.PRIORITY_HIGH, signal.SIGINT, \
                             self.handle_unix_signal, None)

    def handle_unix_signal(self, user_data):
        app.quit()

    def on_click_swap(self):
        label = self.label.text()
        self.label.setText(self.entry.text())
        self.entry.setText(label)

app = QApplication(list())
sample = Sample()

sample.show()
app.exec()

このプログラムを実行するために、⁠python3-pyqt5」パッケージをインストールしてください。プログラムを実行すると、Gtk+3を使ったサンプルと似たウィンドウが開きます。

$ sudo apt-get install python3-pyqt5
$ gedit qt5.py
$ chmod a+x qt5.py
$ ./qt5.py

利用した実装環は完全に異なりますが、レイアウトの決定、ウィジェットの配置、イベントループ、Unixシグナルの処理[2]といった考え方は、Gtk+3と同じです。

今回のプログラムについて

今回掲載したプログラムのライセンスはすべてパブリックドメインです。

また、それぞれのプログラムが利用している実装に関心のある方は以下のリファレンスを参照してください。

おすすめ記事

記事・ニュース一覧