トップページ -> PyQtでリアルタイム音階判定GUIアプリを作ってみた -> PyQtでのGUIの作成

PyQtでのGUIの作成

今回のアプリを作るのには以下のような機能が必要になります.

そもそもPyQtとは?

PythonでGUI(Graphical User Interface)を作るためのフレームワークです. ボタンなどによって直感的な操作が可能なUIをPythonだけで書くことができます. WidgetやLayoutを配置して簡単にGUIを作ることができます.
【参考(外部サイト)】Qt Widget Gallery
利用できるウィジェットの一覧です.

PyQtで簡単なレイアウトを作ってみる

PyQtで簡単なレイアウトを作ってみます. QHBoxLayout と QVBoxLayoutを組み合わせてボタンを配置してみます. QHBoxLayout(HはHorizontal)内に配置された要素は横に,QVBoxLayout(VはVertical)に配置された要素は縦に並んでいきます. h_layout, v_layoutを用意してそれぞれにボタン・入力欄を3つずつ配置し,2つのレイアウトをmain_layoutに追加して表示します. 以下のコードはボタンを押しても何も起きません.
※ 音階判定アプリのimport文を流用しているため,ページ中のコードは余計なものもインポートしています.

サンプルGUI
サンプルGUI

from PyQt5.QtWidgets import QApplication, QWidget, QPushButton, QVBoxLayout, QLabel, QFileDialog, QLineEdit, QHBoxLayout, QScrollArea, QGraphicsLineItem, QGraphicsView, QGraphicsScene, QGraphicsRectItem, QStackedWidget, QSplitter, QInputDialog, QMessageBox, QSlider, QComboBox, QCheckBox
from PyQt5.QtGui import QIntValidator, QDoubleValidator, QPixmap, QFont, QBrush, QColor, QPen, QIcon
from PyQt5.QtCore import Qt, QRectF, QTimer, QUrl, QEvent, QObject, QThread, pyqtSignal, QSize
from PyQt5.QtMultimedia import QMediaPlayer, QMediaContent
import sys

class SampleGUI(QWidget):
    def __init__(self):
        super().__init__()
        self.initUI()

    # UIの初期化
    def initUI(self):
        self.setWindowTitle('ここにアプリのタイトルを入力') # ウィンドウのタイトルを入力する
        self.setGeometry(100, 100, 800, 400) # (x0,y0,x1,y1) ウィンドウの初期位置を設定する

        # メインレイアウトを作る
        main_layout = QVBoxLayout()

        # 2種類のレイアウトを作る
        h_layout = QHBoxLayout()
        v_layout = QVBoxLayout()

        # 説明のラベルを追加
        h_label = QLabel('QHBoxLayoutは横に要素が並びます') # QLabelで文字が書けます.
        h_layout.addWidget(h_label)

        # h_layoutにボタンを3つ追加
        h_button1 = QPushButton("ボタン1")
        h_layout.addWidget(h_button1)
        h_button2 = QPushButton("ボタン2")
        h_layout.addWidget(h_button2)
        h_button3 = QPushButton("ボタン3")
        h_layout.addWidget(h_button3)

        # 説明のラベルを追加
        v_label = QLabel('QVBoxLayoutは縦に要素が並びます') # QLabelで文字が書けます.
        v_layout.addWidget(v_label)

        # v_layoutに入力欄を3つ追加
        v_input1 = QLineEdit()
        v_layout.addWidget(v_input1)
        v_input2 = QLineEdit()
        v_layout.addWidget(v_input2)
        v_input3 = QLineEdit()
        v_layout.addWidget(v_input3)

        # レイアウトをmain_layoutに追加
        main_layout.addLayout(h_layout)
        main_layout.addLayout(v_layout)

        # setLayoutでmain_layoutを表示
        self.setLayout(main_layout)

if __name__ == '__main__':
    app = QApplication(sys.argv)
    window = SampleGUI()
    window.show()
    sys.exit(app.exec_())

使用例と実装方法

PyQtが取り揃えているボタン,入力欄,スライダーなどをaddWidgetでレイアウトやウィジェットに追加することでウィンドウを作ることができました. ここからは音階判定アプリ内での使用例を紹介しながら簡単なサンプルプログラムで実装方法を説明します.

