6
\$\begingroup\$

This script opens a basic form which allows one to search all files in a directory for instances of a string, before outputting results to a textbox and CSV (found in the script location). Each line outputted is of the format:

Word {string} in {file} on line {line number}: {full line}

Steps:

  1. Specify Directory
  2. Input string you would like to search for
  3. Toggle ignore case on or off
  4. Click Go.

Notes:

This is not completely finished yet. Script will only search basic TXT files. I'm also a beginner with Tkinter/GUIs, so haven't moved buttons around, etc. I've just added the buttons & labels one after the other. This will eventually be rectified. There's also an issue where if I'm searching a large directory of files, the form will look like it's frozen until it's finished searching. Finally, I may have went overboard on some of the error handling. I'm newish to this as well.

I'm trying to be a better programmer, particularly with structuring and readability, so any constructive criticism would be much appreciated.

enter image description here

from tkinter import filedialog

from tkinter.scrolledtext import ScrolledText

import pandas as pd

import tkinter as tk

import os

import re

import sys

################ FUNCTIONS ################

def save_to_file(wordlist):
    """Save list to CSV format and save CSV to script directory"""
    script_directory = os.path.dirname(sys.argv[0]) # Path where script is being run from
    df = pd.DataFrame(data={"Results": wordlist})
    df.to_csv(script_directory+"/mycsv.csv", sep=",", index=False, line_terminator='\n')
    
def print_to_textbox(wordlist):
    """Print all lines in wordlist to textbox"""
    for lines in wordlist:
        text_box.insert("end", "\n"+lines)
    if len(wordlist) == 0:
        text_box.insert("1.0", "\nNothing To Display")

def browse_button():
    """Button will open a window for directory selection"""
    global folder_path
    selected_directory = filedialog.askdirectory()
    folder_path.set(selected_directory)
    
def search_files():
    """Search all files in specified directory"""
    folderPath = folder_path.get()
    searchString = string_entry.get()
    text_box.delete("1.0", tk.END)
    
    # Set word case option on/off.
    if var1.get() == 1:
        IGNOREWORDCASE = True
    else:
        IGNOREWORDCASE = False
        
    # List to store all lines where string is found.
    wordlist = []  
    
    # Loop through all files and search for string, line by line.
    for (path, directories, files) in os.walk(folderPath, topdown=True):
        for file in files:
            filepath = os.path.join(path, file)
            
            try: 
                
                with open(filepath, 'r') as currentfile:          
                        for lineNum, line in enumerate(currentfile, 1):  
                            line = line.strip()
                            match = re.search(searchString, line, re.IGNORECASE) if IGNOREWORDCASE else re.search(searchString, line)
                            if match:
                                word = f"Word '{searchString}' in '{file}' on line {lineNum}: {line}"
                                wordlist.append(word)
                                
            except IOError as ex:
                words = f"Error; {file}; {ex}"
                wordlist.insert(0, words)
                
            except EnvironmentError as ex:
                words = f"Error; {file}; {ex}"
                wordlist.insert(0, words)
                
            except OSError as ex:
                words = f"Error; {file}; {ex}"
                wordlist.insert(0, words)
                
            except UnicodeDecodeError as ex:
                words = f"Error; {file}; {ex}"
                wordlist.insert(0, words)
                
            except:
                words = f"Error; {file}"
                wordlist.insert(0, words)

    # Print all lines to text box.
    print_to_textbox(wordlist)
    
    # Save to file.
    save_to_file(wordlist)
                
################ TKINTER SCRIPT ################

# Setup Window.
window = tk.Tk()
window.geometry("900x500")
window.title("String Search")

# Button to select directory.
select_directory = tk.Button(window, text = "Select Directory", command=browse_button)
select_directory.pack()

# Label to store chosen directory.
folder_path = tk.StringVar()
directory_label = tk.Label(window, textvariable = folder_path, bg="#D3D3D3", width=70)
directory_label.pack()

# Entry to type search string.
string_entry = tk.Entry(window, bg="#D3D3D3")
string_entry.pack()

# Check button to turn ignore case on/off.
var1 = tk.IntVar()
stringCase_select = tk.Checkbutton(window, text='Ignore Case',variable=var1, onvalue=1, offvalue=0)
stringCase_select.pack()

# Button to run main script.
go_button = tk.Button(window, text="Go", command=search_files)
go_button.pack()

# Button to quit the app.
quit_button = tk.Button(window, text = "Quit", command=window.quit)
quit_button.pack()

# Text box to display output of main text.
text_box = ScrolledText(width=110, borderwidth=2, relief="sunken", padx=20)
text_box.pack()

# Button to clear the text box display.
clear_button = tk.Button(window, text = "Clear", command = lambda: text_box.delete("1.0", tk.END))
clear_button.pack()

# Run an event loop.
window.mainloop()
\$\endgroup\$
4
\$\begingroup\$

PEP-8

To maintain consistency while writing code, Python code follows the PEP-8 naming convention. For example

Source

Function names should be lowercase, with words separated by underscores as necessary to improve readability.

