Build simple Books Manager application with Python and Tkinter from scratch


Get free PDF now!



INTRODUCTION 

Hello. This tutorial will show you in details how to build Books Manager program using python programming language. 

Why this tutorial? You can easily find online basic programming tutorials. Stuff like what are variables and loops, basic language commands, how to write "hello world" program or how to build simple calculator.

However, when you finish those beginners lessons you often find yourself repeating same tutorials all over again in a not so productive cycle.

This tutorial will help you break that cycle and build something useful, a project to move ahead to more intermediate programming. 

You will learn how to setup python on your computer (you can skip this part if you already have python installed), how to build user interface using tkinter, builtin python library and finally how to use sqlite database to save and retrieve data in your program.

At the end I will give you some ideas how you can expand software you have built.



 
CONTENTS

Setup
Application structure
GUI Layout
Adding books dialog
Edit book dialog
Working with databases
Misc functions
Final words


SETUP 

For this tutorial you will need python installed on your computer. Installation is easy and quick, it takes only few minutes to complete. 

I'll show you how to install python on Windows but installation is also straightforward on other systems.

In your browser open python download page and click to download latest python version. I'm using in this tutorial python version 3.7.4 but any newer version should work fine.




Run the file you just downloaded. Python install wizard will open, which is really easy to use. You only have to accept the default settings, wait until the install is finished, and you are done with setup.

If you have any problems during install, leave comment and I'll try to help you. 

You can use python builtin IDLE editor or any other IDE for python you want. You can actually use any text editor, create new text file, rename it to app.py and start typing.

Now let's write some software.


APPLICATION STRUCTURE

Application consists of imports, main class Library and few lines to start the app. Here is application code structure:

from tkinter import *
from tkinter import ttk
import sqlite3
import os.path
import csv
class Library:
    def __init__(self, master):

    # UI setup 

    def add_book_dialog(self):
        '''
        Add new book window
        '''
    def edit_book_dialog(self):
        '''
Edit book window
'''
    def add_book(self):
        '''
        Function adds book to database
        '''
    def enter_changes(self):
        '''
        Enter changes to database function (when editing book)
        '''
    def delete_book(self, name):
        '''
        Deletes chosen book from database and list
        '''
    def update_list(self):
        '''
        Updates book list from database into treeview 
        '''
    def config(self):
        '''
        Windows management function
        '''
root = Tk()
root.title("Library")
application = Library(root)
root.mainloop()
Since this is very simple application, frontend and backend functions are all in the same file. It is a good practice to separate it however.

Last 4 lines of code are defining root variable as TK, giving it a title and starting the app.

Lat's populate our functions and explain it one by one.


GUI LAYOUT 

Just below UI setup comment add code for UI. Make sure your indentation is correct.

leftFrame = Frame(width=150, height=600)
leftFrame.grid(row=0, column=0, padx=10, pady=5, sticky=N)
       
self.addbtn = ttk.Button(leftFrame, text="New book", width=15, command=self.add_book_dialog)
self.addbtn.grid(row=0, column=0, sticky=W+N)

self.modbtn = ttk.Button(leftFrame, text="Edit book", width=15, command=self.edit_book_dialog)
self.modbtn.grid(row=1, column=0, sticky=W+N)

rightFrame = Frame(width=150, height=600)
rightFrame.grid(row=0, column=1, padx=0, pady=5)                

self.tree = ttk.Treeview(rightFrame, show="headings", height=20, column=4, selectmode="browse")
self.tree.grid(row=0, column=1, rowspan=20, sticky=N)
self.vsb = ttk.Scrollbar(rightFrame, orient="vertical", command=self.tree.yview)
self.vsb.grid(row=0, column=2, sticky=N+S+E+W, rowspan=20)
self.tree.configure(yscrollcommand=self.vsb.set)
self.tree["columns"]=("one","two","tree","four")
self.tree.column("one", width=140)
self.tree.column("two", width=240)
self.tree.column("tree", width=100)
self.tree.column("four", width=100)
self.tree.heading("one", text='Author', anchor=N)
self.tree.heading("two", text='Title', anchor=N)
self.tree.heading("tree", text='Pages', anchor=N)
self.tree.heading("four", text='Date', anchor=N)

