How can I modify a QtableWidget which is on a QMainWindow from a QGraphicsView method? - pyqt5

Essentially I created a QMainWindow which has a Splitter which, in turn, has on its left side a QTableWidget and on the right side a QGraphicsView.
I have also created a method to zoom in and out in the QGraphicsView. Now, I want to resize the height of the rows depending on the amount of zoom provided by the user.
class MyWindow(QMainWindow):
def __init__(self):
super(MyWindow, self).__init__()
# Main characteristics of the window
self.setGeometry(50, 50, 1000, 700)
#User Interface
self.initUI()
def initUI(self):
#Creation of table and timeline splitter
self.table_and_view_splitter = QtWidgets.QSplitter()
self.table_and_view_splitter.setOrientation(QtCore.Qt.Horizontal)
#Creation of metadata table
self.create_table()
self.table_and_view_splitter.addWidget(self.table)
#Creation of View and Scene for timeline
self.create_view()
self.table_and_view_splitter.addWidget(self.view)
# Creation of vertical splitter
self.vertical_splitter = QtWidgets.QSplitter()
self.vertical_splitter.setOrientation(QtCore.Qt.Vertical)
self.vertical_splitter.insertWidget(1, self.table_and_view_splitter)
# Choosing the sizes of the upper and lower widgets of the Qsplitter
self.sizes_list = [100, 5000]
self.vertical_splitter.setSizes(self.sizes_list)
self.setCentralWidget(self.vertical_splitter)
def create_table(self):
self.table = QTableWidget()
self.table.setColumnCount(3)
self.table.setRowCount(100)
for i in range(self.table.rowCount()):
self.table.setRowHeight(i, 10)
def create_view(self):
self.view = viewFor()
self.scene = QtWidgets.QGraphicsScene()
self.scene.addEllipse(1, 1, 10, 10)
self.view.setScene(self.scene)
class viewFor(QGraphicsView):
def __init__(self):
super(viewFor, self).__init__()
self.drag = False
self.setTransformationAnchor(self.NoAnchor)
def wheelEvent(self, event):
self.setTransformationAnchor(self.AnchorUnderMouse)
zoom_in_factor = 1.1
zoom_out_factor = 1 / zoom_in_factor
# Save the scene pos
old_position = self.mapToScene(event.pos())
if QApplication.keyboardModifiers() == Qt.ControlModifier:# CTRL + Scroll -> X and Y Zoom
# Zoom
if event.angleDelta().y() > 0:
zoom_factor = zoom_in_factor
else:
zoom_factor = zoom_out_factor
self.scale(zoom_factor, zoom_factor)
#HERE I WANT TO RESIZE THE ROWS HEIGHT ACCORDING TO THE zoom_factor
# Get the new position
new_position = self.mapToScene(event.pos())
# Move scene to old position
delta = new_position - old_position
self.translate(delta.x(), delta.y())
else:# Only Scroll -> only X Zoom
# Zoom
if event.angleDelta().y() > 0:
zoom_factor = zoom_in_factor
else:
zoom_factor = zoom_out_factor
self.scale(zoom_factor, 1)
# Get the new position
new_position = self.mapToScene(event.pos())
# Move scene to old position
delta = new_position - old_position
self.translate(delta.x(), delta.y())
app = QApplication([])
foo = MyWindow()
foo.show()
sys.exit(app.exec_())