1. ボタンがクリックされたときの処理を書く

音階判定のウィンドウ内でもたくさんのボタンが使われています. ボタンがクリックされたときの処理をbutton.clicked.connect(`押されたときの処理`)で以下のように書きます. 以下のコードはボタンを押すと「おはよう」と「こんばんは」が切り替わるコードです. 入力欄やチェックボックスに関しても同様に inputbox.currentIndexChanged.connect(`処理`) ,checkbox.toggled.connect(`処理`) などで処理を行うことができます.

ボタンがクリックされたら文字が変わる
Thumbnail

# UIの初期化
def initUI(self):
    self.setWindowTitle('ここにアプリのタイトルを入力') # ウィンドウのタイトルを入力する
    self.setGeometry(100, 100, 800, 400) # (x0,y0,x1,y1) ウィンドウの初期位置を設定する

    # メインレイアウトを作る classのメソッドでレイアウトやラベル,ボタンを参照したいのでアトリビュートにしておく
    self.main_layout = QVBoxLayout()

    # あいさつのラベルを追加
    self.greeting_label = QLabel('おはよう') # QLabelで文字が書けます.
    self.main_layout.addWidget(self.greeting_label)

    # ボタンを作って追加
    self.button = QPushButton("ボタン")
    self.button.clicked.connect(self.changeLabel)
    self.main_layout.addWidget(self.button)

    # setLayoutでmain_layoutを表示
    self.setLayout(self.main_layout)

# ボタンが押されたときの処理
def changeLabel(self):
    if self.greeting_label.text() == "おはよう":
        self.greeting_label.setText("こんばんは")
    else:
        self.greeting_label.setText("おはよう")

2. QStackedWidgetによる表示の切り替え

音階判定アプリではプロジェクトの作成 または 読み込みを選んだ後にQStackedWidgetを使って画面を切り替えています. 以下はボタンを押すと時間に応じてあいさつが切り替わるコードです. stack_widgetにウィジェットを追加し,stack_widget.setCurrentIndex(next_index)で表示するウィジェットを切り替えることができます. ついでに,button.setEnabled(False)でボタンを無効化(クリックできなく)したりしています.
下の例は本来はラベルのテキストを変えるだけで済んでしまうのでありがたみが薄いですが,全く違うレイアウトを表示したいときに便利です.

ボタンがクリックされたら表示が変わる
Thumbnail

# UIの初期化
def initUI(self):
    self.setWindowTitle('ここにアプリのタイトルを入力') # ウィンドウのタイトルを入力する
    self.setGeometry(100, 100, 800, 400) # (x0,y0,x1,y1) ウィンドウの初期位置を設定する

    # メインレイアウトを作成
    self.main_layout = QVBoxLayout()

    # 3つのレイアウトを切り替える
    self.morning_widget = QWidget()
    self.daytime_widget = QWidget()
    self.night_widget = QWidget()

    # ラベルを追加 QLable("文字", 親要素(widget)) で1行で追加できる
    self.morning_label = QLabel('おはよう', self.morning_widget)
    self.daytime_label = QLabel('こんにちは', self.daytime_widget)
    self.night_label = QLabel('こんばんは', self.night_widget)

    # QStackedWidgetで状態を保存
    self.stack_widget = QStackedWidget()
    self.stack_widget.addWidget(self.morning_widget)
    self.stack_widget.addWidget(self.daytime_widget)
    self.stack_widget.addWidget(self.night_widget)

    # self.stack_widget を追加
    self.main_layout.addWidget(self.stack_widget)

    # ボタンを作って追加
    self.button_layout = QHBoxLayout()
    self.morning_button = QPushButton("朝")
    self.morning_button.clicked.connect(lambda: self.switchLayout(0))

    self.daytime_button = QPushButton("昼")
    self.daytime_button.clicked.connect(lambda: self.switchLayout(1))

    self.night_button = QPushButton("夜")
    self.night_button.clicked.connect(lambda: self.switchLayout(2))

    # buttonのリストを作る
    self.button_list = [self.morning_button, self.daytime_button, self.night_button]
    self.morning_button.setEnabled(False) # 最初は朝なのでクリックできないようにしておく

    # レイアウトに追加
    self.button_layout.addWidget(self.morning_button)
    self.button_layout.addWidget(self.daytime_button)
    self.button_layout.addWidget(self.night_button)

    self.main_layout.addLayout(self.button_layout)

    # setLayoutでmain_layoutを表示
    self.setLayout(self.main_layout)

