Python Forum
[Tkinter] MultiThreading Return value issue
Thread Rating:
  • 0 Vote(s) - 0 Average
  • 1
  • 2
  • 3
  • 4
  • 5
[Tkinter] MultiThreading Return value issue
#1
Hi,

I have a small GUI program which reads a drives content and saves it in a csv file. The scanning of the drive takes some time and I am trying to put it into a separate thread. I wrote the following code but not sure if the logic is proper and also having issues returning variable from the threaded function. I tried two methods by defining it as a separate class as well as a separate function but none of them worked.

Here is my code and specific problem location is between lines 134 ~ 170 and the class method is in lines 60 ~ 82.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
import tkinter as tk
from tkinter import ttk,filedialog,messagebox,simpledialog,BOTH,END,LEFT
import threading, time
import pandas as pd
import os
#import Queue
import sys
 
import win32api
 
 
 
sys.setrecursionlimit(1500)
pd.set_option("display.max_colwidth", 10000)
pd.set_option('display.max_columns', 15)
pd.set_option('precision', 8)
 
TITLE_FONT = ("Arial",14)
LARGE_FONT = ("Times New Roman",11)
 
 
class HardDiskContent(tk.Tk):
     
    def __init__(self, *args, **kwargs):
         
        tk.Tk.__init__(self, *args, **kwargs)
         
        tk.Tk.wm_title(self,"HDD Content Reader")
         
        container = tk.Frame(self)
        container.pack(side="top", fill="both", expand=True)
        container.grid_rowconfigure(0,weight=1)
        container.grid_columnconfigure(0,weight=1)
         
        self.SystemParam = {
                "DriveLetter":tk.StringVar(),
                "SaveFile":tk.StringVar()}
         
        # Store different page information. Add here new page for initialization
        self.frames = {}
         
#        for F in (StartPage,GeneralParameters):
        F = StartPage   
        frame = F(container,self)
        self.frames[F] = frame
        frame.grid(row=0,column=0,sticky="nsew")
         
        self.show_frame(StartPage)
    # Function for showing a frame   
    def show_frame(self,cont):
        frame = self.frames[cont]
        frame.tkraise()
        frame.update()
        frame.event_generate("<<ShowFrame>>")
     
         
    def get_page(self, page_class):
        return self.frames[page_class]
     
class Threader(threading.Thread):
 
    def __init__(self, Drive_letter):
 
        threading.Thread.__init__(self, Drive_letter)
        self.daemon = True
        self.Drive_letter = Drive_letter
 
    def run(self):
 
#         while True:
#        print("Look a while true loop that doesn't block the GUI!")
        print("Scanning Drive: ",self.Drive_letter)
#        time.sleep(1)
         
        directory_list = []
         
        for (dirpath, dirnames, filenames) in os.walk(self.Drive_letter):
            for k in range(1):
                directory_list.append(dirpath)
         
        print(directory_list)       
        return directory_list
     
     
class StartPage(tk.Frame):
    # Initialization loop. Anything declared here is executed as soon as the program is run. Even if the page is not visible
    def __init__(self,parent,controller):
        tk.Frame.__init__(self,parent)
        self.controller=controller
        # Label for start Page
        label = tk.Label(self, text=" Sakata Lab ",font=TITLE_FONT)
        label.pack(pady=10,padx=10)
                         
        
        top_frame = tk.LabelFrame(self,text="Selection",padx=5,pady=5)
         
        label0 = tk.Label(top_frame, text="Select Drive")
        label0.grid(row=1,column=1,columnspan=2)
         
        label1 = tk.Label(top_frame, text="Save FileName")
        label1.grid(row=2,column=1,columnspan=2)
         
         
        button = tk.Button(top_frame, text="Scan", command =lambda: [self.scan(controller)])
        button.grid(row=1,column=5,columnspan=2,padx=10,pady=5)
         
        drives = win32api.GetLogicalDriveStrings()
        drives = drives.split('\000')[:-1]
#        drives = [drivestr in drives.split('\000') if drivestr]
        print(drives)
         
        self.drive_selection = ttk.Combobox(top_frame,values=drives,height=4,width=10)
        self.drive_selection.grid(row=1, column=3,columnspan=2)
        self.drive_selection.bind("<<ComboboxSelected>>", self.Drive_letter)
         
        self.entry2 = tk.Entry(top_frame, textvariable=self.controller.SystemParam["SaveFile"],width=20)
        self.entry2.grid(row=2,column=3,columnspan=2)
         
       
        button2 = tk.Button(top_frame, text="Save", command =lambda: [self.save_data(controller)])
        button2.grid(row=2,column=5,columnspan=2,padx = 10)
 
        ## LAYOUT
        top_frame.pack()
 
         
    def Drive_letter(self,event):
        try:
            self.controller.SystemParam["DriveLetter"] = self.drive_selection.get()
            print("Selected Drive Letter = ", self.controller.SystemParam["DriveLetter"])
        except:
            messagebox.showwarning("Drive Selection Notification","No Option Selected")
             
    def Drive_Scanner(self,Drive_letter):
         
        print("Scanning Drive: ",Drive_letter)
        directory_list = []
         
        for (dirpath, dirnames, filenames) in os.walk(self.Drive_letter):
            for k in range(1):
                directory_list.append(dirpath)
         
        self.directory_list = directory_list