You need to emit a signal whenever the scale is changed, and QHeaderView.setDefaultSectionSize() for the vertical header. Note that you should probably use setSectionResizeMode(QHeaderView.Fixed) to avoid user resizing (or just leave it to Interactive, but certainly don't use Stretch or ResizeToContents).
You should obviously ensure that the range is valid or find your own algorithm (that sanitizes the value to a valid range that has at least a minimum of 1).
In this case I used the default original value and multiplied it using the scale factor of the view's transformation (see QTransform > rendering graphics about the meaning of the transformation matrix).
class MyWindow(QtWidgets.QMainWindow):
def __init__(self):
super(MyWindow, self).__init__()
# Main characteristics of the window
self.setGeometry(50, 50, 1000, 700)
#User Interface
self.initUI()
self.view.scaleChanged.connect(self.resizeRows)
self.defaultSize = self.table.verticalHeader().defaultSectionSize()
def resizeRows(self, scale):
self.table.verticalHeader().setDefaultSectionSize(scale * self.defaultSize)
class viewFor(QtWidgets.QGraphicsView):
scaleChanged = QtCore.pyqtSignal(float)
# ...
def wheelEvent(self, event):
self.setTransformationAnchor(self.AnchorUnderMouse)
zoom_in_factor = 1.1
zoom_out_factor = 1 / zoom_in_factor
# Save the scene pos
old_position = self.mapToScene(event.pos())
if event.modifiers() == QtCore.Qt.ControlModifier:# CTRL + Scroll -> X and Y Zoom
# Zoom
if event.angleDelta().y() > 0:
zoom_factor = zoom_in_factor
else:
zoom_factor = zoom_out_factor
self.scale(zoom_factor, zoom_factor)
# emit the signal based on the transformation scale factor
self.scaleChanged.emit(self.transform().m11())
# ...
Note that you don't need to use QApplication.keyboardModifiers, as you can access modifiers() of all keyboard/mouse events.

Related

How to open context menu for an object that is empty, but has a size?

I am creating a GUI for a dependency graphing software... And am not able to figure out how to get a context menu to open for my lines.
What I want to do, right click on/near a MyLine widget and open a context menu... What is happening right clicks are not detected.
It is currently not detecting right clicks on the line widgets location to open a context menu (Purpose of this is to allow the user to delete/edit lines by right clicking on them).
What am I doing wrong here?
class MyLine(QWidget):
def __init__(self, destination: Node, source: Node, parent=None):
super().__init__(parent)
self.setContextMenuPolicy(Qt.CustomContextMenu)
self.customContextMenuRequested.connect(self.showMenu)
self.destination = destination
self.source = source
self.setAutoFillBackground(True)
p = self.palette()
p.setColor(self.backgroundRole(), Qt.red)
self.setPalette(p)
def update_line_size(self):
origin = self.source.get_line_draw_pos(self.destination.pos())
destination = self.destination.get_line_draw_pos(self.source.pos())
leftcornerX = origin.x() if origin.x() < destination.x() else destination.x()
leftcornerY = origin.y() if origin.y() < destination.y() else destination.y()
sizeX = abs(origin.x() - destination.x())
sizeY = abs(origin.y() - destination.y())
self.setGeometry(leftcornerX, leftcornerY, sizeX, sizeY)
def showMenu(self, _):
menu = QMenu()
menu.addAction("Delete", self.remove)
menu.exec_(self.cursor().pos())
def draw(self, painter: QPainter):
origin = self.source.get_line_draw_pos(self.destination.pos())
destination = self.destination.get_line_draw_pos(self.source.pos())
painter.drawLine(origin, destination)
# DRAW ARROW HEAD
ARROW_SIZE = 10 # Might change
line_angle = calculate_line_angle(destination, origin)
draw_arrow_head(destination, painter, line_angle, ARROW_SIZE)
def remove(self):
self.parent().delete_line(self)
self.deleteLater()
Edit:
required types for reproducibility
class Node(QLabel):
def __init__(self, text: str, parent=None):
super().__init__(text, parent)
self.setContextMenuPolicy(Qt.CustomContextMenu)
self.customContextMenuRequested.connect(self.showMenu)
def get_line_draw_pos(self, other_side: QPoint):
x = self.pos().x() if other_side.x() < self.pos().x() else (self.pos().x() + self.width())
y = self.pos().y() if other_side.y() < self.pos().y() else (self.pos().y() + self.height())
return QPoint(x, y)
def showMenu(self, _):
pass #purposefully left as a stub
def calculate_line_angle(destination: QPoint, origin: QPoint):
return math.atan2(destination.y() - origin.y(), destination.x() - origin.x())
def draw_arrow_head(destination: QPoint, painter: QPainter, line_angle: float, arrow_size: float = 10):
angle1 = math.radians(22.5) + line_angle
angle2 = math.radians(-22.5) + line_angle
arrow1 = QPoint( int(destination.x() - arrow_size * math.cos(angle1)), int(destination.y() - arrow_size * math.sin(angle1)))
arrow2 = QPoint( int(destination.x() - arrow_size * math.cos(angle2)), int(destination.y() - arrow_size * math.sin(angle2)))
painter.drawLine(destination, arrow1)
painter.drawLine(destination, arrow2)
class MainWindow(QWidget):
def __init__(self):
super().__init__()
self.setAcceptDrops(True) # add a drop rule
self.setMouseTracking(True)
self.track_origin = None
self.track_mouse = QPoint(0,0)
self.lines = []
def paintEvent(self, event):
painter = QPainter(self)
for line in self.lines:
line.draw(painter)
line.update_line_size()
def connectNodes(self, destination: Node, source: Node):
self.lines.append(MyLine(destination, source))
self.update()
def delete_line(self, line: MyLine):
self.lines.remove(line)
self.update()
app = QApplication([])
window = MainWindow()
window.setWindowTitle("Right Click to remove label")
window.setGeometry(100, 100, 400, 200)
window.move(60,15)
nodes = []
for index, node_name in enumerate(["hello.txt", "not_a_villain.txt", "nope.txt"]):
node = Node(node_name, window)
node.move(50 + index*100, 50 + (index%2) * 50)
nodes.append(node)
window.connectNodes(nodes[0], nodes[1])
window.connectNodes(nodes[0], nodes[2])
window.connectNodes(nodes[1], nodes[2])
window.show()
sys.exit(app.exec_())

PyQt5: How to flip QGraphicsItem parent item only

I am trying to perform flipping on a QGraphicsItem that has child and grandchild QGraphicsItem.
The original item looks like this:
(The blue rectangle and text are child and the number inside it is grandchild
I apply the following transformation to the parent item:
parentItem.setTransformOriginPoint(parentItem.boundingRect().center())
parentItem.setTransform(QTransform.fromScale(-1, 1))
Result after flipping parent item:
Since I want to reflip the text and number to be readable, I attempt to re-flip them after the parent's transformation as followed:
# For the child text
child.setTransformOriginPoint(child.boundingRect().center())
child.setTransform(QTransform.fromScale(-1, 1), True)
...
# For the grandchild number
grandchild.setTransformOriginPoint(grandchild.boundingRect().center())
grandchild.setTransform(QTransform.fromScale(-1, 1), True)
Here is the result after re-flipped the child and grandchild item:
.
It seems that the translation is not correct. Can someone advice?
Thanks!
Minimal reproducible example below:
from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5.QtGui import *
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
class ParentItem(QGraphicsRectItem):
def __init__(self, pos, name, parent=None):
w, h = 550, 220
super().__init__(-w/2, -h/2, w, h)
self.setPos(pos)
self.name = ChildText(self.boundingRect().topLeft() - QPointF(0, 100), f">NAME{name}", self)
self.value = ChildText(self.boundingRect().topLeft()- QPointF(0, 50), f">VALUE_{name}", self)
self.ChildPad1 = ChildPad(QPointF(-150, 0), "1", self)
self.ChildPad2 = ChildPad(QPointF(+150, 0), "2", self)
self.color = QColor(192, 192, 192)
self.setPen(QPen(self.color, 5))
self.setFlag(self.ItemIsMovable, True)
def flipParent(self):
self.setTransformOriginPoint(self.boundingRect().center())
self.setTransform(QTransform.fromScale(-1, 1))
def reflipChilds(self):
# Child Texts
self.name.setTransformOriginPoint(self.name.boundingRect().center())
self.name.setTransform(QTransform.fromScale(-1, 1))
self.value.setTransformOriginPoint(self.value.boundingRect().center())
self.value.setTransform(QTransform.fromScale(-1, 1))
# GrandChild Numbers
for child in self.childItems():
if isinstance(child, ChildPad):
child.Number.setTransformOriginPoint(child.Number.boundingRect().center())
child.Number.setTransform(QTransform.fromScale(-1, 1))
class ChildText(QGraphicsTextItem):
def __init__(self, pos, text=">Text", parent=None):
super().__init__(parent)
self.setPos(pos)
self.parent = parent
self.text = text
self.color = QColor(255, 0, 0)
self.setDefaultTextColor(self.color)
self.setFlag(self.ItemIsMovable, True)
f = QFont()
f.setPointSizeF(min(self.parent.boundingRect().width()/8, self.parent.boundingRect().height()/8))
self.setFont(f)
self.setHtml(f"<p><center>{self.text}</center></p>")
class ChildPad(QGraphicsRectItem):
def __init__(self, pos, pinNumber, parent=None):
w, h = 200, 100
super().__init__(-w/2, -h/2, w, h, parent)
self.setPos(pos)
self.parent = parent
self.color = QColor(255, 0, 0)
self.setPen(QPen(self.color, Qt.MiterJoin, 1))
self.setBrush(QBrush(self.color))
self.Number = GrandChildNumber(pinNumber, self)
class GrandChildNumber(QGraphicsTextItem):
def __init__(self, pinNumber, parent=None):
super().__init__(parent)
self.parent = parent
self.color = QColor(32, 32, 32)
self.setHtml(f"{pinNumber}")
self.moveToParentCenter()
def moveToParentCenter(self):
f = QFont()
f.setPointSizeF(min(self.parent.boundingRect().width()/4, self.parent.boundingRect().height()/4))
self.setFont(f)
rect = self.boundingRect()
rect.moveCenter(self.parent.boundingRect().center())
self.setPos(rect.topLeft())
self.adjustSize()
def main():
import sys
app = QtWidgets.QApplication(sys.argv)
scene = QGraphicsScene()
# No Transformation applied
originalItem = ParentItem(QPointF(300, 100), "ORIGINAL", scene)
scene.addItem(originalItem)
# Flipped the whole parent item
flipParentItem = ParentItem(QPointF(300, 500), "FLIPP_PARENT", scene)
flipParentItem.flipParent()
scene.addItem(flipParentItem)
# Flipped the whole parent item, then reflip the Text and Number
reflipChildItem = ParentItem(QPointF(300, 900), "REFLIP_CHILDS", scene)
flipParentItem.flipParent()
reflipChildItem.reflipChilds()
scene.addItem(reflipChildItem)
view = QtWidgets.QGraphicsView(scene)
view.setRenderHints(QtGui.QPainter.Antialiasing)
view.show()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
If you want to ignore transformations, you should use the ItemIgnoresTransformations flag.
Then, be aware that setTransformation() doesn't apply the transformation "on top" of the existing one, but completely sets a new transformation on the item (transforms inherited from the parent are not considered).
A proper flip() function should toggle the transformation, so it must consider the current transform() and flip it.
Now, the problem with text items is that they always use the origin point as the top left of their contents. While the basic repositioning might work for generic usage, it will not whenever any parent has a transformation.
The problem you're seeing is because you are just mapping the position based on the parent, but since the parent is "flipped", the top left corner of the resulting rect is on the opposite side (relative to the center) in parent coordinates.
To properly get the actual position relative to the parent, you must always use the cumulative transformations, which means map coordinates to the scene and map them back to the parent.
In order to make the code simpler to understand, I moved the repositioning function to the parent of the text item.
class ParentItem(QGraphicsRectItem):
# ...
def flip(self):
self.setTransformOriginPoint(self.boundingRect().center())
self.setTransform(self.transform().scale(-1, 1))
for child in self.childItems():
if isinstance(child, ChildPad):
child.updateNumber()
class ChildPad(QGraphicsRectItem):
def __init__(self, pos, pinNumber, parent=None):
w, h = 200, 100
super().__init__(-w/2, -h/2, w, h, parent)
self.setPos(pos)
self.parent = parent
self.color = QColor(255, 0, 0)
self.setPen(QPen(self.color, Qt.MiterJoin, 1))
self.setBrush(QBrush(self.color))
self.number = GrandChildNumber(pinNumber, self)
self.updateNumber()
def updateNumber(self):
br = self.boundingRect()
f = QFont()
f.setPointSizeF(min(br.width() / 4, br.height() / 4))
self.number.setFont(f)
# get the "visual" rect of the parent in scene coordinates
parentRect = self.mapToScene(br).boundingRect()
rect = self.number.boundingRect()
rect.moveCenter(parentRect.center())
# map the new rect position *from* the scene in local coordinates
topLeft = self.mapFromScene(rect.topLeft())
self.number.setPos(topLeft)
class GrandChildNumber(QGraphicsTextItem):
def __init__(self, pinNumber, parent=None):
super().__init__(parent)
self.parent = parent
self.setFlag(self.ItemIgnoresTransformations)
self.color = QColor(32, 32, 32)
self.setHtml(str(pinNumber))

Synchronize two QGraphicsView with different images

I would like to show two images next to each other, such that when I zoom or pan on one image the other image follows along. My current approach is to emit a viewUpdated event after resolving mouse events. The event contains the viewportTransformation and is used to update the transform in the other view. This sort of works for the zoom part, but panning does not work.
The code below is based on the qt5 version provided in this answer: https://stackoverflow.com/a/35514531/185475
from PyQt5 import QtCore, QtGui, QtWidgets
# Code from https://stackoverflow.com/a/35514531
class PhotoViewer(QtWidgets.QGraphicsView):
photoClicked = QtCore.pyqtSignal(QtCore.QPoint)
viewUpdated = QtCore.pyqtSignal(QtGui.QTransform)
def __init__(self, parent):
super(PhotoViewer, self).__init__(parent)
self._zoom = 0
self._empty = True
self._scene = QtWidgets.QGraphicsScene(self)
self._photo = QtWidgets.QGraphicsPixmapItem()
self._scene.addItem(self._photo)
self.setScene(self._scene)
self.setTransformationAnchor(QtWidgets.QGraphicsView.AnchorUnderMouse)
self.setResizeAnchor(QtWidgets.QGraphicsView.AnchorUnderMouse)
self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
self.setBackgroundBrush(QtGui.QBrush(QtGui.QColor(30, 30, 30)))
self.setFrameShape(QtWidgets.QFrame.NoFrame)
def hasPhoto(self):
return not self._empty
def fitInView(self, scale=True):
rect = QtCore.QRectF(self._photo.pixmap().rect())
if not rect.isNull():
self.setSceneRect(rect)
if self.hasPhoto():
unity = self.transform().mapRect(QtCore.QRectF(0, 0, 1, 1))
self.scale(1 / unity.width(), 1 / unity.height())
viewrect = self.viewport().rect()
scenerect = self.transform().mapRect(rect)
factor = min(viewrect.width() / scenerect.width(),
viewrect.height() / scenerect.height())
self.scale(factor, factor)
self._zoom = 0
def setPhoto(self, pixmap=None):
self._zoom = 0
if pixmap and not pixmap.isNull():
self._empty = False
self.setDragMode(QtWidgets.QGraphicsView.ScrollHandDrag)
self._photo.setPixmap(pixmap)
else:
self._empty = True
self.setDragMode(QtWidgets.QGraphicsView.NoDrag)
self._photo.setPixmap(QtGui.QPixmap())
self.fitInView()
def wheelEvent(self, event):
if self.hasPhoto():
if event.angleDelta().y() > 0:
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.fitInView()
else:
self._zoom = 0
self.viewUpdated.emit(self.viewportTransform())
def toggleDragMode(self):
if self.dragMode() == QtWidgets.QGraphicsView.ScrollHandDrag:
self.setDragMode(QtWidgets.QGraphicsView.NoDrag)
elif not self._photo.pixmap().isNull():
self.setDragMode(QtWidgets.QGraphicsView.ScrollHandDrag)
def mousePressEvent(self, event):
if self._photo.isUnderMouse():
self.photoClicked.emit(self.mapToScene(event.pos()).toPoint())
super(PhotoViewer, self).mousePressEvent(event)
self.viewUpdated.emit(self.viewportTransform())
def set_transform(self, transform):
self.setTransform(transform)
class Window(QtWidgets.QWidget):
def __init__(self):
super(Window, self).__init__()
self.viewer = PhotoViewer(self)
self.viewerSecondImage = PhotoViewer(self)
self.viewer.viewUpdated.connect(self.viewerSecondImage.set_transform)
self.viewerSecondImage.viewUpdated.connect(self.viewer.set_transform)
# 'Load image' button
self.btnLoad = QtWidgets.QToolButton(self)
self.btnLoad.setText('Load image')
self.btnLoad.clicked.connect(self.loadImage)
# Button to change from drag/pan to getting pixel info
self.btnPixInfo = QtWidgets.QToolButton(self)
self.btnPixInfo.setText('Enter pixel info mode')
self.btnPixInfo.clicked.connect(self.pixInfo)
self.editPixInfo = QtWidgets.QLineEdit(self)
self.editPixInfo.setReadOnly(True)
self.viewer.photoClicked.connect(self.photoClicked)
# Arrange layout
VBlayout = QtWidgets.QVBoxLayout(self)
HBlayoutImageViewers = QtWidgets.QHBoxLayout()
HBlayoutImageViewers.addWidget(self.viewer)
HBlayoutImageViewers.addWidget(self.viewerSecondImage)
VBlayout.addLayout(HBlayoutImageViewers)
HBlayout = QtWidgets.QHBoxLayout()
HBlayout.setAlignment(QtCore.Qt.AlignLeft)
HBlayout.addWidget(self.btnLoad)
HBlayout.addWidget(self.btnPixInfo)
HBlayout.addWidget(self.editPixInfo)
VBlayout.addLayout(HBlayout)
def loadImage(self):
self.viewer.setPhoto(QtGui.QPixmap('input/490px-Dostojka_adype.jpg'))
self.viewerSecondImage.setPhoto(QtGui.QPixmap('input/490px-Dostojka_adype.jpg'))
def pixInfo(self):
self.viewer.toggleDragMode()
def photoClicked(self, pos):
if self.viewer.dragMode() == QtWidgets.QGraphicsView.NoDrag:
self.editPixInfo.setText('%d, %d' % (pos.x(), pos.y()))
if __name__ == '__main__':
import sys
app = QtWidgets.QApplication(sys.argv)
window = Window()
window.setGeometry(500, 300, 800, 600)
window.show()
sys.exit(app.exec_())
You only need to syncronize scrollbar values of two QGraphicsViews
def bindScrollBars(scrollBar1, scrollBar2):
# syncronizing scrollbars syncrnonously somehow breaks zooming and doesn't work
# scrollBar1.valueChanged.connect(lambda value: scrollBar2.setValue(value))
# scrollBar2.valueChanged.connect(lambda value: scrollBar1.setValue(value))
# syncronizing scrollbars asyncronously works ok
scrollBar1.valueChanged.connect(lambda _: QtCore.QTimer.singleShot(0, lambda: scrollBar2.setValue(scrollBar1.value())))
scrollBar2.valueChanged.connect(lambda _: QtCore.QTimer.singleShot(0, lambda: scrollBar1.setValue(scrollBar2.value())))
class Window(QtWidgets.QWidget):
def __init__(self):
...
bindScrollBars(self.viewer.horizontalScrollBar(), self.viewerSecondImage.horizontalScrollBar())
bindScrollBars(self.viewer.verticalScrollBar(), self.viewerSecondImage.verticalScrollBar())
Also wheelEvent can be simplified
def wheelEvent(self, event):
if self.hasPhoto():
factor = 1.25
if event.angleDelta().y() > 0:
self.scale(factor, factor)
else:
self.scale(1/factor, 1/factor)
self.viewUpdated.emit(self.transform())

PyQt5 - How to calculate corner points of a QGraphicsRectItem after rotation by its center point?

My problem is that I couldn't find the pixel values of each corner points of a HighwayItem (which is a QGraphicsRectItem) after rotation it by angle theta about the center point of it.
I used the Rotation Matrix which explained here and I also looked thisexplanation. But, I cannot find the true values.
Any help will be great. Thanks.
Here is MapViewer() class. A HighwayItem is created in this view.
from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5.QtGui import QPixmap
from PyQt5.QtCore import Qt, QPoint, QPointF, QRectF
from PyQt5.QtWidgets import QGraphicsScene, \
QGraphicsView, QGraphicsPixmapItem, \
from class_graphical_items import HighwayItem
class MapViewer(QGraphicsView):
def __init__(self, parent, ui):
super(MapViewer, self).__init__(parent)
self.ui = ui
# Attributes for highway
self.add_highway_control = False
self.current_highway = None
self.start = QPointF()
self.hw_counter = 0
self._scene = QGraphicsScene(self)
self._map = QGraphicsPixmapItem()
self._scene.addItem(self._map)
self.setScene(self._scene)
self.setTransformationAnchor(QGraphicsView.AnchorUnderMouse)
self.setResizeAnchor(QGraphicsView.AnchorUnderMouse)
self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.setFrameShape(QtWidgets.QFrame.NoFrame)
def mousePressEvent(self, event):
if self._map.isUnderMouse():
if self.add_highway_control:
# Create a yellow highway
self.current_highway = HighwayItem(self._scene, self.ui)
self.hw_counter += 1
self.start = self.mapToScene(event.pos()).toPoint()
r = QRectF(self.start, self.start)
self.current_highway.setRect(r)
self._scene.addItem(self.current_highway)
# When adding HW, set drag mode NoDrag
self.setDragMode(QGraphicsView.NoDrag)
super(MapViewer, self).mousePressEvent(event)
def mouseMoveEvent(self, event):
if self.add_highway_control and self.current_highway is not None:
# When adding HW, set drag mode NoDrag
self.setDragMode(QGraphicsView.NoDrag)
r = QRectF(self.start, self.mapToScene(event.pos()).toPoint()).normalized()
self.current_highway.setRect(r)
super(MapViewer, self).mouseReleaseEvent(event)
def mouseReleaseEvent(self, event):
if self.add_highway_control:
if self.current_highway is not None:
# When finish the adding HW, set drag mode ScrollHandDrag
self.setDragMode(QGraphicsView.ScrollHandDrag)
self.update_item_dict(self.current_highway)
self.update_item_table(self.current_highway)
self.current_highway = None
self.add_highway_control = False
super(MapViewer, self).mouseReleaseEvent(event)
This is the HighwayItem class. It has some specs like color, opacity etc.
By doubleclicking on created HighwayItem, I'm activating a spinbox which was in a QTreeWidget in main window (ui).
By changing the spinbox value, the user can rotate the item.
class HighwayItem(QGraphicsRectItem):
def __init__(self, scene, ui):
QGraphicsRectItem.__init__(self)
self.scene = scene
self.ui = ui
self.setBrush(QtCore.Qt.yellow)
self.setOpacity(0.5)
self.setZValue(4.0)
self.setFlag(QGraphicsItem.ItemIsMovable, True)
self.setFlag(QGraphicsItem.ItemIsSelectable, True)
self.setFlag(QGraphicsItem.ItemSendsGeometryChanges, True)
self.setFlag(QGraphicsItem.ItemIsFocusable, True)
self.setAcceptHoverEvents(True)
# Here, I'm activating the spinbox by double clicking
# on HighwayItem. In spinbox, I'm entering the rotation angle
# of HighwayItem.
def mouseDoubleClickEvent(self, event):
selected_item = self.scene.selectedItems()
if selected_item:
for i in range(self.ui.treeWidget_objects.topLevelItemCount()):
toplevel_item = self.ui.treeWidget_objects.topLevelItem(i)
heading_item = toplevel_item.child(2)
spinbox = self.ui.treeWidget_objects.itemWidget(heading_item, 2)
if str(toplevel_item.text(2)) == str(selected_item[0]):
if 'HighwayItem' in str(selected_item[0]):
spinbox.setEnabled(True)
else:
spinbox.setEnabled(False)
This is the HWHeadingSpinBox() class which sets the rotation angle of HWItem. My problem starts here. In rotate_hw() method, I am transforming the created HighwayItem by its center point and giving it a rotation by its center point.
BUT, when I try to calculate new corners of hw in calc_rotated_coords() method, I'm messing up.
class HWHeadingSpinBox(QSpinBox):
def __init__(self, viewer, selected_hw):
QSpinBox.__init__(self)
self.selected_hw = selected_hw
self.viewer = viewer
# First coords of HW
tl = self.selected_hw.rect().topLeft()
tr = self.selected_hw.rect().topRight()
br = self.selected_hw.rect().bottomRight()
bl = self.selected_hw.rect().bottomLeft()
self.temp_list = [tl, tr, br, bl]
self.setRange(-180, 180)
self.setSuffix('°')
self.setEnabled(False)
self.valueChanged.connect(self.rotate_hw)
def heading_val(self):
return self.value()
def rotate_hw(self):
angle = self.heading_val()
self.selected_hw.prepareGeometryChange()
offset = self.selected_hw.boundingRect().center()
self.selected_hw.sceneBoundingRect().center()
transform = QTransform()
transform.translate(offset.x(), offset.y())
transform.rotate(-angle)
transform.translate(-offset.x(), -offset.y())
self.selected_hw.setTransform(transform)
# br_rect = self.selected_hw.sceneBoundingRect()
# sbr_rect = self.selected_hw.sceneBoundingRect()
# r_rect = self.selected_hw.sceneBoundingRect()
#
# rectitem = QtWidgets.QGraphicsRectItem(br_rect)
# rectitem.setBrush(Qt.red)
# self.viewer._scene.addItem(rectitem)
#
# rectitem = QtWidgets.QGraphicsRectItem(sbr_rect)
# rectitem.setBrush(Qt.green)
# self.viewer._scene.addItem(rectitem)
#
# rectitem = QtWidgets.QGraphicsRectItem(r_rect)
# rectitem.setBrush(Qt.blue)
# self.viewer._scene.addItem(rectitem)
def calc_rotated_coords(self):
# center point
cx = self.selected_hw.rect().center().x()
cy = self.selected_hw.rect().center().y()
# rotation angle
theta = math.radians(angle)
rotated_corners = []
for item in self.temp_list:
x = item.x()
y = item.y()
temp_x = x - cx
temp_y = y - cy
rot_x = temp_x * math.cos(theta) + temp_y * math.sin(theta)
rot_y = -temp_x * math.sin(theta) + temp_y * math.cos(theta)
rotated_corners.append([rot_x, rot_y])
self.temp_list = rotated_corners
print("\nPIXEL VALUES OF HW: \n{}".format(self.temp_list))
Here is the solution:
I added the itemChange(self, change, value) event in to HighwayItem and if change is ItemPositionHasChanged, I calculated all items' corners as such:
def itemChange(self, change, value):
if change == QGraphicsItem.ItemPositionHasChanged:
top_left = self.mapToScene(self.rect().topLeft())
top_right = self.mapToScene(self.rect().topRight())
bottom_left = self.mapToScene(self.rect().bottomLeft())
bottom_right = self.mapToScene(self.rect().bottomRight())
changed_pos = [top_left, top_right, bottom_right, bottom_left]
return super(HighwayItem, self).itemChange(change, value)

How do I rebuild a gridLayout during resize?

I am trying to catch resize event of the window, and when I do, basically delete all the widget items in a gridLayout and rebuild the rows/columns to fit the new resized window. I am having trouble getting this to work properly and not sure if this is the best method that I have used. Right now two issues happen:
It doesn't seem to be deleting items, rebuilding and adding the columns properly as I resize the window bigger (some items delete, some get added, but seem to just overlap and never fit to the new window size).
Resize seems to get called on start/creation of the window.
class Window (QtGui.QMainWindow):
def __init__(self):
super(Window, self).__init__()
self.thumbs = []
self.thumbWidgets = []
self._resize_timer = None
self.resizeCompleted.connect(self.handleResizeCompleted)
self.setGeometry(100, 100, 800, 600)
self.home()
def home(self):
self.centralwidget = QtGui.QWidget(self)
'''MainLAYOUT
'''
self.mainLayout = QtGui.QVBoxLayout(self.centralwidget)
self.thumb_tab_QGroupBox = QtGui.QGroupBox(self.centralwidget)
'''GroupBoxLAYOUT
'''
self.vLayout = QtGui.QVBoxLayout(self.thumb_tab_QGroupBox)
self.vLayout.setObjectName("GroupVLayout")
#Scroll Area
self.thumbScrollArea = QtGui.QScrollArea(self.thumb_tab_QGroupBox)
self.thumbScrollArea.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
self.thumbScrollArea.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
self.thumbScrollArea.setWidgetResizable(True)
self.thumbScrollArea.setAlignment(QtCore.Qt.AlignLeft)
self.thumbScrollArea.setObjectName("thumb_scrollArea")
self.scrollAreaWidgetContents = QtGui.QWidget()
self.scrollAreaWidgetContents.setMinimumSize(QtCore.QSize(840, scrollAreaX))
self.scrollAreaWidgetContents.setObjectName("scrollAreaWidgetContents")
self.thumbScrollArea.setWidget(self.scrollAreaWidgetContents)
self.vLayout.addWidget(self.thumbScrollArea)
self.mainLayout.addWidget(self.thumb_tab_QGroupBox)
#Grid in Scroll Area
self.gridLayoutWidget = QtGui.QWidget(self.scrollAreaWidgetContents)
self.gridLayoutWidget.setObjectName("gridLayoutWidget")
self.gridLayout_QGridLayout = QtGui.QGridLayout(self.gridLayoutWidget)
self.gridLayout_QGridLayout.setObjectName("gridLayout")
#Loads thumbnails
self.getThumbnails()
self.mainLayout.setAlignment(QtCore.Qt.AlignLeft)
self.setCentralWidget(self.centralwidget)
def resizeEvent(self, resizeEvent):
self.updateResizeTimer(300)
def updateResizeTimer(self, interval=None):
if self._resize_timer is not None:
self.killTimer(self._resize_timer)
if interval is not None:
self._resize_timer = self.startTimer(interval)
else:
self._resize_timer = None
def timerEvent(self, event):
if event.timerId() == self._resize_timer:
self.updateResizeTimer()
self.resizeCompleted.emit()
def handleResizeCompleted(self):
print('resize complete')
# Get new window size on resize
width = self.centralwidget.frameGeometry().width()
height = self.centralwidget.frameGeometry().height()
thumbsPerRow = width / 200
print "numThumbnails per Width", thumbsPerRow
self.gridLayoutWidget.adjustSize()
self.gridLayout_QGridLayout.maximumSize()
for widget in self.thumbWidgets:
print "Removing widget", widget
self.gridLayout_QGridLayout.removeWidget(widget)
#widget.deleteLater()
self.populate(self.thumbWidgets, QtCore.QSize(200,200), thumbsPerRow)
def queryThumbnailCount(self):
....
...
..
return sizeX
def getThumbnails(self):
.....
....
...
.
self.createThumbWidgets(self.thumbs, QtCore.QSize(200,200))
self.populate(self.thumbs, QtCore.QSize(200,200))
def createThumbWidgets(self, pics, size, imagesPerRow=4, flags=QtCore.Qt.KeepAspectRatioByExpanding):
for pic in pics:
label = QtGui.QLabel("")
pixmap = QtGui.QPixmap(pic)
pixmap = pixmap.scaled(size, flags)
label.setPixmap(pixmap)
self.thumbWidgets.append(label)
#Add thumbnails to grid
def populate(self, pics, size, imagesPerRow=6, flags=QtCore.Qt.KeepAspectRatioByExpanding):
row = col = 0
for widget in self.thumbWidgets:
print "Adding Image to column "+str(col)
self.gridLayout_QGridLayout.addWidget(widget, row, col)
col +=1
if col % imagesPerRow == 0:
row += 1
col = 0
GUI = Window()
GUI.show()
This might be better achieved with a QGraphicsView.
Create a subclass of a QGraphicsView that also creates a QGraphicsScene for itself. Have it store the list of pixmaps you want it to display. Override the resizeEvent in your subclass and have it clear the QGraphicsScene and re-add all the pixmaps to the scene using QGraphicsPixmapItems. Before you add them to the scene, get the total width and height from the QGraphicsView.viewport(). You can get the individual pixmap width/height by dividing by rows/columns. Then scale each pixmap before you add it to the scene.