# ボタンが押されたときの処理
def switchLayout(self, next_index):
    # 表示するウィジェットを切り替える
    self.stack_widget.setCurrentIndex(next_index)

    # i == next_index であればボタンを無効化し,そうでなければ有効化する
    for i in range(len(self.button_list)):
        # こんがらがらなければ self.button_list[i].setEnabled(i!=next_index)などでも可
        if i == next_index:
            self.button_list[i].setEnabled(False)
        elif i != next_index:
            self.button_list[i].setEnabled(True)

3. 画像を使ったボタンの配置

音階判定アプリの右側に配置されているような画像を用いたボタンを配置します. button.setIcon(QIcon("FilePath"))で画像を設定することができます. 以下のコードはボタンを押すと表示される画像が切り替わるコードです. 実際に画像を表示するには同じディレクトリ内にimage0.pngとimage1.pngが必要です.
※ 下のコード中ではアイコンのサイズを変更していますが,サイズを変えないと動画のような小さいアイコンになります.

画像ボタンの配置
Thumbnail

# UIの初期化
def initUI(self):
    self.setWindowTitle('ここにアプリのタイトルを入力') # ウィンドウのタイトルを入力する
    self.setGeometry(100, 100, 800, 400) # (x0,y0,x1,y1) ウィンドウの初期位置を設定する

    # メインレイアウトを作成
    self.main_layout = QVBoxLayout()

    # ボタンを作って追加
    self.image_idx = 1 # 画像のID
    self.image_button = QPushButton()
    self.image_button.setIcon(QIcon("image1.png"))
    self.image_button.setIconSize(QSize(50, 50)) # アイコンのサイズを変える
    self.image_button.clicked.connect(self.switchIcon)
    self.main_layout.addWidget(self.image_button)

    # setLayoutでmain_layoutを表示
    self.setLayout(self.main_layout)

# ボタンが押されたときの処理
def switchIcon(self):
    # 表示する画像を切り替える
    self.image_idx = 1 - self.image_idx # IDを切り替え
    self.image_button.setIcon(QIcon(f"image{self.image_idx}.png"))

4. QSplitterによる画面分割

音階判定アプリではタイムスタンプの設定画面と音階を表示する領域を左右で分割しています. 以下のコードは左右・上下の画面分割の実装例です.
折返しをTrueにしない場合,左のようにハンドラーを寄せても文字が改行されません.

画像ボタンの配置
Thumbnail

# UIの初期化
def initUI(self):
    self.setWindowTitle('ここにアプリのタイトルを入力') # ウィンドウのタイトルを入力する
    self.setGeometry(100, 100, 1200, 400) # (x0,y0,x1,y1) ウィンドウの初期位置を設定する

    isHorizontal = True

    # メインレイアウトを作成
    if isHorizontal:
        self.main_layout = QVBoxLayout()
    else:
        self.main_layout = QHBoxLayout()

    # 左側のレイアウトを用意
    self.left_widget = QWidget()
    self.left_layout = QVBoxLayout(self.left_widget) # !!! (セットでコメントアウトすると挙動が変わります)
    self.left_label = QLabel("いろはにほへと ちりぬるを わかよたれそ つねならむ うゐのおくやま けふこえて あさきゆめみし ゑひもせす",self.left_widget)
    self.left_label.setWordWrap(True)  # テキストを折り返すように設定(ウィジェットではなくレイアウトの中に入れないと正しく折り返しません)
    self.left_layout.addWidget(self.left_label) # !!! (セットでコメントアウトすると挙動が変わります)

    # 右側のレイアウトを用意
    self.right_widget = QWidget()
    self.right_layout = QVBoxLayout(self.right_widget) # ??? (セットでコメントアウトすると挙動が変わります)
    self.right_label = QLabel("きみがよは ちよにやちよに さざれいしの いわおとなりて こけのむすまで",self.right_widget)
    # self.right_label.setWordWrap(True) # 折り返したい場合は冒頭の # を消す
    self.right_layout.addWidget(self.right_label) # ??? (セットでコメントアウトすると挙動が変わります)

    # QSplitterを作成しウィジェットを追加
    if isHorizontal:
        self.splitter = QSplitter(Qt.Horizontal)
    else:
        self.splitter = QSplitter(Qt.Vertical)
    self.splitter.addWidget(self.left_widget)
    self.splitter.addWidget(self.right_widget)

    # スプリッターハンドルにスタイルシートを設定
    self.splitter.setHandleWidth(10) # ハンドルの太さを設定
    self.splitter.setStyleSheet("""
        QSplitter::handle {
            background: black;
        }
    """)
    self.main_layout.addWidget(self.splitter)

    # setLayoutでmain_layoutを表示
    self.setLayout(self.main_layout)

