Edited at

GUIとしてのPyQt5の使い方

More than 1 year has passed since last update.


はじめに

2018年現在、Pythonは非常に人気な言語であって、数値計算やサーバーサイド、機械学習と幅広い領域で使われている。そんなPythonのGUIツールとしては、Python標準ライブラリのTkInter, 最近人気が高いwxPython, kivyなどあるが、今回はPyQt5について書く。


PyQt5とは

Qt(キュート)はC++のクロスプラットフォームフレームワークで、GUIツールの開発を行える。商用利用にはライセンスの縛りがあるが、営利目的でなければ十分使うことができる。大量のモジュールを持ち開発が用意で高速に実行可能であるが、

connect/slotという独特の書き方によってイベントを取得するため慣れが必要である。Qt5が現在の最新バージョンである。

QtはRubyやJava等多くの言語へのバインディングがなされていて、そのうちPython用に作られてたのがPyQtであり、Qt5に対応するのがPyQt5である。C++だといささか癖の強いQt5ではあるが、PyQt5であればだいぶ使い勝手は良い。しかし、最大の問題は(日本語)資料の少なさである。というわけで、今回はPyQt5の簡単な使い方についてまとめる。


PyQt5を始める


導入

$ pip install PyQt5

でインストールできる。ただ、環境によっては必ずしもネットワークを介してpipが使えるわけではない。

そんなとき、PyQt5は依存関係が一つ(sip)しかないので楽である。

$ pip show PyQt5

Name: PyQt5
Version: 5.10.1
Summary: Python bindings for the Qt cross platform UI and application toolkit
()
Requires: sip
Required-by:
$ pip show sip
Name: sip
Version: 4.19.8
Summary: Python extension module generator for C and C++ libraries
()
Requires:
Required-by: PyQt5

たくさんのファイルを集める必要なく、比較的かんたんにインストールできる。


基本的な書き方

tutorialを参考に、簡単な書き方を説明する。appを宣言→画面を定義→表示(show)→終了処理の流れとなる。

#!/usr/bin/env python

# coding: utf-8
#

import sys
from PyQt5.QtWidgets import QApplication, QWidget

if __name__ == '__main__':

app = QApplication(sys.argv)
widget = QWidget()
widget.resize(250, 150)
widget.move(300, 300)
widget.setWindowTitle('sample')
widget.show()

sys.exit(app.exec_())

スクリーンショット 2018-04-22 14.21.30.png


クラス化

画面はクラス化して管理する。先のプログラムは、次のようになる。

#!/usr/bin/env python

# coding: utf-8
#

import sys
from PyQt5.QtWidgets import QApplication, QWidget

class ExampleWidget(QWidget):

def __init__(self):
super().__init__()
self.initUI()

def initUI(self):
self.resize(250, 150)
self.move(300, 300)
self.setWindowTitle('sample')
self.show()

if __name__ == '__main__':

app = QApplication(sys.argv)
ew = ExampleWidget()
sys.exit(app.exec_())

以降は、これをベースに作成する。


PyQt5でアプリを作る


connect(とsignalとconnect)

Qtの重要な機能は、signal/connect/slotの3つである。特定のイベントをsignalに、イベントをトリガに呼び出す関数をslotに定義する。signalとslotを結びつけるのがconnectである。本家のC++では次のように書く。

#include <QApplication>

#include <QPushButton>

int main(int argc, char** argv)
{
QApplication app(argc, argv);
QPushButton* button = new QPushButton("Quit");
QObject::connect(button, SIGNAL( clicked() ),
&app, SLOT(quit()) );
button->show();
return app.exec();
}

これは、Quitボタンを押すと画面が閉じるだけのアプリである。PyQt5でも同様の概念が存在するものの、明示的に書くのはconnectだけである。


#!/usr/bin/env python
# coding: utf-8
#

import sys
from PyQt5.QtWidgets import QApplication, QWidget, QPushButton, QGridLayout, QLabel

class ExampleWidget(QWidget):

def __init__(self):
super().__init__()
self.initUI()

def initUI(self):
self.resize(250, 150)
self.move(300, 300)
self.setWindowTitle('sample')

# buttonの設定
self.button = QPushButton('Clear!!')
self.label = QLabel('connected')

# buttonのclickでラベルをクリア
self.button.clicked.connect(self.label.clear)

# レイアウト配置
self.grid = QGridLayout()
self.grid.addWidget(self.button, 0, 0, 1, 1)
self.grid.addWidget(self.label, 1, 0, 1, 2)
self.setLayout(self.grid)

# 表示
self.show()

if __name__ == '__main__':

app = QApplication(sys.argv)
ew = ExampleWidget()
sys.exit(app.exec_())

