[PR]今日のニュースは
「Infoseek モバイル」


PyQtの湯けむりウィジェットツアー

 はい、PyQtの、というかQtのウィジェットツアーです。 PyQtはC++で書くQtと同じ感覚ではるかに簡単に書けるため、 使い心地はなかなかよいです。 PerlQtがまだかなり古いQtにしか対応していないので、 Qtの最も強力なスクリプト言語バインディングは今のところこのPyQtと言ってよいと思います。


ラインエディットとレイアウトマネージャ

 Tcl/Tkでいうエントリは、 QtではQLineEditというクラスですが、 使う目的は同じで、1行のキー入力を受け付ける入力欄です。 下の例では、入力欄に入力してボタンを押すと、 ダイアログが出てきてその名前に対して挨拶をします。

import sys
from qt import *

class MyWidget(QWidget):

  def hello(self, *args):
    tname = str(self.txfa.text())
    if tname == '':
      QMessageBox.warning(self, 'ah!', 'Input Your Name!')
    else:
      QMessageBox.information(self, 'Hello', 'Hello, ' + tname)

  def __init__(self, *args):
    apply(QWidget.__init__, (self,) + args)

    self.resize(200, 80)
    self.setCaption('QLineEdit example')
    vl = QVBoxLayout(self)
    hl1 = QHBoxLayout()
    hl2 = QHBoxLayout()
    laba = QLabel(self.tr('Input Your Name:'), self)
    self.txfa = QLineEdit(self)
    cmda = QPushButton('OK', self)
    cmde = QPushButton('Quit', self)
    self.connect(cmda, SIGNAL("clicked()"), self.hello)
    self.connect(cmde, SIGNAL("clicked()"), self.close)
    for e in (laba, self.txfa, cmda, cmde):
      e.setMaximumSize(e.sizeHint())
      e.setMinimumSize(e.sizeHint())

    vl.addLayout(hl1, 0)
    vl.addLayout(hl2, 0)

    hl1.addWidget(laba, 0, Qt.AlignRight | Qt.AlignTop)
    hl1.addWidget(self.txfa, 0, Qt.AlignCenter | Qt.AlignVCenter)
    hl2.addSpacing(120)
    hl2.addWidget(cmda, 0, Qt.AlignCenter)
    hl2.addWidget(cmde, 0, Qt.AlignCenter)

a = QApplication(sys.argv)
w = MyWidget()
a.setMainWidget(w)
w.show()
a.exec_loop()
# end.

スクリーンショット

 QLineEditに入力された内容は、 textメソッドで取得できますが、 はまりやすい注意点をひとつ!Python世界のString(文字列)と、 Qt世界のQStringクラスは全くの別物です。 QStringをそのままPython文字列に代入しようとするとエラーになってしまいます。 必ずstr関数で文字列に変換しないといけません。

 で、Qtで親ウィンドウの中に各ウィジェットを配置するのに最も簡単な方法は、 ウィジェットの左上隅の親ウィンドウの左上隅からの座標とサイズ(幅、高さ) をピクセル数で指定する、 moveresizesetGeometryの各メソッドを使う方法です。 しかし、子ウィジェットを縦横に整列して配置するために 「レイアウトマネージャ」というクラスがいくつか用意されています。 上の例では垂直に並べるQVBoxLayout、 水平に並べるQHBoxLayoutが出てきています。 レイアウトマネージャの中に別のレイアウトマネージャを載せることもできます。 その場合は、一番下の(親ウィンドウに直接載る)レイアウトマネージャのみ 親ウィンドウを指定して作り、 その上に載るレイアウトマネージャは親を指定せずに作ります。

    # この例では self.vl が一番下、残りはその上に載る
    vl = QVBoxLayout(self)
    hl1 = QHBoxLayout()
    hl2 = QHBoxLayout()

    vl.addLayout(hl1, 0)
    vl.addLayout(hl2, 0)
子レイアウトマネージャを載せるにはaddLayoutメソッドを使います。 これは、 子レイアウトマネージャが子(孫)ウィジェットを載せるより前に必ず行います。
    hl1.addWidget(laba, 0, Qt.AlignRight | Qt.AlignTop)
    hl1.addWidget(self.txfa, 0, Qt.AlignCenter | Qt.AlignVCenter)
    hl2.addSpacing(120)
    hl2.addWidget(cmda, 0, Qt.AlignCenter)
    hl2.addWidget(cmde, 0, Qt.AlignCenter)
