How to stop other processes or finish processes in Python / Tkinter program - python

I have been drawing some graphics with a Python / Tkinter program. The program has a main menu with menu items for drawing different figures. It works quite well but I came up against a problem. If the program is part way through drawing one figure and the user clicks to draw a second figure then the program draws the second figure, but when it has finished drawing the second figure it goes back and finishes drawing the first figure. What I want it to do is stop drawing the first figure and not go back to drawing the first figure even when the second figure has finished drawing. I created an simpler example program to demonstrate the scenario. To see the problem in this program click "Draw -> Red" and then click "Draw -> Blue" before the red has finished drawing. How do I get the program to abort any previous drawing? Here is the example program:
from tkinter import *
import random
import math
def line(canvas, w, h, p, i):
x0 = random.randrange(0, w)
y0 = random.randrange(0, h)
x1 = random.randrange(0, w)
y1 = random.randrange(0, h)
canvas.create_line(x0, y0, x1, y1, fill=p.col(i))
class Color:
def __init__(self, r, g, b):
self.red = r
self.gre = g
self.blu = b
def hexVal(self, v):
return (hex(v)[2:]).zfill(2)
def str(self):
return "#" + self.hexVal(self.red) + self.hexVal(self.gre) + self.hexVal(self.blu)
class Palette:
def __init__(self, n0, y):
self.colors = []
self.n = n0
self.m = 0
if y == "red":
self.red()
elif y == "blue":
self.blue()
def add(self, c):
self.colors.append(c)
self.m += 1
def red(self):
self.add(Color(127, 0, 0))
self.add(Color(255, 127, 0))
def blue(self):
self.add(Color(0, 0, 127))
self.add(Color(0, 127, 255))
def col(self, i):
k = i % (self.n*self.m)
z = k // self.n
j = k % self.n
c0 = self.colors[z]
c1 = self.colors[(z + 1) % self.m]
t0 = (self.n - j)/self.n
t1 = j/self.n
r = int(math.floor(c0.red*t0 + c1.red*t1))
g = int(math.floor(c0.gre*t0 + c1.gre*t1))
b = int(math.floor(c0.blu*t0 + c1.blu*t1))
c = Color(r, g, b)
return c.str()
def upd(canvas):
try:
canvas.update()
return True
except TclError:
return False
def tryLine(canvas, w, h, p, i, d):
try:
line(canvas, w, h, p, i)
if i % d == 0:
upd(canvas)
return True
except TclError:
return False
class MenuFrame(Frame):
def __init__(self, parent):
Frame.__init__(self, parent)
self.parent = parent
self.initUI()
def initUI(self):
self.WIDTH = 800
self.HEIGHT = 800
self.canvas = Canvas(self.parent, width=self.WIDTH, height=self.HEIGHT)
self.pack(side=BOTTOM)
self.canvas.pack(side=TOP, fill=BOTH, expand=1)
self.parent.title("Line Test")
menubar = Menu(self.parent)
self.parent.config(menu=menubar)
self.parent.protocol('WM_DELETE_WINDOW', self.onExit)
menu = Menu(menubar)
menu.add_command(label="Red", command=self.onRed)
menu.add_command(label="Blue", command=self.onBlue)
menu.add_command(label="Exit", command=self.onExit)
menubar.add_cascade(label="Draw", menu=menu)
self.pRed = Palette(256, "red")
self.pBlue = Palette(256, "blue")
def onRed(self):
# How to abort here any processes currently running?
self.canvas.delete("all")
for i in range(0, 7000):
tryLine(self.canvas, self.WIDTH, self.HEIGHT, self.pRed, i, 100)
upd(self.canvas)
def onBlue(self):
# How to abort here any processes currently running?
self.canvas.delete("all")
for i in range(0, 7000):
tryLine(self.canvas, self.WIDTH, self.HEIGHT, self.pBlue, i, 100)
upd(self.canvas)
def onExit(self):
self.canvas.delete("all")
self.parent.destroy()
def main():
root = Tk()
frame = MenuFrame(root)
root.mainloop()
if __name__ == '__main__':
main()

That's the simple example? ;)
You can add a variable that tracks the currently selected method, then check if that variable exists before completing the for loop. Here's an even simpler example:
class Example(Frame):
def __init__(self, parent):
Frame.__init__(self, parent)
Button(self, text='Task A', command=self._a).pack()
Button(self, text='Task B', command=self._b).pack()
self.current_task = None # this var will hold the current task (red, blue, etc)
def _a(self):
self.current_task = 'a' # set the current task
for i in range(1000):
if self.current_task == 'a': # only continue this loop if its the current task
print('a')
self.update()
def _b(self):
self.current_task = 'b'
for i in range(1000):
if self.current_task == 'b':
print('b')
self.update()
root = Tk()
Example(root).pack()
root.mainloop()
Let me know if that doesn't make sense or doesn't work out for you.

Related

Unable to detect mouse clicks for randomly generated buttons

