Create scrollbar only when text length greater than text area - python

Here is some simple code:
from tkinter import *
from tkinter import ttk
rootwin = Tk()
roomtext = Text(rootwin)
roomtext.pack(side = 'left', fill = "both", expand = True)
rtas = ttk.Scrollbar(roomtext, orient = "vertical", command = roomtext.yview)
rtas.pack(side = "right" , fill = "both")
roomtext.config(yscrollcommand = rtas.set)
rootwin.mainloop()
As such, a default scrollbar appears straight away.
How is it possible to make the scrollbar appear once text entered is greater than text area?
So when I run code, first, scrollbar must not show. Then when enough text is entered scrollbar shows (i.e. text in roomtext longer than roomtext area).

Maybe this code is what you are looking for (changed pack to grid as I'm more familiar with it... You should be able to revert that easily if you want):
from tkinter import *
from tkinter import ttk
rootwin = Tk()
roomtext = Text(rootwin)
roomtext.grid(column=0, row=0)
def create_scrollbar():
if roomtext.cget('height') < int(roomtext.index('end-1c').split('.')[0]):
rtas = ttk.Scrollbar(rootwin, orient = "vertical", command = roomtext.yview)
rtas.grid(column=1, row=0, sticky=N+S)
roomtext.config(yscrollcommand = rtas.set)
else:
rootwin.after(100, create_scrollbar)
create_scrollbar()
rootwin.mainloop()
It checks if it needs to create a scrollbar 10 times every second.
With some additional changes you can even make it remove the scrollbar when no longer needed (text too short):
from tkinter import *
from tkinter import ttk
rootwin = Tk()
roomtext = Text(rootwin)
roomtext.grid(column=0, row=0)
rtas = ttk.Scrollbar(rootwin, orient = "vertical", command = roomtext.yview)
def show_scrollbar():
if roomtext.cget('height') < int(roomtext.index('end-1c').split('.')[0]):
rtas.grid(column=1, row=0, sticky=N+S)
roomtext.config(yscrollcommand = rtas.set)
rootwin.after(100, hide_scrollbar)
else:
rootwin.after(100, show_scrollbar)
def hide_scrollbar():
if roomtext.cget('height') >= int(roomtext.index('end-1c').split('.')[0]):
rtas.grid_forget()
roomtext.config(yscrollcommand = None)
rootwin.after(100, show_scrollbar)
else:
rootwin.after(100, hide_scrollbar)
show_scrollbar()
rootwin.mainloop()

Related

How do I make it so that using CustomTkinter I can accept an input by pressing 'Enter'?

I was able so successfully place the input module earlier but now when I try to implement code to use 'Enter' to accept the input I'm not even able to get it to pack. If anyone can help me it would be thoroughly appreciated. Eventually I am trying to make it so that whatever typed ends up being incrementally placed as a title in a column centered to the left of the entry widget but I will work on that later. (this is the UI for a text adventure game)
Below is the code I have so far
#imports
import time, os, sys, logging
from pynput import *
from tkinter import Button
from tkinter import *
import customtkinter
import tkinter.font as font
from tkinter import simpledialog
import tkinter
import time
import tkinter as tk
userin = ''
#graphics
the_path_text = '''
┏┓┏┓╋╋╋╋╋╋╋╋┏┓┏┓
┃┗┫┗┳━┓┏━┳━┓┃┗┫┗┓
┃┏┫┃┃┻┫┃╋┃╋┗┫┏┫┃┃
┗━┻┻┻━┛┃┏┻━━┻━┻┻┛
╋╋╋╋╋╋╋┗┛'''
#window
customtkinter.set_appearance_mode("dark")
customtkinter.set_default_color_theme("dark-blue")
root = customtkinter.CTk()
root.geometry("800x520")
root.resizable(width = False, height = False)
root.title('The Path')
photo = PhotoImage(file = "D:\\pathpic.png")
root.iconphoto(False, photo)
myfont = ("Roboto Mono",30)
frame = customtkinter.CTkFrame(master = root)
frame.pack(pady = 20, padx = 60, fill = 'both', expand = True)
#splash/start screen
#loading
label = customtkinter.CTkLabel(root, text="Loading", font= ('Roboto', 20))
label.pack()
# Incrementally add dots
for i in range(3):
label.configure(text="Loading" + "." * (i + 1))
time.sleep(.5)
root.update()
# Clear the label text
label.configure(text="")
# Incrementally add dots again
for i in range(3):
label.configure(text="Loading" + "." * (i + 1))
time.sleep(.5)
root.update()
label.configure(text=" ")
#splash logo
label_path_logo = customtkinter.CTkLabel(master=frame, text= the_path_text, font = ('Roboto', 20))
label_path_logo.pack(pady =12, padx = 10)
#clearing splash/ main game loop
start = 0
def clear_start():
time.sleep(.2)
global frame, root
frame.destroy()
frame = Frame(root)
frame.pack()
start_button.destroy()
start = 1
#main game
start_button = customtkinter.CTkButton(master=root, text="Enter the path", command = clear_start)
start_button.pack(padx = 6, pady=20)
while start == 1:
#main text console
main_entry = root.entry = customtkinter.CTkEntry(root, textvariable=userin, placeholder_text="Enter Command")
main_entry.pack(side = BOTTOM,fill = BOTH, pady = 5, padx = 200 )
def process(event=None):
content = main_entry.get() # get the contents of the entry widget
print(content) # for example
main_entry.bind('<Return>', process)
root.mainloop()

Behaviour of DateEntry in Tkinter Window

I have a button that adds a DateEntry widget to a scrollable frame. Unfortunately, the frame is at the bottom of the window, so after adding a few rows, the dropdown calendar hides behind the task bar. Is there any way to change it, so the calendar opens above the field, rather than below?
Here is some test code
from tkinter import *
from tkinter import ttk
from tkcalendar import Calendar, DateEntry
master = Tk()
global rowNumForShiftReport
rowNumForShiftReport=0
shiftDateWidgetList=[]
def myfunction(event):
canvas2.configure(scrollregion=canvas2.bbox("all"), width=100, height=100)
def addEntry2():
global rowNumForShiftReport
rowNumForShiftReport = rowNumForShiftReport + 1
shiftDateWidgetList.append(DateEntry(frame2, state='readonly', width=15))
shiftDateWidgetList[-1].grid(row=rowNumForShiftReport, column=0)
rowNumForShiftReport+1
master.geometry('400x400')
btn_addField2 = ttk.Button(master, text="Add Entry",command=addEntry2)
btn_addField2.grid(row=0, column=1)
#lotFrame2 = Frame(master)
actualLabelFrame=ttk.LabelFrame(master, text="Shift Report", height=300, width=300)
actualLabelFrame.grid(row=0, column=0)
canvas2 = Canvas(actualLabelFrame,width=160)
frame2 = Frame(canvas2,width=160)
frame2.bind("<Configure>", myfunction)
canvas2.create_window((0, 0), window=frame2, anchor='nw')
scrollBar2 = ttk.Scrollbar(actualLabelFrame, orient="vertical", command=canvas2.yview)
canvas2.configure(yscrollcommand=scrollBar2.set)
scrollBar2.grid(row=0, column=2, sticky=N + S)
canvas2.grid(row=0, column=1)
mainloop()
You can achieve the goal by extending DateEntry and override its drop_down() function as below:
class MyDateEntry(DateEntry):
def drop_down(self):
"""Display or withdraw the drop-down calendar depending on its current state."""
if self._calendar.winfo_ismapped():
self._top_cal.withdraw()
else:
self._validate_date()
date = self.parse_date(self.get())
x = self.winfo_rootx()
y = self.winfo_rooty() + self.winfo_height()
if self.winfo_toplevel().attributes('-topmost'):
self._top_cal.attributes('-topmost', True)
else:
self._top_cal.attributes('-topmost', False)
# - patch begin: make sure the drop-down calendar is visible
if x+self._top_cal.winfo_width() > self.winfo_screenwidth():
x = self.winfo_screenwidth() - self._top_cal.winfo_width()
if y+self._top_cal.winfo_height() > self.winfo_screenheight()-30:
y = self.winfo_rooty() - self._top_cal.winfo_height()
# - patch end
self._top_cal.geometry('+%i+%i' % (x, y))
self._top_cal.deiconify()
self._calendar.focus_set()
self._calendar.selection_set(date)
Then replace all DateEntry(...) by MyDateEntry(...) in your code.
Note that it is based on tkcalendar v1.6.1.

How to put a widget beneath widgets that are side-by-side using pack?

I try to put the widgets like this:
I don't understand why my code doesn't do that, tried to look for examples online but didn't find a solution and nothing I tried brought me closer to the requested result.
This is my code so far(if you have any comments about anything in the code feel free to tell me because it's my first try with tkinter and GUIs in general):
from Tkinter import *
class box(object):
def __init__ (self, colour,s):
self.root = root
self.listbox = Listbox(self.root, fg = colour, bg = 'black')
self.s = s
self.place_scrollbar()
self.listbox.pack(side = self.s)
def place_scrollbar(self):
scrollbar = Scrollbar(self.root)
scrollbar.pack(side = self.s, fill = Y)
self.listbox.config(yscrollcommand = scrollbar.set)
scrollbar.config(command = self.listbox.yview)
def write(self, contenet):
self.listbox.insert(END, contenet)
root = Tk()
root.resizable(False, False)
boxs = Frame(root)
boxs.pack()
box.root = boxs
server = box("red", LEFT)
client = box("green", RIGHT )
bf = Frame(root)
bf.pack(side = BOTTOM)
entry = Entry(bf,bg ='black', fg = 'white')
entry.pack()
root.mainloop()
You can't do this without using an additional frame to contain the box objects while still using pack, while still maintaining resizability.
But it is more organized in some cases to: use an additional frame to contain your box objects, by initializing it with a parent option.
Right now the widgets inside the box class are children to global root object. Which isn't really a good practice. So let's first pass and use a parent object to be used for widgets inside.
Replace:
def __init__ (self, colour,s):
self.root = root
self.listbox = Listbox(self.root, ...)
...
def place_scrollbar(self):
scrollbar = Scrollbar(self.root)
...
with:
def __init__ (self, parent, colour,s):
self.parent= parent
self.listbox = Listbox(self.parent, ...)
...
def place_scrollbar(self):
scrollbar = Scrollbar(self.parent)
...
This makes it so that you now need to initialize the object like the following:
server = box(root, "red", LEFT)
client = box(root, "green", RIGHT )
Now that we can pass a parent widget, let's create a parent frame to contain them. Actually, there's an un-used frame already, boxs let's use that by passing it as the parent as opposed to root:
server = box(boxs, "red", LEFT)
client = box(boxs, "green", RIGHT )
Now everything looks fine, optionally if you want to make it so that entry occupies as much left space as possible currently add fill='x' as an option to the pack of both the entry and the frame that contains it:
bf.pack(side = BOTTOM, fill='x')
...
entry.pack(fill='x')
Your whole code should look like:
from Tkinter import *
class box(object):
def __init__ (self, parent, colour,s):
self.parent = parent
self.listbox = Listbox(self.parent, fg = colour, bg = 'black')
self.s = s
self.place_scrollbar()
self.listbox.pack(side = self.s)
def place_scrollbar(self):
scrollbar = Scrollbar(self.parent)
scrollbar.pack(side = self.s, fill = Y)
self.listbox.config(yscrollcommand = scrollbar.set)
scrollbar.config(command = self.listbox.yview)
def write(self, contenet):
self.listbox.insert(END, contenet)
root = Tk()
root.resizable(False, False)
boxs = Frame(root)
boxs.pack()
box.root = boxs
server = box(boxs, "red", LEFT)
client = box(boxs, "green", RIGHT )
bf = Frame(root)
bf.pack(side = BOTTOM, fill='x')
entry = Entry(bf,bg ='black', fg = 'white')
entry.pack(fill='x')
root.mainloop()
Or: use grid instead of pack (with columnspan=2 option for entry).
General Answer
More generally putting a widget beneath two widgets that are side-by-side can be done by:
Encapsulating the side-by-side widgets with a frame, and then simply putting the frame above the other widget:
try: # In order to be able to import tkinter for
import tkinter as tk # either in python 2 or in python 3
except ImportError:
import Tkinter as tk
def main():
root = tk.Tk()
side_by_side_widgets = dict()
the_widget_beneath = tk.Entry(root)
frame = tk.Frame(root)
for name in {"side b", "y side"}:
side_by_side_widgets[name] = tk.Label(frame, text=name)
side_by_side_widgets[name].pack(side='left', expand=True)
frame.pack(fill='x')
the_widget_beneath.pack()
root.mainloop()
if __name__ == '__main__':
main()
Using grid:
try: # In order to be able to import tkinter for
import tkinter as tk # either in python 2 or in python 3
except ImportError:
import Tkinter as tk
def main():
root = tk.Tk()
side_by_side_widgets = dict()
the_widget_beneath = tk.Entry(root)
for index, value in enumerate({"side b", "y side"}):
side_by_side_widgets[value] = tk.Label(root, text=value)
side_by_side_widgets[value].grid(row=0, column=index)
the_widget_beneath.grid(row=1, column=0, columnspan=2)
root.mainloop()
if __name__ == '__main__':
main()
Without using additional frames, by calling pack for the_widget_beneath with side='bottom' as the first pack call, as in Bryan's comment:
try: # In order to be able to import tkinter for
import tkinter as tk # either in python 2 or in python 3
except ImportError:
import Tkinter as tk
def main():
root = tk.Tk()
side_by_side_widgets = dict()
the_widget_beneath = tk.Entry(root)
the_widget_beneath.pack(side='bottom')
for name in {"side b", "y side"}:
side_by_side_widgets[name] = tk.Label(root, text=name)
side_by_side_widgets[name].pack(side='left', expand=True)
root.mainloop()
if __name__ == '__main__':
main()
You can more easily notice reliability to global objects by creating a global main method, and add main-body of your script there and call:
...
def main():
root = Tk()
root.resizable(False, False)
boxs = Frame(root)
boxs.pack()
box.root = boxs
server = box(boxs, "red", LEFT)
client = box(boxs, "green", RIGHT )
bf = Frame(root)
bf.pack(side = BOTTOM, fill='x')
entry = Entry(bf,bg ='black', fg = 'white')
entry.pack(fill='x')
root.mainloop()
if __name__ == '__main__':
main()
How to put a widget beneath two widgets that are side-by-side using pack?
For a very simple layout like in your diagram, you simply need to pack the thing on the bottom first. That is because pack uses a "cavity" model. Each widget is organized in an unfilled cavity. Once that widget has been placed, that portion of the cavity is filled, and is unavailable for any other widgets.
In your case, you want the bottom cavity to be filled with the entry widget, so you should pack it first. Then, in the remaining upper cavity you can place your two frames side-by side, one on the left and one on the right.
For example:
import Tkinter as tk
root = tk.Tk()
entry = tk.Entry(root)
frame1 = tk.Frame(root, width=100, height=100, background="red")
frame2 = tk.Frame(root, width=100, height=100, background="green")
entry.pack(side="bottom", fill="x")
frame1.pack(side="left", fill="both", expand=True)
frame2.pack(side="right", fill="both", expand=True)
root.mainloop()
In the body of your question things get a bit more complicated, as you don't just have three widgets like your title suggests, you have several, with some being packed in the root and some being packed elsewhere, with pack statements scattered everywhere.
When using pack, it's best to group widgets into vertical or horizontal slices, and not mix top/bottom with left/right within the same group. It almost seems like you're trying to do that with your boxes, but then you don't -- your "box" is actually two widgets.
Bottom line: be organized. It also really, really helps if all of your pack (or place or grid) statements for a given parent are all in the same block of code. When you scatter them around it makes it impossible to visualize, and impossible to fix. Also, make sure that widgets have the appropriate parents.

Python : Retrieve ttk.Frame size

I'm tring to get Frame's size - but no luck.
in code below- I got both answers ,"0" ( line marked with ***)
from tkinter import *
from tkinter import ttk
root = Tk()
w = '400'
h = '100'
root.geometry('{}x{}'.format(w, h))
root.configure(bg='lightgreen')
txt = StringVar()
txt.set("Hello")
testframe = ttk.Frame(root)
testframe.grid(row=0, column=1 )
label1 = ttk.Label(testframe, textvariable=txt)
label1.grid(row=0, column=0)
print(testframe["width"], testframe.cget("width")) ***This line
root.mainloop()
The update method needs to be called first, so as to have the widget rendered.
If it is not, then its width is equal to zero.
This is explained in this answer.
Now, the problem with testframe['width'] is that it's only a hint of the actual width of the widget, as explained in this answer.
The following code will ouput 32, as expected.
from tkinter import *
from tkinter import ttk
root = Tk()
w = '400'
h = '100'
root.geometry('{}x{}'.format(w, h))
root.configure(bg='lightgreen')
txt = StringVar()
txt.set("Hello")
testframe = ttk.Frame(root)
testframe.grid(row=0, column=1 )
label1 = ttk.Label(testframe, textvariable=txt)
label1.grid(row=0, column=0)
# Modified lines
testframe.update()
print(testframe.winfo_width())
root.mainloop()
The frame widget will not have a size until it gets mapped on screen. There is a Tk event raised when a widget is mapped so the right solution for this example is to bind the <Map> event for the frame and do the print statement in the event handler function.
def on_frame_mapped(ev):
print(ev.widget.winfo_width())
testframe.bind('<Map>', on_frame_mapped)

python GUI tkinter scrollbar not working for canvas

I am learning python GUI with Tkinter. i just created two listbox and populated those in a canvas so that i can configure canvas with scrollbar and those two listbox scroll togather when i scroll canvas. but something is not working.
Here is the structure:
canvas = Canvas(master)
scrollBar = Scrollbar(master)
movieListBox = Listbox(canvas)
statusListBox = Listbox(canvas)
canvas.config(yscrollcommand = scrollBar.set)
movieListBox.config(width = 55)
statusListBox.config(width = 8)
movieListBox.pack(fill = "y", side = "left")
statusListBox.pack(fill = "y", side = "right")
canvas.pack(side = "left", fill = "y", expand = "true")
scrollBar.config(command = canvas.yview)
scrollBar.pack(fill = "y", side = "left")
for i in range(500):
movieListBox.insert(i, "movie name")
statusListBox.insert(i, "downloading")
master.mainloop()
I'm pretty new to tkinter and python myself, but I did find something that works. This is from another question, (I didn't write the code) which was about having two listboxes of different lengths linked to the same scrollbar. If you copy their code (below) you'll see that it works fine for listboxes of equal length. Your question is pretty old, but I hope it helps someone. Cheers.
Where I found this:
Scrolling two listboxes of different lengths with one scrollbar
try:
# Python2
import Tkinter as tk
except ImportError:
# Python3
import tkinter as tk
class App(object):
def __init__(self,master):
scrollbar = tk.Scrollbar(master, orient='vertical')
self.lb1 = tk.Listbox(master, yscrollcommand=scrollbar.set)
self.lb2 = tk.Listbox(master, yscrollcommand=scrollbar.set)
scrollbar.config(command=self.yview)
scrollbar.pack(side='right', fill='y')
self.lb1.pack(side='left', fill='both', expand=True)
self.lb2.pack(side='left', fill='both', expand=True)
def yview(self, *args):
"""connect the yview action together"""
self.lb1.yview(*args)
self.lb2.yview(*args)
root = tk.Tk()
# use width x height + x_offset + y_offset (no spaces!)
root.geometry("320x180+130+180")
root.title("connect 2 listboxes to one scrollbar")
app = App(root)
# load the list boxes for the test
for n in range(64+26, 64, -1): #listbox 1
app.lb1.insert(0, chr(n)+'ell')
for n in range(70+30, 64, -1):
app.lb2.insert(0, chr(n)+'ell') #listbox 2
root.mainloop()

Categories

Resources