How to place an idlelib Hovertip? - python

The current implementation of Hovertip, packs a Label. I overrided the Hovertip class to place it relative to mouse position like this:
class AbsoluteHovertip(Hovertip):
def __init__(self, anchor_widget, text):
super().__init__(anchor_widget, text)
def showtip(self, x, y):
if self.tipwindow:
return
self.tipwindow = tw = Toplevel(self.anchor_widget)
tw.wm_overrideredirect(1)
try:
tw.tk.call("::tk::unsupported::MacWindowStyle", "style", tw._w,
"help", "noActivates")
except TclError:
pass
self.position_window()
self.showcontents(x, y)
self.tipwindow.update_idletasks()
self.tipwindow.lift()
def showcontents(self, x, y):
label = Label(self.tipwindow, text=self.text, justify=LEFT,
background="#ffffe0", relief=SOLID, borderwidth=1)
label.place(x=x+10, y=y+10)
I am using it like this inside a Treeview:
# self.tv is Treeview
# self.htip is AbsoluteHovertip(self.tv, "")
# self.events is just a list
def treeview_tooltip(self, event):
row = self.tv.identify_row(event.y)
column = self.tv.identify_column(event.x)
values = self.tv.item(row, "values")
if column == "#2":
index = int(values[0])
ev = self.events[index]
self.htip.text = int(ev.id)
self.htip.showtip(event.x, event.y)
elif column == "#3":
text = values[2]
if len(text) >= 30:
self.htip.text = text
self.htip.showtip(event.x, event.y)
else:
self.htip.hidetip()
self.tv.tk.call(self.tv, "tag", "remove", "highlight")
self.tv.tk.call(self.tv, "tag", "add", "highlight", row)
However instead of showing up as expected, a square box of about 100x100 or more shows up below the Treeview at the same place a normal HoverTip would appear.

I've been playing with this and your showtip override gave me an idea. Inside showtip there is a call to position_window so I override that and was able to change the position of the tip window. You can also do it with self.tipwindow.geometry() (probably in any override) if you can get the right position that you need. I needed relative repositioning so override position_window worked well for me. Below I am repositioning the tipwindow by the width of the anchor widget. I also overloaded showcontents so that I could style the tip window.
class Tip(Hovertip):
def position_window(self):
"""(re)-set the tooltip's screen position"""
x, y = self.get_position()
root_x = self.anchor_widget.winfo_rootx() + x - self.anchor_widget.winfo_width()
root_y = self.anchor_widget.winfo_rooty() + y
self.tipwindow.wm_geometry("+%d+%d" % (root_x, root_y))
def showcontents(self):
label = Label(self.tipwindow, text=self.text, justify=LEFT,
relief=SOLID, font=("Segoe UI", 12), borderwidth=0,
background=background, foreground=foreground,
padx=10, pady=10)
label.pack()
Tip(set_btn, "Settings", hover_delay=0)

Related

Getting line mouse is on tkinter text

I am trying to make a sidebar for a tkinter Text widget that displays a red dot next to the line that the mouse hovers over. Currently it is configured to go to the selected line but would be better if it tracked the mouse. I know how to track the mouses y position with root.winfo_pointery() but don't know how the get the corresponding line for that. I also don't want it to display anything if it is out of the text widget's y area or if there is no line. How do I cross compare the y value of the pointer to text lines?
Current code:
from tkinter import *
class BreakpointBar(Canvas):
def __init__(self, *args, **kwargs):
#Initializes the canvas
Canvas.__init__(self, *args, **kwargs, highlightthickness=0)
self.textwidget = None
self.ovals = []
def attach(self, text_widget):
#Attaches the canvas to the text widget
self.textwidget = text_widget
def redraw(self, *args):
#Redraws the canvas
"""redraw line numbers"""
# try:
self.delete("all")
self.unbind_all("<Enter>")
self.unbind_all("<Leave>")
self.ovals = []
index = self.textwidget.index("insert")
index_linenum = str(index).split(".")[0]
i = self.textwidget.index("#0,0")
print(self.winfo_pointerx())
print(self.winfo_pointery())
while True :
dline= self.textwidget.dlineinfo(i)
if dline is None: break
y = dline[1]
linenum = str(i).split(".")[0]
if linenum == index_linenum:
oval = self.create_oval(5, y, 15, y+10, fill="red", outline="red")
self.tag_bind(oval, "<Enter>", lambda event: self.on_enter(event, oval))
self.tag_bind(oval, "<Leave>", lambda event: self.on_exit(event, oval))
self.tag_bind(oval, "<Button-1>", lambda event: self.on_press(event, oval))
self.ovals.append(oval)
i = self.textwidget.index("%s+1line" % i)
# except:
# pass
def on_enter(self, event, oval):
self.itemconfig(oval, fill="dark red", outline="dark red")
def on_exit(self, event, oval):
self.itemconfig(oval, fill="red", outline="red")
def on_press(self, event, oval):
index_linenum = int(str(self.textwidget.index("insert")).split(".")[0])
self.textwidget.insert("{}.end".format(index_linenum), "\nbreakpoint\n")
self.textwidget.mark_set("insert", "{}.0".format(index_linenum+2))
self.textwidget.see("insert")
root = Tk()
frame = Frame(root)
frame.pack(expand=True, fill=BOTH)
text=Text(frame)
text.pack(side=RIGHT, fill=BOTH, expand=True)
bb = BreakpointBar(frame, width=20)
bb.attach(text)
bb.pack(side=LEFT, fill=Y)
root.bind("<Button-1>", bb.redraw)
root.bind("<KeyRelease-Return>", bb.redraw)
root.mainloop()
You can get the index for character nearest to the mouse via an x,y coordinate using the format #x,y.
For example, if you have a function bound to the <Motion> event, you can get the index under the cursor by doing something like this:
def track_mouse(event):
index = event.widget.index(f"#{event.x},{event.y}")
...
In the above example, index will be in the canonical form of a string in the format line.character.
It's important to note that you must give coordinates that are relative to the upper-left corner of the text widget itself, not of the window or display as a whole.

Scrollable Frame with fill in Python Tkinter