self.msg=Label(text='*', fg='red')
self.msg.grid(row=21, column=1)

self.context_open = False
self.update_list()

OK. We have two frame inside root, leftFrame and rightFrame. Left will hold buttons and right one will hold Treeview widget for books data.

Both leftFrame and rightFrame have some basic parameters like width, height, padding and layout position. Here we use grid layout. You can read more about layouts in Tkinter docs. Besides grid there is also pack and place layouts.

Left frame contains two buttons, one for adding new book addbtn and one for editing existing book data modbtn. You place them inside leftFrame with adding leftFrame as first parameter, you add button text, width and command, which is actually name of our function below (see application structure).

Sticky parameter inside grid layout positions buttons as we want them. Value is W-west and N-north. There are also E-east and S-south sides as you probably assumed.

Right frame has also a scrollbarr which you need to attach to treeview widget with tree.configure.

After you add treeview columns with desired text, you also have Label widget at the bottom of the window to display info. Notice how that message (msg) widget grid position row is 21, because we earlier set rowspan=20 for our treeview.

That's it for the interface, at the bottom we call update_list function to populate our tree with existing data from database and we also set context_open to False which we use to enable and disable editing of root elements when other windows are open.


ADDING BOOKS DIALOG


Let's look at the code for Add book window. We have 4 parts at the top and every one of them has label: Author, Title, Pages and Date and Entry widget. At the bottom is Button widget with attached command to execute sql query. More on that later.

def add_book_dialog(self):
    '''
    Add new book dialog 
    '''
    try:
        self.msg["text"] = ""
        self.tl = Tk()
        self.tl.title("Add book")
        self.tl.resizable(False, False)
        
        # window position
        x=root.winfo_rootx()+150
        y=root.winfo_rooty()+50
        self.tl.geometry('+%d+%d' % (x,y))

        Label(self.tl,text='Author:').grid(row=0, column=0, sticky=W)
        ne1var = StringVar()
        ne1 = Entry(self.tl, textvariable=ne1var)
        ne1.grid(row=0, column=1, sticky=W)
        ne1.insert(0,"")

        Label(self.tl,text='Title:').grid(row=1, column=0, sticky=W)
        ne2var = StringVar()
        ne2 = Entry(self.tl, textvariable=ne2var)
        ne2.grid(row=1, column=1, sticky=W)
        ne2.insert(0,"")

        Label(self.tl,text='Pages:').grid(row=2, column=0, sticky=W)
        ne3var = StringVar()
        ne3 = Entry(self.tl, textvariable=ne3var)            
        ne3.grid(row=2, column=1, sticky=W)
        ne3.insert(0,"")

        Label(self.tl,text='Date:').grid(row=3, column=0, sticky=W)
        ne4var = StringVar()
        ne4 = Entry(self.tl, textvariable=ne4var)
        ne4.grid(row=3, column=1, sticky=W)
        ne4.insert(0,"")            

        # Button calls function for executing sql command      
        upbtn = Button(self.tl, bg="grey", fg="white", text= 'Add new book', command=lambda:self.add_book(ne1,ne2,ne3,ne4))
        upbtn.grid(row=5, column=0, sticky=W, pady=10, padx=10)

        self.config()
        self.tl.protocol("WM_DELETE_WINDOW", self.config)
                                
        self.tl.mainloop()
    except IndexError as e:
        self.msg["text"] = "Error while adding a book"
As you see from the code above, entire function is inside try except so we make sure everything is ok. 

We set window to be resizable. With root.winfo_rootx and y we set position of our new window in relation to root window. 

All 4 Label Entry widget pairs are the same except for the names of the variables. As you can see we have to use here tkinter's StringVar type and not regular python type. See docs for more info.

Button has a little style, we set background and foreground colors with bg and fg attributes.

When user clicks on the button add_book function is executed with 4 parameters, values from our previously entered widgets. Here, we don't have code to check validation, which is something to think about for some next version.

When we call config function at the bottom, this disables other buttons and only add book window is operational. 


