各種スクリプトのマルチスレッドでQtを使用する

Qtは多くのウィンドウシステムの例にもれずイベントドリブンのGUIを採用しているので、ユーザが何かを入力するまで待っているのが基本となる。
一般的なツールであればそれだけで良いが、ゲームなどのようにユーザの入力を待ちながら別の処理を行いたいこともある。
そんなときは、スレッドを利用するのが最も楽である。
以下に各種スクリプトによる簡単な例を示す。

例の内容
1秒間に1回「count n」と表示し続けながら、ボタンが押されたら「Push!」と表示する。

Rubyの場合

#!/usr/bin/ruby

require "qte"
include Qte
require "qpe"
include Qpe

class MainWindow < QWidget 
    def initialize(*args)
        super
        vb = QVBoxLayout.new(self)
        b1 = QPushButton.new("Push!", self)
        vb.addWidget(b1)
        @e1 = QMultiLineEdit.new(self)
        vb.addWidget(@e1, 1)
        connect(b1, QSIGNAL("clicked()"), "b1clicked")

        Thread.new {
            count = 1
            while true
                sleep(1)
                append("count #{count}")
                count += 1
            end
        }
    end

    def append(text)
        @e1.append(text)
        @e1.setCursorPosition(@e1.numLines - 1, 0);
    end

    def b1clicked
        append("Push!")
    end
end

def main(argv)
    app = QPEApplication.new(argv)
    mw = MainWindow.new
    mw.setCaption(argv[0])
    app.showMainDocumentWidget(mw)
    mw.show
    return app.exec
end

main([$0] + ARGV)

Pythonの場合

!/usr/bin/python

import sys
from qt import *
from qtpe import *
from threading import *
from time import *

class MainWindow(QWidget):

    SUPER = QWidget

    def __init__(self, *args):
        self.SUPER.__init__(self, *args)
        vb = QVBoxLayout(self)
        b1 = QPushButton("Push!", self)
        vb.addWidget(b1)
        self.e1 = QMultiLineEdit(self)
        vb.addWidget(self.e1, 1)
        self.connect(b1, SIGNAL("clicked()"), self.b1clicked)
        self.closed = False
        self.thr = Thread(target = self.thread_proc)
        self.thr.start()

    def append(self, text):
        self.e1.append(text)
        self.e1.setCursorPosition(self.e1.numLines() - 1, 0) 

    def b1clicked(self):
        self.append("Push!")

    def thread_proc(self):
        count = 1
        while not self.closed:
            sleep(1)
            self.append("count %d" % count)
            count += 1

    def closeEvent(self, ce):
        self.closed = True
        self.thr.join()
        self.SUPER.closeEvent(self, ce)

def main(argv):
    app = QPEApplication(argv)
    mw = MainWindow()
    mw.setCaption(argv[0])
    app.showMainDocumentWidget(mw)
    mw.show()
    app.exec_loop()

main(sys.argv)

Perlの場合

とても残念だが、Perlではスレッドが5.8以降から実装されているため、Perl 5.6.1向けPerlQt/Embeddedでは上記のRubyPythonのようなコードが書けない。
そこで、Windows 3.1でやっていたように、イベントループを回しながら処理を行う方法を紹介する。

#!/usr/bin/perl

package MainWindow;

    use strict;
    no strict 'refs';
    use Qt;
    use Time::HiRes qw(usleep);

    our @ISA = qw(Qt::Widget);

    use Qt::slots qw(
        b1clicked()
    );

    sub new
    {
        my $self = shift->SUPER::new(@_);
        my $vb = new Qt::VBoxLayout($self);
        my $b1 = new Qt::PushButton('Push!', $self);
        $vb->addWidget($b1);
        $self->{e1} = new Qt::MultiLineEdit($self);
        $vb->addWidget($self->{e1}, 1);
        $self->connect($b1, 'clicked()', 'b1clicked()');
        return $self;
    }

    sub append
    {
        my $self = shift;
        my $text = shift;
        my $e1 = $self->{e1};
        $e1->append($text);
        $e1->setCursorPosition($e1->numLines() - 1, 0);
    }

    sub b1clicked
    {
        my $self = shift;
        $self->append("Push!");
    }

    sub thread_modoki
    {
        my $self = shift;
        my $sec = time();
        my $count = 1;
        while (!$app->{quit}) {
            $app->processEvents();
            usleep(1);
            my $now = time();
            next if $now == $sec;
            $sec = $now;
            $self->append("count $count");
            $count++;
        }
    }

    sub super
    {
        my $self = shift;
        my $name = shift;
        $name = "$ISA[0]::$name";
        &$name($self, @_);
    }

    sub closeEvent
    {
        my $self = shift;
        $app->{quit} = 1;
        $self->super('closeEvent', @_);
    }

package main;

    use strict;
    use Qt;;

    $app->setTextCodec("eucJP");
    my $mw = new MainWindow;
    $mw->setCaption($0);
    $app->showMainDocumentWidget($mw);
    $mw->show();
    $mw->thread_modoki();

usleepはマイクロ秒単位でスリープする関数である(Time::HiResを参照)。
1マイクロ秒の僅かなスリープにより、CPUの空き時間が0%になることを防ぐ(実測では40%空き)。