【Python GUI tkinter実用サンプル】ttk.Treeviewを使ってcsvを閲覧編集するcsvビューワーを作成する
概要&使い方
tkinterでcsvを閲覧編集するcsv用ビューワーのサンプルコード。
①閲覧、編集したいファイルの読み込み
②編集したいレコードの選択
③編集したい値をRowDataの中で書き換え
④commitボタンで更新(新規データとする場合はinsertボタン)
⑤saveボタンでファイルに保存
ソースコード説明
ソースコード内に記載されているdocstring(コメント)を参照ください
csvの内容を更新して保存したときのサンプル画像
①csvを開く
②Appleのレコードを選択し、RowDataでValueを250に変更
③commitを押下しデータの更新
④Saveボタンを押下し保存。ファイルを確認するとAppleのValueの値が更新されている
サンプルデータ(.csv)
①果物
index,Name,Num,Value 1,"orange",3,120 2,"Apple",2,180 3,"banana",2,100 4,"strawberry",10,500 5,"kiwi fruit",3,250 6,"grape",1,350
②弊社(リツアン)のRSTCの意味
Initial,Mean,Mean(J) R,Ritsuan,リツアン S,Suprise,驚き T,TeamWork,チームワーク C,Company,会社
サンプルコード
※ViewとControl&Logicどちらも取得ください
view
・csvview.py
from tkinter import *
import tkinter.ttk as ttk
import tkinter.filedialog as filedialog
class FileOpenFrame(ttk.Frame):
"""
ファイルの読み込み用フレーム
"""
def __init__(self, master,file_entry_width=100):
super().__init__(master)
self.filePath = StringVar()
self.createWidget(file_entry_width)
self.pack()
def createWidget(self,entry_width):
filePathLabel = ttk.Label(self,text="FilePath")
filePathLabel.grid(column=0,row=0)
filepathEntry = ttk.Entry(self,textvariable=self.filePath,widt=entry_width)
filepathEntry.grid(column=1,row=0)
filepathButton = ttk.Button(self,text="open",command=self.openFileDialog)
filepathButton.grid(column=2,row=0)
self.readButton = ttk.Button(self,text="read")
self.readButton.grid(column=3,row=0)
def openFileDialog(self):
"""
ファイルダイアログを開く
"""
file = filedialog.askopenfilename(filetypes=[("csv", "*.csv")]);
self.filePath.set(file)
def getFilePath(self):
return self.filePath.get()
def setReadButtonCommand(self,func):
"""
読み込みを押したときのコマンドを指定する
"""
self.readButton["command"] = func
class TreeView(ttk.Frame):
"""
csvのデータを実際に表示するTreeview
"""
def __init__(self,master):
super().__init__(master)
self.tree = None
self.selected_iid = None
self.columns =[]
self.createWidget()
self.pack()
self.setSampleData()
def createWidget(self):
"""
icon列は不要なのでshow="headings"を指定
"""
self.tree = ttk.Treeview(self)
self.tree["show"] = "headings"
self.tree.pack()
def setColomns(self,columns):
"""
テーブルの列名を指定
"""
self.columns = columns
self.tree["columns"] = self.columns
for col in columns:
self.tree.heading(col,text=col)
def setRow(self,index ="" ,row_data=[]):
"""
新規レコードの挿入
"""
self.tree.insert("",index="end",text=index,values = row_data)
def setRows(self,rows_data):
"""
複数の新規レコードの挿入
"""
for i,row_data in enumerate(rows_data):
self.setRow(index = i,row_data = row_data)
def setSampleData(self):
"""
起動時のサンプルデータ
"""
column_data = ("Name","Value")
rows_data = [("None","None")]
self.deleteRows()
self.setColomns(column_data)
self.setRows(rows_data)
def deleteRows(self):
"""
レコードの全削除
"""
children = self.tree.get_children("")
for child in children:
self.tree.delete(child)
def addSelectAction(self,func):
"""
レコードが選択されたときに呼ばれるイベントを登録
"""
self.tree.bind("<<TreeviewSelect>>",func)
def getItem(self):
"""
現在選択状態のレコードの取得
"""
self.selcted_iid = self.tree.focus()
return self.tree.item(self.selcted_iid,"values")
def getRows(self):
"""
全レコードの取得
"""
rows =[]
children = self.tree.get_children("")
for child in children:
item = self.tree.item(child,"values")
rows.append(item)
return rows
def getColumn(self):
"""
列名の取得
"""
return self.columns
def getDataMap(self):
"""
現在選択されているレコードの
列名と値のマップを取得
"""
item = self.getItem()
if len(self.columns) != len(item):
return {"none":"none"}
else:
data_map = {}
for i,column in enumerate(self.columns):
data_map[column] = item[i]
return data_map
def updateValue(self,iid,new_values):
"""
値の更新
"""
self.tree.item(self.iid,values=new_values)
def updateValue(self,new_values):
"""
現在選択されているレコードの値の更新
"""
self.tree.item(self.selcted_iid,values=new_values)
def update(self,value_dict):
"""
マップからリストに変更後
値の更新
"""
data =[]
for column in self.columns:
data.append(value_dict[column])
self.updateValue(data)
def insert(self,value_dict):
"""
マップからリストに変更後
新規レコードの挿入
"""
data =[]
for column in self.columns:
data.append(value_dict[column])
children = self.tree.get_children("")
index = len(children)
self.setRow(index = str(index), row_data=data)
class LabelEntryWidget(ttk.Frame):
"""
LabelとEntryがくっついたWidget
"""
def __init__(self, master,text="property"):
super().__init__(master)
self.value = StringVar()
self.createWidgets(text)
def createWidgets(self,text="property"):
self.label = ttk.Label(self,text=text)
self.label.pack(side="left")
self.entry = ttk.Entry(self,textvariable=self.value)
self.entry.pack(side="left")
def getVar(self):
"""
値を取得するためのWidget変数の取得
"""
return self.value
def setLabelOption(self,key_dict):
"""
Labelのオプション指定(オプションはdictで渡す)
"""
for k in key_dict.keys():
self.label[k] = key_dict[k]
def setEntryOption(self,key_dict):
"""
Entryのオプション指定(オプションはdictで渡す)
"""
for k in key_dict.keys():
self.entry[k] = key_dict[k]
class PropertyView(ttk.Frame):
"""
選択されたレコードの内容を修正、
新規レコードなどを挿入するフレーム
"""
def __init__(self,master):
super().__init__(master)
self.pack()
self.param_dict={}
def createWidget(self,columns):
"""
列の要素数分入力ボックスの作成
"""
self.delete()
self.param_dict ={}
for column in columns:
option = {"width":10}
param = LabelEntryWidget(self,text = column)
param.setLabelOption(option)
param.pack()
self.param_dict[column] = param.getVar()
self.createInsertUpdateButton()
self.createSaveButton()
def createInsertUpdateButton(self):
"""
更新ボタンと挿入ボタンを作成
"""
button_frame = ttk.Frame(self)
button_frame.pack(anchor="e")
self.update_button = update = ttk.Button(button_frame,text = "commit")
self.insert_button = insert = ttk.Button(button_frame,text = "insert")
update.pack(side="left")
insert.pack(side="left")
def createSaveButton(self):
"""
保存ボタンを作成
"""
save_frame = ttk.Frame(self)
save_frame.pack(anchor="e")
self.save_button = save = ttk.Button(save_frame,text = "Save")
save.pack(side="left")
def delete(self):
"""
更新時用
自身のフレームに紐づく子Widgetの削除
"""
children = self.winfo_children()
for child in children:
child.destroy()
def setUpdateButtonCommand(self,command):
self.update_button["command"] = command
def setInsertButtonCommand(self,command):
self.insert_button["command"] = command
def setSaveButtonCommand(self,command):
self.save_button["command"] = command
def setParameter(self,param):
"""
取得したレコードデータを各入力ボックスのWidget変数に振り分け
"""
for key in self.param_dict.keys():
self.param_dict[key].set(param[key])
def getParameter(self):
"""
列名とWidget変数の値をマップにして返す
"""
param_dict = {}
for key,value in self.param_dict.items():
param_dict[key] = value.get()
return param_dict
class CSVView(ttk.Frame):
"""
CsvViewerのメインView
"""
def __init__(self, master):
super().__init__(master,borderwidth=10)
self.tree = None
self.createWidget()
self.setAction()
self.pack()
def createWidget(self):
"""
viewの組み立て
"""
self.createUpperFrame()
self.createLowerFrame()
def createUpperFrame(self):
"""
Csv読み込み用フレーム
"""
upper_frame = ttk.Frame(self)
upper_frame.pack()
self.file_path_frame = FileOpenFrame(upper_frame)
def createLowerFrame(self):
"""
Treeviewとレコード編集Widget用フレーム
"""
lower_frame = ttk.Frame(self)
lower_frame.pack()
left_frame = ttk.LabelFrame(lower_frame,text="CsvData")
left_frame.pack(side="left")
self.tree = TreeView(left_frame)
right_frame = ttk.LabelFrame(lower_frame,text="RowData")
right_frame.pack(side = "right",anchor="n")
self.property = PropertyView(right_frame)
self.property.createWidget(self.tree.getColumn())
def setAction(self):
"""
ツリーアイテム選択アクションの登録
"""
def _updateCommand():
"""
更新アクション
"""
param = self.property.getParameter()
print(param)
self.tree.update(param)
def _insertCommand():
"""
挿入アクション
"""
param = self.property.getParameter()
self.tree.insert(param)
def _func(event):
"""
レコード選択アクション
レコード選択ごとに更新インサートコマンドを登録しなおす
"""
self.property.setParameter(self.tree.getDataMap())
self.property.setUpdateButtonCommand(_updateCommand)
self.property.setInsertButtonCommand(_insertCommand)
self.tree.addSelectAction(_func)
def getFilePath(self):
"""
ファイルパスの取得
"""
return self.file_path_frame.getFilePath()
def setReadButtonCommand(self,func):
"""
読み込みボタンコマンド登録
"""
self.file_path_frame.setReadButtonCommand(func)
def setNewColumnAndData(self,columns,rows):
"""
新しい列名とレコードを設定する。
プロパティのWidgetも更新する。
"""
self.tree.deleteRows()
self.tree.setColomns(columns)
self.tree.setRows(rows)
self.property.createWidget(self.tree.getColumn())
def setSaveButtonCommand(self,func):
"""
保存ボタンコマンド登録
"""
self.property.setSaveButtonCommand(func)
def getColumns(self):
"""
列名リスト取得
"""
return self.tree.getColumn()
def getRows(self):
"""
レコードリスト取得
"""
return self.tree.getRows()
if __name__ == '__main__':
master = Tk()
master.title("csvview")
CSVView(master)
master.geometry("800x400")
master.mainloop()
Control & Logic
・csvcontrol.py
import csv
import tkinter.messagebox as messagebox
from tkinter import *
import tkinter.ttk as ttk
from csvview import CSVView
import os
class CSVLogic:
"""
csvViewer読み込み、書き込みロジック
"""
def __init__(self):
"""
列とレコード用の配列を初期化
"""
self.header =[]
self.data =[]
def readCsv(self,data_path):
"""
csvを読み込んで内部にデータを反映する
1行目を列名、他の行をデータとして取得する
"""
ret = True
header = []
data =[]
try :
with open(data_path, "r",newline="") as csv_file:
f = csv.reader(csv_file, delimiter=",", doublequote=True, lineterminator="\n", quotechar='"', skipinitialspace=True)
header = next(f)
print(header)
for row in f:
data.append(row)
except IOError as e:
print(e)
ret = False
self.header = header
self.data = data
return ret
def writeCsv(self,data_path,columns,rows):
"""
与えられた列名リストとレコードリストを書きだす
"""
ret = True
csv_file = open(data_path, "w",newline="")
try:
with open(data_path, 'w') as csv_file:
writer = csv.writer(csv_file, lineterminator='\n')
writer.writerow(columns)
writer.writerows(rows)
except IOError as e:
print(e)
ret = False
return ret
def getHeader(self):
return self.header
def getData(self):
return self.data
class CSVControl:
"""
csvViewerのコントローラー
"""
def __init__(self):
"""
アプリの立ち上げとイベント登録
"""
master = Tk()
master.title("CsvViewer")
master.geometry("1000x400")
self.view = CSVView(master)
self.logic = CSVLogic()
self.view.setReadButtonCommand(self.readButtonCommand)
master.mainloop()
def readButtonCommand(self):
"""
csv読み込みボタン用コマンド
csvから取得した列名、データをViewに反映する。
csvが変更されるごとにRowDataフレームがリロードされるので、
保存ボタンコマンドも再設定
"""
columns,datas = self.readCsv()
self.view.setNewColumnAndData(columns,datas)
self.view.setSaveButtonCommand(self.saveButtonCommand)
def saveButtonCommand(self):
"""
保存ボタン用コマンド
指定されたパスにviewで指定された情報をcsv形式で書きだす
"""
file_path = self.view.getFilePath()
columns = self.view.getColumns()
rows =self.view.getRows()
ret = self.logic.writeCsv(file_path,columns,rows)
if ret:
messagebox.showinfo("writecsv","succeed")
else:
messagebox.showerror("writecsv","failed")
def readCsv(self):
"""
csv読み込んで列名とデータを返却
"""
ret = False;
file_path = self.view.getFilePath()
if os.path.exists(file_path) :
ret = self.logic.readCsv(file_path)
if ret:
messagebox.showinfo("readcsv","succeed")
else:
messagebox.showerror("readcsv","failed")
return self.logic.getHeader(),self.logic.getData()
if __name__ == '__main__':
control = CSVControl()
# control.readCsv()