class NodeTree: def __init__(self): self._client = KazooClient(hosts='localhost:2181') # self._client = KazooClient(hosts='10.128.62.34:2181,10.128.62.34:2182,10.128.62.34:2183') self._client.start() self._selected_node = None self._win = None self._root = None self._tree_canvas = None self._tree_frame_id = None self.init_ui() self.init_tree() self._count = 0 # self.treeify() print(self._count) self._win.mainloop() def init_ui(self): self._win = tk.Tk() self._win.title("zk-client") self._win.geometry('900x700+500+300') self._win.resizable(height=False, width=False) tree_wrapper = tk.Frame(self._win, bg='red', width=300) tree_wrapper.pack(side=tk.LEFT, fill=tk.Y) canvas = self._tree_canvas = tk.Canvas(tree_wrapper, width=300, height=700, scrollregion=(0, 0, 300, 700), bg='gray') # 创建canvas canvas.place(x=0, y=0) # 放置canvas的位置 frame = tk.Frame(canvas) # 把frame放在canvas里 # frame.place(width=180, height=600) # frame的长宽,和canvas差不多的 vbar = Scrollbar(canvas, orient=tk.VERTICAL) # 竖直滚动条 vbar.place(x=281, width=20, height=700) vbar.configure(command=canvas.yview) hbar = Scrollbar(canvas, orient=tk.HORIZONTAL) # 水平滚动条 hbar.place(x=0, y=680, width=280, height=20) hbar.configure(command=canvas.xview) canvas.config(xscrollcommand=hbar.set, yscrollcommand=vbar.set) # 设置 canvas.bind_all( "<MouseWheel>", lambda event: canvas.yview_scroll( int(-1 * (event.delta / 120)), "units")) self._tree_frame_id = canvas.create_window( 0, 0, window=frame, anchor='nw') # create_window) self._root = Treeview(frame) # self._root.pack(expand=True, fill=tk.BOTH) # self._root.bind("<Button-1>", self.clear_pre_selected) # self._root.bind("<< TreeviewClose>>", self.clear_pre_selected) self._root.bind("<<TreeviewOpen>>", self.open_node) def adjust_size(self): self._root.update() width = self._root.winfo_width() height = self._root.winfo_height() print(width, height) self._tree_canvas.itemconfigure(self._tree_frame_id, height=height) # 设定窗口tv_frame的高度 self._tree_canvas.config(scrollregion=(0, 0, width, height)) self._tree_canvas.config( scrollregion=self._tree_canvas.bbox("all")) # 滚动指定的范围 self._tree_canvas.config(height=height) pass def init_tree(self): tops = self._client.get_children("/") for index, top in enumerate(tops): self._root.insert('', index, text=top) children = self._client.get_children("/" + top + "/") if children is not None and len(children) > 0: pass def clear_pre_selected(self, event): focus = self._root.focus() if focus == '': return children = self._root.get_children(focus) for child in children: self._root.delete(child) def open_node(self, event): iid = self._root.focus() path = self.get_current_path(iid) children = self._client.get_children(path) for index, p in enumerate(children): self._root.insert(iid, index, text=p) self.adjust_size() def get_current_path(self, item): curr_item = self._root.item(item) pid = self._root.parent(item) if pid == '': return '/' + curr_item['text'] + "/" else: return self.get_current_path(pid) + curr_item['text'] + "/" def treeify(self): self._client.start() self.treeify_recursive("/", parent='', index=0) def treeify_recursive(self, path, parent, index): self._count += 1 cd = self._client.get_children(path=path) if cd and len(cd) > 0: for i, c in enumerate(cd): tmp_path = path + c + '/' tc = self._client.get_children(tmp_path) if tc and len(tc) > 0: insert = self._root.insert(parent, i, text=c) self.treeify_recursive(path=tmp_path, parent=insert, index=i) else: self._root.insert(parent, i, text=c)
class SearchBox(Frame): '''A Treeview widget on top, Entry on bottom, using queues for interaction with outside functions''' def __init__(self, parent=None, db=DB, fdict={}): Frame.__init__(self, parent) self.Tests = Tests self.db = db self.fdict = dbm.open(self.db, 'c') #self.fdict['**']='' self.db_update = False #will hold the query to be processed self.query = None self.drives = self.get_drives() self.start_func = StartFunc( ) #platform dependent double-click response self.results = iter( ()) #initiating query results as an empty generator self.total_width = self.winfo_screenwidth( ) #to adjust column widths relative to screen size #Remember scorch mode self.scorch = False self.encoding = ENCODING #for scorch mode #self.keylist=self.fdict.keys() self.keylist_index = 0 self.keylist_counter = 0 #keystroke indicator self.counter = 0 #queues for passing search queries and results self.query_queue = Queue() self.result_queue = Queue() #for usage by db generating function self.dbinit_queue = Queue() #--Search Results panel at top self.panel = Treeview(columns=('Path', 'Size', 'Date')) self.panel.pack(expand=True, fill='both') self.panel.heading('#0', text='Name') self.panel.heading(0, text='Path') self.panel.heading(1, text='Size') self.panel.heading(2, text='Date Modified') #--Starting geometry of the search panel try: #start maximized self.panel.master.attributes('-zoomed', 1) except: self.panel.master.state('zoomed') self.panel_width = self.panel.winfo_width() #Name - 2/5-----Path - 2/5-----Size -1/25-----Date -4/25 # '#0' is the 'Name' column self.panel.column('#0', width=int(self.total_width * 0.4)) self.panel.column('Path', width=int(self.total_width * 0.4)) self.panel.column('Size', width=int(self.total_width * 0.06)) self.panel.column('Date', width=int(self.total_width * 0.14)) #--Panel font, style self.font = Font(family='Helvetica', size=11) '''TkDefaultFont - {'family': 'Segoe UI', 'overstrike': 0, 'size': 9, 'slant': 'roman', 'underline': 0, 'weight': normal'}''' self.style = Style() #linespace - adjust the row height to the font, doesn't happen on its own in tkinter self.style.configure('SearchBox.Treeview', font=self.font, rowheight=self.font.metrics('linespace')) self.panel.config(style='SearchBox.Treeview') #alternating background colors self.panel.tag_configure('color1', background='gray85') #, foreground='white') self.panel.tag_configure('color2', background='gray90') #, foreground='white') #'dark sea green', 'wheat3', 'black' #--App title and icon, currently transparent self.panel.master.title('Jiffy') self.icon = PhotoImage(height=16, width=16) self.icon.blank() #transparent icon, works on all but Py35/Win #loading the transparent icon. black on Py35/Win try: self.master.wm_iconphoto('True', self.icon) except: #For some reason this jammed Python 3.5 with Tk 8.6 on Windows self.tk.call('wm', 'iconphoto', self.master._w, self.icon) #--A string variable to monitor input to the Entry box self.entry_var = StringVar() #self.entry_var.set('Type to search. [F5 - Refresh Database]. [F12 - Scorch Mode]') # [Ctrl-O - Options]. [Ctrl-I - Info] self.entry_var.trace('w', self.update_query) #--Entry line on the bottom self.entry_box = Entry(textvariable=self.entry_var) #keep it as a single line on all window sizes self.entry_box.pack(side='bottom', fill='x') #--Widget Bindings #self.master.bind('<Ctrl-Z>', self.quit) #alternative to Alt-F4 self.master.bind('<Control-equal>', self.scaleup) self.master.bind('<Control-Button-4>', self.scaleup) self.master.bind('<Control-minus>', self.scaledown) self.master.bind('<Control-Button-5>', self.scaledown) self.master.bind('<Control-MouseWheel>', self.scale_mouse) self.panel.bind('<Double-1>', self.doubleclick) self.panel.bind('<Return>', self.doubleclick) #allow scrolling and typing without switching focus self.entry_box.bind('<MouseWheel>', self.scroll_from_entry) self.entry_box.bind('<Button-4>', self.scroll_from_entry) self.entry_box.bind('<Button-5>', self.scroll_from_entry) self.master.bind('<F5>', self.make_database) #self.master.bind('<F12>', self.scorch_mode) #--Starting up with entry box active self.entry_box.focus_set() #--Generating a starting message based on existence of a database if not self.fdict: self.panel.insert('', 'end', text='No cache database found', values=('Hit F5 to generate database', )) else: self.panel.insert( '', 'end', text='Type to search. [F5 - Refresh Database]', values=('[Ctrl - +/-/MouseWheel - Adjust font size]', )) # [Ctrl-O - Options]. [Ctrl-I - Info] #self.panel.insert('', 'end', text='Scorch Mode is faster but uses more memory', values=('Loads the entire database into RAM',)) self.update_searchbox() #Initializing the query managing function in a separate thread (upgrade to pool?) thread.start_new_thread( TMakeSearch, (self.fdict, self.query_queue, self.result_queue)) ##GUI functionality-------------------------------------------------------- #O change to initiation from parameter to SearchBox for more modularity def get_drives(event): return GetDrives() def scroll_from_entry(self, event): '''Scroll results without deactivating entry box, called from entry_box''' self.panel.yview_scroll(1, 'units') def scaleup(self, event): '''The Treeview widget won't auto-adjust the row height, so requires manual resetting upon font changing''' self.font['size'] += 1 self.style.configure('SearchBox.Treeview', rowheight=self.font.metrics('linespace') + 1) def scaledown(self, event): self.font['size'] -= 1 self.style.configure('SearchBox.Treeview', rowheight=self.font.metrics('linespace') + 1) def scale_mouse(self, event): self.scaleup(event) if event.delta > 0 else self.scaledown(event) def doubleclick(self, event): '''Invoke default app on double-click or Enter''' #getting file/folder name and removing '[' and ']' for folders selection = self.panel.item(self.panel.focus()) filename = selection['text'] #remove folder indicating square brackets if filename[0] == '[': filename = filename[1:-2] #SPLIT_TOKEN='\\' if 'win' in sys.platform else '/' full_path = selection['values'][0] + SPLIT_TOKEN + filename self.start_func(full_path) def quit(self, event): '''Currently Alt-F4 exits program, in case I want to add more shortcuts. Also add thread closing management here''' self.master.destroy() ##Cheese: update_database()->is_sb_generated(), trace_results(), update_query() def make_database(self, event): '''Using a thread to generate the dictionary to prevent GUI freezing''' #* dbm might not be thread safe - best might be to restart TMakeSearch self.gtime = time() # for testing self.entry_var.set('Updating Database') self.entry_box.icursor('end') #Resulting dicitionay will be passed via dbinint_queue thread.start_new_thread(RecursiveCreateDict, (self.drives, self.dbinit_queue, self.fdict)) #Wait for the dictionary to be generated self.is_db_generated() def is_db_generated(self): '''Update database if available or sleep and try again''' if not self.dbinit_queue.empty(): #A new dictionary was passed #retrieving new dict self.newdict, self.unsearched = self.dbinit_queue.get() #Messaging TMakeSearch to stop querying the dictionary self.db_update = True self.query_queue.put(None) sleep(0.11) #TMakeSearch takes 0.1s naps. Check further ''' if whichdb(self.db) == ('dbhash'): '''For dumbdbm, this jams the app, as does manual updating. it's not dumb, it's just not worthy''' self.fdict.update(self.newdict) else: for key in self.newdict: self.fdict[key] = self.newdict[key] print('fdict is created') self.db_update = False #save new database self.fdict.sync() print('fdict synced') #Open a new TMakeSearch with the updated database #thread.start_new_thread(TMakeSearch, (self.fdict, self.query_queue, self.result_queue)) #Cleaning up self.newdict.clear() self.newdict = None self.gtime = time() - self.gtime #to read about {}.format #also, a label may be simpler self.entry_var.set('Database generation time- ' + str(self.gtime) + 's. Type to search. [F5 - Refresh Database]') #Pass a signal to close TMakeSearch, then reopen it self.query_queue.put(True) thread.start_new_thread( TMakeSearch, (self.fdict, self.query_queue, self.result_queue)) self.entry_box.icursor(0) #self.loading.destroy() self.panel.delete(*self.panel.get_children()) self.panel.insert( '', 0, text='Scorch Mode is faster but uses more memory', values=('Loads database into RAM', )) #self.keylist=fdict.keys() #for scorch mode self.counter = 0 #self.IS_1ST_PRESS=True #for testing #print time()-self.start #print self.dict_size() else: self.after(100, self.is_db_generated) def update_searchbox(self): '''Update GUI with new result batches ''' self.even = True #for splitting size and date from the keys self.separator = ' * '.encode(self.encoding) while not self.result_queue.empty(): qresult = self.result_queue.get() #print ('is_batch_recieved:', qresult) #if qcounter==self.counter: #currently assuming results will arrive by querying order #break try: #if nothing in queue this will raise an error, saves a preemptive if clause self.results, self.is_new = qresult if Tests.is_batch_recieved: print('is_batch_recieved:', self.results) except: pass #no new results if Tests.is_batch_recieved: print('is_batch_recieved: no new results') else: #if self.panel.get_children()!=(): #results for a newer query, erase old results if self.is_new: self.panel.delete(*self.panel.get_children()) for key in self.results: try: name, size, date = key.decode(self.encoding).split(u'*') #name, size, date=key.split(self.separator) if Tests.is_result_parsed: print(name) except: if Tests.is_result_parsed: print('parsing issue with', key) else: path = self.fdict[key].decode(self.encoding) '''if 'win' in sys.platform and top[0] is u'/': top=u'C:\\'+top[1:] ''' color = 'color1' if self.even else 'color2' self.even = not self.even self.panel.insert('', 'end', text=name, values=(path, size, date), tags=(color, )) self.after(60, self.update_searchbox) def update_query(self, x=None, y=None, z=None): '''Invoked by StringVar().trace() method, which passes 3 arguments that are honorably ditched ''' #Deactivate while switching dictionaries if self.db_update: pass #Cleaning up for 1st keystroke or after a message in the Entry box if not self.counter: '''Entry box needs to be cleaned, the new char put in and the cursor placed after it''' #get&set the 1st search char. user may've changed cursor location b4 typing self.entry_var.set( self.entry_var.get()[self.entry_box.index(INSERT) - 1]) #move cursor after the first char self.entry_box.icursor(1) #counter goes up either way self.counter += 1 self.query = self.entry_var.get() self.query_queue.put(self.query) if Tests.is_query_sent: print(self.query) print(self.counter) ##Not in use ---------------------------------------------------------------- def trace_query(self): '''If I opt for periodically checking the StringVar''' if self.counter: #when counter=0 there's a message/notification in the entry box if self.query != self.entry_var.get(): self.query = self.entry_var.get() self.query_queue.put(self.query) self.after(100, self.trace_query) def trace_and_update(self): ''' In-GUI implementation of query searching, uses a list for iterating over the keys''' '''works smoother when results are generated quickly, but with sparse results GUI becomes unresponsive for short whiles. Relevant only if results are guaranteed to be generated swiftly''' #print self.query #if new query, resetting search parameters and GUI if self.query != self.entry_var.get(): self.keylist_counter = 0 self.query = self.entry_var.get() self.search_list = self.query.lower().split() self.panel.delete(*self.panel.get_children()) self.insertion_counter = 0 self.keylist_index = self.keylist_counter for key in self.keylist[self.keylist_index:]: filename = key.split('*')[0].lower() #If a match, parse and add to the Treeview if self.all(token in filename for token in self.search_list): name, size, date = key.split('*') self.panel.insert('', 'end', text=name, values=(self.fdict[key], size, date)) self.insertion_counter += 1 self.keylist_counter += 1 if self.insertion_counter >= self.INSERTIONS_PER_CYCLE: #50 ##or not dict_counter: break #nap time self.after(60, self.trace_and_update)