【Python GUI tkinter実用サンプル】ttk.Treeviewを使ってをSQLiteのDBを閲覧編集するSQLiteビューワーを作成する

<tkinterトップページに戻る>

概要&使い方

tkinterでSQLiteでDBを閲覧編集するSQLite用ビューワーのサンプルコード。

①閲覧、編集したいDBファイルの読み込み

②閲覧、編集したいテーブルの選択

③編集したいレコードの選択

④編集したい値をRowDataの中で書き換え

⑤commitボタンで更新(新規データとする場合はinsertボタン)

⑥saveボタンでファイルに保存

サンプルコード実行時のサンプル画像

①起動

Sqlite_0

②DBファイル選択

③テーブル選択

 

サンプルコード

※ViewとControl&Logicどちらも取得ください

※データ作成用スクリプトを実行するとサンプルDBデータが作成されます。

View

・dbview.py

from tkinter import *
import tkinter.ttk as ttk
import tkinter.filedialog as filedialog

TITLE_NAME_LABEL="SQLiteViewer"
FILE_PATH_LABEL="FilePath"
FILE_OPEN_BUTTON_LABEL="open"
FILE_READ_BUTTON_LABEL="read"
TABLE_LIST_LABEL="TableList"
TABLE_SELECT_BUTTON_LABEL="select"
TREE_TITLE_LABEL="DBData"
ROW_DATA_TITLE_LABEL="RawData"
ROW_DATA_UPDATE_BUTTON_LABEL="update"
ROW_DATA_INSERT_BUTTON_LABEL="insert"
ROW_DATA_SAVE_BUTTON_LABEL="save"
INSERT_MESSAGE_DIALOG_TITLE="insert"
INSERT_MESSAGE_SUCCEED="insert succeed"
INSERT_MESSAGE_FAILED="insert failed"
UPDATE_MESSAGE_DIALOG_TITLE="update"
UPDATE_MESSAGE_SUCCEED="update succeed"
SAVE_MESSAGE_DIALOG_TITLE="save"
SAVE_MESSAGE_SUCCEED="save succeed"
SAVE_MESSAGE_FAILED="save failed"

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=FILE_PATH_LABEL)
        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=FILE_OPEN_BUTTON_LABEL,command=self.openFileDialog)
        filepathButton.grid(column=2,row=0)
        self.readButton = ttk.Button(self,text=FILE_READ_BUTTON_LABEL)
        self.readButton.grid(column=3,row=0)

    def openFileDialog(self):
        """
        ファイルダイアログを開く
        """
        file  = filedialog.askopenfilename(filetypes=[("sqliteファイル", "*.*")]);
        self.filePath.set(file)

    def getFilePath(self):
        return self.filePath.get()

    def setReadButtonCommand(self,func):
        """
        読み込みを押したときのコマンドを指定する
        """
        self.readButton["command"] = func

class ComboboxFrame(ttk.Frame):
    def __init__(self, master,combo_width=100):
        super().__init__(master)
        self.table = StringVar()
        self.createWidget(combo_width)
        self.pack()

    def createWidget(self,entry_width):
        table_label = ttk.Label(self,text=TABLE_LIST_LABEL)
        table_label.grid(column=0,row=0)
        self.combo= combo = ttk.Combobox(self,textvariable=self.table,width=entry_width,state="readonly")
        combo.grid(column=1,row=0)
        self.select_button = select_button = ttk.Button(self,text=TABLE_SELECT_BUTTON_LABEL)
        select_button.grid(column=2,row=0)

    def setComboValues(self,values):
        self.table.set(values[0])
        print(self.table.get())
        self.combo['values'] = values

    def getTableName(self):
        return self.table.get()

    def setTableCommand(self,func):
        self.select_button['command'] = func

