How to draw points and lines on widgets using PyQt - python-3.x

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.

Related

PyQt QScrollArea doesn't display widgets

I am somewhat new to GUI programming and very new to PyQt, and I'm trying to build a GUI that displays a list of questions. I have created a QuestionBank class that subclasses QWidget and overrides the .show() method to display the list properly. I have tested this alone and it works correctly. However, the list of questions can be quite long, so I've been trying to make it scrollable. Rather than add a QScrollBar to the widget and then set up the event triggers by hand, I've been trying to my QuestionBank widget in a QScrollArea based on the syntax I've seen in examples online. While the scroll area shows up fine, it does not at all display the question bank but rather just shows a blank outline.
The QuestionBank class looks like this:
class QuestionBank(QWidget):
BUFFER = 10 # space between questions (can be modified)
def __init__(self, parent, questions):
# `parent` should be the QWidget that contains the QuestionBank, or None if
# QuestionBank is top level
# `questions` should be a list of MasterQuestion objects (not widgets)
QWidget.__init__(self, parent)
self.questions = [MasterQuestionWidget(self, q) for q in questions]
self.bottomEdge = 0
def show(self, y=BUFFER):
QWidget.show(self)
for q in self.questions:
# coordinates for each each question
q.move(QuestionBank.BUFFER, y)
q.show()
# update y-coordinate so that questions don't overlap
y += q.frameGeometry().height() + QuestionBank.BUFFER
self.bottomEdge = y + 3 * QuestionBank.BUFFER
# ... other methods down here
My code for showing the scroll bar looks like this:
app = QApplication(sys.argv)
frame = QScrollArea()
qs = QuestionBank(None, QFileManager.importQuestions())
qs.resize(350, 700)
frame.setGeometry(0, 0, 350, 300)
frame.setWidget(qs)
frame.show()
sys.exit(app.exec_())
I have tried many variants of this, including calling resize on frame instead of qs, getting rid of setGeometry, and setting the parent of qs to frame instead of None and I have no idea where I'm going wrong.
If it helps, I'm using PyQt5
Here is the question bank without the scroll area, to see what it is supposed to look like:
Here is the output of the code above with the scroll area:
This variation on the code is the only one that produces any output whatsoever, the rest just have blank windows. I'm convinced its something simple I'm missing, as the frame is obviously resizing correctly and it obviously knows what widget to display but its not showing the whole thing.
Any help is much appreciated, thank you in advance.

Splitting tkinter canvas and frame apart

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)

Pygame drawable text class fails to draw

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)

QDoppelganger (clone a QWidget into another container)

Overflowers: We have a complex dock widget with several complex, aggregate QWidgets in it.
I need to allow users to minimize the dock widget down to a ribbon (as a QToolBar), containing only the most important widgets. But because these widgets are rather complex, I would rather not reproduce them in code, and painstakingly transfer their custom values back and forth. Clicking one copy of a widget should behave exactly the same as clicking another.
So it seems I would like to invent a QDoppelganger widget, which delegates input and render events back and forth, to make a functioning copy of another control:
class QDoppelganger(QtGui.QWidget):
def __init__(self, dupeMe, parent):
super(QDoppelganger, self).__init__(parent)
self.dupeMe = dupeMe
formerResizer = self.dupeMe.resizeEvent
def resizeMe(qResizeEvent):
self.setFixedSize(self.dupeMe.size())
return formerResizer(qResizeEvent)
self.dupeMe.resizeEvent = resizeMe
formerPainter = self.dupeMe.paintEvent
def paintMe(qPaintEvent):
self.update()
return formerPainter(qPaintEvent)
self.dupeMe.paintEvent = paintMe
def mouseMoveEvent(self, qMouseEvent): return self.dupeMe.mouseMoveEvent(qMouseEvent)
def mousePressEvent(self, qMouseEvent): return self.dupeMe.mousePressEvent(qMouseEvent)
def mouseReleaseEvent(self, qMouseEvent): return self.dupeMe.mouseReleaseEvent(qMouseEvent)
def paintEvent(self, qPaintEvent):
pix = QtGui.QPixmap(self.dupeMe.width(), self.dupeMe.height())
painter = QtGui.QPainter()
painter.begin(pix)
self.dupeMe.render(painter)
painter.end()
qp = QtGui.QPainter()
qp.begin(self)
qp.drawImage(0, 0, pix.toImage())
qp.end()
Its driver code is trivial:
button = QtGui.QPushButton('dupe me')
doppel = QtUtil.QDoppelganger(button, None)
Stick one widget into the dock widget, and another into the QToolBar, and we are done.
If I have nailed it, then this post is a useful snippet.
If not, may I request a review of the techniques? No matter how few lines of PyQt code I write, it seems someone always comes along with some "cleverSignal.connect(cleverSlot)" to simply plug things together with even fewer lines of code. Specifically, instead of rendering one control into a QPixmap, then painting that into the doppelganger control, can't I simply connect the paint event directly?

Why the pixmap(a football field image) disappear when I try to draw something on its QLabel?

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()

Resources