How to display line numbers in tkinter.Text widget? - python

I'm writing my own code editor and I want it to have numbered lines on left side. Based on this answer I wrote this sample code:
#!/usr/bin/env python3
import tkinter
class CodeEditor(tkinter.Frame):
def __init__(self, root):
tkinter.Frame.__init__(self, root)
# Line numbers widget
self.__line_numbers_canvas = tkinter.Canvas(self, width=40, bg='#555555', highlightbackground='#555555', highlightthickness=0)
self.__line_numbers_canvas.pack(side=tkinter.LEFT, fill=tkinter.Y)
self.__text = tkinter.Text(self)
self.__text['insertbackground'] = '#ffffff'
self.__text.pack(side=tkinter.LEFT, fill=tkinter.BOTH, expand=True)
def __update_line_numbers(self):
self.__line_numbers_canvas.delete("all")
i = self.__text.index('#0,0')
self.__text.update() #FIX: adding line
while True:
dline = self.__text.dlineinfo(i)
if dline:
y = dline[1]
linenum = i[0]
self.__line_numbers_canvas.create_text(1, y, anchor="nw", text=linenum, fill='#ffffff')
i = self.__text.index('{0}+1line'.format(i)) #FIX
else:
break
def load_from_file(self, path):
self.__text.delete('1.0', tkinter.END)
f = open(path, 'r')
self.__text.insert('0.0', f.read())
f.close()
self.__update_line_numbers()
class Application(tkinter.Tk):
def __init__(self):
tkinter.Tk.__init__(self)
code_editor = CodeEditor(self)
code_editor.pack(fill=tkinter.BOTH, expand=True)
code_editor.load_from_file(__file__)
def run(self):
self.mainloop()
if __name__ == '__main__':
app = Application()
app.run()
Unfortunately something is wrong inside __update_line_numbers. This method should write line numbers from top to bottom on my Canvas widget but it prints only the number for the first line (1) and then exits. Why?

The root problem is that you're calling dlineinfo before returning to the runloop, so the text hasn't been laid out yet.
As the docs explain:
This method only works if the text widget is updated. To make sure this is the case, you can call the update_idletasks method first.
As usual, to get more information, you have to turn to the Tcl docs for the underlying object, which basically tell you that the Text widget may not be correct about which characters are and are not visible until it's updated, in which case it may be returning None not because of any problem, but just because, as far as it's concerned, you're asking for the bbox of something that's off-screen.
A good way to test whether this is the problem is to call self.__text.see(i) before calling dlineinfo(i). If it changes the result of dlineinfo, this was the problem. (Or, if not that, at least something related to that—for whatever reason, Tk thinks everything after line 1 is off-screen.)
But in this case, even calling update_idletasks doesn't work, because it's not just updating the line info that needs to happen, but laying out the text in the first place. What you need to do is explicitly defer this call. For example, add this line to the bottom of load_from_file and now it works:
self.__text.after(0, self.__update_line_numbers)
You could also call self.__text.update() before calling self.__update_line_numbers() inline, and I think that should work.
As a side note, it would really help you to either run this under the debugger, or add a print(i, dline) at the top of the loop, so you can see what you're getting, instead of just guessing.
Also wouldn't it be easier to just increment a linenumber and use '{}.0'.format(linenumber) instead of creating complex indexes like #0,0+1line+1line+1line that (at least for me) don't work. You can call Text.index() to convert any index to canonical format, but why make it so difficult? You know that what you want is 1.0, 2.0, 3.0, etc., right?

The root cause of the problem is that the text hasn't been drawn on the screen yet, so the call to dlineinfo will not return anything useful.
If you add a call to self.update() before drawing the line numbers, your code will work a little better. It won't work perfectly, because you have other bugs. Even better, call the function when the GUI goes idle, or on a Visibility event or something like that. A good rule of thumb is to never call update unless you understand why you should never call update(). In this case, however, it's relatively harmless.
Another problem is that you keep appending to i, but always use i[0] when writing to the canvas. When you get to line 2, i will be "1.0+1line". For line three it will be "1.0+1line+1line", and so on. The first character will always be "1".
What you should be doing is asking tkinter to convert your modified i to a canonical index, and using that for the line number. For example:
i = self.__text.index('{0}+1line'.format(i))
This will convert "1.0+1line" to "2.0", and "2.0+1line" to "3.0" and so on.

Related

Tkinter: Grab content of a ScrolledText text pad

