粗大メモ置き場

個人用,たまーに来訪者を意識する雑記メモ

Python,PyQtで簡単にGUIを作る ② Splitterを使った配置とcsvのPlotter

目標

PythonでちゃっちゃとGUIを作ることを目標にしています。

前回に引き続き下記の2つを満たすようなものを作成していきます。

  • 複数ある要素を選択
  • 選択した要素をグラフに随時プロット

ossyaritoori.hatenablog.com

f:id:ossyaritoori:20210718014237p:plain
今回つくるやつです。左でcsvのHeaderを選択して右の領域に該当する値がPlotされます。

今回やること

ざっくりと下記のことをやっていきます。

  • Splitterを用いた配置
  • pandasと組み合わせて数値をPlot

Splitterを用いた配置

SetGeometryなどを用いずにSplitterという仕切りを使ってWidgetを配置する方法を用います。

下記のチュートリアルを見てみましょう。

www.finddevguides.com

使い方はVboxたちと似通っていて定義後はaddWidgetでWidgetを追加できます。

#from PyQt5.QtWidgets import *
      splitter1 = QSplitter(Qt.Horizontal) #横向きにStackする
      textedit = QTextEdit()
      textedit2 = QTextEdit()
      splitter1.addWidget(textedit2)
      splitter1.addWidget(textedit)
# 中略
# 最後に予め作成したQVboxlayoutに登録
      vbox.addWidget(splitter1)

また,SplitterにSplitterを追加できるため下記のようにプログラムを書けばいろいろ追加できます。

f:id:ossyaritoori:20210718103855p:plain
上がSplitter1で分けた領域,そしてSplitter2ではSplitter1と下のWidgetを分けている

#!/usr/bin/python3
# -*- coding: utf-8 -*-

import sys
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *

"""
Qt splitter のチュートリアル
https://www.finddevguides.com/Pyqt-qsplitter-widget

Example:Qwidgetから継承
SplitterはaddWidgetで部品を追加可能。Splitter自身も追加可能
基本的に一つの領域に一つの部品を配置できる。
"""

class Example(QWidget):

   def __init__(self):
      super(Example, self).__init__()

      self.initUI()

   def initUI(self):

      hbox = QHBoxLayout(self)
      
      # フレームを作る
      topleft = QFrame()
      topleft.setFrameShape(QFrame.StyledPanel)
      bottom = QFrame()
      bottom.setFrameShape(QFrame.StyledPanel)

      # 水平に分割
      splitter1 = QSplitter(Qt.Horizontal)
      textedit = QTextEdit()
      textedit2 = QTextEdit()
      textedit3 = QTextEdit()
      splitter1.addWidget(textedit2)
      splitter1.addWidget(textedit)
      splitter1.setSizes([100,200])

      splitter2 = QSplitter(Qt.Vertical)
      splitter2.addWidget(splitter1)
      splitter2.addWidget(textedit3)

      # Widgetを追加
      hbox.addWidget(splitter2)

      # レイアウト適用
      self.setLayout(hbox)
      QApplication.setStyle(QStyleFactory.create('Cleanlooks'))

      # Windowサイズを規定
      self.setGeometry(300, 300, 300, 200)
      self.setWindowTitle('QSplitter demo')
      self.show()

def main():
   app = QApplication(sys.argv)
   ex = Example()
   sys.exit(app.exec_())

if __name__ == '__main__':
   main()

csvファイルを読んでPlotする

前回の記事と合わせると冒頭で示したようなcsvファイルから数値列を読んでPlotするGUIが作れます。

流れは下記のとおりです。

  • csvを読み込んでPandasのDataFrameに落とす
  • 数値列のみを抽出してColumn名をリスト化
  • GUIを作成,選択したColumnリストをもとにPlot

データとして使うCSVはAtushi Sakai大先生のリポジトリから拝借してきました。

github.com

pandasで非数値列を弾く方法

CSVを読んでPlotするのは良いですがその際,非数値が混じってエラー終了するのは避けたいです。

下記のようにselect_dtypesを使えば非数値列を排除できて安全です。

        # dfの非数値の列のみ選択
        self.df = df.select_dtypes(include='number')

コード

#!/usr/bin/python3
# -*- coding: utf-8 -*-

import sys
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
import pandas as pd 
import matplotlib.pyplot as plt
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas


"""
Qt splitter のチュートリアル
https://www.finddevguides.com/Pyqt-qsplitter-widget

Example:Qwidgetから継承
SplitterはaddWidgetで部品を追加可能。Splitter自身も追加可能
基本的に一つの領域に一つの部品を配置できる。
"""

