6
\$\begingroup\$

This is my first relatively big program. First it shows an askdirectory() screen to get the path. It gets all subfolders of that path and appends them to a list. It checks every folders' items if they are wanted file types by checking extensions and puts them into a proper sqlite table. After all files written to a database file, a GUI appears to get what user wants to search. After clicking search_button it gets my_entry and checks if it's in database. If yes, appends it to a list and after all database is searched, it shows results in a listbox.

There are two lists because I couldn't find the way to get path while only writing file names to a list. So one of the list is shown and only have file names, the other one is hidden and contains whole path for that file.

My code is working relatively slow while writing to a database or reading from it but my real problem is, this program should search 3TB of data which contain lots of folders, files etc.. So when I try to show hundreds (sometimes it is thousands) of items in listBox, GUI freezes. I cannot scroll up/down or open folders by double clicking on their names.

  1. How can I handle that problem? Is there a way to do it with Tkinter or should I use something else?

  2. Any tips on style?

  3. Lastly, about writing to/reading from database performance, should I use threading?

#-*-coding: utf8-*-

import os
import sys
import time
import sqlite3
import tkinter
from tkinter import ttk
from tkinter import filedialog
from tkinter import messagebox
from win32com.shell import shell, shellcon

class Prompt(tkinter.Frame):
    def search_button(self): 
        reading.reader()   ##to get data from database tables
        self.my_entry = self.ent.get()
        if self.my_entry:
            for line in reading.lines:
                if self.my_entry.lower() in line.split("/")[-1].lower(): ##if entry from textbox in file/path name
                    self.results.append(line)
        self.shown_list()
        self.hidden_list()
        self.results.clear() #cleared because of next searches

    def search_button2(self):    
        """i'm destroying first button and adding 2nd one because of UI thing. 
           Without this UI looks worse than it is now"""
        self.lst.destroy()      
        self.btn2.pack_forget()
        self.search_button()

    def on_frame_configure(self, event):
        self.cvs.configure(scrollregion=self.cvs.bbox("all"))

    def __init__(self, den): 
        self.fp = filepath
        self.results = []
        self.lbl1 = tkinter.Label(den, text="Select the File Type")
        self.lbl2 = tkinter.Label(den, text="Enter the Keyord")
        self.ent = tkinter.Entry(den)
        self.btn = tkinter.Button(den, text="Search", command=self.search_button)
        self.cmb = tkinter.ttk.Combobox(den)
        self.cmb["values"] = ["Music","Image","Document","Other","All"]
        self.cmb.current("0")
        self.lbl1.pack()
        self.cmb.pack()
        self.lbl2.pack()
        self.ent.pack()
        self.btn.pack()
##needed to create a canvas to use scrollbar on my Listbox
        tkinter.Frame.__init__(self,den)
        self.cvs = tkinter.Canvas(den, borderwidth=0, background="#ffffff")
        self.frame = tkinter.Frame(self.cvs, background="#ffffff")
        self.vsb = tkinter.Scrollbar(den, orient="vertical", command=self.cvs.yview)
        self.cvs.configure(yscrollcommand=self.vsb.set)
        self.vsb.pack(side="right", fill="y")
        self.cvs.pack(fill="both", expand=True)
        self.cvs.create_window((4,4), window=self.frame, anchor="nw", 
                                  tags="self.frame")
        self.frame.bind("<Configure>", self.on_frame_configure)

    def shown_list(self):  
        results = self.results
        self.lst = tkinter.Listbox(self.frame, selectmode="SINGLE", height = len(results), width = "100")
        if self.my_entry and not results:
            tkinter.messagebox.showwarning(title = "Can not find",\
                                           message = "There is no result matching with the keyword")
        if not self.my_entry:
            tkinter.messagebox.showwarning(title = "No keyword to Search" \
                                           ,message = "Enter a Keyword")
        for index, item in enumerate(results, start=1):
            self.lst.insert(index, item.split("\\")[-1][:-4])

        if results:
            self.lst.bind("<Double-Button-1>", self.open_folder)
        self.btn.destroy()
        self.btn2 = tkinter.Button(den, text="New Search", command= self.search_button2)
        self.btn2.pack()
        self.lst.pack(fill="both", expand=True)

    def hidden_list(self):  
        results = self.results
        self.lst2 = tkinter.Listbox(self.frame)
        for index, item in enumerate(results, start=1):
            self.lst2.insert(index, item)
        self.lst2.pack()
        self.lst2.pack_forget()
        self.results = results

    def open_folder(self,event): #this opens folder with selected item using shell commands
        selected_name = []
        self.selection = self.lst2.get(self.lst.curselection()[0])
        selected_path = self.selection.split(self.selection.split("\\")[-1])[0][:-1]
        selected_name.append(self.selection.split("\\")[-1])
        try:
            launch_file_explorer(selected_path,selected_name)
        except KeyError:
            tkinter.messagebox.showwarning(title = "Can not Find the File " \
                                           ,message = "Make Sure File Isn't Modified")

