GUI開発

paintEventを使って画像をウィンドウ上に表示させる方法

こんにちは、タナカです。

この記事では、paintEventを使って画像をウィンドウ上に表示させる方法を紹介します。

以前、【PyQt5】画像をウィンドウ上に表示する方法で画像を表示させる方法を説明しましたが、それとは別の方法になります。

個人的にはこれから紹介する方法の方がいろいろカスタマイズができると思っています。

この記事の内容
  • paintEventを使って画像をウィンドウ上に表示させる方法がわかる

1. paintEventとは

paintEvent関数は、QWidgetが持つ関数の一つで描画オブジェクトを生成し、pixmapをウィンドウ上に表示させる機能を持ちます。

pixmapとは、図や画像などの情報を保持し、ウィンドウ上に表示されるものです。

また、paintEventで画像を表示させる際は、QPainterという描画させるためのクラスを使って、そのQpainterに描画させるための情報を保持させます。その情報にはpixmapも含まれるため、画像をウィンドウ上に表示させることができます。

私自身、初めのうちはこの概念が理解できず、試行錯誤したことを覚えています。

paintEventはウィンドウに何かしらの動作があると実行されます。例えば、ウィンドウのサイズを手動で変更したり、ファイルを選択したときなどです。

2. 実装したいこと

実装したい内容な下記になります。

  • ファイルを選択できるようにする
  • 画像をウィンドウに表示させる
  • ウィンドウサイズに応じて画像サイズが変わる

3. 画像を表示させるコード

メニュバーの作成方法はこちらの記事で紹介しています。

コードについては、labelmeというアノテーションツールのソースコードを参考にさせていただきました。

作成したコードはこちらになります。コードの内容はコメントを参照ください。

from PyQt5.QtWidgets import QApplication, QMainWindow, QWidget, QAction, QFileDialog, QGridLayout, QSpinBox
from PyQt5 import QtGui
from PyQt5 import QtCore

import sys
import os

class MainWindow(QMainWindow):
    def __init__(self):
        super(MainWindow, self).__init__()

        # 初期値
        self.setGeometry(0, 0, 500, 500)
        self.image = QtGui.QImage()
        
        # canvas
        self.canvas = Canvas()
        # canvasをMainWindowにセット
        self.setCentralWidget(self.canvas)

        # menubarを作成
        self.createMenubar()

        # zoomwidge
        self.zoomWidget = ZoomWidget()
        self.zoomWidget.valueChanged.connect(self.paintCanvas)

    def createMenubar(self):
        # menubar
        self.menubar = self.menuBar()

        # menubarにメニューを追加
        self.filemenu = self.menubar.addMenu('File')

        # アクションの追加
        self.openAction()

    def openAction(self):
        # アクションの作成
        self.open_act = QAction('開く')
        self.open_act.setShortcut('Ctrl+O') # shortcut
        self.open_act.triggered.connect(self.openFile) # open_actとメソッドを紐づける

        # メニューにアクションを割り当てる
        self.filemenu.addAction(self.open_act)

    def openFile(self):
        self.filepath = QFileDialog.getOpenFileName(self, 'open file', '', 'Images (*.jpeg *.jpg *.png *.bmp)')[0]
        if self.filepath:
            self.canvas.openImage(self.filepath)
            self.paintCanvas()

    def paintCanvas(self):
        # canvasのスケールを更新
        self.canvas.scale = self.scaleFitWindow()
        self.canvas.update()

    def scaleFitWindow(self):
        # MainWindowのウィンドウサイズ
        e = 2.0 # 余白
        w1 = self.centralWidget().width() - e
        h1 = self.centralWidget().height() - e
        a1 = w1 / h1
        
        # pixmapのサイズ
        w2 = self.canvas.pixmap.width()
        h2 = self.canvas.pixmap.height()
        a2 = w2 / h2

        # a1が大きい -> 高さに合わせる
        # a2が大きい -> 幅に合わせる
        return w1 / w2 if a2 >= a1 else h1 / h2

    def adjustScale(self):
        value = self.scaleFitWindow()
        value = int(100 * value)
        self.zoomWidget.setValue(value)


    def resizeEvent(self, event):
        # canvasがTrue かつ pixmapがnullじゃない場合
        if self.canvas and not self.canvas.pixmap.isNull():
            self.adjustScale()
        super(MainWindow, self).resizeEvent(event)


class Canvas(QWidget):
    def __init__(self):
        super(Canvas, self).__init__()

        # 初期値
        self.painter = QtGui.QPainter()
        self.pixmap = QtGui.QPixmap()
        self.scale = 1.0

        # canvasのレイアウト
        self.canvas_layout = QGridLayout()
        
        # canvas_layoutをQWidget(self)にセット
        self.setLayout(self.canvas_layout)

    def openImage(self, filepath):
        img = QtGui.QImage()
        
        # 画像ファイルの読み込み
        if not img.load(filepath):
            return False

        # QImage -> QPixmap
        self.pixmap = QtGui.QPixmap.fromImage(img)

    def paintEvent(self, event):
        if not self.pixmap:
            return super(Canvas, self).paintEvent(event)

        # paintオブジェクトの生成(描画するためのもの)
        p = self.painter

        # paintができる状態にする
        p.begin(self)

        # 画像のスケール情報
        p.scale(self.scale, self.scale)

        # 原点の移動
        p.translate(self.offsetToCenter())

        # 画像を描画する
        p.drawPixmap(0, 0, self.pixmap)

        # paintの終了
        p.end()
    
    # 原点を画像左上に補正
    def offsetToCenter(self):
        scale = self.scale 
        area = super(Canvas, self).size()
        w, h = self.pixmap.width() * scale, self.pixmap.height() * scale
        aw, ah = area.width(), area.height()
        x = (aw - w) / (2 * scale) if aw > w else 0
        y = (ah - h) / (2 * scale) if ah > h else 0

        return QtCore.QPoint(int(x), int(y))

class ZoomWidget(QSpinBox):
    def __init__(self, value=100):
        super(ZoomWidget, self).__init__()

def main():
    app = QApplication(sys.argv)
    win = MainWindow()
    win.show()
    sys.exit(app.exec_())

main()

 

処理の流れはこのようになっています。

  • 1. ウィンドウの作成(画像なし)
  • 2. 画像の選択
  • 3. paintEventで画像(pixmap)を表示 ※ウィンドウサイズにフィットする
  • 4. ウィンドウサイズを変更するとresizeEventが実行される
  • 5. adjustScaleが実行され、zoomWidgetに値がセットされる
  • 6. zoomWidgetに値がセットされるとpaintCanvasが実行される
  • 7. paintCanvasで現在のウィンドウサイズと画像サイズからスケールが決まる
  • 8. 算出したスケールで画像が更新される

処理内容の補足をすると、zoomWidgetはQSpinBoxというクラスを継承しており、QSpinBoxが持つ機能を使っています。

具体的にはsetValueで値を保持し、valueChanged.connectで値が変更されたときにpaintCanvasが実行されるようにしています。

好きな画像ファイルを選択して出力された結果がこちらになります。ただ画像が表示されるだけのものなので、今後の記事でさらに機能を実装していきたいと思います。

QPainter-drawImage

まとめ

paintEventを使って画像をウィンドウ上に表示させる方法を紹介しました。

画像を表示させる方法はいくつかあるので、一例として参考にしていただければと思います。

以前、ViewやSceneを使って画像を表示させる方法も実装しましたので、そちらの記事も参考してください。