I am trying to implement a scrollable frame in Python with Tkinter:
if the content changes, the size of the widget is supposed to stay constant (basically, I don't really care whether the size of the scrollbar is subtracted from the frame or added to the parent, although I do think that it would make sense if this was consistent but that does not seem to be the case currently)
if the content becomes too big a scrollbar shall appear so that one can scroll over the entire content (but not further)
if the content fits entirely into the widget the scrollbar shall disappear and it shall not be possible to scroll anymore (no need to scroll, because everything is visible)
if the req size of the content becomes smaller than the widget, the content shall fill the widget
I am surprised how difficult it seems to get this running, because it seems like a pretty basic functionality.
The first three requirements seem relatively easy but I am having a lot of trouble since trying to fill the widget.
The following implementation has the following problems:
first time a scrollbar appears, frame does not fill canvas (seems to depend on available space):
add one column. The horizontal scrollbar appears. Between the scrollbar and the white background of the frame the red background of the canvas becomes visible. This red area looks around as high as the scrollbar.
When adding or removing a row or column or resizing the window the red area disappears and does not seem to appear again.
size jumps:
add elements until horizontal scrollbar becomes visible. make window wider (not higher). the height [!] of the window increases with a jump.
infinite loop:
add rows until the vertical scrollbar appears, remove one row so that vertical scrollbar disappears again, add one row again. The window's size is rapidly increasing and decreasing. The occurence of this behaviour depends on the size of the window. The loop can be broken by resizing or closing the window.
What am I doing wrong?
Any help would be appreciated.
#!/usr/bin/env python
# based on https://stackoverflow.com/q/30018148
try:
import Tkinter as tk
except:
import tkinter as tk
# I am not using something like vars(tk.Grid) because that would override too many methods.
# Methods like Grid.columnconfigure are suppossed to be executed on self, not a child.
GM_METHODS_TO_BE_CALLED_ON_CHILD = (
'pack', 'pack_configure', 'pack_forget', 'pack_info',
'grid', 'grid_configure', 'grid_forget', 'grid_remove', 'grid_info',
'place', 'place_configure', 'place_forget', 'place_info',
)
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:
self.grid_remove()
else:
self.grid()
tk.Scrollbar.set(self, lo, hi)
def pack(self, *args, **kwargs):
raise TclError('Cannot use pack with this widget.')
def place(self, *args, **kwargs):
raise TclError('Cannot use place with this widget.')
#TODO: first time a scrollbar appears, frame does not fill canvas (seems to depend on available space)
#TODO: size jumps: add elements until horizontal scrollbar becomes visible. make widget wider. height jumps from 276 to 316 pixels although it should stay constant.
#TODO: infinite loop is triggered by
# - add rows until the vertical scrollbar appears, remove one row so that vertical scrollbar disappears again, add one row again (depends on size)
# was in the past triggered by:
# - clicking "add row" very fast at transition from no vertical scrollbar to vertical scrollbar visible
# - add columns until horizontal scrollbar appears, remove column so that horizointal scrollbar disappears again, add rows until vertical scrollbar should appear
class ScrollableFrame(tk.Frame):
def __init__(self, master, *args, **kwargs):
self._parentFrame = tk.Frame(master)
self._parentFrame.grid_rowconfigure(0, weight = 1)
self._parentFrame.grid_columnconfigure(0, weight = 1)
# scrollbars
hscrollbar = AutoScrollbar(self._parentFrame, orient = tk.HORIZONTAL)
hscrollbar.grid(row = 1, column = 0, sticky = tk.EW)
vscrollbar = AutoScrollbar(self._parentFrame, orient = tk.VERTICAL)
vscrollbar.grid(row = 0, column = 1, sticky = tk.NS)
# canvas & scrolling
self.canvas = tk.Canvas(self._parentFrame,
xscrollcommand = hscrollbar.set,
yscrollcommand = vscrollbar.set,
bg = 'red', # should not be visible
)
self.canvas.grid(row = 0, column = 0, sticky = tk.NSEW)
hscrollbar.config(command = self.canvas.xview)
vscrollbar.config(command = self.canvas.yview)
# self
tk.Frame.__init__(self, self.canvas, *args, **kwargs)
self._selfItemID = self.canvas.create_window(0, 0, window = self, anchor = tk.NW)
# bindings
self.canvas.bind('<Enter>', self._bindMousewheel)
self.canvas.bind('<Leave>', self._unbindMousewheel)
self.canvas.bind('<Configure>', self._onCanvasConfigure)
# geometry manager
for method in GM_METHODS_TO_BE_CALLED_ON_CHILD:
setattr(self, method, getattr(self._parentFrame, method))
def _bindMousewheel(self, event):
# Windows
self.bind_all('<MouseWheel>', self._onMousewheel)
# Linux
self.bind_all('<Button-4>', self._onMousewheel)
self.bind_all('<Button-5>', self._onMousewheel)
def _unbindMousewheel(self, event):
# Windows
self.unbind_all('<MouseWheel>')
# Linux
self.unbind_all('<Button-4>')
self.unbind_all('<Button-5>')
def _onMousewheel(self, event):
if event.delta < 0 or event.num == 5:
dy = +1
elif event.delta > 0 or event.num == 4:
dy = -1
else:
assert False
if (dy < 0 and self.canvas.yview()[0] > 0.0) \
or (dy > 0 and self.canvas.yview()[1] < 1.0):
self.canvas.yview_scroll(dy, tk.UNITS)
return 'break'
def _onCanvasConfigure(self, event):
self._updateSize(event.width, event.height)
def _updateSize(self, canvWidth, canvHeight):
hasChanged = False
requWidth = self.winfo_reqwidth()
newWidth = max(canvWidth, requWidth)
if newWidth != self.winfo_width():
hasChanged = True
requHeight = self.winfo_reqheight()
newHeight = max(canvHeight, requHeight)
if newHeight != self.winfo_height():
hasChanged = True
if hasChanged:
print("update size ({width}, {height})".format(width = newWidth, height = newHeight))
self.canvas.itemconfig(self._selfItemID, width = newWidth, height = newHeight)
return True
return False
def _updateScrollregion(self):
bbox = (0,0, self.winfo_reqwidth(), self.winfo_reqheight())
print("updateScrollregion%s" % (bbox,))
self.canvas.config( scrollregion = bbox )
def updateScrollregion(self):
# a function called with self.bind('<Configure>', ...) is called when resized or scrolled but *not* when widgets are added or removed (is called when real widget size changes but not when required/requested widget size changes)
# => useless for calling this function
# => this function must be called manually when adding or removing children
# The content has changed.
# Therefore I need to adapt the size of self.
# I need to update before measuring the size.
# It does not seem to make a difference whether I use update_idletasks() or update().
# Therefore according to Bryan Oakley I better use update_idletasks https://stackoverflow.com/a/29159152
self.update_idletasks()
self._updateSize(self.canvas.winfo_width(), self.canvas.winfo_height())
# update scrollregion
self._updateScrollregion()
def setWidth(self, width):
print("setWidth(%s)" % width)
self.canvas.configure( width = width )
def setHeight(self, height):
print("setHeight(%s)" % width)
self.canvas.configure( height = height )
def setSize(self, width, height):
print("setSize(%sx%s)" % (width, height))
self.canvas.configure( width = width, height = height )
# ==================== TEST ====================
if __name__ == '__main__':
class Test(object):
BG_COLOR = 'white'
PAD_X = 1
PAD_Y = PAD_X
# ---------- initialization ----------
def __init__(self):
self.root = tk.Tk()
self.buttonFrame = tk.Frame(self.root)
self.buttonFrame.pack(side=tk.TOP)
self.scrollableFrame = ScrollableFrame(self.root, bg=self.BG_COLOR)
self.scrollableFrame.pack(side=tk.TOP, expand=tk.YES, fill=tk.BOTH)
self.scrollableFrame.grid_columnconfigure(0, weight=1)
self.scrollableFrame.grid_rowconfigure(0, weight=1)
self.contentFrame = tk.Frame(self.scrollableFrame, bg=self.BG_COLOR)
self.contentFrame.grid(row=0, column=0, sticky=tk.NSEW)
self.labelRight = tk.Label(self.scrollableFrame, bg=self.BG_COLOR, text="right")
self.labelRight.grid(row=0, column=1)
self.labelBottom = tk.Label(self.scrollableFrame, bg=self.BG_COLOR, text="bottom")
self.labelBottom.grid(row=1, column=0)
tk.Button(self.buttonFrame, text="add row", command=self.addRow).grid(row=0, column=0)
tk.Button(self.buttonFrame, text="remove row", command=self.removeRow).grid(row=1, column=0)
tk.Button(self.buttonFrame, text="add column", command=self.addColumn).grid(row=0, column=1)
tk.Button(self.buttonFrame, text="remove column", command=self.removeColumn).grid(row=1, column=1)
self.row = 0
self.col = 0
def start(self):
self.addRow()
widget = self.contentFrame.grid_slaves()[0]
width = widget.winfo_width() + 2*self.PAD_X + self.labelRight.winfo_width()
height = 4.9*( widget.winfo_height() + 2*self.PAD_Y ) + self.labelBottom.winfo_height()
#TODO: why is size saved in event different from what I specify here?
self.scrollableFrame.setSize(width, height)
# ---------- add ----------
def addRow(self):
if self.col == 0:
self.col = 1
columns = self.col
for col in range(columns):
button = self.addButton(self.row, col)
self.row += 1
self._onChange()
def addColumn(self):
if self.row == 0:
self.row = 1
rows = self.row
for row in range(rows):
button = self.addButton(row, self.col)
self.col += 1
self._onChange()
def addButton(self, row, col):
button = tk.Button(self.contentFrame, text = '--------------------- %d, %d ---------------------' % (row, col))
# note that grid(padx) seems to behave differently from grid_columnconfigure(pad):
# grid : padx = "Optional horizontal padding to place around the widget in a cell."
# grid_rowconfigure: pad = "Padding to add to the size of the largest widget in the row when setting the size of the whole row."
# http://effbot.org/tkinterbook/grid.htm
button.grid(row=row, column=col, sticky=tk.NSEW, padx=self.PAD_X, pady=self.PAD_Y)
# ---------- remove ----------
def removeRow(self):
if self.row <= 0:
return
self.row -= 1
columns = self.col
if columns == 0:
return
for button in self.contentFrame.grid_slaves():
info = button.grid_info()
if info['row'] == self.row:
button.destroy()
self._onChange()
def removeColumn(self):
if self.col <= 0:
return
self.col -= 1
rows = self.row
if rows == 0:
return
for button in self.contentFrame.grid_slaves():
info = button.grid_info()
if info['column'] == self.col:
button.destroy()
self._onChange()
# ---------- other ----------
def _onChange(self):
print("=========== user action ==========")
print("new size: %s x %s" % (self.row, self.col))
self.scrollableFrame.updateScrollregion()
def mainloop(self):
self.root.mainloop()
test = Test()
test.start()
test.mainloop()
EDIT: I do not think that this is a duplicate of this question. The answer to that question is certainly a good starting point if you don't know how to start. It explains the basic concept of how to handle scrollbars in Tkinter. That however, is not my problem. I think that I am aware of the basic idea and I think that I have implemented that.
I have noticed that the answer mentions the possibility of directly drawing on the canvas instead of putting a frame on it. However, I would like to have a reusable solution.
My problem is that when I tried to implement that the content shall fill the frame (like with pack(expand=tk.YES, fill=tk.BOTH)) if the req size is smaller than the size of the canvas the three above listed weird effects occured which I do not understand. Most importantly that is that the program runs into an infinite loop when I add and remove rows as described (without changing the window size).
EDIT 2: I have reduced the code even further:
# based on https://stackoverflow.com/q/30018148
try:
import Tkinter as tk
except:
import tkinter as tk
class AutoScrollbar(tk.Scrollbar):
def set(self, lo, hi):
if float(lo) <= 0.0 and float(hi) >= 1.0:
self.grid_remove()
else:
self.grid()
tk.Scrollbar.set(self, lo, hi)
class ScrollableFrame(tk.Frame):
# ---------- initialization ----------
def __init__(self, master, *args, **kwargs):
self._parentFrame = tk.Frame(master)
self._parentFrame.grid_rowconfigure(0, weight = 1)
self._parentFrame.grid_columnconfigure(0, weight = 1)
# scrollbars
hscrollbar = AutoScrollbar(self._parentFrame, orient = tk.HORIZONTAL)
hscrollbar.grid(row = 1, column = 0, sticky = tk.EW)
vscrollbar = AutoScrollbar(self._parentFrame, orient = tk.VERTICAL)
vscrollbar.grid(row = 0, column = 1, sticky = tk.NS)
# canvas & scrolling
self.canvas = tk.Canvas(self._parentFrame,
xscrollcommand = hscrollbar.set,
yscrollcommand = vscrollbar.set,
bg = 'red', # should not be visible
)
self.canvas.grid(row = 0, column = 0, sticky = tk.NSEW)
hscrollbar.config(command = self.canvas.xview)
vscrollbar.config(command = self.canvas.yview)
# self
tk.Frame.__init__(self, self.canvas, *args, **kwargs)
self._selfItemID = self.canvas.create_window(0, 0, window = self, anchor = tk.NW)
# bindings
self.canvas.bind('<Configure>', self._onCanvasConfigure)
# ---------- setter ----------
def setSize(self, width, height):
print("setSize(%sx%s)" % (width, height))
self.canvas.configure( width = width, height = height )
# ---------- listen to GUI ----------
def _onCanvasConfigure(self, event):
self._updateSize(event.width, event.height)
# ---------- listen to model ----------
def updateScrollregion(self):
self.update_idletasks()
self._updateSize(self.canvas.winfo_width(), self.canvas.winfo_height())
self._updateScrollregion()
# ---------- internal ----------
def _updateSize(self, canvWidth, canvHeight):
hasChanged = False
requWidth = self.winfo_reqwidth()
newWidth = max(canvWidth, requWidth)
if newWidth != self.winfo_width():
hasChanged = True
requHeight = self.winfo_reqheight()
newHeight = max(canvHeight, requHeight)
if newHeight != self.winfo_height():
hasChanged = True
if hasChanged:
print("update size ({width}, {height})".format(width = newWidth, height = newHeight))
self.canvas.itemconfig(self._selfItemID, width = newWidth, height = newHeight)
return True
return False
def _updateScrollregion(self):
bbox = (0,0, self.winfo_reqwidth(), self.winfo_reqheight())
print("updateScrollregion%s" % (bbox,))
self.canvas.config( scrollregion = bbox )
# ==================== TEST ====================
if __name__ == '__main__':
labels = list()
def createLabel():
print("========= create label =========")
l = tk.Label(frame, text="test %s" % len(labels))
l.pack(anchor=tk.W)
labels.append(l)
frame.updateScrollregion()
def removeLabel():
print("========= remove label =========")
labels[-1].destroy()
del labels[-1]
frame.updateScrollregion()
root = tk.Tk()
tk.Button(root, text="+", command=createLabel).pack()
tk.Button(root, text="-", command=removeLabel).pack()
frame = ScrollableFrame(root, bg="white")
frame._parentFrame.pack(expand=tk.YES, fill=tk.BOTH)
createLabel()
frame.setSize(labels[0].winfo_width(), labels[0].winfo_height()*5.9)
#TODO: why is size saved in event object different from what I have specified here?
root.mainloop()
procedure to reproduce the infinite loop is unchanged:
click "+" until the vertical scrollbar appears, click "-" once so that vertical scrollbar disappears again, click "+" again. The window's size is rapidly increasing and decreasing. The occurence of this behaviour depends on the size of the window. The loop can be broken by resizing or closing the window.
to reproduce the jump in size:
click "+" until horizontal [!] scrollbar appears (the window height then increases by the size of the scrollbar, which is ok). Increase width of window until horizontal scrollbar disappears. The height [!] of the window increases with a jump.
to reproduce that the canvas is not filled:
comment out the line which calls frame.setSize. Click "+" until vertical scrollbar appears.
Between the scrollbar and the white background of the frame the red background of the canvas becomes visible. This red area looks around as wide as the scrollbar. When clicking "+" or "-" or resizing the window the red area disappears and does not seem to appear again.

Display message when hovering over something with mouse cursor in Python

I have a GUI made with TKinter in Python. I would like to be able to display a message when my mouse cursor goes, for example, on top of a label or button. The purpose of this is to explain to the user what the button/label does or represents.
Is there a way to display text when hovering over a tkinter object in Python?
I think this would meet your requirements.
Here's what the output looks like:
First, A class named ToolTip which has methods showtip and hidetip is defined as follows:
from tkinter import *
class ToolTip(object):
def __init__(self, widget):
self.widget = widget
self.tipwindow = None
self.id = None
self.x = self.y = 0
def showtip(self, text):
"Display text in tooltip window"
self.text = text
if self.tipwindow or not self.text:
return
x, y, cx, cy = self.widget.bbox("insert")
x = x + self.widget.winfo_rootx() + 57
y = y + cy + self.widget.winfo_rooty() +27
self.tipwindow = tw = Toplevel(self.widget)
tw.wm_overrideredirect(1)
tw.wm_geometry("+%d+%d" % (x, y))
label = Label(tw, text=self.text, justify=LEFT,
background="#ffffe0", relief=SOLID, borderwidth=1,
font=("tahoma", "8", "normal"))
label.pack(ipadx=1)
def hidetip(self):
tw = self.tipwindow
self.tipwindow = None
if tw:
tw.destroy()
def CreateToolTip(widget, text):
toolTip = ToolTip(widget)
def enter(event):
toolTip.showtip(text)
def leave(event):
toolTip.hidetip()
widget.bind('<Enter>', enter)
widget.bind('<Leave>', leave)
The widget is where you want to add the tip. For example, if you want the tip when you hover over a button or entry or label, the instance of the same should be provided at the call time.
Quick note: the code above uses from tkinter import *
which is not suggested by some of the programmers out there, and they have valid points. You might want to make necessary changes in such case.
To move the tip to your desired location, you can change x and y in the code.
The function CreateToolTip() helps to create this tip easily. Just pass the widget and string you want to display in the tipbox to this function, and you're good to go.
This is how you call the above part:
button = Button(root, text = 'click mem')
button.pack()
CreateToolTip(button, text = 'Hello World\n'
'This is how tip looks like.'
'Best part is, it\'s not a menu.\n'
'Purely tipbox.')
Do not forget to import the module if you save the previous outline in different python file, and don't save the file as CreateToolTip or ToolTip to avoid confusion.
This post from Fuzzyman shares some similar thoughts, and worth checking out.
You need to set a binding on the <Enter> and <Leave> events.
Note: if you choose to pop up a window (ie: a tooltip) make sure you don't pop it up directly under the mouse. What will happen is that it will cause a leave event to fire because the cursor leaves the label and enters the popup. Then, your leave handler will dismiss the window, your cursor will enter the label, which causes an enter event, which pops up the window, which causes a leave event, which dismisses the window, which causes an enter event, ... ad infinitum.
For simplicity, here's an example that updates a label, similar to a statusbar that some apps use. Creating a tooltip or some other way of displaying the information still starts with the same core technique of binding to <Enter> and <Leave>.
import Tkinter as tk
class Example(tk.Frame):
def __init__(self, *args, **kwargs):
tk.Frame.__init__(self, *args, **kwargs)
self.l1 = tk.Label(self, text="Hover over me")
self.l2 = tk.Label(self, text="", width=40)
self.l1.pack(side="top")
self.l2.pack(side="top", fill="x")
self.l1.bind("<Enter>", self.on_enter)
self.l1.bind("<Leave>", self.on_leave)
def on_enter(self, event):
self.l2.configure(text="Hello world")
def on_leave(self, enter):
self.l2.configure(text="")
if __name__ == "__main__":
root = tk.Tk()
Example(root).pack(side="top", fill="both", expand="true")
root.mainloop()
You can refer to this- HoverClass
It is exactly what you need. Nothing more, nothing less
from Tkinter import *
import re
class HoverInfo(Menu):
def __init__(self, parent, text, command=None):
self._com = command
Menu.__init__(self,parent, tearoff=0)
if not isinstance(text, str):
raise TypeError('Trying to initialise a Hover Menu with a non string type: ' + text.__class__.__name__)
toktext=re.split('\n', text)
for t in toktext:
self.add_command(label = t)
self._displayed=False
self.master.bind("<Enter>",self.Display )
self.master.bind("<Leave>",self.Remove )
def __del__(self):
self.master.unbind("<Enter>")
self.master.unbind("<Leave>")
def Display(self,event):
if not self._displayed:
self._displayed=True
self.post(event.x_root, event.y_root)
if self._com != None:
self.master.unbind_all("<Return>")
self.master.bind_all("<Return>", self.Click)
def Remove(self, event):
if self._displayed:
self._displayed=False
self.unpost()
if self._com != None:
self.unbind_all("<Return>")
def Click(self, event):
self._com()
Example app using HoverInfo:
from Tkinter import *
from HoverInfo import HoverInfo
class MyApp(Frame):
def __init__(self, parent=None):
Frame.__init__(self, parent)
self.grid()
self.lbl = Label(self, text='testing')
self.lbl.grid()
self.hover = HoverInfo(self, 'while hovering press return \n for an exciting msg', self.HelloWorld)
def HelloWorld(self):
print('Hello World')
app = MyApp()
app.master.title('test')
app.mainloop()
Screenshot:
I have a very hacky solution but it has some advantages over the current answers so I figured I would share it.
lab=Label(root,text="hover me")
lab.bind("<Enter>",popup)
def do_popup(event):
# display the popup menu
root.after(1000, self.check)
popup = Menu(root, tearoff=0)
popup.add_command(label="Next")
popup.tk_popup(event.x_root, event.y_root, 0)
def check(event=None):
x, y = root.winfo_pointerxy()
widget = root.winfo_containing(x, y)
if widget is None:
root.after(100, root.check)
else:
leave()
def leave():
popup.delete(0, END)
The only real issue with this is it leaves behind a small box that moves focus away from the main window
If anyone knows how to solve these issues let me know
If anyone is on Mac OSX and tool tip isn't working, check out the example in:
https://github.com/python/cpython/blob/master/Lib/idlelib/tooltip.py
Basically, the two lines that made it work for me on Mac OSX were:
tw.update_idletasks() # Needed on MacOS -- see #34275.
tw.lift() # work around bug in Tk 8.5.18+ (issue #24570)
Here is a simple solution to your problem that subclasses the tk.Button object. We make a special class that tk.Button inherits from, giving it tooltip functionality. The same for tk.Labels.
I don't know what would be cleanest and the easiest way to maintain code for keeping track of the text that goes into the tooltips. I present here one way, in which I pass unique widget IDs to MyButtons, and access a dictionary for storing the tooltip texts. You could store this file as a JSON, or as a class attribute, or as a global variable (as below). Alternatively, perhaps it would be better to define a setter method in MyButton, and just call this method every time you create a new widget that should have a tooltip. Although you would have to store the widget instance in a variable, adding one extra line for all widgets to include.
One drawback in the code below is that the self.master.master syntax relies on determining the "widget depth". A simple recursive function will catch most cases (only needed for entering a widget, since by definition you leave somewhere you once were).
Anyway, below is a working MWE for anyone interested.
import tkinter as tk
tooltips = {
'button_hello': 'Print a greeting message',
'button_quit': 'Quit the program',
'button_insult': 'Print an insult',
'idle': 'Hover over button for help',
'error': 'Widget ID not valid'
}
class ToolTipFunctionality:
def __init__(self, wid):
self.wid = wid
self.widet_depth = 1
self.widget_search_depth = 10
self.bind('<Enter>', lambda event, i=1: self.on_enter(event, i))
self.bind('<Leave>', lambda event: self.on_leave(event))
def on_enter(self, event, i):
if i > self.widget_search_depth:
return
try:
cmd = f'self{".master"*i}.show_tooltip(self.wid)'
eval(cmd)
self.widget_depth = i
except AttributeError:
return self.on_enter(event, i+1)
def on_leave(self, event):
cmd = f'self{".master" * self.widget_depth}.hide_tooltip()'
eval(cmd)
class MyButton(tk.Button, ToolTipFunctionality):
def __init__(self, parent, wid, **kwargs):
tk.Button.__init__(self, parent, **kwargs)
ToolTipFunctionality.__init__(self, wid)
class MyLabel(tk.Label, ToolTipFunctionality):
def __init__(self, parent, wid, **kwargs):
tk.Label.__init__(self, parent, **kwargs)
ToolTipFunctionality.__init__(self, wid)
class Application(tk.Tk):
def __init__(self):
tk.Tk.__init__(self)
self.tooltip = tk.StringVar()
self.tooltip.set(tooltips['idle'])
self.frame = tk.Frame(self, width=50)
self.frame.pack(expand=True)
MyLabel(self.frame, '', text='One Cool Program').pack()
self.subframe = tk.Frame(self.frame, width=40)
self.subframe.pack()
MyButton(self.subframe, 'button_hello', text='Hello!', command=self.greet, width=20).pack()
MyButton(self.subframe, 'button_insutl', text='Insult', command=self.insult, width=20).pack()
MyButton(self.subframe, 'button_quit', text='Quit', command=self.destroy, width=20).pack()
tk.Label(self.subframe, textvar=self.tooltip, width=20).pack()
def show_tooltip(self, wid):
try:
self.tooltip.set(tooltips[wid])
except KeyError:
self.tooltip.set(tooltips['error'])
def hide_tooltip(self):
self.tooltip.set(tooltips['idle'])
def greet(self):
print('Welcome, Fine Sir!')
def insult(self):
print('You must be dead from the neck up')
if __name__ == '__main__':
app = Application()
app.mainloop()
The best way I have found to create a popup help window is to use the tix.Balloon. I have modified it below to make it look better and show an example (note the use of tix.Tk):
import tkinter as tk
import tkinter.tix as tix
class Balloon(tix.Balloon):
# A modified tix popup balloon (to change the default delay, bg and wraplength)
init_after = 1250 # Milliseconds
wraplength = 300 # Pixels
def __init__(self, master):
bg = root.cget("bg")
# Call the parent
super().__init__(master, initwait=self.init_after)
# Change background colour
for i in self.subwidgets_all():
i.config(bg=bg)
# Modify the balloon label
self.message.config(wraplength=self.wraplength)
root = tix.Tk()
l = tk.Label(root, text="\n".join(["text"] * 5))
l.pack()
b = Balloon(root.winfo_toplevel())
b.bind_widget(l, balloonmsg="Some random text")
root.mainloop()
OLD ANSWER:
Here is an example using <enter> and <leave> as #bryanoakley suggested with a toplevel (with overridedirect set to true). Use the hover_timer class for easy use of this. This needs the widget and help-text (with an optional delay argument - default 0.5s) and can be easily called just by initiating the class and then cancelling it.
import threading, time
from tkinter import *
class hover_window (Toplevel):
def __init__ (self, coords, text):
super ().__init__ ()
self.geometry ("+%d+%d" % (coords [0], coords [1]))
self.config (bg = "white")
Label (self, text = text, bg = "white", relief = "ridge", borderwidth = 3, wraplength = 400, justify = "left").grid ()
self.overrideredirect (True)
self.update ()
self.bind ("<Enter>", lambda event: self.destroy ())
class hover_timer:
def __init__ (self, widget, text, delay = 2):
self.wind, self.cancel_var, self.widget, self.text, self.active, self.delay = None, False, widget, text, False, delay
threading.Thread (target = self.start_timer).start ()
def start_timer (self):
self.active = True
time.sleep (self.delay)
if not self.cancel_var: self.wind = hover_window ((self.widget.winfo_rootx (), self.widget.winfo_rooty () + self.widget.winfo_height () + 20), self.text)
self.active = False
def delayed_stop (self):
while self.active: time.sleep (0.05)
if self.wind:
self.wind.destroy ()
self.wind = None
def cancel (self):
self.cancel_var = True
if not self.wind: threading.Thread (target = self.delayed_stop).start ()
else:
self.wind.destroy ()
self.wind = None
def start_help (event):
# Create a new help timer
global h
h = hover_timer (l, "This is some additional information.", 0.5)
def end_help (event):
# If therre is one, end the help timer
if h: h.cancel ()
if __name__ == "__main__":
# Create the tkinter window
root = Tk ()
root.title ("Hover example")
# Help class not created yet
h = None
# Padding round label
Frame (root, width = 50).grid (row = 1, column = 0)
Frame (root, height = 50).grid (row = 0, column = 1)
Frame (root, width = 50).grid (row = 1, column = 2)
Frame (root, height = 50).grid (row = 2, column = 1)
# Setup the label
l = Label (root, text = "Hover over me for information.", font = ("sans", 32))
l.grid (row = 1, column = 1)
l.bind ("<Enter>", start_help)
l.bind ("<Leave>", end_help)
# Tkinter mainloop
root.mainloop ()
I wanted to contribute to the answer of #squareRoot17 as he inspired me to shorten his code while providing the same functionality:
import tkinter as tk
class ToolTip(object):
def __init__(self, widget, text):
self.widget = widget
self.text = text
def enter(event):
self.showTooltip()
def leave(event):
self.hideTooltip()
widget.bind('<Enter>', enter)
widget.bind('<Leave>', leave)
def showTooltip(self):
self.tooltipwindow = tw = tk.Toplevel(self.widget)
tw.wm_overrideredirect(1) # window without border and no normal means of closing
tw.wm_geometry("+{}+{}".format(self.widget.winfo_rootx(), self.widget.winfo_rooty()))
label = tk.Label(tw, text = self.text, background = "#ffffe0", relief = 'solid', borderwidth = 1).pack()
def hideTooltip(self):
tw = self.tooltipwindow
tw.destroy()
self.tooltipwindow = None
This class can then be imported and used as:
import tkinter as tk
from tooltip import ToolTip
root = tk.Tk()
your_widget = tk.Button(root, text = "Hover me!")
ToolTip(widget = your_widget, text = "Hover text!")
root.mainloop()

How to make ttk.Treeview's rows editable?

Is there any way to use ttk Treeview with editable rows?
I mean it should work more like a table. For example on double click on the item make the #0 column 'editable'.
If this isn't possible, any way to allow mouse selecting on the item would be just fine. I haven't found any mention of this in tkdocs or other documents.
After long research I haven't found such feature so I guess there's any. Tk is very simple interface, which allows programmer to build 'high-level' features from the basics. So my desired behaviour this way.
def onDoubleClick(self, event):
''' Executed, when a row is double-clicked. Opens
read-only EntryPopup above the item's column, so it is possible
to select text '''
# close previous popups
# self.destroyPopups()
# what row and column was clicked on
rowid = self._tree.identify_row(event.y)
column = self._tree.identify_column(event.x)
# get column position info
x,y,width,height = self._tree.bbox(rowid, column)
# y-axis offset
# pady = height // 2
pady = 0
# place Entry popup properly
text = self._tree.item(rowid, 'text')
self.entryPopup = EntryPopup(self._tree, rowid, text)
self.entryPopup.place( x=0, y=y+pady, anchor=W, relwidth=1)
This is method within a class which composes ttk.Treeview as self._tree
And EntryPopup is then very simple sub-class of Entry:
class EntryPopup(Entry):
def __init__(self, parent, iid, text, **kw):
''' If relwidth is set, then width is ignored '''
super().__init__(parent, **kw)
self.tv = parent
self.iid = iid
self.insert(0, text)
# self['state'] = 'readonly'
# self['readonlybackground'] = 'white'
# self['selectbackground'] = '#1BA1E2'
self['exportselection'] = False
self.focus_force()
self.bind("<Return>", self.on_return)
self.bind("<Control-a>", self.select_all)
self.bind("<Escape>", lambda *ignore: self.destroy())
def on_return(self, event):
self.tv.item(self.iid, text=self.get())
self.destroy()
def select_all(self, *ignore):
''' Set selection on the whole text '''
self.selection_range(0, 'end')
# returns 'break' to interrupt default key-bindings
return 'break'
You could also pop up a tool window with the editable fields listed with Entries to update the values. This example has a treeview with three columns, and does not use subclasses.
Bind your double click to this:
def OnDoubleClick(self, treeView):
# First check if a blank space was selected
entryIndex = treeView.focus()
if '' == entryIndex: return
# Set up window
win = Toplevel()
win.title("Edit Entry")
win.attributes("-toolwindow", True)
####
# Set up the window's other attributes and geometry
####
# Grab the entry's values
for child in treeView.get_children():
if child == entryIndex:
values = treeView.item(child)["values"]
break
col1Lbl = Label(win, text = "Value 1: ")
col1Ent = Entry(win)
col1Ent.insert(0, values[0]) # Default is column 1's current value
col1Lbl.grid(row = 0, column = 0)
col1Ent.grid(row = 0, column = 1)
col2Lbl = Label(win, text = "Value 2: ")
col2Ent = Entry(win)
col2Ent.insert(0, values[1]) # Default is column 2's current value
col2Lbl.grid(row = 0, column = 2)
col2Ent.grid(row = 0, column = 3)
col3Lbl = Label(win, text = "Value 3: ")
col3Ent = Entry(win)
col3Ent.insert(0, values[2]) # Default is column 3's current value
col3Lbl.grid(row = 0, column = 4)
col3Ent.grid(row = 0, column = 5)
def UpdateThenDestroy():
if ConfirmEntry(treeView, col1Ent.get(), col2Ent.get(), col3Ent.get()):
win.destroy()
okButt = Button(win, text = "Ok")
okButt.bind("<Button-1>", lambda e: UpdateThenDestroy())
okButt.grid(row = 1, column = 4)
canButt = Button(win, text = "Cancel")
canButt.bind("<Button-1>", lambda c: win.destroy())
canButt.grid(row = 1, column = 5)
Then confirm the changes:
def ConfirmEntry(self, treeView, entry1, entry2, entry3):
####
# Whatever validation you need
####
# Grab the current index in the tree
currInd = treeView.index(treeView.focus())
# Remove it from the tree
DeleteCurrentEntry(treeView)
# Put it back in with the upated values
treeView.insert('', currInd, values = (entry1, entry2, entry3))
return True
Here's how to delete an entry:
def DeleteCurrentEntry(self, treeView):
curr = treeView.focus()
if '' == curr: return
treeView.delete(curr)
I have tried #dakov solution but it did not work for me since my treeView has multiple columns and for few more reasons. I made some changes that enhanced it so here is my version
class Tableview(ttk.Treeview):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
tv.bind("<Double-1>", lambda event: self.onDoubleClick(event))
def onDoubleClick(self, event):
''' Executed, when a row is double-clicked. Opens
read-only EntryPopup above the item's column, so it is possible
to select text '''
# close previous popups
try: # in case there was no previous popup
self.entryPopup.destroy()
except AttributeError:
pass
# what row and column was clicked on
rowid = self.identify_row(event.y)
column = self.identify_column(event.x)
# handle exception when header is double click
if not rowid:
return
# get column position info
x,y,width,height = self.bbox(rowid, column)
# y-axis offset
pady = height // 2
# place Entry popup properly
text = self.item(rowid, 'values')[int(column[1:])-1]
self.entryPopup = EntryPopup(self, rowid, int(column[1:])-1, text)
self.entryPopup.place(x=x, y=y+pady, width=width, height=height, anchor='w')
The EntryPopup class
class EntryPopup(ttk.Entry):
def __init__(self, parent, iid, column, text, **kw):
ttk.Style().configure('pad.TEntry', padding='1 1 1 1')
super().__init__(parent, style='pad.TEntry', **kw)
self.tv = parent
self.iid = iid
self.column = column
self.insert(0, text)
# self['state'] = 'readonly'
# self['readonlybackground'] = 'white'
# self['selectbackground'] = '#1BA1E2'
self['exportselection'] = False
self.focus_force()
self.select_all()
self.bind("<Return>", self.on_return)
self.bind("<Control-a>", self.select_all)
self.bind("<Escape>", lambda *ignore: self.destroy())
def on_return(self, event):
rowid = self.tv.focus()
vals = self.tv.item(rowid, 'values')
vals = list(vals)
vals[self.column] = self.get()
self.tv.item(rowid, values=vals)
self.destroy()
def select_all(self, *ignore):
''' Set selection on the whole text '''
self.selection_range(0, 'end')
# returns 'break' to interrupt default key-bindings
return 'break'
from tkinter import ttk
from tkinter import *
root = Tk()
columns = ("Items", "Values")
Treeview = ttk.Treeview(root, height=18, show="headings", columns=columns) #
Treeview.column("Items", width=200, anchor='center')
Treeview.column("Values", width=200, anchor='center')
Treeview.heading("Items", text="Items")
Treeview.heading("Values", text="Values")
Treeview.pack(side=LEFT, fill=BOTH)
name = ['Item1', 'Item2', 'Item3']
ipcode = ['10', '25', '163']
for i in range(min(len(name), len(ipcode))):
Treeview.insert('', i, values=(name[i], ipcode[i]))
def treeview_sort_column(tv, col, reverse):
l = [(tv.set(k, col), k) for k in tv.get_children('')]
l.sort(reverse=reverse)
for index, (val, k) in enumerate(l):
tv.move(k, '', index)
tv.heading(col, command=lambda: treeview_sort_column(tv, col, not reverse))
def set_cell_value(event):
for item in Treeview.selection():
item_text = Treeview.item(item, "values")
column = Treeview.identify_column(event.x)
row = Treeview.identify_row(event.y)
cn = int(str(column).replace('#', ''))
rn = int(str(row).replace('I', ''))
entryedit = Text(root, width=10 + (cn - 1) * 16, height=1)
entryedit.place(x=16 + (cn - 1) * 130, y=6 + rn * 20)
def saveedit():
Treeview.set(item, column=column, value=entryedit.get(0.0, "end"))
entryedit.destroy()
okb.destroy()
okb = ttk.Button(root, text='OK', width=4, command=saveedit)
okb.place(x=90 + (cn - 1) * 242, y=2 + rn * 20)
def newrow():
name.append('to be named')
ipcode.append('value')
Treeview.insert('', len(name) - 1, values=(name[len(name) - 1], ipcode[len(name) - 1]))
Treeview.update()
newb.place(x=120, y=(len(name) - 1) * 20 + 45)
newb.update()
Treeview.bind('<Double-1>', set_cell_value)
newb = ttk.Button(root, text='new item', width=20, command=newrow)
newb.place(x=120, y=(len(name) - 1) * 20 + 45)
for col in columns:
Treeview.heading(col, text=col, command=lambda _col=col: treeview_sort_column(Treeview, _col, False))
root.mainloop()
After so much research while doing my project got this code, it helped me a lot.
Double click on the element you want to edit, make the required change and click 'OK' button
I think this is what exactly you wanted
#python #tkinter #treeview #editablerow
New row
Editable row
This is just for creating a tree for the specified path that is set in the constructor. you can bind your event to your item on that tree. The event function is left in a way that the item could be used in many ways. In this case, it will show the name of the item when double clicked on it. Hope this helps somebody.
import ttk
from Tkinter import*
import os*
class Tree(Frame):
def __init__(self, parent):
Frame.__init__(self, parent)
self.parent = parent
path = "/home/...."
self.initUI(path)
def initUI(self, path):
self.parent.title("Tree")
self.tree = ttk.Treeview(self.parent)
self.tree.bind("<Double-1>", self.itemEvent)
yScr = ttk.Scrollbar(self.tree, orient = "vertical", command = self.tree.yview)
xScr = ttk.Scrollbar(self.tree, orient = "horizontal", command = self.tree.xview)
self.tree.configure(yscroll = yScr.set, xScroll = xScr.set)
self.tree.heading("#0", text = "My Tree", anchor = 'w')
yScr.pack(side = RIGHT, fill = Y)
pathy = os.path.abspath(path)
rootNode = self.tree.insert('', 'end', text = pathy, open = True)
self.createTree(rootNode, pathy)
self.tree.pack(side = LEFT, fill = BOTH, expand = 1, padx = 2, pady = 2)
self.pack(fill= BOTH, expand = 1)
def createTree(self, parent, path)
for p in os.listdir(path)
pathy = os.path.join(path, p)
isdir = os.path.isdir(pathy)
oid = self.tree.insert(parent, 'end' text = p, open = False)
if isdir:
self.createTree(oid, pathy)
def itemEvent(self, event):
item = self.tree.selection()[0] # now you got the item on that tree
print "you clicked on", self.tree.item(item,"text")
def main():
root = Tk.Tk()
app = Tree(root)
root.mainloop()
if __name__ == '__main__'
main()
You should not do this manually
there are ready to use pack that have this Feature and many more such as
tkintertable
it have some insane features
there is also pygubu-editable-treeview
if you are intrested in pygubu,
as for the the reason you shouldnt code your own ,
in order to do a good treeview you will need to build more Feature that make your gui easier to use
however such Feature takes hundred lines of code to create.(takes a long time to get right)
unless you are making a custom TREE-View-widget,it doesnot worth the effort.
I don't know about making the row editable, but to capture clicking on a row, you use the <<TreeviewSelect>> virtual event. This gets bound to a routine with the bind() method, then you use the selection() method to get the ids of the items selected.
These are snippets from an existing program, but show the basic sequence of calls:
# in Treeview setup routine
self.tview.tree.bind("<<TreeviewSelect>>", self.TableItemClick)
# in TableItemClick()
selitems = self.tview.tree.selection()
if selitems:
selitem = selitems[0]
text = self.tview.tree.item(selitem, "text") # get value in col #0

How to draw geometry in sub-frame by click on it and corresponding button with Tkinter

I have create a frame but do not know how to draw the geometry on the sub-frame.
Here is the code of my current window:
class App:
def __init__(self, master):
frame = Frame(master)
frame.grid()
self.Quit = Button(frame, text = "QUIT", command = frame.quit)
self.Quit.grid(row = 0, column = 48, sticky = N)
self.adpt = Button(frame, text = "Add Point", command = self.adpt)
self.adpt.grid(row = 0, column = 49, sticky = N)
self.adln = Button(frame, text = "Add Line", command = self.adln)
self.adln.grid(row = 0, column = 50, sticky = N)
self.adpg = Button(frame, text = "Add Polygon", command = self.adpg)
self.adpg.grid(row = 0, column = 51, stick = N)
iframe = Frame(frame, bd = 2, relief = RAISED, width=1000, height=500)
iframe.grid(row = 1, columnspan = 100, sticky = N)
def adpt(self):
pass
def adln(self):
pass
def adpg(self):
pass
I need to create each kind of geometry by clicking the corresponding button, and then draw it on the sub-frame, but I do not know how to use event to draw geometry in the sub-frame (iframe). For example, to draw point, click the button "Add point". Then Click on the sub-frame to generate a point. Double click the sub-frame to save the points to a point list.
The first problem is how to draw the point on the sub-frame by click on it.
The second problem is how to make the sub-frame handle double click and click separately. When I double click a widget, it first go through the click event, and then the double click event.
I have create classes to draw geometry with canvas. The class of point, line, polygon can draw geometry with canvas.
Here is the example codes for point class:
class Point:
def __init__(self,x, y):
self.x = x
self.y = y
def __str__(self):
return " (" + str(self.x) + "," + str(self.y) + ")"
def draw(self,canvas):
canvas.create_line(self.x-10,self.y,self.x+10,self.y)
canvas.create_line(self.x,self.y-10,self.x,self.y+10)
If you are selecting the type of geometry with the buttons, on their event handlers you can set the class that is going to be used. Then you can use the coordinates of the event information to draw the item on the canvas.
self.adln = Button(frame, text = "Add Line", command=self.adln)
self.adpt = Button(frame, text = "Add Point", command=self.adpt)
self.canvas.bind("<Button-1>", self.click)
#...
def adln(self):
self.geometry = "Line"
def adpt(self):
self.geometry = "Points"
#...
def click(self, event):
if self.start is None:
self.start = (event.x, event.y)
else:
self.draw_geometry()
self.start = None
def draw_geometry(self):
if self.geometry == "Points":
p1 = Point(*self.start)
p2 = Point(event.x, event.y)
p1.draw(self.canvas)
p2.draw(self.canvas)
elif self.geometry == "Line":
line = Line(event.x, event.y, *self.start)
line.draw(self.canvas)
Note the number of arguments of the constructors and the existance of the draw method may not correspond with what you have, since this is just an example. It is a bit unpythonic, but it's the simplest way I have come out.
On the other hand, since the event <Button-1> is always triggered on a double click, I suggest you to use another button for a separate action, like <Button-2> or <Button-3>.

Categories

Resources