Match Tkinter Text tag to string? - python

I'm wondering if it's possible to bind a tag, such as 'keyword', to a string, such as 'print' to my Text widget in Tkinter (Python).
I'd like to know this because I'm not sure how I can use regular expressions to connect to my Text widget, and it seems extremely over-complicated, so I was wondering if there was any normal way.
My current code (SyntaxHighlighting.py):
global lang
lang = 'py'
def highlighter_bind(obj):
# Bind a highlighting language to a text widget within obj.
# Notice that obj must be a Frame with a .root property being
# the root it is binded to, and a .text widget being where I
# can highlight the text.
#
# The lang variable must be specified as a string that
# contains the file extension of whatever you want to
# highlight.
#
# Supported languages:
# - py
def command(self):
# Sub command for highlighting, it's what will
# be "binded" to <Key>.
if lang == 'py':
print 'imported language py'
# This is to get all the contents from the Text widget (property).
t = o.text.get('1.0', END).split('\n')[0:len(o.text.get('1.0', END).split('\n')) - 1]
# Now loop through the text and find + add a tag to each of the line's contents, assuming that there is something to be found.
for i in range(0, len(t)):
# We use the try / except because search.group will return an error if it's a NoneType.
try:
# Now we need to search through the line and see if we can find print.
if search('print ', t[i]).group(0):
# Now, WHERE did we find print? I get confused there,
# because I have no idea where to find the index(es) of
# the string I found within a line. I use a regex to find the
# string, but how do I find where?
print "Ok!" # Temporary, just stating that I found it. It works.
except:
pass
else:
print 'unrecognized language:', lang
o.root.bind('<Key>', command)
Ideal code were it to actually work:
...
def command(self):
if lang == 'py':
obj.text.tag_match("print", 'keyword')
# And even better, using regex to match..
obj.text.tag_match("(if|elif|else)", 'keyword')
obj.text.tag_match("(not|and|or)", 'keyword')
obj.text.tag_match("(\+|-|*|/|**|%%)", 'keyword') # %% is to put a modulus in.

The way to apply highlighting according to a regular expression or fixed string is to use the text widget search method. Your method of searching the raw text and then trying to determine the index is not the right approach.
Here is an example of applying the tag "keyword" to all instances of the word "print":
import Tkinter as tk
class Example(tk.Frame):
def __init__(self, parent):
tk.Frame.__init__(self, parent)
self.text = tk.Text(self, wrap="none")
xsb = tk.Scrollbar(self, orient="horizontal", command=self.text.xview)
ysb = tk.Scrollbar(self, orient="vertical", command=self.text.yview)
self.text.configure(yscrollcommand=ysb.set, xscrollcommand=xsb.set)
ysb.grid(row=0, column=1, sticky="ns")
xsb.grid(row=1, column=0, sticky="ew")
self.text.grid(row=0, column=0, sticky="nsew")
self.grid_rowconfigure(0, weight=1)
self.grid_columnconfigure(0, weight=1)
self.text.tag_configure("keyword", foreground="#b22222")
# this probably isn't what you want to do in production,
# but it's good enough for illustrative purposes
self.text.bind("<Any-KeyRelease>", self.highlight)
self.text.bind("<Any-ButtonRelease>", self.highlight)
def highlight(self, event=None):
self.text.tag_remove("keyword", "1.0", "end")
count = tk.IntVar()
self.text.mark_set("matchStart", "1.0")
self.text.mark_set("matchEnd", "1.0")
while True:
index = self.text.search("print", "matchEnd","end", count=count)
if index == "": break # no match was found
self.text.mark_set("matchStart", index)
self.text.mark_set("matchEnd", "%s+%sc" % (index, count.get()))
self.text.tag_add("keyword", "matchStart", "matchEnd")
if __name__ == "__main__":
root = tk.Tk()
Example(root).pack(fill="both", expand=True)
root.mainloop()
For an example that subclasses the text widget and adds a method for highlighting any regular expression, see this answer to the question How to highlight text in a tkinter Text widget