5. QInputDialog, QFileDialogによる入力・ファイル選択ダイアログの表示

音階判定アプリではプロジェクトを新規作成するときに入力ダイアログを表示してファイル名を入力してもらったり,既存のプロジェクトを開くときにファイル選択ダイアログを表示しています. 以下のコードはボタンが押されたときに入力・ファイル選択ダイアログが表示されるコードです. 入力が完了するとラベルに結果が表示されます.キャンセルされた場合はラベルは変更されません.

ファイル選択・入力ダイアログのサンプル
サンプルGUI

# UIの初期化
def initUI(self):
    self.setWindowTitle('ここにアプリのタイトルを入力') # ウィンドウのタイトルを入力する
    self.setGeometry(100, 100, 800, 400) # (x0,y0,x1,y1) ウィンドウの初期位置を設定する

    # メインレイアウトを作成
    self.main_layout = QVBoxLayout()

    # 入力ダイアログを表示するボタン
    self.input_dialog_button = QPushButton("入力ダイアログを表示")
    self.input_dialog_button.clicked.connect(self.displayInputDialog)
    self.main_layout.addWidget(self.input_dialog_button)
    # 入力された文字を表示するラベル
    self.input_label = QLabel("まだ文字は入力されていません")
    self.main_layout.addWidget(self.input_label)

    # ファイル選択ダイアログを表示するボタン
    self.file_dialog_button = QPushButton("ファイル選択ダイアログを表示")
    self.file_dialog_button.clicked.connect(self.displayFileDialog)
    self.main_layout.addWidget(self.file_dialog_button)
    # 選択されたファイル名を表示するラベル
    self.file_label = QLabel("まだファイルは選択されていません")
    self.main_layout.addWidget(self.file_label)

    # setLayoutでmain_layoutを表示
    self.setLayout(self.main_layout)

def displayInputDialog(self):
    # 入力された文字列と入力が完了したかを受け取ります.(なにも入力されずにOKが押されたのか,キャンセルなのかを区別できます)
    input_string, ok = QInputDialog.getText(self, '入力ダイアログのタイトル', '好きな文字を入力してね:')
    # ok は cancelされなかった場合
    if ok:
        self.input_label.setText(f"入力された文字:{input_string}")

def displayFileDialog(self):
    folder_path = QFileDialog.getExistingDirectory(self, 'ファイル選択ダイアログのタイトル')
    
    # folder_path = "" ならキャンセルされている
    if folder_path:
        self.file_label.setText(f"選択されたファイル:{folder_path}")

6. キーが押されたときの処理の実装

QWidgetクラスのイベントハンドラのkeyPressEventを使ってキー入力を監視します・ 以下のコードは3行3列のレイアウトにラベルを設置し,矢印キーで選択できるようにしています. 音階判定アプリの中では使用していませんが,QGridLayout()を使って3行3列にラベルを配置しています.

矢印キーが押されたら選ばれている要素が変わる
Thumbnail

# 追加
from PyQt5.QtWidgets import QGridLayout

# UIの初期化
def initUI(self):
    self.setWindowTitle('ここにアプリのタイトルを入力') # ウィンドウのタイトルを入力する
    self.setGeometry(100, 100, 800, 400) # (x0,y0,x1,y1) ウィンドウの初期位置を設定する

    # メインレイアウトを作成 3列3行にしたいのでQGridLayout
    self.main_layout = QGridLayout()
    
    # ラベルを作成
    self.labels = [QLabel(f'Label {i+1}', self) for i in range(9)]
    for label in self.labels:
        label.setStyleSheet('background-color: white')
        label.setAlignment(Qt.AlignCenter)

    # 現在のインデックスを0で初期化し,背景色を変える
    self.current_index = 0
    self.labels[self.current_index].setStyleSheet('background-color: yellow')

    # 位置を指定してラベルを追加 (3行3列)
    for i, label in enumerate(self.labels):
        row = i // 3
        col = i % 3
        self.main_layout.addWidget(label, row, col)

    # setLayoutでmain_layoutを表示
    self.setLayout(self.main_layout)