all. I'm working on a simple Notepad-like program that saves files and closes the program when the escape key is pressed. I mention this because it is in this method that the program runs into problems. textpad is a ScrolledText object.
This line:
`contents = self.textPad.get(self, 1.0, END)`
results in the following error:
Exception in Tkinter callback
Traceback (most recent call last):
File "/usr/lib/python2.7/lib-tk/Tkinter.py", line 1535, in __call__
return self.func(*args)
File "todopad.py", line 24, in save_and_quit
contents = self.textPad.get(self, 1.0, END)
AttributeError: Event instance has no attribute 'textPad'
I know this is the problem, because the program executes and terminates without issue when this line is commented out. Although I don't understand the error at all.
This has been a very long-winded way of asking: How can I retrieve the contents of a ScrolledText text pad and save it to a variable or directly write it to a file? And also an explanation about the error message?
Thank you in advance.
EDIT: As requested, here is the code for the entire thing.
import sys
import Tkinter
from Tkinter import *
from ScrolledText import *
root = Tkinter.Tk(className = "TodoPad");
textPad = ScrolledText(root, width = 80, height = 20)
def start_and_open():
textFile = open('/home/colin/documents/prog/py/todopad/todo', 'r')
contents = textFile.read()
textPad.insert('1.0', contents)
textFile.close()
def save_and_quit(self):
textFile = open('/home/colin/documents/prog/py/todopad/todo', 'w')
#contents = self.textPad.get(self, 1.0, END) # The line in question
#textFile.write(contents)
textFile.close()
root.destroy()
textPad.pack()
root.bind('<Escape>', save_and_quit)
root.after(1, start_and_open)
root.mainloop()
Since I have posted the whole thing I may as well explain the rationale behind everything. It's supposed to be a fast little thing that opens a to-do list and displays what's already on the list in the text box. I make whatever edits I like, then it saves before closing when I hit escape, problem being is that it doesn't like closing because of the line that I mentioned previously in my post.
First of all, kudos on identifying the problem.
Placing the Widget
To answer what is going wrong: you need to actually place the widget into the window frame. You have a choice between .grid() and .pack(). The first allows you to pick exactly where you want it to go, the second puts in a (technically) default location.
Right now, the instance of your widget is not preset, so your program has no idea where to pull the value from. You have to set a location. i would recommend using .grid(), but for the example .pack() will work as well.
textPad = ScrolledText(root, width = 80, height = 20)
textPad.pack()
Try this, and see if it works. This should fix it, but I could be wrong.
Do NOT just do
textPad = ScrolledText(root, width = 80, height = 20).pack()
The pack() function returns a NULL and will nullify your widget.
Your Issue With Self
Finally, why are you using self at all? You are not using any classes--you need to globalize the variable. The error that is thrown is a result of your program not knowing what class you are pulling the self instance from. Remove the self variables from the program, and put this into the function:
global textPad
This will make it global and all functions will be able to use it.
This should solve all the problems you have right now. However, give it a try and report what happens.
Here are some resources on global variables, getting input from widgets, and saving to files;
http://www.python-course.eu/python3_global_vs_local_variables.php
http://effbot.org/tkinterbook/text.htm
http://www.afterhoursprogramming.com/tutorial/Python/Writing-to-Files/
Happy coding, and best of luck!!

Creating A UI Window For My Previous Script

folks! So, thanks to you guys I was able to figure out what it was I was doing wrong in my previous script of staggering animation for selected objects in a scene. I am now on part two of this little exercise: Creating a UI for it.
This involves creating a window with a button and user input of how much the animation will be staggered by. So, instead of me putting how much the stagger should increment by (which was two in my previous script), I'd now allow the user to decide.
The script I have so far created the window, button, and input correctly, though I am having some trouble with getting the UI to properly execute, meaning when I click on the button, no error pops up; in fact, nothing happens at all to change the scene. I get the feeling it's due to my not having my increment variable in the correct spot, or not utilizing it the right way, but I'm not sure where/how exactly to address it. Any help would be greatly appreciated.
The code I have (with suggested edits) is as follows:
import maya.cmds as cmds
spheres = cmds.ls(selection=True)
stagWin = cmds.window(title="Stagger Tool", wh=(300,100))
cmds.columnLayout()
button = cmds.button(label="My Life For Aiur!")
count = cmds.floatFieldGrp(fieldgroup, query=True, value=True)
fieldgroup = cmds.floatFieldGrp(numberOfFields=1)
cmds.button(button, edit=True, command=lambda _:stagger(fieldgroup))
cmds.showWindow(stagWin)
def stagger(fieldgroup):
for i in spheres:
cmds.selectKey(i)
cmds.keyframe(edit=True, relative=True, timeChange=count)
print "BLAH"
Moving the comments into an answer because I think I've got it all figured out finally:
First of all, the better practice is to pass the stagger object to the button command rather than the string. so that would be:
cmds.button(label="My Life For Aiur!", command=stagger)
Secondly, the count isn't getting updated, so it stays 0 as per your third line. To update that:
count = cmds.floatFieldGrp(fieldgroup, query=True, value=True)
But wait, where did fieldgroup come from? We need to pass it into the function. So go back to your button code and take out the command entirely, also saving the object to a variable:
button = cmds.button(label="My Life For Aiur!")
Now store the object for the fieldgroup when you make it:
fieldgroup = cmds.floatFieldGrp(numberOfFields=1)
Now that you have fieldgroup, you can pass that in the function for the button, like this:
cmds.button(button, edit=True, command=lambda _:stagger(fieldgroup))
I had to wrap the function in a lambda because we're passing fieldgroup, but if I just put stagger(fieldgroup) it would call that and pass the result of that into the command for the button
Also update stagger def with fieldgroup argument:
def stagger(fieldgroup):
One final note that won't actually affect this, but good to know:
when you shift the keyframes inside stagger you're using a DIFFERENT count variable than the one you declared as 0 up above. The outer one is global, and the inner is local scope. Generally it's best to avoid global in the first place, which fortunately for you means just taking out count = 0
Putting that all together:
import maya.cmds as cmds
spheres = cmds.ls(selection=True)
stagWin = cmds.window(title="Stagger Tool", wh=(300,100))
cmds.columnLayout()
button = cmds.button(label="My Life For Aiur!")
fieldgroup = cmds.floatFieldGrp(numberOfFields=1)
cmds.button(button, edit=True, command=lambda _:stagger(fieldgroup))
cmds.showWindow(stagWin)
def stagger(fieldgroup):
count = 0
increment = cmds.floatFieldGrp(fieldgroup, query=True, value=True)[0]
print count
for i in spheres:
cmds.selectKey(i)
cmds.keyframe(edit=True, relative=True, timeChange=count)
count += increment
print "BLAH"

Text entry event?