Related

How to display text beside cursor in tkinter Listbox

I have this few lines of code which print the selected content in the Listbox when double clicked but i want to display text beside the cursor like double click the selected content to print to prompt the user to take such action before it can be printed.
I search the listbox documentation and found the is an attribute cursor which you set the cursor type to display when the widget has focus but didn't find something like cursor-text to do that.Is the a way i can work around to achieve that, your suggestions are welcomed.
from tkinter import *
def test(event=None):
print("woow test works")
print(l.get(ACTIVE))
root = Tk()
l = Listbox(root, cursor="tcross")
l.pack()
l.insert(END, ("today"),("tomorrow"))
l.bind("<Double-Button-1>", test)
root.mainloop()
I don't quite understand your English, but you can see this page for an example of using tooltips, which make text appear on mouse hover.
Yes, it's possible to add specific info or comments to listbox lines using Hovertip from idlelib.tooltip. In the example below, Mark Lutz's customizable scrolled listbox works normally, except that right-clicking on any line opens a tip with info/comments for that line.
Ref. : Programming Python 4th ed. Example 9-9
from tkinter import *
from idlelib.tooltip import Hovertip
class ScrolledList(Frame):
def __init__(self, options, parent=None):
Frame.__init__(self, parent)
self.pack(expand=YES, fill=BOTH)
botfrm = Frame(parent)
botfrm.pack(side=BOTTOM)
Label(botfrm,
text="Select a line and right-click for info").pack()
self.makeWidgets(options)
self.myTip = None
def handleList(self, event):
if self.myTip:
self.myTip.__del__()
label = self.lstbx.get(ACTIVE)
self.runCommand(label)
def overflyLine(self, event):
if self.myTip:
self.myTip.__del__()
self.myTip = Hovertip(self.lstbx,f"Comments for {self.lstbx.get(ACTIVE)}")
self.myTip.showtip()
def makeWidgets(self, options):
sbar = Scrollbar(self)
list = Listbox(self, relief=SUNKEN, bg='misty rose')
sbar.config(command=list.yview)
list.config(yscrollcommand=sbar.set)
sbar.pack(side=RIGHT, fill=Y)
list.pack(side=LEFT, expand=YES, fill=BOTH)
for label in options:
list.insert(END, label)
self.lstbx = list
list.bind('<Button-3>', self.overflyLine)
list.bind('<Double-1>', self.handleList)
list.bind('<Return>', self.handleList)
def runCommand(self, selection):
print('You selected:', selection)
if __name__ == '__main__':
options = (('Lumberjack-%s' % x) for x in range(20))
scr = ScrolledList(options).mainloop()

tkinter, "refresh" the mainwindow

