Deleting lines on a Python tkinter canvas - python

I'm attempting to have the user delete lines when the right mouse button is clicked. I have binded a button 3 press event to the canvas, and passed that to the following function
def eraseItem(self,event):
objectToBeDeleted = self.workspace.find_closest(event.x, event.y, halo = 5)
if objectToBeDeleted in self.dictID:
del self.dictID[objectToBeDeleted]
self.workspace.delete(objectToBeDeleted)
However nothing happens when I right click the lines. I have tested the dictionary separately and the line objects are being stored correctly.
Here is my binding:
self.workspace.bind("<Button-3>", self.eraseItem)
Per request some other snippets from the dictionary initialization
def __init__(self, parent):
self.dictID = {}
... Some irrelevant code omitted
For the line creation I have two handlers, an on click and an on release which draws the lines between both the coordinates
def onLineClick(self, event):
self.coords = (event.x, event.y)
def onLineRelease(self, event):
currentLine = self.workspace.create_line(self.coords[0], self.coords[1], event.x, event.y, width = 2, capstyle = ROUND)
self.dictID[currentLine] = self.workspace.coords(currentLine)
print(self.dictID.keys()) #For testing dictionary population
print(self.dictID.values()) #For testing dictionary population
The dictionary prints fine here.
Note that these are all functions within one class.

I've tried making a working example based on your code, and now I know where is the problem: find_closest returns a tuple with one element if an item is found, so when you check if it is in the dictionary, first you have to retrieve the first element of the tuple.
def eraseItem(self,event):
tuple_objects = self.workspace.find_closest(event.x, event.y, halo = 5)
if len(tuple_objects) > 0 and tuple_objects[0] in self.dictID:
objectToBeDeleted = tuple_objects[0]
del self.dictID[objectToBeDeleted]
self.workspace.delete(objectToBeDeleted)

Related

wx.ComboCtrl with wx.ListCtrl highlight background and sizing

I am building a wx.ComboCtrl with a wx.ListCtrl attached. The reason for doing this is because I want to set the foreground colour of the choices (the colour shows the user the status of the item). I want these colours to show when the box is dropped down and when a user has made a selection.
The problem I run into is that on Linux (Ubuntu 20.04), after a selection was made, the background colour of the wx.ComboCtrl remains blue (and the foreground colour remains white), even if I move focus to another widget. It doesn't matter which colour I set for the text to be displayed on the ComboCtrl, it remains white text with a blue background. See screenshot.
I can only get it to show me the default (gray) background with my selected foreground colour if I move the focus to another window and then back to my own window.
In Windows this doesn't happen: after selecting an item, the background colour of the ComboCtrl is default (gray), however it does show a little dotted line around the selection. See screenshot.
Here is the modified demo code that I am using to reproduce the issue. The comments in the code are left overs from some of the things I tried.
#!/usr/bin/env python
import wx
import os
#----------------------------------------------------------------------
#----------------------------------------------------------------------
# This class is used to provide an interface between a ComboCtrl and the
# ListCtrl that is used as the popoup for the combo widget.
class ListCtrlComboPopup(wx.ComboPopup):
def __init__(self):
wx.ComboPopup.__init__(self)
self.lc = None
def AddItem(self, txt, _colour):
self.lc.InsertItem(self.lc.GetItemCount(), txt)
_entry = self.lc.GetItem(self.lc.GetItemCount() - 1)
_entry.SetTextColour(_colour)
#_entry.SetItemTextColour(_colour)
self.lc.SetItem(_entry)
def OnMotion(self, evt):
item, flags = self.lc.HitTest(evt.GetPosition())
if item >= 0:
self.lc.Select(item)
self.curitem = item
def OnLeftDown(self, evt):
self.value = self.curitem
self.Dismiss()
# The following methods are those that are overridable from the
# ComboPopup base class. Most of them are not required, but all
# are shown here for demonstration purposes.
# This is called immediately after construction finishes. You can
# use self.GetCombo if needed to get to the ComboCtrl instance.
def Init(self):
self.value = -1
self.curitem = -1
# Create the popup child control. Return true for success.
def Create(self, parent):
self.lc = wx.ListCtrl(parent, style=wx.LC_SINGLE_SEL | wx.SIMPLE_BORDER | wx.LC_REPORT | wx.LC_NO_HEADER)
self.lc.InsertColumn(0, '')
self.lc.Bind(wx.EVT_MOTION, self.OnMotion)
self.lc.Bind(wx.EVT_LEFT_DOWN, self.OnLeftDown)
return True
# Return the widget that is to be used for the popup
def GetControl(self):
return self.lc
# Called just prior to displaying the popup, you can use it to
# 'select' the current item.
def SetStringValue(self, val):
idx = self.lc.FindItem(-1, val)
if idx != wx.NOT_FOUND:
self.lc.Select(idx)
# Return a string representation of the current item.
def GetStringValue(self):
if self.value >= 0:
return self.lc.GetItemText(self.value)
return ""
# Called immediately after the popup is shown
def OnPopup(self):
wx.ComboPopup.OnPopup(self)
# Called when popup is dismissed
def OnDismiss(self):
print (self.GetStringValue())
wx.ComboPopup.OnDismiss(self)
# This is called to custom paint in the combo control itself
# (ie. not the popup). Default implementation draws value as
# string.
def PaintComboControl(self, dc, rect):
wx.ComboPopup.PaintComboControl(self, dc, rect)
# Receives key events from the parent ComboCtrl. Events not
# handled should be skipped, as usual.
def OnComboKeyEvent(self, event):
wx.ComboPopup.OnComboKeyEvent(self, event)
# Implement if you need to support special action when user
# double-clicks on the parent wxComboCtrl.
def OnComboDoubleClick(self):
wx.ComboPopup.OnComboDoubleClick(self)
# Return final size of popup. Called on every popup, just prior to OnPopup.
# minWidth = preferred minimum width for window
# prefHeight = preferred height. Only applies if > 0,
# maxHeight = max height for window, as limited by screen size
# and should only be rounded down, if necessary.
def GetAdjustedSize(self, minWidth, prefHeight, maxHeight):
return wx.ComboPopup.GetAdjustedSize(self, minWidth, prefHeight, maxHeight)
# Return true if you want delay the call to Create until the popup
# is shown for the first time. It is more efficient, but note that
# it is often more convenient to have the control created
# immediately.
# Default returns false.
def LazyCreate(self):
return wx.ComboPopup.LazyCreate(self)
#----------------------------------------------------------------------
class MyTestPanel(wx.Panel):
def __init__(self, parent, log):
self.log = log
wx.Panel.__init__(self, parent, -1)
txt = wx.TextCtrl(self, wx.ID_ANY, pos=(100,100))
comboCtrl = wx.ComboCtrl(self, wx.ID_ANY, "Third item", (10,10), size=(200,-1), style=wx.CB_READONLY)
popupCtrl = ListCtrlComboPopup()
# It is important to call SetPopupControl() as soon as possible
comboCtrl.SetPopupControl(popupCtrl)
# Populate using wx.ListView methods
popupCtrl.AddItem("First Item", [255, 127, 0])
popupCtrl.AddItem("Second Item", [192, 127, 45])
popupCtrl.AddItem("Third Item", [25, 223, 172])
#popupCtrl.GetAdjustedSize(100, 35, 100)
#comboCtrl.SetTextColour(_colour)
comboCtrl.SetForegroundColour(wx.Colour(235, 55, 55))
#----------------------------------------------------------------------
def runTest(frame, nb, log):
win = MyTestPanel(nb, log)
return win
#----------------------------------------------------------------------
overview = """<html><body>
<h2><center>wx.combo.ComboCtrl</center></h2>
A combo control is a generic combobox that allows a totally custom
popup. In addition it has other customization features. For instance,
position and size of the dropdown button can be changed.
</body></html>
"""
if __name__ == '__main__':
import sys,os
import run
run.main(['', os.path.basename(sys.argv[0])] + sys.argv[1:])
Question 1:
How can I make it so that once an item has been selected the appropriate text colour (the one I programmatically set) and default (gray) background colour is shown.
Question 2:
When dropping down the ComboCtrl, it is showing the ListCtrl, which has a single column only. You can see that the "Second item" on the list is not displayed entirely because the column is too narrow. How can I make it so that the column is always the same width as the widget itself, even when the ComboCtrl resizes (as a result of resizing the parent window)?
Question 3:
Not overly important, but while we are on the subject: is there a way to get rid of the little dotted box that is shown around the selected item when running this in Windows?
In advance, thank you very much for your thoughts and ideas on this.
Marc.

Why binding same function to keyboard modifies way that tkinter buttons work?

I have function which adds number to tk.StringVar(). I'm creating numeric buttons for my Gui in loop. It worked perfect until I tried to add binds to keyboard. Now clicking on keyboard e.g. 7 will behave as expected. StringVar() will be appended by 7, but when I try to use my mouse to click Gui button it adds 1. Instead 7 I will get 17 now. Removing those two self.binds brings back proper way of working for Gui buttons. Here is my code:
import tkinter as tk
import tkinter.ttk as ttk
class Gui(tk.Tk):
def __init__(self):
super().__init__()
self.user_input = tk.StringVar()
tk.Label(self, textvariable=self.user_input, font=("Times New Roman",30)).grid(row=0, column =0)
for button in self.create_buttons(self): button.grid()
def write(self, *_, button=None):
current = self.user_input.get()
if button:
current += button
else:
current = current[:-1]
self.user_input.set(current)
def create_buttons(self, frame):
buttons = []
for x in range(10):
func = lambda *_, x=x: self.write(button=str(x))
buttons.append(ttk.Button(frame, text=str(x), command=func))
self.bind('<'+str(x)+'>', func)
self.bind('<KP_'+str(x)+'>', func)
buttons.append(ttk.Button(frame, text="<-", command=self.write))
self.bind("<BackSpace>", self.write)
return buttons
Gui().mainloop()
What am I missing here? Why self.bind modifies my program that way and why it adds exactly 1, not any other number? How can I correct it?
So the only change you need to do in your code is in the create_button() function and its:
def create_buttons(self, frame):
buttons = []
for x in range(10):
func = lambda *_, x=x: self.write(button=str(x))
buttons.append(ttk.Button(frame, text=str(x), command=func))
self.bind(str(x), func)
self.bind('<KP_'+str(x)+'>', func)
buttons.append(ttk.Button(frame, text="<-", command=self.write))
self.bind("<BackSpace>", self.write)
return buttons
this will stop the weird behavior of adding 1 to the desired key. to bind the key stroke of numeric value or even simple char you need only the string representation of that value for more sophisticated presses you can see the bind() table in the following link. You don't need to add the "<",">" to bind the desired numeric key.
If you still want to keep the notion with the "<",">" you can do it with the following line instead of the one mentioned above without "<",">":
self.bind('<KeyPress-'+str(x)+'>', func)
you can see the full dodumantion on proper bindings etc here

Multi-Line Combobox in Tkinter

Is it possible to have a multi-line text entry field with drop down options?
I currently have a GUI with a multi-line Text widget where the user writes some comments, but I would like to have some pre-set options for these comments that the user can hit a drop-down button to select from.
As far as I can tell, the Combobox widget does not allow changing the height of the text-entry field, so it is effectively limited to one line (expanding the width arbitrarily is not an option). Therefore, what I think I need to do is sub-class the Text widget and somehow add functionality for a drop down to show these (potentially truncated) pre-set options.
I foresee a number of challenges with this route, and wanted to make sure I'm not missing anything obvious with the existing built-in widgets that could do what I need.
Terry's feedback made it clear that there was no simple way to solve this, so I created a custom class which wraps a Text and a Button into a frame, with a Toplevel containing a Listbox spawned by the button's callback function. I added a couple of "nice-to-have" features, like option highlighting within the Listbox, and I mapped bindings of the main widget onto the internal Text widget to make it easier to work with. Please leave a comment if there's any glaring bad practices here; I'm definitely still pretty inexperienced! But I hope this helps anybody else who's looking for a multi-line combobox!
class ComboText(tk.Frame):
def __init__(self, parent=None, **kwargs):
super().__init__(parent)
self.parent = parent
self._job = None
self.data = []
self['background'] = 'white'
self.text = tk.Text(self, **kwargs)
self.text.pack(side=tk.LEFT, expand=tk.YES, fill='x')
symbol = u"\u25BC"
self.button = tk.Button(self,width = 2,text=symbol, background='white',relief = 'flat', command = self.showOptions)
self.button.pack(side=tk.RIGHT)
#pass bindings from parent frame widget to the inner Text widget
#This is so you can bind to the main ComboText and have those bindings
#apply to things done within the Text widget.
#This could also be applied to the inner button widget, but since
#ComboText is intended to behave "like" a Text widget, I didn't do that
bindtags = list(self.text.bindtags())
bindtags.insert(0,self)
self.text.bindtags(tuple(bindtags))
def showOptions(self):
#Get the coordinates of the parent Frame, and the dimensions of the Text widget
x,y,width,height = [self.winfo_rootx(), self.winfo_rooty(), self.text.winfo_width(), self.text.winfo_height()]
self.toplevel = tk.Toplevel()
self.toplevel.overrideredirect(True) #Use this to get rid of the menubar
self.listbox = tk.Listbox(self.toplevel,width=width, height =len(self.data))
self.listbox.pack()
#Populate the options in the listbox based on self.data
for s in self.data:
self.listbox.insert(tk.END,s)
#Position the Toplevel so that it aligns well with the Text widget
list_height = self.listbox.winfo_reqheight()
self.toplevel.geometry("%dx%d+%d+%d" % (width, list_height, x, y+height))
self.listbox.focus_force()
self.listbox.bind("<Enter>", self.ListboxHighlight)
self.listbox.bind("<Leave>",self.stopListboxHighlight)
self.listbox.bind("<Button-1>",self.selectOption)
self.toplevel.bind("<Escape>", self.onCancel)
self.toplevel.bind("<FocusOut>", self.onCancel)
def ListboxHighlight(self,*ignore):
#While the mouse is moving within the listbox,
#Highlight the option the mouse is over
x,y = self.toplevel.winfo_pointerxy()
widget = self.toplevel.winfo_containing(x,y)
idx = self.listbox.index("#%s,%s" % (x-self.listbox.winfo_rootx(),y-self.listbox.winfo_rooty()))
self.listbox.selection_clear(0,100) #very sloppy "Clear all"
self.listbox.selection_set(idx)
self.listbox.activate(idx)
self._job = self.after(25,self.ListboxHighlight)
def stopListboxHighlight(self,*ignore):
#Stop the recurring highlight function.
if self._job:
self.after_cancel(self._job)
self._job = None
def onCancel(self,*ignore):
#Stop callback function to avoid error once listbox destroyed.
self.stopListboxHighlight()
#Destroy the popup Toplevel
self.toplevel.destroy()
def selectOption(self,event):
x,y = [event.x,event.y]
idx = self.listbox.index("#%s,%s" % (x,y))
if self.data:
self.text.delete('1.0','end')
self.text.insert('end',self.data[idx])
self.stopListboxHighlight()
self.toplevel.destroy()
self.text.focus_force()
def setOptions(self,optionList):
self.data = optionList
#Map the Text methods onto the ComboText class so that
#the ComboText can be treated like a regular Text widget
#with some other options added in.
#This was necessary because ComboText is a subclass of Frame, not Text
def __getattr__(self,name):
def textMethod(*args, **kwargs):
return getattr(self.text,name)(*args, **kwargs)
return textMethod
if __name__ == '__main__':
root = tk.Tk()
ct = ComboText(root, width = 50, height = 3)
ct.pack()
ct.setOptions(['Option %d' % i for i in range (0,5)])
root.mainloop()
I don't think you are missing anything. Note that ttk.Combobox is a composite widget. It subclasses ttk.Entry and has ttk.Listbox attached.
To make multiline equivalent, subclass Text. as you suggested. Perhaps call it ComboText. Attach either a frame with multiple read-only Texts, or a Text with multiple entries, each with a separate tag. Pick a method to open the combotext and methods to close it, with or without copying a selection into the main text. Write up an initial doc describing how to operate the thing.

Tkinter: Integers as labels overwrite