class Example(QWidget):
    def __init__(self,filename=None):
        super(Example, self).__init__()
        
        self.loadcsv(filename)
        self.initUI()
    
    def loadcsv(self,filename):
        df = pd.read_csv(filename)
        # 非数値の列のみ選択
        self.df = df.select_dtypes(include='number')
        print(self.df)
        self.columnlist = self.df.columns # list

    def initUI(self):
        hbox = QHBoxLayout(self)
    
        # list/figure作成
        self.create_listwidget()
        self.create_figure()

        # 左側作成
        splitter_l = QSplitter(Qt.Vertical)
        splitter_l.addWidget(self.add_label("Plotする要素を選択"))
        splitter_l.addWidget(self.listWidget)
        # 右側作成
        splitter = QSplitter(Qt.Horizontal)
        splitter.addWidget(splitter_l)
        splitter.addWidget(self.FigureCanvas)
        #splitter.setSizes([100,200])

        # Widgetを追加
        hbox.addWidget(splitter)

        # レイアウト適用
        self.setLayout(hbox)
        QApplication.setStyle(QStyleFactory.create('Cleanlooks'))

        # Windowサイズを規定
        #self.setGeometry(300, 300, 300, 200)
        self.setWindowTitle('CSV plotter demo')
        self.show()

    def create_listwidget(self):
        # リスト作成
        self.listWidget = QListWidget()
        self.listWidget.setSelectionMode( #複数選択可能にする
            QAbstractItemView.MultiSelection
        )
        for col in self.columnlist:
            item = QListWidgetItem(col)
            self.listWidget.addItem(item)
        self.listWidget.itemClicked.connect(self.printItemText)
    
    def create_figure(self):
        # Figureを作成
        self.Figure = plt.figure()
        self.FigureCanvas = FigureCanvas(self.Figure)  # FigureをFigureCanvasに追加
        self.axis = self.Figure.add_subplot(1,1,1) # axを持っておく



    def add_label(self,text):
        # ラベルを作る
        label=QLabel(self)
        label.setText(text)
        label.adjustSize()
        return label


    def printItemText(self):
        # 選択したアイテムを表示
        items = self.listWidget.selectedItems()
        self.selected_columns = []
        for i in range(len(items)):
            self.selected_columns.append(self.listWidget.selectedItems()[i].text())

        self.plot_selected_columns()

    # Plotをアップデート
    def plot_selected_columns(self):
        self.axis.cla() # リセットを掛ける必要がある。
        df = self.df[self.selected_columns]
        df.plot(ax=self.axis)
        plt.grid(); 
        self.FigureCanvas.draw()


def main():
   app = QApplication(sys.argv)
   ex = Example("samplecsv.csv")
   sys.exit(app.exec_())

if __name__ == '__main__':
   main()

余談:Checkbox形式への変更

Listwidgetは結構見た目があれなので自分的にはCheckboxのほうが好きだったりします。 下記のように設定すればCheckboxを導入できます。

    # Checkbox Widgetの作成
    def add_checkbox(self,name):
      chxlist = QCheckBox(name, self)
      chxlist.stateChanged.connect(self.checklist_clicked)
      return chxlist
    
    #  クリックされたCheckboxをみてリストに追加 or 削除
    def checklist_clicked(self, state):
        checked_name = str(self.sender().text())
        if state == Qt.Checked: # if checked
            # add to the list
            self.clicked_list.append(checked_name)
        else: # if unchecked
            # remove from list
            self.clicked_list = [x for x in self.clicked_list if not x == checked_name]

余談2: Checkboxをまとめて導入

Checkboxをまとめて導入したいときは,上記のように一個一個Widgetを作るのではなく,VboxLayoutを用意してその中に入れていくのが良いです。

ちょっと長めの記述になります。

    def create_checkboxes(self):
        vbox = QVBoxLayout()
        for col in self.columnlist:
            vbox.addWidget(self.add_checkbox(col))
        # 箱を用意する
        frame = QFrame(self) # Frame定義
        frame.setFrameShape(QFrame.StyledPanel) # 形状決定。パネル。
        vbox.addStretch(1) # 行間を定義
        frame.setLayout(vbox)
        return frame

補足:Checkboxに変更した版のGUI

以上をまとめると下記のような感じになります。

f:id:ossyaritoori:20210718115306p:plain
記述量は増えますが自分はこっちのほうがスッキリしていて好みです。