Though I think that the solution might be similar to this one: tkinter, display a value from subwindow in mainwindow , I still decided to ask this question since I have trouble to figure it out on my own.
I have the list "fields" with which I am creating any given number of rows, with two labes inside of them. After opening a subwindow, I want to be able to manipulate that list (in my example simply just append at the moment) and after clicking on a button (here "ADD"), I want the mainwindow to update, so that it shows the rows of the manipulated list. It works fine for the most part but I dont what is the best way to update the mainwindow in this example.
The only solution I was able to come up with, is to destroy the mainwindow and recreate it but I have the feeling that this might not be the best solution. Is there a better one?
import tkinter as tk
a=0
fields=[("a",1),("c",2),("e",3)]
class clsApp(object):
def __init__(self):
self.root=tk.Tk()
self.root.title("MainWindow")
##Labels##
self.rootLabel=tk.Label(self.root, text="WindowAppExperiment", padx=100)
self.aLabel=tk.Label(self.root, text=a, padx=100)
##Buttons##
self.BtExit=tk.Button(self.root, text="Quit", fg="red", command=self.root.quit)
###self.BtNewWindow=tk.Button(self.root, text ="Edit", command=lambda:self.clsNewWindow(self.root, self.aLabel).run())
self.BtNewField=tk.Button(self.root, text ="New Field", padx=30, command=lambda:self.clsNewFields(self.root).run())
def grid (self):
self.rootLabel.pack()
self.aLabel.pack()
self.fckPackFields()
self.BtNewField.pack()
self.BtExit.pack(side="left")
###self.BtNewWindow.pack(side="right")
def fckPackFields(self):
if fields:
for field in fields:
##create labels##
row=tk.Frame(self.root)
nameLabel=tk.Label(row, text =field[0], width=20, anchor="w")
valueLabel=tk.Label(row, text =field[1], width=5)
##pack labels##
row.pack(side="top", fill="x", padx=5, pady=5)
nameLabel.pack(side="left")
valueLabel.pack(side="right", expand=True, fill="x")
def run(self):
self.grid()
self.root.mainloop()
self.root.destroy()
class clsNewFields(object):
def __init__(self, Parent):
self.parent=Parent
##Window##
self.top=tk.Toplevel()
self.top.title("Add Fields")
##Labels##
self.enterNameLabel=tk.Label(self.top, text ="Enter fieldname", padx=10)
self.enterValueLabel=tk.Label(self.top, text ="Enter value", padx=10)
##Entryfields##
self.EntryName=tk.Entry(self.top)
self.EntryValue=tk.Entry(self.top)
##Buttons##
self.BtADD=tk.Button(self.top, text ="ADD", command=lambda:self.fckAddField(self.EntryName, self.EntryValue))
self.BtClose=tk.Button(self.top, text ="Close", command=self.top. quit)
def grid(self):
self.enterNameLabel.pack()
self.enterValueLabel.pack()
self.EntryName.pack()
self.EntryValue.pack()
self.BtADD.pack()
self.BtClose.pack()
def fckAddField(self, Name, Value):
self.name=Name.get()
self.value=Value.get()
global fields
fields.append((self.name, self.value))
print(fields)
self.parent.update
def run(self):
self.grid()
self.top.mainloop()
self.top.destroy()
clsApp().run()
Welcome to StackOverflow.
First of all - do you really want to declare the clsNewFields inside your clsApp ? Yes, the Fields should be used inside App, but i do not see a need for using class-in-class-declaration.
Second - you are packing the Fields in def fckPackFields(self):. This is not automatically called when you update it.
You are not calling update function by using self.parent.update.
You are using global variable for fields, what does not really suit your needs. Why not having a list inside your App-class like:
def __init__(self):
self.__fields=[]
def __set_fields(self, value):
self.__fields=value
def __get_fields(self):
return self.__fields
Fields = property(__get_fields, __set_fields)
def __loadUI(self, event=None):
# This function should be called at the end of __init__
self.fieldFrame=tk.Frame(self.root)
self.fieldFrame.pack(side="top")
def fckPackFields(self):
#First clean area
[...]
#Then add fields
for field in self.__fields:
# create the row, etc.
# !!! but do it inside self.fieldFrame !!!
[...]
I would prefer using grid instead of pack over here, because there I think it is easier to place a frame at a certain position, then you could just destroy self.fieldFrame and recreate it at the same position for placing the fields in it.
UPDATE:
Just checked your code again. With some simple tricks your can tweak your GUI to do what you want:
def __init__(self):
self.fieldFrame=None #this line has been added
#completely reworked this function
def grid(self):
self.rootLabel.grid(row=1, column=0, columnspan=2, sticky=tk.NW+tk.SE)
self.fckPackFields()
self.BtNewField.grid(row=3, column=0, sticky=tk.NW+tk.SE)
self.BtExit.grid(row=3, column=1, sticky=tk.NW+tk.SE)
#Only one line changed / one added
def fckPackFields(self):
self.__cleanFields() #added this line, function beyond
if fields:
for field in fields:
##create labels##
row=tk.Frame(self.fieldFrame) #add the row to the fieldFrame
[...]
#In here we destroy and recreate fieldFrame as needed
def __cleanFields(self):
if self.fieldFrame:
self.fieldFrame.destroy()
##FieldFrame##
self.fieldFrame=tk.Frame(self.root)
self.fieldFrame.grid(row=2, column=0, columnspan=2)
In clsNewFields:
def fckAddField(self, Name, Value):
[...]
self.parent.fckPackFields() # instead of self.parent.update
EDIT:
Have a look at these two questions:
Class inside a Class
Benefit of nested Classes
I did not mean to point out that nested classes are to be avoided in general but I do want to focus you into the thought of "is there a real necessity or benefit of it for my use-case".