class TreeView(ttk.Frame):
    """
    DBのデータを実際に表示する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={}
        self.save_func = None

    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 = ROW_DATA_UPDATE_BUTTON_LABEL)
        self.insert_button = insert = ttk.Button(button_frame,text = ROW_DATA_INSERT_BUTTON_LABEL)
        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 = ROW_DATA_SAVE_BUTTON_LABEL)
        if self.save_func:
            self.save_button["command"] = self.save_func
        save.pack(side="left")

    def delete(self):
        """
        更新時用
        自身のフレームに紐づく子Widgetの削除
        """
        children = self.winfo_children()
        for child in children:
            child.destroy()

    def setUpdateButtonCommand(self,command):
        """
        updateボタンにコマンドを登録する
        """
        self.update_button["command"] = command

    def setInsertButtonCommand(self,command):
        """
        insertボタンにコマンドを登録する
        """
        self.insert_button["command"] = command

    def setSaveButtonCommand(self,command):
        """
        saveボタンに保存コマンドを登録する
        データが登録されるごとに登録しなおす必要があるので関数も別途保持する
        """
        self.save_button["command"] =self.save_func = 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 DBView(ttk.Frame):
    """
    DBViewerのメインView

    """
    def __init__(self, master):
        super().__init__(master,borderwidth=10)
        self.tree = None
        self.p_key_id =-1
        self.createWidget()
        self.setAction()
        self.pack()

    def createWidget(self):
        """
        viewの組み立て
        """
        self.createUpperFrame()
        self.createLowerFrame()

    def createUpperFrame(self):
        """
        DB読み込み用フレーム
        """
        upper_frame = ttk.Frame(self)
        upper_frame.pack()
        self.file_path_frame = FileOpenFrame(upper_frame)
        self.combo_box_frame = ComboboxFrame(upper_frame)
        self.combo_box_frame.setComboValues(["none"])

    def createLowerFrame(self):
        """
        Treeviewとレコード編集Widget用フレーム
        """
        lower_frame = ttk.Frame(self)
        lower_frame.pack()
        left_frame = ttk.LabelFrame(lower_frame,text=TREE_TITLE_LABEL)
        left_frame.pack(side="left")
        self.tree = TreeView(left_frame)
        right_frame = ttk.LabelFrame(lower_frame,text=ROW_DATA_TITLE_LABEL)
        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()
            item = self.tree.getDataMap()
            if self.p_key != "":
                if param[self.p_key] != item[self.p_key]:
                    _insertCommand()
                    return
            self.tree.update(param)
            messagebox.showinfo(UPDATE_MESSAGE_DIALOG_TITLE,UPDATE_MESSAGE_SUCCEED)

        def _insertCommand():
            """
            挿入アクション
            """
            param = self.property.getParameter()
            # 重複チェックを行う
            if self.checkPrimaryKey(param):
                self.tree.insert(param)
                messagebox.showinfo(INSERT_MESSAGE_DIALOG_TITLE,INSERT_MESSAGE_SUCCEED)
            else:
                messagebox.showerror(INSERT_MESSAGE_DIALOG_TITLE,INSERT_MESSAGE_FAILED)

        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()

    def setTableNameList(self,values):
        """
        DBに入っているテーブルのリストを設定する
        """
        self.combo_box_frame.setComboValues(values)

    def getSelectTable(self):
        """
        選択されているテーブル名を取得する
        """
        return self.combo_box_frame.getTableName()

    def setTableCommand(self,func):
        """
        テーブル名を決定したときのコマンドを登録する
        """

        self.combo_box_frame.setTableCommand(func)

    def setPrimaryKey(self,p_key):
        """
        主キーを登録する
        主キーと列名から対応する列番号も保持しておく
        """
        self.p_key = ""
        self.p_key_id= -1
        for id,column in enumerate(self.getColumns()):
            if column == p_key:
                self.p_key = p_key
                self.p_key_id= id

    def checkPrimaryKey(self,new_data):
        """
        主キーが登録済みでない(登録可能)か調べる
        主キーが設定されていなければ常に登録可能
        """
        if self.p_key == "" or self.p_key_id == -1:
            return True
        print(self.p_key,new_data)
        check_data = new_data[self.p_key]
        print(check_data)
        for row in self.getRows():
            if check_data == row[self.p_key_id]:
                return False
        return True



if __name__ == '__main__':
    master = Tk()
    master.title("DBview")
    DBView(master)
    master.geometry("800x400")
    master.mainloop()

Control&Logic

・dbcontrol.py

import tkinter.messagebox as messagebox
from tkinter import *
import tkinter.ttk as ttk
from dbview import *
import sqlite3
import os

SQL_EROROR_MESSAGE = "sqlite3 Error:"
class DBLogic:
    """
    DBViewer読み込み、書き込みロジック
    """

    def __init__(self):
        """
        列とレコード用の配列を初期化
        """
        self.header =[]
        self.data =[]
        self.db_path=""
        self.table=""

    def readDataBase(self,db_path):
        """
        dbを読み込んでテーブルのリストを返す
        """
        table_list = []
        self.db_path = db_path
        if os.path.exists(db_path) == False:
            print("file not found")
            return table_list

        try :
            connection = sqlite3.connect(db_path)
            cursor = connection.cursor()
            cursor.execute("select name from sqlite_master where type='table'")
            table_list=[data[0] for data in cursor.fetchall()]
            connection.close()
        except sqlite3.Error as e:
            print(SQL_EROROR_MESSAGE, e)

        return table_list

    def readColumn(self,table_name):
        """
        選択されたテーブルの列名と主キーを取得する
        cursor.descriptionから列名をとることができるが、
        主キー情報を取得するためSQLiteのPRAGMA TABLE_INFOコマンドを発行し取得する
        """
        self.table_name = table_name
        column=[]
        p_key=""
        try :
            connection = sqlite3.connect(self.db_path)
            cursor = connection.cursor()
            # cursor.execute("select * from " + table_name)
            # for data in cursor.description:
            #     print(data)
            # column=[data[0] for data in cursor.description]
            sql = "PRAGMA TABLE_INFO("+table_name+")"
            cursor.execute(sql)
            for data in cursor.fetchall():
                column.append(data[1])
                if data[5] == 1:
                    p_key = data[1]

            connection.close()
        except sqlite3.Error as e:
            print(SQL_EROROR_MESSAGE, e)
        return column,p_key

    def readData(self,table_name):
        """
        選択されたテーブル名でSELECT文検索しデータを抽出する
        """
        raws=[]
        try :
            connection = sqlite3.connect(self.db_path)
            cursor = connection.cursor()
            cursor.execute("select * from " + table_name)
            raws=[data for data in cursor.fetchall()]
            connection.close()
        except sqlite3.Error as e:
            print(SQL_EROROR_MESSAGE, e)
        return raws

    def updateRowData(self,coloumns,rows):
        """
        DBのデータを更新する
        """
        ret = True
        p =""
        r_pre = [column for column in coloumns]
        q_pre = ["?" for column in coloumns]

        r = ",".join(r_pre)
        q = ",".join(q_pre)
        replace = "replace into "+self.table_name+"("+r+")" + "values("+q+")"
        print(replace)
        try :
            connection = sqlite3.connect(self.db_path)
            cursor = connection.cursor()
            for row in rows:
                cursor.execute(replace,row)
            connection.commit()
            connection.close()
        except sqlite3.Error as e:
            print(SQL_EROROR_MESSAGE, e)
            ret = False
        return ret

class DBControl:
    """
    DBViewerのコントローラー
    """
    def __init__(self):
        """
        アプリの立ち上げとイベント登録
        """
        master = Tk()
        master.title(TITLE_NAME_LABEL)
        master.geometry("1000x400")
        self.view = DBView(master)
        self.logic = DBLogic()
        self.view.setReadButtonCommand(self.readButtonCommand)
        self.view.setTableCommand(self.readTableCommand)
        self.view.setSaveButtonCommand(self.saveButtonCommand)
        master.mainloop()

    def readButtonCommand(self):
        """
        DB読み込みボタン用コマンド
        DBから取得した列名、データをViewに反映する。
        """
        file_path = self.view.getFilePath()
        table_list = self.logic.readDataBase(file_path)
        print(table_list)
        self.view.setTableNameList(table_list)

    def saveButtonCommand(self):
        """
        保存ボタン用コマンド
        指定されたパスにviewで指定された情報をDBに書きだす
        """
        columns = self.view.getColumns()
        rows =self.view.getRows()
        ret = self.logic.updateRowData(columns,rows)
        if ret:
            messagebox.showinfo(SAVE_MESSAGE_DIALOG_TITLE,SAVE_MESSAGE_SUCCEED)
        else:
            messagebox.showerror(SAVE_MESSAGE_DIALOG_TITLE,SAVE_MESSAGE_FAILED)

    def readTableCommand(self):
        """
        選択されたテーブル名から
        列名、主キー、データを取得し画面に反映させる
        """
        table_name = self.view.getSelectTable()
        columns,p_key = self.logic.readColumn(table_name)
        datas = self.logic.readData(table_name)
        self.view.setNewColumnAndData(columns,datas)
        self.view.setPrimaryKey(p_key)


if __name__ == '__main__':
    control =  DBControl()

データ作成用スクリプト

import sqlite3
import os

def makeTable(cursor):
    cursor.execute("CREATE TABLE IF NOT EXISTS FRUIT (id integer primary key, name text,num integer,value integer)")
    cursor.execute("CREATE TABLE IF NOT EXISTS RSTC_MEAN (Initial text primary key, Mean text,Mean_J text)")
def insertData(cursor):
    cursor.execute("insert into FRUIT values(1,'orange',3,120)")
    cursor.execute("insert into FRUIT values(2,'Apple',2,180)")
    cursor.execute("insert into FRUIT values(3,'banana',2,100)")

    cursor.execute("insert into RSTC_MEAN values('R','Ritsuan','リツアン')")
    cursor.execute("insert into RSTC_MEAN values('S','Suprise','驚き')")
    cursor.execute("insert into RSTC_MEAN values('T','TeamWork','チームワーク')")
    cursor.execute("insert into RSTC_MEAN values('C','Company','会社')")

def readData(cursor):
    cursor.execute("select * from FRUIT")
    data = cursor.fetchone()
    print(data)
    print(data.keys())

def readTableNames(cursor):
    cursor.execute("select name from sqlite_master where type='table'")
    for data in cursor.fetchall():
        print(data)

def readTableInfo(cursor,table = "FRUIT"):
    cursor.execute("PRAGMA TABLE_INFO(FRUIT)")
    cols = cursor.fetchall()
    print([item[1] for item in cols])

if __name__ == '__main__':
    db_name = "sample.db"
    workdir = os.path.dirname(__file__)
    db_path = os.path.join(workdir,db_name)
    print(db_path)
    connection = sqlite3.connect(db_path)
    # connection.row_factory = sqlite3.Row
    cursor = connection.cursor()
    try:
        makeTable(cursor)
        insertData(cursor)
        # readData(cursor)
        # readTableNames(cursor)
        # readTableInfo(cursor)
    except sqlite3.Error as e:
        print('sqlite3 Error occurred:', e.args[0])
    connection.commit()
    connection.close()

 

あわせて読みたい