レイアウトマネージャにウィジェットを載せるには、addWidgetメソッドを使います。3番目の引数はTcl/Tkの -anchor みたいなもので、 領域が余ったとき、 子ウィジェットをどの端につけるか(または全体を埋めるように膨らませるか) を指定するものです。 その値は本来のQtでは「AlignCenter」などの記号定数になっているのですが、 PyQtでも同様に上のように「Qt.AlignCenter」などの論理和(OR)で表現します。 また、位置を調整するためにaddSpacingも使っています。 (ここでの使い方が適切かは分かりませんけどね…)

ちょっとメモ(2) - アプリケーションの終わり方
Python/Tkinterでは普通、 プログラムを終わらせるにはsys.exit(0) とやりますが、Qtの場合は、 QApplication::setMainWidget メソッドで指定したウィジェットが QWidget::closeメソッドで消滅するとアプリケーション自体も終了します。 PyQtもQtと同じように
def quit(self, *args):
  self.close(0)
こうやってsetMainWidgetで指定したメインウィジェットを消滅させると、 QApplication.exec_loop()メソッドが終了し、 その次の行に実行が移ります。

ちょっとメモ(3) - ウィジェットのベストの大きさを知る
 レイアウトマネージャを使う方法では、 基本的に子ウィジェットは、 どれもこれも許される最大の大きさまで膨らもうとするので、 ボタンなどはかなりでかくなって見苦しくなります。 Qtでも使われますが、子ウィジェットのメソッドsizeHint が返す大きさ以上に膨らまないようにsetMaximumSize を使うと綺麗な大きさになります。
  laba.setMaximumSize(laba.sizeHint())

ちょっとメモ(4) - メッセージダイアログ
Python/Tkinter同様PyQtでも、というかTcl/Tk同様Qtでも、 メッセージダイアログは1行で出せます。めんどくさいGtk+に比べてはるかに楽です。
    if tname == '':
      QMessageBox.warning(self, 'ah!', 'ah!, input your name.')
    else:
      QMessageBox.information(self, 'Hello', 'Hello, ' + tname)
このように、出す目的に応じた名前の QMessageBoxのクラスメソッドになっています。


リストボックス

 リストボックスです。Qtでは、アイテムをたくさん追加すると、 リストボックスの隣にスクロールバーが自然に現れます。Gtk+に比べ、とても便利です。

#! /usr/local/bin/python
import sys
from qt import *

class MyWidget(QWidget):
  def ok_clicked(self):
    index = self.lsta.currentItem()
    if index == -1:
      print "no item is selected."
    else:
      itemtext = str(self.lsta.text(index))
      print "[" + str(index) + "]:" + itemtext + " is selected."

  def __init__(self, *args):
    apply(QWidget.__init__, (self,)+args)
    self.resize(300, 130)
    self.setCaption('Listbox example')

    gl = QGridLayout(self, 2, 2)
    hl = QHBoxLayout()
    gl.addLayout(hl, 1, 1)

    self.lsta = QListBox(self)
    self.lsta.resize(250, 80)
    cmda = QPushButton('OK', self)
    cmde = QPushButton('Quit', self)
    gl.setMargin(10)
    gl.setRowStretch(0, 2)
    gl.setRowStretch(1, 1)
    gl.addMultiCellWidget(self.lsta, 0, 0, 0, 1)
    hl.setMargin(10)
    hl.addSpacing(100)
    hl.addWidget(cmda)
    hl.addWidget(cmde)
    cmda.setMaximumSize(50, 20)
    cmde.setMaximumSize(50, 20)

    for e in ['apple', 'orange', 'melon', 'peach', 'grape', 'egg']:
        self.lsta.insertItem(e, -1)

    self.connect(cmda, SIGNAL("clicked()"), self.ok_clicked)
    self.connect(cmde, SIGNAL("clicked()"), self.close)

a = QApplication(sys.argv)
w = MyWidget()
a.setMainWidget(w)
w.show()
a.exec_loop()
# end.