def launch_file_explorer(path, files):  
    folder_pidl = shell.SHILCreateFromPath(path,0)[0]
    desktop = shell.SHGetDesktopFolder()
    shell_folder = desktop.BindToObject(folder_pidl, None,shell.IID_IShellFolder)
    name_to_item_mapping = dict([(desktop.GetDisplayNameOf(item, 0), item) for item in shell_folder])
    to_show = []
    for file in files:
        to_show.append(name_to_item_mapping[file])
    shell.SHOpenFolderAndSelectItems(folder_pidl, to_show, 0)

class Reading:
    def reader(self):
        """ reads items from tables depending on combobox selection"""
        if prompt.cmb.get() == "Music": 
            self.reader_music()

        elif prompt.cmb.get() == "Image":
            self.reader_image()

        elif prompt.cmb.get() == "Document":
            self.reader_document()

        elif prompt.cmb.get() == "Other":
            self.reader_others()

        elif prompt.cmb.get() == "All":
            self.reader_music()
            mlines = self.lines

            self.reader_image()
            ilines = self.lines

            self.reader_document()
            dlines = self.lines

            self.reader_others()
            olines = self.lines

            self.lines = mlines + ilines + dlines + olines

    def reader_music(self): 
        self.lines = []
        müzikler = data.execute("""select * from müzik""")
        for row in müzikler:
            self.lines.append(os.path.join(row[0],row[1]))
            """if only row[1] it searches only file names but since my target has lots of 
images named like DSC123123 I needed to search in both file name and path."""

    def reader_image(self): 
        self.lines = []
        resimler = data.execute("""select * from resim""")
        for row in resimler:
            self.lines.append(os.path.join(row[0],row[1]))

    def reader_document(self): 
        self.lines = []
        dökümanlar = data.execute("""select * from döküman""")
        for row in dökümanlar:
            self.lines.append(os.path.join(row[0],row[1]))

    def reader_others(self): 
        self.lines = []
        diğerleri = data.execute("""select * from diğer""")
        for row in diğerleri:
            if not os.path.isdir(os.path.join(row[0],row[1])): #to not show folders
                self.lines.append(os.path.join(row[0],row[1]))

class Searcher:
    def writer(self):  ##creates tables and calls the function to insert items
        data.execute(""" create table if not exists müzik(path text,
                filename text UNIQUE) """)
        data.execute(""" create table if not exists resim(path text,
                filename text UNIQUE) """)
        data.execute(""" create table if not exists döküman(path text,
                filename text UNIQUE) """)
        data.execute(""" create table if not exists diğer(path text,
                filename text UNIQUE) """)

        for roots ,dirs ,files in os.walk(searchFolder):
            for item in os.listdir(roots):
                table_writing(item,roots)

        conn.commit()