# QWidgetクラスのイベントハンドラを使ってキー入力を監視
def keyPressEvent(self, event):
    if event.key() == Qt.Key_Up:
        self.change_selection(-3)
    elif event.key() == Qt.Key_Down:
        self.change_selection(3)
    elif event.key() == Qt.Key_Left:
        self.change_selection(-1)
    elif event.key() == Qt.Key_Right:
        self.change_selection(1)

# 選択されているラベルの背景色を変更する
def change_selection(self, direction):
    self.labels[self.current_index].setStyleSheet('background-color: white')
    self.current_index = (self.current_index + direction) % len(self.labels)
    if self.current_index < 0:
        self.current_index += len(self.labels)
    self.labels[self.current_index].setStyleSheet('background-color: yellow')

7. QScrollAreaによるスクロール処理と自動スクロール

音階判定アプリ内ではタイムスタンプの取得中にスペースキーが押された(離された)ときに,その時間を表示するようにしています. 後述のQPlainTextEditを使った方法で綺麗に簡単に表示することができますが,知らなかったのでScrollAreaを使ってしまいました. 以下は単純なスクロールエリアの例です.QScrollArea内の要素はスクロールできます.

長いテキストのスクロール
Thumbnail

def initUI(self):
    self.setWindowTitle('ここにアプリのタイトルを入力') # ウィンドウのタイトルを入力する
    self.setGeometry(100, 100, 800, 400) # (x0,y0,x1,y1) ウィンドウの初期位置を設定する

    self.allowAutoRepeat = False # 長押し中もSpace Key Pressedを表示するかどうか

    # メインレイアウトを作成 3列3行にしたいのでQGridLayout
    self.main_layout = QVBoxLayout()

    # 一番上までスクロールするボタン
    top_scroll_button = QPushButton("一番上までスクロール")
    top_scroll_button.clicked.connect(self.scrollToTop)
    self.main_layout.addWidget(top_scroll_button)

    # 一番下までスクロールするボタン
    bottom_scroll_button = QPushButton("一番下までスクロール")
    bottom_scroll_button.clicked.connect(self.scrollToBottom)
    self.main_layout.addWidget(bottom_scroll_button)

    # 十分に長いテキストを用意しスクロールできるようにする
    text = "long_text_sample"*100
    self.scroll_contents = QWidget()
    self.scroll_layout = QVBoxLayout(self.scroll_contents)
    for _ in range(100):
        long_text_label = QLabel(text)
        self.scroll_layout.addWidget(long_text_label)

    # scrollAreaにウィジェットを追加する
    self.scroll_area = QScrollArea()
    self.scroll_area.setWidget(self.scroll_contents)
    self.main_layout.addWidget(self.scroll_area)

    # setLayoutでmain_layoutを表示
    self.setLayout(self.main_layout)

def scroll_to_top(self):
    # スクロールエリアのスクロールバーを一番上に移動
    scroll_bar = self.scroll_area.verticalScrollBar()
    scroll_bar.setValue(scroll_bar.minimum())

def scroll_to_bottom(self):
    # スクロールエリアのスクロールバーを一番下に移動
    scroll_bar = self.scroll_area.verticalScrollBar()
    scroll_bar.setValue(scroll_bar.maximum())

QPlainTextEditを使うと行数に関わらず綺麗に表示できます.

スペースボタンが押されたら表示する
Thumbnail

# 追加
from PyQt5.QtWidgets import QPlainTextEdit

