GUIとしてのPyQt5の使い方

# はじめに
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を始める
## 導入
```bash
$ pip install PyQt5
```
でインストールできる。ただ、環境によっては必ずしもネットワークを介してpipが使えるわけではない。
そんなとき、PyQt5は依存関係が一つ(sip)しかないので楽である。

```bash
$ 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)→終了処理の流れとなる。

```python
#!/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](https://qiita-image-store.s3.amazonaws.com/0/93354/1b36a248-54f6-a6b7-c278-cee1c684cba3.png)

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

```python
#!/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の３つである。特定のイベントをsignalに、イベントをトリガに呼び出す関数をslotに定義する。signalとslotを結びつけるのがconnectである。本家のC++では次のように書く。

```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だけである。

```python

#!/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](https://qiita-image-store.s3.amazonaws.com/0/93354/6a449831-c7a6-af3f-4cdd-d4ae541f4b30.png)
押すと次のようになる。
![スクリーンショット 2018-04-22 15.22.41.png](https://qiita-image-store.s3.amazonaws.com/0/93354/4108edd8-f4a2-045c-bae7-0d3bb1127365.png)


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

### 各関数のimport
どこに何があるかの一覧。

```python
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
```


### アプリの定義
```python
app = QApplication(sys.argv)
```

### アイコンの定義
```python
path = os.path.join(os.path.dirname(sys.modules[__name__].__file__), 'icon_persephone.png')
app.setWindowIcon(QIcon(path))
```
以降、小分けにするとわかりづらくなるので、コメント付きのコードをのせる。

### persephonep.py
```python
#!/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
``` python
#!/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でインターフェースを作る際の候補として、決して他のツールに負けていない。