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


PyQt湯けむりウィジェットツアー(第6弾)

 Qtで図形や画像を表示することも、もちろんできます。 Qtのそれは本来CADなどのパワフリャな使い方をするための機能が豊富ですが、 基本的な使い方はとても簡単です。 豊富なQtの描画機能をぜいたくに使ってフォン・ド・ボーといきましょう (なんのことやら)。


だらだらオプションツアー1

 まずは描画関連のイベントと記述スタイルをメモっておくために簡単なサンプルをつくりました。 これを材料に動きを少しだけ紹介してみます。

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

class OeWidget(QWidget):

  def __init__(self, *args):
    apply(QWidget.__init__, (self,) + args)
    self.resize(300, 324)
    fmenu = QPopupMenu(self)
    fmenu.insertItem(self.tr("終了"), self.bye, 0)
    self.mb = QMenuBar(self)
    self.mb.insertItem(self.tr("ファイル(&F)"), fmenu)
    self.mb.setGeometry(0, 0, 300, 24)
    self.setBackgroundMode(QWidget.NoBackground)
    self.offscreenBuffer = None
    self.px = self.py = None

  def bye(self):
    self.close()

  def mousePressEvent(self, ev):
    self.px = ev.x(); self.py = ev.y()

  def mouseMoveEvent(self, ev):
    pa = QPainter();
    pa.begin(self.offscreenBuffer)
    if self.px != None:
      pen = QPen(QColor("blue"), 4, QWidget.SolidLine)
      pa.setPen(pen)
      # 線を描く
      #pa.drawLine(self.px, self.py, ev.x(), ev.y())
      # 長方形を描く
      pa.drawRect(ev.x()-3, ev.y()-3, 6, 6)
    self.px = ev.x(); self.py = ev.y()
    pa.end()
    self.repaint()

  def resizeEvent(self, ev):
    if self.offscreenBuffer == None:
      self.offscreenBuffer = QPixmap(ev.size())
      self.offscreenBuffer.fill(QWidget.white)
    else:
      temp = QPixmap(self.offscreenBuffer)
      self.offscreenBuffer.resize(ev.size())
      self.offscreenBuffer.fill(QWidget.white)
      bitBlt(self.offscreenBuffer, 0, 0, temp)

  def paintEvent(self, ev):
    bitBlt(self, 0, 0, self.offscreenBuffer)

a = QApplication(sys.argv)
a.setFont(QFont("helvetica", 16, QFont.Normal))
a.setDefaultCodec(QTextCodec.codecForName("eucJP"))
w = OeWidget()
a.setMainWidget(w)
w.show()
a.exec_loop()
# end.

スクリーンショット

 このサンプルを実行すると、メニューバーをもつ白地のウィンドウが現れ、 マウスの左ボタンを押したままひこずるとその跡に長方形が現れます。 書き方ですが、 Tkのキャンバス、Gtk+のドローイングエリアに当たるウィジェットは QtではQWidgetになります。普通QWidgetは灰地で塗りつぶされますが、 表示前に次のようにしてその機能を止めておきます。

  self.setBackgroundMode(QWidget.NoBackground)