「Clear!!」ボタンを押すことで、下の文字が消える。

スクリーンショット 2018-04-22 15.22.13.png

押すと次のようになる。

スクリーンショット 2018-04-22 15.22.41.png


各関数の使い方

PyQt5の各関数の使い方は、http://pyqt.sourceforge.net/Docs/PyQt5/index.html# に書かれているが、結局Qt5のページに飛ばされてC++を読むことになったりする。というわけで、ここでメモ的にまとめる。なお、コード自体は、自分で作ったPyQt5製ブラウザ PersephonePのものを使う。(過去の投稿


各関数のimport

どこに何があるかの一覧。

from PyQt5.QtWidgets import QMainWindow, QWidget, QPushButton, QTabWidget, QApplication, QHBoxLayout, QVBoxLayout, QLabel, QGridLayout, QLineEdit, QTextEdit, QDesktopWidget

from PyQt5.QtCore import pyqtSlot, QUrl
from PyQt5.QtGui import QIcon
from PyQt5.QtWebEngineWidgets import QWebEngineView, QWebEngineProfile, QWebEngineDownloadItem


アプリの定義

app = QApplication(sys.argv)


アイコンの定義

path = os.path.join(os.path.dirname(sys.modules[__name__].__file__), 'icon_persephone.png')

app.setWindowIcon(QIcon(path))

以降、小分けにするとわかりづらくなるので、コメント付きのコードをのせる。


persephonep.py

#!/usr/bin/env python

# coding: utf-8
#

import sys
from PyQt5.QtWidgets import QMainWindow, QWidget, QPushButton, QTabWidget, QApplication, QHBoxLayout, QVBoxLayout, QLabel, QGridLayout
from PyQt5.QtGui import QIcon
from func_persephonep import *
from PyQt5.QtCore import pyqtSlot

__program__ = 'PERSEPHONEP'

class PersephoneMainWidget(QMainWindow):

def __init__(self):
super().__init__()
self.title = __program__
self.left = 100
self.top = 100
self.width = 1200
self.height = 800
self.setWindowTitle(self.title)
self.setGeometry(self.left, self.top, self.width, self.height)
self.table_widget = PersephoneTableWidget(self)
self.setCentralWidget(self.table_widget)

self.show()

class PersephoneTableWidget(QWidget):

def __init__(self, parent):
super(QWidget, self).__init__(parent)
self.layout = QVBoxLayout(self)

# initialize tab screen
self.tabs = QTabWidget()
# self.tab1 = QWidget()
# self.tab2 = QWidget()
self.tab = []
self.tabs.resize(1200, 800)

# Add Tabs
self._addTab(0)
# self.tabs.addTab(self.tab1, 'Tab 1')
# self.tabs.addTab(self.tab2, 'Tab 2')
self.add_button = QPushButton('+')
self.add_button.setStyleSheet('background-color:gray')
self.add_button.clicked.connect(self._addTab)
self.app_info = QLabel('PERSEPHONE is developed by @montblanc18. This is a Web Browser based on Python 3 and PyQt5.')

# define the delete tab process
self.tabs.tabCloseRequested.connect(self.closeTab)

# Create first tab
self.tabs.setTabsClosable(True);
self.tab[0].layout = QVBoxLayout(self)
self.pushButton1 = QPushButton('PyQt5 button')
self.tab[0].layout.addWidget(self.pushButton1)
self.tab[0].setLayout(self.tab[0].layout)

# Add tabs to widget
self.layout.addWidget(self.add_button)
self.layout.addWidget(self.tabs)
self.setLayout(self.layout)

def _addTab(self, index):
''' add Tab
'''

self.tab.append(PersephoneWindow(parent = self))
self.tabs.addTab(self.tab[index-1], '')

def closeTab(self, index):
''' close Tab.
'''

widget = self.tabs.widget(index)
self.tabs.removeTab(index)

def center(self):
''' centering widget
'''

qr = self.frameGeometry()
cp = QDesktopWidget().availableGeometry().center()
qr.moveCenter(cp)
self.move(qr.topLeft())

@pyqtSlot()
def on_click(self):
print("\n")
for currentQTableWidgetItem in self.tableWidget.selectedItems():
print(currentQTableWidgetItem.row(), currentQTableWidgetItem.column(), currentQTableWidgetItem.text())

if __name__ == '__main__':
app = QApplication(sys.argv)

# setWindowIcon is a method for QApplication, not for QWidget
path = os.path.join(os.path.dirname(sys.modules[__name__].__file__), 'icon_persephone.png')
app.setWindowIcon(QIcon(path))

ui = PersephoneMainWidget()
sys.exit(app.exec_())


func_persephonep.py