I created a program that allows the user to randomly generate buttons on a grid, but I cannot detect if the user presses them or not. Here is what I have so far:
from graphics import *
from time import *
from random import *
class Button:
def __init__(self, win, center, width, height, label):
w,h = width/2.0, height/2.0
x,y = center.getX(), center.getY()
self.xmax, self.xmin = x+w, x-w
self.ymax, self.ymin = y+h, y-h
p1 = Point(self.xmin, self.ymin)
p2 = Point(self.xmax, self.ymax)
self.rect = Rectangle(p1,p2)
self.rect.setFill("blue")
self.rect.draw(win)
self.label = Text(center, label)
self.label.draw(win)
self.label.setSize(8)
self.activate()
def clicked(self, p):
#print("clicked", p.getX(), p.getY(), self.xmin, self.xmax)
return (self.active and
self.xmin <= p.getX() <= self.xmax and
self.ymin <= p.getY() <= self.ymax)
def getLabel(self):
return self.label.getText()
def activate(self):
self.label.setFill("black")
self.rect.setWidth(2)
self.active = True
def deactivate(self):
self.label.setFill("darkgray")
self.rect.setWidth(1)
self.active = False
def setColor(self, color):
self.rect.setFill(color)
class Grid:
def __init__(self, win, startX, startY, numCols, numRows, squareWidth, squareHeight):
self.ButtonMatrix = []
self.numCols = numCols
self.numRows = numRows
for y in range(startY, numRows):
buttonList = []
for x in range(startX,numCols):
label = str(x) + str(y)
buttonList.append(Button(win,Point(x,y), squareWidth, squareHeight, label))
self.ButtonMatrix.append(buttonList)
sleep(0.03)
def getClickPos(self, clickPt):
for y in range(self.numRows):
for x in range(self.numCols):
if self.ButtonMatrix[y][x].clicked(clickPt):
return y,x
def GenerateRandomColor(self, X,Y, color):
self.ButtonMatrix[X][Y].setColor(color)
#def insideBox(x,y):
def setSquareColor(self,r,c,color):
self.ButtonMatrix[r][c].setColor(color)
def setRowColor(self,rowNum,color):
for c in range(15):
self.ButtonMatrix[rowNum][c].setColor(color)
def main():
SIZE = 15
#application window
win = GraphWin("Memory Game", 600, 600)
win.setBackground(color_rgb(45,59,57))
win.setCoords(-3, -3, SIZE + 2, SIZE + 2)
grid = Grid(win, 0, 1, SIZE, SIZE, 1, 1)
quitButton = Button(win, Point(SIZE, SIZE+1), 2, 1, "Quit")
for i in range(10):
X = randrange(13)
Y = randrange(13)
grid.GenerateRandomColor(X,Y, "white")
Coords = X,Y
sleep(0.1)
print(Coords) #checking to see if each button coord will be printed out
pt = win.getMouse()
if grid.getClickPos(pt) == Coords:
print("pressed random button")
else:
print("did not press a random button")
if __name__ == "__main__":
main()

How to pass pixbuf from a class in a module,after user interaction, in Python

