I'm writing Paint-like program on PyQt5. I encountered a problem during writing pen tool.
My idea: if currentMode == 1 (penMode key) and mouse pressed - then program creates circle with width value == self.width and color == self.color. But my program terminates with exit code -1073740791 (0xC0000409). I'm not native English and I can't find way to fix this problem.
My code (main part):
class TPaintWorker(QtWidgets.QMainWindow, TPaintGUI):
def __init__(self):
super().__init__()
self.setupUi(self)
self.currentMode = 0
self.width = 0
self.drawX = 0
self.drawY = 0
self.endX = 0
self.endY = 0
self.doPaint = False
self.color = QtGui.QColor(255, 255, 255)
self.penMode.clicked.connect(self.penDrawerActivate)
self.polygonMode.clicked.connect(self.polygonDrawerActivate)
self.circleMode.clicked.connect(self.circleDrawerActivate)
self.eraserMode.clicked.connect(self.eraserActivate)
self.saveImage.clicked.connect(self.saveProcess)
self.loadImage.clicked.connect(self.loadProcess)
def mousePressEvent(self, event):
if self.currentMode:
self.drawX = event.x
self.drawY = event.y
self.paintPrepare()
self.update()
def paintPrepare(self):
self.doPaint = True
self.repaint()
def paintEvent(self, event):
if self.doPaint:
qp = QtGui.QPainter()
qp.begin(self)
if self.currentMode == 1:
self.penDrawer(qp)
print("im out")
qp.end()
def penDrawerActivate(self):
self.currentMode = 1
self.width = QtWidgets.QInputDialog.getInt(self, "Input width value", "Width value:", 5, 1, 100, 1)
self.color = QtWidgets.QColorDialog.getColor(QtGui.QColor(255, 255, 255))
def penDrawer(self, qp):
print("im in")
self.pen = QtGui.QPen(self.color)
qp.setPen(self.pen)
qp.drawEllipse(self.drawX, self.drawY, self.width, self.width)
def polygonDrawerActivate(self):
self.currentMode = 2
self.dots = QtWidgets.QInputDialog.getInt(self, "Input number of dots", "Number of dots:", 4, 3, 25, 1)
self.width = QtWidgets.QInputDialog.getInt(self, "Input width value", "Width value:", 5, 1, 100, 1)
self.color = QtWidgets.QColorDialog.getColor(QtGui.QColor(255, 255, 255))
def circleDrawerActivate(self):
self.currentMode = 3
self.radius = QtWidgets.QInputDialog.getInt(self, "Input radius of circle", "Radius:", 50, 5, 200, 1)
self.width = QtWidgets.QInputDialog.getInt(self, "Input width value", "Width value:", 5, 1, 100, 1)
self.color = QtWidgets.QColorDialog.getColor(QtGui.QColor(255, 255, 255))
def eraserActivate(self):
self.currentMode = 4
self.width = QtWidgets.QInputDialog.getInt(self, "Input width of eraser", "Width value:", 50, 5, 200, 1)
def loadProcess(self):
loadPath, _ = QtWidgets.QFileDialog.getOpenFileName(self, "Load image", "C:/", "PNG (*.png);;JPEG (*.jpg)")
if loadPath:
pixmap = QtGui.QPixmap(loadPath)
self.canvas.setPixmap(pixmap)
def saveProcess(self):
savePath, _ = QtWidgets.QFileDialog.getSaveFileName(self, "Save image", "C:/", "PNG (*.png);;JPEG (*.jpg)")
if savePath:
image = QtGui.QImage(self.canvas.pixmap())
image.save(savePath)
First of all, I suggest you to check the output of errors by running your program in a shell/prompt (note that IDEs sometimes are not completely able to show the full traceback): you'd have probably track your issues on your own.
The first problem is that you're not getting the coordinates in the mousePressEvent(); in Qt, access to almost all properties is done through callables (except some very specific exceptions, like properties of QStyleOption subclasses).
The result is that drawX and drawY are not actually coordinates, but references to event.x and event.y functions respectively.
Then, all QInputDialog static methods return a tuple made of (value, accepted): the second value specifies if the dialog has been accepted or not (if the user pressed Cancel, Esc or closed the window).
Since you're setting all values to the full tuple, Qt will obviously crash whenever you're trying to use those values for drawing, as the related QPainter function will not accept those parameters; as a side note, consider that if the user doesn't accept the dialog (because maybe she/he clicked the wrong button), you should not set those values, nor go on asking other parameters.
Finally, two suggestions.
painting on the main window should not happen unless really required; this is because a QMainWindow is a very special kind of QWidget, and has a specific set of features that can potentially create issues with custom painting; drawing on a dedicated QWidget should be preferred instead. This can be done by installing an event filter on that widget (probably, the central widget) and paint when a paint event is detected, or by subclassing QWidget, implement its own paintEvent and add that widget to the main window.
(this is quite trivial, but it can slightly simplify your code) you don't need to manually call begin and end on the QPainter instance, as you can just create the instance with the paint device in the constructor (qp = QtGui.QPainter(self)) and ignore the ending, since the painter will be automatically closed and correctly destroyed as soon as the paintEvent function returns.
Related
I'm pretty new to curses in python. I've tried to create race selection menu for my RPG game in python, but I've come to a problem with displaying the window.
Function, that uses class for window creation:
def character_race_scene(player_name):
# Clear window
stdscr.erase()
# Create character panels and making their borders
label = "What race are you?"
stdscr.addstr(hrows - 16, hcols - (len(label) // 2), label)
# Window creation for race select
stdscr.refresh()
race_select = Menu(10, 20, 5, 20)
race_select.win.addstr("Hello")
# adding races to select
#race_select.add_menu_label("Human", "Elf", "Org")
# printing select on the screen
#race_select.print_menu(1, 1, 0, 0, 1)
char_race = True
while char_race:
try:
key = stdscr.getkey()
if key == "z":
game(player_name, "Human", "Warrior")
except:
pass
Window creation class:
class Menu:
def __init__(self, height, width, y, x):
self.height = height
self.width = width
self.y = y
self.x = x
self.highlight = 0
self.menu_arr = []
self.win = curses.newwin(self.height, self.width, self.y, self.x)
#self.max_y_x = self.win.getmaxyx()
#self.max_y = self.max_y_x[0]
#self.max_x = self.max_y_x[1]
#self.hmax_y = self.max_y_x[0]//2
#self.hmax_x = self.max_y_x[1]//2
logging.debug(f"Menu was created.")
def print_menu(self, y, x, y_off=0, x_off=0, spacing=0):
if len(self.menu_arr) > 0:
for index, item in enumerate(self.menu_arr):
self.win.addstr((index+spacing) + y+y_off, x+x_off, item)
def add_menu_label(self, *argv):
for arg in argv:
self.menu_arr += arg
Honestly, don't know the reason, that the window isn't displaying on the screen.
When I open the game, there is only the label shown "What race are you?".
Thank you for any answers!
Edit: Here is the source code of the whole program.
https://github.com/MrEll3n/ASCIItor
I have user-adjustable annotations in a graphics scene. The size/rotation of annotations is handled by dragging corners of a rectangle about the annotation. I'm using a custom rect (instead of the boundingRect) so it follows the rotation of the parent annotation. The control corners are marked by two ellipses whose parent is the rect so transformations of rect/ellipse/annotation are seamless.
I want to detect when the cursor is over one of the corners, which corner it is, and the exact coordinates. For this task it seems that I should filter the hoverevents with the parent rect using a sceneEventFilter.
I've tried umpty zilch ways of implementing the sceneEventFilter to no avail. All events go directly to the hoverEnterEvent function. I've only found a few bits of example code that do something like this but I'm just plain stuck. btw, I'm totally self taught on Python and QT over the past 3 months, so please bear with me. I'm sure I'm missing something very basic. The code is a simplified gui with two ellipses. We're looking to capture events in the sceneEventFilter but always goes to hoverEnterEvent.
from pyqtgraph.Qt import QtGui, QtCore
import pyqtgraph as pg
from PyQt5.QtWidgets import QGraphicsScene, QGraphicsView, QGraphicsItem
import sys
class myHandle(QtGui.QGraphicsEllipseItem):
def __init__(self, parent = None):
super(myHandle, self).__init__(parent)
def addTheHandle(self, h_parent = 'null', kind = 'null'):
handle_w = 40
if kind == 'scaling handle':
handle_x = h_parent.boundingRect().topRight().x() - handle_w/2
handle_y = h_parent.boundingRect().topRight().y() - handle_w/2
if kind == 'rotation handle':
handle_x = h_parent.boundingRect().topLeft().x() - handle_w/2
handle_y = h_parent.boundingRect().topLeft().y() - handle_w/2
the_handle = QtGui.QGraphicsEllipseItem(QtCore.QRectF(handle_x, handle_y, handle_w, handle_w))
the_handle.setPen(QtGui.QPen(QtGui.QColor(255, 100, 0), 3))
the_handle.setParentItem(h_parent)
the_handle.setAcceptHoverEvents(True)
the_handle.kind = kind
return the_handle
class myRect(QtGui.QGraphicsRectItem):
def __init__(self, parent = None):
super(myRect, self).__init__(parent)
def rectThing(self, boundingrectangle):
self.setAcceptHoverEvents(True)
self.setRect(boundingrectangle)
mh = myHandle()
rotation_handle = mh.addTheHandle(h_parent = self, kind = 'rotation handle')
scaling_handle = mh.addTheHandle(h_parent = self, kind = 'scaling handle')
self.installSceneEventFilter(rotation_handle)
self.installSceneEventFilter(scaling_handle)
return self, rotation_handle, scaling_handle
def sceneEventFilter(self, event):
print('scene ev filter')
return False
def hoverEnterEvent(self, event):
print('hover enter event')
class Basic(QtGui.QMainWindow):
def __init__(self):
super(Basic, self).__init__()
self.initUI()
def eventFilter(self, source, event):
return QtGui.QMainWindow.eventFilter(self, source, event)
def exit_the_program(self):
pg.exit()
def initUI(self):
self.resize(300, 300)
self.centralwidget = QtGui.QWidget()
self.setCentralWidget(self.centralwidget)
self.h_layout = QtGui.QHBoxLayout(self.centralwidget)
self.exit_program = QtGui.QPushButton('Exit')
self.exit_program.clicked.connect(self.exit_the_program)
self.h_layout.addWidget(self.exit_program)
self.this_scene = QGraphicsScene()
self.this_view = QGraphicsView(self.this_scene)
self.this_view.setMouseTracking(True)
self.this_view.viewport().installEventFilter(self)
self.h_layout.addWidget(self.this_view)
self.circle = self.this_scene.addEllipse(QtCore.QRectF(40, 40, 65, 65), QtGui.QPen(QtCore.Qt.black))
mr = myRect()
the_rect, rotation_handle, scaling_handle = mr.rectThing(self.circle.boundingRect())
the_rect.setPen(QtGui.QPen(QtCore.Qt.black))
the_rect.setParentItem(self.circle)
self.this_scene.addItem(the_rect)
self.this_scene.addItem(rotation_handle)
self.this_scene.addItem(scaling_handle)
def main():
app = QtGui.QApplication([])
main = Basic()
main.show()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
The main problem is that you are installing the event filter of the target items on the rectangle: the event filter of the rectangle will never receive anything. Moreover, sceneEventFilter accepts two arguments (the watched item and the event), but you only used one.
What you should do is to install the event filter of the rectangle on the target items:
rotation_handle.installSceneEventFilter(self)
scaling_handle.installSceneEventFilter(self)
That said, if you want to use those ellipse items for scaling or rotation of the source circle, your approach is a bit wrong to begin with.
from math import sqrt
# ...
class myRect(QtGui.QGraphicsRectItem):
def __init__(self, parent):
super(myRect, self).__init__(parent)
self.setRect(parent.boundingRect())
# rotation is usually based on the center of an object
self.parentItem().setTransformOriginPoint(self.parentItem().rect().center())
# a rectangle that has a center at (0, 0)
handleRect = QtCore.QRectF(-20, -20, 40, 40)
self.rotation_handle = QtGui.QGraphicsEllipseItem(handleRect, self)
self.scaling_handle = QtGui.QGraphicsEllipseItem(handleRect, self)
# position the handles by centering them at the right corners
self.rotation_handle.setPos(self.rect().topLeft())
self.scaling_handle.setPos(self.rect().topRight())
for source in (self.rotation_handle, self.scaling_handle):
# install the *self* event filter on the handles
source.installSceneEventFilter(self)
source.setPen(QtGui.QPen(QtGui.QColor(255, 100, 0), 3))
def sceneEventFilter(self, source, event):
if event.type() == QtCore.QEvent.GraphicsSceneMouseMove:
# map the handle event position to the ellipse parent item; we could
# also map to "self", but using the parent is more consistent
localPos = self.parentItem().mapFromItem(source, event.pos())
if source == self.rotation_handle:
# create a temporary line to get the rotation angle
line = QtCore.QLineF(self.boundingRect().center(), localPos)
# add the current rotation to the angle between the center and the
# top left corner, then subtract the new line angle
self.parentItem().setRotation(135 + self.parentItem().rotation() - line.angle())
# note that I'm assuming that the ellipse is a circle, so the top
# left angle will always be at 135°; if it's not a circle, the
# rect width and height won't match and the angle will be
# different, so you'll need to compute that
# parentRect = self.parentItem().rect()
# oldLine = QtCore.QLineF(parentRect.center(), parentRect.topLeft())
# self.parentItem().setRotation(
# oldLine.angle() + self.parentItem().rotation() - line.angle())
elif source == self.scaling_handle:
# still assuming a perfect circle, so the rectangle is a square;
# the line from the center to the top right corner is used to
# compute the square side size, which is the double of a
# right-triangle cathetus where the hypotenuse is the line
# between the center and any of its corners;
# if the ellipse is not a perfect circle, you'll have to
# compute both of the catheti
hyp = QtCore.QLineF(self.boundingRect().center(), localPos)
size = sqrt(2) * hyp.length()
rect = QtCore.QRectF(0, 0, size, size)
rect.moveCenter(self.rect().center())
self.parentItem().setRect(rect)
self.setRect(rect)
# update the positions of both handles
self.rotation_handle.setPos(self.rect().topLeft())
self.scaling_handle.setPos(self.rect().topRight())
return True
elif event.type() == QtCore.QEvent.GraphicsSceneMousePress:
# return True to the press event (which is almost as setting it as
# accepted, so that it won't be processed further more by the scene,
# allowing the sceneEventFilter to capture the following mouseMove
# events that the watched graphics items will receive
return True
return super(myRect, self).sceneEventFilter(source, event)
class Basic(QtGui.QMainWindow):
# ...
def initUI(self):
# ...
self.circle = self.this_scene.addEllipse(QtCore.QRectF(40, 40, 65, 65), QtGui.QPen(QtCore.Qt.black))
mr = myRect(self.circle)
self.this_scene.addItem(mr)
So I'm trying to learn animations in PyQt and throughout all the examples I can find online, they all seem to use the self.update() or self.repaint() methods to increment the animations. This means basically the code has to erase then redraw the entire widget for every frame, even though much of what I intend to animate is static.
For example the code below, this generates a circular progress pie. The important bit is the paint() method under ProgressMeter (first class): for every frame in the animation this example paints the background, the actual progress pie, and the percentage indicator.
If I change the code to something like:
if self.angle > 120:
# do not draw background
then after 120 frames, the background does not get drawn anymore.
This seems terribly inefficient because (logically) the background should only be drawn once, no?
What would you recommend for animations like these?
Addendum: I have lurked a lot on this site to steal examples and code, but haven't posted for a long time. Please let me know about proper etiquette etc if I am not following it properly.
import sys
from PyQt4 import QtGui, QtCore
class ProgressMeter(QtGui.QGraphicsItem):
def __init__(self, parent):
super(ProgressMeter, self).__init__()
self.parent = parent
self.angle = 0
self.per = 0
def boundingRect(self):
return QtCore.QRectF(0, 0, self.parent.width(),
self.parent.height())
def increment(self):
self.angle += 1
self.per = int(self.angle / 3.6)
if self.angle > 360:
return False
else:
return True
def paint(self, painter, option, widget):
self.drawBackground(painter, widget)
self.drawMeter(painter, widget)
self.drawText(painter)
def drawBackground(self, painter, widget):
painter.setRenderHint(QtGui.QPainter.Antialiasing)
painter.setPen(QtCore.Qt.NoPen)
p1 = QtCore.QPointF(80, 80)
g = QtGui.QRadialGradient(p1 * 0.2, 80 * 1.1)
g.setColorAt(0.0, widget.palette().light().color())
g.setColorAt(1.0, widget.palette().dark().color())
painter.setBrush(g)
painter.drawEllipse(0, 0, 80, 80)
p2 = QtCore.QPointF(40, 40)
g = QtGui.QRadialGradient(p2, 70 * 1.3)
g.setColorAt(0.0, widget.palette().midlight().color())
g.setColorAt(1.0, widget.palette().dark().color())
painter.setBrush(g)
painter.drawEllipse(7.5, 7.5, 65, 65)
def drawMeter(self, painter, widget):
painter.setPen(QtCore.Qt.NoPen)
painter.setBrush(widget.palette().highlight().color())
painter.drawPie(7.5, 7.5, 65, 65, 0, -self.angle * 16)
def drawText(self, painter):
text = "%d%%" % self.per
font = painter.font()
font.setPixelSize(11)
painter.setFont(font)
brush = QtGui.QBrush(QtGui.QColor("#000000"))
pen = QtGui.QPen(brush, 1)
painter.setPen(pen)
# size = painter.fontMetrics().size(QtCore.Qt.TextSingleLine, text)
painter.drawText(0, 0, 80, 80,
QtCore.Qt.AlignCenter, text)
class MyView(QtGui.QGraphicsView):
def __init__(self):
super(MyView, self).__init__()
self.initView()
self.setupScene()
self.setupAnimation()
self.setGeometry(300, 150, 250, 250)
def initView(self):
self.setWindowTitle("Progress meter")
self.setRenderHint(QtGui.QPainter.Antialiasing)
policy = QtCore.Qt.ScrollBarAlwaysOff
self.setVerticalScrollBarPolicy(policy)
self.setHorizontalScrollBarPolicy(policy)
self.setBackgroundBrush(self.palette().window())
self.pm = ProgressMeter(self)
self.pm.setPos(55, 55)
def setupScene(self):
self.scene = QtGui.QGraphicsScene(self)
self.scene.setSceneRect(0, 0, 250, 250)
self.scene.addItem(self.pm)
self.setScene(self.scene)
def setupAnimation(self):
self.timer = QtCore.QTimeLine()
self.timer.setLoopCount(0)
self.timer.setFrameRange(0, 100)
self.animation = QtGui.QGraphicsItemAnimation()
self.animation.setItem(self.pm)
self.animation.setTimeLine(self.timer)
self.timer.frameChanged[int].connect(self.doStep)
self.timer.start()
def doStep(self, i):
if not self.pm.increment():
self.timer.stop()
self.pm.update()
app = QtGui.QApplication([])
view = MyView()
view.show()
sys.exit(app.exec_())
The Qt's documentation about QWidget repaint slot says:
Repaints the widget directly by calling paintEvent() immediately, unless updates are disabled or the widget is hidden.
We suggest only using repaint() if you need an immediate repaint, for example during animation. In almost all circumstances update() is better, as it permits Qt to optimize for speed and minimize flicker.
Warning: If you call repaint() in a function which may itself be called from paintEvent(), you may get infinite recursion. The update() function never causes recursion.
That should give you an answer about when to use or not repaint or update slots.
About making animations I'd suggest you also to take a look to the Qt4's animation framework or Qt5's animation framework, which is a really powerful way to animate widgets on Qt.
In my program I'm trying to map the coordinates of a mousepress back to the coordinate dimensions of an image. I'm using PyQt4 in Python. The program below demonstrates the problem. I have a widget that makes a few image transformations. After those image transformations the image is shown in the center of the widget, while maintaining the original aspect ratio of the image. Since the image is scaled and translated, a coordinate of a MouseEvent must be remapped to the coordinate system of the Image.
The program below has a class "ScalingWidget", that should be able to do these transformations and should also be able to remap the coordinate in a mouseReleaseEvent back to the coordinate system of the image. This works perfectly as expected when I show the widget outside a Layout and mainwindow, but it gets evil when I embed the widget in a bigger gui. Then the coordinates after mapping them back to the Image coordinates suddenly displays an offset.
The minimal program below can be started with and without the bug by specifing flag -b when starting the program. The option -n can put the instance of ScalingWidget deep and deeper inside a "gui", and the deeper it is embedded in the layouts the more strong the bug will be visible.
The stupid thing is, although drawing indicates that the transformations are correct, the mapped coordinates (printed in the window title and console) indicate that remapping them back to the image coordinates is screwed up when the -b flag is present.
So my question is: What am I doing wrong with remapping the mouse coordinates back to the image dimensions when my ScalingWidget is embedded in a layout?
I don't expect the remapping to be pixel perfect, but just as accurate as the end user can position the mouse. There are two points x=20, y=20 and at x=380 and y=380 these can be used as reference point.
Any help is most welcome!
#!/usr/bin/env python
from PyQt4 import QtGui
from PyQt4 import QtCore
import sys
import argparse
class ScalingWidget (QtGui.QWidget):
''' Displays a pixmap optimally in the center of the widget, in such way
the pixmap is shown in the middle
'''
white = QtGui.QColor(255,255,255)
black = QtGui.QColor( 0, 0, 0)
arcrect = QtCore.QRect(-10, -10, 20, 20)
def __init__(self):
super(ScalingWidget, self).__init__()
self.pixmap = QtGui.QPixmap(400, 400)
painter = QtGui.QPainter(self.pixmap)
painter.fillRect(self.pixmap.rect(), self.white)
self.point1 = QtCore.QPoint(20, 20)
self.point2 = QtCore.QPoint(380, 380)
painter.setPen(self.black)
painter.drawRect(QtCore.QRect(self.point1, self.point2))
painter.end()
self.matrix = None
def sizeHint(self):
return QtCore.QSize(500,400)
##
# Applies the default transformations
#
def _default_img_transform(self, painter):
#size of widget
winheight = float(self.height())
winwidth = float(self.width())
#size of pixmap
scrwidth = float(self.pixmap.width())
scrheight = float(self.pixmap.height())
assert(painter.transform().isIdentity())
if scrheight <= 0 or scrwidth <= 0:
raise RuntimeError(repr(self) + "Unable to determine Screensize")
widthr = winwidth / scrwidth
heightr = winheight / scrheight
if widthr > heightr:
translate = (winwidth - heightr * scrwidth) /2
painter.translate(translate, 0)
painter.scale(heightr, heightr)
else:
translate = (winheight - widthr * scrheight) / 2
painter.translate(0, translate)
painter.scale(widthr, widthr)
# now store the matrix used to map the mouse coordinates back to the
# coordinates of the pixmap
self.matrix = painter.deviceTransform()
def paintEvent(self, e):
painter = QtGui.QPainter(self)
painter.setClipRegion(e.region())
# fill the background of the entire widget.
painter.fillRect(self.rect(), QtGui.QColor(0,0,0))
# transform to place the image nicely in the center of the widget.
self._default_img_transform(painter)
painter.drawPixmap(self.pixmap.rect(), self.pixmap, self.pixmap.rect())
pen = QtGui.QPen(QtGui.QColor(255,0,0))
# Just draw on the points used to make the black rectangle of the pix map
# drawing is not affected, be remapping those coordinates with the "same"
# matrix is.
pen.setWidth(4)
painter.setPen(pen)
painter.save()
painter.translate(self.point1)
painter.drawPoint(0,0)
painter.restore()
painter.save()
painter.translate(self.point2)
painter.drawPoint(0,0)
painter.restore()
painter.end()
def mouseReleaseEvent(self, event):
x, y = float(event.x()), float(event.y())
inverted, invsucces = self.matrix.inverted()
assert(invsucces)
xmapped, ymapped = inverted.map(x,y)
print x, y
print xmapped, ymapped
self.setWindowTitle("mouse x,y = {}, {}, mapped x, y = {},{} "
.format(x, y, xmapped, ymapped)
)
def start_bug():
''' Displays the mouse press mapping bug.
This is a bit contrived, but in the real world
a widget is embedded in deeper in a gui
than a single widget, besides the problem
grows with the depth of embedding.
'''
app = QtGui.QApplication(sys.argv)
win = QtGui.QWidget()
layout = QtGui.QVBoxLayout()
win.setLayout(layout)
widget = None
for i in range(0, args.increase_bug):
if i < args.increase_bug-1:
widget = QtGui.QWidget()
layout.addWidget(widget)
layout= QtGui.QVBoxLayout()
widget.setLayout(layout)
else:
layout.addWidget(ScalingWidget())
win.show()
sys.exit(app.exec_())
def start_no_bug():
''' Does not show the mapping bug, the mouse event.x() and .y() map nicely back to
the coordinate system of the pixmap
'''
app = QtGui.QApplication(sys.argv)
win = ScalingWidget()
win.show()
sys.exit(app.exec_())
# parsing arguments
parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument('-b', '--display-bug', action='store_true',
help="Toggle this option to get the bugged version"
)
parser.add_argument('-n', '--increase-bug', type=int, default=1,
help="Increase the bug by n times."
)
if __name__ == "__main__":
args = parser.parse_args()
if args.display_bug:
start_bug()
else:
start_no_bug()
The basic idea of the _default_image_transform is correct. The error is in the end of the function.
def _default_img_transform(self, painter):
#size of widget
winheight = float(self.height())
winwidth = float(self.width())
#size of pixmap
scrwidth = float(self.pixmap.width())
scrheight = float(self.pixmap.height())
assert(painter.transform().isIdentity())
if scrheight <= 0 or scrwidth <= 0:
raise RuntimeError(repr(self) + "Unable to determine Screensize")
widthr = winwidth / scrwidth
heightr = winheight / scrheight
if widthr > heightr:
translate = (winwidth - heightr * scrwidth) /2
painter.translate(translate, 0)
painter.scale(heightr, heightr)
else:
translate = (winheight - widthr * scrheight) / 2
painter.translate(0, translate)
painter.scale(widthr, widthr)
# now store the matrix used to map the mouse coordinates back to the
# coordinates of the pixmap
self.matrix = painter.deviceTransform() ## <-- error is here
The last line of the function _default_image_transform should be:
self.matrix = painter.transform()
According to the documentation one should only call the QPainter.deviceTransform() when you are working with QT::HANDLE which is an platform dependent handle. Since I wasn't working with a platform dependent handle I shouldn't have called it. It works out when I show the widget, but not when it is embedded in a layout. Then the deviceTransform matrix is different from the normal QPainter.transform() matrix. See also http://doc.qt.io/qt-4.8/qpainter.html#deviceTransform
I am trying to make a simple game with pyglet, and it has to include an intro screen. Unfortunately, it's been proving more difficult than I expected.
The following code is a simpler version of what I am trying to do.
import pyglet
from game import intro
game_window = pyglet.window.Window(800, 600)
intro.play(game_window)
#game_window.event
def on_draw():
game_window.clear()
main_batch.draw()
def update(dt):
running = True
if __name__ == '__main__':
pyglet.clock.schedule_interval(update, 1/120.0)
main_batch = pyglet.graphics.Batch()
score_label = pyglet.text.Label(text = 'RUNNING GAME', x = 400, y = 200, batch=main_batch)
pyglet.app.run()
Where game/intro.py has the following written in it:
import pyglet
from time import sleep
def play(game_window):
game_window.clear()
studio = pyglet.text.Label('foo studios', font_size=36, font_name='Arial', x=400, y=300)
studio.draw()
sleep(5)
This opens a window (the intro window) and waits 5 seconds, after which the message "RUNNING GAME" appears, but the "foo studios" message does not appear.
Clearly I am doing something wrong.
I am not very experienced with pyglet, but I managed to get the game running (needs a bit of tweaking, but it's essentially done). All I need left is the intro screen.
If anyone knows a good way of doing an intro screen (just with text, I don't need any animations of music for now), I would be very grateful.
You're better off creating classes based on for instance pyglet.sprite.Sprite and using those objects as "windows" or "screens".
Feels like i'm pasting this code everywhere but use this, and in "def render()` put the different "scenarios"/"windows" you'd wish to be rendered at the time.
import pyglet
from time import time, sleep
class Window(pyglet.window.Window):
def __init__(self, refreshrate):
super(Window, self).__init__(vsync = False)
self.frames = 0
self.framerate = pyglet.text.Label(text='Unknown', font_name='Verdana', font_size=8, x=10, y=10, color=(255,255,255,255))
self.last = time()
self.alive = 1
self.refreshrate = refreshrate
def on_draw(self):
self.render()
def render(self):
self.clear()
if time() - self.last >= 1:
self.framerate.text = str(self.frames)
self.frames = 0
self.last = time()
else:
self.frames += 1
self.framerate.draw()
self.flip()
def on_close(self):
self.alive = 0
def run(self):
while self.alive:
self.render()
event = self.dispatch_events() # <-- This is the event queue
sleep(1.0/self.refreshrate)
win = Window(23) # set the fps
win.run()
What does it is the fact that you have a rendering function that clears and flips the entire graphical memory X times per second and you descide which objects are included in that render perior in the render function.
Try it out and see if it helps.
Here is a example using the above example, it consists of 3 things:
* A main window
* A Intro screen
* A Menu screen
You can ignore class Spr() and def convert_hashColor_to_RGBA(), these are mere helper functions to avoid repetative code further down.
I will also go ahead and mark the important bits that actually do things, the rest are just initation-code or positioning things.
import pyglet
from time import time, sleep
__WIDTH__ = 800
__HEIGHT__ = 600
def convert_hashColor_to_RGBA(color):
if '#' in color:
c = color.lstrip("#")
c = max(6-len(c),0)*"0" + c
r = int(c[:2], 16)
g = int(c[2:4], 16)
b = int(c[4:], 16)
color = (r,g,b,255)
return color
class Spr(pyglet.sprite.Sprite):
def __init__(self, texture=None, width=__WIDTH__, height=__HEIGHT__, color='#000000', x=0, y=0):
if texture is None:
self.texture = pyglet.image.SolidColorImagePattern(convert_hashColor_to_RGBA(color)).create_image(width,height)
else:
self.texture = texture
super(Spr, self).__init__(self.texture)
## Normally, objects in graphics have their anchor in the bottom left corner.
## This means that all X and Y cordinates relate to the bottom left corner of
## your object as positioned from the bottom left corner of your application-screen.
##
## We can override this and move the anchor to the WIDTH/2 (aka center of the image).
## And since Spr is a class only ment for generating a background-image to your "intro screen" etc
## This only affects this class aka the background, so the background gets positioned by it's center.
self.image.anchor_x = self.image.width / 2
self.image.anchor_y = self.image.height / 2
## And this sets the position.
self.x = x
self.y = y
def _draw(self):
self.draw()
## IntoScreen is a class that inherits a background, the background is Spr (our custom background-image class)
## IntoScreen contains 1 label, and it will change it's text after 2 seconds of being shown.
## That's all it does.
class IntroScreen(Spr):
def __init__(self, texture=None, width=300, height = 150, x = 10, y = 10, color='#000000'):
super(IntroScreen, self).__init__(texture, width=width, height=height, x=x, y=y, color=color)
self.intro_text = pyglet.text.Label('Running game', font_size=8, font_name=('Verdana', 'Calibri', 'Arial'), x=x, y=y, multiline=False, width=width, height=height, color=(100, 100, 100, 255), anchor_x='center')
self.has_been_visible_since = time()
def _draw(self): # <-- Important, this is the function that is called from the main window.render() function. The built-in rendering function of pyglet is called .draw() so we create a manual one that's called _draw() that in turn does stuff + calls draw(). This is just so we can add on to the functionality of Pyglet.
self.draw()
self.intro_text.draw()
if time() - 2 > self.has_been_visible_since:
self.intro_text.text = 'foo studios'
## Then we have a MenuScreen (with a red background)
## Note that the RED color comes not from this class because the default is black #000000
## the color is set when calling/instanciating this class further down.
##
## But all this does, is show a "menu" (aka a text saying it's the menu..)
class MenuScreen(Spr):
def __init__(self, texture=None, width=300, height = 150, x = 10, y = 10, color='#000000'):
super(MenuScreen, self).__init__(texture, width=width, height=height, x=x, y=y, color=color)
self.screen_text = pyglet.text.Label('Main menu screen', font_size=8, font_name=('Verdana', 'Calibri', 'Arial'), x=x, y=y+height/2-20, multiline=False, width=300, height=height, color=(100, 100, 100, 255), anchor_x='center')
def _draw(self):
self.draw()
self.screen_text.draw()
## This is the actual window, the game, the glory universe that is graphics.
## It will be blank, so you need to set up what should be visible when and where.
##
## I've creates two classes which can act as "screens" (intro, game, menu etc)
## And we'll initate the Window class with the IntroScreen() and show that for a
## total of 5 seconds, after 5 seconds we will swap it out for a MenuScreeen().
##
## All this magic is done in __init__() and render(). All the other functions are basically
## just "there" and executes black magic for your convencience.
class Window(pyglet.window.Window):
def __init__(self, refreshrate):
super(Window, self).__init__(vsync = False)
self.alive = 1
self.refreshrate = refreshrate
self.currentScreen = IntroScreen(x=320, y=__HEIGHT__/2, width=50) # <-- Important
self.screen_has_been_shown_since = time()
def on_draw(self):
self.render()
def on_key_down(self, symbol, mod):
print('Keyboard down:', symbol) # <-- Important
def render(self):
self.clear()
if time() - 5 > self.screen_has_been_shown_since and type(self.currentScreen) is not MenuScreen: # <-- Important
self.currentScreen = MenuScreen(x=320, y=__HEIGHT__-210, color='#FF0000') # <-- Important, here we switch screen (after 5 seconds)
self.currentScreen._draw() # <-- Important, draws the current screen
self.flip()
def on_close(self):
self.alive = 0
def run(self):
while self.alive:
self.render()
event = self.dispatch_events()
sleep(1.0/self.refreshrate)
win = Window(23) # set the fps
win.run()