そんでそんで、ええと、描画に関係するイベントは、 Qtおなじみのシグナルとスロットの機構は使わず、 すべて仮想関数を使います。つまり、QWidgetの「****Event」 という仮想関数をオーバーライドすることで行います。 最低限オーバーライドしておかなくてはいけない仮想関数は resizeEventpaintEvent の2つです。 resizeEventは、 などに呼ばれます。一方、paintEventは、 などに呼ばれます。そんでそんで、あー、実際に図形を描いたりする処理ですが、 ここではダブルバッファリングと呼ばれる手法を使っています。
  1. 初めて画面に表示する際、描画対象のQWidgetと同じサイズをもつ QPixmapのインスタンスを、「隠しバッファ」 として生成します。
          self.offscreenBuffer = QPixmap(ev.size())
    
  2. マウスでひこずられる、キーが入力されるなどのイベントに応じて、 仮想関数をオーバーライドし、QPainterQPenQBrush などを使って描画を行います。 QPainterは描画処理をつかさどる司令役みたいなもの、 QPenというのは線や線からなる図形を描画する際の線の幅や色をまとめて管理する構造体みたいなもの、 QBrushは同じく塗りつぶしを行う図形の色や塗りつぶし濃度や、網掛けといった情報をもつ構造体みたいなものと考えてよいでしょう。 いずれにせよ、ここでやっているのはごく基本的な処理だけです。
      def mouseMoveEvent(self, ev):
        pa = QPainter();
        pa.begin(self.offscreenBuffer)
        if self.px != None:
          pen = QPen(QColor("blue"), 4, QWidget.SolidLine)
          pa.setPen(pen)
          pa.drawRect(ev.x()-3, ev.y()-3, 6, 6)
        self.px = ev.x(); self.py = ev.y()
        pa.end()
        self.repaint()
    
    おおっと、2点ほど注意してください。 QPainterのbeginメソッドで指定する描画対象は、 表示したいQWidgetではなく、隠しバッファのほうです。 また、QPainterのendメソッドで一連の描画が終了したあかつきには、 忘れずにQWidget.repaint()を実行して強制的にpaintEventを発生させるように司令する必要があります。
  3. paintEventには、隠しバッファたるQPixmapのインスタンスから、 描画対象のQWidgetのインスタンスに描画をコピーする処理を書きます。 これをやるのはbitBltという関数を1行書くだけでOKです。 ここでも大注意! bitBltは関数です。QWidgetやQPaintDeviceのメンバ関数(メソッド)ではないので、 修飾は不要です。
      def paintEvent(self, ev):
        bitBlt(self, 0, 0, self.offscreenBuffer)
    


だらだらオプションツアー2

 オプションツアーの2日目は図形の描き方を指定する QPenQBrush の使い方にこだわってみます。マニアも納得!(せえへんせえへん)

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

class DrawingToolManager:
  def __init__(self, *args):
    self.pen = QPen()
    self.brush = QBrush()
    self.tool = 'line'
    self.width = 4
    self.r = 10