スクリーンショット

 リストボックスのクラスはQListBoxです。 リストボックスに項目を追加するにはinsertItem メソッドを使います。2番目の引数は追加する位置で、先頭が零から数え始めますが、 最後に追加するときは常に-1でOKです。
 現在選択されている項目を取得するにはcurrentItem を使うと項目の位置(一番上が零)が返るので、さらにtext メソッドにその位置を渡すと項目の文字列が返ってきます。 どの項目も選択されていない場合はcurrentItem は-1を返します。

ちょっとメモ(5) - グリッドレイアウト
 QGridLayoutもレイアウトマネージャの一種で、 子ウィジェットを縦横の格子状に配置できるものです。 addWidgetのほかに、 addMultiCellWidgetを使うことで、 複数の格子を占有することもできます。
    self.gl.addMultiCellWidget(self.lsta, 0, 0, 0, 1)
setRowStretchsetColStretchは、 子を全部配置した後親のサイズが余った時、 どの行や列を膨らませるかという比率を指定します。 何もしないと、 最後(一番下、一番右)のグリッドが余った領域を全部吸い取って膨らみます。


チェックボタンとラジオボタン

 チェックボタンとラジオボタンも、もちろん用意されています。 それぞれクラスの名前はQCheckBoxQRadioButton。 ラジオボタンは普通複数配置して、 それらの中で常に1つだけ選択状態になるように使うのが普通ですが、 この機能はラジオボタン単体では発生せず、 QButtonGroup というウィジェットに載せることで実現できます。 QButtonGroupQGroupBox の導出クラスで、これらの周囲には見出しテキストと縁が描かれます。

#! /usr/local/bin/python
import sys
from qt import *

class MyWidget(QWidget):

  def createCheckBoxes(self, gl):
    vgb1 = QGroupBox('check all items which you like:', self)
    gl.addWidget(vgb1, 0, 0)
    self.chks = {}
    y = 20
    for e in ['Apple', 'Melon', 'Orange']:
      self.chks[e] = QCheckBox(e, vgb1)
      self.chks[e].move(20, y)
      y = y + 20

  def createRadioButtons(self, gl):
    vgb2 = QButtonGroup('choose one item:', self)
    gl.addWidget(vgb2, 0, 1)
    self.rads = {}
    y = 20
    for e in ['Apple', 'Melon', 'Orange']:
      self.rads[e] = QRadioButton(e, vgb2)
      self.rads[e].move(20, y)
      y = y + 20

  def createButtons(self, gl):
    hl = QHBoxLayout()
    hl.setMargin(5)
    gl.addLayout(hl, 1, 1)
    gl.setRowStretch(0, 2)
    gl.setRowStretch(1, 1)
    gl.setColStretch(0, 1)
    gl.setColStretch(1, 1)
    cmda = QPushButton('OK', self)
    cmde = QPushButton('Quit', self)
    for e in [cmda, cmde]:
      e.setMaximumSize(e.sizeHint())
      hl.addWidget(e)
    self.connect(cmda, SIGNAL("clicked()"), self.chosen)
    self.connect(cmde, SIGNAL("clicked()"), self.close)

  def chosen(self):
    c = ''
    s = ''
    for e in ['Apple', 'Melon', 'Orange']:
      if self.chks[e].isChecked():
        if c == '':
          c = e
        else:
          c = c + ', ' + e
      if self.rads[e].isChecked():
        if s == '':
          s = e
        else:
          s = s + ', ' + e
    if s == '':
      s = '(NONE)'
    if c == '':
      c = '(NONE)'
    QMessageBox.information(self, 'Result',
      'You like ' + c + ', and you\'ve chose ' + s + ', ok.')

  def __init__(self, *args):
    apply(QWidget.__init__, (self,)+args)
    self.resize(340, 160)
    self.setCaption('Checks & Radioes')
    gl = QGridLayout(self, 2, 2)
    gl.setMargin(10)
    gl.setSpacing(10)
    self.createCheckBoxes(gl)
    self.createRadioButtons(gl)
    self.createButtons(gl)

a = QApplication(sys.argv)
w = MyWidget()
a.setMainWidget(w)
w.show()
a.exec_loop()
# end.

スクリーンショット