As part of my program I ask the user for their name and class (high school class). Once the user presses 'Enter' after typing their name the button is disabled and the 'tutor' field appears. However, the user is in essence able to submit their name even if they haven't typed anything. I only want the 'Enter' button to be active once the user has started typing.
What I have done below doesn't seem to work :(
Also, my input validation doesn't work - know why?
class Enter_Name_Window(tk.Toplevel):
'''A simple instruction window'''
def __init__(self, parent):
tk.Toplevel.__init__(self, parent)
self.text = tk.Label(self, width=40, height=2, text= "Please enter your name and class." )
self.text.pack(side="top", fill="both", expand=True)
name_var = StringVar()
def validate_enter_0():
self.Enter_0.config(state=(NORMAL if name_var.get() else DISABLED))
print("validate enter worked")
name_var.trace('w', lambda name, index, mode: validate_enter_0)
enter_name = Entry(self, textvariable=name_var)
enter_name.pack()
enter_name.focus_set()
def callback():
if len(name_var) > 10 or any(l not in string.ascii_letters for l in name_var):
print("Input validation worked")
self.display_name = tk.Label(self, width=40, height=2, text = "Now please enter your tutor group.")
self.display_name.pack(side="top", fill="both", expand=True)
tutor_var = StringVar()
def validate_enter_2():
self.Enter_0_2.config(state=(NORMAL if tutor_var.get() else DISABLED))
print("validate enter worked")
tutor_var.trace('w', lambda name, index, mode: validate_enter_0_2)
tutor = Entry(self, textvariable=tutor_var)
tutor.pack()
tutor.focus_set()
self.Enter_0.config(state="disabled")
self.Enter_0_2 = Button(self, text="Enter", width=10, command=self.destroy)
self.Enter_0_2.pack()
self.Enter_0 = Button(self, text="Enter", width=10, command=callback)
self.Enter_0.pack()
The first obvious problem is this line:
tutor_var.trace('w', lambda name, index, mode: validate_enter_0_2)
You've created a function that takes three variables, and returns the validate_enter_0_2 function as a function object. That doesn't do any good.
You want to create a function that calls the validate_enter_0_2 function. Like this:
tutor_var.trace('w', lambda name, index, mode: validate_enter_0_2())
You have the exact same problem with name_var and will also need to fix it there, of course.
On top of that, you don't actually have a function named validate_enter_0_2 to call, because you defined it as validate_enter_2. This means your validation function just raises a NameError instead of doing useful. Or, if you have a validate_enter_2 function defined somewhere else in your code, it calls the wrong function. (This is one reason that cryptic names like enter_0_2 and enter_2 are not a good thing.)
There's at least one other problem with your code: You're repeatedly trying to use name_var, which is a StringVar object, as if it were a string. You can't do that. And if you actually look at the console output, Tkinter will tell you this, with tracebacks like this:
Exception in Tkinter callback
Traceback (most recent call last):
File "/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/lib-tk/Tkinter.py", line 1410, in __call__
return self.func(*args)
File "tkval2.py", line 25, in callback
if len(name_var) > 10 or any(l not in string.ascii_letters for l in name_var):
AttributeError: StringVar instance has no attribute '__len__'
And that exception is happening before you get a chance to create the new Entry.
To fix that, you need to call get on the StringVar whenever you want to get its value, like this:
if len(name_var.get()) > 10 or any(l not in string.ascii_letters for l in name_var.get())
Finally, as I explained in the answer to your other question, your trace validator isn't going to get called until something changes. This means you will either need to call it explicitly, or explicitly name_var.set(''), or just start the button off disabled. As written, it will start off enabled, and only disable if you type something and then erase it.
I'm not sure whether those are the only problems with your code, but all of them will certainly prevent your validation from working as expected. So, you need to fix all of them, and any other errors in your code, before it will work.
From your comments:
I am however wondering how to create a pop up message displaying an error…
When do you want to do that? What condition do you want to check, and when do you want to check it?
At any rate, as in most GUIs, the way to "pop up" anything like this is a dialog. Dialog Windows in the Tkinter book explains everything you need to know. But you don't need to copy-paste all that code, or write it from scratch; the stdlib comes with Tkinter helper modules that do most of the work for you. In your case, you probably just want tkMessageBox.
… and something that forces the user to re enter their name
Force them how? Just erasing the existing Entry contents would leave them with an empty box to fill in, which would also disable the button. Is that what you want?
Anyway, guessing at what you want, it could look something like this:
def callback():
if len(name_var.get()) > 10:
tkMessageBox.showerror("Bad name", "Your name is too long. What's wrong with good American names like Joe?")
name_var.set('')
return
# the rest of your code here
In the callback function (called when they click the button after typing their name), instead of just checking some condition and printing something out, I check a condition and pop up an error dialog, clear out the existing name, and return early instead of creating the second half of the form. I didn't handle your other condition (any non-ASCII letters), but it should be obvious how to add that.
However, validation like this might be better done through actual validation—instead of making them wait until they click the button, catch it as soon as they try to type the 11th character, or a space or accented character, or whatever else you don't like. Then you can pop up a message box, and either disable the button until they fix it, reject/undo the change (which is easier with a validatecommand function than with a trace function, as shown in my answer to your previous question).
One last thing: Instead of a message box, it may be better to just embed the error as, say, a Label that appears in the form itself (maybe with the error description in red, with a big flag icon). This is common in web apps and in more modern GUIs because it provides more immediate feedback, and is less obtrusive to the user flow.

Python Tkinter Calculator wont evaluate text from entry widget

