Python,PyQtで簡単にGUIを作る ② Splitterを使った配置とcsvのPlotter
目標
PythonでちゃっちゃとGUIを作ることを目標にしています。
前回に引き続き下記の2つを満たすようなものを作成していきます。
- 複数ある要素を選択
- 選択した要素をグラフに随時プロット
今回やること
ざっくりと下記のことをやっていきます。
- Splitterを用いた配置
- pandasと組み合わせて数値をPlot
Splitterを用いた配置
SetGeometryなどを用いずにSplitterという仕切りを使ってWidgetを配置する方法を用います。
下記のチュートリアルを見てみましょう。
使い方は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を追加できるため下記のようにプログラムを書けばいろいろ追加できます。
#!/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はAtushi Sakai大先生のリポジトリから拝借してきました。
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
以上をまとめると下記のような感じになります。
#!/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
いくつか目処は立っていますが下記の要素を徐々に追加していくとそれっぽくなるものと思われます。
- ファイル選択ダイアログの作成:【PythonでGUI】PyQt5 -ダイアログ- - Qiita
- Plotした関数のマウスGUI実装
- Saveボタンを押した時点で画像をSaveなどグラフ操作に関するボタン