2D clickable surface in Pyside2 - python

I'm writing a simple GUI interface for a project using PySide2.
I'm using the typical MVC design pattern, for the sake of clarity I will just post the code of my GUI (without controller and support methods ecc...)
Here's the code:
from PySide2.QtWidgets import *
from PySide2.QtWidgets import QSizePolicy
from PySide2.QtGui import QIcon
from PySide2.QtWidgets import (QPushButton, QMainWindow)
class myView(QMainWindow):
def __init__(self, parent=None):
"""View initializer."""
#Creates blank view of a given size
super().__init__()
# Set some main window's properties
self.setWindowTitle('8D.me')
self.setFixedSize(800, 500) # Block user resize of the window
self.setIcon()
self.generalLayout = QHBoxLayout() #Layout generale
self.button = QPushButton('test3',self)
self.button.setSizePolicy(
QSizePolicy.Preferred,
QSizePolicy.Expanding)
self.generalLayout.addWidget(QPushButton('test2',self),1)
self.generalLayout.addWidget(self.button,3)
# Set the central widget
self._centralWidget = QWidget(self) #creates a QWidget object to play the role of a central widget. Remember that since your GUI class inherits from QMainWindow, you need a central widget. This object will be the parent for the rest of the GUI component.
self.setCentralWidget(self._centralWidget)
self._centralWidget.setLayout(self.generalLayout)
# Insert methods for creating/adding elements to the default view.
# Mehods....
def setIcon(self):
appIcon = QIcon('logo')
self.setWindowIcon(appIcon)
#Insert here the public methods called by the Controller to update the view...
My GUI right no is pretty simple and looks like this:
What I would like to do is change the test 3 button and insert a 2D clickable surface.
More in details, I would like to be able to click anywhere on this surface and get the position of the mouse click.
Basically I would like to create a 2D xy axis and retrieve the coordinates of my mouse click, something like this:
And then if I click at position (1,1) I wll print something like "You clicked at (1,1) on the axis", pretty simple.
I looked around for examples, tutorials and documentation, but I didn't find any proper tool to create what I wanted.
Is there any class inside the PySide2 package that could help me?

If you took literally that your goal is to get the X-Y plane in your image then a possible solution is to use a QGraphicsView:
import math
import sys
from PySide2.QtCore import Signal, QPointF
from PySide2.QtGui import QColor, QPainterPath
from PySide2.QtWidgets import (
QApplication,
QGraphicsScene,
QGraphicsView,
QHBoxLayout,
QMainWindow,
QPushButton,
QWidget,
)
class GraphicsScene(QGraphicsScene):
clicked = Signal(QPointF)
def drawBackground(self, painter, rect):
l = min(rect.width(), rect.height()) / 30
x_left = QPointF(rect.left(), 0)
x_right = QPointF(rect.right(), 0)
painter.drawLine(x_left, x_right)
right_triangle = QPainterPath()
right_triangle.lineTo(-0.5 * math.sqrt(3) * l, 0.5 * l)
right_triangle.lineTo(-0.5 * math.sqrt(3) * l, -0.5 * l)
right_triangle.closeSubpath()
right_triangle.translate(x_right)
painter.setBrush(QColor("black"))
painter.drawPath(right_triangle)
y_top = QPointF(0, rect.top())
y_bottom = QPointF(0, rect.bottom())
painter.drawLine(y_top, y_bottom)
top_triangle = QPainterPath()
top_triangle.lineTo(.5*l, -0.5 * math.sqrt(3) * l)
top_triangle.lineTo(-.5*l, -0.5 * math.sqrt(3) * l)
top_triangle.closeSubpath()
top_triangle.translate(y_bottom)
painter.setBrush(QColor("black"))
painter.drawPath(top_triangle)
def mousePressEvent(self, event):
sp = event.scenePos()
self.clicked.emit(sp)
super().mousePressEvent(event)
class MyView(QMainWindow):
def __init__(self, parent=None):
super().__init__(parent)
self.setWindowTitle("8D.me")
self.setFixedSize(800, 500)
self.btn = QPushButton("test2")
self.view = QGraphicsView()
self.view.scale(1, -1)
self.scene = GraphicsScene()
self.view.setScene(self.scene)
central_widget = QWidget()
self.setCentralWidget(central_widget)
layout = QHBoxLayout(central_widget)
layout.addWidget(self.btn)
layout.addWidget(self.view)
self.scene.clicked.connect(self.handle_clicked)
def handle_clicked(self, p):
print("clicked", p.x(), p.y())
if __name__ == "__main__":
app = QApplication(sys.argv)
w = MyView()
w.show()
sys.exit(app.exec_())