QGroupBoxQButtonGroupの使い方はほとんど同じです。 但し、これらの上に載せるウィジェットのレイアウトは自前でやらないといけないみたいです。 そこでこの例では、3つのボタンのY座標をそれぞれ20,40,60と固定して、 move()メソッドで位置を指定しているというこすいマネをしています。

  def createRadioButtons(self, gl):
    vgb2 = QButtonGroup('choose one item:', self)
    gl.addWidget(vgb2, 0, 1)
    self.rads = {}
    y = 20
    for e in ['Apple', 'Melon', 'Orange']:
      self.rads[e] = QRadioButton(e, vgb2)
      self.rads[e].move(20, y)
      y = y + 20

ちょっとメモ(6) - ウィンドウのタイトルバー
 ウィンドウのタイトルバーに文字列を表示したい場合は、 QWidget.setCaptionというメソッドを使います。
  self.setCaption('Checks & Radioes')

ちょっとメモ(7) - マージンとスペース
 グリッドレイアウトを使っていると、 中に載せるウィジェットの間隔がうまく指定できずに、 ウズウズしてしまうことがあります。 setMargin()は、 レイアウトの外枠とウィジェットの間の間隔を指定し、 setSpacing()は、 レイアウトの中のウィジェット同士の間隔を指定するものです。
  gl.setMargin(10)
  gl.setSpacing(10)


キーボードイベント

 Qtはボタンが押されたりリストの選択状態が変わったりという、 いわゆるウィンドウイベントのトラップにシグナルとスロットという独特の機構を使いますが、 もっと低位の「キーが押される」「マウスが動く」「ウィンドウが画面に出る」 というようなイベントのトラップには、 仮想関数を使います。QWidgetクラスには、 キーボードイベントをトラップする keyPressEvent、 マウスの動きをトラップする mousePressEvent などの仮想関数がありますが、 すべて何もしないものばかりです。 そこで、QWidgetを継承した自分のクラスの中で、 これらをオーバーライドして何か処理を書けば、実行されるようになります。

import sys
from qt import *

class MyWidget(QWidget):
  def __init__(self, *args):
    apply(QWidget.__init__, (self,)+args)
    self.resize(100, 100)

  def keyPressEvent(self, e):
    print "pressed! [" + str(e.key()) + ":" + chr(e.ascii()) + "]"

a = QApplication(sys.argv)
w = MyWidget()
a.setMainWidget(w)
w.show()
a.exec_loop()
# end.

スクリーンショット


メニュー

 どのGUIツールキットでもメニューの作成は結構ホネですが、 Qtでのプルダウンメニューの作り方はとても素直なので、すぐ覚えられるでしょう。

#! /usr/local/bin/python
import sys
from qt import *

class MyWidget(QWidget):

  def fileActivated(self, id):
    if id == self.ids['open']:
      QMessageBox.information(self, 'open', 'write "open" action here!')
    elif id == self.ids['save']:
      QMessageBox.information(self, 'save', 'write "save" action here!')
    elif id == self.ids['saveas']:
      QMessageBox.information(self, 'save as', 'write "save as" action here!')
    elif id == self.ids['quit']:
      self.close(0)

  def __init__(self, *args):
    apply(QWidget.__init__, (self,)+args)
    self.resize(300, 200)
    self.setCaption('Menu example')
    self.ids = { }
    mbar = QMenuBar(self)
    txaa = QMultiLineEdit(self)
    mbar.setGeometry(0, 0, 300, 30)
    txaa.setGeometry(0, 33, 300, 160)

    self.p = { }
    for e in ('File', 'Edit'):
      self.p[e] = QPopupMenu()
      mbar.insertItem(e, self.p[e])
    self.pf = { }
    p = self.pf['New'] = QPopupMenu()
    p.insertItem('New File')
    p.insertItem('New Temporary Buffer')
    p = self.p['File']
    self.ids['open'] = p.insertItem('Open...')
    self.ids['new'] = p.insertItem('New', self.pf['New'])
    self.ids['save'] = p.insertItem('Save')
    self.ids['saveas'] = p.insertItem('Save As...')
    p.insertSeparator()
    self.ids['quit'] = p.insertItem('Quit')

    p = self.p['Edit']
    p.insertItem('Cut')
    p.insertItem('Copy')
    p.insertItem('Paste')

    self.connect(self.p['File'], SIGNAL("activated(int)"),
                 self.fileActivated)

a = QApplication(sys.argv)
w = MyWidget()
a.setMainWidget(w)
w.show()
a.exec_loop()
# end.

