18 KiB
用 wxPython 创建文件搜索 GUI
原文:https://www.blog.pythonlibrary.org/2021/09/02/creating-a-file-search-gui-with-wxpython/
你曾经需要在你的计算机上搜索一个文件吗?大多数操作系统都有办法做到这一点。Windows 资源管理器有搜索功能,现在开始菜单中也有内置的搜索功能。其他操作系统如 Mac 和 Linux 也类似。你也可以下载一些应用程序,它们搜索硬盘的速度有时比内置的要快。
在本文中,您将使用 wxPython 创建一个简单的文件搜索实用程序。
您需要支持文件搜索工具的以下任务:
- 按文件类型搜索
- 区分大小写的搜索
- 在子目录中搜索
你可以在 GitHub 上下载这篇文章的源代码。
我们开始吧!
设计您的文件搜索工具
尝试再造一个你自己使用的工具总是很有趣的。然而,在这种情况下,您只需利用上面提到的特性,创建一个简单明了的用户界面。您可以使用wx.SearchCtrl来搜索文件,使用ObjectListView来显示结果。对于这个特定的实用程序,一两个wx.CheckBox将很好地告诉您的应用程序在子目录中搜索,或者搜索项是否区分大小写。
以下是应用程序最终外观的模型:
文件搜索模型
既然你心中有了目标,那就让我们开始编码吧!
创建文件搜索实用程序
您的搜索工具将需要两个模块。第一个模块将被称为 main ,它将保存你的用户界面和应用程序的大部分逻辑。第二个模块被命名为 search_threads ,它将包含使用 Python 的threading模块搜索文件系统所需的逻辑。找到结果后,您将使用pubsub更新主模块。
主脚本
主模块拥有您的应用程序的大部分代码。如果您继续并增强这个应用程序,代码的搜索部分可能最终拥有大部分代码,因为这可能是您的代码应该进行大量改进的地方。
不管怎样,这里是主的开始:
# 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
这一次,您将使用更多的内置 Python 模块,比如os、sys、subprocess和time。其他的导入都很正常,最后一个是几个类,你将从threading模块中基于 Python 的Thread类创建。
不过现在,让我们只关注主模块。
下面是您需要创建的第一个类:
class SearchResult:
def __init__(self, path, modified_time):
self.path = path
self.modified = time.strftime('%D %H:%M:%S',
time.gmtime(modified_time))
SearchResult类用于保存搜索结果的相关信息。它也由ObjectListView小部件使用。目前,您将使用它来保存搜索结果的完整路径以及文件的修改时间。您可以很容易地增强它,使其包括文件大小、创建时间等。
现在让我们创建包含大部分 UI 代码的MainPanel:
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')
__init__()方法设置好了一切。这里您创建了main_sizer,一个空的search_results列表和一个使用pubsub的监听器或订阅。您还可以调用create_ui()将用户界面小部件添加到面板中。
让我们看看现在有什么:
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)
有很多小部件可以添加到这个用户界面中。首先,添加一行由标签、文本控件和按钮组成的小部件。这一系列小部件允许用户使用按钮选择他们想要搜索的目录。文本控件将保存他们的选择。
现在让我们添加另一行小部件:
# 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)
这一行小部件包含另一个标签、一个文本控件和两个wx.Checkbox实例。这些是过滤器部件,控制你要搜索的内容。您可以根据以下任何一项进行筛选:
- 文件类型
- 搜索子目录(选中时)或只搜索选定的目录
- 搜索词区分大小写
后两个选项通过使用wx.Checkbox小部件来表示。
接下来让我们添加搜索控件:
# 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)
wx.SearchCtrl是用于搜索的小部件。不过你可以很容易地使用一个wx.TextCtrl来代替。无论如何,在这种情况下,您绑定到 Enter 键的按下和控件内的放大类的鼠标单击。如果你做了这些动作中的任何一个,你将呼叫search()。
现在让我们添加最后两个小部件,您将完成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)
您的搜索结果将出现在您的ObjectListView小工具中。您还需要添加一个按钮,尝试在包含文件夹中显示结果,有点像 Mozilla Firefox 有一个名为“打开包含文件夹”的右键菜单,用于打开下载的文件。
下一个要创建的方法是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())
您需要允许用户选择您想要在其中进行搜索的文件夹。您可以让用户输入路径,但是这很容易出错,并且您可能需要添加特殊的错误检查。相反,您选择使用一个wx.DirDialog,它可以防止用户输入一个不存在的路径。用户可以选择文件夹,然后在执行搜索之前删除文件夹,但这种情况不太可能发生。
现在您需要一种用 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()
on_show_result()方法将检查代码在什么平台下运行,然后尝试启动该平台的文件管理器。Windows 用 Explorer 而 Linux 用 xdg-open 比如。
在测试过程中,我们注意到在 Windows 上,即使 Explorer 成功打开,它也会返回一个非零结果,所以在这种情况下,您可以忽略该错误。但是在其他平台上,您可以向用户显示一条消息,说明您无法打开该文件夹。
您需要编写的下一段代码是on_search()事件处理程序:
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)
当你点击“搜索”按钮时,你希望它做一些有用的事情。这就是上面的代码发挥作用的地方。这里你得到了search_term和file_type。为了防止出现问题,您将文件类型设置为小写,并在搜索过程中执行同样的操作。
接下来,检查是否勾选了sub_directories复选框。如果sub_directories没有勾选,那么你调用search_current_folder_only();不然你叫search()。
让我们先看看search()里有什么:
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())
在这里,您获取用户选择的文件夹。如果用户没有选择文件夹,搜索按钮将不起任何作用。但是如果他们已经选择了什么,那么你就用合适的参数调用SearchSubdirectoriesThread线程。您将在后面的部分看到该类中的代码。
但是首先,您需要创建search_current_folder_only()方法:
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())
这段代码与前面的函数非常相似。它唯一的区别是执行
SearchFolderThread而不是SearchSubdirectoriesThread。
下一个要创建的函数是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()
当找到一个搜索结果时,线程将使用线程安全的方法和pubsub把结果发送回主应用程序。假设主题与您在__init__()中创建的订阅相匹配,这个方法将被调用。一旦被调用,这个方法将把结果追加到search_results中,然后调用update_ui()。
说到这里,你现在可以编码了:
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)
update_ui()方法定义了在ObjectListView小部件中显示的列。它还调用SetObjects(),这将更新小部件的内容,并向用户显示您的搜索结果。
为了包装主模块,您需要编写Search类:
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()
这个类创建了MainPanel,它保存了用户将会看到并与之交互的大多数小部件。它还设置应用程序的初始大小及其标题。还有一个状态栏,用于通知用户搜索何时完成,以及搜索完成需要多长时间。
下面是该应用程序的外观:
现在让我们继续创建保存搜索线程的模块。
搜索线程模块
search_threads 模块包含两个用于搜索文件系统的Thread类。线程类实际上在形式和功能上非常相似。
让我们开始吧:
# search_threads.py
import os
import time
import wx
from pubsub import pub
from threading import Thread
这些是使代码工作所需的模块。您将使用os模块来检查路径、遍历文件系统并从文件中获取统计数据。当您的搜索返回结果时,您将使用pubsub与您的应用程序通信。
下面是第一节课:
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()
这个线程接受要搜索的folder、要查找的search_term、一个file_type过滤器以及搜索词是否为case_sensitive。您接受这些变量,并将它们赋给同名的实例变量。这个线程的目的只是搜索传入的文件夹的内容,而不是它的子目录。
您还需要覆盖线程的run()方法:
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)
这里你收集线程的开始时间。然后使用os.scandir()循环遍历folder的内容。如果路径是一个文件,您将检查search_term是否在路径中,以及是否有正确的file_type。如果这两个函数都返回True,那么您将获得必要的数据,并使用wx.CallAfter()将其发送到您的应用程序,这是一个线程安全的方法。
最后,您获取end_time并使用它来计算搜索的总运行时间,然后将其发送回应用程序。然后,应用程序会用搜索时间更新状态栏。
现在让我们看看另一个类:
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()
SearchSubdirectoriesThread线程不仅用于搜索传入的folder,还用于搜索它的子目录。它接受与前一个类相同的参数。
下面是您需要放入它的run()方法中的内容:
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)
对于这个线程,需要使用os.walk()来搜索传入的folder及其子目录。除此之外,条件语句实际上与前面的课程相同。
包扎
创建搜索工具并不特别困难,但是很耗时。在创建软件时,找出边缘情况以及如何考虑它们通常是最耗时的事情。在本文中,您了解了如何创建一个实用程序来搜索计算机上的文件。
以下是您可以添加到该程序中的一些增强功能:
- 添加停止搜索的功能
- 防止多个搜索同时发生
- 添加其他过滤器
相关阅读
想学习如何用 wxPython 创建更多的 GUI 应用程序吗?然后查看以下资源:

