4

I am writing a program that involves displaying some text in a create_text() box on a Tkinter canvas, within a loop. Each word is displayed, then replaced by the next. Sort of like flash cards.

I need to color one letter of each word, close to the middle of the word, so that when the user is reading the words their eyes focus on the middle of the word. So if len(i)=1, color i[0], if len(i)>= 2 and <= 5, color i[1], and so on. It needs to be done using the Canvas, and using

canvas.create_text(text = i[focus_index],fill = 'red') 

The result should print like this

exaMple

(but obviously "m" would be colored red, not be uppercase)

fhdrsdg
  • 10,297
  • 2
  • 41
  • 62
zachcapp
  • 51
  • 1
  • 2
  • 7
  • you would have to create your own method to do this, as create_text only takes a single colour as an argument, i would suggest you look at ways to measure the size of the text on the canvas, and then write a method that breaks the string down and create each separately with their own colour – James Kent Mar 31 '15 at 13:56
  • If this is just to tell the user where to stare at, why not just put a colored dot _behind_ the text? – tobias_k Mar 31 '15 at 13:59
  • So it is a speed-reader program, sort of imitating a Spritz widget. Speed reading strategy involves focusing on the middle of the word and sort of processing the entire word into your brain with peripheral information. I need to present the word with this "focal point" being a letter in the middle of the word, of a different color than the rest of the text. I avoided using the word highlight because there is a highlight() method in Tkinter that does something else – zachcapp Mar 31 '15 at 14:56
  • Is there a reason you must use a canvas? Can you use labels or the text widget? – Bryan Oakley Mar 31 '15 at 15:23
  • The color focus is a challenge presented by my instructor. My program has worked well but I really want to implement this challenge, and this is the way he wants us to do it – zachcapp Mar 31 '15 at 16:04

2 Answers2

3

I assume you want something like this?

This is the closest I could get for now. It creates three text boxes and uses the anchor attribute to keep them somewhat in the right place. It doesn't work so good for really wide or narrow letters though. It's not perfect, but it may be a start.

import Tkinter as tk

root = tk.Tk()

c = tk.Canvas(root)
c.pack(expand=1, fill=tk.BOTH)

words = '''I am writing a program that involves displaying some text in a create_text() box on a Tkinter canvas, within a loop. Each word is displayed, then replaced by the next. Sort of like flash cards. I need to color one letter of each word, close to the middle of the word, so that when the user is reading the words their eyes focus on the middle of the word. So if len(i)=1, color i[0], if len(i)>= 2 and <= 5, color i[1], and so on. It needs to be done using the Canvas, and using canvas.create_text(text = i[focus_index],fill = 'red') The result should print like this exaMple (but obviously "m" would be colored red, not be uppercase)'''
words = words.split()

def new_word(i):
    if i == len(words):
        i = 0

    word = words[i]
    middle = (len(word)+1)//2
    c.itemconfigure(t1, text=word[:middle-1]+' ')
    c.itemconfigure(t2, text=word[middle-1:middle])
    c.itemconfigure(t3, text=word[middle:])

    root.after(100, lambda: new_word(i+1))


t1 = c.create_text(200,100,text='', anchor='e', font=("Courier", 25))
t2 = c.create_text(200,100,text='', anchor='e', font=("Courier", 25), fill='red')
t3 = c.create_text(200,100,text='', anchor='w', font=("Courier", 25))
new_word(0)

root.geometry('400x200+200+200')
root.mainloop()

Ok, using the link from Bryan Oakley's comment, I improved the code some more so it works with any font, not only monospace ones. The code keeps the center of the colored letter at the same place and places the front and back of the word at the correct distance around it.

import Tkinter as tk
import tkFont

root = tk.Tk()
c = tk.Canvas(root)
c.pack(expand=1, fill=tk.BOTH)

fn = "Helvetica"
fs = 24
font = tkFont.Font(family=fn, size=fs)

words = '''I am writing a program that involves displaying some text in a create_text() box on a Tkinter canvas, within a loop. Each word is displayed, then replaced by the next. Sort of like flash cards. I need to color one letter of each word, close to the middle of the word, so that when the user is reading the words their eyes focus on the middle of the word. So if len(i)=1, color i[0], if len(i)>= 2 and <= 5, color i[1], and so on. It needs to be done using the Canvas, and using canvas.create_text(text = i[focus_index],fill = 'red') The result should print like this exaMple (but obviously "m" would be colored red, not be uppercase)'''
words = words.split()