Tkinter - Find closest object of a certain type

The proglem I have here is that I am not able to find the closest object of a certain type. It is best if I give you the code and for you to see what I mean:
from Tkinter import *
root = Tk()
f=Frame(root)
f.grid()
w=Canvas(f)
def identify(event): ## this should identify the tag near to click
item = w.find_closest(event.x, event.y)[0]
print item
line1=w.create_line(50,50,150,150, width=5, tags="line")
line2=w.create_line(100,100,100,350, width=3, tags="line")
line3=w.create_line(150,150,150,450, width=3, tags = "line")
w.grid(row=0, column=0)
w.bind("<ButtonRelease-1>", identify)
u=Frame(f)
u.grid(row=0, column=1)
root.mainloop()
As you can see here, you are able to click anywhere on the screen and it will return the closest object. This is what I would like to achieve but having other objects on the screen which I WISH TO BE IGNORED. I am able to do this by using tags but the problem here is you need to click on the actual object for some reason. This code is being used to show my problem. My actual code is a Towers of Hanoi game, and I am aiming to find the closest pole so the disk is able to snap to it, but I cannot find the closest pole without clicking on every pole before moving a disk.
Here is the code showing my problem. Note: I have only changed "w.bind("", identify)" to "w.tag_bind("line", "", identify)"
from Tkinter import *
root = Tk()
f=Frame(root)
f.grid()
w=Canvas(f)
def identify(event): ## this should identify the tag near to click
item = w.find_closest(event.x, event.y)[0]
print item
line1=w.create_line(50,50,150,150, width=5, tags="line")
line2=w.create_line(100,100,100,350, width=3, tags="line")
line3=w.create_line(150,150,150,450, width=3, tags = "line")
w.grid(row=0, column=0)
w.tag_bind("line", "<ButtonRelease-1>", identify)
u=Frame(f)
u.grid(row=0, column=1)
root.mainloop()
It's little late for you, but maybe it will be useful for others...
I just faced the same problem today, and resolved it using a second Canvas with duplicates of the desired items.
This canvas is never printed. It's a little tricky, but I couldn't find better.
draft = Canvas(w) # if w is deleted, the draft is deleted
draft.delete(ALL) # if you use the fake canvas for other uses
concerned = w.find_withtag("line") # what you want
for obj in concerned: # copy on draft
# first, get the method to use
if w.type(obj) == "line": create = draft.create_line
# use "elif ..." to copy more types of objects
else: continue
# copy the element with its attributes
config = {opt:w.itemcget(obj, opt) for opt in w.itemconfig(obj)}
config["tags"] = str(obj) # I can retrieve the ID in "w" later with this trick
create(*w.coords(obj), **config)
# use coordinates relative to the canvas
x = w.canvasx(event.x)
y = w.canvasy(event.y)
item = draft.find_closest(x,y) # ID in draft (as a tuple of len 1)
if item: item = int( draft.gettags(*item)[0] ) # ID in w
else: item = None # closest not found
print(item)
I wish I could have used create(*w.coords(obj), **w.itemconfig(obj)), but itemconfig without arguments returns a dictionary whose values cannot be reused.
About item copy: Copy Tkinter Canvas Items
By the way, I used python 3, I don't know if it works in python 2.
You can determine the type of the object selected in your identify() via w.type(item). Since you may have lines that aren't poles in your game, you can add tags to identify which one(s) are.
Here's what I mean (note this is Python 3 code, but the same idea would have worked in Python 2.x):
import tkinter as tk
root = tk.Tk()
f = tk.Frame(root)
f.grid()
w = tk.Canvas(f)
def identify(event):
item = w.find_closest(event.x, event.y)[0]
print(f'type of item closest to click: {w.type(item)}')
# Check tags to see if it's a pole.
if w.type(item) == 'line': # Canvas line?
for tag in w.gettags(item): # Check its tags.
if tag.startswith('pole'):
print(f' You clicked near pole {w.gettags(item)[1]}')
break
else:
print(' You did not click near a pole')
line1 = w.create_line(50,50,150,150, width=5)
line2 = w.create_line(100,100,100,350, width=3, tags='pole 1')
line3 = w.create_line(150,150,150,450, width=3, tags='pole 2')
w.grid(row=0, column=0)
w.bind('<ButtonRelease-1>', identify)
u = tk.Frame(f)
u.grid(row=0, column=1)
root.mainloop()

