I have a PyQt application in which I have to implement a drap n drop functionality of points made via QPainter. My problem is that I'm even able to drag those points outside the window scope e.g. I can drag the point to the title bar or taskbar and leave it there and once left there, I can no longer drag them back to my Mainwindow.
Please provide a solution so that I can never ever drag them there.
Code:
import sys
import numpy as np
from PyQt4 import QtCore, QtGui
class Canvas(QtGui.QWidget):
DELTA = 100 #for the minimum distance
def __init__(self, parent=None):
super(Canvas, self).__init__(parent)
self.draggin_idx = -1
self.points = np.array([[x[0],x[1]] for x in [[100,200], [200,200], [100,400], [200,400]]], dtype=np.float)
self.id = None
self.points_dict = {}
for i, x in enumerate(self.points):
point=(int(x[0]),int(x[1]))
self.points_dict[i] = point
def paintEvent(self, e):
qp = QtGui.QPainter()
qp.begin(self)
self.drawPoints(qp)
self.drawLines(qp)
qp.end()
def drawPoints(self, qp):
# qp.setPen(QtCore.Qt.red)
pen = QtGui.QPen()
pen.setWidth(10)
pen.setColor(QtGui.QColor('red'))
qp.setPen(pen)
for x,y in self.points:
qp.drawPoint(x,y)
def drawLines(self, qp):
# pen.setWidth(5)
# pen.setColor(QtGui.QColor('red'))
qp.setPen(QtCore.Qt.red)
qp.drawLine(self.points_dict[0][0], self.points_dict[0][1], self.points_dict[1][0], self.points_dict[1][1])
qp.drawLine(self.points_dict[1][0], self.points_dict[1][1], self.points_dict[3][0], self.points_dict[3][1])
qp.drawLine(self.points_dict[3][0], self.points_dict[3][1], self.points_dict[2][0], self.points_dict[2][1])
qp.drawLine(self.points_dict[2][0], self.points_dict[2][1], self.points_dict[0][0], self.points_dict[0][1])
def _get_point(self, evt):
return np.array([evt.pos().x(),evt.pos().y()])
#get the click coordinates
def mousePressEvent(self, evt):
if evt.button() == QtCore.Qt.LeftButton and self.draggin_idx == -1:
point = self._get_point(evt)
int_point = (int(point[0]), int(point[1]))
min_dist = ((int_point[0]-self.points_dict[0][0])**2 + (int_point[1]-self.points_dict[0][1])**2)**0.5
for i, x in enumerate(list(self.points_dict.values())):
distance = ((int_point[0]-x[0])**2 + (int_point[1]-x[1])**2)**0.5
if min_dist >= distance:
min_dist = distance
self.id = i
#dist will hold the square distance from the click to the points
dist = self.points - point
dist = dist[:,0]**2 + dist[:,1]**2
dist[dist>self.DELTA] = np.inf #obviate the distances above DELTA
if dist.min() < np.inf:
self.draggin_idx = dist.argmin()
def mouseMoveEvent(self, evt):
if self.draggin_idx != -1:
point = self._get_point(evt)
self.points[self.draggin_idx] = point
self.update()
def mouseReleaseEvent(self, evt):
if evt.button() == QtCore.Qt.LeftButton and self.draggin_idx != -1:
point = self._get_point(evt)
int_point = (int(point[0]), int(point[1]))
self.points_dict[self.id] = int_point
self.points[self.draggin_idx] = point
self.draggin_idx = -1
self.update()
if __name__ == "__main__":
app = QtGui.QApplication([])
win = Canvas()
win.showMaximized()
sys.exit(app.exec_())
This has nothing to do with the painting (which obviously cannot go "outside"), but to the way you're getting the coordinates.
Just ensure that the point is within the margins of the widget:
def _get_point(self, evt):
pos = evt.pos()
if pos.x() < 0:
pos.setX(0)
elif pos.x() > self.width():
pos.setX(self.width())
if pos.y() < 0:
pos.setY(0)
elif pos.y() > self.height():
pos.setY(self.height())
return np.array([pos.x(), pos.y()])
Related
I know this is possible by using the scale method of the QGraphicsView and setting the anchor using setTransformationAnchor(QGraphicsView.AnchorUnderMouse). However, doing so would result to the image quality being compromised when zooming.
So what I did instead is to scale the pixmap, and set it as the QGraphicsPixmapItem of the scene. To zoom in on the current mouse position, I use the mapToScene method and position of the wheel event, but this is still not as smooth as the "traditional" implementation.
Code:
class Zoom_View(QGraphicsView):
def __init__(self):
super().__init__()
self.scene = QGraphicsScene()
self.setScene(self.scene)
self.pix_map = QPixmap("path/to/file")
self.pix_map_item = self.scene.addPixmap(self.pix_map)
self.global_factor = 1
def scale_image(self, factor):
_pixmap = self.pix_map.scaledToHeight(int(factor*self.viewport().geometry().height()), Qt.SmoothTransformation)
self.pix_map_item.setPixmap(_pixmap)
self.scene.setSceneRect(QRectF(_pixmap.rect()))
def wheelEvent(self, event):
factor = 1.5
if QApplication.keyboardModifiers() == Qt.ControlModifier:
view_pos = event.pos()
scene_pos = self.mapToScene(view_pos)
self.centerOn(scene_pos)
if event.angleDelta().y() > 0 and self.global_factor < 20:
self.global_factor *= factor
self.scaleImage(self.global_factor)
elif event.angleDelta().y() < 0 and self.global_factor > 0.2:
self.global_factor /= factor
self.scaleImage(self.global_factor)
else:
return super().wheelEvent(event)
I have a graphics scene with QGraphicsEllipseitem circles that are movable. I am trying to have the one I am dragging move around the other circle instead of allowing them to overlap aka collide. So far I was able to stop the collision but its not moving around smoothly it snaps to a corner. I dont know how to fix it.
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
from PyQt5.QtGui import *
import math
class Circleitem(QGraphicsEllipseItem):
def __init__(self, size, brush):
super().__init__()
radius = size / -2
self.setRect(radius, radius, size, size)
self.setBrush(brush)
self.setFlag(self.ItemIsMovable)
self.setFlag(self.ItemIsSelectable)
def paint(self, painter, option, a):
option.state = QStyle.State_None
return super(Circleitem, self).paint(painter,option)
def mouseMoveEvent(self, event):
super().mouseMoveEvent(event)
self.scene().views()[0].parent().movearound()
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.gscene = QGraphicsScene(0, 0, 1000, 1000)
gview = QGraphicsView(self.gscene)
self.setCentralWidget(gview)
self.circle1 = Circleitem (123, brush=QColor(255,255,0))
self.circle2 =Circleitem(80, brush=QColor(0,255,0))
self.gscene.addItem(self.circle1)
self.gscene.addItem(self.circle2)
self.circle1.setPos(500, 500)
self.circle2.setPos(300, 300)
self.show()
def movearound(self):
if self.gscene.selectedItems()[0] == self.circle1:
moveditem = self.circle1
stillitem = self.circle2
else:
moveditem = self.circle2
stillitem = self.circle1
if len(self.gscene.collidingItems(moveditem)) != 0:
xdist = moveditem.x() - stillitem.x()
ydist = moveditem.y() - stillitem.y()
totaldist = moveditem.rect().width()/2 + stillitem.rect().width()/2
totaldist *= math.sqrt(1 + pow(math.pi, 2)/10)/2
if ( abs(xdist) < totaldist or abs(ydist) < totaldist ):
if xdist > 0:
x = stillitem.x() + totaldist
else:
x = stillitem.x() - totaldist
if ydist > 0:
y = stillitem.y() + totaldist
else:
y = stillitem.y() - totaldist
moveditem.setPos(x, y)
app = QApplication([])
win = MainWindow()
app.exec()
It is simpler to keep the logic in Circleitem.mouseMoveEvent and use QLineF to find the distance and new position.
class Circleitem(QGraphicsEllipseItem):
def __init__(self, size, brush):
super().__init__()
radius = size / -2
self.setRect(radius, radius, size, size)
self.setBrush(brush)
self.setFlag(self.ItemIsMovable)
self.setFlag(self.ItemIsSelectable)
def paint(self, painter, option, a):
option.state = QStyle.State_None
return super(Circleitem, self).paint(painter,option)
def mouseMoveEvent(self, event):
super().mouseMoveEvent(event)
colliding = self.collidingItems()
if colliding:
item = colliding[0] # Add offset if points are equal so length > 0
line = QLineF(item.pos(), self.pos() + QPoint(self.pos() == item.pos(), 0))
min_distance = (self.rect().width() + item.rect().width()) / 2
if line.length() < min_distance:
line.setLength(min_distance)
self.setPos(line.p2())
I need to create a widget that is used to pick a time. QTimeEdit widget doesn't seem intuitive or a good design. So I decided to create a time picker similar to the time picker in smartphones.
I managed to create the clock and click that makes the pointer (something similar to the pointer in the image) move to the currently clicked position (note: it's not perfect, it still looks bad). I would like to have help with making the inner clock
Here is my code:
from PyQt5 import QtWidgets, QtGui, QtCore
import math, sys
class ClockWidget(QtWidgets.QWidget): # I want to be able to reuse this class for other programs also, so please don't hard code values of the list, start and end
def __init__(self, start, end, lst=[], *args, **kwargs):
super(ClockWidget, self).__init__(*args, **kwargs)
self.lst = lst
if not self.lst:
self.lst = [*range(start, end)]
self.index_start = 0 # tune this to move the letters in the circle
self.pointer_angles_multiplier = 9 # just setting the default values
self.current = None
self.rects = []
#property
def index_start(self):
return self._index_start
#index_start.setter
def index_start(self, index):
self._index_start = index
def paintEvent(self, event):
self.rects = []
painter = QtGui.QPainter(self)
pen = QtGui.QPen()
pen.setColor(QtCore.Qt.red)
pen.setWidth(2)
painter.setPen(pen)
x, y = self.rect().x(), self.rect().y()
width, height = self.rect().width(), self.rect().height()
painter.drawEllipse(x, y, x + width, x + height)
s, t, equal_angles, radius = self.angle_calc()
radius -= 30
pen.setColor(QtCore.Qt.green)
pen.setWidth(2)
painter.setPen(pen)
""" pointer angle helps in determining to which position the pointer should be drawn"""
self.pointer_x, self.pointer_y = s + ((radius-30) * math.cos(self.pointer_angles_multiplier * equal_angles)), t \
+ ((radius-30) * math.sin(self.pointer_angles_multiplier * equal_angles))
""" The pendulum like pointer """
painter.drawLine(QtCore.QPointF(s, t), QtCore.QPointF(self.pointer_x, self.pointer_y))
painter.drawEllipse(QtCore.QRectF(QtCore.QPointF(self.pointer_x - 20, self.pointer_y - 40),
QtCore.QPointF(self.pointer_x + 30, self.pointer_y + 10)))
pen.setColor(QtCore.Qt.blue)
pen.setWidth(3)
font = self.font()
font.setPointSize(14)
painter.setFont(font)
painter.setPen(pen)
""" Drawing the number around the circle formula y = t + radius * cos(a)
y = s + radius * sin(a) where angle is in radians (s, t) are the mid point of the circle """
for index, char in enumerate(self.lst, start=self.index_start):
angle = equal_angles * index
y = t + radius * math.sin(angle)
x = s + radius * math.cos(angle)
# print(f"Add: {add_x}, index: {index}; char: {char}")
rect = QtCore.QRectF(x - 30, y - 40, x + 60, y) # clickable point
self.rects.append([index, char, rect]) # appends index, letter, rect
painter.setPen(QtCore.Qt.blue)
painter.drawRect(rect) # helps in visualizing the points where the click can received
print(f"Rect: {rect}; char: {char}")
painter.setPen(QtCore.Qt.red)
points = QtCore.QPointF(x, y)
painter.drawText(points, str(char))
def mousePressEvent(self, event):
for x in self.rects:
index, char, rect = x
if event.button() & QtCore.Qt.LeftButton and rect.contains(event.pos()):
self.pointer_angles_multiplier = index
self.current = char
self.update()
break
def angle_calc(self):
"""
This will simply return (midpoints of circle, divides a circle into the len(list) and return the
angle in radians, radius)
"""
return ((self.rect().width() - self.rect().x()) / 2, (self.rect().height() - self.rect().y()) / 2,
(360 / len(self.lst)) * (math.pi / 180), (self.rect().width() / 2))
def resizeEvent(self, event: QtGui.QResizeEvent):
"""This is supposed to maintain a Square aspect ratio on widget resizing but doesn't work
correctly as you will see when executing"""
if event.size().width() > event.size().height():
self.resize(event.size().height(), event.size().width())
else:
self.resize(event.size().width(), event.size().width())
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
message = ClockWidget(1, 13)
message.index_start = 10
message.show()
sys.exit(app.exec())
The Output:
The blue rectangles represent the clickable region. I would be glad if you could also, make the pointer move to the closest number when clicked inside the clock (Not just move the pointer when the clicked inside the blue region)
There is one more problem in my code, that is the numbers are not evenly spaced from the outer circle. (like the number 12 is closer to the outer circle than the number 6)
Disclaimer: I will not explain the cause of the error but the code I provide I think should give a clear explanation of the errors.
The logic is to calculate the position of the centers of each small circle, and use the exinscribed rectangle to take it as a base to draw the text and check if the point where you click is close to the texts.
from functools import cached_property
import math
import sys
from PyQt5 import QtCore, QtGui, QtWidgets
class ClockWidget(QtWidgets.QWidget):
L = 12
r = 40.0
DELTA_ANGLE = 2 * math.pi / L
current_index = 9
def paintEvent(self, event):
painter = QtGui.QPainter(self)
painter.setRenderHint(QtGui.QPainter.Antialiasing)
R = min(self.rect().width(), self.rect().height()) / 2
margin = 4
Rect = QtCore.QRectF(0, 0, 2 * R - margin, 2 * R - margin)
Rect.moveCenter(self.rect().center())
painter.setBrush(QtGui.QColor("gray"))
painter.drawEllipse(Rect)
rect = QtCore.QRectF(0, 0, self.r, self.r)
if 0 <= self.current_index < 12:
c = self.center_by_index(self.current_index)
rect.moveCenter(c)
pen = QtGui.QPen(QtGui.QColor("red"))
pen.setWidth(5)
painter.setPen(pen)
painter.drawLine(c, self.rect().center())
painter.setBrush(QtGui.QColor("red"))
painter.drawEllipse(rect)
for i in range(self.L):
j = (i + 2) % self.L + 1
c = self.center_by_index(i)
rect.moveCenter(c)
painter.setPen(QtGui.QColor("white"))
painter.drawText(rect, QtCore.Qt.AlignCenter, str(j))
def center_by_index(self, index):
R = min(self.rect().width(), self.rect().height()) / 2
angle = self.DELTA_ANGLE * index
center = self.rect().center()
return center + (R - self.r) * QtCore.QPointF(math.cos(angle), math.sin(angle))
def index_by_click(self, pos):
for i in range(self.L):
c = self.center_by_index(i)
delta = QtGui.QVector2D(pos).distanceToPoint(QtGui.QVector2D(c))
if delta < self.r:
return i
return -1
def mousePressEvent(self, event):
i = self.index_by_click(event.pos())
if i >= 0:
self.current_index = i
self.update()
#property
def hour(self):
return (self.current_index + 2) % self.L + 1
def minumumSizeHint(self):
return QtCore.QSize(100, 100)
def main():
app = QtWidgets.QApplication(sys.argv)
view = ClockWidget()
view.resize(400, 400)
view.show()
sys.exit(app.exec_())
if __name__ == "__main__":
main()
Currently I have this custom rotated QDial() widget with the dial handle pointing upward at the 0 position instead of the default 180 value position.
To change the tick spacing, setNotchTarget() is used to space the notches but this creates an even distribution of ticks (left). I want to create a custom dial with only three adjustable ticks (right).
The center tick will never move and will always be at the north position at 0. But the other two ticks can be adjustable and should be evenly spaced. So for instance, if the tick was set at 70, it would place the left/right ticks 35 units from the center. Similarly, if the tick was changed to 120, it would space the ticks by 60.
How can I do this? If this is not possible using QDial(), what other widget would be capable of doing this? I'm using PyQt4 and Python 3.7
import sys
from PyQt4 import QtGui, QtCore
class Dial(QtGui.QWidget):
def __init__(self, rotation=0, parent=None):
QtGui.QWidget.__init__(self, parent)
self.dial = QtGui.QDial()
self.dial.setMinimumHeight(160)
self.dial.setNotchesVisible(True)
# self.dial.setNotchTarget(90)
self.dial.setMaximum(360)
self.dial.setWrapping(True)
self.label = QtGui.QLabel('0')
self.dial.valueChanged.connect(self.label.setNum)
self.view = QtGui.QGraphicsView(self)
self.scene = QtGui.QGraphicsScene(self)
self.view.setScene(self.scene)
self.graphics_item = self.scene.addWidget(self.dial)
self.graphics_item.rotate(rotation)
# Make the QGraphicsView invisible
self.view.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
self.view.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
self.view.setFixedHeight(self.dial.height())
self.view.setFixedWidth(self.dial.width())
self.view.setStyleSheet("border: 0px")
self.layout = QtGui.QVBoxLayout()
self.layout.addWidget(self.view)
self.layout.addWidget(self.label)
self.setLayout(self.layout)
if __name__ == '__main__':
app = QtGui.QApplication(sys.argv)
dialexample = Dial(rotation=180)
dialexample.show()
sys.exit(app.exec_())
First of all. Qt's dials are a mess. They are nice widgets, but they've been mostly developed for simple use cases.
If you need "special" behavior, you'll need to override some important methods. This example obviously requires paintEvent overriding, but the most important parts are the mouse events and wheel events. Tracking keyboard events required to set single and page step to the value range, and to "overwrite" the original valueChanged signal to ensure that the emitted value range is always between -1 and 1. You can obviously change those values by adding a dedicated function.
Theoretically, QDial widgets should always use 240|-60 degrees angles, but that might change in the future, so I decided to enable the wrapping to keep degrees as an "internal" value. Keep in mind that you'll probably need to provide your own value() property implementation also.
from PyQt4 import QtCore, QtGui
from math import sin, cos, atan2, degrees, radians
import sys
class Dial(QtGui.QDial):
MinValue, MidValue, MaxValue = -1, 0, 1
__valueChanged = QtCore.pyqtSignal(int)
def __init__(self, valueRange=120):
QtGui.QDial.__init__(self)
self.setWrapping(True)
self.setRange(0, 359)
self.valueChanged.connect(self.emitSanitizedValue)
self.valueChanged = self.__valueChanged
self.valueRange = valueRange
self.__midValue = valueRange / 2
self.setPageStep(valueRange)
self.setSingleStep(valueRange)
QtGui.QDial.setValue(self, 180)
self.oldValue = None
# uncomment this if you want to emit the changed value only when releasing the slider
# self.setTracking(False)
self.notchSize = 5
self.notchPen = QtGui.QPen(QtCore.Qt.black, 2)
self.actionTriggered.connect(self.checkAction)
def emitSanitizedValue(self, value):
if value < 180:
self.valueChanged.emit(self.MinValue)
elif value > 180:
self.valueChanged.emit(self.MaxValue)
else:
self.valueChanged.emit(self.MidValue)
def checkAction(self, action):
value = self.sliderPosition()
if action in (self.SliderSingleStepAdd, self.SliderPageStepAdd) and value < 180:
value = 180 + self.valueRange
elif action in (self.SliderSingleStepSub, self.SliderPageStepSub) and value > 180:
value = 180 - self.valueRange
elif value < 180:
value = 180 - self.valueRange
elif value > 180:
value = 180 + self.valueRange
else:
value = 180
self.setSliderPosition(value)
def valueFromPosition(self, pos):
y = self.height() / 2. - pos.y()
x = pos.x() - self.width() / 2.
angle = degrees(atan2(y, x))
if angle > 90 + self.__midValue or angle < -90:
value = self.MinValue
final = 180 - self.valueRange
elif angle >= 90 - self.__midValue:
value = self.MidValue
final = 180
else:
value = self.MaxValue
final = 180 + self.valueRange
self.blockSignals(True)
QtGui.QDial.setValue(self, final)
self.blockSignals(False)
return value
def value(self):
rawValue = QtGui.QDial.value(self)
if rawValue < 180:
return self.MinValue
elif rawValue > 180:
return self.MaxValue
return self.MidValue
def setValue(self, value):
if value < 0:
QtGui.QDial.setValue(self, 180 - self.valueRange)
elif value > 0:
QtGui.QDial.setValue(self, 180 + self.valueRange)
else:
QtGui.QDial.setValue(self, 180)
def mousePressEvent(self, event):
self.oldValue = self.value()
value = self.valueFromPosition(event.pos())
if self.hasTracking() and self.oldValue != value:
self.oldValue = value
self.valueChanged.emit(value)
def mouseMoveEvent(self, event):
value = self.valueFromPosition(event.pos())
if self.hasTracking() and self.oldValue != value:
self.oldValue = value
self.valueChanged.emit(value)
def mouseReleaseEvent(self, event):
value = self.valueFromPosition(event.pos())
if self.oldValue != value:
self.valueChanged.emit(value)
def wheelEvent(self, event):
delta = event.delta()
oldValue = QtGui.QDial.value(self)
if oldValue < 180:
if delta < 0:
outValue = self.MinValue
value = 180 - self.valueRange
else:
outValue = self.MidValue
value = 180
elif oldValue == 180:
if delta < 0:
outValue = self.MinValue
value = 180 - self.valueRange
else:
outValue = self.MaxValue
value = 180 + self.valueRange
else:
if delta < 0:
outValue = self.MidValue
value = 180
else:
outValue = self.MaxValue
value = 180 + self.valueRange
self.blockSignals(True)
QtGui.QDial.setValue(self, value)
self.blockSignals(False)
if oldValue != value:
self.valueChanged.emit(outValue)
def paintEvent(self, event):
QtGui.QDial.paintEvent(self, event)
qp = QtGui.QPainter(self)
qp.setRenderHints(qp.Antialiasing)
qp.translate(.5, .5)
rad = radians(self.valueRange)
qp.setPen(self.notchPen)
c = -cos(rad)
s = sin(rad)
# use minimal size to ensure that the circle used for notches
# is always adapted to the actual dial size if the widget has
# width/height ratio very different from 1.0
maxSize = min(self.width() / 2, self.height() / 2)
minSize = maxSize - self.notchSize
center = self.rect().center()
qp.drawLine(center.x(), center.y() -minSize, center.x(), center.y() - maxSize)
qp.drawLine(center.x() + s * minSize, center.y() + c * minSize, center.x() + s * maxSize, center.y() + c * maxSize)
qp.drawLine(center.x() - s * minSize, center.y() + c * minSize, center.x() - s * maxSize, center.y() + c * maxSize)
class Test(QtGui.QWidget):
def __init__(self, *sizes):
QtGui.QWidget.__init__(self)
layout = QtGui.QGridLayout()
self.setLayout(layout)
if not sizes:
sizes = 70, 90, 120
self.dials = []
for col, size in enumerate(sizes):
label = QtGui.QLabel(str(size))
label.setAlignment(QtCore.Qt.AlignCenter)
dial = Dial(size)
self.dials.append(dial)
dial.valueChanged.connect(lambda value, dial=col: self.dialChanged(dial, value))
layout.addWidget(label, 0, col)
layout.addWidget(dial, 1, col)
def dialChanged(self, dial, value):
print('dial {} changed to {}'.format(dial, value))
def setDialValue(self, dial, value):
self.dials[dial].setValue(value)
if __name__ == '__main__':
app = QtGui.QApplication(sys.argv)
dialexample = Test(70, 90, 120)
# Change values here
dialexample.setDialValue(1, 1)
dialexample.show()
sys.exit(app.exec_())
EDIT: I updated the code to implement keyboard navigation and avoid unnecessary multiple signal emissions.
I am painting some squares and a circle on in a graphics scene and I would like to connect those squares with multiple lines that will start at the circle and branch out like a tree. When the user clicks the button in the lower left corner the lines should draw in the scene. Once the lines are drawn in the scene the user should be able to zoom into the lines and select the lines just like you can with the circle and squares.
The path is determined by a function called group. This function will take the points and make a graph structure for an outline of how the points are supposed to be connected. I know this can be better and I would like it to be better, but I didn't want to ask too many questions in this post. As an example, the output should look something like
{0: [1, 5, 6], 1: [7], 7: [12], 5: [4]}
This means point 0 is connected to points 1,5and 6 and point 1 is connected to point 7 and so on.
Then I have a class called PathLine that should set all of the attributes for changing colors when it is selected and whatever else I would like the user to be able to see in the future.
When the user clicks the button it will run a function in my Viewer class called drawConnectingLines this is where I wanted it to draw the lines. I can tell the function is running and I am not getting any errors, but nothing is showing up in the window.
I have tried adding QApplication.processEvents(), self.update(), and self.scene().update(), but nothing seems to make a difference. Also If I try to draw the lines on the load of the program it will not draw anything. Not even the points.
Any help is greatly appreciated.
rom PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
import sys
from math import sqrt,cos,acos,asin,degrees,pi,hypot
class LogObject(QObject):
hovered = pyqtSignal()
notHovered = pyqtSignal()
def create_square():
scale = 250
path = QPainterPath()
path.addRect(-0.076,-0.07,0.1520,0.1400)
tr = QTransform()
tr.scale(scale, scale)
path = tr.map(path)
return path
def create_circle():
scale = 250
path = QPainterPath()
path.addEllipse(QPointF(0,0), 0.0750, 0.0750) # Using QPointF will center it
tr = QTransform()
tr.scale(scale, scale)
path = tr.map(path)
return path
def drawPath(x1,y1,x2,y2):
scale = 250
path = QPainterPath()
path.moveTo(x1,y1)
path.lineTo(x2,y2)
tr = QTransform()
tr.scale(scale, scale)
path = tr.map(path)
return path
class PathLine(QGraphicsPathItem):
def __init__(self,x1,y1,x2,y2):
super(PathLine,self).__init__()
self.x1 = x1
self.y1 = y1
self.x2 = x2
self.y2 = y2
# self.name = name
self.setFlag(QGraphicsItem.ItemIsSelectable,True)
self.setScale(1.5)
self.setPath(drawPath(x1,y1,x2,y2))
self.setAcceptHoverEvents(True)
self.log = LogObject()
self.setPos(x1, y1)
self.isSelected = False
def findLineWidth(self,zoomValue): # This function is for creating the line width value of all the drawn Objects
if zoomValue > 18:
zoomValue = 18
lineWidthF = -0.0000177256625115696*(zoomValue)**4 + 0.000440875172476041*(zoomValue)**3 + 0.00941580772740735*(zoomValue)**2 - 0.370069940941448*(zoomValue) + 3
self.updateLineWidth(lineWidthF)
def updateLineWidth(self,lineWidth):
pen = self.pen()
pen.setWidthF(lineWidth)
self.setPen(pen)
def itemChange(self, change, value):
if change == self.ItemSelectedChange:
color = QColor(Qt.green) if value else QColor(Qt.white)
pen = self.pen()
pen.setColor(color)
self.setPen(pen)
return QGraphicsItem.itemChange(self, change, value)
def hoverEnterEvent(self, event):
color = QColor("red")
pen = self.pen()
pen.setColor(color)
self.setPen(pen)
self.log.hovered.emit()
QGraphicsItem.hoverMoveEvent(self, event)
def hoverLeaveEvent(self, event):
color = QColor(Qt.green) if self.isSelected else QColor(Qt.white)
pen = self.pen()
pen.setColor(color)
self.setPen(pen)
self.log.notHovered.emit()
QGraphicsItem.hoverMoveEvent(self, event)
class Point(QGraphicsPathItem):
def __init__(self, x, y, r, name):
super(Point, self).__init__()
self.setFlag(QGraphicsItem.ItemIsSelectable, True)
self.name = name
if self.name.split('__')[1] == '0':
self.setPath(create_circle())
else:
self.setPath(create_square())
self.setScale(1.5)
self.x = x
self.y = y
self.r = r
self.setRotation(180+self.r)
self.setAcceptHoverEvents(True)
self.log = LogObject()
self.setPos(x, y)
self.isSelected = False
pen = QPen(Qt.white)
pen.setStyle(Qt.SolidLine)
pen.setWidthF(3)
self.setPen(pen)
def findLineWidth(self,zoomValue): # This function is for creating the line width value of all the drawn Objects
if zoomValue > 18:
zoomValue = 18
lineWidthF = -0.0000177256625115696*(zoomValue)**4 + 0.000440875172476041*(zoomValue)**3 + 0.00941580772740735*(zoomValue)**2 - 0.370069940941448*(zoomValue) + 3
self.updateLineWidth(lineWidthF)
def updateLineWidth(self,lineWidth):
pen = self.pen()
pen.setWidthF(lineWidth)
self.setPen(pen)
def itemChange(self, change, value):
if change == self.ItemSelectedChange:
color = QColor(Qt.green) if value else QColor(Qt.white)
pen = self.pen()
pen.setColor(color)
self.setPen(pen)
return QGraphicsItem.itemChange(self, change, value)
def hoverEnterEvent(self, event):
color = QColor("red")
pen = self.pen()
pen.setColor(color)
self.setPen(pen)
self.log.hovered.emit()
QGraphicsItem.hoverMoveEvent(self, event)
def hoverLeaveEvent(self, event):
color = QColor(Qt.green) if self.isSelected else QColor(Qt.white)
pen = self.pen()
pen.setColor(color)
self.setPen(pen)
self.log.notHovered.emit()
QGraphicsItem.hoverMoveEvent(self, event)
def mouseDoubleClickEvent(self,event):
print(self.name)
class Viewer(QGraphicsView):
photoClicked = pyqtSignal(QPoint)
rectChanged = pyqtSignal(QRect)
def __init__(self, parent):
super(Viewer, self).__init__(parent)
self.rubberBand = QRubberBand(QRubberBand.Rectangle, self)
self.setMouseTracking(True)
self.origin = QPoint()
self.changeRubberBand = False
self.setRenderHints(QPainter.Antialiasing)
self._zoom = 0
self._empty = True
self.setScene(QGraphicsScene(self))
self.setTransformationAnchor(QGraphicsView.AnchorUnderMouse)
self.setResizeAnchor(QGraphicsView.AnchorUnderMouse)
self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.setFrameShape(QFrame.NoFrame)
self.area = float()
self.setPoints()
self.viewport().setCursor(Qt.ArrowCursor)
QTimer.singleShot(0, self.reset_fit)
self.selectedItems = []
self.setBackgroundBrush(Qt.black)
def setItems(self):
self.data = {
"x": [
-2415594.9965,
-2414943.8686,
-2417160.6592,
# -2417160.6592,
-2417856.1783,
-2417054.7618,
-2416009.9966,
-2416012.5232,
# -2418160.8952,
-2418160.8952,
# -2416012.5232,
# -2417094.7694,
-2417094.7694,
],
"y": [
10453172.2426,
10454269.7008,
10454147.2672,
# 10454147.2672,
10453285.2456,
10452556.8132,
10453240.2808,
10455255.8752,
# 10455183.1912,
10455183.1912,
# 10455255.8752,
# 10456212.5959,
10456212.5959,
],
"rotation":[
0,
313.9962,
43.9962,
# 223.9962,
227.7070,
227.7070,
313.9962,
43.9962,
# 43.9962,
223.9962,
# 223.9962,
# 43.9962,
223.9962,
]
}
self.adjustedPoints = {}
for i, (x, y,r) in enumerate(zip(self.data["x"], self.data["y"],self.data["rotation"])):
p = Point(x, y,r, "Point__" + str(i))
p.log.hovered.connect(self.hoverChange)
p.log.notHovered.connect(self.notHoverChange)
self.scene().addItem(p)
self.adjustedPoints[i] = [x,y]
# if i == 0:
# self.adjustedPoints['c__'+str(i)] = [x,y]
# else:
# self.adjustedPoints['s__'+str(i)] = [x,y]
def drawConnectingLines(self):
# result = self.group(self.adjustedPoints, 'c__0')
result = self.group(self.adjustedPoints, 0)
for startPoint in result.items():
x1 = self.adjustedPoints[startPoint[0]][0]
y1 = self.adjustedPoints[startPoint[0]][1]
for endPoint in startPoint[1]:
x2 = self.adjustedPoints[endPoint][0]
y2 = self.adjustedPoints[endPoint][1]
connectingLine = PathLine(x1,y1,x2,y2)
# connectingLine.drawPath()
self.scene().addItem(connectingLine)
# QApplication.processEvents()
self.scene().update()
def findMinDistance(self,data, start):
xStart, yStart = data[start]
distances = []
for item,[x,y] in data.items():
if item != start and item != 0:
distances.append(hypot(abs(xStart - x),abs(yStart-y)))
output = self.mean(distances)-min(distances)
if output < min(distances):
output = min(distances)
return output
def mean(self,numbers):
return float(sum(numbers)) / max(len(numbers), 1)
def group(self,d, start,seen = []):
x, y = d[start]
r =[]
print(start)
dist = self.findMinDistance(d,start)
print(dist)
for a, [j, k] in d.items():
if a != start and a not in seen and hypot(abs(x-j), abs(y-k)) <= dist:
r.append(a)
if not r:
return {}
result = {start:r}
for i in r:
result.update(self.group(d, i, seen+[start, *r]))
return result
def setPoints(self):
self.setItems()
# self.drawConnectingLines()
self.setDragMode(self.ScrollHandDrag)
def wheelEvent(self, event):
for item in self.scene().items():
item.findLineWidth(self._zoom)
if event.angleDelta().y() > 0: # angleDelta is positive 120 zooming in and -120 going out
factor = 1.25
self._zoom += 1
else:
factor = 0.8
self._zoom -= 1
if self._zoom > 0:
self.scale(factor, factor)
elif self._zoom == 0:
self.reset_fit()
else:
self._zoom = 0
def hoverChange(self):
self.viewport().setCursor(Qt.PointingHandCursor)
def notHoverChange(self):
self.viewport().setCursor(Qt.ArrowCursor)
def mousePressEvent(self, event):
if event.button() == Qt.LeftButton:
singleItem = self.itemAt(event.pos().x(), event.pos().y())
if singleItem != None:
if QApplication.keyboardModifiers() == Qt.ShiftModifier: # This will determine if the shift key is depressed
if singleItem.isSelected == True:
singleItem.setSelected(False)
singleItem.isSelected = False
self.selectedItems.remove(singleItem)
elif singleItem.isSelected == False:
singleItem.setSelected(True)
singleItem.isSelected = True
self.selectedItems.append(singleItem)
else:
self.origin = event.pos()
self.rubberBand.setGeometry(QRect(self.origin, QSize()))
self.rectChanged.emit(self.rubberBand.geometry())
self.rubberBand.show()
self.changeRubberBand = True
return
elif event.button() == Qt.MidButton:
self.viewport().setCursor(Qt.ClosedHandCursor)
self.original_event = event
handmade_event = QMouseEvent(
QEvent.MouseButtonPress,
QPointF(event.pos()),
Qt.LeftButton,
event.buttons(),
Qt.KeyboardModifiers(),
)
QGraphicsView.mousePressEvent(self, handmade_event)
super(Viewer, self).mousePressEvent(event)
def mouseReleaseEvent(self, event):
point = event.pos()
if event.button() == Qt.LeftButton:
self.changeRubberBand = False
if self.rubberBand.isVisible():
self.rubberBand.hide()
rect = self.rubberBand.geometry()
rect_scene = self.mapToScene(rect).boundingRect()
selected = self.scene().items(rect_scene)
if selected:
# print(selected)
for selectedPoints in selected:
if QApplication.keyboardModifiers() == Qt.ShiftModifier: # This will determine if the shift key is depressed
if selectedPoints.isSelected == True:
selectedPoints.setSelected(False)
selectedPoints.isSelected = False
self.selectedItems.remove(selectedPoints)
elif selectedPoints.isSelected == False: # if the shif key is not depressed and its not selected, then select it
selectedPoints.setSelected(True)
selectedPoints.isSelected = True
self.selectedItems.append(selectedPoints)
print( "".join("Item: %s\n" % child.name for child in self.selectedItems))
else:
print(" Nothing\n")
for selected in self.selectedItems:
selected.setSelected(False)
selected.isSelected = False
self.selectedItems.clear()
QGraphicsView.mouseReleaseEvent(self, event)
elif event.button() == Qt.MidButton:
self.viewport().setCursor(Qt.ArrowCursor)
handmade_event = QMouseEvent(
QEvent.MouseButtonRelease,
QPointF(event.pos()),
Qt.LeftButton,
event.buttons(),
Qt.KeyboardModifiers(),
)
QGraphicsView.mouseReleaseEvent(self, handmade_event)
def mouseMoveEvent(self, event):
if self.changeRubberBand:
self.rubberBand.setGeometry(
QRect(self.origin, event.pos()).normalized()
)
self.rectChanged.emit(self.rubberBand.geometry())
QGraphicsView.mouseMoveEvent(self, event)
super(Viewer, self).mouseMoveEvent(event)
def reset_fit(self):
r = self.scene().itemsBoundingRect()
self.resetTransform()
self.setSceneRect(r)
self.fitInView(r, Qt.KeepAspectRatio)
self._zoom = 0
self.scale(1, -1)
class Window(QWidget):
def __init__(self):
super(Window, self).__init__()
self.viewer = Viewer(self)
self.btnFindPath = QToolButton(self)
self.btnFindPath.setText("Draw Path")
self.btnFindPath.clicked.connect(self.autoDrawLines)
VBlayout = QVBoxLayout(self)
VBlayout.addWidget(self.viewer)
HBlayout = QHBoxLayout()
HBlayout.setAlignment(Qt.AlignLeft)
HBlayout.addWidget(self.btnFindPath)
VBlayout.addLayout(HBlayout)
def autoDrawLines(self):
self.viewer.drawConnectingLines()
if __name__ == '__main__':
import sys
app = QApplication(sys.argv)
window = Window()
window.setGeometry(500, 300, 800, 600)
window.show()
sys.exit(app.exec_())
drawPath(x1,y1,x2,y2) effectively draws a line from scale*(x1, y1) to scale*(x2, y2) in item coordinates where scale = 250. In PathLine.__init__ you then move this line to (x1, y1) (in scene coordinates) and you scale it by a factor 1.5 which means that in scene coordinates the lines go from (1.5*scale+1)*(x1,y1) to 1.5*scale*(x2,y2)+(x1,y1).
However the circles and squares generated by create_circle and create_square are centered around (0,0) in item coordinates and moved to (x1,y1) in Point.__init__.
Therefore, to get the lines to match up with the circles and squares you should set scale to 1.0 in drawPath, keep the position of the line at (0,0) and its scale to 1.0 in PathLine.__init__. Also, you need to set an initial line with and line color to be able to see them, i.e.
def drawPath(x1,y1,x2,y2):
path = QPainterPath()
path.moveTo(x1,y1)
path.lineTo(x2,y2)
return path
class PathLine(QGraphicsPathItem):
def __init__(self,x1,y1,x2,y2):
super(PathLine,self).__init__()
self.x1 = x1
self.y1 = y1
self.x2 = x2
self.y2 = y2
# self.name = name
self.setFlag(QGraphicsItem.ItemIsSelectable,True)
self.setPath(drawPath(x1,y1,x2,y2))
self.setAcceptHoverEvents(True)
self.log = LogObject()
pen = QPen(Qt.white)
pen.setStyle(Qt.SolidLine)
pen.setWidthF(4)
self.setPen(pen)
self.isSelected = False