Python tkinter: how to restrict mouse cursor within canvas? - python

I use tkinter's canvas to load an image and draw a vector on top of it (using create_line).
I would like to restrict the mouse movement when drawing this vector, so that it cannot be dragged outside of the image area, whatever it may be. The mouse cursor should just snap back to image boundaries.
I tried searching, and found various ways of dealing with this, ideally this would need to be cross-platform. So far, I couldn't make any of those various ways working... so I'm kindly asking for help! Thank you :)

OK in the end I decided not to restrict mouse cursor physically (by forcing it not to go beyond certain coordinates), but rather virtually (by storing the mouse position to a variable, then if-elseing it around the bounding box that it needed to stay in). So the mouse cursor goes wherever it wants, but when it's actually drawing something in - it stays within the designated area I want it to.
Drawing lines on a Canvas was the task, over the loaded image. Line shouldn't pass by the boundaries of the image. This is how it worked out:
imgsize = (int(self.viewport.cget('width')) - 1,int(self.viewport.cget('height')) - 1)
# limit the draggable mouse area to just the image dimensions
if event.x < 4:
currentx = 4
elif event.x > imgsize[0]:
currentx = imgsize[0]
else:
currentx = event.x
if event.y < 4:
currenty = 4
elif event.y > imgsize[1]:
currenty = imgsize[1]
else:
currenty = event.y
Then from that point onward it's create_line time.

Related

Why can't I draw onto two different bitmaps in wxPython?

I am trying to draw a button on a bitmap object. Depending on the y position, it should draw on bitmap1 if the y position is within bmp1's height value, and bitmap2 if it isn't.
For some reason this does not work:
wx.Button(bitmap1 if ypos <= bmp1.GetHeight() else bitmap2, label='Run', id=i, pos=(xpos, ypos))
I can only draw the button on one wx.StaticBitmap image or the panel. The images parents are the panel.
This works fine if I want to switch between the bitmap or onto the panel directly.
What gives?
NOTE:
I managed to work around this using PIL to create a dynamic image large enough to accomodate my generated buttons (a continuous y-size, according to their count and placement), however this idea/code should still be valid.
If I substitute the 'bitmap2' value for the panel, and shift the bitmap2 image drawn on the panel by a bit, then I see that the program draws underneath bitmap2. Why? The image is placed exactly like bitmap1, and bitmap1 has no problems being drawn on it by buttons? :O
I figured out the problem:
The button's parent object should get the ypos according to the parent's dimensions, not on where it is drawn on the frame, like so:
wx.Button(bitmap1 if ypos <= bmp1.GetHeight() else bitmap2, label='Run {i}', id=i, pos=(80, ypos if ypos <= bmp1.GetHeight() else ypos-img_height))
ypos if ypos <= bmp1.GetHeight() else ypos-img_height
Finally!

With Pygame, is there a faster way to draw hundreds of small squares?