EDIT BOOK DIALOG

 
Edit book window is very similar to add book window. Labels and Entry widgets are also there.

This window has two buttons. Enter details button actually enters data into database and Delete book button is doing two things. First is to delete the book from the database and second one is to update data in Treeview widget. 

Here is the code:

def edit_book_dialog(self):
    try:
        self.msg["text"] = " "
        conn = sqlite3.connect('data.db')
        c = conn.cursor()
        name = self.tree.item(self.tree.selection()[0])['values'][1]
        
        query = "SELECT * FROM t WHERE Title = '%s';" %name
        db_data = c.execute(query)

        for item in db_data:
            _author = item[0]
            _title = item[1]
            _pages = item[2]
            _date = item[3]
            
        self.tl = Tk()
        self.tl.title("Edit details")
        x = root.winfo_rootx()+120
        y = root.winfo_rooty()+50
        self.tl.geometry('%dx%d+%d+%d' % (380, 155, x, y))
        self.tl.resizable(False, False)

        Label(self.tl,text='Author: ').grid(row=0, column=0, sticky=E+W)
        new_author = Entry(self.tl, width=30)
        new_author.grid(row=0, column=1, sticky=W, padx=10)
        new_author.insert(0,_author)

        Label(self.tl, text='Title: ').grid(row=1, column=0,sticky=E+W)
        new_title = Entry(self.tl, width=30)
        new_title.grid(row=1, column=1, sticky=W, padx=10)
        new_title.insert(0,_title)

        Label(self.tl, text='Pages:').grid(row=2, column=0,sticky=E+W)
        new_pages = Entry(self.tl, width=30)
        new_pages.grid(row=2, column=1, sticky=W, padx=10)
        new_pages.insert(0,_pages)

        Label(self.tl, text='Date:').grid(row=3, column=0,sticky=E+W)
        new_date = Entry(self.tl, width=30)
        new_date.grid(row=3, column=1, sticky=W, padx=10)
        new_date.insert(0,_date)
                                    
        upbtn = Button(self.tl, bg="grey", fg="white", text='Enter details',
                       command=lambda:self.enter_changes(new_author,new_title,new_pages,new_date,name))
        upbtn.grid(row=4, column=0, sticky=W, padx=10, pady=10)

        dbtn = Button(self.tl, bg="grey", fg="white", text="Delete book", command=lambda:self.delete_book(name))
        dbtn.grid(row=4, column=1, sticky=W, padx=10, pady=10)

        conn.commit()
        c.close()
        
        self.config()
        self.tl.protocol("WM_DELETE_WINDOW", self.config)

        self.tl.mainloop()
        
    except IndexError as e:
        self.msg["text"] = "Select book to edit"


First part of the function is database stuff. We will discuss databases later. Adding Labels, Entry widgets and Buttons is exactly the same as in previous function. Only difference are functions which are called when buttons are clicked, enter_changes and delete_book functions. 

One more thing. When user clicks edit button one item from Treeview must be selected, otherwise message is displayed: "select book to edit" and window is not opened. That's why function body is inside try except block.

WORKING WITH DATABASE

When you start the application for the first time there is no database create, so we have to add code to create if it does not exist. We do this at the beginning of Library class, just below init method and above our UI setup code. Place this code there and watch for correct indentation:

if not os.path.exists('data.db'):
    print('create database ...')
    con = sqlite3.connect("data.db")
    cur = con.cursor()
    cur.execute("CREATE TABLE t (Author,Title,Pages,Date);")
    con.commit()
    con.close()
    if os.path.exists('data.db'):
        print('done.')
    else:
        print('error creating database.')
First we check if data.db already exists. If not we create one. We use sqlite3 connect function, we must then set the cursor to execute sql query. 

Our table t has 4 fields: Author, Title, Pages and Date. In the end, commit changes and close the connection. This is how you create new database to store values. Simple.

Remember edit_book_dialog function? We connect to our database there also, but first we take selected treeview item, actually its title value and compare it with appropriate row in database to return entire row. We use this line:

name = self.tree.item(self.tree.selection()[0])['values'][1]
Value [1] is second value in list (Title), since zero is first (Author). We have set in our code for user to be able to select only one item per selection, so selection value will always be [0] in selection()[0] part.

We have 3 more function to work with our database:

def add_book(self,a,b,c,d):
    '''
    Function adds book to database
    '''
    a1 = a.get()
    b1 = b.get()
    c1 = c.get()
    d1 = d.get()
    conn = sqlite3.connect('data.db')
    c = conn.cursor()
    c.execute("INSERT INTO t(Author,Title,Pages,Date) VALUES (?,?,?,?)", (a1,b1,c1,d1))
    conn.commit()                  
    c.close()
    self.msg["text"] = "Book added."
    
    self.config()
    self.update_list()
    
def enter_changes(self,new_author,new_title,new_pages,new_date,name):
    '''
    Enter changes to database function
    '''
    inAuthor = new_author.get()
    inTitle = new_title.get()
    inPages = new_pages.get()
    inDate = new_date.get()
    inName = name
    conn = sqlite3.connect('data.db')
    c = conn.cursor()
    c.execute('UPDATE t SET Author=(?), Title=(?), Pages=(?), Date=(?) WHERE Title=(?) AND Author=(?)',
              (inAuthor,inTitle,inPages,inDate,inName,inAuthor))
    conn.commit()
    c.close()
    self.msg['text'] = "Data for '%s' is changed" %name
    self.config()
    self.update_list()

def delete_book(self, name):
    '''
    Deletes chosen book from database and list
    '''
    dName = name
    conn = sqlite3.connect('data.db')
    c = conn.cursor()
    sql_query = """DELETE from t WHERE Title=(?)"""
    c.execute(sql_query, (dName,))
    conn.commit()
    c.close()
    self.msg["text"] = "Book is deleted"
    self.config()
    self.update_list()

Add book function adds new book entry to database and enter changes data come from edit book dialog, so enter changes function will change data for already existing book.

Delete book, off course deletes the book. Note that this function takes only Title as value to find database item, so if there are 2 or more books in the database with the same Title (possible situation) all will be deleted. Solution will be to check to values, for example Title and Author's name. You can do this as exercise.

MISC FUNCTIONS

We have only left with two last helper functions and application is finished. Let's see them.

def update_list(self):
    '''
    Updates book list from database into treeview 
    '''
    
    # delete current items
    x = self.tree.get_children()
    for item in x:
        self.tree.delete(item)
    # read new data
    conn = sqlite3.connect('data.db')
    c = conn.cursor()
    lst = c.execute("SELECT * FROM t ORDER BY Date(Date) desc")
    for row in lst:
        self.tree.insert("", END, text="", values=(row[0], row[1], row[2], row[3]))
    conn.commit()    
    c.close()

def config(self):
    '''
    Windows management function
    '''
    if self.context_open:
        self.addbtn.config(state=NORMAL)
        self.modbtn.config(state=NORMAL)
        self.tree.config(selectmode="browse")
        self.tl.destroy()
        # restore root close button function
        root.protocol('WM_DELETE_WINDOW', root.destroy)
    else:
        # ignore root close button
        root.protocol('WM_DELETE_WINDOW', lambda:0)
        self.addbtn.config(state=DISABLED)
        self.modbtn.config(state=DISABLED)
        self.tree.config(selectmode="none")
    self.context_open = not self.context_open        
Update list function first deletes entire Treeview and then connects with database, reads the data and populates Treeview again. This is to make sure that when we change or delete a book, list is updated.

Finally config function, as I said before, disables buttons and treeview selection when other dialogs are open. 

FINAL WORDS

Please notice this is one way of doing things, but it is not the only way. You task is to find your own style and way of thinking how to solve problems with programming. Programming is much more about thinking and less about typing.

Here are some things you can do to add more features to this app.

Make statistics button and display top authors by number of books.

Make statistics button and display number of pages you read per year.

Use Mathplot lib to display those data as graphs.

Make function to export data as text file or to email it to someone.

etc.

You can star and fork entire code on Github

If you have any question about the code post comment or contact via Facebook or Twitter.

Thank you for reading.

o_o

Post a Comment

0 Comments