Variable names follow the same convention as function names.

The library that you are currently using, which is tkinter also follows the same naming convention. You can see that classes like Button,Canvas, and StringVar(). All follow the CamelCase since they are classes.

Positioning widgets in your application

Although .pack() works, you will soon see its limits when you start trying to position the widgets in specific places, like an exit button typically stays in the corner, A title usually is placed at the top. In these scenarios .pack() is just very limited.

A common way is to use .place() to position widgets in your application. It has many arguments like bordermode and anchor to customize your task, the two main are x and y which are basically just the horizontal and vertical points at which your widget will be placed.

Here is a simple Tkinter window of geometry("500x500") I have created to show the usage of place(). It also has a Label as a simple widget.

window

Example: widget.place(x = 300,y = 50)

window2

You get to decide where you want to place your widgets accurately.

Structuring a Tk application

@Reinderien suggested your TKINTER_SCRIPT into a function. This makes sense because you already have good functions like search_files(), why would you have your Tkinter application in the main scope?

While some might disagree, my suggestion is that you opt for an Object-oriented approach, which will help keep your code clean. In your situation, it makes sense to have a single MainApplication() class. Here is a simple example of what that would look like

class MainApplication:
    def __init__(self,window,size = "500x500"):
        self.window = window
        window.geometry(size)
        self.entry_box = # Entry box widget
        self.title = # Title label 
        # More widgets that are relevant to MainApplication

    def close_application():
        # print any messages on window like "Thank you for using..."
        self.window.close()

root = tk.Tk()
window = MainApplication(root)

root.mainloop()

Structuring a Tkinter application

Small suggestions

  • Use root.iconbitmap( #icon image ) to set a 16x16 icon for your tkinter application, this will appear instead of the little feather that comes
  • Tkinter has a huge variety of widgets, it is possible that you might find a new one from this list of main widgets that might be perfect for your application.
  • message boxes in tkinter will be suitable for when a user might do something wrong, or maybe you want to get a confirmation.

Example of a message box


yn

\$\endgroup\$
5
  • 1
    \$\begingroup\$ This is brilliant, thanks! I'll take a look through all this tonight and tomorrow and let you know how I get on. Still trying to wrap my head around OOP. For the sample you've provided, should I be keeping my functions (e.g. search_files()) as functions and adding them as methods inside the MainApplication Class? \$\endgroup\$ – user231641 Oct 14 '20 at 18:28
  • \$\begingroup\$ @AaronWright No, search_files has nothing to do with your GUI, hence you should either keep a class that would handle the files, or a few functions would work too \$\endgroup\$ – Aryan Parekh Oct 14 '20 at 18:35
  • 1
    \$\begingroup\$ Okay. I'll aim to create another class to deal with this. I presume all the widgets for the form will need to be declared within the MainApplication Class? \$\endgroup\$ – user231641 Oct 14 '20 at 19:21
  • \$\begingroup\$ @AaronWright Yes, I feel like you should have a look at some good programs made in tkinter. You can find several on github. This will give you some nice ideas \$\endgroup\$ – Aryan Parekh Oct 14 '20 at 19:23
  • 1
    \$\begingroup\$ Thanks. I'll take a look! I have tried to find a good script on GitHub but never had much luck. Do you have any suggestions? I'm also trying to covert my other script that I submitted here (regarding a book library) to an tkinter application and have been struggling to implement it. It may be too advanced for me. \$\endgroup\$ – user231641 Oct 14 '20 at 21:29
2
\$\begingroup\$

Consider using PEP-484 type hints for your function signatures.

Use a linter like pyflakes, flake8 or black that will give you a number of suggestions about your code format.

Move your TKINTER SCRIPT into one or more functions, instead of in global scope.

Use pathlib, so that this:

script_directory+"/mycsv.csv

can be

Path(script_directory) / 'mycsv.csv'

If var1.get() returns 0/1, then you can simply write

IGNORE_WORD_CASE = bool(var1.get())

though you should give var1 a more meaningful name.

\$\endgroup\$
4
  • \$\begingroup\$ Thank you. I don't know much about type hints, but I'll look them up. And great additional suggestions. Is it standard practice to have the main tkinter script into a function? \$\endgroup\$ – user231641 Oct 14 '20 at 17:00
  • \$\begingroup\$ @AaronWright you can have an Object-oriented approach too, check this What is the best way to structure a tkinter application? \$\endgroup\$ – Aryan Parekh Oct 14 '20 at 17:13
  • \$\begingroup\$ @AryanParekh Thanks. Yes I have this (and another) script I would like to convert to OOP style. However I'm struggling with it a bit. Do you know where I could find a good, simple, OOP program with Tkinter so I could see the general structure? The examples given in that link show how you might start it, but it would be good to see a full script. \$\endgroup\$ – user231641 Oct 14 '20 at 18:10
  • \$\begingroup\$ OOP is an option, but not the only one. It's also possible to simply throw everything into a main function, which would still be better than what you have (particularly for unit testing). \$\endgroup\$ – Reinderien Oct 14 '20 at 23:10

Your Answer

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