class OeWidget(QWidget):

  def __init__(self, *args):
    apply(QWidget.__init__, (self,) + args)
    self.resize(400, 424)
    fmenu = QPopupMenu(self)
    fmenu.insertItem(self.tr("終了"), self.bye, 0)
    self.mb = QMenuBar(self)
    self.mb.insertItem(self.tr("ファイル(&F)"), fmenu)
    self.mb.setGeometry(0, 0, 400, 24)
    self.setBackgroundMode(QWidget.NoBackground)
    self.offscreenBuffer = None
    self.px = self.py = None
    self.dtm = DrawingToolManager()

  def bye(self):
    self.close()

  def setting(self, k):
    k = str(k)
    # たびたびくどいですが、QStringのインスタンスとPythonの文字列
    # は全くの別物なので、== で比べても絶対に真になりません。そこで、
    # QStringのインスタンスをstr()関数でPython文字列に変換します。
    if k == 'b':
      self.dtm.pen.setColor(QColor("blue"))
    elif k == 'r':
      self.dtm.pen.setColor(QColor(128, 0, 64))
    elif k == 'B':
      self.dtm.brush.setColor(QColor("blue"))
    elif k == 'R':
      self.dtm.brush.setColor(QColor(128, 0, 64))
    elif k == '2' or k == '3' or k == '4' or k == '5':
      self.dtm.pen.setWidth(int(k))
    elif k == 's':
      self.dtm.pen.setStyle(QWidget.SolidLine)
    elif k == 'd':
      self.dtm.pen.setStyle(QWidget.DashLine)
    elif k == 'o':
      self.dtm.pen.setStyle(QWidget.DotLine)
    elif k == 'n':
      self.dtm.brush.setStyle(QWidget.NoBrush)
    elif k == 'f':
      self.dtm.brush.setStyle(QWidget.SolidPattern)
    elif k == 'h':
      self.dtm.brush.setStyle(QWidget.Dense4Pattern)
    elif k == 'z':
      self.dtm.tool = 'rectangle'
    elif k == 'e':
      self.dtm.tool = 'ellipse'
    elif k == 'w':
      self.dtm.tool = 'winfocusrect'
    elif k == 'n':
      self.dtm.tool = 'roundrect'
    elif k == 'a':
      self.dtm.tool = 'arc'
    elif k == 'p':
      self.dtm.tool = 'pie'
    elif k == 'c':
      self.dtm.tool = 'chord'
    elif k == 'y':
      self.dtm.tool = 'polygon'
    elif k == 't':
      self.dtm.tool = 'text'

  def keyPressEvent(self, ev):
    # keyは単純なキーコード、asciiはASCIIコード
    print "key pressed",
    if ev.key() != None: print ev.key(),
    try:
      if ev.text() != None: print ev.text(),
    except TypeError:
      pass
    if ev.ascii() != None: print ev.ascii(),
    print ""
    self.setting(ev.text())

  def mouseReleaseEvent(self, ev):
    self.px = ev.pos().x(); self.py = ev.pos().y()
    pa = QPainter();
    pa.begin(self.offscreenBuffer)
    pa.setPen(self.dtm.pen)
    pa.setBrush(self.dtm.brush)
    r = self.dtm.r
    if self.dtm.tool == 'line':
      pa.drawLine(self.px, self.py, 200, 200)
    elif self.dtm.tool == 'rectangle':
      pa.drawRect(self.px-r, self.py-r, r*2, r*2)
    elif self.dtm.tool == 'ellipse':
      pa.drawEllipse(self.px-r, self.py-r, r*2, r*2)
    elif self.dtm.tool == 'winfocusrect':
      pa.drawWinFocusRect(self.px-r, self.py-r, r*2, r*2)
    elif self.dtm.tool == 'roundrect':
      pa.drawRoundRect(self.px-r, self.py-r, r*2, r*2)
    elif self.dtm.tool == 'arc':
      pa.drawArc(self.px-r, self.py-r, r*2, r*2, 100*16, 160*16)
    elif self.dtm.tool == 'pie':
      pa.drawPie(self.px-r, self.py-r, r*2, r*2, 100*16, 160*16)
    elif self.dtm.tool == 'chord':
      pa.drawChord(self.px-r, self.py-r, r*2, r*2, 100*16, 160*16)
    elif self.dtm.tool == 'polygon':
      arr = QPointArray(4)
      points = ( (self.px-r, self.py-r), (self.px-r, self.py+r),
                 (self.px+r, self.py+r), (self.px, self.py-r) )
      for i in range(0, 4):
        arr.setPoint(i, points[i][0], points[i][1])
      pa.drawPolygon(arr)
    elif self.dtm.tool == 'text':
      pa.drawText(self.px-r, self.py-r, "ABC")
    pa.end()
    self.repaint()

  def resizeEvent(self, ev):
    if self.offscreenBuffer == None:
      self.offscreenBuffer = QPixmap(ev.size())
      self.offscreenBuffer.fill(QWidget.white)
    else:
      temp = QPixmap(self.offscreenBuffer)
      self.offscreenBuffer.resize(ev.size())
      self.offscreenBuffer.fill(QWidget.white)
      bitBlt(self.offscreenBuffer, 0, 0, temp)

  def paintEvent(self, ev):
    bitBlt(self, 0, 0, self.offscreenBuffer)

a = QApplication(sys.argv)
a.setFont(QFont("helvetica", 16, QFont.Normal))
a.setDefaultCodec(QTextCodec.codecForName("eucJP"))
w = OeWidget()
a.setMainWidget(w)
w.show()
a.exec_loop()
# end.

スクリーンショット

 図形のこだわりを出してくる前に、キーボードイベントの処理についてメモっておきましょう。 キーが押されたときに何か処理をさせる場合には、 keyPressEventをオーバーライドします。 この関数には引数としてQKeyEventクラスのインスタンスが渡されます。 実際にどのキーが押されたかを特定するために、 典型的にはその3つの関数を使い分けます。

 さて、図形の輪郭線の色やスタイルを指定するには、 QPenの関数を使います。

      pen = QPen()
      pen.setColor(QColor("blue"))     # 線を青色に
      pen.setColor(QColor(128, 0, 64)) # 線の色をRGBで指定
      pen.setWidth(5)                  # 線の幅をピクセルで指定
      pen.setStyle(QWidget.SolidLine)  # 線のスタイル。デフォルトはこの実線
      pen.setStyle(QWidget.DashLine)   # 破線
      pen.setStyle(QWidget.DotLine)    # ドット打ちじゃ
      painter.setPen(pen)