def initUI(self):
    self.setWindowTitle('ここにアプリのタイトルを入力') # ウィンドウのタイトルを入力する
    self.setGeometry(100, 100, 800, 400) # (x0,y0,x1,y1) ウィンドウの初期位置を設定する

    self.allowAutoRepeat = False # 長押し中もSpace Key Pressedを表示するかどうか

    # メインレイアウトを作成 
    self.main_layout = QVBoxLayout()
    
    # QPlainTextEditを作成してReadOnlyにする
    self.text_edit = QPlainTextEdit()
    self.text_edit.setReadOnly(True)
    # textEditやボタンなどにフォーカスが当たっているとスペースのキーイベントが取れないので
    self.text_edit.setFocusPolicy(Qt.NoFocus) 
    self.main_layout.addWidget(self.text_edit)
    self.text_edit.appendPlainText('スペースキーのイベントを監視しています...')

    # setLayoutでmain_layoutを表示
    self.setLayout(self.main_layout)

# QWidgetクラスのイベントハンドラを使ってキー入力を監視
def keyPressEvent(self, event):
    if event.key() == Qt.Key_Space:
        # 長押しを許容するか否か
        if not event.isAutoRepeat() or self.allowAutoRepeat:
            self.text_edit.appendPlainText("Space Key Pressed")
            self.text_edit.ensureCursorVisible() # 新しい行が見える位置までスクロールする
            
# QWidgetクラスのイベントハンドラを使ってキー入力を監視
def keyReleaseEvent(self, event):
    if event.key() == Qt.Key_Space:
        # 長押しを許容するか否か
        if not event.isAutoRepeat() or self.allowAutoRepeat:
            self.text_edit.appendPlainText("Space Key Released")
            self.text_edit.ensureCursorVisible() # 新しい行が見える位置までスクロールする

8. 設定画面の表示

音階判定アプリでは設定ウィンドウを表示できるようにしています. 以下のコードは設定ウィンドウを表示するものです. SettingsWindowとMainWindowを作っています. MainWindowの設定値ラベルにアクセスできるようにSettinsWindowの初期化時にMainWindowインスタンスを受け渡すようにしています.


from PyQt5.QtWidgets import (QApplication, QWidget, QPushButton, QVBoxLayout, QLabel, QFileDialog, QLineEdit, QHBoxLayout, QScrollArea, QGraphicsLineItem, QGraphicsView, QGraphicsScene, QGraphicsRectItem, QStackedWidget, QSplitter, QInputDialog, QMessageBox, QSlider, QComboBox, QCheckBox)
from PyQt5.QtGui import QIntValidator, QDoubleValidator, QPixmap, QFont, QBrush, QColor, QPen, QIcon
from PyQt5.QtCore import Qt, QRectF, QTimer, QUrl, QEvent, QObject, QThread, pyqtSignal, QSize
from PyQt5.QtMultimedia import QMediaPlayer, QMediaContent

class SettingsWidget(QWidget):
    # 初期化時にMainWindowインスタンスを渡す
    def __init__(self, main_window, parent=None):
        super().__init__(parent)
        self.main_window = main_window  # MainWindowのインスタンスを保存
        self.initUI()
    
    def initUI(self):
        # レイアウトを作成
        layout = QVBoxLayout()

        # 設定を受け取る入力欄
        self.setting_value = QLineEdit()
        layout.addWidget(self.setting_value)
        
        # 保存用のボタン
        save_button = QPushButton("保存する")
        save_button.clicked.connect(self.saveSettings)
        layout.addWidget(save_button)

        # レイアウトをセット
        self.setLayout(layout)

    def saveSettings(self):
        # メインウィンドウの設定値ラベルを受け取ってテキストを変更する
        setting_label = self.main_window.current_setting_label
        text = self.setting_value.text()
        setting_label.setText(f"現在の設定値: {text}")

        # 設定ウィンドウを閉じる
        self.close()

class MainWindow(QWidget):
    def __init__(self):
        super().__init__()
        self.initUI()
        self.setting_window = None # 設定画面をNoneにしておく

    def initUI(self):
        self.setWindowTitle('Main Window') # ウィンドウのタイトルを入力する
        self.setGeometry(100, 100, 500, 500) # (x0,y0,x1,y1) ウィンドウの初期位置を設定する

        layout = QVBoxLayout()

        # 現在の設定値を表示する
        self.current_setting_label = QLabel("現在の設定値: 0")
        layout.addWidget(self.current_setting_label)

        # 設定画面を表示するためのボタン
        setting_button = QPushButton("設定画面を表示する")
        setting_button.clicked.connect(self.showSettingWindow)
        layout.addWidget(setting_button)

        self.setLayout(layout)

    def showSettingWindow(self):
        # 設定画面がまだ作られていなければ作成
        if not self.setting_window:
            self.setting_window = SettingsWidget(self)

        self.setting_window.show()

