Is there a way to clone a tkinter widget? - python-3.x

I'm trying to create of grid of widgets. This grid of widgets starts out as labels telling me their coordinates. I then have a list of starting and ending points for buttons that will replace them.
Say I have a button that will go from (0, 0) to (0, 2), I remove the labels from this location and put a button there with the correct rowspan.
If a button will be replacing another button (not just a label), I create a frame, then I want to clone the button as a way of changing the parent (which I've read is not a possibility with tkinter) and add the new button to the frame as well. The frame will then replace the widgets (labels and old buttons) on the grid with the buttons side by side instead of overlapping.
So this example image shows a grid of Labels, then where the first button is placed, then where the second button should go, and the resulting frame with both buttons in it side by side.
The big issue for me is having to remove the first button and re-place it on the grid because it's not possible to change the parent of a widget. Although I'm welcome to better ideas on getting buttons side by side on the grid as well.

There is no direct way to clone a widget, but tkinter gives you a way to determine the parent of a widget, the class of a widget, and all of the configuration values of a widget. This information is enough to create a duplicate.
It would look something like this:
def clone(widget):
parent = widget.nametowidget(widget.winfo_parent())
cls = widget.__class__
clone = cls(parent)
for key in widget.configure():
clone.configure({key: widget.cget(key)})
return clone

To expand the answer by Bryan Oakley, here a function that allows you to completely clone a widget, including all of its children:
def clone_widget(widget, master=None):
"""
Create a cloned version o a widget
Parameters
----------
widget : tkinter widget
tkinter widget that shall be cloned.
master : tkinter widget, optional
Master widget onto which cloned widget shall be placed. If None, same master of input widget will be used. The
default is None.
Returns
-------
cloned : tkinter widget
Clone of input widget onto master widget.
"""
# Get main info
parent = master if master else widget.master
cls = widget.__class__
# Clone the widget configuration
cfg = {key: widget.cget(key) for key in widget.configure()}
cloned = cls(parent, **cfg)
# Clone the widget's children
for child in widget.winfo_children():
child_cloned = clone_widget(child, master=cloned)
if child.grid_info():
grid_info = {k: v for k, v in child.grid_info().items() if k not in {'in'}}
child_cloned.grid(**grid_info)
elif child.place_info():
place_info = {k: v for k, v in child.place_info().items() if k not in {'in'}}
child_cloned.place(**place_info)
else:
pack_info = {k: v for k, v in child.pack_info().items() if k not in {'in'}}
child_cloned.pack(**pack_info)
return cloned
Example:
root = tk.Tk()
frame = tk.Frame(root, bg='blue', width=200, height=100)
frame.grid(row=0, column=0, pady=(0, 5))
lbl = tk.Label(frame, text='test text', bg='green')
lbl.place(x=10, y=15)
cloned_frame = clone_widget(frame)
cloned_frame.grid(row=1, column=0, pady=(5, 0))
root.mainloop()
Gives:

Related

ttk, How to position heading inside Labelframe instead of on the border?

I am trying to create an own ttk Theme based on my company's CI. I took the Sun Valley theme as starting point and swapped out graphics, fonts and colors.
However I am stuck on the Label frame. I am trying to position the Label within the frame, kind of like a heading. I.e. there should be some margin between top edge and label, and appropriate top-padding for the content (child widgets).
Now:
+-- Label ------
| ...
Desired:
+---------------
| Label
| ...
I tried to set the padding option:
within the Layout
on TLabelframe itself
on TLabelframe.Label
but the label did not move a pixel. How to achieve this?
Generally I am very confused about what identifiers and options are legal within ttk:style layout, ttk:style element and ttk:style configure, because documentation is hazy and scattered all over the 'net, and there are no error messages whatsoever. Any helpful tips?
Edit: What I found out since posting:
The Labelframe label is a separate widget altogether, with the class TLabelframe.Label.
It is possible to override its layout and add a spacer on top, shifting the text down.
However, the label widget is v-centered on the frame line. If its height increases, it pushes "upward" as much as downward. I found no way to alter the alignment w.r.t. to the actual frame.
It might be possible to replace Labelframe altogether with a custom Frame subclass with the desired layout. But that means changing the "client" code in many places. :-/
This can be done by changing the layout definitions so that the text element is held by the Labelframe layout and the Layoutframe.Label no longer draws the text element. Adding a bit of padding ensures the contained widgets leave the label clear.
Example code:
import sys
import tkinter as tk
import tkinter.ttk as ttk
class CustomLabelframe(ttk.Labelframe):
def __init__(self, master, **kwargs):
"""Initialize the widget with the custom style."""
kwargs["style"] = "Custom.Labelframe"
super(CustomLabelframe, self).__init__(master, **kwargs)
#staticmethod
def register(master):
style = ttk.Style(master)
layout = CustomLabelframe.modify_layout(style.layout("TLabelframe"), "Custom")
style.layout('Custom.Labelframe.Label', [
('Custom.Label.fill', {'sticky': 'nswe'})])
style.layout('Custom.Labelframe', [
('Custom.Labelframe.border', {'sticky': 'nswe', 'children': [
('Custom.Labelframe.text', {'side': 'top'}),
('Custom.Labelframe.padding', {'side': 'top', 'expand': True})
]})
])
if (style.configure('TLabelframe')):
style.configure("Custom.Labelframe", **style.configure("TLabelframe"))
# Add space to the top to prevent child widgets overwriting the label.
style.configure("Custom.Labelframe", padding=(0,12,0,0))
style.map("Custom.Labelframe", **style.map("TLabelframe"))
master.bind("<<ThemeChanged>>", lambda ev: CustomLabelframe.register(ev.widget))
#staticmethod
def modify_layout(layout, prefix):
"""Copy a style layout and rename the elements with a prefix."""
result = []
for item in layout:
element,desc = item
if "children" in desc:
desc["children"] = HistoryCombobox.modify_layout(desc["children"], prefix)
result.append((f"{prefix}.{element}",desc))
return result
class App(ttk.Frame):
"""Test application for the custom widget."""
def __init__(self, master, **kwargs):
super(App, self).__init__(master, **kwargs)
self.master.wm_geometry("640x480")
frame = self.create_themesframe()
frame.pack(side=tk.TOP, fill=tk.BOTH)
for count in range(3):
frame = CustomLabelframe(self, text=f"Frame {count}", width=160, height=80)
frame.pack(side=tk.TOP, expand=True, fill=tk.BOTH)
button = ttk.Button(frame, text="Test")
button.pack(side=tk.LEFT)
self.pack(side=tk.TOP, expand=True, fill=tk.BOTH)
def create_themesframe(self):
frame = ttk.Frame(self)
label = ttk.Label(frame, text="Theme: ")
themes = ttk.Combobox(frame, values=style.theme_names(), state="readonly")
themes.current(themes.cget("values").index(style.theme_use()))
themes.bind("<<ComboboxSelected>>", lambda ev: style.theme_use(ev.widget.get()))
label.pack(side=tk.LEFT)
themes.pack(side=tk.LEFT)
return frame
def main(args=None):
global root, app, style
root = tk.Tk()
style = ttk.Style(root)
CustomLabelframe.register(root)
app = App(root)
try:
import idlelib.pyshell
sys.argv = [sys.argv[0], "-n"]
root.bind("<Control-i>", lambda ev: idlelib.pyshell.main())
except Exception as e:
print(e)
root.mainloop()
return 0
if __name__ == '__main__':
sys.exit(main(sys.argv[1:]))
It is relatively easy to place ttk.Labelframe text below, on or above the relief graphic. This example uses the text attribute but labelwidget can also be used.
In order for the relief to be visible the background color of Labelframe.Label must be set to "".
import tkinter as tk
from tkinter import font
from tkinter import ttk
message = "Hello World"
master = tk.Tk()
style = ttk.Style(master)
style.theme_use(themename = "default")
actualFont = font.Font(
family = "Courier New", size = 20, weight = "bold")
style.configure(
"TLabelframe.Label", background = "", font = actualFont)
frame = ttk.LabelFrame(
master, labelanchor = "n", text = message)
frame.grid(sticky = tk.NSEW)
frame.rowconfigure(0, weight = 1)
frame.columnconfigure(0, weight = 1)
def change_heading():
if frame["text"][0] == "\n":
frame["text"] = f"{message}\n"
else:
frame["text"] = f"\n{message}"
button = tk.Button(
frame, text = "Change", command = change_heading)
button.grid(sticky = "nsew")
master.mainloop()
Looking through the source of the Labelframe widget, I found that:
The label is either placed vertically-centered on the frame's border, or flush above it, depending on the -labeloutside config option. (for default NW anchor)
i.e. by adding whitespace on top of the text by any means, the label box will extend upwards the same amount as downwards, creating a "dead space" above the frame.
There might still be a way to get it "inside" by increasing the border width, but I couldn't get it to work.
I now used the labeloutside option to make a "tab-like" heading.
# ... (define $images array much earlier) ...
ttk::style element create Labelframe.border image $images(card2) \
-border 6 -padding 6 -sticky nsew
ttk::style configure TLabelframe -padding {8 8 8 8} -labeloutside 1 -labelmargins {2 2 2 0}
ttk::style element create Label.fill image $images(header2) -height 31 -padding {8 0 16 0} -border 1
With suitable images, this is nearly what I was aiming for, only that the header does not stretch across the full frame width. Tkinter elements use a "9-patch"-like subdivision strategy for images, so you can make stretchable frames using the -border argument for element create.
Result is approximately this:
+-------------+
| Heading |
+-------------+----------------+
| ... |

Why a button in a scrollable canvas doesn't move? (Python3 + tkinter)

I'm tring to write a program in python3 using the tkinter module. I've created a canvas widget with a y scrollbar, but as I try to add a button in the canvas and scroll the region, the button doesn't move. Here is the code:
# defining the tool bar
class toolBar(object):
def __init__(self, master):
''' creates the toolbar object '''
self.master = master
# creating the toolbarobject
self.toolbar = tk.Canvas(self.master, width=70, height=200, bg="lightgrey")
self.toolbar.grid(row=0, column=1, sticky="nwes", rowspan=2)
self.toolbar.configure(scrollregion=(0, 0, 0, 2000))
b1 = tk.Button(self.toolbar, text="Try")
b1.grid()
# creating the y scrollling
self.scroll_y = tk.Scrollbar(self.parent.master, orient="vertical", command=self.toolbar.yview)
self.scroll_y.grid(row=0, column=0, sticky="ns", rowspan=2)
self.toolbar.configure(yscrollcommand=self.scroll_y.set)
where the master is a tk.Tk() object passed to the class. Do you have any solution for this issue?
P.S.: I have another question: when I run my program, the canvas that contains the button fits the button width, is it possible to place the button whitout changing the canvas width?
The canvas can't scroll items added with pack, place, or grid. It will only scroll widgets added with the create_window method.

Tkinter capturing location of two different buttons in Button1 click and release

I have a grid of buttons in an 8x12 grid. Eventually, I want to be able to color a section (like a top left 3x3 grid) a specific color. For now, I have this question. Is it possible to get one button widget using button.bind("<Button-1>", myfunc2) and then get a second button widget using button.bind("<ButtonRelease-1>", myfunc2)? An outline of the code I have right now is below
class MyApp:
def __init__(self, main):
self.button_frame = tk.Frame(main)
tk.Grid.rowconfigure(root, 0, weight=1)
tk.Grid.columnconfigure(root, 0, weight=1)
self.button_frame.grid(row=0, column=0, sticky='nsew')
self.grid = tk.Frame(self.button_frame)
self.grid.grid(sticky='nsew', column=0, row=7, columnspan=2)
tk.Grid.rowconfigure(self.button_frame, 7, weight=1)
tk.Grid.columnconfigure(self.button_frame, 0, weight=1)
self.button_list = {}
self.createbuttongrid()
def createbuttongrid(self):
label = 1
for row in range(8):
for column in range(12):
button = tk.Button(self.button_frame, text='Well %s' % label)
button.bind("<Button-1>", self.buttonclick) # this line is in question
button.bind("<ButtonRelease-1>", self.buttonrelease) # along with this line
button.grid(row=row, column=column, sticky='nsew')
self.button_list[button] = (row, column)
label += 1
def buttonclick(self, event):
first_button = event.widget
print(self.button_list[first_button])
def buttonrelease(self, event):
second_button = event.widget
print(self.button_list[second_button])
if __name__ == "__main__":
import tkinter as tk
root = tk.Tk()
MyApp(main=root)
root.mainloop()
(The resizing (with the above example) doesn't work perfectly, but that's not important for now.)
Currently, when I run this and click on the top left button, I get (0, 0), and when I release I also get (0, 0). I think this is because the same widget is being passed into def buttonclick and def buttonrelease, but I'm not 100% sure
The <ButtonRelease> event will return the same widget that caught the <ButtonPress> event. That is due to the fact that clicking on a button causes the button to do a grab, which means all events are sent to the button rather than to any other button.
You can use the universal widget method winfo_containing to determine the widget at a given x/y coordinate. You must give it an x and y coordinate relative to the root window, which is conveniently supplied by the event object passed to an event handler.
def buttonrelease(event):
second_button = event.widget.winfo_containing(event.x_root, event.y_root)
...

tkinter resize label width (grid_propagate not working)

I'm having problem with adjusting the width of the label to reflect current width of the window. When the window size changes I'd like label to fill the rest of the width that is left after other widgets in row consume width they need.
Putting the label in a Frame and using grid_propagate(False) does not seem to work.
Consider following code:
import tkinter as tk
import tkinter.ttk as ttk
class PixelLabel(ttk.Frame):
def __init__(self,master, w, h=20, *args, **kwargs):
'''
creates label inside frame,
then frame is set NOT to adjust to child(label) size
and the label keeps extending inside frame to fill it all,
whatever long text inside it is
'''
ttk.Frame.__init__(self, master, width=w, height=h,borderwidth=1)
#self.config(highlightbackground="blue")
self.grid_propagate(False) # don't shrink
self.label = ttk.Label(*args, **kwargs)
self.label.grid(sticky='nswe')
def resize(self,parent,*other_lenghts):
'''
resizes label to take rest of the width from parent
that other childs are not using
'''
parent.update()
new_width = parent.winfo_width()
print(new_width)
for lenght in other_lenghts:
new_width -= lenght
print(new_width)
self.configure(width = new_width)
root = tk.Tk()
master = ttk.Frame(root)
master.grid()
label = ttk.Label(master,text='aaa',borderwidth=1, relief='sunken')
label.grid(row=0,column=0)
label1_width = 7
label1 = ttk.Label(master,text='bbbb',borderwidth=1, relief='sunken',width=label1_width)
label1.grid(row=0,column=1)
label2 = ttk.Label(master,text='ccccccccccccccccccccccccccccccccccccc',borderwidth=1, relief='sunken')
label2.grid(row=0,column=2)
label3_width = 9
label2 = ttk.Label(master,text='ddddd',borderwidth=1, relief='sunken',width=label2_width)
label2.grid(row=0,column=3)
label4 = ttk.Label(master,text='ee',borderwidth=1, relief='sunken')
label4.grid(row=1,column=0)
label5 = ttk.Label(master,text='f',borderwidth=1, relief='sunken')
label5.grid(row=1,column=1,sticky='we')
nest_frame = ttk.Frame(master)
nest_frame.grid(row=2,columnspan=4)
label8_width = 9
label8 = ttk.Label(nest_frame,text='xxxxx',borderwidth=1, relief='sunken',width=label8_width)
label8.grid(row=0,column=0)
label9 = PixelLabel(nest_frame, 5, text='should be next to xxxxx but is not?',borderwidth=1, relief='sunken')
label9.grid(row=0,column=1)
label9.resize(root,label2_width)
root.mainloop()
Why label9 does not appear next to label8
How to make label9 resize to meet current window size (this code is just a sample, I would like to be able to resize label9 as the window size changes dynamically when functions are reshaping the window)
It's not clear why you are using a label in a frame. I suspect this is an XY problem. You can get labels to consume extra space without resorting to putting labels inside frames. However, since you posted some very specific code with very specific questions, that's what I'll address.
Why label9 does not appear next to label8
Because you are creating the label as a child of the root window rather than a child of the frame. You need to create the label as a child of self inside PixelLabel:
class PixelLabel(...):
def __init__(...):
...
self.label = ttk.Label(self, ...)
...
How to make label9 resize to meet current window size (this code is just a sample, I would like to be able to resize label9 as the window size changes dynamically when functions are reshaping the window)
There are a couple more problems. First, you need to give column zero of the frame inside PixelFrame a non-zero weight so that it uses all available space (or, switch to pack).
class PixelLabel(...):
def __init__(...):
...
self.grid_columnconfigure(0, weight=1)
...
Second, you need to use the sticky attribute when placing nest_frame in the window so that it expands to fill its space:
nest_frame.grid(..., sticky="ew")

Python tkinter Copy Widget to New Master

I wrote a collapsible frame widget and was hoping to give it a dock/undock property. From what I've read, widgets can't be placed "over" other widgets (except on canvases, which I wish to avoid) so I can't just "lift" the frame, and their master cannot be changed so I can't simply place the frame into a new Toplevel. The only other option I can think of is to copy the widget into the new Toplevel. Unfortunately, I don't see any options on the copy or deepcopy operations to change the Master before the new widget is created.
So, the question:
Are these assumptions accurate, or is there a way to do any of these things?
If not, do I have any other options than the solution I put together here:
def copywidget(self, frame1, frame2):
for child in frame1.winfo_children():
newwidget = getattr(tkinter,child.winfo_class())(frame2)
for key in child.keys(): newwidget[key] = child.cget(key)
if child.winfo_manager() == 'pack':
newwidget.pack()
for key in child.pack_info():
newwidget.pack_info()[key] = child.pack_info()[key]
elif child.winfo_manager() == 'grid':
newwidget.grid()
for key in child.grid_info():
newwidget.grid_info()[key] = child.grid_info()[key]
elif child.winfo_manager() == 'place':
newwidget.place()
for key in child.place_info():
newwidget.place_info()[key] = child.place_info()[key]
There is no way to reparent a widget to a different toplevel. The easiest thing is to make a method that recreates the widgets in a new parent.
Widgets can be stacked on top of each other, though it requires care. You can, for instance, use grid to place two widgets in the same cell, and you can use place to put one widget on top of another .
Using this answer you are able to copy one widget onto another master. Then, you simply forget the previous widget, and it will work as a moved widget.
Example code:
root = tk.Tk()
frame1 = tk.Frame(root, bg='blue', width=200, height=100)
frame1.grid(row=0, column=0, pady=(0, 5))
frame_to_clone = tk.Frame(frame1)
frame_to_clone.place(x=10, y= 15)
tk.Label(frame_to_clone, text='test text').grid(row=0, column=0)
frame2 = tk.Frame(root, bg='green', width=80, height=180)
frame2.grid(row=1, column=0, pady=(0, 5))
# Move frame_to_clone from frame1 to frame2
cloned_frame = clone_widget(frame_to_clone, frame2)
cloned_frame.place(x=10, y=15)
# frame_to_clone.destroy() # Optional destroy or forget of the original widget
root.mainloop()
Gives:

Resources