I want to create a text box with two auto hiding scroll bars, horizontally and vertically. The thing is, I'm struggling to set them correctly. Here's an example code:
from tkinter import *
class AutoScrollbar(Scrollbar):
# a scrollbar that hides itself if it's not needed. only
# works if you use the pack geometry manager.
def set(self, lo, hi):
if float(lo) <= 0.0 and float(hi) >= 1.0:
self.pack_forget()
else:
if self.cget("orient") == HORIZONTAL:
self.pack(fill=X, side=BOTTOM)
else:
self.pack(fill=Y, side=RIGHT)
Scrollbar.set(self, lo, hi)
def grid(self, **kw):
raise (TclError, "cannot use grid with this widget")
def place(self, **kw):
raise (TclError, "cannot use place with this widget")
class Listboxapp(Tk):
def __init__(self):
super().__init__()
self.geometry("400x400")
# Text box frame
self.frame1 = Frame(self)
self.frame1.pack(pady=5)
# Create vertical scrollbar
self.text_scroll = AutoScrollbar(self.frame1)
self.text_scroll.pack()
# Create Horizontal AutoScrollbar
self.hor_scroll = AutoScrollbar(self.frame1, orient=HORIZONTAL)
self.hor_scroll.pack()
# Create text box
self.my_text = Text(self.frame1, yscrollcommand=self.text_scroll.set,
wrap=NONE, xscrollcommand=self.hor_scroll.set)
self.my_text.pack()
# Configure scrollbars
self.text_scroll.config(command=self.my_text.yview)
self.hor_scroll.config(command=self.my_text.xview)
if __name__ == '__main__':
w = Listboxapp()
w.mainloop()
This is what it looks like the horizontal scroll bar:
It works fine, however, once the vertical one appears, it looks like this:
It doesn't set correctly on the right side, besides the horizontal one dissapears.
I also tried self.my_text(side=LEFT) but it looks like this:
Related
I'm writing a class that adds a scrolling frame. It detects when the frame's contents exceed its height, and then configures the scrollbar. The problem is that when I scroll down, the items in the frame scroll outside of the top of the frame and appear above it.
I've tested it out using plain labels, and it worked fine, but I'm using a class object that has some nested frames, and the child objects are what show up above the scrolling frame.
This is the gist of the code that's giving me problems (please note that the layout doesn't match the full project. I used this as my reference for the ScrollFrame() class.)
Just running this, pressing the button, and scrolling down will show you what's wrong with it.
import tkinter as tk
import tkinter.simpledialog as sd
class ScrollFrame(tk.Frame):
def __init__(self, master):
tk.Frame.__init__(self, master)
### setting up the objects used ###
self.canvas = tk.Canvas(master)
self.frame = tk.Frame(self.canvas)
self.scrollbar = tk.Scrollbar(master, orient = 'vertical',
command = self.canvas.yview)
### scrollbar moves with current canvas scrollamount ###
self.canvas.configure(yscrollcommand = self.scrollbar.set)
self.scrollbar.pack(side = 'right', fill = 'y')
self.canvas.pack(side = 'left', fill = 'both', expand = True)
### creating frame to pack widgets onto ###
self.canvas.create_window((4, 4), window = self.frame,
anchor = 'nw', tags = 'self.frame')
### setting scrollbar height on load ###
self.frame.bind('<Configure>', self.frameConfig)
### scroll when a user's mouse wheel is used inside the canvas ###
def scrollCanvas(event):
self.canvas.yview_scroll(-1*(event.delta//120), 'units')
self.canvas.bind('<MouseWheel>', scrollCanvas)
### set the scrollregion of the canvas ###
def frameConfig(self, event):
self.canvas.configure(scrollregion = self.canvas.bbox('all'))
class OptionSet(tk.Frame):
def __init__(self, master, **kwargs):
super().__init__()
self.all = tk.Frame(master)
self.all.configure(bd = 1, relief = 'solid')
# independent label
self.text = '' if not kwargs['text'] else kwargs['text']
self.label = tk.Label(text = self.text)
# list of all buttons
self.buttons = tk.Frame()
buttons = [] if not kwargs['buttons'] else kwargs['buttons']
self.button_list = []
if buttons:
for button in buttons:
self.button_list.append(
tk.Button(self.buttons, text = button)
)
self.style()
def style(self, default = 1, **kwargs):
if default:
self.label.pack(in_ = self.all, side = 'left')
for button in self.button_list:
button.pack(side = 'left')
self.buttons.pack(in_ = self.all, side = 'right')
root = tk.Tk()
list_items = []
current = {
'month': 'August'
# ...
}
def btn_fcn(num):
for i in list_items:
i.grid_forget()
'''
# get event as input
event = sd.askstring('Event Input',
f"What is happening on {current['month']} {num}?")
# insert new list_item
list_items.append(OptionSet(event_list.frame, text = event,
buttons = ['Edit', 'Delete']))
print(event)
'''
for i in range(10):
list_items.append(OptionSet(event_list.frame, text = 'test',
buttons = ['Edit', 'Delete']))
for i in list_items:
i.all.grid(sticky = 'we')
tk.Button(root, text = 'Add', command = lambda: btn_fcn(22)).pack()
event_list = ScrollFrame(root)
event_list.pack()
root.mainloop()
I want the buttons and labels to cut off outside of the ScrollFrame. I don't know whether they're overflowing from the frame or the canvas, but they should cut off normally if all goes according to plan.
Thanks.
The problem is that you're being very sloppy with where you create your widgets. For example, you aren't putting the canvas and scrollbar in the ScrollFrame, you're putting it in master. Every widget defined in ScrolLFrame needs to be in self or a child of self.
The same is true with OptionSet - you're putting the inner frame (self.all) a d the other widgets in master rather than inside the OptionSet itself.
My recommendation is to temporarily remove all of the OptionSet code (or just don't use it) and instead, just add labels to the scroll frame. Focus on getting that working without the complexity of a custom class being added to a custom class. Once you are able to scroll just labels, the you can add back in the OptionSet code.
I am making a kiosk app for a raspberry pi in python with a 7 inch touchscreen.
Everything works well and now, I am trying to make the scroll works like it does on touchscreens. I know that raspbian haven't got a properly touch interface, so, every touch on the screen work as a mouse-click, and if I move my finger touching the screen, works like the drag function.
To make this on python I use my modified version code of this Vertical Scrolled Frame using canvas and I need to add events binding <ButtonPress-1> and <B1-Motion>.
<ButtonPress-1> might save the y position of the click and enable the bind_all function for <B1-Motion>.
<B1-Motion> might set the scroll setting up or down the differrence between the y value saved by <ButtonPress-1> and the event.y of this event.
<ButtonRelease-1> might disable the bind_all function with <unbind_all> of the scroll.
My added code of the events is this, but I don't know how to make it work properly the .yview function of the canvas to make my function works as desired.
def moving(event):
#In this part I don't know how to make my effect
self.canvas.yview('scroll',event.y,"units")
def clicked(event):
global initialy
initialy = event.y
self.canvas.bind_all('<B1-Motion>', moving)
def released(event):
self.canvas.unbind_all('<B1-Motion>')
self.canvas.bind_all('<ButtonPress-1>',clicked)
self.canvas.bind_all('<ButtonRelease-1>', released)
Taking Saad's code as a base, I have modified it to make it work on every S.O. (win, linux,mac) using yview_moveto and I have applied some modifications as I explain here.
EDIT: I have edited the code to show the complete class.
class VerticalScrolledFrame(Frame):
"""A pure Tkinter scrollable frame that actually works!
* Use the 'interior' attribute to place widgets inside the scrollable frame
* Construct and pack/place/grid normally
* This frame only allows vertical scrolling
"""
def __init__(self, parent, bg,*args, **kw):
Frame.__init__(self, parent, *args, **kw)
# create a canvas object and a vertical scrollbar for scrolling it
canvas = Canvas(self, bd=0, highlightthickness=0,bg=bg)
canvas.pack(side=LEFT, fill=BOTH, expand=TRUE)
# reset the view
canvas.xview_moveto(0)
canvas.yview_moveto(0)
self.canvasheight = 2000
# create a frame inside the canvas which will be scrolled with it
self.interior = interior = Frame(canvas,height=self.canvasheight,bg=bg)
interior_id = canvas.create_window(0, 0, window=interior,anchor=NW)
# track changes to the canvas and frame width and sync them,
# also updating the scrollbar
def _configure_interior(event):
# update the scrollbars to match the size of the inner frame
size = (interior.winfo_reqwidth(), interior.winfo_reqheight())
canvas.config(scrollregion="0 0 %s %s" % size)
if interior.winfo_reqwidth() != canvas.winfo_width():
# update the canvas's width to fit the inner frame
canvas.config(width=interior.winfo_reqwidth())
interior.bind('<Configure>', _configure_interior)
def _configure_canvas(event):
if interior.winfo_reqwidth() != canvas.winfo_width():
# update the inner frame's width to fill the canvas
canvas.itemconfigure(interior_id, width=canvas.winfo_width())
canvas.bind('<Configure>', _configure_canvas)
self.offset_y = 0
self.prevy = 0
self.scrollposition = 1
def on_press(event):
self.offset_y = event.y_root
if self.scrollposition < 1:
self.scrollposition = 1
elif self.scrollposition > self.canvasheight:
self.scrollposition = self.canvasheight
canvas.yview_moveto(self.scrollposition / self.canvasheight)
def on_touch_scroll(event):
nowy = event.y_root
sectionmoved = 15
if nowy > self.prevy:
event.delta = -sectionmoved
elif nowy < self.prevy:
event.delta = sectionmoved
else:
event.delta = 0
self.prevy= nowy
self.scrollposition += event.delta
canvas.yview_moveto(self.scrollposition/ self.canvasheight)
self.bind("<Enter>", lambda _: self.bind_all('<Button-1>', on_press), '+')
self.bind("<Leave>", lambda _: self.unbind_all('<Button-1>'), '+')
self.bind("<Enter>", lambda _: self.bind_all('<B1-Motion>', on_touch_scroll), '+')
self.bind("<Leave>", lambda _: self.unbind_all('<B1-Motion>'), '+')
I modified the VerticalScrolledFrame with few lines of code which does scroll how you want. I tested the code with the VerticalScrolledFrame and it works fine with the mouse. Add the below code to the VerticalScrolledFrame.
self.offset_y = 0
def on_press(evt):
self.offset_y = evt.y_root
def on_touch_scroll(evt):
if evt.y_root-self.offset_y<0:
evt.delta = -1
else:
evt.delta = 1
# canvas.yview_scroll(-1*(evt.delta), 'units') # For MacOS
canvas.yview_scroll( int(-1*(evt.delta/120)) , 'units') # For windows
self.bind("<Enter>", lambda _: self.bind_all('<Button-1>', on_press), '+')
self.bind("<Leave>", lambda _: self.unbind_all('<Button-1>'), '+')
self.bind("<Enter>", lambda _: self.bind_all('<B1-Motion>', on_touch_scroll), '+')
self.bind("<Leave>", lambda _: self.unbind_all('<B1-Motion>'), '+')
I hope you can find this useful.
i am going to create an tkinter gui app, and i know how i want it to look. but after playing around with tkinter, i found no way to toggle between screens when you press buttons down at the bottom. i know it does nothing but below is the simple layout i want to have, and switch between "myframe1" and "myframe2" kind of like the Apple App Store layout. is this possible?
from tkinter import *
tk = Tk()
tk.geometry("300x300")
myframe1 = Frame(tk,background="green",width=300,height=275)
myframe1.pack()
myframe2 = Frame(tk,background="cyan",width=300,height=275)
myframe2.pack()
btnframe = Frame(tk)
btn1 = Button(btnframe,text="screen1",width=9)
btn1.pack(side=LEFT)
btn2 = Button(btnframe,text="screen2",width=9)
btn2.pack(side=LEFT)
btn3 = Button(btnframe,text="screen3",width=9)
btn3.pack(side=LEFT)
btn4 = Button(btnframe,text="screen4",width=9)
btn4.pack(side=LEFT)
myframe1.pack()
btnframe.pack()
tk.mainloop()
something for you to get started with:
def toggle(fshow,fhide):
fhide.pack_forget()
fshow.pack()
btn1 = Button(btnframe,text="screen1", command=lambda:toggle(myframe1,myframe2),width=9)
btn1.pack(side=LEFT)
btn2 = Button(btnframe,text="screen2",command=lambda:toggle(myframe2,myframe1),width=9)
btn2.pack(side=LEFT)
Are you looking for something like a tabbed widget? You could use forget and pack as suggested here
Here is a class that I use in my code that works:
class MultiPanel():
"""We want to setup a pseudo tabbed widget with three treeviews. One showing the disk, one the pile and
the third the search results. All three treeviews should be hooked up to exactly the same event handlers
but only one of them should be visible at any time.
Based off http://code.activestate.com/recipes/188537/
"""
def __init__(self, parent):
#This is the frame that we display
self.fr = tki.Frame(parent, bg='black')
self.fr.pack(side='top', expand=True, fill='both')
self.widget_list = []
self.active_widget = None #Is an integer
def __call__(self):
"""This returns a reference to the frame, which can be used as a parent for the widgets you push in."""
return self.fr
def add_widget(self, wd):
if wd not in self.widget_list:
self.widget_list.append(wd)
if self.active_widget is None:
self.set_active_widget(0)
return len(self.widget_list) - 1 #Return the index of this widget
def set_active_widget(self, wdn):
if wdn >= len(self.widget_list) or wdn < 0:
logger.error('Widget index out of range')
return
if self.widget_list[wdn] == self.active_widget: return
if self.active_widget is not None: self.active_widget.forget()
self.widget_list[wdn].pack(fill='both', expand=True)
self.active_widget = self.widget_list[wdn]
I have worked with the AutoScrollbar (http://effbot.org/zone/tkinter-autoscrollbar.htm), and added a horizontal one too.
from Tkconstants import VERTICAL, N, S, HORIZONTAL, W, E, END
import Tkinter as Tk
class AutoScrollbar(Tk.Scrollbar):
""" a scrollbar that hides itself if it's not needed. only
works if you use the grid geometry manager."""
def set(self, lo, hi):
if float(lo) <= 0.0 and float(hi) >= 1.0:
# grid_remove is currently missing from Tkinter!
self.tk.call("grid", "remove", self)
else:
self.grid()
Tk.Scrollbar.set(self, lo, hi)
def pack(self, **kw):
raise Tk.TclError, "cannot use pack with this widget"
def place(self, **kw):
raise Tk.TclError, "cannot use place with this widget"
if __name__ == '__main__':
root = Tk.Tk()
scrollbarY = AutoScrollbar(root,orient=VERTICAL)
scrollbarY.grid(column=1, row=0,sticky=N+S)
scrollbarX = AutoScrollbar(root,orient=HORIZONTAL)
scrollbarX.grid(column=0, row=1,sticky=W+E)
mylist = Tk.Listbox(root, xscrollcommand = scrollbarX.set, yscrollcommand = scrollbarY.set )
for line in range(50):
mylist.insert(END, "This is line number " + str(line) + ", and then just for whatever reason this text goes on and on")
mylist.grid(column=0, row=0,sticky=N+S+E+W)
scrollbarX.config( command = mylist.xview )
scrollbarY.config( command = mylist.yview )
root.columnconfigure(0, weight=1)
root.rowconfigure(0, weight=1)
Tk.mainloop()
Now I thought: wouldn't it be rad to have this functionality without all the clutter?
So I set out to code a tk.Frame with autohiding scrollbars.
The Idea is: you add a Widget to the AutoscrollFrame, the Atuoscrollframe internally puts that widget at (0,0) on a 2x2 grid, with (0,1) and (1,0) being x- and y- autohiding scrollbars and (1,1) being left blank.
The scrollbars would of course only be visible if they are needed.
Now this idea seemed pretty easy to implement in my head. Sadly I soon saw that it wasn't.
What method do I have to override to implement the 'add child widget to frame' functionality (that adds the widget not to the frame in general but to the (0,0) grid position and also registers the scrollbars with the widget)?
My code so far looks like this:
import Tkinter as tk
from Tkconstants import VERTICAL, N, S, HORIZONTAL, W, E, END
class AutoscrollFrame(tk.Frame):
"""A Frame extension that automatically adds scrollbars
if the content of the frame exceeds it's dimensions.
"""
def __init__(self,master,*args,**kwargs):
self.master = master
super(AutoscrollFrame,self).__init__(master=master,*args,**kwargs)
self.scrollbarY = _AutoScrollbar(self,orient=VERTICAL)
self.scrollbarY.grid(column=1, row=0,sticky=N+S)
self.scrollbarX = _AutoScrollbar(self,orient=HORIZONTAL)
self.scrollbarX.grid(column=0, row=1,sticky=W+E)
self.columnconfigure(0, weight=1)
self.rowconfigure(0, weight=1)
def add_child_placeholder(self,child):
self.child = child
self.child.config(xscrollcommand=scrollbarX.set, yscrollcommand=scrollbarY.set )
self.child.grid(column=0, row=0,sticky=N+S+E+W)
self.scrollbarX.config( command = self.child.xview )
self.scrollbarY.config( command = self.child.yview )
the add_child_placeholder is a prototype for the method I'd like to call when something like this happens:
frame = AutoscrollFrame(root)
myList = tk.Listbox(frame)
meaning if I set the frame as master to another Widget, I want to call the frames add_child_placeholder method.
Is this a sensible way to do it? I'm not totally sure this isn't a brainfart, so a little help would be greatly appreciated.
PS: I wanted to add this functionality to tk.Frame (as opposed to tk.ListBox) since I'm not yet sure if Listbox is what I will end up using.
I want to make a window in Tk that has a custom titlebar and frame. I have seen many questions on this website dealing with this, but what I'm looking for is to actually render the frame using a canvas, and then to add the contents to the canvas. I cannot use a frame to do this, as the border is gradiented.
According to this website: http://effbot.org/tkinterbook/canvas.htm#Tkinter.Canvas.create_window-method, I cannot put any other canvas items on top of a widget (using the create_window method), but I need to do so, as some of my widgets are rendered using a canvas.
Any suggestions on how to do this? I'm clueless here.
EDIT: Bryan Oakley confirmed that rendering with a canvas would be impossible. Would it then be possible to have a frame with a custom border color? And if so, could someone give a quick example? I'm sort of new with python.
You can use the canvas as if it were a frame in order to draw your own window borders. Like you said, however, you cannot draw canvas items on top of widgets embedded in a canvas; widgets always have the highest stacking order. There is no way around that, though it's not clear if you really need to do that or not.
Here's a quick and dirty example to show how to create a window with a gradient for a custom border. To keep the example short I didn't add any code to allow you to move or resize the window. Also, it uses a fixed color for the gradient.
import Tkinter as tk
class GradientFrame(tk.Canvas):
'''A gradient frame which uses a canvas to draw the background'''
def __init__(self, parent, borderwidth=1, relief="sunken"):
tk.Canvas.__init__(self, parent, borderwidth=borderwidth, relief=relief)
self._color1 = "red"
self._color2 = "black"
self.bind("<Configure>", self._draw_gradient)
def _draw_gradient(self, event=None):
'''Draw the gradient'''
self.delete("gradient")
width = self.winfo_width()
height = self.winfo_height()
limit = width
(r1,g1,b1) = self.winfo_rgb(self._color1)
(r2,g2,b2) = self.winfo_rgb(self._color2)
r_ratio = float(r2-r1) / limit
g_ratio = float(g2-g1) / limit
b_ratio = float(b2-b1) / limit
for i in range(limit):
nr = int(r1 + (r_ratio * i))
ng = int(g1 + (g_ratio * i))
nb = int(b1 + (b_ratio * i))
color = "#%4.4x%4.4x%4.4x" % (nr,ng,nb)
self.create_line(i,0,i,height, tags=("gradient",), fill=color)
self.lower("gradient")
class SampleApp(tk.Tk):
def __init__(self):
tk.Tk.__init__(self)
self.wm_overrideredirect(True)
gradient_frame = GradientFrame(self)
gradient_frame.pack(side="top", fill="both", expand=True)
inner_frame = tk.Frame(gradient_frame)
inner_frame.pack(side="top", fill="both", expand=True, padx=8, pady=(16,8))
b1 = tk.Button(inner_frame, text="Close",command=self.destroy)
t1 = tk.Text(inner_frame, width=40, height=10)
b1.pack(side="top")
t1.pack(side="top", fill="both", expand=True)
if __name__ == "__main__":
app = SampleApp()
app.mainloop()
Here is a rough example where the frame, titlebar and close button are made with canvas rectangles:
import Tkinter as tk
class Application(tk.Tk):
def __init__(self):
tk.Tk.__init__(self)
# Get rid of the os' titlebar and frame
self.overrideredirect(True)
self.mCan = tk.Canvas(self, height=768, width=768)
self.mCan.pack()
# Frame and close button
self.lFrame = self.mCan.create_rectangle(0,0,9,769,
outline='lightgrey', fill='lightgrey')
self.rFrame = self.mCan.create_rectangle(760,0,769,769,
outline='lightgrey', fill='lightgrey')
self.bFrame = self.mCan.create_rectangle(0,760,769,769,
outline='lightgrey', fill='lightgrey')
self.titleBar = self.mCan.create_rectangle(0,0,769,20,
outline='lightgrey', fill='lightgrey')
self.closeButton = self.mCan.create_rectangle(750,4,760, 18,
activefill='red', fill='darkgrey')
# Binds
self.bind('<1>', self.left_mouse)
self.bind('<Escape>', self.close_win)
# Center the window
self.update_idletasks()
xp = (self.winfo_screenwidth() / 2) - (self.winfo_width() / 2)
yp = (self.winfo_screenheight() / 2) - (self.winfo_height() / 2)
self.geometry('{0}x{1}+{2}+{3}'.format(self.winfo_width(),
self.winfo_height(),
xp, yp))
def left_mouse(self, event=None):
obj = self.mCan.find_closest(event.x,event.y)
if obj[0] == self.closeButton:
self.destroy()
def close_win(self, event=None):
self.destroy()
app = Application()
app.mainloop()
If I were going to make a custom GUI frame I would consider creating it with images,
made with a program like Photoshop, instead of rendering canvas objects.
Images can be placed on a canvas like this:
self.ti = tk.PhotoImage(file='test.gif')
self.aImage = mCanvas.create_image(0,0, image=self.ti,anchor='nw')
More info →here←