if __name__ == '__main__':
    app = QApplication([])
    window = MainWindow()
    window.show()
    app.exec_()

9. QGraphicsViewでクリック・移動可能な正方形を表示する

音階判定アプリでは譜面領域の描画にクリック可能な音階バーを表示し,矢印キーで移動できるようにしています. 簡単なサンプルとして,2つの正方形を画面上に表示してクリックで選択して矢印キーで動かせるようにします. QGraphicsView -> QGraphicsScene -> Itemになるように配置していきます. keyPressEventとmousePressEventでクリックとキー入力を監視しています.

クリック・移動可能な正方形の表示
Thumbnail

from PyQt5.QtWidgets import (QApplication, QWidget, QPushButton, QVBoxLayout, QLabel, QFileDialog, QLineEdit, QHBoxLayout, QScrollArea, QGraphicsLineItem, QGraphicsView, QGraphicsScene, QGraphicsRectItem, QStackedWidget, QSplitter, QInputDialog, QMessageBox, QSlider, QComboBox, QCheckBox)
from PyQt5.QtGui import QIntValidator, QDoubleValidator, QPixmap, QFont, QBrush, QColor, QPen, QIcon
from PyQt5.QtCore import Qt, QRectF, QTimer, QUrl, QEvent, QObject, QThread, pyqtSignal, QSize
from PyQt5.QtMultimedia import QMediaPlayer, QMediaContent

class GraphicsView(QGraphicsView):
    def __init__(self):
        super().__init__()
        # QGraphicsView -> QGraphicsScene -> Itemになるようにする
        self.scene = QGraphicsScene(self)
        self.setScene(self.scene)

        # 長方形を作って sceneに追加
        self.rect1 = QGraphicsRectItem(QRectF(0, 0, 50, 50))
        self.rect1.setBrush(QBrush(Qt.blue))
        self.scene.addItem(self.rect1)

        self.rect2 = QGraphicsRectItem(QRectF(100, 0, 50, 50))
        self.rect2.setBrush(QBrush(Qt.blue))
        self.scene.addItem(self.rect2)

        # 最初は選択されているアイテムはなし
        self.selected_item = None

    # 選択されているアイテムがあればキーの方向に応じて移動させる
    def keyPressEvent(self, event):
        if self.selected_item:
            if event.key() == Qt.Key_Left:
                self.selected_item.moveBy(-10, 0)
            elif event.key() == Qt.Key_Right:
                self.selected_item.moveBy(10, 0)
            elif event.key() == Qt.Key_Up:
                self.selected_item.moveBy(0, -10)
            elif event.key() == Qt.Key_Down:
                self.selected_item.moveBy(0, 10)

    # mousePressEventでクリックについて監視する
    def mousePressEvent(self, event):
        super().mousePressEvent(event)
        # クリックされた場所のアイテムを取得
        item = self.itemAt(event.pos()) 
        # アイテムがRectItemであった場合は選択されていたものがあれば青くし,選択されたものを赤くする
        if item and isinstance(item, QGraphicsRectItem):
            if self.selected_item:
                self.selected_item.setBrush(QBrush(Qt.blue))
            self.selected_item = item
            self.selected_item.setBrush(QBrush(Qt.red))

class MainWindow(QWidget):
    def __init__(self):
        super().__init__()
        self.initUI()
    
    def initUI(self):
        self.setWindowTitle('Main Window') # ウィンドウのタイトルを入力する
        self.setGeometry(100, 100, 800, 800) # (x0,y0,x1,y1) ウィンドウの初期位置を設定する

        # レイアウトを作成
        self.layout = QVBoxLayout()

        # GraphicsViewを作成 表示内容はGraphicsViewで定義
        self.view = GraphicsView()
        self.layout.addWidget(self.view)

        self.setLayout(self.layout)

if __name__ == '__main__':
    app = QApplication(sys.argv)
    main_window = MainWindow()
    main_window.show()
    sys.exit(app.exec_())

【目次に戻る】 次へ進む ->