I am trying to make a simple calculator app using tkinter, but everytime I run the code below i get an error message saying
Traceback (most recent call last):
File "C:\Python33\Lib\site-packages\pythonwin\pywin\framework\scriptutils.py", line 326, in RunScript
exec(codeObject, __main__.__dict__)
File "C:\Users\csp\Python\Calculator App.py", line 17, in <module>
solved = eval(expression)
File "<string>", line 0
^
SyntaxError: unexpected EOF while parsing
CODE:
from tkinter import *
tk = Tk()
tk.title('Calculator')
inp = Entry(tk,text="Enter Expression Here",width=20)
inp.pack()
exit = False
def exitbtn():
global exit
exit = True
return exit
btn = Button(tk,text="Quit?",command=exitbtn)
btn.pack
canvas = Canvas(tk,width=200,height=200)
canvas.pack()
while not exit:
expression = inp.get()
solved = eval(expression)
canvas.create_text(100,100,text=expression,font=('Times', 15))
canvas.create_text(100,150,text=solved,font=('Times', 15))
if exit == True:
break
tk.destroy()
i am really new to Python and dont understand why the "solved = eval(expression)" line wont work. please help
So, the reason why eval is not working is because when you first start your program, expression is just an empty string. If you go to the python shell, and type in eval(''), you'll see the same error appear.
One solution would be to check if expression is an empty string or not, and do something like this:
expression = inp.get()
if expression != '':
solved = eval(expression)
else:
solved = '?'
However, even after you apply this fix, your program won't work, for unrelated reasons. The primary reason is that you never call tk.mainloop() (or whatever it's called), so the window will not show up.
This is because of your while loop -- what you wanted to do was to constantly check the input field and update your canvas whenever you get new input after running it through eval.
However, GUI programs, in general, don't work that way and require a different mindset and approach while writing them. Instead of writing loops to check and update program state, you write functions that will automatically be called whenever the program state changes (which are called events). It'll feel a bit backwards at first, but over time it'll help make your code cleaner and easier to manage.
You're actually already doing this in one part of your program -- with your exitbtn function. Now, you just need to convert your while loop into a similar function and bind it to the Entry object.
EDIT:
Here's some example code that does what you want:
import sys
from tkinter import *
# Create the GUI
tk = Tk()
tk.title('Calculator')
inp = Entry(tk, text="Enter Expression Here", width=20)
inp.pack()
btn = Button(tk, text="Quit?")
btn.pack()
canvas = Canvas(tk, width=200, height=200)
canvas.pack()
# Create callback functions
def end_program(event):
'''Destroys the window and ends the program without needing
to use global variables or a while loop'''
tk.destroy()
sys.exit() # Automatically ends any Python program
def update_canvas(event):
'''Gets the input, tries to eval it, and displays it to the canvas'''
expression = inp.get()
try:
solved = eval(expression)
except SyntaxError:
# The expression wasn't valid, (for example, try typing in "2 +")
# so I defaulted to something else.
solved = '??'
canvas.delete('all') # remove old text to avoid overlapping
canvas.create_text(100, 100, text=expression,font=('Times', 15))
canvas.create_text(100, 150, text=solved,font=('Times', 15))
# Bind callbacks to GUI elements
btn.bind('<Button-1>', end_program)
inp.bind('<KeyRelease>', update_canvas)
# Run the program
tk.mainloop()
Some things to note:
I moved your code for checking inp and writing to the canvas to the update_canvas function, and got rid of the while loop.
The update_canvas function will automatically be called whenever somebody lets go of a key while typing in the inp object (the <KeyRelease> event).
This can cause some problems -- this will mean your update_canvas function will be called while the user is in the process of typing text into your calculator. For example, what if the user types in 2 + 2 *? It's not a complete expression, so can't be parsed by eval.
To solve this, I just wrapped eval in a try-except to prevent any bad input from mucking up the program.
Similarly, end_program will be called whenever somebody left-clicks on the btn object (the <Button-1> event).

Python-Tkinter Text Insert not affecting box

First time poster, found the site very helpful before registering though.
I am having issues using Tkinter on Python 2.7 (Windows7):
The code (I have truncated it because the whole thing is massive) looks something like this:
-------------------------------------------------------
CODE:
#set up stuff, importing variables, etc, then we have:
class App:
global RXSerial
RXSerial=''
#The following lines define the topFrame, lays out the widgets.
def __init__(self, master):
topFrame = Frame(master)
topFrame.pack()
middleFrame = Frame(master)
middleFrame.pack()
#--------------defining state variables------------
self.inputConsole = Text(middleFrame)
self.inputConsole.insert(INSERT,"Data recieved from Serial:")
self.inputConsole.config(width=100,height=20)
self.inputConsole.pack(side=LEFT,padx=20,pady=20)
#blah blah blah, insert a bunch of stuff (buttons etc.) here:
#The following lines define the functions to be called when the buttons are pressed.
def engineFire(self,engineUse,pwm):
RXSerial='this should pop up in the text called inputConsole'
print RXSerial
self.inputConsole.insert(INSERT, RXSerial)
---------------------------------------------------
so yeah, basically RXSerial is a string (that I have checked that is working, the print RXSerial line successfully prints when called by a button. The problem is that the self.inputConsole.insert(INSERT,RXSerial) line is not working. Can anybody please help? I have tried a bunch of combinations of stuff but cant seem to get it working. Thank you.
If you're trying to insert the text from another thread it may fail to work. Also, if at some point you configured the text widget to be in the disabled state then inserting will fail. If that's the case (widget is disabled), setting the state to "normal" temporarily will solve the problem.
Without more information it's impossible to say for sure.

Categories

Resources