Have you ever needed to search for a file on your computer? Most operating systems have a way to do this. Windows Explorer has a search function and there’s also a search built-in to the Start Menu now. Other operating systems like Mac and Linux are similar. There are also applications that you can download that are sometimes faster at searching your hard drive than the built-in ones are.
In this article, you will be creating a simple file search utility using wxPython.
You will want to support the following tasks for the file search tool:
- Search by file type
- Case sensitive searches
- Search in sub-directories
You can download the source code from this article on GitHub.
Let’s get started!
Designing Your File Search Utility
It is always fun to try to recreate a tool that you use yourself. However in this case, you will just take the features mentioned above and create a straight-forward user interface. You can use a wx.SearchCtrl
for searching for files and an ObjectListView
for displaying the results. For this particular utility, a wx.CheckBox
or two will work nicely for telling your application to search in sub-directories or if the search term is case-sensitive or not.
Here is a mockup of what the application will eventually look like:
Now that you have a goal in mind, let’s go ahead and start coding!
Creating the File Search Utility
Your search utility will need two modules. The first module will be called main and it will hold your user interface and most of the application’s logic. The second module is named search_threads and it will contain the logic needed to search your file system using Python’s threading
module. You will use pubsub
to update the main module as results are found.
The main script
The main module has the bulk of the code for your application. If you go on and enhance this application, the search portion of the code could end up having the majority of the code since that is where a lot of the refinement of your code should probably go.
Regardless, here is the beginning of main:
# main.py import os import sys import subprocess import time import wx from ObjectListView import ObjectListView, ColumnDefn from pubsub import pub from search_threads import SearchFolderThread, SearchSubdirectoriesThread
This time around, you will be using a few more built-in Python modules, such as os
, sys
, subprocess
and time
. The other imports are pretty normal, with the last one being a couple of classes that you will be creating based around Python’s Thread
class from the threading
module.
For now though, let’s just focus on the main module.
Here’s the first class you need to create:
class SearchResult: def __init__(self, path, modified_time): self.path = path self.modified = time.strftime('%D %H:%M:%S', time.gmtime(modified_time))
The SearchResult
class is used for holding information about the results from your search. It is also used by the ObjectListView
widget. Currently, you will use it to hold the full path to the search result as well as the file’s modified time. You could easily enhance this to also include file size, creation time, etc.
Now let’s create the MainPanel
which houses most of UI code:
class MainPanel(wx.Panel): def __init__(self, parent): super().__init__(parent) self.search_results = [] self.main_sizer = wx.BoxSizer(wx.VERTICAL) self.create_ui() self.SetSizer(self.main_sizer) pub.subscribe(self.update_search_results, 'update')
The __init__()
method gets everything set up. Here you create the main_sizer
, an empty list of search_results
and a listener or subscription using pubsub
. You also call create_ui()
to add the user interface widgets to the panel.
Let’s see what’s in create_ui()
now:
def create_ui(self): # Create the widgets for the search path row_sizer = wx.BoxSizer() lbl = wx.StaticText(self, label='Location:') row_sizer.Add(lbl, 0, wx.ALL | wx.CENTER, 5) self.directory = wx.TextCtrl(self, style=wx.TE_READONLY) row_sizer.Add(self.directory, 1, wx.ALL | wx.EXPAND, 5) open_dir_btn = wx.Button(self, label='Choose Folder') open_dir_btn.Bind(wx.EVT_BUTTON, self.on_choose_folder) row_sizer.Add(open_dir_btn, 0, wx.ALL, 5) self.main_sizer.Add(row_sizer, 0, wx.EXPAND)
There are quite a few widgets to add to this user interface. To start off, you add a row of widgets that consists of a label, a text control and a button. This series of widgets allows the user to choose which directory they want to search using the button. The text control will hold their choice.
Now let’s add another row of widgets:
# Create search filter widgets row_sizer = wx.BoxSizer() lbl = wx.StaticText(self, label='Limit search to filetype:') row_sizer.Add(lbl, 0, wx.ALL|wx.CENTER, 5) self.file_type = wx.TextCtrl(self) row_sizer.Add(self.file_type, 0, wx.ALL, 5) self.sub_directories = wx.CheckBox(self, label='Sub-directories') row_sizer.Add(self.sub_directories, 0, wx.ALL | wx.CENTER, 5) self.case_sensitive = wx.CheckBox(self, label='Case-sensitive') row_sizer.Add(self.case_sensitive, 0, wx.ALL | wx.CENTER, 5) self.main_sizer.Add(row_sizer)
This row of widgets contains another label, a text control and two instances of wx.Checkbox
. These are the filter widgets which control what you are searching for. You can filter based on any of the following:
- The file type
- Search sub-directories (when checked) or just the chosen directory
- The search term is case-sensitive
The latter two options are represented by using the wx.Checkbox
widget.
Let’s add the search control next:
# Add search bar self.search_ctrl = wx.SearchCtrl( self, style=wx.TE_PROCESS_ENTER, size=(-1, 25)) self.search_ctrl.Bind(wx.EVT_SEARCHCTRL_SEARCH_BTN, self.on_search) self.search_ctrl.Bind(wx.EVT_TEXT_ENTER, self.on_search) self.main_sizer.Add(self.search_ctrl, 0, wx.ALL | wx.EXPAND, 5)
The wx.SearchCtrl
is the widget to use for searching. You could quite easily use a wx.TextCtrl
instead though. Regardless, in this case you bind to the press of the Enter key and to the mouse click of the magnifying class within the control. If you do either of these actions, you will call search()
.
Now let’s add the last two widgets and you will be done with the code for create_ui()
:
# Search results widget self.search_results_olv = ObjectListView( self, style=wx.LC_REPORT | wx.SUNKEN_BORDER) self.search_results_olv.SetEmptyListMsg("No Results Found") self.main_sizer.Add(self.search_results_olv, 1, wx.ALL | wx.EXPAND, 5) self.update_ui() show_result_btn = wx.Button(self, label='Open Containing Folder') show_result_btn.Bind(wx.EVT_BUTTON, self.on_show_result) self.main_sizer.Add(show_result_btn, 0, wx.ALL | wx.CENTER, 5)
The results of your search will appear in your ObjectListView
widget. You also need to add a button that will attempt to show the result in the containing folder, kind of like how Mozilla Firefox has a right-click menu called “Open Containing Folder” for opening downloaded files.
The next method to create is on_choose_folder()
:
def on_choose_folder(self, event): with wx.DirDialog(self, "Choose a directory:", style=wx.DD_DEFAULT_STYLE, ) as dlg: if dlg.ShowModal() == wx.ID_OK: self.directory.SetValue(dlg.GetPath())
You need to allow the user to select a folder that you want to conduct a search in. You could let the user type in the path, but that is error-prone and you might need to add special error checking. Instead, you opt to use a wx.DirDialog
, which prevents the user from entering a non-existent path. It is possible for the user to select the folder, then delete the folder before executing the search, but that would be an unlikely scenario.
Now you need a way to open a folder with Python:
def on_show_result(self, event): """ Attempt to open the folder that the result was found in """ result = self.search_results_olv.GetSelectedObject() if result: path = os.path.dirname(result.path) try: if sys.platform == 'darwin': subprocess.check_call(['open', '--', path]) elif 'linux' in sys.platform: subprocess.check_call(['xdg-open', path]) elif sys.platform == 'win32': subprocess.check_call(['explorer', path]) except: if sys.platform == 'win32': # Ignore error on Windows as there seems to be # a weird return code on Windows return message = f'Unable to open file manager to {path}' with wx.MessageDialog(None, message=message, caption='Error', style= wx.ICON_ERROR) as dlg: dlg.ShowModal()
The on_show_result()
method will check what platform the code is running under and then attempt to launch that platform’s file manager. Windows uses Explorer while Linux uses xdg-open for example.
During testing, it was noticed that on Windows, Explorer returns a non-zero result even when it opens Explorer successfully, so in that case you just ignore the error. But on other platforms, you can show a message to the user that you were unable to open the folder.
The next bit of code you need to write is the on_search()
event handler:
def on_search(self, event): search_term = self.search_ctrl.GetValue() file_type = self.file_type.GetValue() file_type = file_type.lower() if '.' not in file_type: file_type = f'.{file_type}' if not self.sub_directories.GetValue(): # Do not search sub-directories self.search_current_folder_only(search_term, file_type) else: self.search(search_term, file_type)
When you click the “Search” button, you want it to do something useful. That is where the code above comes into play. Here you get the search_term
and the file_type
. To prevent issues, you put the file type in lower case and you will do the same thing during the search.
Next you check to see if the sub_directories
check box is checked or not. If sub_directories
is unchecked, then you call search_current_folder_only()
; otherwise you call search()
.
Let’s see what goes into search()
first:
def search(self, search_term, file_type): """ Search for the specified term in the directory and its sub-directories """ folder = self.directory.GetValue() if folder: self.search_results = [] SearchSubdirectoriesThread(folder, search_term, file_type, self.case_sensitive.GetValue())
Here you grab the folder that the user has selected. In the event that the user has not chosen a folder, the search button will not do anything. But if they have chosen something, then you call the SearchSubdirectoriesThread
thread with the appropriate parameters. You will see what the code in that class is in a later section.
But first, you need to create the search_current_folder_only()
method:
def search_current_folder_only(self, search_term, file_type): """ Search for the specified term in the directory only. Do not search sub-directories """ folder = self.directory.GetValue() if folder: self.search_results = [] SearchFolderThread(folder, search_term, file_type, self.case_sensitive.GetValue())
This code is pretty similar to the previous function. Its only difference is that it executesSearchFolderThread
instead of SearchSubdirectoriesThread
.
The next function to create is update_search_results()
:
def update_search_results(self, result): """ Called by pubsub from thread """ if result: path, modified_time = result self.search_results.append(SearchResult(path, modified_time)) self.update_ui()
When a search result is found, the thread will post that result back to the main application using a thread-safe method and pubsub
. This method is what will get called assuming that the topic matches the subscription that you created in the __init__()
. Once called, this method will append the result to search_results
and then call update_ui()
.
Speaking of which, you can code that up now:
def update_ui(self): self.search_results_olv.SetColumns([ ColumnDefn("File Path", "left", 300, "path"), ColumnDefn("Modified Time", "left", 150, "modified") ]) self.search_results_olv.SetObjects(self.search_results)
The update_ui()
method defines the columns that are shown in your ObjectListView
widget. It also calls SetObjects()
which will update the contents of the widget and show your search results to the user.
To wrap up the main module, you will need to write the Search
class:
class Search(wx.Frame): def __init__(self): super().__init__(None, title='Search Utility', size=(600, 600)) pub.subscribe(self.update_status, 'status') panel = MainPanel(self) self.statusbar = self.CreateStatusBar(1) self.Show() def update_status(self, search_time): msg = f'Search finished in {search_time:5.4} seconds' self.SetStatusText(msg) if __name__ == '__main__': app = wx.App(False) frame = Search() app.MainLoop()
This class creates the MainPanel
which holds most of the widgets that the user will see and interact with. It also sets the initial size of the application along with its title. There is also a status bar that will be used to communicate to the user when a search has finished and how long it took for said search to complete.
Here is what the application will look like:
Now let’s move on and create the module that holds your search threads.
The search_threads Module
The search_threads module contains the two Thread
classes that you will use for searching your file system. The thread classes are actually quite similar in their form and function.
Let’s get started:
# search_threads.py import os import time import wx from pubsub import pub from threading import Thread
These are the modules that you will need to make this code work. You will be using the os
module to check paths, traverse the file system and get statistics from files. You will use pubsub
to communicate with your application when your search returns results.
Here is the first class:
class SearchFolderThread(Thread): def __init__(self, folder, search_term, file_type, case_sensitive): super().__init__() self.folder = folder self.search_term = search_term self.file_type = file_type self.case_sensitive = case_sensitive self.start()
This thread takes in the folder
to search in, the search_term
to look for, a file_type
filter and whether or not the search term is case_sensitive
. You take these in and assign them to instance variables of the same name. The point of this thread is only to search the contents of the folder that is passed-in, not its sub-directories.
You will also need to override the thread’s run()
method:
def run(self): start = time.time() for entry in os.scandir(self.folder): if entry.is_file(): if self.case_sensitive: path = entry.name else: path = entry.name.lower() if self.search_term in path: _, ext = os.path.splitext(entry.path) data = (entry.path, entry.stat().st_mtime) wx.CallAfter(pub.sendMessage, 'update', result=data) end = time.time() # Always update at the end even if there were no results wx.CallAfter(pub.sendMessage, 'update', result=[]) wx.CallAfter(pub.sendMessage, 'status', search_time=end-start)
Here you collect the start time of the thread. Then you use os.scandir()
to loop over the contents of the folder
. If the path is a file, you will check to see if the search_term
is in the path and has the right file_type
. Should both of those return True
, then you get the requisite data and send it to your application using wx.CallAfter()
, which is a thread-safe method.
Finally you grab the end_time
and use that to calculate the total run time of the search and then send that back to the application. The application will then update the status bar with the search time.
Now let’s check out the other class:
class SearchSubdirectoriesThread(Thread): def __init__(self, folder, search_term, file_type, case_sensitive): super().__init__() self.folder = folder self.search_term = search_term self.file_type = file_type self.case_sensitive = case_sensitive self.start()
The SearchSubdirectoriesThread
thread is used for searching not only the passed-in folder
but also its sub-directories. It accepts the same arguments as the previous class.
Here is what you will need to put in its run()
method:
def run(self): start = time.time() for root, dirs, files in os.walk(self.folder): for f in files: full_path = os.path.join(root, f) if not self.case_sensitive: full_path = full_path.lower() if self.search_term in full_path and os.path.exists(full_path): _, ext = os.path.splitext(full_path) data = (full_path, os.stat(full_path).st_mtime) wx.CallAfter(pub.sendMessage, 'update', result=data) end = time.time() # Always update at the end even if there were no results wx.CallAfter(pub.sendMessage, 'update', result=[]) wx.CallAfter(pub.sendMessage, 'status', search_time=end-start)
For this thread, you need to use os.walk()
to search the passed in folder
and its sub-directories. Besides that, the conditional statements are virtually the same as the previous class.
Wrapping Up
Creating search utilities is not particularly difficult, but it can be time-consuming. Figuring out the edge cases and how to account for them is usually what takes the longest when creating software. In this article, you learned how to create a utility to search for files on your computer.
Here are a few enhancements that you could add to this program:
- Add the ability to stop the search
- Prevent multiple searches from occurring at the same time
- Add other filters
Related Reading
Want to learn how to create more GUI applications with wxPython? Then check out these resources below:
-
Creating a GUI Application for NASA’s API with wxPython (article)
- Creating GUI Applications with wxPython (book) on Leanpub, Gumroad, or Amazon.