Related

Painting with PYQT - MouseEvent uses wrong coordinates

My Problem is: The coordinates where my mouse is and where the painting starts is wrong.
The Canvas starts on the top left corner (0,0) but the label are on somewhere at 250,500(Because ive done this with the designer)
so if draw i have to draw outside the label to get something in the canvas :( I didnt find the problem.
Here is my Code:
import sys
import os
from PyQt5 import QtCore, QtGui, QtWidgets, uic
from PyQt5.QtGui import QIcon, QImage, QPen, QPainter, QColor
from PyQt5.QtCore import Qt
from ui.main import Ui_MainWindow
class MainWindow(QtWidgets.QMainWindow):
def __init__(self, parent = None):
super().__init__(parent)
self.main = Ui_MainWindow()
self.main.setupUi(self)
path = "G:/SoftwareDevelopement/SignInPrototyp/signin.png"
self.main.labelSign.setPixmap(QtGui.QPixmap(path))
#self.main.labelSign.move(250,410)
self.drawing = False
self.brushSize = 2
self.brushColor = Qt.black
self.brushStyle = Qt.SolidLine
self.brushCap = Qt.RoundCap
self.brushJoin = Qt.RoundJoin
self.last_x, self.last_y = None, None
def mouseMoveEvent(self, e):
print(e.x)
print(e.y)
if self.last_x is None: # First event.
self.last_x = e.x()
self.last_y = e.y()
return # Ignore the first time.
painter = QtGui.QPainter(self.main.labelSign.pixmap())
painter.setPen(QPen(self.brushColor, self.brushSize, self.brushStyle, self.brushCap, self.brushJoin))
painter.drawLine(self.last_x, self.last_y, e.x(), e.y())
painter.end()
self.update()
# Update the origin for next time.
self.last_x = e.x()
self.last_y = e.y()
def mouseReleaseEvent(self, e):
self.last_x = None
self.last_y = None
# Open and Exit main Window
if __name__ == "__main__":
global app
global window
# SCALE TO ALL RESOLUTIONS! 1
os.environ["QT_AUTO_SCREEN_SCALE_FACTOR"] = "1"
# AND THIS
app.setAttribute(QtCore.Qt.AA_EnableHighDpiScaling)
window = MainWindow()
window.raise_()
window.showMinimized()
#window.show()
window.showNormal()
window.activateWindow()
sys.exit(app.exec_())
######################!
This dont work, i have to draw on the left top corner to get inside the white
Point 0,0 of window is at top left corner of window, point 0,0 of label is at top left corner of label, you use window coordinates to draw on label (you overriden window event not label event), this two coordinate systems are not equal and to go from one to another you need to translate coordinates. For example like this:
offset = self.main.labelSign.pos()
painter.drawLine(self.last_x - offset.x(), self.last_y - offset.y(), e.x() - offset.x(), e.y() - offset.y())
You can make your drawable label self contained by creating class that inherits from QLabel and overrides mouseMoveEvent and mouseReleaseEvent, this way you dont need to translate coordinates.
so i got after long trial and error and solution:
painter = QtGui.QPainter(self.main.labelSign.pixmap())
painter_map = self.main.labelSign.geometry()
painter.setWindow(painter_map
simple remap.

pyside/pyqt how to animate an arc simply?

I'm looking for a solution, to animate this arc from 0 - 360°. I'm relative new to Pyside/Pyqt and I don't find such a simple solution (only beginner "unfriedly"). I tried it with while loops aswell, but it doesn't works. At the moment I don't understand this animation system, but I want to work on it.
import sys
from PySide6 import QtCore
from PySide6.QtWidgets import QApplication, QMainWindow
from PySide6.QtCore import Qt
from PySide6.QtGui import QBrush, QPen, QPainter
class MainWindow(QMainWindow):
def __init__(self):
super(MainWindow, self).__init__()
self.setWindowTitle("AnimateArc")
self.setGeometry(100, 100, 600, 600)
def paintEvent(self, event):
self.anim = QtCore.QPropertyAnimation(self, b"width", duration=1000) #<- is there a documentation for b"width", b"geometry"?
self.anim.setStartValue(0)
start = 0
painter = QPainter(self)
painter.setPen(QPen(Qt.black, 5, Qt.SolidLine))
painter.drawArc(100, 100, 400, 400, 90 * 16, start * 16) # I want to make the change dynamicly
self.anim.setEndValue(360)
if __name__ == '__main__':
app = QApplication(sys.argv)
w = MainWindow()
w.show()
app.exec()
QPropertyAnimation is used to animate Qt properties of any QObject. If you refer to self (the current instance of QMainWindow), then you can animate all properties of a QMainWindow and all inherited properties (QMainWindow inherits from QWidget, so you can animate all the QWidget properties as well).
In your case, you're trying to animate the width property of the window, and that's certainly not what you want to do.
Since what you want to change is a value that is not a property of the window, you cannot use QPropertyAnimation (unless you create a Qt property using the #Property decorator), and you should use a QVariantAnimation instead.
Then, a paintEvent is called by Qt every time the widget is going to be drawn (which can happen very often), so you cannot create the animation there, otherwise you could end up with a recursion: since the animation would require a repaint, you would create a new animation everytime the previous requires an update.
Finally, consider that painting on a QMainWindow is normally discouraged, as a Qt main window is a special kind of QWidget intended for advanced features (menus, status bar, etc) and uses a central widget to show the actual contents.
The correct approach is to create and set a central widget, and implement the painting on that widget instead.
Here is a revised and working version of your code:
class ArcWidget(QWidget):
def __init__(self):
super().__init__()
self.anim = QtCore.QVariantAnimation(self, duration=1000)
self.anim.setStartValue(0)
self.anim.setEndValue(360)
self.anim.valueChanged.connect(self.update)
self.anim.start()
def paintEvent(self, event):
painter = QPainter(self)
painter.setPen(QPen(Qt.black, 5, Qt.SolidLine))
painter.drawArc(
100, 100, 400, 400, 90 * 16, self.anim.currentValue() * 16)
class MainWindow(QMainWindow):
def __init__(self):
super(MainWindow, self).__init__()
self.setWindowTitle("AnimateArc")
self.setGeometry(100, 100, 600, 600)
self.arcWidget = ArcWidget()
self.setCentralWidget(self.arcWidget)
The valueChanged connection ensures that everytime the value changes the widget schedules an update (thus calling a paintEvent as soon as the event queue allows it), then you can use the current value of the animation to draw the actual arc.
Thanks #musicamante for the solution regarding animating arc using QAnimationProperty
Modified #musicmante code to create a loading effect guess it will help developers and might save their time who are trying to make loading effect using Qt
Source
#!/usr/bin/env python3.10
import sys
import string
import random
from PySide6.QtWidgets import (QMainWindow, QPushButton, QVBoxLayout,
QApplication, QWidget)
from PySide6.QtCore import (Qt, QVariantAnimation)
from PySide6.QtGui import (QPen, QPainter, QColor)
class Arc:
colors = list(string.ascii_lowercase[0:6]+string.digits)
shades_of_blue = ["#7CB9E8","#00308F","#72A0C1", "#F0F8FF",
"#007FFF", "#6CB4EE", "#002D62", "#5072A7",
"#002244", "#B2FFFF", "#6F00FF", "#7DF9FF","#007791",
"#ADD8E6", "#E0FFFF", "#005f69", "#76ABDF",
"#6A5ACD", "#008080", "#1da1f2", "#1a1f71", "#0C2340"]
shades_of_green = ['#32CD32', '#CAE00D', '#9EFD38', '#568203', '#93C572',
'#8DB600', '#708238', '#556B2F', '#014421', '#98FB98', '#7CFC00',
'#4F7942', '#009E60', '#00FF7F', '#00FA9A', '#177245', '#2E8B57',
'#3CB371', '#A7F432', '#123524', '#5E8C31', '#90EE90', '#03C03C',
'#66FF00', '#006600', '#D9E650']
def __init__(self):
self.diameter = random.randint(100, 600)
#cols = list(Arc.colors)
#random.shuffle(cols)
#_col = "#"+''.join(cols[:6])
#print(f"{_col=}")
#self.color = QColor(_col)
#self.color = QColor(Arc.shades_of_blue[random.randint(0, len(Arc.shades_of_blue)-1)])
self.color = QColor(Arc.shades_of_green[random.randint(0, len(Arc.shades_of_green)-1)])
#print(f"{self.color=}")
self.span = random.randint(40, 150)
self.direction = 1 if random.randint(10, 15)%2 == 0 else -1
self.startAngle = random.randint(40, 200)
self.step = random.randint(100, 300)
class ArcWidget(QWidget):
def __init__(self):
super().__init__()
self.initUI()
self.arcs = [Arc() for i in range(random.randint(10, 20))]
self.startAnime()
def initUI(self):
#self.setAutoFillBackground(True)
self.setAttribute(Qt.WA_StyledBackground, True)
self.setStyleSheet("background-color:black;")
def startAnime(self):
self.anim = QVariantAnimation(self, duration = 2000)
self.anim.setStartValue(0)
self.anim.setEndValue(360)
self.anim.valueChanged.connect(self.update)
self.anim.start()
def paintEvent(self, event):
painter = QPainter(self)
painter.setRenderHint(QPainter.Antialiasing)
#painter.setPen(QPen(QColor("#b6faec"), 5, Qt.SolidLine))
#painter.drawArc(
# 100, 100, 400, 400, 90*16,
# self.anim.currentValue() * 16)
#width = 400
#height = 400
#painter.drawArc(self.width()/2 -width/2, self.height()/2 - height/2, 400, 400, self.anim.currentValue()*16, 45*16)
for arc in self.arcs:
painter.setPen(QPen(arc.color, 6, Qt.SolidLine))
painter.drawArc(self.width()/2 - arc.diameter/2,
self.height()/2 - arc.diameter/2, arc.diameter,
arc.diameter, self.anim.currentValue()*16*arc.direction+arc.startAngle*100, arc.span*16)
#print(f"currentValue : {self.anim.currentValue()}")
#arc.startAngle = random.randint(50, 200)
if self.anim.currentValue() == 360:
#print("360")
self.startAnime()
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("Animate Arc")
self.setGeometry(100, 100, 600, 600)
self.arcWidget = ArcWidget()
self.setCentralWidget(self.arcWidget)
if __name__ == "__main__":
app = QApplication(sys.argv)
mainWindow = MainWindow()
mainWindow.show()
app.exec()
output:
$ ./arc_widget.py

Recursion Error on PyQt QStackedWidget when using enterEvent and leaveEvent

I am using a QStackedWidget which has its own enterEvent and leaveEvent. When I move my mouse to the QStackedWidget the enterEvent sets the current index to 1 and on the leaveEvent it sets the current index to 0 so that a different widget is shown on mouse enter and mouse leave in the area of QStackedWidget. It does what I want only if I quickly move my mouse in and out, if I place my mouse too long in the area I get RecursionError: maximum recursion depth exceeded.
Is this because the widgets are changing so fast that the internal stack can't keep up? My question is "How can I make sure this error doesn't occur? I want to display one widget as long as the mouse is over the QStackedWidget and when it is not I want to display the original widget."
The following is the code that I modified (Original Source used buttons to set the index and it is PyQt4)
import sys
from PyQt5.QtWidgets import *
from PyQt5.QtCore import QTimeLine
from PyQt5.QtGui import *
class FaderWidget(QWidget):
def __init__(self, old_widget, new_widget):
QWidget.__init__(self, new_widget)
self.old_pixmap = QPixmap(new_widget.size())
old_widget.render(self.old_pixmap)
self.pixmap_opacity = 1.0
self.timeline = QTimeLine()
self.timeline.valueChanged.connect(self.animate)
self.timeline.finished.connect(self.close)
self.timeline.setDuration(333)
self.timeline.start()
self.resize(new_widget.size())
self.show()
def animate(self, value):
self.pixmap_opacity = 1.0 - value
self.repaint()
class StackedWidget(QStackedWidget):
def __init__(self, parent = None):
QStackedWidget.__init__(self, parent)
def setCurrentIndex(self, index):
self.fader_widget = FaderWidget(self.currentWidget(), self.widget(index))
super().setCurrentIndex(index)
def enterEvent(self,event):
self.setCurrentIndex(1)
def leaveEvent(self,event):
self.setCurrentIndex(0)
if __name__ == "__main__":
app = QApplication(sys.argv)
window = QWidget()
stack = StackedWidget()
cal=QCalendarWidget()
stack.addWidget(cal)
editor = QTextEdit()
editor.setPlainText("Hello world! "*100)
stack.addWidget(editor)
layout = QGridLayout(window)
layout.addWidget(stack, 0, 0, 1, 2)
window.show()
sys.exit(app.exec_())
The recursion occurs because when you start the FaderWidget it changes focus and enterEvent is called again which creates a new FaderWidget.
The solution is to verify that the old index is different from the new index to just create the FadeWidget:
import sys
from PyQt5.QtCore import QTimeLine
from PyQt5.QtGui import QPainter, QPixmap
from PyQt5.QtWidgets import (
QApplication,
QCalendarWidget,
QGridLayout,
QStackedWidget,
QTextEdit,
QWidget,
)
class FaderWidget(QWidget):
def __init__(self, old_widget, new_widget):
QWidget.__init__(self, new_widget)
self.pixmap_opacity = 1.0
self.old_pixmap = QPixmap(new_widget.size())
old_widget.render(self.old_pixmap)
self.timeline = QTimeLine()
self.timeline.valueChanged.connect(self.animate)
self.timeline.finished.connect(self.close)
self.timeline.setDuration(333)
self.timeline.start()
self.resize(new_widget.size())
self.show()
def paintEvent(self, event):
painter = QPainter(self)
painter.setOpacity(self.pixmap_opacity)
painter.drawPixmap(0, 0, self.old_pixmap)
def animate(self, value):
self.pixmap_opacity = 1.0 - value
self.update()
class StackedWidget(QStackedWidget):
def setCurrentIndex(self, index):
if self.currentIndex() != index:
self.fader_widget = FaderWidget(self.currentWidget(), self.widget(index))
super().setCurrentIndex(index)
def enterEvent(self, event):
self.setCurrentIndex(1)
def leaveEvent(self, event):
self.setCurrentIndex(0)
if __name__ == "__main__":
app = QApplication(sys.argv)
window = QWidget()
stack = StackedWidget()
cal = QCalendarWidget()
stack.addWidget(cal)
editor = QTextEdit()
editor.setPlainText("Hello world! " * 100)
stack.addWidget(editor)
layout = QGridLayout(window)
layout.addWidget(stack, 0, 0, 1, 2)
window.show()
sys.exit(app.exec_())

QVariantAnimation of opacity : the item appears at the end of the animation

here is an example where the graphicsitem appears at the end of the animation, and not progressively, as it should.
import sys
from PyQt5 import QtWidgets, QtCore
from PyQt5.QtCore import Qt, QEasingCurve
from PyQt5.QtGui import QPixmap
from PyQt5.QtWidgets import QMainWindow, QGraphicsScene, QGraphicsView, QGraphicsPixmapItem, QGraphicsItem
class QStone(QGraphicsPixmapItem):
def __init__(self):
QGraphicsPixmapItem.__init__(self)
white = QPixmap("white2.png")
self.setPixmap(white.scaled(60, 60, Qt.KeepAspectRatio))
self.w = self.boundingRect().width()
self.h = self.boundingRect().height()
class QBoard(QGraphicsView):
def __init__(self,scene):
QGraphicsView.__init__(self)
self.scene=scene
self.setScene(scene)
def display_stone(self, x, y):
stone = QStone()
stone.setZValue(10)
stone.setOpacity(0)
stone.setPos(x - stone.w / 2, y - stone.h / 2)
self.scene.addItem(stone)
animation = QtCore.QVariantAnimation(self.scene)
animation.setDuration(3000)
animation.valueChanged.connect(stone.setOpacity)
# animation.setStartValue(0)
# animation.setEndValue(1)
animation.setParent(self.scene)
animation.setEasingCurve(QEasingCurve.BezierSpline)
animation.start()
class MainWindow(QMainWindow):
def __init__(self):
#all the usual stuff
QMainWindow.__init__(self)
centralWidget = QtWidgets.QWidget(self)
self.setCentralWidget(centralWidget)
mainLayout = QtWidgets.QGridLayout()
centralWidget.setLayout(mainLayout)
self.scene = QGraphicsScene()
self.view = QBoard(self.scene)
mainLayout.addWidget(self.view,0,0)
self.scene.setSceneRect(-200.0,-150.0,400.0,300.0)
self.view.display_stone(0,0)
app = QtWidgets.QApplication(sys.argv)
main_win = MainWindow()
main_win.show()
sys.exit(app.exec_())
instead of white2.png, plz put any image file.
any idea why it works like this?
all is said, I could also use QPropertyAnimation but it is more work for maybe the same result.
QVariantAnimation generates an animation using the type of data deducted by the value passed in startValue and endValue, in your case by not placing it that implies using integer, or placing 0 and 1 that is the same makes integer values be used in the interpolation. What integer values can be interpolated between 0 and 1? because only 0 and 1, for example for t = 0.5 * T, the opacity value should be 0.5 considering if it is linear but how to use integers then the rounding set it to 0, and it will only be visible when t = T. The solution is to pass it as startValue at 0.0 and endValue at 1.0.
animation = QtCore.QVariantAnimation(self.scene)
animation.setDuration(3000)
animation.valueChanged.connect(stone.setOpacity)
animation.setStartValue(0.0) # <---
animation.setEndValue(1.0) # <---
animation.setParent(self.scene)
animation.setEasingCurve(QEasingCurve.BezierSpline)
animation.start()

QGraphicsItem line width / transformation changes from PyQt4 to PyQt5

I have some small utility GUIs that I have made with PyQt, which use a QGraphicsScene with some items in it, and a view that response to user clicks (for making boxes, selecting points, etc).
On the (offline) machine I use, the software was just upgraded from Anaconda 2.5 to Anaconda 4.3, including the switch from PyQt4 to PyQt5. Everything still works, except somehow the transformations for my various QGraphicsItem objects are messed up if the scene rect is defined in anything but pixel coordinates.
Upfront question: What changed as far as item transformations from PyQt4 to PyQt5?
Here is an example of what I'm talking about: The top row is a box selector containing a dummy grayscale in a scene with a bounding rect of (0, 0, 2pi, 4pi). The green box is a QGraphicsRectItem drawn by the user from which I get the LL and UR points (in scene coordinates) after "Done" is clicked. The bottom row is a point layout with a user-clicked ellipse, on a small dummy image that has been zoomed in by 20.
The left and right were made with identical code. The version on the left is the result using PyQt4 under Anaconda 2.5 Python 3.5, whereas the result on the right is using PyQt5 under Anaconda 4.3 Python 3.6.
Clearly there is some sort of item transformation that is handled differently but I haven't been able to find it in any of the PyQt4->PyQt5 documentation (it's all about the API changes).
How do I go about making the line width of the QGraphicsItems be one in device coordinates while still maintaining the correct positions in scene coordinates? More generally, how do I scale a general QGraphicsItem so that it doesn't blow up or get fat based on scene size?
The code is below. SimpleDialog is the primary base class I use for various picker utilities, and it includes MouseView and ImageScene which automatically build in a vertical flip and a background image. The two utilities I used here are BoxSelector and PointLayout.
# Imports
import numpy as np
try:
from PyQt5.QtCore import Qt, pyqtSignal, pyqtSlot, QPointF
from PyQt5.QtGui import QImage, QPixmap, QFont, QBrush, QPen, QTransform
from PyQt5.QtWidgets import (QGraphicsView, QGraphicsScene, QDialog, QSizePolicy,
QVBoxLayout, QPushButton, QMainWindow, QApplication)
except ImportError:
from PyQt4.QtCore import Qt, pyqtSignal, pyqtSlot, QPointF
from PyQt4.QtGui import (QImage, QPixmap, QFont, QBrush, QPen, QTransform,
QGraphicsView, QGraphicsScene, QDialog, QSizePolicy,
QVBoxLayout, QPushButton, QMainWindow, QApplication)
class MouseView(QGraphicsView):
"""A subclass of QGraphicsView that returns mouse click events."""
mousedown = pyqtSignal(QPointF)
mouseup = pyqtSignal(QPointF)
mousemove = pyqtSignal(QPointF)
def __init__(self, scene, parent=None):
super(MouseView, self).__init__(scene, parent=parent)
self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.setSizePolicy(QSizePolicy.Fixed,
QSizePolicy.Fixed)
self.scale(1, -1)
self.moving = False
def mousePressEvent(self, event):
"""Emit a mouse click signal."""
self.mousedown.emit(self.mapToScene(event.pos()))
def mouseReleaseEvent(self, event):
"""Emit a mouse release signal."""
self.mouseup.emit(self.mapToScene(event.pos()))
def mouseMoveEvent(self, event):
"""Emit a mouse move signal."""
if self.moving:
self.mousemove.emit(self.mapToScene(event.pos()))
class ImageScene(QGraphicsScene):
"""A subclass of QGraphicsScene that includes a background pixmap."""
def __init__(self, data, scene_rect, parent=None):
super(ImageScene, self).__init__(parent=parent)
bdata = ((data - np.min(data)) / (np.max(data) - np.min(data)) * 255).astype(np.uint8)
wid, hgt = data.shape
img = QImage(bdata.T.copy(), wid, hgt, wid, QImage.Format_Indexed8)
self.setSceneRect(*scene_rect)
px = QPixmap.fromImage(img)
self.px = self.addPixmap(px)
px_trans = QTransform.fromTranslate(scene_rect[0], scene_rect[1])
px_trans = px_trans.scale(scene_rect[2]/wid, scene_rect[3]/hgt)
self.px.setTransform(px_trans)
class SimpleDialog(QDialog):
"""A base class for utility dialogs using a background image in scene."""
def __init__(self, data, bounds=None, grow=[1.0, 1.0], wsize=None, parent=None):
super(SimpleDialog, self).__init__(parent=parent)
self.grow = grow
wid, hgt = data.shape
if bounds is None:
bounds = [0, 0, wid, hgt]
if wsize is None:
wsize = [wid, hgt]
vscale = [grow[0]*wsize[0]/bounds[2], grow[1]*wsize[1]/bounds[3]]
self.scene = ImageScene(data, bounds, parent=self)
self.view = MouseView(self.scene, parent=self)
self.view.scale(vscale[0], vscale[1])
quitb = QPushButton("Done")
quitb.clicked.connect(self.close)
lay = QVBoxLayout()
lay.addWidget(self.view)
lay.addWidget(quitb)
self.setLayout(lay)
def close(self):
self.accept()
class BoxSelector(SimpleDialog):
"""Simple box selector."""
def __init__(self, *args, **kwargs):
super(BoxSelector, self).__init__(*args, **kwargs)
self.rpen = QPen(Qt.green)
self.rect = self.scene.addRect(0, 0, 0, 0, pen=self.rpen)
self.view.mousedown.connect(self.start_box)
self.view.mouseup.connect(self.end_box)
self.view.mousemove.connect(self.draw_box)
self.start_point = []
self.points = []
self.setWindowTitle('Box Selector')
#pyqtSlot(QPointF)
def start_box(self, xy):
self.start_point = [xy.x(), xy.y()]
self.view.moving = True
#pyqtSlot(QPointF)
def end_box(self, xy):
lx = np.minimum(xy.x(), self.start_point[0])
ly = np.minimum(xy.y(), self.start_point[1])
rx = np.maximum(xy.x(), self.start_point[0])
ry = np.maximum(xy.y(), self.start_point[1])
self.points = [[lx, ly], [rx, ry]]
self.view.moving = False
#pyqtSlot(QPointF)
def draw_box(self, xy):
newpoint = [xy.x(), xy.y()]
minx = np.minimum(self.start_point[0], newpoint[0])
miny = np.minimum(self.start_point[1], newpoint[1])
size = [np.abs(i - j) for i, j in zip(self.start_point, newpoint)]
self.rect.setRect(minx, miny, size[0], size[1])
class PointLayout(SimpleDialog):
"""Simple point display."""
def __init__(self, *args, **kwargs):
super(PointLayout, self).__init__(*args, **kwargs)
self.pen = QPen(Qt.green)
self.view.mousedown.connect(self.mouse_click)
self.circles = []
self.points = []
self.setWindowTitle('Point Layout')
#pyqtSlot(QPointF)
def mouse_click(self, xy):
self.points.append((xy.x(), xy.y()))
pt = self.scene.addEllipse(xy.x()-0.5, xy.y()-0.5, 1, 1, pen=self.pen)
self.circles.append(pt)
And here is the code I used to do the tests:
def test_box():
x, y = np.mgrid[0:175, 0:100]
img = x * y
app = QApplication.instance()
if app is None:
app = QApplication(['python'])
picker = BoxSelector(img, bounds=[0, 0, 2*np.pi, 4*np.pi])
picker.show()
app.exec_()
return picker
def test_point():
np.random.seed(159753)
img = np.random.randn(10, 5)
app = QApplication.instance()
if app is None:
app = QApplication(['python'])
pointer = PointLayout(img, bounds=[0, 0, 10, 5], grow=[20, 20])
pointer.show()
app.exec_()
return pointer
if __name__ == "__main__":
pick = test_box()
point = test_point()
I found that explicitly setting the pen width to zero restores the previous behaviour:
class BoxSelector(SimpleDialog):
def __init__(self, *args, **kwargs):
...
self.rpen = QPen(Qt.green)
self.rpen.setWidth(0)
...
class PointLayout(SimpleDialog):
def __init__(self, *args, **kwargs):
...
self.pen = QPen(Qt.green)
self.pen.setWidth(0)
...
It seems that the default was 0 in Qt4, but it is 1 in Qt5.
From the Qt Docs for QPen.setWidth:
A line width of zero indicates a cosmetic pen. This means that the pen
width is always drawn one pixel wide, independent of the
transformation set on the painter.

Categories