#!/usr/bin/env python

# -*- coding: utf-8 -*-

import sys
import os
from PyQt5.QtWidgets import (QWidget, QLabel, QLineEdit,
QTextEdit, QGridLayout, QApplication, QPushButton, QDesktopWidget)
from PyQt5.QtGui import QIcon
from PyQt5.QtCore import QUrl
from PyQt5.QtWebEngineWidgets import QWebEngineView, QWebEngineProfile, QWebEngineDownloadItem

__program__ = 'PERSEPHONE'

class PersephoneWindow(QWidget):

def __init__(self, parent = None):
super(PersephoneWindow, self).__init__()
self.initUI(parent = parent)

def initUI(self, parent = None):

initurl = 'https://www.google.co.jp'

# setting window
self.window = QWebEngineView()
self.window.load(QUrl(initurl))
self.window.resize(1000,600)
self.window.move(200,200)
self.window.setWindowTitle(__program__)

# setting button
self.back_button = QPushButton('back')
self.back_button.setToolTip('Go back to previous page.')
self.back_button.clicked.connect(self.window.back)
self.forward_button = QPushButton('forward')
self.forward_button.setToolTip('Go to the next page.')
self.forward_button.clicked.connect(self.window.forward)
self.reload_button = QPushButton('reload')
self.reload_button.setToolTip('Reload this page.')
self.reload_button.clicked.connect(self.window.reload)
self.url_edit = QLineEdit()
self.url_edit.setToolTip('URL box')
self.move_button = QPushButton('move')
self.move_button.setToolTip('Move to the page set at URL box.')
self.move_button.clicked.connect(self.loadPage)
self.url_edit.returnPressed.connect(self.loadPage)
self.home_button = QPushButton('home')
self.home_button.setToolTip('Move to the home page.')
self.home_button.clicked.connect(self.loadHomePage)

# signal catch from moving web pages.
self.window.urlChanged.connect(self.updateCurrentUrl)
self.window.page().profile().downloadRequested.connect(self._downloadRequested)

# setting layout
grid = QGridLayout()
grid.setSpacing(0)
grid.addWidget(self.back_button, 1, 0)
grid.addWidget(self.forward_button, 1, 1)
grid.addWidget(self.reload_button, 1, 2)
grid.addWidget(self.url_edit, 1, 3, 1, 10)
grid.addWidget(self.move_button, 1, 14)
grid.addWidget(self.home_button, 1, 15)
grid.addWidget(self.window,2, 0, 5, 16)
self.setLayout(grid)

if parent == None:
self.resize(1200, 800)
self.center()
self.setWindowTitle(__program__)
self.show()

def center(self):
''' centering widget
'''

qr = self.frameGeometry()
cp = QDesktopWidget().availableGeometry().center()
qr.moveCenter(cp)
self.move(qr.topLeft())

def loadPage(self):
''' move web page which is set at url_edit
'''

move_url = self.url_edit.text()
# check url
# If the head of move_url equals 'http://' or 'https://', query to google search form.
if not (move_url[0:7] == 'http://' or move_url[0:8] == "https://" or move_url[0:8] == 'file:///' or move_url[0:6] == 'ftp://'):
search_word = move_url.replace(' ', '+').replace(' ', '+')
google_search_url = 'https://www.google.co.jp/search?ie=utf-8&oe=utf-8&q={}&hl=ja&btnG=search'.format(search_word)
move_url = google_search_url

move_url = QUrl(move_url)
self.window.load(move_url)
self.updateCurrentUrl

def updateCurrentUrl(self):
''' rewriting url_edit when you move different web page.
'''

# current_url = self.window.url().toString()
self.url_edit.clear()
self.url_edit.insert(self.window.url().toString())

def loadHomePage(self):
''' move to the home page
'''

initurl = 'https://www.google.co.jp'
self.window.load(QUrl(initurl))

def saveFile(self):
print('download')

def _downloadRequested(self, item): # QWebEnginDownloadItem
# print('downloading to', item.path)
item.accept()

if __name__ == '__main__':
# mainPyQt5()
app = QApplication(sys.argv)

# setWindowIcon is a method for QApplication, not for QWidget
path = os.path.join(os.path.dirname(sys.modules[__name__].__file__), 'icon_persephone.png')
app.setWindowIcon(QIcon(path))

ex = PersephoneWindow()
sys.exit(app.exec_())

( 実はタブの管理がうまくいってなかったりする。。。 タブ管理うまく直せた様子。)


最後に

Qt自体は非常に強力なツールであり、C++ではなくPythonを使うことで簡単にGUIを作ることができる。Pythonでインターフェースを作る際の候補として、決して他のツールに負けていない。