Im working on a python module to handle screenshots. The module itself works fine if alone. however when i import it into my main project i cant figure out how to return a pixbuf after waiting for the user to select a region to capture.
here is the screenshot module:
class CaptureRegion():
def __init__(self):
self.ThePic = None
self.drawArea = Gtk.DrawingArea()
self.drawArea.connect('draw',self.onDraw)
mBox = Gtk.VBox(False,2)
mBox.pack_start(self.drawArea,True,True,2)
self.MaskWin = Gtk.Window()
self.MaskWin.set_position(Gtk.WindowPosition.CENTER)
self.MaskWin.connect("key-press-event", self.KeyPress)
self.MaskWin.connect("button-press-event", self.ButtonPress)
self.MaskWin.connect("button-release-event", self.ButtonRelease)
self.MaskWin.connect("motion-notify-event", self.Motion)
self.MaskWin.add(mBox)
self.button_x = 0
self.button_y = 0
self.button_down = False
self.sel_rect = Gdk.Rectangle(-10, -10, 10, 10)
self.capfull = CaptureScreen()
self.fullpixbuf = self.capfull.Go()
def onDraw(self, widget, event):
print("Draw")
myGdkWindow = self.MaskWin.get_window()
self.cr = Gdk.cairo_create(myGdkWindow)
self.cr.set_operator(cairo.OPERATOR_SOURCE)
self.cr.set_source_rgb(1,1,1)
Gdk.cairo_set_source_pixbuf(self.cr, self.fullpixbuf, 5, 5)
self.cr.paint()
self.cr.set_line_width(1)
self.cr.set_source_rgb(0,0,0)
if self.button_down == True:
x = self.sel_rect.x
y = self.sel_rect.y
w = self.sel_rect.width
h = self.sel_rect.height
if w <= 0 or h <= 0:
return True
self.cr.rectangle(x,y,w,h)
self.cr.set_source_rgba(0.2, 0.6, 0.8, 0.35)
self.cr.set_line_width(2)
self.cr.stroke()
return True
def Motion(self,widget,event):
if self.button_down == False:
return False
x1,y1 = self.button_x, self.button_y
x2,y2 = event.x, event.y
x = int(min(x1,x2))
y = int(min(y1,y2))
w = int(abs(x1-x2))
h = int(abs(y1-y2))
old = self.sel_rect
self.sel_rect.x = x
self.sel_rect.y = y
self.sel_rect.width = w
self.sel_rect.height = h
win = self.MaskWin.get_window()
sx, sy, sw, sh = self.MaskWin.get_window().get_geometry()
self.drawArea.queue_draw_area(sx,sy,sw,sh)
def ButtonPress(self,widget,event):
if not event.button == 1:
return False
print("button down")
self.debounce = 0
self.button_x, self.button_y = event.x, event.y
self.button_down = True
return True
def ButtonRelease(self,widget,event):
self.button_down = False
x = self.sel_rect.x
y = self.sel_rect.y
w = self.sel_rect.width
h = self.sel_rect.height
self.drawArea.queue_draw_area(x-1, y-1, w-1, h-1)
if self.debounce == 0:
self.debounce += 1
print("Snipping x:%s y:%s w:%s h:%s" %(x,y,w,h))
self.ThePic = Gdk.pixbuf_get_from_window(self.MaskWin.get_window(), x, y, w, h)
self.MaskWin.hide()
#option 2: to return ThePic here after the selection is made thru a callback to a function in the main class
# i.e.
#self.CallbackInMain(self.ThePic) (not sure how to callback to the main class, self is wrong maybe?)
def KeyPress(self,widget,event):
if event.keyval == Gdk.KEY_Escape:
self.ThePic = None
return
def WaitThread(self):
for i in range(1, 50):
time.sleep(0.1)
print(i)
self.done_waiting.set()
def Go(self):
self.MaskWin.fullscreen()
self.MaskWin.show_all()
self.MaskWin.set_decorated(False)
#option 1 find a way to wait for the selection to complete like this:
#(this code doesnt end, the self.done_waiting.set() call doesnt stop it)
# self.done_waiting = threading.Event()
# self.thread = threading.Thread(target=self.WaitThread)
# self.thread.start()
# self.done_waiting.wait(1)
# print("Main thread is done waiting")
return(self.ThePic)
class CaptureScreen():
def __init__(self):
self.ThePic = None
def Go(self):
screen = Gdk.get_default_root_window()
x, y, width, height = screen.get_geometry()
self.fullpixbuf = Gdk.pixbuf_get_from_window(screen, x, y, width, height)
return(self.fullpixbuf)
im calling the class like this:
import Screentshot as Shot
self.captureregion = Shot.CaptureRegion()
pic = self.captureregion.Go()
pic.savev("region.png",'png',(),())
with the CaptureScreen class im just capturing the whole screen so i can return the pixbuf directly from the CaptureScreen.Go() function. however with the CaptureRegion class i have to wait for the user to select an area, and im not sure how to have CaptureRegion.Go() return the pixbuf after the users selection. I have two ideas, one use threading to wait for the selection to complete and then return the pixbuf from Capture Region.Go() (option 1 in code) , or second somehow have the screenshot class callback to function in main with the pixbuf as an arg(option 2 in code)
Im not very familiar with classes nor have i ever used the threading module. ive searched and found lots of info, i just cant quite wrap my head around what i need to do.
Of course now that i finally post a question about this, i figured it out. i just needed to add a second arg to Go(self,mainself) and call back to a function in the main code.
this is what i changed:
def ButtonRelease(self,widget,event):
self.button_down = False
x = self.sel_rect.x
y = self.sel_rect.y
w = self.sel_rect.width
h = self.sel_rect.height
self.drawArea.queue_draw_area(x-1, y-1, w-1, h-1)
if self.debounce == 0:
self.debounce += 1
print("Snipping x:%s y:%s w:%s h:%s" %(x,y,w,h))
self.ThePic = Gdk.pixbuf_get_from_window(self.MaskWin.get_window(), x, y, w, h)
self.MaskWin.hide()
self.GotTheShot = True
self.mainself.ScreenShotCallBack(self.ThePic)
def Go(self,main):
self.mainself = main
self.GotTheShot = False
self.MaskWin.fullscreen()
self.MaskWin.show_all()
self.MaskWin.set_decorated(False)
and here is the call and callback:
def ScreenShotRegion(self):
pic = self.captureregion.Go(self)# self,mainself... self passed by default
pic.savev("region.png",'png',(),())
def ScreenShotCallBack(self,capturepixbuf):
self.window.show_all()
capturepixbuf.savev("region.png",'png',(),())

Grid Generation for minesweeper