一方、内部を塗りつぶす場合には、その色やパターニングを指定するのに QBrushの関数どもを使います。
      brush = QBrush()
      brush.setColor(QColor("blue"))        # 青で塗りつぶす
      brush.setColor(QColor(128, 0, 64))    # 塗りつぶし色をRGBで指定
      brush.setStyle(QWidget.NoBrush)       # 塗りつぶしなし剃りのこしなし!
      brush.setStyle(QWidget.SolidPattern)  # 100%純色たっぷりで塗りつぶせ
      brush.setStyle(QWidget.Dense4Pattern) # 濃度4(30%)で塗ってくれ
      painter.setBrush(brush)
上の例のDense4Patternのような塗りつぶしの透明度を表す記号定数はいくつか定義されています。 これはQtのマニュアルのQBrushのクラスリファレンスからたどっていくと一覧があるので詳しい説明はそちらでご確認くださいね。

 そして最後に、ではQtでどんな図形が描けるのか、というのを紹介していきます。

  1. Rectangle(長方形)
  2. Ellipse(楕円)
  3. RoundRect(角が丸い長方形)
  4. Arc(楕円弧)
  5. Chord
    楕円の円周上にa,bの2点をとり、 線分a-bとa-bを結ぶ円周からなる閉じた図形のことです。
  6. Pie
    楕円の円周上にa,bの2点をとり、 aと楕円の中心、bと楕円の中心、a-bを結ぶ円周からなる閉じた図形のことです。
  7. Polygon(多角形)
  8. Text(文字列)


画像ファイルを読む 足跡をつけて生きる

 画像ファイルを読むということを臨床心理学的にインプルーブするために、 画面に足跡をつけてみることにしてみましょう。

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

class OeWidget(QWidget):

  def __init__(self, *args):
    apply(QWidget.__init__, (self,) + args)
    self.resize(300, 324)
    self.setCaption(self.tr("足跡をつけて生きる"))
    fmenu = QPopupMenu(self)
    fmenu.insertItem(self.tr("終了"), self.bye, 0)
    self.mb = QMenuBar(self)
    self.mb.insertItem(self.tr("ファイル(&F)"), fmenu)
    self.mb.setGeometry(0, 0, 300, 24)
    self.setBackgroundMode(QWidget.NoBackground)
    self.offscreenBuffer = None
    self.image = QImage("/usr/share/pixmaps/gnome-logo-icon.png")

  def bye(self):
    self.close()

  def mousePressEvent(self, ev):
    pa = QPainter();
    pa.begin(self.offscreenBuffer)
    pa.drawImage(ev.x(), ev.y(), self.image)
    pa.end()
    self.repaint()

  def resizeEvent(self, ev):
    if self.offscreenBuffer == None:
      self.offscreenBuffer = QPixmap(ev.size())
      self.offscreenBuffer.fill(QWidget.white)
    else:
      temp = QPixmap(self.offscreenBuffer)
      self.offscreenBuffer.resize(ev.size())
      self.offscreenBuffer.fill(QWidget.white)
      bitBlt(self.offscreenBuffer, 0, 0, temp)

  def paintEvent(self, ev):
    # 大注意! bitBltは関数です。QWidgetやQPaintDeviceのメンバ関数(メソッド)
    # ではないので、修飾は不要です。
    bitBlt(self, 0, 0, self.offscreenBuffer)

a = QApplication(sys.argv)
a.setFont(QFont("helvetica", 16, QFont.Normal))
a.setDefaultCodec(QTextCodec.codecForName("eucJP"))
w = OeWidget()
a.setMainWidget(w)
w.show()
a.exec_loop()
# end.

スクリーンショット

 Qtで扱う画像は、そのコンストラクタで画像ファイル名を指定したところの QImageのインスタンスとして作るのが普通です。 使い方はとても簡単、QPainterのdrawImage 関数でその左上の座標とQImageのインスタンスを指定すればOKです。
 Qtで読める画像フォーマットは、静的関数 QImage.inputFormats()を使えば確認できます。 通常のインストールでは、 PPM/PGM/PBM、XPM、PNGの各フォーマットは読めるようになっていると思います。

  def mousePressEvent(self, ev):
    image = QImage("/usr/share/pixmaps/gnome-logo-icon.png")
    pa = QPainter();
    pa.begin(self.offscreenBuffer)
    pa.drawImage(ev.x(), ev.y(), image)
    pa.end()
    self.repaint()

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