def table_writing(item,path): 
    """
    depending on their extensions, inserts items to corresponding tables """
    if "."+item.split(".")[-1].lower() in audio_ext:
        data.execute(""" INSERT OR IGNORE into müzik
            (path, filename) VALUES (?,?)""",(path,item))

    elif "."+item.split(".")[-1].lower() in image_ext:
            data.execute(""" INSERT OR IGNORE into resim
            (path, filename) VALUES (?,?)""",(path,item))

    elif "."+item.split(".")[-1].lower() in document_ext:
        data.execute(""" INSERT OR IGNORE into döküman
            (path, filename) VALUES (?,?)""",(path,item))
    else:
        data.execute(""" INSERT OR IGNORE into diğer
            (path, filename) VALUEs (?,?)""",(path,item))

class FolderSelect:  
    """ I need to monitor if there are any changes in files. So I needed to store chosen path in database """
    def __init__(self):
            data.execute("""CREATE TABLE if not EXISTS SearchPath(num INT, path TEXT) """)
            data.execute(""" select * from SearchPath""")    
            b = data.fetchone()
            if b is None:
                self.searchPath = tkinter.filedialog.askdirectory(parent=den,initialdir="/",title='Please select a directory')
                data.execute("""INSERT INTO SearchPath(num,path) VALUES(?,?)""",(1,self.searchPath))
            data.execute(""" select * from SearchPath""")
            self.searchPath = data.fetchone()[1]
            self.searchPath = self.searchPath.replace("/","\\")            


if __name__ == "__main__":
    audio_ext    = [".mp3",".flac",".wma",".wav",".m4a"] 
    image_ext    = [".jpg",".png",".bmp"]
    document_ext = [".doc",".txt",".pdf",".docx"] ##these ext. types will be increased
    filepath = os.getcwd() 
    directories = []
    wasItThere = True   ##checks if there was a database file beforehand.
    if not os.path.isfile("test.db"): 
        wasItThere = False
    conn = sqlite3.connect("test.db")
    data  = conn.cursor()
    den = tkinter.Tk()
    folder = FolderSelect()
    searchFolder = folder.searchPath
    reading = Reading()
    searcher = Searcher()
    if not wasItThere:   ##if there is, program doesn't bother to check all folders,items again and again
        searcher.writer()
    den.title("Arama")
    prompt = Prompt(den)
    den.mainloop()

    conn.close()
\$\endgroup\$
  • \$\begingroup\$ Have you considered using easygui? This is all good and well for a Tkinter practice, but at the end of the day easygui is a pretty well written, if limited, module. I could probably write this program in 1/3 of the lines with it, using perhaps two other modules. Just a thought. \$\endgroup\$ – KnightOfNi Feb 12 '14 at 23:13
  • \$\begingroup\$ Never heard of EasyGui before. I will try to learn and implement it if my pageing thing won't work. Now I am -kinda- using Sam's idea and will split results in parts and try to display them in more than one list. \$\endgroup\$ – Lafexlos Feb 15 '14 at 3:07
5
\$\begingroup\$

In looking at your code, there are a few things I would do:

  1. Subclass TK.. class MainGUI(Tk):
  2. Attach all gui components to this class.... your frame for example.
  3. Add a queue and a polling method for GUI updates to the MainGUI class (this may help fix your freezing issue). You can use the TK.after method to ask the mainloop to do this periodically.
  4. Do the heavy lifting items such as db reads, filesystem, etc in a separate thread if they block too long, but make sure they don't update the GUI directly from that thread, you can pass a reference to the GUI to them and they should place a callback on the updateq for the main loop to execute as it runs a polling cycle.
  5. Check the code for pep8 compliance.

Here is an example of how I have done GUI's using TKinter:

class MainGUI(Tk):
    """ The GUI """

    def __init__(self):
        super().__init__()
        self.updateq = queue.Queue()

        # Build main components

        # Layout components

        # Key bindings

        self.poll()
        self.mainloop()

    def poll(self):
        """ Polls the Queue for the arrival of various callbacks """
        # You may want to process more or less callbacks each cycle...
        # here it uses 100 as a maximum
        for i in range(100):
            if self.updateq.empty():
                break

            callback, args = self.updateq.get()
            if args:
                callback(args)
            else:
                callback()

        # Recursive call
        self.after(50, self.poll)
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.