スクリーンショット

順を追っていくと、

  1. まず、メニューバーを作って、ウィンドウに載せます。
      mbar = QMenuBar(self)
      mbar.setGeometry(0, 0, 300, 30)
    

  2. メニューバーに表示する最上位の項目(TkでいうMenubutton)を作ります。 これらの項目は、押されるとサブメニューがポップアップ (というかプルダウン)するように作るのが普通なので、 ここでもそのサブメニューを QPopupMenu を作ります。
      self.p["File"] = QPopupMenu()
      mbar.insertItem("File", self.p["File"])
    
    insertItem()の最初の引数がサブメニューのタイトルです。 サブメニューのタイトルは、 これすなわちメニューバーに表示される文字列でもあります。 ちょっとややこしいですが、試して頂けるとすぐ分かりますよ。
  3. 今度はいよいよサブメニューに、 実際に選ぶと何か処理を発生させる項目を追加します。
      self.ids['open'] = p.insertItem('Open...')
    
    今度もinsertItem() を使いますが、引数が1個しかありません。 で、このinsertItem()はメニュー項目のIDを返すので、 この値を覚えておくのがポイントです。
  4. サブメニューのactivated(int)シグナルに、 イベント処理を行う関数を接続します。
      self.connect(self.p['File'], SIGNAL("activated(int)"),
                   self.fileActivated)
    

  5. その関数の定義ですが、ここではfileActivatedと名づけました。 シグナルの名前が「activated(int)」とintがついているので、 この関数には整数を受け取るパラメータが必要です。
      def fileActivated(self, id):
        if id == self.ids['open']:
          QMessageBox.information(self, 'open', 'write "open" action here!')
    
    で、その整数に、選んだメニュー項目のIDが渡されてくるので、 各メニュー項目のIDと比較して、どの項目が選ばれたのかを知って、 処理を行います。


プログレスバーとLCDとタイマー

 プログレスバー(QProgressBar)は、 ソフトウェアのインストーラなどでよく使われる、 時間がかかる処理の進み具合を比率で表示するためのウィジェットです。 またLCDナンバー(QLCDNumber)は、 液晶時計のようなデジタル表示で数字を表示するかっちょいいウィジェットで、 KDEをお使いの方はKDEのCDプレイヤー「kscd」などで見ることができます。 こいつらは「数値を刻む」ところが共通していますが、 その兼ね合いでついでにタイマー(QTimer) の使い方もここで一緒にメモっておきましょう。

#! /usr/local/bin/python
import sys
from qt import *

class MyWidget(QWidget):

  def progress(self):
    self.count = self.count + 1
    self.lcda.display(self.count)
    if self.count >= self.pbar.totalSteps():
      self.pbar.setProgress(self.pbar.totalSteps())
      self.cmda.setEnabled(1)
      self.timer.stop()
    else:
      self.pbar.setProgress(self.count)

  def start(self):
    self.cmda.setEnabled(0)
    self.count = 0
    #self.pbar.setProgress(self.count)
    self.pbar.reset()
    self.lcda.display(self.count)
    self.timer.start(500, 0)

  def __init__(self, *args):
    apply(QWidget.__init__, (self,)+args)
    self.setCaption('Progress Bar example')
    self.pbar = QProgressBar(self)
    self.lcda = QLCDNumber(self)
    self.timer = QTimer()
    self.cmda = QPushButton('Start', self)
    self.cmde = QPushButton('Quit', self)
    self.connect(self.cmde, SIGNAL("clicked()"), self.close)
    self.connect(self.cmda, SIGNAL("clicked()"), self.start)
    self.connect(self.timer, SIGNAL("timeout()"), self.progress)
    self.pbar.setTotalSteps(10)
    self.lcda.setNumDigits(2)
    self.resize(300, 100)
    self.pbar.setGeometry(10, 10, 270, 30)
    self.lcda.setGeometry(30, 50, 60, 30)
    self.cmda.setGeometry(150, 54, 50, 24)
    self.cmde.setGeometry(210, 54, 50, 24)

a = QApplication(sys.argv)
w = MyWidget()
a.setMainWidget(w)
w.show()
a.exec_loop()
# end.

スクリーンショット

セクションのサブメニューに戻る
(first uploaded 2000/04/27 last updated 2002/03/21)