Hi so I am making a minesweeper game and I am a bit stuck with the grid generation part. This is my code so far:
from random import randint
import pygame
def MineGen():
mineamount = 100
grid_across = 40
grid_up = 25
mine_list = []
my2dthatlist = []
numacoss = 0
for i in range (mineamount):
numacoss = randint(1,40)
my2dthatlist.append(numacoss)
numup = randint(1,25)
my2dthatlist.append(numup)
mine_list.append(my2dthatlist)
my2dthatlist = []
return mine_list
def GridGen():
grid_across = 40
grid_up = 25
GRIDD = [[0]* grid_across for i in range(grid_up)]
return GRIDD
def MineGrid(GridOutMine, mine_list):
mineplace = 0
placeX = 0
placeY = 0
for i in range(100):
mineplace = mine_list[i]
placeX = mineplace[0]
placeY = mineplace[1]
GridOutMine[placeX][placeY] = 1
print(GridOutMine)
mine_list = MineGen()
GridOutMine = GridGen()
MineGrid(GridOutMine, mine_list)
My issue is that i am getting a list index out of range for the
GridOutMine[placeX][placeY] = 1
part. I don't really know why this is. If you could give me some assistance in what to do, or just some general comments on my code, I would really appreciate it thanks.
That's because, unlike range, random.randint outputs numbers within the specified bounds inclusively. That is, randint(1, 25) could output 25, which is not a valid index for a list that's only 25 elements long (since the last index is 24).
In MineGen, you need to change randint(1, 25) to randint(1, 25-1) or randint(1, 24), and likewise for randint(1, 40) which needs to be randint(1, 39). I'd actually suggest randint(0, 24) and (0, 39), but I don't know if that's intentional.
There are many other things that could/should be improved about this code, but I'd suggest you ask for that kind of input over on CodeReview instead of here once your code is working (they don't fix broken code).
EDIT:
Also, you're indexing your grid in the wrong order. It's a list (25-long) of rows (40-long), so you need to index it in the Y dimension first, then X: GridOutMine[placeY][placeX] = 1
If you are trying to build a grid for the game and want to display buttons in a GUI your users can interact with, you may want to start with the following code as a basic framework for the rest of the code you will be writing.
import tkinter
import functools
class MineSweep(tkinter.Frame):
#classmethod
def main(cls, width, height):
root = tkinter.Tk()
window = cls(root, width, height)
root.mainloop()
def __init__(self, master, width, height):
super().__init__(master)
self.__width = width
self.__height = height
self.__build_buttons()
self.grid()
def __build_buttons(self):
self.__buttons = []
for y in range(self.__height):
row = []
for x in range(self.__width):
button = tkinter.Button(self)
button.grid(column=x, row=y)
button['text'] = '?'
command = functools.partial(self.__push, x, y)
button['command'] = command
row.append(button)
self.__buttons.append(row)
def __push(self, x, y):
print('Column = {}\nRow = {}'.format(x, y))
if __name__ == '__main__':
MineSweep.main(10, 10)
If you want a more complete example of a minesweeper game to either borrow ideas from or adapt for your own needs, the following program implements much of the functionality you might want from a finished game.
import tkinter
import functools
import random
from tkinter.simpledialog import askstring, Dialog
from tkinter.messagebox import showinfo
import os.path
################################################################################
class MineSweep(tkinter.Frame):
#classmethod
def main(cls, width, height, mines, scores):
root = tkinter.Tk()
root.resizable(False, False)
root.title('MineSweep')
window = cls(root, width, height, mines, scores)
root.protocol('WM_DELETE_WINDOW', window.close)
root.mainloop()
################################################################################
def __init__(self, master, width, height, mines, scores):
super().__init__(master)
self.__width = width
self.__height = height
self.__mines = mines
self.__wondering = width * height
self.__started = False
self.__playing = True
self.__scores = ScoreTable()
self.__record_file = scores
if os.path.isfile(scores):
self.__scores.load(scores)
self.__build_timer()
self.__build_buttons()
self.grid()
def close(self):
self.__scores.save(self.__record_file)
self.quit()
def __build_timer(self):
self.__secs = tkinter.IntVar()
self.__timer = tkinter.Label(textvariable=self.__secs)
self.__timer.grid(columnspan=self.__width, sticky=tkinter.EW)
self.__after_handle = None
def __build_buttons(self):
self.__reset_button = tkinter.Button(self)
self.__reset_button['text'] = 'Reset'
self.__reset_button['command'] = self.__reset
self.__reset_button.grid(column=0, row=1,
columnspan=self.__width, sticky=tkinter.EW)
self.__reset_button.blink_handle = None
self.__buttons = []
for y in range(self.__height):
row = []
for x in range(self.__width):
button = tkinter.Button(self, width=2, height=1,
text='?', fg='red')
button.grid(column=x, row=y+2)
command = functools.partial(self.__push, x, y)
button['command'] = command
row.append(button)
self.__buttons.append(row)
def __reset(self):
for row in self.__buttons:
for button in row:
button.config(text='?', fg='red')
self.__started = False
self.__playing = True
self.__wondering = self.__width * self.__height
if self.__after_handle is not None:
self.after_cancel(self.__after_handle)
self.__after_handle = None
self.__secs.set(0)
def __push(self, x, y, real=True):
button = self.__buttons[y][x]
if self.__playing:
if not self.__started:
self.__build_mines()
while self.__buttons[y][x].mine:
self.__build_mines()
self.__started = True
self.__after_handle = self.after(1000, self.__tick)
if not button.pushed:
self.__push_button(button, x, y)
elif real:
self.__blink(button, button['bg'], 'red')
elif real:
self.__blink(button, button['bg'], 'red')
def __blink(self, button, from_bg, to_bg, times=8):
if button.blink_handle is not None and times == 8:
return
button['bg'] = (to_bg, from_bg)[times & 1]
times -= 1
if times:
blinker = functools.partial(self.__blink, button,
from_bg, to_bg, times)
button.blink_handle = self.after(250, blinker)
else:
button.blink_handle = None
def __tick(self):
self.__after_handle = self.after(1000, self.__tick)
self.__secs.set(self.__secs.get() + 1)
def __push_button(self, button, x, y):
button.pushed = True
if button.mine:
button['text'] = 'X'
self.__playing = False
self.after_cancel(self.__after_handle)
self.__after_handle = None
self.__blink(self.__reset_button, button['bg'], 'red')
else:
button['fg'] = 'SystemButtonText'
count = self.__total(x, y)
button['text'] = count and str(count) or ' '
self.__wondering -= 1
if self.__wondering == self.__mines:
self.after_cancel(self.__after_handle)
self.__after_handle = None
self.__finish_game()
def __finish_game(self):
self.__playing = False
score = self.__secs.get()
for row in self.__buttons:
for button in row:
if button.mine:
button['text'] = 'X'
if self.__scores.eligible(score):
name = askstring('New Record', 'What is your name?')
if name is None:
name = 'Anonymous'
self.__scores.add(name, score)
else:
showinfo('You did not get on the high score table.')
HighScoreView(self, 'High Scores', self.__scores.listing())
def __total(self, x, y):
count = 0
for x_offset in range(-1, 2):
x_index = x + x_offset
for y_offset in range(-1, 2):
y_index = y + y_offset
if 0 <= x_index < self.__width and 0 <= y_index < self.__height:
count += self.__buttons[y_index][x_index].mine
if not count:
self.__propagate(x, y)
return count
def __propagate(self, x, y):
for x_offset in range(-1, 2):
x_index = x + x_offset
for y_offset in range(-1, 2):
y_index = y + y_offset
if 0 <= x_index < self.__width and 0 <= y_index < self.__height:
self.__push(x_index, y_index, False)
def __build_mines(self):
mines = [True] * self.__mines
empty = [False] * (self.__width * self.__height - self.__mines)
total = mines + empty
random.shuffle(total)
iterator = iter(total)
for row in self.__buttons:
for button in row:
button.mine = next(iterator)
button.pushed = False
button.blink_handle = None
################################################################################
class ScoreTable:
def __init__(self, size=10):
self.__data = {999: [''] * size}
def add(self, name, score):
assert self.eligible(score)
if score in self.__data:
self.__data[score].insert(0, name)
else:
self.__data[score] = [name]
if len(self.__data[max(self.__data)]) == 1:
del self.__data[max(self.__data)]
else:
del self.__data[max(self.__data)][-1]
def eligible(self, score):
return score <= max(self.__data)
def listing(self):
for key in sorted(self.__data.keys()):
for name in self.__data[key]:
yield name, key
def load(self, filename):
self.__data = eval(open(filename, 'r').read())
def save(self, filename):
open(filename, 'w').write(repr(self.__data))
################################################################################
class HighScoreView(Dialog):
def __init__(self, parent, title, generator):
self.__scores = generator
super().__init__(parent, title)
def body(self, master):
self.__labels = []
for row, (name, score) in enumerate(self.__scores):
label = tkinter.Label(master, text=name)
self.__labels.append(label)
label.grid(row=row, column=0)
label = tkinter.Label(master, text=str(score))
self.__labels.append(label)
label.grid(row=row, column=1)
self.__okay = tkinter.Button(master, command=self.ok, text='Okay')
self.__okay.grid(ipadx=100, columnspan=2, column=0, row=row+1)
return self.__okay
def buttonbox(self):
pass
################################################################################
if __name__ == '__main__':
MineSweep.main(10, 10, 10, 'scores.txt')
Reference: ActiveState Code » Recipes » MineSweep