I am creating labels in a for loop that display integers every time I fire an event (a mouse click) on my application. The problem is that old labels don't get erased and the new ones come on top of them causing a big mess.
Here is the working code that you can try out:
import numpy as np
import Tkinter as tk
class Plot(object):
def __init__(self, win):
self.win = win
self.bu1 = tk.Button(win,text='Load',command=self.populate,fg='red').grid(row=0,column=0)
self.listbox = tk.Listbox(win, height=5, width=5)
self.listbox.grid(row=1,column=0)#, rowspan=10, columnspan=2)
self.listbox.bind("<Button-1>", self.print_area)
def populate(self):
"""Populate listbox and labels"""
self.time = [1,2,3]
self.samples = ['a','b','c']
for item in self.time:
self.listbox.insert(tk.END,item)
for i,v in enumerate(self.samples):
tk.Label(self.win, text=v).grid(row=2+i,column=0,sticky=tk.W)
self.lbl_areas = []
for i in range(0, len(self.samples)):
self.lbl=tk.IntVar()
self.lbl.set(0)
self.lbl_areas.append(tk.Label(self.win,textvariable=self.lbl).grid(row=2+i,column=1,sticky=tk.W))
def print_area(self, event):
"""Prints the values"""
widget = event.widget
selection=widget.curselection()
value = widget.get(selection[0])
#Here is the dictionary that maps time with values
self.d = {1:[('a',33464.1),('b',43.5),('c',64.3)],
2:[('a',5.1),('b',3457575.5),('c',25.3)],
3:[('a',12.1),('b',13.5),('c',15373.3)]}
lbl_val = []
for i in range(0, len(self.samples)):
lbl_val.append(self.d[value][i][1])
for i in range(0, len(self.samples)):
self.lbl=tk.IntVar()
self.lbl.set(lbl_val[i])
tk.Label(self.win,textvariable=self.lbl).grid(row=2+i,column=1,sticky=tk.W)
def main():
root = tk.Tk()
app = Plot(root)
tk.mainloop()
if __name__ == '__main__':
main()
If You try to run this code and click on LOAD you will see the numbers appearing in the listbox and labels a,b,c with values set to zero at the beginning. If you click on the number in the listbox the values (mapped into the dictionary d) will appear but you will see the overwrite problem. How can I fix that?
How can I overcome this problem? Thank you
Don't create new labels. Create the labels once and then update them on mouse clicks using the configure method of the labels.
OR, before creating new labels delete the old labels.If you design your app so that all of these temporary labels are in a single frame you can delete and recreate the frame, and all of the labels in the frame will automatically get deleted. In either case (destroying the frame or destroying the individual labels) you would call the destroy method on the widget you want to destroy.

Python Tkinter: Listbox mouse enter event for a specific entry

It is possible to create events for when the mouse pointer enters/leaves the entire Listbox using <Enter>/<Leave>. How can I track when the mouse enters or leaves a specific entry (row) in the Listbox?
I want to color in different color the background of the entry over which the mouse pointer is currently located.
Here's an (half) attempt to do what you want by binding to the <Motion> event instead of the pair <Enter> and <Leave>. This because <Enter> is raised only when we enter the Listbox from outside it, but once we are inside a Listbox with the mouse, no other <Enter> event will be raised, and we cannot keep track of which item the mouse is above.
Calling a function every time the mouse moves might result in an overload of work, so I don't think this feature is worthing doing it (in this way).
The program does not still work perfectly, and I still have to understand why: basically, sometimes the item's background and font color are not changed properly, there's some kind of delay or something.
from tkinter import *
class CustomListBox(Listbox):
def __init__(self, master=None, *args, **kwargs):
Listbox.__init__(self, master, *args, **kwargs)
self.bg = "white"
self.fg = "black"
self.h_bg = "#eee8aa"
self.h_fg = "blue"
self.current = -1 # current highlighted item
self.fill()
self.bind("<Motion>", self.on_motion)
self.bind("<Leave>", self.on_leave)
def fill(self, number=15):
"""Fills the listbox with some numbers"""
for i in range(number):
self.insert(END, i)
self.itemconfig(i, {"bg": self.bg})
self.itemconfig(i, {"fg": self.fg})
def reset_colors(self):
"""Resets the colors of the items"""
for item in self.get(0, END):
self.itemconfig(item, {"bg": self.bg})
self.itemconfig(item, {"fg": self.fg})
def set_highlighted_item(self, index):
"""Set the item at index with the highlighted colors"""
self.itemconfig(index, {"bg": self.h_bg})
self.itemconfig(index, {"fg": self.h_fg})
def on_motion(self, event):
"""Calls everytime there's a motion of the mouse"""
print(self.current)
index = self.index("#%s,%s" % (event.x, event.y))
if self.current != -1 and self.current != index:
self.reset_colors()
self.set_highlighted_item(index)
elif self.current == -1:
self.set_highlighted_item(index)
self.current = index
def on_leave(self, event):
self.reset_colors()
self.current = -1
if __name__ == "__main__":
root = Tk()
CustomListBox(root).pack()
root.mainloop()
Note that I have used from tkinter import * for typing faster, but I recommend you to use import tkinter as tk.
No, you cannot track when it enters/leaves a specific row. However, you can track when it enters/leaves the widget, and you can compute which item the mouse is over by using the index method of the listbox. If you give an index of the form "#x,y", it will return the numerical index.
For example:
self.listbox.bind("<Enter>", self.on_enter)
...
def on_enter(self, event):
index = self.listbox.index("#%s,%s" % (event.x, event.y))
...

Categories

Resources