#!/usr/bin/python3
# -*- coding: utf-8 -*-

import sys
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
import pandas as pd 
import matplotlib.pyplot as plt
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas


"""
Qt splitter のチュートリアル
https://www.finddevguides.com/Pyqt-qsplitter-widget

Example:Qwidgetから継承
SplitterはaddWidgetで部品を追加可能。Splitter自身も追加可能
基本的に一つの領域に一つの部品を配置できる。
"""

class Example(QWidget):
    def __init__(self,filename=None):
        super(Example, self).__init__()
        
        self.loadcsv(filename)
        self.initUI()
    
    def loadcsv(self,filename):
        df = pd.read_csv(filename)
        # 非数値の列のみ選択
        self.df = df.select_dtypes(include='number')
        print(self.df)
        self.columnlist = self.df.columns # list

    def initUI(self):
        hbox = QHBoxLayout(self)
    
        # list/figure作成
        #self.create_listwidget()
        self.create_figure()

        # 左側作成
        splitter_l = QSplitter(Qt.Vertical)
        splitter_l.addWidget(self.add_label("Plotする要素を選択"))
        #splitter_l.addWidget(self.listWidget)
        splitter_l.addWidget(self.create_checkboxes())
        # 右側作成
        splitter = QSplitter(Qt.Horizontal)
        splitter.addWidget(splitter_l)
        splitter.addWidget(self.FigureCanvas)
        #splitter.setSizes([100,200])

        # Widgetを追加
        hbox.addWidget(splitter)

        # レイアウト適用
        self.setLayout(hbox)
        QApplication.setStyle(QStyleFactory.create('Cleanlooks'))

        # Windowサイズを規定
        #self.setGeometry(300, 300, 300, 200)
        self.setWindowTitle('CSV plotter demo')
        self.show()

    def create_listwidget(self):
        # リスト作成
        self.listWidget = QListWidget()
        self.listWidget.setSelectionMode( #複数選択可能にする
            QAbstractItemView.MultiSelection
        )
        for col in self.columnlist:
            item = QListWidgetItem(col)
            self.listWidget.addItem(item)
        self.listWidget.itemClicked.connect(self.printItemText)
    
    def create_figure(self):
        # Figureを作成
        self.Figure = plt.figure()
        self.FigureCanvas = FigureCanvas(self.Figure)  # FigureをFigureCanvasに追加
        self.axis = self.Figure.add_subplot(1,1,1) # axを持っておく

    def create_checkboxes(self):
        vbox = QVBoxLayout()
        for col in self.columnlist:
            vbox.addWidget(self.add_checkbox(col))
        # 箱を用意してはる
        frame = QFrame(self) # Frame定義
        frame.setFrameShape(QFrame.StyledPanel) # 形状決定。パネル。
        vbox.addStretch(1) # 行間を定義
        frame.setLayout(vbox)
        return frame

    def add_checkbox(self,name):
        chxlist = QCheckBox(name, self)
        chxlist.stateChanged.connect(self.checklist_clicked)
        return chxlist

    #  クリックされたCheckboxをみてリストに追加 or 削除
    def checklist_clicked(self, state):
        checked_name = str(self.sender().text())
        
        try:
            self.clicked_list
        except:
            self.clicked_list = []

        if state == Qt.Checked: # if checked
            # add to the list
            self.clicked_list.append(checked_name)
        else: # if unchecked
            # remove from list
            self.clicked_list = [x for x in self.clicked_list if not x == checked_name]
        
        self.selected_columns = self.clicked_list
        self.plot_selected_columns()


    def add_label(self,text):
        # ラベルを作る
        label=QLabel(self)
        label.setText(text)
        label.adjustSize()
        return label


    def printItemText(self):
        # 選択したアイテムを表示
        items = self.listWidget.selectedItems()
        self.selected_columns = []
        for i in range(len(items)):
            self.selected_columns.append(self.listWidget.selectedItems()[i].text())

        self.plot_selected_columns()

    # Plotをアップデート
    def plot_selected_columns(self):
        self.axis.cla() # リセットを掛ける必要がある。
        df = self.df[self.selected_columns]
        df.plot(ax=self.axis)
        plt.grid(); 
        self.FigureCanvas.draw()


def main():
   app = QApplication(sys.argv)
   ex = Example("samplecsv.csv")
   sys.exit(app.exec_())

if __name__ == '__main__':
   main()

TODO

いくつか目処は立っていますが下記の要素を徐々に追加していくとそれっぽくなるものと思われます。