I need to allow the users to type text into the Canvas Widget, making the canvas update as the user types new text.
Here's what I have tried so far, but am not getting it to work.
First I have a mouseDown method which is bound to Button-1 event
widget.bind(self.canvas, "<Button-1>", self.mouseDown)
This mouseDown method returns the startx, starty positions to my method drawText
def drawText(self, x, y, fg):
self.currentObject = self.canvas.create_text(x,y,fill=fg,text=self.typedtext)
I also have a global binding on the canvas widget to capture any key press like this:
Widget.bind(self.canvas, "<Any KeyPress>", self.currentTypedText)
def currentTypedText(self, event):
self.typedtext = str(event.keysym)
self.drawText(self, self.startx, self.starty,self.foreground)
However there's no error and nothing gets printed on the canvas.
What you want to do is pretty complex and will require quite a bit of code to get working nicely. You will need to handle click events, keypress events, special keypress events (such as "Shift" and "Ctrl"), "Backspace" and delete events, and a lot more.
Nevertheless, first is first and that is getting text to appear in the canvas as a user types. Now, since I don't have your full script, I can't really work with your stuff as is. However, I went and made my own little app that does exactly what you want. Hopefully, it will shine some light on where to go:
from Tkinter import *
class App(Tk):
def __init__(self):
Tk.__init__(self)
# self.x and self.y are the current mouse position
# They are set to None here because nobody has clicked anywhere yet.
self.x = None
self.y = None
self.makeCanvas()
self.bind("<Any KeyPress>", lambda event: self.drawText(event.keysym))
def makeCanvas(self):
self.canvas = Canvas(self)
self.canvas.pack()
self.canvas.bind("<Button-1>", self.mouseDown)
def mouseDown(self, event):
# Set self.x and self.y to the current mouse position
self.x = event.x
self.y = event.y
def drawText(self, newkey):
# The if statement makes sure we have clicked somewhere.
if None not in {self.x, self.y}:
self.canvas.create_text(self.x, self.y, text=newkey)
# I set x to increase by 5 each time (it looked the nicest).
# 4 smashed the letters and 6 left gaps.
self.x += 5
App().mainloop()
Once you click somewhere in the canvas and start typing, you will see text appear. Note however that I have not enabled this to handle deletion of text (that is a little tricky and beyond the scope of your question).
Related
I intend to make a Py code which creates a tkinter dot that turns on a key press and deletes on a key press of couple keys.
The dot already is functional but i need it switch on and off on certain keypresses/mouse clicks which means i need an outside tkinter.mainloop() Update function.
The Update function with a while in it to constantly check if conditions to turn it off/on are present. But the Tkinter widget Somehow gets applied to the screen Only when the function nds. Like widget could be created but it will only take effect when function ends. And i need to turn it off/on dynamically.
I have tried to use a tkinter.after() with additional one at the end of called function only to find out an error of Recursion depth. What i expected to happen was that the function would be called over and over again, instead it runs that function like a while loop. I also have tried Asyncio.run() but it would result not making it visible till the function ends at least once. And I need to change it dynamically.
from tkinter import *
from tkinter import Canvas
from winsound import Beep
from time import sleep
import asyncio
import keyboard
import mouse
root = Tk()
width = root.winfo_screenwidth()
height = root.winfo_screenheight()
class tk_Dot():
def __init__(self,x=-1,y=-1,radius=4,color="red"):
self.x = x
if x == -1:
self.x = width/2-radius//2
print(self.x)
self.y = y
if y == -1:
self.y = height/2+radius//2
print(self.y)
self.radius=radius
self.color = color
self.lines = []
self.count = 1
def line(self,i):
return canvas.create_line(self.x, self.y-i, self.x+self.radius, self.y-i, fill=self.color)
def create(self):
self.lines = []
for i in range(0,self.radius):
self.lines.append(self.line(i))
def delete(self):
for i in range(0,self.radius):
canvas.delete(self.lines[i])
canvas.dtag(self.lines[i])
opacity_of_tk_window = 1 # From 0 to 1 0 meaning completely transparent 1 meaning everything created in canvas will give the color it was given
root.attributes('-alpha',opacity_of_tk_window)
# Invisible Tkinter window label
root.overrideredirect(True)
# Makes Transparent background
transparent_color = '#f0f0f0'
root.wm_attributes('-transparent', transparent_color)
canvas = Canvas()
# Rectangle filled with color that is specified above as the transparent color so practically making transparent background
canvas.create_rectangle(0, 0, width, height, fill=transparent_color)
canvas.pack(fill=BOTH, expand=1)
radius = 2
radius = 1+radius\*2
# Create a dot class
game_dot = tk_Dot(width/2-radius//2+1,height/2+1+radius//2,radius,"Red")
# Create a Dot at the middle of the calorant crosshair
# game_dot.create()
# Delete the dot
# game_dot.delete()
def Update():
game_dot.create()
print("Dot should be visible by now")
print("Is it?")
sleep(5) #sec
print("Oh yeah after the function ends.") # the problem
def Delete():
game_dot.delete()
root.geometry('%dx%d+%d+%d' % (width, height, -2,-2))
# Tkinter window always on top
root.attributes('-topmost',True)
root.after(1000,Update())
root.mainloop()
I'm trying to create a rectangle with text on top, but the text shows below the rectangle.
How can I make the text go to the front layer?
My code so far:
from turtle import Turtle
class Rectangle(Turtle):
def __init__(self, x, y, width, height, color='white'):
super().__init__()
self.penup()
self.goto(x, y)
self.color(color)
self.shape('square')
self.shapesize(stretch_wid=(height / 20), stretch_len=(width / 20))
class Writer(Turtle):
def __init__(self, x, y):
super().__init__()
self.penup()
self.goto(x, y)
self.hideturtle()
def writetext(self, text, font="Arial", size=8, textType="normal", color="black", x=None, y=None):
if x is None:
x = self.xcor()
if y is None:
y = self.ycor()
self.goto(x, y)
self.color(color)
self.write(text, move=False, align="center", font=(font, size, textType))
class Button(Rectangle):
def __init__(self, position, width, height, text, onclick, color="gray"):
position = list(position)
super().__init__(position[0], position[1], width, height, color)
writer = Writer(position[0], position[1])
writer.writetext(text, color="white")
self.goto(position[0], position[1])
self.color(color)
self.onclick(onclick)
def start_game(_arg1=None, _arg2=None): # requires arguments since Python turtle module passes two in when calling it using onclick
print('game started')
Button((0, 0), 50, 20, 'click me to start the game', start_game)
I've been searching on google for over half an hour and couldn't find anything
I believe the design has some fundamental flaws that arise from subclassing Turtle.
The issue is that the Button drawing is happening outside of the constructor where the text is written. The drawing function is called automatically by the turtle library. Regardless of whether you're hooking into Turtle's classes or not, a constructor isn't typically the ideal place to draw things for this reason. I don't see a clean way to make a Button out of two separate turtles with subclassing.
A quick (but poor) fix is to override the internal method that turtle calls to update the object so your button can inject the text after the super() call draws the rectangle (you could also try to hook into drawing with _drawturtle):
class Button(Rectangle):
# ....
def _update(self):
super()._update()
Writer(*self.position()).writetext("foobar", color="white")
But since the leading _ indicates a private, internal method, this is circumventing the turtle API -- not a good idea.
A second try would be to disable the internal update loop with turtle.tracer(0) and take control of the update loop manually, but then that seems to defeat the purpose of subclassing Turtle, which is that you want things to "just work" automatically.
There's a deeper problem here, though, which is that once you get your text on top of the button as shown above in the workaround, the text blocks clicks from being detected by the rectangle underneath it.
After playing around with it quite a bit, the best alternatives I came up with were to create an image with the text pre-rendered in, or else use a global click listener and use the coordinates to determine if the click occurred on the button. The following code isn't reusable, but it's a proof of concept that you could abstract into a function or class by parameterizing everything:
import turtle
from turtle import Turtle
def handle_btn_click(x, y):
if (x > -btn_size and y > -btn_size / 2 and
x < btn_size and y < btn_size / 2):
print("game started")
turtle.tracer(1)
turtle.clearscreen()
turtle.bye()
turtle.tracer(0)
r = Turtle(shape="square")
btn_size = 80
r.shapesize(btn_size // 20, btn_size // 10)
r.color("black")
turtle.update()
t = Turtle()
t.color("white")
t.ht()
text = "click me to start the game"
t.write(text, move=False, align="center", font=("Arial", 8, "normal"))
turtle.Screen().onclick(handle_btn_click)
turtle.mainloop()
As a final note, if you're only adding a single button, I suggest keeping it simple and imperative. A class structure is often overkill for most turtle apps, and the large parameter lists are a pain to deal with for only a single instance.
The text shows below the button seems to happen when you call any methods of the Button class after you wrote the text.
Try the following code:
class Button(Rectangle):
def __init__(self, position, width, height, text, onclick, color="gray"):
#...
writer.writetext(text, color="red")
self.goto(position[0], position[1])
self.color(color)
self.onclick(onclick)
writer.writetext("abc", color="blue")
If you set turtle.tracer with a bit of delay, you can actually see the grey button being created first, then the text in red on the grey button, then the grey button is brought back up by the three self. methods, then the text "abc" in blue written on top of everything.
Still, as mentioned in ggorlen's answer, the text will block the button from being clicked. Please refer to ggorlen's answer for the alternative solution.
I have a base program I was playing with this morning I wrote up a few years back. It only had horizontal scrolling and zooming. I managed to get it so I could scroll(one axis at a time) and zoom on both axis. I'm wanting to create a program that will allow me to move around the screen in both x and y directions at the same time(like Google Earth where you can hold down the down and left key at the same time to move to lower left). It would be nice to do it without having the scroll bars on the screen as well. I don't want to have to go back and forth and click on the appropriate scroll bar to be able to scroll in that axis. Currently to change which axis I'm scrolling in I have to click on the opposite axis.
I tried the program at [http://www.tkdocs.com/tutorial/canvas.html#scrolling][1] but I already have the capability of doing that. It doesn't allow me to scroll both directions simultaneously and if I want to change which direction I'm scrolling without having to click on the opposite axis.
Is there a way of doing what I'm trying to do with Tkinter or should I look elsewhere and if so, where?
Thanks.
edit:
With the code Bryan posted below I added in the following code to try to get it two work with the keyboard versus only the mouse. I would like the be able to use the cursor keys to move the image around versus the mouse. I have a nastily touch sensitive mouse on this computer, that has a mind of its own and as a result I would like to stick with the keyboard. Plus, given the naturedness of this darn project I have to leave all option open or else I know I will regret it sometime before this entire project gets finished.
self.canvas.bind("<Left>", self.on_press)
self.canvas.bind("<Right>", self.on_press)
I also tried directing it to self.on_motion and neither one accepted the cursor keys.
Yes, this is possible. There's nothing preventing you from directly calling the canvas xview and yview methods with any arguments you want.
You first need to create bindings that tracks the clicking and the motion of the mouse. In the bound function you can compute the direction that the mouse moved, then use the results to call both the xview and yview methods of the widget at the same time.
Here's an example:
import tkinter as tk
import random
class Example(tk.Frame):
def __init__(self, parent):
tk.Frame.__init__(self, parent)
self.canvas = tk.Canvas(self, background="bisque", width=400, height=400)
self.canvas.pack(fill="both", expand=True)
self.canvas.configure(scrollregion=(-1000, -1000, 1000, 1000))
self.canvas.bind("<ButtonPress-1>", self.on_press)
self.canvas.bind("<B1-Motion>", self.on_motion)
# the following two values cause the canvas to scroll
# one pixel at a time
self.canvas.configure(xscrollincrement=1, yscrollincrement=1)
# finally, draw something on the canvas so we can watch it move
for i in range(1000):
x = random.randint(-1000, 1000)
y = random.randint(-1000, 1000)
color = random.choice(("red", "orange", "green", "blue", "violet"))
self.canvas.create_oval(x, y, x+20, y+20, fill=color)
def on_press(self, event):
self.last_x = event.x
self.last_y = event.y
def on_motion(self, event):
delta_x = event.x - self.last_x
delta_y = event.y - self.last_y
self.last_x = event.x
self.last_y = event.y
self.canvas.xview_scroll(-delta_x, "units")
self.canvas.yview_scroll(-delta_y, "units")
if __name__ == "__main__":
root = tk.Tk()
Example(root).pack(fill="both", expand=True)
root.mainloop()
I have a class with some mouse events I made :
class graphic_object(object):
def mouse_click(self,event):
#do something
def mouse_move(self,event):
#do something
def mouse_unpressed(self,event):
#do something
Instances of this class aren't literally graphic objects on the screen, but they have their graphic representation, which is circle-shaped, and as I said, they listen to the mouse events. Both, graphic representation and event handling are managed by tkinter.Canvas object, which is their visual container.
When I make one istance of this class:
graphic1 = graphic_object(a,b,c,d) # init method takes coordinates of the circle as arguments; a,b,c,d - numbers
Everything works as it should, object responds on the mouse events in desired way. But when I make two instances:
graphic1 = graphic_object(a,b,c,d)
graphic2 = graphic_object(e,f,g,h)
only the last created object responds on the mouse events.
This is the condition where I check if the mouse is over the circle:
if d < self.radius:
where d is distance between mouse position, and the center of the circle, and radius is radius of the circle.
In the debugger I see that self.center is always the center of the last created object, so condition is always on
the second circle. So, how can I make that both objects respond to the mouse events?
Events handling:
C = Canvas()
C.bind("<Button-1>" ,self.mouse_click)
C.bind("<B1-Motion>",self.mouse_move)
C.bind("<ButtonRelease-1>",self.mouse_unpressed)
It appears that in your mouse binding you are relying on a pre-computed global variable (d). This is not how you should implement such bindings. The first thing you should do in the binding is get the current mouse coordinates, and then calculate d.
Your other choice is to put the binding on each canvas object using the tag_bind method of the canvas. See this question for an example: How do I attach event bindings to items on a canvas using Tkinter?
You wrote in a comment to this answer that you are only sometimes getting mouse clicks. There is not enough detail in your code to know what you're doing, but I can assure you that the canvas doesn't normally fail in such a manner.
I can't debug your code since you are only showing bits and pieces, but here's a working example that tries to illustrate the use of tag_bind. I took some liberties with your code. For example, I added a name parameter so I can print out which circle you clicked on. When I test this, every click seems to register on the proper circle.
import Tkinter as tk
class Example(tk.Frame):
def __init__(self, parent):
tk.Frame.__init__(self, parent)
self.canvas = tk.Canvas(self, width=400, height=400,
background="bisque")
self.canvas.pack(fill="both", expand=True)
graphic1 = GraphicObject(10,10,100,100, name="graphic1")
graphic2 = GraphicObject(110,110,200,200, name="graphic2")
graphic1.draw(self.canvas)
graphic2.draw(self.canvas)
class GraphicObject(object):
def __init__(self, x0,y0,x1,y1, name=None):
self.coords = (x0,y0,x1,y1)
self.name = name
def draw(self, canvas, outline="black", fill="white"):
item = canvas.create_oval(self.coords, outline=outline, fill=fill)
canvas.tag_bind(item, "<1>", self.mouse_click)
def mouse_click(self, event):
print "I got a mouse click (%s)" % self.name
if __name__ == "__main__":
root = tk.Tk()
Example(root).pack(fill="both", expand=True)
root.mainloop()
I have 3 panels and I want to make drags on them.
The problem is that when I do a drag on one this happens:
http://img41.yfrog.com/img41/9043/soundlog.png http://img41.yfrog.com/img41/9043/soundlog.png
How can I refresh the frame to happear its color when the panel is no longer there?
This is the code that I have to make the drag:
def onMouseMove(self, event):
(self.pointWidth, self.pointHeight) = event.GetPosition()
(self.width, self.height) = self.GetSizeTuple()
if (self.pointWidth>100 and self.pointWidth<(self.width-100) and self.pointHeight < 15) or self.parent.dragging:
self.SetCursor(wx.StockCursor(wx.CURSOR_SIZING))
"""implement dragging"""
if not event.Dragging():
self.w = 0
self.h = 0
return
self.CaptureMouse()
if self.w == 0 and self.h == 0:
(self.w, self.h) = event.GetPosition()
else:
(posw, posh) = event.GetPosition()
displacement = self.h - posh
self.SetPosition( self.GetPosition() - (0, displacement))
else:
self.SetCursor(wx.StockCursor(wx.CURSOR_ARROW))
def onDraggingDown(self, event):
if self.pointWidth>100 and self.pointWidth<(self.width-100) and self.pointHeight < 15:
self.parent.dragging = 1
self.SetCursor(wx.StockCursor(wx.CURSOR_ARROW))
self.SetBackgroundColour('BLUE')
self.parent.SetTransparent(220)
self.Refresh()
def onDraggingUp(self, event):
self.parent.dragging = 0
self.parent.SetTransparent(255)
self.SetCursor(wx.StockCursor(wx.CURSOR_ARROW))
and this are the binds for this events:
self.Bind(wx.EVT_MOTION, self.onMouseMove)
self.Bind(wx.EVT_LEFT_DOWN, self.onDraggingDown)
self.Bind(wx.EVT_LEFT_UP, self.onDraggingUp)
With this, if I click on the top of the panel, and move down or up, the panel position changes (I drag the panel) to the position of the mouse.
To refresh the parent on every repositioning of self, you could add
self.parent.Refresh()
right after your existing call to self.SetPosition in your def onMouseMove method. Right now you're refreshing the frame only in the def onDraggingDown method, i.e., the first time the mouse left button is clicked and held down, not every time the mouse is moved while said button is held down (i.e., the "dragging" action itself).
I was unable to download your code for testing purposes, due to the rather "spammy peculiar" site you chose to upload it to -- the site keeps bombarding me with ads, no clear way for me to just do the download, occasionally complaining that it doesn't support my machine (I use a Mac and Google Chrome, the site at some spots insists on Windows with IE or Firefox...), etc etc. I'm sure you can find other sites, more usable than that one, for people who are trying to help you out!-)