The item configure method didn't work in Tkinter

I tried to use the Tkinter library for my small project in python. I create a 500 by 500 square with 10000 small square in it.
And I want each small square turns black when user click on it. Can someone please tell me why, I would really appreciate it. Here is the graphics code:
from Tkinter import *
from button import *
class AppFrame(Frame):
def __init__(self):
self.root = Tk()
self.root.geometry = ("1000x1000")
self.f = Frame(self.root, relief = 'sunken', width = 600, height = 600)
self.w = Canvas(self.f,width = 505, height =505)
##get the x, y value whenever the user make a mouse click
self.w.bind("<Button-1>", self.xy)
self.bolist = []
for k in range(1,101):
for i in range(1, 101):
button = Buttons(self.w, i * 5, k * 5, i * 5 + 5, k * 5 + 5)
self.bolist.append(button)
self.f.grid(column =0, columnspan = 4)
self.w.grid(column = 0)
self.root.mainloop()
def xy (self, event):
self.x, self.y = event.x, event.y
print (self.x, self.y)
##check each button if it's clicked
for hb in self.bolist:
if hb.clicked(self.x, self.y):
print ("hurry")
hb.activate()
And
##button.py
from Tkinter import *
class Buttons:
def __init__(self,canvas,bx,by,tx,ty):
self.canvas = canvas
self.rec = canvas.create_rectangle((bx,by,tx,ty),fill = "lightgray",
activefill= 'black', outline = 'lightgray')
self.xmin = bx
self.xmax = tx
self.ymin = by
self.ymax = ty
##print (bx, by, tx, ty)
def clicked(self, px, py):
return (self.active and self.xmin <= px <= self.xmax and
self.ymin <= py <= self.ymax)
def activate(self):
self.canvas.itemconfigure(slef.rec, fill = 'black')
self.active = True
The problem is that you don't initialize the active attribute, so it doesn't exist until the cell becomes active. To fix that, add self.active = False inside the __init__ method of Buttons.
You also have a typo in this line (notice you use slef rather than self):
self.canvas.itemconfigure(slef.rec, fill = 'black')
Instead of a global binding on the canvas, it would be more efficient to set a binding on each individual rectangle. You can then use the binding to pass the instance of the Buttons class to the callback. This way you don't have to iterate over several thousand widgets looking for the one that was clicked on.
To do this, use the tag_bind method of the canvas. You can make it so that your main program passes in a reference to a function to call when the rectangle is clicked, then the binding can call that method and pass it a reference to itself.
For example:
class Buttons:
def __init__(self,canvas,bx,by,tx,ty, callback):
...
self.rec = canvas.create_rectangle(...)
self.canvas.tag_bind(self.rec, "<1>",
lambda event: callback(self))
...
class AppFrame(Frame):
def __init__(...):
...
button = Buttons(..., self.callback)
...
def callback(self, b):
b.activate()
Here, I looked at your code, debugged it, and made some adjustments. It works now.
Just keep both the scripts in one folder and run your AppFrame script (the second one in this answer)
##button.py
from Tkinter import *
class Buttons:
def __init__(self,canvas,bx,by,tx,ty):
self.canvas = canvas
self.rec = canvas.create_rectangle((bx,by,tx,ty),fill = "lightgray", activefill= 'black', outline = 'lightgray')
self.xmin = bx
self.xmax = tx
self.ymin = by
self.ymax = ty
##print (bx, by, tx, ty)
def clicked(self, px, py):
return (self.xmin <= px <= self.xmax and
self.ymin <= py <= self.ymax)
def activate(self):
self.canvas.itemconfigure(self.rec, fill = 'black')
AND
from Tkinter import *
from button import *
class AppFrame(Frame):
def __init__(self):
self.root = Tk()
self.root.geometry = ("1000x1000")
self.f = Frame(self.root, relief = 'sunken', width = 600, height = 600)
self.w = Canvas(self.f,width = 505, height =505)
##get the x, y value whenever the user make a mouse click
self.w.bind("<Button-1>", self.xy)
self.bolist = []
for k in range(1,101):
for i in range(1, 101):
button = Buttons(self.w, i * 5, k * 5, i * 5 + 5, k * 5 + 5)
self.bolist.append(button)
self.f.grid(column =0, columnspan = 4)
self.w.grid(column = 0)
self.root.mainloop()
def xy (self, event):
self.x, self.y = event.x, event.y
print (self.x, self.y)
##check each button if it's clicked
for hb in self.bolist:
if hb.clicked(self.x, self.y):
print ("hurry")
hb.activate()
newApp = AppFrame()