I'm attempting to make a tile based game, not done anything like this before so I'm learning as I go along. However, I've got a big problem with speed, and I was wondering if anyone had any solutions/advice. I tried to separate recalculating bits and the actual drawing, though as you can only move the camera currently, it's got to do both at once, and it's very noticeable how slow it runs if you have a small tilesize and large resolution.
I thought an idea would be to split it into chunks, so you calculate an x*x area, and instead of checking each tile if it's within the screen bounds, you only check the group of tiles, then somehow cache it the first time it's drawn so you then end up drawing a single image from memory. However I didn't find anything on that when googling it.
As to the drawing part, it runs to the effect of:
for tile in tile_dict:
pygame.draw.rect(precalculated stuff)
With the same tilesize as the image below, at 720p it runs at 100fps, and at 1080p it runs at 75fps. This is with literally nothing but drawing squares. Each block is a slightly different colour, so I can't just draw a bigger square. I know not to redraw every frame by the way.
As to the recalculation part, it's a bit longer but still quite easy to understand. I calculate which coordinates would be at the edge of the screen, and use that to build a list of all on screen tiles. I then delete any tiles that are outside of this area, move the cooordinate to the new location if the tile has moved on screen, and calculate any tiles that have just appeared. This runs at about 90fps at 720p, or 45fps at 1080p, which is really not good.
def recalculate(self):
overflow = 2
x_min = self.cam.x_int + 1 - overflow
y_min = self.cam.y_int + 1 - overflow
x_max = self.cam.x_int + int(self.WIDTH / self.tilesize) + overflow
y_max = self.cam.y_int + int(self.HEIGHT / self.tilesize) + overflow
self.screen_coordinates = [(x, y)
for x in range(x_min, x_max)
for y in range(y_min, y_max)]
#Delete the keys that have gone off screen
del_keys = []
for key in self.screen_block_data:
if not x_min < key[0] < x_max or not y_min < key[1] < y_max:
del_keys.append(key)
for key in del_keys:
del self.screen_block_data[key]
#Rebuild the new list of blocks
block_data_copy = self.screen_block_data.copy()
for coordinate in self.screen_coordinates:
tile_origin = ((coordinate[0] - self.cam.x_int) - self.cam.x_float,
(coordinate[1] - self.cam.y_int) - self.cam.y_float)
tile_location = tuple(i * self.tilesize for i in tile_origin)
#Update existing point with new location
if coordinate in self.screen_block_data:
self.screen_block_data[coordinate][2] = tile_location
continue
block_type = get_tile(coordinate)
#Generate new point info
block_hash = quick_hash(*coordinate, offset=self.noise_level)
#Get colour
if coordinate in self.game_data.BLOCK_TAG:
main_colour = CYAN #in the future, mix this with the main colour
else:
main_colour = TILECOLOURS[block_type]
block_colour = [min(255, max(0, c + block_hash)) for c in main_colour]
self.screen_block_data[coordinate] = [block_type,
block_colour,
tile_location]
I realised in what I wrote above, I probably could cache the info for a 10x10 area or something to cut down on what needs to be done when moving the camera, but that still doesn't get around the problem with drawing.
I can upload the full code if anyone wants to try stuff with it (it's split over a few files so probably easier to not paste everything here), but here's a screenshot of how it looks currently for a bit of reference:
To increase the speed of drawing the small squares, you can draw them onto non-screen surface (any pygame surface that will be big enough to hold all the squares) and then blit this surface on the screen with correct coordinates.
This way you won't need to check if any squares are outside the screen and it will be only necessary to provide inverted camera (viewpoint) coordinates (If camera position is [50,20] then you should blit the surface with tiles onto [-50,-20]).

Pygame: Problems with pointing image to mouse

From my understanding, this:
angle_to_pointer = degrees(atan2((py+32)-mouse[0], px-mouse[1]))+90
is a good way to get the angle between points..
I have this image:
and I'm trying to make it point to the mouse with this script:
import pygame
from pygame.locals import *
from math import degrees,atan2
pygame.init()
screen=pygame.display.set_mode((640,480))
arrow=pygame.image.load('arrow.png')
px=30
py=30
while True:
screen.fill((0,0,255))
mouse=pygame.mouse.get_pos()
angle_to_pointer = degrees(atan2((py+32)-mouse[0], px-mouse[1]))+90
for e in pygame.event.get():
if e.type==QUIT:
exit()
spr=pygame.transform.rotate(arrow,angle_to_pointer)
screen.blit(spr,(px,py))
pygame.display.flip()
It appears to work at first, but upon closer inspection, it appears to be pointing a little bit away from the mouse.
I tried fiddling with the values, but the result never came out the way I wanted it to, the code I posted contains the best combination I could create.
Could someone tell me what I am doing incorrectly?
This is getting too much for a comment. In your angle_to_pointer calculation you are offsetting your mouse in the Y coordinate by 32, which puts you at the bottom left of your unrotated image. you probably ment to add the 32 to the X coordinate which would put you on the center for X but still off on the Y. Also I think your mouse coordinates are backwards.
Even if you added 16 to the Y and 32 to the X this is still all based on the unrotated image. Once you rotate the image your size will change. The easiest way I can think of to do what you are wanting is to not draw your image off of the top left, but use the center. Find the point you want to be the center and base your angle_to_pointer off that. Then when you blit use the new rotated image size to find the top left.
for example:
your image is 64x32 so for fun, lets use the point (37,37) as our center (to keep it from going over the edge of the screen)
px=37 # center of arrow
py=37 # center of arrow
while True:
screen.fill((0,0,255))
mouseX, mouseY=pygame.mouse.get_pos() # unpack to avoid confustion
angle_to_pointer = degrees(atan2(mouseY - py, mouseX - px)) # calculate off center of image
for e in pygame.event.get():
if e.type == QUIT:
exit()
spr=pygame.transform.rotate(arrow, -angle_to_pointer) # clockwise rotation
# adjust draw top left based on center and rotated image size
blit_pos = (px - spr.get_width()//2, py - spr.get_height//2)
screen.blit(spr, blit_pos)
pygame.display.flip()
**disclaimer, haven't tried this since my work computer doesn't have pygame,

How can I make my circles fly off the screen in pygame?

I am a begginner at python and I'm trying to make a circle game. So far it draws a circle at your mouse with a random color and radius when you click.
Next, I would like the circle to fly off the screen in a random direction. How would I go about doing this? This is the main chunk of my code so far:
check1 = None
check2 = None
while True:
for event in pygame.event.get():
if event.type == QUIT:
pygame.quit()
sys.exit
if event.type == MOUSEBUTTONDOWN:
last_mouse_pos = pygame.mouse.get_pos()
if last_mouse_pos:
global check
color1 = random.randint(0,255)
color2 = random.randint(0,255)
color3 = random.randint(0,255)
color = (color1,color2,color3)
radius = random.randint (5,40)
posx,posy = last_mouse_pos
if posx != check1 and posy != check2:
global check1, check2
screen.lock()
pygame.draw.circle(screen, color, (posx,posy), radius)
screen.unlock()
check1,check2 = posx,posy
pygame.display.update()
Again, I want the circle to fly off the screen in a random direction.
I have made a few attempts but no successes yet.
Also, thanks to jdi who helped me s
When you create the circle (on click), generate 2 random numbers. These will be your (x,y) components for a two dimensional Euclidean velocity vector:
# interval -1.0 to 1.0, adjust as necessary
vx, vy = ( (random.random()*2) -1, (random.random()*2) - 1 )
Then after the ball has been created, on each iteration of the game loop, increment your ball's position by the velocity vector:
posx, posy = posx + vx, posy + vy
Note that the overall speed might be variable. If you want to always have a speed of 1.0 per seconds, normalize the vector:
To normalize the vector, you divide it by its magnitude:
So in your case:
And hence:
So in Python, after importing math with import math:
mag = math.sqrt(vx*vx + vy*vy)
v_norm = vx/mag, vy/mag
# use v_norm instead of your (vx, vy) tuple
Then you can multiply this with some speed variable, to get reliable velocity.
Once you progress to having multiple objects moving around and potentially off screen, it is useful to remove the offscreen objects which have no chance of coming back, and have nothing to do with your program anymore. Otherwise, if you keep tracking all those offscreen objects while creating more, you get essentially a memory leak, and will run out of memory given enough time/actions.
While what you are doing right now is quite simple, assuming you haven't already, learning some basic vector math will pay itself off very soon. Eventually you may need to foray into some matrix math to do certain transformations. If you are new to it, its not as hard as it first looks. You can probably get away with not studying it, but you will save yourself effort later if you start attempting to do more ambitious things.
Right now, you are doing the following (drastically simplifying your code)...
while True:
if the mouse was clicked:
draw a circle on the screen where the mouse was clicked
Let's make things a little easier, and build up gradually.
Start with the circle without the user clicking
To keep things simple, let's make the circle near the top left of the screen, that way we can always assume there will be a circle (making some of the logic easier)
circle_x, circle_y = 10,10
while True:
draw the circle at circle_x, circle_y
pygame.display.update()
Animate the circle
Before going into too much about "random directions", let's just make it easy and go in one direction (let's say, always down and to the right).
circle_x, circle_y = 0,0
while True:
# Update
circle_x += 0.1
circle_y += 0.1
# Draw
draw the circle at circle_x, circle_y
update the display
Now, every time through the loop, the center of the circle moves a bit, and then you draw it in its new position. Note that you might need to reduce the values that you add to circle_x and y (in my code, 0.1) in case the circle moves too fast.
However, you'll notice that your screen is now filling up with circles! Rather than one circle that is "moving", you're just drawing the circle many times! To fix this, we're going to "clear" the screen before each draw...
screen = ....
BLACK = (0,0,0) # Defines the "black" color
circle_x, circle_y = 0,0
while True:
# Update
circle_x += 0.1
circle_y += 0.1
# Draw
screen.fill(BLACK)
draw the circle at circle_x, circle_y
update the display
Notice that we are "clearing" the screen by painting the entire thing black right before we draw our circle.
Now, you can start work the rest of what you want back into your code.
Keep track of multiple circles
You can do this by using a list of circles, rather than two circle variables
circles = [...list of circle positions...]
while True:
# Update
for circle in circles:
... Update the circle position...
# Draw
screen.fill(BLACK)
for circle in circles:
draw the circle at circle position # This will occur once for each circle
update the display
One thing to note is that if you keep track of the circle positions in a tuple, you won't be able to change their values. If you're familiar with Object Oriented Programming, you could create a Circle class, and use that to keep track of the data relating to your circles. Otherwise, you can every loop create a list of updated coordinates for each circle.
Add circle when the user clicks
circles = []
while True:
# event handling
for event in pygame.event.get():
if event.type == MOUSEBUTTONDOWN:
pos = pygame.mouse.get_pos()
circles.append( pos ) # Add a new circle to the list
# Update all the circles
# ....
# Draw
clear the screen
for circle_position in circles:
draw the circle at circle_position # This will occur once for each circle
update the display
Have the circle move in a random direction
This is where a good helping of math comes into play. Basically, you'll need a way to determine what to update the x and y coordinate of the circle by each loop. Keep in mind it's completely possible to just say that you want it to move somewhere between -1 and 1 for each axis (X, y), but that isn't necessarily right. It's possible that you get both X and Y to be zero, in which case the circle won't move at all! The next Circle could be 1 and 1, which will go faster than the other circles.
I'm not sure what your math background is, so you might have a bit of learning to do in order to understand some math behind how to store a "direction" (sometimes referred to as a "vector") in a program. You can try Preet's answer to see if that helps. True understanding is easier with a background in geometry and trigonometry (although you might be able to get by without it if you find a good resource).
Some other thoughts
Some other things you'll want to keep in mind:
Right now, the code that we're playing with "frame rate dependent". That is, the speed in which the circles move across the screen is entirely dependent on how fast the computer can run; a slower computer will see the circles move like snails, while a faster computer will barely see the circles before they fly off the screen! There are ways of fixing this, which you can look up on your own (do a search for "frame rate dependence" or other terms in your favorite search engine).
Right now, you have screen.lock() and screen.unlock(). You don't need these. You only need to lock/unlock the screen's surface if the surface requires it (some surfaces do not) and if you are going to manually access the pixel data. Doing things like drawing circles to the screen, pygame in lock/unlock the surfaces for you automatically. In short, you don't need to deal with lock/unlock right now.

How to connect two state circles with an arrow in tkinter?

I am currently writing a fsm editor with tkinter. But, I stuck on connecting two states. I have two questions:
1) How can make the transition arrow growable according to mouse movement?
2) How can I stick the starting point of the arrow on a state and the end point of the arrow on another state?
PS. Do you think the documentation of tkinter is good enough?
Here's an example that shows the concept. In a nutshell, use tags to associate lines with boxes, and simply adjust the coordinates appropriately when the user moves the mouse.
Run the example, then click and drag from within the beige box.
Of course, for production code you need to make a more general solution, but hopefully this shows you how easy it is to create a box with arrows that adjust as you move the box around.
from Tkinter import *
class CanvasDemo(Frame):
def __init__(self, width=200, height=200):
Frame.__init__(self, root)
self.canvas = Canvas(self)
self.canvas.pack(fill="both", expand="1")
self.canvas.create_rectangle(50, 25, 150, 75, fill="bisque", tags="r1")
self.canvas.create_line(0,0, 50, 25, arrow="last", tags="to_r1")
self.canvas.bind("<B1-Motion>", self.move_box)
self.canvas.bind("<ButtonPress-1>", self.start_move)
def move_box(self, event):
deltax = event.x - self.x
deltay = event.y - self.y
self.canvas.move("r1", deltax, deltay)
coords = self.canvas.coords("to_r1")
coords[2] += deltax
coords[3] += deltay
self.canvas.coords("to_r1", *coords)
self.x = event.x
self.y = event.y
def start_move(self, event):
self.x = event.x
self.y = event.y
root = Tk()
canvas = CanvasDemo(root)
canvas.pack()
mainloop()
Tkinter is perfectly fine for this sort of application. In the past I've worked on tools that were boxes connected with arrows that stayed connected as you move the boxes around (which is what I think you are asking about). Don't let people who don't know much about Tkinter sway you -- it's a perfectly fine toolkit and the canvas is very flexible.
The solution to your problem is simple math. You merely need to compute the coordinates of the edges or corners of boxes to know where to anchor your arrows. To make it "grow" as you say, simply make a binding on mouse movements and update the coordinates appropriately.
To make the line growable all you have to do is adjust the coordinates of the line each time the mouse moves. The easiest thing to do is make liberal use of canvas tags. With the tags you can know which arrows connect to which boxes so that when you move the box you adjust the coordinates of any arrows that point to or from it.

Categories

Resources