#        print(directory_list)       
#        return directory_list
         
         
        
    # Function to notify the user of selection    
    def scan(self,controller):
         
        if not self.drive_selection.get():
            print("Drive Not Selected")
        else:
            print(self.controller.SystemParam["DriveLetter"])
             
            Scan_Process = threading.Thread(target = self.Drive_Scanner,args = self.controller.SystemParam["DriveLetter"])
            Scan_Process.daemon = True
            Scan_Process.start()
             
            directory_list = self.controller.directory_list
            print(directory_list)
#            directory_list = []
#    #        file_list_test = []
#   
#            for (dirpath, dirnames, filenames) in os.walk(self.controller.SystemParam["DriveLetter"]):
#                for k in range(1):
#    #            for dir in dirpath:
#    #                f_test.append(dir)
#                    directory_list.append(dirpath)
##                    print(dirpath)
#    #                file_list_test.append(os.path.join(dirpath+"\\"+dir))
            info = win32api.GetVolumeInformation(self.controller.SystemParam["DriveLetter"])
            print( "disk serial number = %d" % info[1])
            directory_list.insert(0,info[1])
            directory_list.insert(0,info[0])
            print(pd.DataFrame(directory_list))
             
            self.new_data = directory_list
             
            messagebox.showinfo("Scan Complete","Selected drive scanned successfully!")
         
    def set_save_filepath(self,controller):
 
#        print(self.CableDatabase_Entry.index("end"))
        try:
            self.entry2.delete(0, END)
            filepath = filedialog.askopenfilename(initialdir = "/",title = "Open file for Database",filetypes = (("xlsx files","*.xlsx"),("all files","*.*")))
            self.controller.SystemParam["SaveFile"] = filepath
            print(self.controller.SystemParam["SaveFile"])
            self.entry2.insert(END, filepath)
        except:
            self.entry2.insert(END, "There was an error opening ")
            self.entry2.insert(END, filepath)
            self.entry2.insert(END, "\n")
             
        self.save_data(controller)
                     
    def save_data(self,controller):
         
        if not self.controller.SystemParam["SaveFile"].get():
            messagebox.showwarning("File Name Missing","File Name not entered")
        else:
            filename = self.controller.SystemParam["SaveFile"].get()
            print(filename)
        
            directory_list = pd.DataFrame(self.new_data)
            directory_list.to_csv(filename,header=None,index=False)
            messagebox.showinfo("Save Successful","Directory Names saved successfully!")
             
             
app = HardDiskContent()
app.geometry("600x200")
app.mainloop()
The errors I am getting are

Error:
TypeError: Drive_Scanner() takes 2 positional arguments but 4 were given Exception in Tkinter callback AttributeError: '_tkinter.tkapp' object has no attribute 'directory_list'
Any help or suggestion would be appreciated. I am new to python and tkinter, kindly be gentle with your reply if the issue seems too naive for posting in this forum. Smile
Reply
#2
(Nov-01-2019, 08:51 PM)AvisPaul1 Wrote: TypeError: Drive_Scanner() takes 2 positional arguments but 4 were given

Your function only has two params: a reference to the object (self), and the DriveLetter. But when the callback is triggered, tkinter is passing extra arguments, which is throwing an error. So the first step, is taking a peek at what else is getting passed, so you can handle it properly.

The easy way of doing so, is adding *args to the function definition, and then printing it out so you can see what's there.

The second error, about the directory_list, I'd rather see the entire error before commenting on. But it looks like you're trying to use a variable before creating it.
Reply
#3
Thanks for your prompt reply and suggestion. It seems that when I was passing the drive letter eg. 'K:\\' it was treating the variables as K, :, \\ separately. Now I am passing the variable differently and not getting the first error anymore. However the second error remains. The lines in the code from 134 to 157 are modified to

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
def Drive_Scanner(self,*args):
         
        print('Extra Arguments ',args)
#        print("Scanning Drive: ",Drive_letter)
        print(self.controller.SystemParam["DriveLetter"])
        directory_list = []
         
        for (dirpath, dirnames, filenames) in os.walk(self.controller.SystemParam["DriveLetter"]):
            for k in range(1):
                directory_list.append(dirpath)
         
#        self.directory_list = directory_list
#        print(directory_list)       
        return directory_list
                
        
    # Function to notify the user of selection    
    def scan(self,controller):
         
        if not self.drive_selection.get():
            print("Drive Not Selected")
        else:
#            print(self.controller.SystemParam["DriveLetter"])
             
            Scan_Process = threading.Thread(target = self.Drive_Scanner)
The error that I am getting now is
Error:
Exception in Tkinter callback Traceback (most recent call last): File "D:\Anaconda3\lib\tkinter\__init__.py", line 1705, in __call__ return self.func(*args) File "C:/Users/SakataWoolley/Desktop/Hard Disk GUI/HDDApp3.py", line 112, in <lambda> button = tk.Button(top_frame, text="Scan", command =lambda: [self.scan(controller)]) File "C:/Users/SakataWoolley/Desktop/Hard Disk GUI/HDDApp3.py", line 172, in scan directory_list = self.controller.directory_list File "D:\Anaconda3\lib\tkinter\__init__.py", line 2101, in __getattr__ return getattr(self.tk, attr) AttributeError: '_tkinter.tkapp' object has no attribute 'directory_list'
Kindly let me know how to go about this. Please note the line number mentioned in the error and the code that I posted may not match but should be close by.
Reply
#4
I might be wrong here because I have been dealing with the PyQt5 QThread but I am going to guess that the Python Thread should not be sub-classed either so you might want to check that out as well.
Reply


Forum Jump:

User Panel Messages

Announcements
Announcement #1 8/1/2020
Announcement #2 8/2/2020
Announcement #3 8/6/2020