Python tkinter调用askopenfilename后进程无法正常退出
问题背景
本人并非专业程序员,问题标题表述可能存在偏差。
开发了一款处理IFC文件的Python程序:
- IFC是建筑行业常用文件格式,存储3D图纸的几何信息及图纸内对象的关联数据
- 程序运行流程:启动后先选择目标IFC文件,之后弹出交互对话框,支持用户输入GUID(对象标识符),检索IFC文件中与该GUID对应对象关联的全部信息,最终将检索结果打印到命令提示符窗口
故障现象
使用tkinter的askopenfilename函数实现可视化文件选择时,关闭后续弹出的GUID查询对话框后,程序进程仍处于活跃/循环状态无法自动退出,必须按下Ctrl+C强制终止进程。
复现代码
#imports to search for files import os # imports tkinter for selection purposes, so that one can select the files instead of reading in them automaticall from a directory import tkinter as tk from tkinter.filedialog import askopenfilename #importing the counter to counte the occurences in a list from collections import Counter #importing regex functionality to check values import re #this is the numbering, for example #433, #4781 etc, that we either want to look for or change - now it looks for up to a number of length 7 regexIFCnumbers = '#\d{1,7}' tk.Tk().withdraw() # we don't want a full GUI, so keep the root window from appearing file1 = askopenfilename(initialdir=os.getcwd()) # show an "Open" dialog box and return the path to the selected file - this in the current directory where we'll start looking file1Name = (os.path.basename(file1)) #only the file's name #read files def readFile(file): x = [] f = open(file, 'r') x = f.readlines() f.close() return(x) x1 = readFile(file1) #checks the GUIDs def checkGUID(GUID, IFC): A = [] for row in IFC: if GUID in row: #print(re.findall(regexIFCnumbers, row)) A.extend(re.findall(regexIFCnumbers, row)) return(A) #the numbering is not perfectly ordered and varies in some places, so need to index it all to find the correct lines def indexIFC(IFC): A = [] for index, row in enumerate(IFC): if re.findall('^#', row): #starts with a hash # B = re.findall(r'\#\w+', row)[0] A.append([B, index]) return(A) def recurseThrough(inputList, IFC, checkedList, fullList, indexednumbersList): for item in inputList: for hashvalueList in indexednumbersList: if item == hashvalueList[0]: positionInIFC = hashvalueList[1] if re.search('^'+item, IFC[positionInIFC]) and item not in checkedList: #if the row begins with the number item in the list checkedList.append(item) fullList.append(IFC[positionInIFC]) recurseThrough(re.findall(regexIFCnumbers, IFC[positionInIFC])[1:], IFC, checkedList, fullList, indexednumbersList) #recurses through the next list return(fullList) from os import system, name def clear(): if name == 'nt': _ = system('cls') def runTheGUIDCheck(setValue): inputValue = str(setValue) print(inputValue) clear() try: B1 = checkGUID(inputValue, x1) #This returns a list with for example [#122, #5, #7889] checkedList = [] #the list of already checked items fullList = [] indexedIFClist1 = indexIFC(x1) #run the function with the initial input, sending in empty array as well as they should be empty/none at the start outList1 = recurseThrough(B1, x1, [], [], indexedIFClist1) for index, item in enumerate(outList1): print(index, item.strip()) return None except: print("inserted GUID not found or didn't work") #dialog dialog = tk.Tk() dialog.geometry('500x200') t1 = tk.Label(dialog, text = 'Check GUID') t1.grid(row = 0, column = 0, sticky = 'w') testGUIDAttributes = tk.Entry(dialog, width = 40) testGUIDAttributes.grid(row = 0, column = 1, columnspan = 50) button = tk.Button(dialog, text='Run GUID', command = lambda: runTheGUIDCheck(testGUIDAttributes.get())) button.grid(row = 5, column = 0, sticky = 'w') dialog.mainloop()
对比测试结果
- 测试操作:移除
askopenfilename调用,直接硬编码文件路径:
file1 = 'thefile.ifc'
替换原代码中文件选择段:
tk.Tk().withdraw() # we don't want a full GUI, so keep the root window from appearing file1 = askopenfilename(initialdir=os.getcwd()) # show an "Open" dialog box and return the path to the selected file - this in the current directory where we'll start looking
- 测试结果:进程无法退出的问题消失,关闭GUID查询对话框后,命令提示符窗口可正常接收新指令,进程自动结束。
故障原因
代码中创建了两个独立的Tk根窗口实例:
- 执行
tk.Tk().withdraw()时生成第一个隐藏的根窗口,用于承载文件选择对话框,该实例没有被手动销毁,后台始终运行事件循环 - 后续创建GUID对话框时执行
dialog = tk.Tk(),生成第二个独立的根窗口
关闭GUID对话框时仅销毁第二个Tk实例,第一个隐藏的根窗口仍在后台运行,因此进程不会自动退出。硬编码文件路径时没有创建第一个隐藏Tk实例,无残留事件循环,因此进程可正常退出。
修复方案
Tkinter程序全程只能保留一个Tk根窗口实例,禁止重复创建:
- 程序启动时先初始化唯一的根窗口,隐藏后用于弹出文件选择框
- GUID对话框不要新建
tk.Tk()实例,使用Toplevel创建依附于根窗口的子窗口 - 绑定对话框关闭事件,触发时直接销毁唯一的根窗口,终止所有事件循环
核心修改代码参考:
# 初始化唯一的Tk根实例,全局复用 root = tk.Tk() root.withdraw() # 初始隐藏,用于弹出文件选择对话框 file1 = askopenfilename(initialdir=os.getcwd()) # 增加空路径判断,避免用户点取消后读文件报错 if not file1: exit() # --- 中间的文件读取、自定义函数部分无需修改 --- # GUID对话框使用Toplevel创建子窗口,不要新建tk.Tk() dialog = tk.Toplevel(root) dialog.geometry('500x200') # 绑定窗口关闭事件,关闭时销毁根实例,彻底退出程序 def on_dialog_close(): root.destroy() dialog.protocol("WM_DELETE_WINDOW", on_dialog_close) # --- 原有标签、输入框、按钮的布局代码无需修改 --- # 启动根窗口的事件循环 root.mainloop()
补充优化建议:原代码的文件读取、GUID检索逻辑缺少异常捕获,建议补充文件不存在、GUID无匹配结果的分支提示,避免程序无响应或抛出非预期错误。
内容的提问来源于stack exchange,提问作者user2703642
相关产品推荐
相关产品推荐

