Making multiple selections in tkinter - python

Is there any way to make multiple selections in tkinter?
Here's the code:
from tkinter import *
root = Tk()
text = Text(root , width = 65 , height = 20 , font = "consolas 14")
text.pack()
text.insert('1.0' , "This is the first line.\nThis is the second line.\nThis is the third line.")
mainloop()
Here, I want be able to select multiple text from where ever I want.
Here is an Image(GIF) that explains what I mean:
Is there any way to achieve this in tkinter?
It would be great if anyone could help me out.

I made a short demo, with Control key hold you could select multiple text. Check this:
import tkinter as tk
class SelectableText(tk.Text):
def __init__(self, master, **kwarg):
super().__init__(master, **kwarg)
self.down_ind = ''
self.up_ind = ''
self.bind("<Control-Button-1>", self.mouse_down)
self.bind("<B1-Motion>", self.mouse_drag)
self.bind("<ButtonRelease-1>", self.mouse_up)
self.bind("<BackSpace>", self.delete_)
def mouse_down(self, event):
self.down_ind = self.index(f"#{event.x},{event.y}")
def mouse_drag(self, event):
self.up_ind = self.index(f"#{event.x},{event.y}")
if self.down_ind and self.down_ind != self.up_ind:
self.tag_add(tk.SEL, self.down_ind, self.up_ind)
self.tag_add(tk.SEL, self.up_ind, self.down_ind)
def mouse_up(self, event):
self.down_ind = ''
self.up_ind = ''
def delete_(self, event):
selected = self.tag_ranges(tk.SEL)
if len(selected) > 2:
not_deleting = ''
for i in range(1, len(selected) - 1):
if i % 2 == 0:
not_deleting += self.get(selected[i-1].string, selected[i].string)
self.delete(selected[0].string, selected[-1].string)
self.insert(selected[0].string, not_deleting)
return "break"
root = tk.Tk()
text = SelectableText(root, width=50, height=10)
text.grid()
text.insert('end', "This is the first line.\nThis is the second line.\nThis is the third line.")
root.mainloop()
So I was trying to delete each selection with the Text.delete(index1, index2) but when the first selection in one line is deleted, the indices changes, making the subsequent delete deleting indices not selected (or out of range in the particular line.
I had to work around another way - first deleting from the first selected to the last selected, just like what BackSpace would do by default, then put back every unselected part in the middle. The Text.tag_ranges gives you a list of ranges selected in this way:
[start1, end1, start2, end2, ...]
where each entry is a <textindex object> with a string property (the index). So you can extract the text between end1 and start2, between end2 and start3, etc. to the end, and store these into a variable (not_deleting) so you can insert them back into the text.
There should be better and neater solutions but for now this is what it is... Hope it helps.

For the delete_ method proposed by Qiaoxuan Zhang, I advise to use the Text.tag_nextrange method in a loop like this :
def delete_sel(self):
while 1:
result = self.tag_nextrange(tk.SEL, 1.0)
if result :
self.delete(result[0] , result[1])
else :
break
For those who would like to add the missing features:
correction of selection when changing direction of sliding
take into account the double-click
Take a look here : French site

Short answer: set the exportselection attribute of each Text widget to False
Tkinter gives you control over this behaviour with the exportselection configuration option for the Text widget as well as the Entry and Listbox widgets. Setting it to False prevents the export of the selection to the X selection, allowing the widget to retain its selection when a different widget gets focus.
For example:
import tkinter as tk
...
text1 = tk.Text(..., exportselection=False)
text2 = tk.Text(..., exportselection=False)
you can find more info here: http://tcl.tk/man/tcl8.5/TkCmd/options.htm#M-exportselection

Related

Python Tkinter Listbox Back Forward Button

I have a listbox and I printed some contents with it. I want to put a back and forth button, as I press the button, it goes back and forth in the indexes and this selection appears in the listbox.
For example, 0 index is selected in the photo. When I press Forth, I want it to come to 1 index.
For this, I did research and analyzed the sample codes, but did not have the idea that I could not find the result.
import tkinter as tk
from tkinter import *
root = tk.Tk()
my_frame = Frame(root)
my_scrollbar = Scrollbar(my_frame, orient=VERTICAL)
list = Listbox(my_frame, width=85, height=20, yscrollcommand=my_scrollbar.set)
for i in range(50):
list.insert(END, str(i)+".index")
my_scrollbar.config(command=list.yview)
my_scrollbar.pack(side=RIGHT, fill=Y)
my_frame.pack()
list.pack()
So if I understand correctly, you'd like 2 buttons that allows you to cycle forward and backwards through the indexes.
So below is your code modified that allows you to visibly cycle through the listbox by using the GUI.
(EXPLANATION BELOW)
import tkinter as tk
from tkinter import *
root = tk.Tk()
my_frame = Frame(root)
my_frame.pack()
my_scrollbar = Scrollbar(my_frame)
my_scrollbar.pack(side=RIGHT, fill=Y)
listBox_0 = tk.Listbox(my_frame, width=85, height=20, yscrollcommand=my_scrollbar.set, selectmode='single')
listBox_0.pack()
my_scrollbar.config(command=listBox_0.yview)
def backButtonFN(my_frame):
curselection = listBox_0.curselection()
indexLB = curselection[0]
indexLB = indexLB - 1
if indexLB > 0 or indexLB == 0:
listBox_0.selection_clear(0, last='end')
listBox_0.selection_set(indexLB)
else:
None
pass
def forwardButtonFN(my_frame):
curselection = listBox_0.curselection() #current selection method
indexLB = curselection[0] #get value of index
indexLB = indexLB + 1 #add 1 to current value
listBox_0.selection_clear(0, last='end') #delete old selection
listBox_0.selection_set(indexLB) #select new index
pass
backButton = tk.Button(my_frame)
backButton.configure(text='BACK')
backButton.pack(anchor='s', side='left')
backButton.bind('<1>', backButtonFN, add='' )
forwardButton = tk.Button(my_frame)
forwardButton.configure(text='FOWARD')
forwardButton.pack(anchor='s', side='right')
forwardButton.bind('<1>', forwardButtonFN, add='' )
for i in range(50):
listBox_0.insert(END, str(i)+".index")
listBox_0.selection_set(0) #activate first index
Initially you called your listbox list, which is a big no-go as it can cause issues in terms of syntax later on down the line, so I changed the name of the Listbox to listBox_0, so this is your new Listbox as you originally intended.
once I did that, I created 2 functions for each of the forward and backwards buttons, I also had to rearrange the code to allow it to <bind> correctly.
I started off by making the first index active, that essentially will give me a value for curselection(), I then took that value, turned it into an integer I could play with, then each time you press the button, it will turn that extracted index, add one to it, then remove the previous selection and then add the new updated selection with indexLB
Then the same process for backButtonFN except this time, I am deducting 1 instead of adding, and then making sure it doesn't continue to go back once it hits 0.
I've also changed your configuration for listBox_0 to selectmode='single' to allow you to select one item at a row, you can disable this if you'd like to select multiple.
It can definitely use some tidying up as the pro's may immediately see, but I hope this answers your question

How can I change the text of each button in a nested list?

I've looked around SO and tried the solutions offered, but I can't seem to change the text of any button I've made using a double for loop.
The loops are there so I can add them to a list of lists of buttons so I can (supposedly) access them conveniently through the nested list by calling board_button[2][3] or something. They are also dynamically created after the user inputs the board_size such that it generates a grid of nxn buttons so there's that. These are all done inside a class method, and there's another class method that should change the button's text when it's called by the button.
I've tried using the solutions offered here, but none of them actually worked for my problem.
Pardon the long block of code, but I honestly think the way I've made it may have contributed to the problem, and give more insight as result.
from tkinter import filedialog
from tkinter import *
class MainWindow(Frame):
board_size = None
file_input = None
board_buttons = None
board_strvars = None
row = []
def __init__ (self, parent):
Frame.__init__(self, parent)
# initialize widgets but don't show them yet
self.initWidgets()
# show the starting window
self.startWindow()
def generateBoard(self, to_forget=None):
# hides the groups of the method that called it
if to_forget != None:
for i in to_forget:
self.row[i].forget()
# get the board_size from user
self.board_size = int(self.size_entry.get())
# initialize text variables for each button
self.board_strvars = []
for i in range(self.board_size):
self.row_strvars=[]
for j in range(self.board_size):
var = StringVar()
var.set(" ")
self.row_strvars.append(var)
self.board_strvars.append(self.row_strvars)
# insert list of lists of buttons here
self.row[1].pack(fill=X)
self.board_buttons = []
for i in range(self.board_size):
self.row_buttons=[]
for j in range(self.board_size):
self.row_buttons.append(Button(self.row[1], textvariable=self.board_strvars[i][j], command=lambda:self.place(i, j)))
self.row_buttons[j].grid(row=i, column=j)
self.board_buttons.append(self.row_buttons)
# for solve and back button
self.row[2].pack(fill=X)
def initWidgets(self):
# create the rows or groups of widgets
for i in range(3):
self.row.append(Frame())
# row 0; startWindow
self.size_entry = Entry(self.row[0])
self.size_entry.pack(fill=X, side=LEFT)
self.size_button = Button(self.row[0], text="Enter", command=lambda:self.generateBoard([0]))
self.size_button.pack(fill=X, side=LEFT)
self.load_button = Button(self.row[0], text="Load", command=self.loadFile)
self.load_button.pack(fill=X, side=LEFT)
# row 2; generateBoard
self.solve_button = Button(self.row[2], text="Solve", command=self.showSolutions)
self.solve_button.pack(fill=X, side=LEFT)
self.back_button = Button(self.row[2], text="Back", command=lambda:self.startWindow(to_forget=[0,2], to_destroy=[1]))
self.back_button.pack(fill=X, side=RIGHT)
def loadFile(self):
print("file loaded!")
def place(self, i, j):
if self.board_strvars[i][j].get() == " ":
self.board_strvars[i][j].set("C")
else:
self.board_strvars[i][j].set(" ")
def showSolutions(self):
print("solutions shown!")
def startWindow(self, to_forget=None, to_destroy=None):
# hides the groups of the method that called it
if to_forget != None:
for i in to_forget:
self.row[i].forget()
# destroys the groups' child widgets and hides the group
if to_destroy != None:
for i in to_destroy:
for child in self.row[i].winfo_children():
child.destroy()
self.row[i].forget()
self.row[0].pack(fill=X)
if __name__ == "__main__":
root=Tk()
root.title("test")
app = MainWindow(root)
root.mainloop()
I originally wanted to define a function that will change the text of the button that called it. But so far I've found no way to do so.
Doing the solutions offered in the post I linked changes nothing to the buttons. In the code I provided though, I used the StringVar() to be assigned as textvariable of the button. However, it only changes the last row, last column button element no matter which button you click. It's supposed to work in a way that the button that was clicked, will get its text changed.
Thanks!
Change your lambda function to force a closure:
def generateBoard(self, to_forget=None):
...
for i in range(self.board_size):
self.row_buttons=[]
for j in range(self.board_size):
self.row_buttons.append(Button(self.row[1], textvariable=self.board_strvars[i][j], command=lambda i=i, j=j:self.place(i, j)))
self.row_buttons[j].grid(row=i, column=j)
self.board_buttons.append(self.row_buttons)
Also note that its better to not call your own method place since there is already a place method in Tk.

Replacing Buttons linked to input boxes with checkboxes

This is my current code for selecting different options and have them appearing in the box (Minecraft ArmorStand Generator).
from tkinter import *
default = "/summon ArmorStand ~ ~ ~ {CustomNameVisible:1}"
NoAI = ",NoAI:1"
inputbox = Entry()
inputbox.place(x=10,y=10,width=900,height=50)
root = Tk()
def addNOAI():
inputbox.insert(45, NoAI)
inputbox = Entry()
inputbox.place(x=10,y=10,width=900,height=50)
Button(text="Add NoAI",command=addNOAI,relief = FLAT, bg = "#eF651A", fg = "white", width= 25, height = 2).place(x=10,y=123)
root.title("WIP")
root.wm_state('zoomed')
root.mainloop()
What I'd like to to do is replace the buttons with tick boxes, to prevent the buttons being pressed multiple times. If they click the button, add the text, if they untick, remove it.. I'm not sure where to start with this so any hint in the right direction would be nice.
I've got a working solution, you can try it below.
from tkinter import *
default = "/summon ArmorStand ~ ~ ~ {CustomNameVisible:1}"
NoAI = ",NoAI:1"
inputbox = Entry()
inputbox.place(x=10,y=10,width=900,height=50)
root = Tk()
def addNOAI():
state = var.get()
if state == 1: #if the state is checked
inputbox.insert(45, NoAI) #then add the text
else: #if the state is not check
inputbox.delete(0, 7) #delete the text
inputbox = Entry()
inputbox.place(x=10,y=10,width=900,height=50)
var = IntVar() #sets up variable for check button
c = Checkbutton(text="Add NoAI", command=addNOAI, variable=var) #defining check button variable and command
c.place(x=10,y=123)
root.title("WIP")
root.wm_state('zoomed')
root.mainloop()
The only problem is at the moment, you are deleting everything in the entry box (more accurately from position 0, to position 7). I assume that there will be multiple check buttons, all adding their own strings to the entry box.
As a solution, I would suggest extracting everything from the entry box, finding the string you want, taking it out, and putting everything back in again. Here's an example.
def addNOAI():
state = var.get()
if state == 1: #if the state is checked
inputbox.insert(45, NoAI) #then add the text
else: #if the state is not check
contents = inputbox.get() #gets all of contents
position = contents.find(NoAI) #finds the first position of desired string to remove
newcontents = contents[:position]+contents[position+7:] #gets string before the word, and after the word, and joins them
inputbox.delete(0, 'end') #clears input box for new entry
inputbox.insert(45, newcontents) #re-inserts the string
Here, when the user unchecks the box, the program finds the starting position of the string within the contents of the inputbox. Because you know how long the string will be (in this case 7), you can remove the string from the current contents of the inputbox, and place this inside a new variable. Now you have a new string, without the one that was unchecked, you can clear the inputbox, and put the new one in.
Hope this helps!

Updating a label in Tkinter on a button press

I am trying to make a button that when clicked updates the number on a label. What I am trying to accomplish is that when someone scores a goal, you can click the Goal! button and it will update the teams score.
import sys
from tkinter import *
root = Tk()
class team1:
score = 0
def goal(self):
self.score += 1
team1_attempt.set(text = self.score)
team1 = team1()
team1_attempt = Label(text = team1.score).pack()
team1_button = Button(text="Goal!", command = team1.goal).pack()
Hope someone can help! New to python.
You have two problems with your code.
First problem:
team1_attempt = Label(text = team1.score).pack()
This sets team1_attempt to None, because pack(0 returns None. If you want to save a reference to a widget so you can interact with it later you must do widget creation and widget layout in two steps.
Second problem:
team1_attempt.set(text = self.score)
To change an attribute of a widget, use the configure method. I don't know what documentation you read that says to call set on a label widget, but that documentation is wrong. Use configure, like so:
test1_attempt.configure(text=self.score)
Instead of using a label, try using an Entry widget that inserts the score into the Entry widget. For example:
class test:
def __init__(self, master):
self.goalButton = Button(master,
text = "goal!",
command = self.goalUpdate)
self.goalButton.pack()
self.goalDisplay = Entry(master,
width = 2)
self.goalDisplay.pack()
self.score = 0
def goalUpdate(self):
self.goalDisplay.delete(1.0, END) # Deletes whatever is in the score display
score = str(self.score)
self.goalDisplay.insert(0, score) # Inserts the value of the score variable

How to set the text/value/content of an `Entry` widget using a button in tkinter

I am trying to set the text of an Entry widget using a button in a GUI using the tkinter module.
This GUI is to help me classify thousands of words into five categories. Each of the categories has a button. I was hoping that using a button would significantly speed me up and I want to double check the words every time otherwise I would just use the button and have the GUI process the current word and bring the next word.
The command buttons for some reason are not behaving like I want them to. This is an example:
import tkinter as tk
from tkinter import ttk
win = tk.Tk()
v = tk.StringVar()
def setText(word):
v.set(word)
a = ttk.Button(win, text="plant", command=setText("plant"))
a.pack()
b = ttk.Button(win, text="animal", command=setText("animal"))
b.pack()
c = ttk.Entry(win, textvariable=v)
c.pack()
win.mainloop()
So far, when I am able to compile, the click does nothing.
You might want to use insert method. You can find the documentation for the Tkinter Entry Widget here.
This script inserts a text into Entry. The inserted text can be changed in command parameter of the Button.
from tkinter import *
def set_text(text):
e.delete(0,END)
e.insert(0,text)
return
win = Tk()
e = Entry(win,width=10)
e.pack()
b1 = Button(win,text="animal",command=lambda:set_text("animal"))
b1.pack()
b2 = Button(win,text="plant",command=lambda:set_text("plant"))
b2.pack()
win.mainloop()
If you use a "text variable" tk.StringVar(), you can just set() that.
No need to use the Entry delete and insert. Moreover, those functions don't work when the Entry is disabled or readonly! The text variable method, however, does work under those conditions as well.
import Tkinter as tk
...
entry_text = tk.StringVar()
entry = tk.Entry( master, textvariable=entry_text )
entry_text.set( "Hello World" )
You can choose between the following two methods to set the text of an Entry widget. For the examples, assume imported library import tkinter as tk and root window root = tk.Tk().
Method A: Use delete and insert
Widget Entry provides methods delete and insert which can be used to set its text to a new value. First, you'll have to remove any former, old text from Entry with delete which needs the positions where to start and end the deletion. Since we want to remove the full old text, we start at 0 and end at wherever the end currently is. We can access that value via END. Afterwards the Entry is empty and we can insert new_text at position 0.
entry = tk.Entry(root)
new_text = "Example text"
entry.delete(0, tk.END)
entry.insert(0, new_text)
Method B: Use StringVar
You have to create a new StringVar object called entry_text in the example. Also, your Entry widget has to be created with keyword argument textvariable. Afterwards, every time you change entry_text with set, the text will automatically show up in the Entry widget.
entry_text = tk.StringVar()
entry = tk.Entry(root, textvariable=entry_text)
new_text = "Example text"
entry_text.set(new_text)
Complete working example which contains both methods to set the text via Button:
This window
is generated by the following complete working example:
import tkinter as tk
def button_1_click():
# define new text (you can modify this to your needs!)
new_text = "Button 1 clicked!"
# delete content from position 0 to end
entry.delete(0, tk.END)
# insert new_text at position 0
entry.insert(0, new_text)
def button_2_click():
# define new text (you can modify this to your needs!)
new_text = "Button 2 clicked!"
# set connected text variable to new_text
entry_text.set(new_text)
root = tk.Tk()
entry_text = tk.StringVar()
entry = tk.Entry(root, textvariable=entry_text)
button_1 = tk.Button(root, text="Button 1", command=button_1_click)
button_2 = tk.Button(root, text="Button 2", command=button_2_click)
entry.pack(side=tk.TOP)
button_1.pack(side=tk.LEFT)
button_2.pack(side=tk.LEFT)
root.mainloop()
Your problem is that when you do this:
a = Button(win, text="plant", command=setText("plant"))
it tries to evaluate what to set for the command. So when instantiating the Button object, it actually calls setText("plant"). This is wrong, because you don't want to call the setText method yet. Then it takes the return value of this call (which is None), and sets that to the command of the button. That's why clicking the button does nothing, because there is no command set for it.
If you do as Milan Skála suggested and use a lambda expression instead, then your code will work (assuming you fix the indentation and the parentheses).
Instead of command=setText("plant"), which actually calls the function, you can set command=lambda:setText("plant") which specifies something which will call the function later, when you want to call it.
If you don't like lambdas, another (slightly more cumbersome) way would be to define a pair of functions to do what you want:
def set_to_plant():
set_text("plant")
def set_to_animal():
set_text("animal")
and then you can use command=set_to_plant and command=set_to_animal - these will evaluate to the corresponding functions, but are definitely not the same as command=set_to_plant() which would of course evaluate to None again.
One way would be to inherit a new class,EntryWithSet, and defining set method that makes use of delete and insert methods of the Entry class objects:
try: # In order to be able to import tkinter for
import tkinter as tk # either in python 2 or in python 3
except ImportError:
import Tkinter as tk
class EntryWithSet(tk.Entry):
"""
A subclass to Entry that has a set method for setting its text to
a given string, much like a Variable class.
"""
def __init__(self, master, *args, **kwargs):
tk.Entry.__init__(self, master, *args, **kwargs)
def set(self, text_string):
"""
Sets the object's text to text_string.
"""
self.delete('0', 'end')
self.insert('0', text_string)
def on_button_click():
import random, string
rand_str = ''.join(random.choice(string.ascii_letters) for _ in range(19))
entry.set(rand_str)
if __name__ == '__main__':
root = tk.Tk()
entry = EntryWithSet(root)
entry.pack()
tk.Button(root, text="Set", command=on_button_click).pack()
tk.mainloop()
e= StringVar()
def fileDialog():
filename = filedialog.askopenfilename(initialdir = "/",title = "Select A
File",filetype = (("jpeg","*.jpg"),("png","*.png"),("All Files","*.*")))
e.set(filename)
la = Entry(self,textvariable = e,width = 30).place(x=230,y=330)
butt=Button(self,text="Browse",width=7,command=fileDialog).place(x=430,y=328)

Categories

Resources