def new_word(i):
    if i == len(words):
        i = 0

    word = words[i]
    middle = (len(word)+1)//2

    front = word[:middle-1]
    letter = word[middle-1:middle]
    back = word[middle:]

    c.itemconfigure(t1, text=front)
    c.itemconfigure(t2, text=letter)
    c.itemconfigure(t3, text=back)
    c.coords(t1, 200-font.measure(letter)/2, 100)
    c.coords(t3, 200+font.measure(letter)/2, 100)

    root.after(100, lambda: new_word(i+1))


t1 = c.create_text(200,100,text='', anchor='e', font=font)
t2 = c.create_text(200,100,text='', anchor='c', font=font, fill='red')
t3 = c.create_text(200,100,text='', anchor='w', font=font)
new_word(0)

root.geometry('400x200+200+200')
root.mainloop()
fhdrsdg
  • 10,297
  • 2
  • 41
  • 62
  • Yes this works well with some altering, but now I can't get it to delete the t1,t2,t3 before presenting the next word. It just piles them over one another, which is a problem i had in the early stages of the program but had worked out fine with the delete() method – zachcapp Mar 31 '15 at 16:05
  • That's why I create them once and then use `itemconfigure` to replace the text instead of creating new text boxes. – fhdrsdg Mar 31 '15 at 16:11
  • Right!! Makes sense. Works great now. One last question I promise.. The font is Courier, so it is monospace, but there seems to be an extra area of space on either side of the red letter, but when I delete the space in ' ' for each sub string, it over laps the beginning and end with the middle letter. How do I fix this to make the spacing equal throughout the entire word? – zachcapp Mar 31 '15 at 16:29
  • Ah, having a monospace font helps a lot. I've edited the placement of the text boxes in my answer. I believe this works pretty good. – fhdrsdg Mar 31 '15 at 17:31
  • 1
    FWIW, tkinter has a way to measure the rendered width of a string in a given font so that you don't have to rely on having a fixed width font. See, for example, http://stackoverflow.com/a/7210681/7432 – Bryan Oakley Mar 31 '15 at 18:50
  • @BryanOakley Ah yes, that's perfect for placing the text boxes in the right spot, thanks. I've added code which uses `font.measure` to make this work for any font. – fhdrsdg Apr 01 '15 at 08:28
2

You cannot apply formatting to individual characters in a canvas text item. You would need to create a distinct item for the red character and do a little math to overlay it on top of the string.

If you don't have to use a canvas, I recommend a text widget because it's easy to apply formatting to a single character. Here's a complete working example:

import Tkinter as tk

words = '''
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi mi leo, vulputate a consectetur in, congue sit amet elit. Fusce lacinia placerat mi, vitae maximus leo congue sed. Donec non diam dapibus, fringilla risus at, interdum sem. Interdum et malesuada fames ac ante ipsum primis in faucibus. 
'''.strip().split()

class Example(tk.Frame):
   def __init__(self, parent):
      tk.Frame.__init__(self, parent)
      self.text = tk.Text(self, wrap=None, font="Helvetica 24",
                          highlightthickness=0)
      self.text.pack(side="top", fill="x")

      self.text.tag_configure("center", justify="center")
      self.text.tag_configure("red", foreground="red")

      self.show_words(0)

   def show_words(self, index):
      self.show_word(words[index])
      next = index + 1 if index < len(words)-1 else 0
      self.after(200, self.show_words, next)

   def show_word(self, word):
      self.text.configure(state="normal")
      self.text.delete("1.0", "end")
      self.text.insert("1.0", word, "center")
      offset = len(word)/2
      self.text.tag_add("red", "1.0 + %sc" % offset)
      self.text.configure(state="disabled")

if __name__ == "__main__":
   root = tk.Tk()
   Example(root).pack(fill="both", expand=True)
   root.mainloop()
Bryan Oakley
  • 370,779
  • 53
  • 539
  • 685
  • Thanks I've played around with this, seems easier for sure. For this project I needed to use the canvas – zachcapp Mar 31 '15 at 16:27