how to repaint only part of a QWidget in PyQt4?

I'm trying to create a program that displays a large grid of numbers (say, filling up a 6 by 4000 grid), where the user can move a cursor around via keyboard or mouse and enter in numbers into the grid. (This is for a guitar tablature program.) I'm new to python GUI programming, and thus far my idea is to have a very large QWidget window (say, 1000x80000 pixels) inside of a QScrollArea inside of the main window. The problem is that every mouse click or cursor movement causes the whole thing to repaint, causing a delay, when I just want to repaint whatever changes I just made to make things faster. In PyQt, is there a way to buffer already-painted graphics and change just the graphics that need changing?
edit: I've posted the code below, which I've run with python3.3 on Mac OS 10.7. The main point is that in the TabWindow init function, the grid size can be set by numXGrid and numYGrid (currently set to 200 and 6), and this grid is filled with random numbers by the generateRandomTablatureData() method. If the grid is filled with numbers, then there's a noticeable lag with every key press, which gets worse with larger grids. (There is also an initial delay due to generating the data, but my question is on the delay after each key press which I assume is due to having to repaint every number.)
There are two files. This is the main one, which I called FAIT.py:
import time
start_time = time.time()
import random
import sys
from PyQt4 import QtGui, QtCore
import Tracks
# generate tracks
tracks = [Tracks.Track(), Tracks.Track(), Tracks.Track()]
fontSize = 16
# margins
xMar = 50
yMar = 50
trackMar = 50 # margin between tracks
class MainWindow(QtGui.QWidget):
def __init__(self):
super(MainWindow, self).__init__()
self.initUI()
end_time = time.time()
print("Initializing time was %g seconds" % (end_time - start_time))
def initUI(self):
# attach QScrollArea to MainWindow
l = QtGui.QVBoxLayout(self)
l.setContentsMargins(0,0,0,0)
l.setSpacing(0)
s=QtGui.QScrollArea()
l.addWidget(s)
# attach TabWindow to QScrollArea so we can paint on it
self.tabWindow=TabWindow(self)
self.tabWindow.setFocusPolicy(QtCore.Qt.StrongFocus)
self.setFocusPolicy(QtCore.Qt.NoFocus)
vbox=QtGui.QVBoxLayout(self.tabWindow)
s.setWidget(self.tabWindow)
self.positionWindow() # set size and position of main window
self.setWindowTitle('MainWindow')
self.show()
def positionWindow(self):
qr = self.frameGeometry()
cp = QtGui.QDesktopWidget().availableGeometry().center()
width = QtGui.QDesktopWidget().availableGeometry().width() - 100
height = QtGui.QDesktopWidget().availableGeometry().height() - 100
self.resize(width, height)
qr = self.frameGeometry()
cp = QtGui.QDesktopWidget().availableGeometry().center()
qr.moveCenter(cp)
self.move(qr.topLeft())
def keyPressEvent(self, e):
print('key pressed in MainWindow')
def mousePressEvent(self, e):
print('mouse click in MainWindow')
class TabWindow(QtGui.QWidget):
def __init__(self, parent=None):
QtGui.QWidget.__init__(self, parent)
# size of tablature grid
numXGrid = 200
numYGrid = 6
# initialize tablature information first
for i in range(0, len(tracks)):
tracks[i].numXGrid = numXGrid
self.arrangeTracks() # figure out offsets for each track
self.trackFocusNum = 0 # to begin with, focus is on track 0
self.windowSizeX = tracks[0].x0 + tracks[0].dx*(tracks[0].numXGrid+2)
self.windowSizeY = tracks[0].y0
for i in range(0, len(tracks)):
self.windowSizeY = self.windowSizeY + tracks[i].dy * tracks[i].numYGrid + trackMar
self.resize(self.windowSizeX,self.windowSizeY) # size of actual tablature area
# generate random tablature data for testing
self.generateRandomTablatureData()
def keyPressEvent(self, e):
print('key pressed in TabWindow')
i = self.trackFocusNum
if e.key() == QtCore.Qt.Key_Up:
tracks[i].moveCursorUp()
if e.key() == QtCore.Qt.Key_Down:
tracks[i].moveCursorDown()
if e.key() == QtCore.Qt.Key_Left:
tracks[i].moveCursorLeft()
if e.key() == QtCore.Qt.Key_Right:
tracks[i].moveCursorRight()
# check for number input
numberKeys = (QtCore.Qt.Key_0,
QtCore.Qt.Key_1,
QtCore.Qt.Key_2,
QtCore.Qt.Key_3,
QtCore.Qt.Key_4,
QtCore.Qt.Key_5,
QtCore.Qt.Key_6,
QtCore.Qt.Key_7,
QtCore.Qt.Key_8,
QtCore.Qt.Key_9)
if e.key() in numberKeys:
num = int(e.key())-48
# add data
tracks[i].data.addToTab(tracks[i].iCursor, tracks[i].jCursor, num)
# convert entered number to pitch and play note (do later)
# spacebar, backspace, or delete remove data
if e.key() in (QtCore.Qt.Key_Space, QtCore.Qt.Key_Backspace, QtCore.Qt.Key_Delete):
tracks[i].data.removeFromTab(tracks[i].iCursor, tracks[i].jCursor)
self.update()
def mousePressEvent(self, e):
print('mouse click in TabWindow')
xPos = e.x()
yPos = e.y()
# check tracks one by one
for i in range(0, len(tracks)):
if (tracks[i].isPositionInside(xPos, yPos)):
tracks[i].moveCursorToPosition(xPos, yPos)
self.trackFocusNum = i
break
else:
continue
self.update()
def paintEvent(self, e):
qp = QtGui.QPainter()
qp.begin(self)
qp.setPen(QtCore.Qt.black)
qp.setBrush(QtCore.Qt.white)
qp.drawRect(0, 0, self.windowSizeX, self.windowSizeY)
self.paintTracks(qp)
self.paintTunings(qp)
self.paintCursor(qp)
self.paintNumbers(qp)
qp.end()
def paintTracks(self, qp):
qp.setPen(QtCore.Qt.black)
qp.setBrush(QtCore.Qt.white)
for i in range(0, len(tracks)):
qp.drawPolyline(tracks[i].polyline)
def paintCursor(self, qp):
i = self.trackFocusNum
qp.setPen(QtCore.Qt.black)
qp.setBrush(QtCore.Qt.black)
qp.drawPolygon(tracks[i].getCursorQPolygon())
def paintNumbers(self, qp):
# iterate through tracks, and iterate through numbers on each track
for i in range(0, len(tracks)):
# make sure track has data to draw
if len(tracks[i].data.data) > 0:
for j in range(0, len(tracks[i].data.data)):
# do actual painting here
# first set color to be inverse cursor color if at cursor
if i == self.trackFocusNum and \
tracks[i].iCursor == tracks[i].data.data[j][0] and \
tracks[i].jCursor == tracks[i].data.data[j][1]:
qp.setPen(QtCore.Qt.white)
else:
qp.setPen(QtCore.Qt.black)
font = QtGui.QFont('Helvetica', fontSize)
qp.setFont(font)
text = str(tracks[i].data.data[j][2])
x1 = tracks[i].convertIndexToPositionX(tracks[i].data.data[j][0])
y1 = tracks[i].convertIndexToPositionY(tracks[i].data.data[j][1])
dx = tracks[i].dx
dy = tracks[i].dy
# height and width of number character(s)
metrics = QtGui.QFontMetrics(font)
tx = metrics.width(text)
ty = metrics.height()
# formula for alignment:
# xMar = (dx-tx)/2 plus offset
x11 = x1 + (dx-tx)/2
y11 = y1 + dy/2+ty/2
qp.drawText(x11, y11, text)
def paintTunings(self, qp):
qp.setPen(QtCore.Qt.black)
font = QtGui.QFont('Helvetica', fontSize)
qp.setFont(font)
for i in range(0, len(tracks)):
for j in range(0, tracks[i].numStrings):
text = tracks[i].convertPitchToLetter(tracks[i].stringTuning[j])
# height and width of characters
metrics = QtGui.QFontMetrics(font)
tx = metrics.width(text)
ty = metrics.height()
x11 = tracks[i].x0 - tx - 10
y11 = tracks[i].convertIndexToPositionY(j) + (tracks[i].dy+ty)/2
qp.drawText(x11, y11, text)
def arrangeTracks(self):
tracks[0].x0 = xMar
tracks[0].y0 = yMar
tracks[0].generateGridQPolyline()
for i in range(1, len(tracks)):
tracks[i].x0 = xMar
tracks[i].y0 = tracks[i-1].y0 + tracks[i-1].height + trackMar
tracks[i].generateGridQPolyline()
def generateRandomTablatureData(self):
t1 = time.time()
for i in range(0, len(tracks)):
# worst case scenario: fill every number
for jx in range(0, tracks[i].numXGrid):
for jy in range(0, tracks[i].numYGrid):
val = random.randint(0,9)
tracks[i].data.addToTab(jx, jy, val)
t2 = time.time()
print("Random number generating time was %g seconds" % (t2 - t1))
def main():
app = QtGui.QApplication(sys.argv)
ex = MainWindow()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
This is the other file, Tracks.py, which contains the Track class and supplementary methods:
# contains classes and methods relating to individual tracks
import math
from PyQt4 import QtGui, QtCore
# class for containing information about a track
class Track:
def __init__(self):
self.data = TabulatureData()
# position offset
self.x0 = 0
self.y0 = 0
self.dx = 20 # default rectangle width
self.dy = 40 # default rectangle height
# current cursor index coordinates
self.iCursor = 0
self.jCursor = 0
# default size of grid
self.numXGrid = 4000
self.numYGrid = 6
self.numStrings = self.numYGrid
# calculated maximum width and height in pixels
self.maxWidth = self.dx * self.numXGrid
self.maxHeight = self.dy * self.numYGrid
# generate points of grid and cursor
self.generateGridQPolyline()
# tuning
self.setTuning([40, 45, 50, 55, 59, 64])
# calculate bounds
self.height = self.numYGrid*self.dy
self.width = self.numXGrid*self.dx
def getCursorIndexX(self, xPos):
iPos = int(math.floor( (xPos-self.x0)/self.dx ))
return iPos
def getCursorIndexY(self, yPos):
jPos = int(math.floor( (yPos-self.y0)/self.dy ))
return jPos
def convertIndexToCoordinates(self, iPos, jPos):
return [self.ConvertIndexToPositionX(iPos),
self.ConvertIndexToPositionY(jPos)]
def convertIndexToPositionX(self, iPos):
return self.x0 + iPos*self.dx
def convertIndexToPositionY(self, jPos):
return self.y0 + jPos*self.dy
def getCursorQPolygon(self):
x1 = self.convertIndexToPositionX(self.iCursor)
y1 = self.convertIndexToPositionY(self.jCursor)
x2 = self.convertIndexToPositionX(self.iCursor+1)
y2 = self.convertIndexToPositionY(self.jCursor+1)
return QtGui.QPolygonF([QtCore.QPoint(x1, y1),
QtCore.QPoint(x1, y2),
QtCore.QPoint(x2, y2),
QtCore.QPoint(x2, y1)])
def generateGridQPolyline(self):
self.points = []
self.polyline = QtGui.QPolygonF()
for i in range(0, self.numXGrid):
for j in range(0, self.numYGrid):
x1 = self.convertIndexToPositionX(i)
y1 = self.convertIndexToPositionY(j)
x2 = self.convertIndexToPositionX(i+1)
y2 = self.convertIndexToPositionY(j+1)
self.points.append([(x1, y1), (x1, y2), (x2, y2), (x2, y1)])
self.polyline << QtCore.QPoint(x1,y1) << \
QtCore.QPoint(x1,y2) << \
QtCore.QPoint(x2,y2) << \
QtCore.QPoint(x2,y1) << \
QtCore.QPoint(x1,y1)
# smoothly connect different rows
jLast = self.numYGrid-1
x1 = self.convertIndexToPositionX(i)
y1 = self.convertIndexToPositionY(jLast)
x2 = self.convertIndexToPositionX(i+1)
y2 = self.convertIndexToPositionY(jLast+1)
self.polyline << QtCore.QPoint(x2,y1)
def isPositionInside(self, xPos, yPos):
if (xPos >= self.x0 and xPos <= self.x0 + self.width and
yPos >= self.y0 and yPos <= self.y0 + self.height):
return True
else:
return False
def moveCursorToPosition(self, xPos, yPos):
self.iCursor = self.getCursorIndexX(xPos)
self.jCursor = self.getCursorIndexY(yPos)
self.moveCursorToIndex(self.iCursor, self.jCursor)
def moveCursorToIndex(self, iPos, jPos):
# check if bounds are breached, and if cursor's already there,
# and if not, move cursor
if iPos == self.iCursor and jPos == self.jCursor:
return
if iPos >= 0 and iPos < self.numXGrid:
if jPos >= 0 and jPos < self.numYGrid:
self.iCursor = iPos
self.jCursor = jPos
return
def moveCursorUp(self):
self.moveCursorToIndex(self.iCursor, self.jCursor-1)
def moveCursorDown(self):
self.moveCursorToIndex(self.iCursor, self.jCursor+1)
def moveCursorLeft(self):
self.moveCursorToIndex(self.iCursor-1, self.jCursor)
def moveCursorRight(self):
self.moveCursorToIndex(self.iCursor+1, self.jCursor)
# return pitch in midi integer notation
def convertNumberToPitch(self, jPos, pitchNum):
return pitchNum + self.stringTuning[(self.numStrings-1) - jPos]
def convertPitchToLetter(self, pitchNum):
p = pitchNum % 12
letters = ('C', 'Db', 'D', 'Eb', 'E', 'F', 'Gb', 'G', 'Ab', 'A', 'Bb', 'B')
return letters[p]
def setTuning(self, tuning):
self.stringTuning = tuning
class TabulatureData:
def __init__(self):
self.data = []
def addToTab(self, i, j, value):
# check if data is already there, and remove it first
if self.getValue(i, j) > -1:
self.removeFromTab(i, j)
self.data.append([i, j, value])
def getValue(self, i, j):
possibleTuples = [x for x in self.data if x[0] == i and x[1] == j]
if possibleTuples == []:
return -1
elif len(possibleTuples) > 1:
print('Warning: more than one number at a location!')
return possibleTuples[0][2] # return third number of tuple
def removeFromTab(self, i, j):
# first get value, if it exists
value = self.getValue(i,j)
if value == -1:
return
else:
# if it exists, then remove
self.data.remove([i, j, value])
1000*80000 is really huge.
So,maybe you should try QGLWidget or something like that?
Or according to Qt document, you should set which region you want to repaint.
some slow widgets need to optimize by painting only the requested region: QPaintEvent::region(). This speed optimization does not change the result, as painting is clipped to that region during event processing. QListView and QTableView do this, for example.

Categories