Tkinter Text Tag Config not persisting

I'm trying to colour every odd line in my Application, however I just have a line that is moving down the screen. Is this because I don't persist the tag?
Here is some of the code:
def addTextToRaw(self, text, changeColour=False, numberOfLines=0):
self.rawText.config(state=NORMAL)
self.rawText.insert(END,text)
self.rawText.config(state=DISABLED)
if changeColour is True:
print "Changing Coloiur" + str(self.numberOfObjects())
lastLine = int(self.rawText.index('end-1c').split('.')[0])
start = str(lastLine)+".0"
end = str(lastLine)+".0+"+str(1+numberOfLines)+"lines"
self.rawText.tag_add("oddObject"+str(self.numberOfObjects()), start, end)
self.rawText.tag_config("oddObject"+str(self.numberOfObjects()), background="blue", foreground="white")
This method is called using the after method which it gets data from a queue which is being processed.
None of the colour lines stay theres just one line moving down the screen. So what can i do to make the colours persist?
If you want to color every other line, you merely need to add the tag once for each line, and you only need to configure the tag once when you create the widget.
Here's a working example:
import Tkinter as tk
class Example(tk.Frame):
def __init__(self, parent):
tk.Frame.__init__(self, parent)
self.text = tk.Text(self, wrap="word")
self.vsb = tk.Scrollbar(self, orient="vertical", command=self.text.yview)
self.text.configure(yscrollcommand=self.vsb.set)
self.vsb.pack(side="right", fill="y")
self.text.pack(side="left", fill="both", expand=True)
self.text.tag_configure("odd", background="white", foreground="black")
self.text.tag_configure("even", background="blue", foreground="white")
self.addText(tk.__doc__)
def addText(self, text):
# get the starting line number
index = int(self.text.index("end-1c").split(".")[0])
# insert each line, adding a odd or even tag
tag = "even" if (index%2 == 0) else "odd"
for line in text.split("\n"):
self.text.insert("end", line+"\n", tag)
tag = "even" if tag == "odd" else "odd"
if __name__ == "__main__":
root = tk.Tk()
Example(root).pack(fill="both", expand=True)
root.mainloop()
Call the tag_config method in your update method this will then call it. Also you can give it the same tag so this method can be simplified like so:
def addTextToRaw(self, text, changeColour=False):
self.rawText.config(state=NORMAL)
if changeColour is True:
self.rawText.insert(END,text ,'oddObject')
else:
self.rawText.insert(END,text)
self.rawText.config(state=DISABLED)
Then in the update method just call:
self.rawText.tag_config("oddObject", background="blue", foreground="white")

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