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:
- Specify Directory
- Input string you would like to search for
- Toggle ignore case on or off
- 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.
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()
2 Answers 2
PEP-8
To maintain consistency while writing code, Python code follows the PEP-8 naming convention. For example
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)
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 a16x16
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
-
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– user2316412020年10月14日 18:28:58 +00:00Commented Oct 14, 2020 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\$user228914– user2289142020年10月14日 18:35:37 +00:00Commented Oct 14, 2020 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– user2316412020年10月14日 19:21:26 +00:00Commented Oct 14, 2020 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\$user228914– user2289142020年10月14日 19:23:17 +00:00Commented Oct 14, 2020 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– user2316412020年10月14日 21:29:39 +00:00Commented Oct 14, 2020 at 21:29
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.
-
\$\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– user2316412020年10月14日 17:00:32 +00:00Commented Oct 14, 2020 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\$user228914– user2289142020年10月14日 17:13:20 +00:00Commented Oct 14, 2020 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– user2316412020年10月14日 18:10:07 +00:00Commented Oct 14, 2020 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– Reinderien2020年10月14日 23:10:56 +00:00Commented Oct 14, 2020 at 23:10