class ScrollableFrame(Frame): def __init__(self, root, container, *args, **kwargs): super().__init__(container, *args, **kwargs) self.numberOfChilds = 0 self._on_end = lambda: None self.canvas = Canvas(self, *args, **kwargs) self.scrollbar = Scrollbar(self, orient="vertical", command=self.canvas.yview) self.scrollable_frame = Frame(self.canvas, *args, **kwargs) self.scrollable_frame.bind( "<Configure>", lambda e: self.canvas.configure(scrollregion=self. canvas.bbox("all"))) self.canvas.create_window((0, 0), window=self.scrollable_frame, anchor="nw") self.canvas.configure(yscrollcommand=self.scrollbar.set) self.canvas.pack(side="left", fill="both", expand=True) self.scrollbar.pack(side="right", fill="y") root.bind("<MouseWheel>", lambda event: self._on_mousewheel(event)) ''' setOnEnd => this method sets the event OnEnd, it recives a lambda _on_end and by default is None @_on_end => is the lambda funcion for when the mouseWheel turn to 95% ''' def setOnEnd(self, _on_end): self._on_end = _on_end ''' _on_mousewheel => this method is an event reciver for when mouseWheel changes ''' def _on_mousewheel(self, event): self.canvas.yview_scroll(int(-event.delta / 120), "units") if self.scrollbar.get()[1] > 0.95: self._on_end() def adicionarListaFrames(self, listaFrames): if self.scrollable_frame == len(listaFrames): return listaFramesAAdicionarados = listaFrames[self.numberOfChilds:self. numberOfChilds + 10] for frameAAdicionar in listaFramesAAdicionarados: frameAAdicionar.grid(padx=5) self.numberOfChilds = self.numberOfChilds + 10
class DataViewer: ''' DataViewer ====================== David Miller, 2020 The University of Sheffield 2020 Wrapper GUI for a Matplotlib Figure showing the data given on creation. Called by HDF5Viewer when the user double clicks on a dataset. This GUI is designed a means of performing basic inspections of data stored in HDF5 files. If users want to perform something intensive or particular custom, they are advised to do so elsewhere. On creation, the DataViewer opens up the file specified by filename and accesses the dataset specified by the path dataName. It then decides how to plot the data based on the number of dimensions in the dataset. The used plots are as follows using their respective default options: |No. dims | Plots | |---------|----------| | 1 | Line | | 2 | Contourf | | 3 | Contourf | In the case of three dimensions, a scrollbar is added on top of the plot and provides a means for the user to select which 2D slice of the 3D dataset to show. The scrollbar only supports drag operations. Any higher dimensional data is ignored. The title above the Figure displays the name of the dataset and which index, if any is being displayed. Methods ------------------------- on_key_press(event): Handler for key presses used on the Matploblib canvas scroll_data(self,*args): Handler for changing which slice of a 3D dataset is displayed. A new data index is chosen based on where the scrollbar cursor is dragged to. The index is calculated by multiplying the scrollbar positon [0,1] by the number of frames in the dataset and then converted to an integer. Updates the title and canvas on exit. Sets the scrollbar position to where the user left it. Currently this only has support for clicking and dragging the scrollbar cursor. Other operations are ignored. ''' # using filename and name of dataset # create a figure and display the data def __init__(self,master,dataName,filename): self.master = master # save creation options self.dataName = dataName self.filename = filename # set title self.master.title("Data Viewer") # current colormap, default self.curr_cmap = getattr(matplotlib.cm,matplotlib.rcParams['image.cmap']) # currnt line color self.curr_lcol = matplotlib.rcParams['axes.prop_cycle'].by_key()['color'][0] ## menu bar for customisation # root menu menu = Menu(master) # options menu optsmenu = Menu(menu,tearoff=0) # label for graph self.title = StringVar() self.title.set(f'Displaying {dataName}') self.graph_title = Label(master,textvariable=self.title) self.graph_title.pack(side=TOP,pady=10,padx=10) # create figure self.fig = Figure(figsize=(5,5),dpi=100) self.axes = self.fig.add_subplot(111) # get data from dataset and plot data with h5py.File(filename,mode='r') as f: self.data_shape = f[dataName].shape # if the data is 1D, plot as line if len(self.data_shape)==1: self.axes.plot(f[dataName][()],self.curr_lcol) optsmenu.add_command(label="Set color",command=self.set_color) # if data is 2D, plot as filled contour elif len(self.data_shape)==2: self.axes.contourf(f[dataName][()],cmap=self.curr_cmap) optsmenu.add_command(label="Set colormap",command=self.set_colormap) # if data is 3D plot as contourf, but also add a scrollbar for navigation elif len(self.data_shape)==3: optsmenu.add_command(label="Set colormap",command=self.set_colormap) # create scroll bar for viewing different slices self.plot_scroll=Scrollbar(master,orient="horizontal",command=self.scroll_data) # add too gui self.plot_scroll.pack(side=TOP,fill=BOTH,expand=True) # plot first slice of data self.axes.contourf(f[dataName][:,:,0],cmap=self.curr_cmap) # create index for current depth index self.depth_index = 0 self.title.set(f"Displaying {dataName} [{self.depth_index}]") # add to root menu menu.add_cascade(label="Options",menu=optsmenu) self.master.config(menu=menu) # create canvas to render figure self.fig_canvas = FigureCanvasTkAgg(self.fig,self.master) # update result self.fig_canvas.draw() # update canvas to set position and expansion options self.fig_canvas.get_tk_widget().pack(side=TOP,fill=BOTH,expand=True) ## add matplotlib toolbar self.fig_toolbar = NavigationToolbar2Tk(self.fig_canvas,self.master) self.fig_toolbar.update() # add to gui. always one row below the canvas self.fig_canvas._tkcanvas.pack(side=TOP,fill=BOTH,expand=True) ## add key press handlers self.fig_canvas.mpl_connect("key_press_event",self.on_key_press) # ensure elements are expandable in grid num_cols,num_rows = master.grid_size() for c in range(num_cols): master.columnconfigure(c,weight=1) for r in range(num_rows): master.rowconfigure(r,weight=1) # finish any idle tasks and set the minimum size of the window to cur master.update_idletasks() master.after_idle(lambda: master.minsize(master.winfo_width(), master.winfo_height())) # handler for matplotlib keypress events def on_key_press(event): key_press_handler(event,self.fig_canvas,self.fig_toolbar) # handler for using the scrollbar to view slices of data def scroll_data(self,*args): #print(args) # if the user has dragged the scrollbar if args[0] == "moveto": # args is only one element in this case and is a number between 0 and 1 # 0 is left most position and 1 is right most position # the data index is calculated as this number times depth and converted to integer self.depth_index = int(float(args[1])*self.data_shape[2]) # if index exceeds dataset limits # correct it if self.depth_index>=self.data_shape[-1]: self.depth_index = self.data_shape[-1]-1 # set the scrollbar position to where the user dragged it to self.plot_scroll.set(float(args[1]),(self.depth_index+1)/self.data_shape[2]) # reopen file with h5py.File(self.filename,mode='r') as f: self.axes.contourf(f[self.dataName][:,:,self.depth_index],cmap=self.curr_cmap) # update canvas self.fig_canvas.draw() # update title self.title.set(f"Displaying {self.dataName} [{self.depth_index}]") def set_colormap(self): ch = ColormapChooser(self.master).show() if ch: self.curr_cmap = ch self.scroll_data("moveto",str(self.plot_scroll.get()[0])) def update_line(self): # remove first line from plot self.axes.lines.pop(0) with h5py.File(self.filename,mode='r') as f: self.axes.plot(f[self.dataName][()],self.curr_lcol) # update canvas self.fig_canvas.draw() def set_color(self): col = colorchooser.askcolor() if col[1]: self.curr_lcol = col[1] self.update_line()
width=canvasWidth, xscrollcommand=xscrollbar.set, yscrollcommand=yscrollbar.set) canvas.place(x=canvasxPosition, y=canvasyPosition) im = Image.open(imagePath.get()) im_width, im_height = im.size im = im.resize((int(im_width * imageMagnification.get()), int(im_height * imageMagnification.get())), Image.ANTIALIAS) photo = ImageTk.PhotoImage(im) image = canvas.create_image(0, 0, anchor=NW, image=photo) canvas.grid(row=0, column=0, sticky=N + S + E + W) canvas.config(scrollregion=canvas.bbox(ALL)) xscrollbar.config(command=canvas.xview) yscrollbar.config(command=canvas.yview) print(xscrollbar.get()) # ---- zoom function ---- zoomcycle = 0 zimg_id = None if operatingSystem == 2: def zoomer(event): global zoomcycle if (event.delta > 0): if zoomcycle != 4: zoomcycle += 1 elif (event.delta < 0): if zoomcycle != 0: zoomcycle -= 1 crop(event) print("zoooom")
class Gui: def __init__(self, root): global CONFIG, CURPATH self.root = root root.geometry( "%dx%d+0+0" % (round(root.winfo_screenwidth() * 0.8), round(root.winfo_screenheight() * 0.8))) #default window size 80% root.title('Chords Autoscroll 0.9b') root.iconphoto( True, PhotoImage(file=os.path.join(CURPATH, "media", "icon.png"))) root.option_add("*Font", "Helvetica 12") #default font root.protocol("WM_DELETE_WINDOW", self.onClose) #general vars if CONFIG.get("recent"): self.file = FileManager(os.path.dirname(CONFIG.get("recent")[0])) else: self.file = FileManager() self.speed = IntVar() self.speed.set(30) self.runningScroll = False self.settingsPattern = re.compile( '\n\nChordsAutoscrollSettings:(\{.*\})') self.settings = {} #menu self.menubar = Menu(self.root) self.filemenu = Menu(self.menubar, tearoff=0) self.menubar.add_cascade(label="File", menu=self.filemenu) self.filemenu.add_command(label="Open...", command=lambda: self.openNewFile()) self.filemenu.add_separator() self.filemenu.add_command(label="Save (Ctrl+S)", command=lambda: self.saveFile(True)) self.filemenu.add_command(label="Save as...", command=lambda: self.saveFile()) self.filemenu.add_separator() self.filemenu.add_command(label="Close", command=lambda: self.closeFile()) #recent files (I should update this at runtime...) self.filemenu.add_separator() self.recent = Menu(self.filemenu, tearoff=0) self.filemenu.add_cascade(label="Recent files", menu=self.recent) if CONFIG.get("recent") and len(CONFIG.get("recent")) > 0: for n, p in enumerate(CONFIG.get("recent")): self.recent.add_command( label=str(n + 1) + ": " + str(p), command=lambda p=p: self.openNewFile(str(p))) self.root.config(menu=self.menubar) #root frame froot = Frame(root) froot.pack(side=c.TOP, pady=5, padx=5, fill=c.BOTH, expand=1) #main frame fmain = Frame(froot) fmain.pack(side=c.TOP, fill=c.BOTH, expand=1, anchor=c.N) f1 = Frame(fmain) #text window frame f1.pack(side=c.LEFT, fill=c.BOTH, expand=1) self.txtMain = Text( f1, height=1, width=1, font=("Courier", 14), undo=True) #maybe we can set a DARK MODE to help reading self.txtMain.pack(side=c.LEFT, fill=c.BOTH, expand=1) self.scrollbar = Scrollbar(f1, command=self.txtMain.yview) self.scrollbar.pack(side=c.LEFT, fill=c.Y) self.txtMain.config(yscrollcommand=self.scrollbar.set) f2 = Frame(fmain, width=100) #right buttons panel f2.pack(side=c.RIGHT, anchor=c.N, padx=5, fill=c.X) self.btnPlay = Button(f2, text="Play", relief=c.RAISED, font=(None, 0, "bold")) self.btnPlay.pack(side=c.TOP, padx=5, pady=5, fill=c.BOTH, expand=1, ipady=6) self.btnPlay['command'] = lambda: self.autoscroll() f2_1 = Frame(f2) #child frame SPEED CONTROL f2_1.pack(side=c.TOP, anchor=c.N, pady=(10, 0), fill=c.X) Label(f2_1, text="Speed:", font=("*", 8), anchor=c.E).pack(side=c.LEFT, padx=(2, 0)) Label(f2_1, font=("*", 8), anchor=c.W, textvariable=self.speed).pack(side=c.LEFT, padx=(0, 2)) self.btnSpeedUp = Button(f2, text="+") self.btnSpeedUp.pack(side=c.TOP, padx=5, pady=2, fill=c.BOTH, ipady=6) self.btnSpeedUp['command'] = lambda: self.speedAdd(1) self.btnSpeedDown = Button(f2, text="-") self.btnSpeedDown.pack(side=c.TOP, padx=5, pady=(2, 5), fill=c.BOTH, ipady=6) self.btnSpeedDown['command'] = lambda: self.speedAdd(-1) f2_2 = Frame(f2, width=5) #child frame FONT SIZE #f2_2.pack_propagate(0) f2_2.pack(side=c.TOP, anchor=c.N, pady=(10, 0), fill=c.X) self.btnTextUp = Button(f2, text="A", font=(None, 18)) self.btnTextUp.pack(side=c.TOP, padx=5, pady=2, fill=c.BOTH, ipady=0) self.btnTextUp['command'] = lambda: self.changeFontSize(1) self.btnTextDown = Button(f2, text="A", font=(None, 10)) self.btnTextDown.pack(side=c.TOP, padx=5, pady=(2, 5), fill=c.BOTH, ipady=8) self.btnTextDown['command'] = lambda: self.changeFontSize(-1) #credits f4 = Frame(root) f4.pack(side=c.BOTTOM, pady=0, padx=0, fill=c.X, anchor=c.S) Label( f4, text= "© 2017 Pasquale Lafiosca. Distributed under the terms of the Apache License 2.0.", fg='#111111', bg='#BBBBBB', font=('', 9), bd=0, padx=10).pack(fill=c.X, ipady=2, ipadx=2) #shortcuts root.bind('<Control-s>', lambda e: self.saveFile(True)) root.bind('<Control-S>', lambda e: self.saveFile(True)) def startStop(e): if self.runningScroll: self.stopAutoscroll() else: self.autoscroll() root.bind('<Control-space>', startStop) def openNewFile(self, path=None): global CONFIG filename = None if not path: filename = filedialog.askopenfilename( initialdir=self.file.getLastUsedDir(), filetypes=[("Text files", "*.*")], title="Select a text file to open") else: if os.path.isfile(path): filename = path else: messagebox.showwarning("Not found", "Selected file was not found. Sorry.") if filename: self.closeFile() self.recent.delete(0, len(CONFIG.get("recent")) - 1) CONFIG.insertRecentFile(filename) for n, p in enumerate(CONFIG.get("recent")): self.recent.add_command( label=str(n + 1) + ": " + str(p), command=lambda p=p: self.openNewFile(str(p))) self.file.open(filename) self.txtMain.delete(1.0, c.END) content = self.file.getContent() #Settings m = re.search(self.settingsPattern, content) if m and m.group(1): try: self.settings = json.loads( m.group(1)) # Loads settings from file self.speed.set(self.settings["Speed"]) self._setFontSize(self.settings["Size"]) except: messagebox.showwarning("Warning", "Cannot load setting data. Sorry.") self._setSettingsData() else: self._setSettingsData() content = re.sub( self.settingsPattern, '', content) # Remove settings string before write on screen self.txtMain.insert(1.0, content) def _setSettingsData(self): self.settings = { "Speed": self.speed.get(), "Size": self._getFontSize() } def _settingsChanged(self): if "Speed" in self.settings and "Size" in self.settings and ( self.settings["Speed"] != self.speed.get() or self.settings["Size"] != self._getFontSize()): return True else: return False def saveFile(self, current=False): global CONFIG if current: filename = self.file.getLastFile() if not current or not filename: filename = filedialog.asksaveasfilename( initialdir=self.file.getLastUsedDir(), initialfile=self.file.getLastFile(), filetypes=[("Text files", "*.txt")], title="Select destionation", defaultextension=".txt") if filename: CONFIG.insertRecentFile(filename) self.file.open(filename) self._setSettingsData() self.file.writeContent( self.txtMain.get(1.0, c.END)[:-1] + "\n\nChordsAutoscrollSettings:" + json.dumps(self.settings)) def closeFile(self): if not self.txtMain.get(1.0, c.END)[:-1]: # Empty view return True if self.file.hasChanged( hashlib.md5( (self.txtMain.get(1.0, c.END)[:-1] + "\n\nChordsAutoscrollSettings:" + json.dumps(self.settings) ).encode()).hexdigest()) or self._settingsChanged(): if messagebox.askyesno( "Save changes", "Current document has been modified. Do you want to save changes?" ): self.saveFile() self.txtMain.delete(1.0, c.END) self.file.close() return True def mainloop(self): self.root.mainloop() def onClose(self): if messagebox.askokcancel("Quit", "Do you want to quit?"): self.closeFile() self.root.destroy() def _getFontSize(self): return font.Font(font=self.txtMain["font"])["size"] def _setFontSize(self, newsize): f = font.Font(font=self.txtMain["font"]) f.config(size=newsize) self.txtMain.config(font=f) self.txtMain.update_idletasks() def changeFontSize(self, a): f = font.Font(font=self.txtMain["font"]) newsize = f["size"] + a if (newsize < 8 or newsize > 72): #limits return f.config(size=newsize) self.txtMain.config(font=f) self.txtMain.update_idletasks() def autoscroll(self): if not self.runningScroll and threading.active_count( ) < 2: # Check to avoid multiple scrolling threads if (float(self.scrollbar.get()[1]) == 1 ): #if we are at the end, let's start from beginning self.txtMain.see(1.0) self.runningScroll = True #INITIAL DELAY self.txtMain.mark_set("initialDelay", 1.0) self.txtMain.mark_gravity("initialDelay", c.RIGHT) self.txtMain.insert("initialDelay", os.linesep * 20) # SET CONSTANT HERE self.txtMain.config(state=c.DISABLED) self.txtMain.update_idletasks() threading.Thread(target=self.autoscroll_callback, name="ScrollingThread", daemon=True).start() self.btnPlay.config(text="Stop", relief=c.SUNKEN, command=lambda: self.stopAutoscroll()) self.btnPlay.update_idletasks() def autoscroll_callback(self): while (float(self.scrollbar.get()[1]) < 1 and self.runningScroll): self.txtMain.yview(c.SCROLL, 1, c.UNITS) end = time.time() + 60 / self.speed.get() while (time.time() < end and self.runningScroll): # trick to stop immediately time.sleep(.1) if self.runningScroll: self.stopAutoscroll() def stopAutoscroll(self): self.runningScroll = False self.txtMain.config(state=c.NORMAL) self.txtMain.delete(1.0, "initialDelay") self.txtMain.mark_unset("initialDelay") self.txtMain.update_idletasks() self.btnPlay.config(text="Play", relief=c.RAISED, command=lambda: self.autoscroll()) self.btnPlay.update_idletasks() def speedAdd(self, n): n = self.speed.get() + n if (n > 0 and n < 1000): self.speed.set(n)
class Chatbox(object): def __init__(self, master, my_nick=None, command=None, topic=None, entry_controls=None, maximum_lines=None, timestamp_template=None, scrollbar_background=None, scrollbar_troughcolor=None, history_background=None, history_font=None, history_padx=None, history_pady=None, history_width=None, entry_font=None, entry_background=None, entry_foreground=None, label_template=u"{nick}", label_font=None, logging_file=None, tags=None): self.interior = Frame(master, class_="Chatbox") self._command = command self._is_empty = True self._maximum_lines = maximum_lines self._timestamp_template = timestamp_template self._command = command self._label_template = label_template self._logging_file = logging_file if logging_file is None: self._log = None else: try: self._log = open(logging_file, "r") except: self._log = None top_frame = Frame(self.interior, class_="Top") top_frame.pack(expand=True, fill=BOTH) self._textarea = Text(top_frame, state=DISABLED) self._vsb = Scrollbar(top_frame, takefocus=0, command=self._textarea.yview) self._vsb.pack(side=RIGHT, fill=Y) self._textarea.pack(side=RIGHT, expand=YES, fill=BOTH) self._textarea["yscrollcommand"] = self._vsb.set entry_frame = Frame(self.interior, class_="Chatbox_Entry") entry_frame.pack(fill=X, anchor=N) if entry_controls is not None: controls_frame = Frame(entry_frame, class_="Controls") controls_frame.pack(fill=X) entry_controls(controls_frame, chatbox=self) bottom_of_entry_frame = Frame(entry_frame) self._entry_label = Label(bottom_of_entry_frame) self._entry = Entry(bottom_of_entry_frame) else: self._entry_label = Label(entry_frame) self._entry = Entry(entry_frame) self._entry.pack(side=LEFT, expand=YES, fill=X) self._entry.bind("<Return>", self._on_message_sent) self._entry.focus() if history_background: self._textarea.configure(background=history_background) if history_font: self._textarea.configure(font=history_font) if history_padx: self._textarea.configure(padx=history_padx) if history_width: self._textarea.configure(width=history_width) if history_pady: self._textarea.configure(pady=history_pady) if scrollbar_background: self._vsb.configure(background=scrollbar_background) if scrollbar_troughcolor: self._vsb.configure(troughcolor=scrollbar_troughcolor) if entry_font: self._entry.configure(font=entry_font) if entry_background: self._entry.configure(background=entry_background) if entry_foreground: self._entry.configure(foreground=entry_foreground) if label_font: self._entry_label.configure(font=label_font) if tags: for tag, tag_config in tags.items(): self._textarea.tag_config(tag, **tag_config) self.set_nick(my_nick) @property def topic(self): return @topic.setter def topic(self, topic): return def focus_entry(self): self._entry.focus() def bind_entry(self, event, handler): self._entry.bind(event, handler) def bind_textarea(self, event, handler): self._textarea.bind(event, handler) def bind_tag(self, tagName, sequence, func, add=None): self._textarea.tag_bind(tagName, sequence, func, add=add) def focus(self): self._entry.focus() def user_message(self, nick, content): if self._timestamp_template is None: self._write((u"%s:" % nick, "nick"), " ", (content, "user_message")) else: timestamp = datetime.datetime.now().strftime( self._timestamp_template) self._write((timestamp, "timestamp"), " ", (u"%s:" % nick, "nick"), " ", (content, "user_message")) def notification_message(self, content, tag=None): if tag is None: tag = "notification" self._write((content, tag)) notification = notification_message def notification_of_private_message(self, content, from_, to): if self._timestamp_template is None: self.notification_message( u"{from_} -> {to}: {content}".format(from_=from_, to=to, content=content), "notification_of_private_message") else: timestamp = datetime.datetime.now().strftime( self._timestamp_template) self.notification_message( u"{timestamp} {from_} -> {to}: {content}".format( timestamp=timestamp, from_=from_, to=to, content=content), "notification_of_private_message") def new_message(self, message): if isinstance(message, User_Message): self.user_message(message.content, message.nick) elif isinstance(message, Notification_Message): self.notification(message.content, message.tag) elif isinstance(message, Notification_Of_Private_Message): self.notification_of_private_message(message.from_, message.to, message.content) else: raise Exception("Bad message") def tag(self, tag_name, **kwargs): self._textarea.tag_config(tag_name, **kwargs) def clear(self): self._is_empty = True self._textarea.delete('1.0', END) @property def logging_file(self): return self._logging_file def send(self, content): if self._my_nick is None: raise Exception("Nick not set") self.user_message(self._my_nick, content) def _filter_text(self, text): return "".join(ch for ch in text if ch <= u"\uFFFF") def _write(self, *args): if len(args) == 0: return relative_position_of_scrollbar = self._vsb.get()[1] self._textarea.config(state=NORMAL) if self._is_empty: self._is_empty = False else: self._textarea.insert(END, "\n") if self._log is not None: self._log.write("\n") for arg in args: if isinstance(arg, tuple): text, tag = arg # Parsing not allowed characters text = self._filter_text(text) self._textarea.insert(END, text, tag) else: text = arg text = self._filter_text(text) self._textarea.insert(END, text) if self._log is not None: self._log.write(text) if self._maximum_lines is not None: start_line = int(self._textarea.index('end-1c').split('.') [0]) - self._maximum_lines if lines_to_delete >= 1: self._textarea.delete('%s.0' % start_line, END) self._textarea.config(state=DISABLED) if relative_position_of_scrollbar == 1: self._textarea.yview_moveto(1) def _on_message_sent(self, event): message = self._entry.get() self._entry.delete(0, END) self.send(message) if self._command: self._command(message) def set_nick(self, my_nick): self._my_nick = my_nick if my_nick: text = self._label_template.format(nick=my_nick) self._entry_label["text"] = text self._entry_label.pack(side=LEFT, padx=(5, 5), before=self._entry) else: self._entry_label.pack_forget()
class ScrollableFrame(Frame): """ A wrapper for the Frame class to make it scrollable. To make use of this fuctionality, pack anything into the .frame Frame of this object """ def __init__(self, master, *args, **kwargs): self.master = master Frame.__init__(self, self.master, *args, **kwargs) # Block resizing. This is required because we bind the resizing code to # a <Configure> tag. This would result in one resizing to a specific # size to call self.configure_view again but with no arguments. self.block_resize = False # cached canvas view dimensions self._view_dimensions = None self.grid(sticky='nsew') self.grid_rowconfigure(0, weight=1) self.grid_columnconfigure(0, weight=1) self.vsb = Scrollbar(self, orient='vertical') self.hsb = Scrollbar(self, orient='horizontal') self.canvas = Canvas(self, bd=0, bg=OSCONST.CANVAS_BG, highlightthickness=0) self.canvas.grid(row=0, column=0, sticky='nsew') # configure scroll bars self.hsb.config(command=self.canvas.xview) self.canvas.config(xscrollcommand=self.hsb.set) self.hsb.grid(row=1, column=0, sticky='ew') self.vsb.config(command=self.canvas.yview) self.canvas.config(yscrollcommand=self.vsb.set) self.vsb.grid(row=0, column=1, sticky='ns') # everything will go in this frame self.frame = Frame(self.canvas) self.frame.grid(row=0, column=0, sticky='nsew') self.canvas.create_window((0, 0), window=self.frame, anchor="nw") self.hsb.config(command=self.canvas.xview) self.vsb.config(command=self.canvas.yview) self.bind("<Configure>", self.configure_view) # mouse wheel scroll bindings c/o Mikhail. T on stackexchange: # https://stackoverflow.com/a/37858368 self.bind('<Enter>', self._bind_to_mousewheel) self.bind('<Leave>', self._unbind_to_mousewheel) #region public methods def configure_view(self, event=None, move_to_bottom=False, max_size=(None, None), resize_canvas='xy', needs_update=False): """ Configure the size of the scrollable frame. Parameters ---------- move_to_bottom : bool Whether or not to move the scroll bar all the way to the bottom. max_size : tuple of int's Maximum size that the scrollable canvas can be. This is a tuple of length 2. (X, Y). resize_canvas : str The directions along which the scrollable canvas should be stretched. One of `'x'`, `'y'`, or `'xy'` for the x-direction, y-direction, or both respectively. """ if not needs_update: return x_size = None y_size = None bbox = self.canvas.bbox(ALL) if 'x' in resize_canvas: # find the new x size to draw if max_size[0] is not None: x_size = max(min(max_size[0], bbox[2]), self._view_dimensions[0]) else: x_size = bbox[2] if 'y' in resize_canvas: # find the new y size to draw if max_size[1] is not None: y_size = max(min(max_size[1], bbox[3]), self._view_dimensions[1]) else: y_size = bbox[3] self._resize_canvas(x_size, y_size) xview_size = int(self.canvas.config('width')[4]) yview_size = int(self.canvas.config('height')[4]) self.canvas.config(scrollregion=bbox) self._view_dimensions = (xview_size, yview_size) if move_to_bottom: self.canvas.yview_moveto(1.0) def reattach(self): self.canvas.create_window((0, 0), window=self.frame, anchor="nw") #region private methods def _bind_to_mousewheel(self, event): self.canvas.bind_all("<MouseWheel>", self._on_mousewheel) def _on_mousewheel(self, event): if self.vsb.get() != (0.0, 1.0): if os_name() == 'Windows': self.canvas.yview_scroll(int(-1 * (event.delta / 120)), "units") else: self.canvas.yview_scroll(-event.delta, "units") def _resize_canvas(self, width, height): """Resize the canvas to the specified size Parameters ---------- width : int Width of the frame. height : int Height of the frame. """ if self.block_resize or (width is None and height is None): return if width is not None or height is not None: self.block_resize = True else: self.block_resize = False canvas_config = dict() if width is not None: canvas_config['width'] = width if height is not None: canvas_config['height'] = height self.canvas.config(**canvas_config) def _unbind_to_mousewheel(self, event): self.canvas.unbind_all("<MouseWheel>")