I have a class that represents a bit of text that can be drawn to the screen. This class is intended to be used in relation to a Window object that, when it is drawn, passes a subsurface of the surface it was given to the draw() function of the text element.
The window draws, but the text does not. Nor does it draw when I invoke the TextElement's draw() function directly. I've examined the code with a debugger, and the blit is definitely being done. I've also tried switching the font to "Arial" instead of letting Pygame grab the default.
import pygame
#Background and edge color of windows
global bgcolor
global bordercolor
global txtcolor
#Pygame font object that most text will be drawn in
global mainfont
#Defaults; code that imports us can change these.
#Black background with white borders & text
bgcolor=(0,0,0)
txtcolor=(255,255,255)
bordercolor=(255,255,255)
#This will give us whatever font Pygame is set up to use as default
if not __name__== "__main__":
#Font module needs to be initialized for this to work; it should be if we're imported, but won't if
#we're being executed directly.
mainfont=pygame.font.SysFont(None,12)
else:
mainfont=None
class Window:
"""The base UI class. Is more-or-less a way to draw an empty square onto the screen. Other things are derived from this.
Also usable in its own right as a way to manage groups of more complex elements by defining them as children of
a basic window."""
def __init__(self, rect):
self.rect=rect
#'children' of a window are drawn whenever it is drawn, unless their positions are outside the area of the window.
self.children=[]
def draw(self, surface):
"Draw this window to the given surface."
pygame.draw.rect(surface, bgcolor, self.rect)
#Draw non-filled rect with line width 4 as the border
pygame.draw.rect(surface, bordercolor, self.rect, 4)
self._draw_children(surface)
def _draw_children(self,surface):
"Call draw() on each of the children of this window. Intended to be called as part of an element's draw() function."
for thechild in self.children:
#We use a subsurface because we only want our child elements to be able to access the area inside this window.
thechild.draw(surface.subsurface(self.rect))
class TextElement(str):
"""A bit of static text that can be drawn to the screen. Intended to be used inside a Window, but can be drawn
straight to a surface. Immutable; use DynamicTextElement for text that can change and move."""
def __new__(cls,text,font,rect=pygame.Rect((0,0),(0,0))):
self=super().__new__(cls,text)
self.image=font.render(text,True,txtcolor)
self.rect=rect
return self
def __repr__(self):
return str.join("",("TextElement('",self,"')"))
def draw(self,surface):
self.image.blit(surface,self.rect.topleft)
class DynamicTextElement():
"""A bit of text whose text can be changed and that can be moved. Slower than TextElement."""
def __init__(self,text,font,rect):
self.text, self.font, self.rect = text, font, rect
def __repr__(self):
return str.join("",("DynamicTextElement('",self.text,"')"))
def draw(self,surface):
image=self.font.render(self.text,True,txtcolor)
image.blit(surface,self.rect.topleft)
if __name__ == '__main__':
pygame.init()
mainfont=pygame.font.SysFont(None,12)
screen=pygame.display.set_mode((800,600))
mywindow=Window(pygame.Rect(150,150,600,300))
mytext=TextElement("Hello world! This is static, immutable text!",mainfont,pygame.Rect((200,200),(100,100)))
mydyntext=DynamicTextElement("And this is dnyamic, movable text!",mainfont,pygame.Rect((200,230),(100,100)))
print(mytext.image)
mywindow.children.append(mytext)
clock=pygame.time.Clock()
while True:
pygame.event.pump()
clock.tick(60)
screen.fill((55,55,55))
mywindow.draw(screen)
mytext.draw(screen)
pygame.display.flip()
Any ideas?
You use
self.image.blit(surface, self.rect.topleft)
but should be
surface.blit(self.image, self.rect.topleft)
surface and self.image in wrong places.
You tried to draw surface on text.
You have the same problem in DynamicTextElement()
BTW: I use Python 2 so I had to use str
self = str.__new__(cls,text)
in place of super()
self = super().__new__(cls,text)
Related
I'm trying to make a program where the user can paint on the screen. So I want to make an invisible canvas window in fullscreen where only the user's pen marks on the canvas will be visible. The closest thing I found is this function: root.attributes("-transparentcolor","color code here"), which will make all the parts of the window that's in the color you give transparent. So if I give the second parameter the background color of the canvas, then only the pen strokes on the canvas will be visible. This is so close to what I want, except for one thing, the transparent areas can't detect or block mouse clicks! Any mouse clicks will just go through to whatever is behind the tkinter window. Is there a way to make it so the transparent areas will still block mouse clicks? I really need help on this!
Here is a much better way to do this using only tkinter. Explanation is in code comments. Basically uses two windows, one for "blocking" the mouse and being transparent using the "-alpha" attribute and the other window for "hosting" canvas and having one completely transparent color while keeping others opaque using "-transparentcolor" attribute. That also means that this is cross-platform solution too (except I think the -transparentcolor attribute differs a little bit on other OS like Linux where I think it is -splash or sth and maybe something different on MacOS):
from tkinter import Tk, Toplevel, Canvas
# setting the starting coordinate of the line so that
# on motion it is possible to immediately draw it
def set_first(event):
points.extend([event.x, event.y])
# on motion append new coordinates to the list and if there are
# 4 (the minimum), create a new line and save the id
# otherwise update the existing line
def append_and_draw(event):
global line
points.extend([event.x, event.y])
if len(points) == 4:
line = canvas.create_line(points, **line_options)
else:
canvas.coords(line, points)
# when released clear the list to not waste space
# and not necessarily but also set "id" to None
def clear_list(event=None):
global line
points.clear()
line = None
line = None # this is a reference to the current line (id)
points = [] # list to keep track of current line coordinates
line_options = {} # dictionary to allow easier change of line options
# just a variable to more easily store the transparent color
transparent_color = 'grey15'
# creating the root window which will help with drawing the line
# because it will "block" mouse because `-alpha` (0.01 seems to be the lowest value)
# attribute is used, however it makes everything transparent on the window
# so need another window to "host" the canvas
root = Tk()
root.attributes('-alpha', 0.01)
root.attributes('-topmost', True)
root.attributes('-fullscreen', True)
# just press Esc key to close the whole thing, otherwise
# it is only doable by pressing Alt + F4 or turning off
# the computer
root.bind('<Escape>', lambda e: root.quit())
# create the host window, because it allows to have only
# one transparent color while keeping the other opaque and
# visible
top = Toplevel(root)
top.attributes('-transparentcolor', transparent_color)
top.attributes('-topmost', True)
top.attributes('-fullscreen', True)
# set the focus to root because that is where events are bound
root.focus_set()
# create the canvas to draw on
canvas = Canvas(top, bg=transparent_color, highlightthickness=0)
canvas.pack(fill='both', expand=True)
# bind all the events to `root` which "blocks" mouse
# but is also almost (because it has a very small alpha value
# it is not entirely invisible but human eye won't notice much)
# invisible
root.bind('<Button-1>', set_first)
root.bind('<B1-Motion>', append_and_draw)
root.bind('<ButtonRelease-1>', clear_list)
root.mainloop()
Here is an improvable example (you may need to pip install pyautogui, ctypes is a built-in library), it is also Windows only as far as I know:
Note: The other answer using two windows, however, is a lot better but I will keep this too just for the information.
from tkinter import Tk, Canvas
import pyautogui as pag
import ctypes
data = {
'draw': True,
'cur_line_points': [],
'cur_line_id': None
}
# function taken mainly from here: https://stackoverflow.com/a/46596592/14531062
def is_pressed(btn: str = 'left') -> bool:
if btn == 'left':
btn = 0x01
elif btn == 'right':
btn = 0x02
else:
raise Warning("incorrect argument, should be 'left' or 'right'")
return ctypes.windll.user32.GetKeyState(btn) not in (0, 1)
def draw_line(canvas_):
if not data['draw']:
root.after(10, draw_line, canvas_)
return
pressed = is_pressed('left')
cur_line_points = data['cur_line_points']
cur_line_id = data['cur_line_id']
if not pressed:
if cur_line_id is not None:
canvas_.coords(cur_line_id, cur_line_points)
data['cur_line_id'] = None
cur_line_points.clear()
else:
mouse_x, mouse_y = pag.position()
cur_line_points.extend((mouse_x, mouse_y))
len_points = len(cur_line_points)
if len_points == 4:
data['cur_line_id'] = canvas_.create_line(cur_line_points)
elif len_points > 4:
canvas_.coords(cur_line_id, cur_line_points)
root.after(10, draw_line, canvas_)
transparent_color = 'grey15'
root = Tk()
root.config(bg=transparent_color)
root.attributes('-transparentcolor', transparent_color)
root.attributes('-topmost', True)
root.attributes('-fullscreen', True)
canvas = Canvas(root, bg=transparent_color, highlightthickness=0)
canvas.pack(fill='both', expand=True)
draw_line(canvas)
root.mainloop()
Basically detects if mouse button is pressed using the built-in library ctypes and if it is adds the current mouse coordinates (does that using pyautogui library which may need be installed) to a list and then draws a line based on that list (it also keeps the reference of the currently drawn line and simply changes its coordinates instead of drawing a new line each time it loops), the only slight issue is that while drawing the mouse is also interacting with the window below, highlighting text and stuff, couldn't really figure out how to remove that yet but at least you get to draw a line.
Got a tkinter frame on the left being used for labels, checkbuttons, etc. On the right is a canvas displaying a map. I can scroll over the map and it will give the longitude/latitude coordinates of where the mouse pointer is located on the map at the time in question. I can click on the map and it will zoom in on the map. The problem is when I'm on the frame where I want to display underlying map data as I scroll the mouse across the frame the longitude/latitude changes, even though I'm not on the canvas. If I click on the frame, haven't put any checkbuttons on there yet to test it that way, it zooms right in just like it would over on the canvas.
Is there any way to split apart the action 'sensing' of the frame and canvas to keep them separate.
I would post up the code, a bit lengthy, but I got get out of here as I'm already running late.
Edit:
I'm back and thanks to Bryan's reply I think I understand what he was saying to do, just not sure how to do it. In a couple of attempts nothing seemed to work. Granted I'm still not fully sure of the (self,parent) 'addressing' method in the code below.
Also I see high probability coming up in the not to distant future of needing to be able to reference the mouse button to both t he canvas and the frame separately, aka have it do different things depending on where I have clicked on. Fortunately with the delay thanks to having to get out of here earlier and with Bryan's answer I have been able to shorten the code down even more and now have code that is doing exactly what I'm talking about. The delay in posting code worked to my benefit.
import tkinter as tk
from tkinter import *
class Example(tk.Frame):
def __init__(self, parent):
tk.Frame.__init__(self, parent)
self.frame = tk.Frame(self,bg='black', width=1366, height=714)
self.frame1 = tk.Frame(self,bg='gray', width=652, height=714)
self.frame.pack()
self.canvas = tk.Canvas(self, background="black", width=714, height=714)
self.canvas.pack_propagate(0)
self.canvas.place(x=652,y=0)
self.frame1.pack_propagate(0)
self.frame1.place(x=0,y=0)
self.longitudecenter = -95.9477127
self.latitudecenter = 36.989772
self.p = 57.935628
global v
s = Canvas(self, width=150, height=20)
s.pack_propagate(0)
s.place(x=0,y=695)
v = Label(s, bg='gray',fg='black',borderwidth=0,anchor='w')
v.pack()
parent.bind("<Motion>", self.on_motion)
self.canvas.focus_set()
self.canvas.configure(xscrollincrement=1, yscrollincrement=1)
def on_motion(self, event):
self.canvas.delete("sx")
self.startx, self.starty = self.canvas.canvasx(event.x),self.canvas.canvasy(event.y)
px = -(round((-self.longitudecenter + (self.p/2))- (self.startx * (self.p/714)),5))
py = round((self.latitudecenter + (self.p/2))-(self.starty * (self.p /714)),5)
v.config(text = "Longitude: " + str(px))
if __name__ == "__main__":
root = tk.Tk()
Example(root).pack(fill="both", expand=True)
root.mainloop()
This is part of what I've been using. How do I change it so I can bind to to the frame and to the canvas separately. Right now I only need, with the case of the mouse position, to be able to bind to the canvas, but in the future I will need to be able to use mouse clicks, maybe even mouse position separately on the canvas and frame.(who knows given how much this project has changed/advanced since I started it three weeks ago...the sky is the limit).
If you want a binding to only fire for a specific widget, but the binding on that widget rather than on a containing widget.
Change this:
parent.bind("<Motion>", self.on_motion)
To this:
self.canvas.bind("<Motion>", self.on_motion)
I'm making an application, which should draw several points on widget and connect some of them with lines. I've made a form using QT Designer and I want to draw the points on frame for example. I've read that to draw on a widget its paintEvent() method should be reimplemented and I have a problem with it. My MainForm class has following code:
.........
def paintEvent(self, QPaintEvent):
paint = QtGui.QPainter()
paint.begin(self)
paint.setPen(QtCore.Qt.red)
size = self.size()
for i in range(100):
x = random.randint(1, size.width()-1)
y = random.randint(1, size.height()-1)
paint.drawPoint(x, y)
paint.end()
............
That method draws points on main window. How to make paintEvent() draw on exact frame of my form? And one more question: how make it only when I press some button because the code above redraws my window after any event.
I use PyQt v4.10 and Python 3.3 if it's important.
Thanks in advance for any help.
I've solved my problem so: I create my own widget (called PaintSpace) and put it into layout on my main form. Following code is inside MainForm class:
class MyPaintSpace(QtGui.QWidget):
"""My widget for drawing smth"""
def __init__(self):
super(PaintSpace, self).__init__()
<some code>
def paintEvent(self, QPaintEvent):
"""Reimpltmented drawing method of my widget"""
paint = QtGui.QPainter()
paint.begin(self)
<smth we want to draw>
paint.end()
# Make an object...
self.myPaintSpaceYZ = MyPaintSpace()
# ...and put it in layout
self.verticalLayoutYZ.addWidget(self.myPaintSpaceYZ)
After that to redraw my widget I use .update() method.
guys.I have a QLabel with a pixmap-a PNG image(typically a football playground) and I wanna draw some rectangels(represent some robots) on the playground,which I use the painter class to draw actually on its container-the QLabel. But when I use the painter to draw the REC,the RECT showed but the image just turned to blank.I don't know why it failed, and could u plz do me a favor and give me some hints on that?
class FieldLabel(QtGui.QLabel):
positionData = {"1":{"x":13,"y":20},"2":{"x":28,"y":19},"3":{"x":17,"y":21}}
def __init__(self, image_path):
QtGui.QLabel.__init__(self)
self.field = QtGui.QPixmap("field.png")
self.setPixmap(self.field.scaled(self.size(),
QtCore.Qt.KeepAspectRatio))
self.setSizePolicy(QtGui.QSizePolicy.Expanding,
QtGui.QSizePolicy.Expanding)
self.setAlignment(QtCore.Qt.AlignHCenter | QtCore.Qt.AlignVCenter)
def paintEvent(self,e):
draw = QtGui.QPainter()
draw.begin(self)
draw.setBrush(QtCore.Qt.NoBrush)
draw.setPen(QtCore.Qt.blue)
draw.drawRect(0,0,10,10)
draw.end()
paintEvent is responsible for drawing everything in the widget. Since you're overriding it, QLabel's default implementation, which draws the QPixmap, is not invoked.
So, first you should let the QLabel do its painting, then you can paint over it as you like:
def paintEvent(self,e):
# call the base implementation to paint normal interface
super(FieldLabel, self).paintEvent(e)
# then paint over it
draw = QtGui.QPainter()
draw.begin(self)
draw.setBrush(QtCore.Qt.NoBrush)
draw.setPen(QtCore.Qt.blue)
draw.drawRect(0,0,10,10)
draw.end()
So I have this dial image I'm using for a PyQt widget. Right now, as you can see, the gradient indicator, the arc of green-yellow-red is static and is part of the image.
I want to create this dynamically, so that based on some ranges, the color can be determined. For example, instead of equal parts green and red, I might want about 60 degrees worth of it to be red and the rest green and yellow. For this, I shall specify some range (eg. 0-90 degrees worth should be green etc).
Any ideas on how I can draw such a CURVED color gradient?
To accomplish this, one way is to create a widget and do a custom paintEvent. You will build up this result with a couple elements:
Draw the static pixmap of the gauge background
Draw a pie shape with a gradient
Re-fill the center with the original image to cut it back out again
You will need to cache the pixmap once in the init so you don't keep creating it from disk. Then you can give the widget a method to set the value from 0.0 - 1.0.
class GaugeWidget(QtGui.QWidget):
def __init__(self, initialValue=0, *args, **kwargs):
super(GaugeWidget, self).__init__(*args, **kwargs)
self._bg = QtGui.QPixmap("bg.png")
self.setValue(initialValue)
def setValue(self, val):
val = float(min(max(val, 0), 1))
self._value = -270 * val
self.update()
def paintEvent(self, event):
painter = QtGui.QPainter(self)
painter.setRenderHint(painter.Antialiasing)
rect = event.rect()
gauge_rect = QtCore.QRect(rect)
size = gauge_rect.size()
pos = gauge_rect.center()
gauge_rect.moveCenter( QtCore.QPoint(pos.x()-size.width(), pos.y()-size.height()) )
gauge_rect.setSize(size*.9)
gauge_rect.moveCenter(pos)
refill_rect = QtCore.QRect(gauge_rect)
size = refill_rect.size()
pos = refill_rect.center()
refill_rect.moveCenter( QtCore.QPoint(pos.x()-size.width(), pos.y()-size.height()) )
# smaller than .9 == thicker gauge
refill_rect.setSize(size*.9)
refill_rect.moveCenter(pos)
painter.setPen(QtCore.Qt.NoPen)
painter.drawPixmap(rect, self._bg)
painter.save()
grad = QtGui.QConicalGradient(QtCore.QPointF(gauge_rect.center()), 270.0)
grad.setColorAt(.75, QtCore.Qt.green)
grad.setColorAt(.5, QtCore.Qt.yellow)
grad.setColorAt(.25, QtCore.Qt.red)
painter.setBrush(grad)
painter.drawPie(gauge_rect, 225.0*16, self._value*16)
painter.restore()
painter.setBrush(QtGui.QBrush(self._bg.scaled(rect.size())))
painter.drawEllipse(refill_rect)
super(GaugeWidget,self).paintEvent(event)
You can expand on this widget even more by exposing a way to set the colors and also maybe even changing the start and end ranges. They would be attributes that the paintEvent would reference, just like the value attribute it is using now.
You can also adjust the color stop values to change the balance of the ranges.
Make sure to have the paintEvent do as little processing as possible.