Pyqt Embedding Graphics object in Widget - python

So I have a main window. I use setCentralWidget to set it to a class I made called mainWindowWidget to handle the UI.
I am now trying to add a graphics view widget I made to that but cannot seem to get anything to display. If I set the graphics view as the central widget I can see it and all my behaviour is working.
Do I need to do anything to get the graphics view to display within another widget?
Below are the sections of code I think are relevant to the question followed by the entire application. I am really new to PyQt and any guidance would be appreciated.
class mainWindowWidget(QtGui.QWidget):
grid = None
scene = None
def __init__(self):
self.initScene()
QtGui.QWidget.__init__(self)
def initScene(self):
self.grid = QtGui.QGridLayout()
'''Node Interface'''
self.scene = Scene(0, 0, 1280, 720, self)
self.view = QtGui.QGraphicsView()
self.view.setScene(self.scene)
self.grid.addWidget(self.view)
'''AttributeWindow'''
class MainWindowUi(QtGui.QMainWindow):
def __init__(self):
mainDataGraber = ind.dataGraber()
QtGui.QMainWindow.__init__(self)
self.setWindowTitle('RIS RIB Generator')
mainwindowwidget = mainWindowWidget()
self.setCentralWidget(mainwindowwidget)
This is the main GUI file for the application
#!/usr/bin/python
# -*- coding: utf-8 -*-
"""
This is the base py file for the GUI
Todo list
-----------------
- Pop up menu for adding new Nodes
- node connectivity
- create data structure for storing
"""
#import code mods
import sys
import uuid
import gtk, pygtk
from PyQt4 import QtGui, QtCore
from array import *
#import StyleMod
import RRG_NodeInterfaceGUIStyle as ns
import RRG_importNodeData as ind
"""
Base class for a node. Contains all the inilization, drawing, and containing inputs and outputs
"""
class node(QtGui.QGraphicsRectItem):
nid = 0
width = ns.nodeWidth
height = ns.nodeHeight
color = ns.nodeBackgroundColor
alpha = ns.nodeBackgroundAlpha
x = 90
y = 60
inputs=[]
outputs=[]
viewObj = None
isNode = True
scene = None
def __init__(self, n_x, n_y, n_width,n_height, n_scene):
QtGui.QGraphicsRectItem.__init__(self, n_x, n_y, n_width, n_height)
self.width = n_width
self.height = n_height
self.x = n_x
self.y = n_y
self.scene = n_scene
self.nid = uuid.uuid4()
print(self.nid)
self.setFlag(QtGui.QGraphicsItem.ItemIsMovable, True)
self.iniNodeData()
def mousePressEvent(self, e):
print("Square got mouse press event")
print("Event came to us accepted: %s"%(e.isAccepted(),))
a = []
self.scene.selectedNodes = a
self.scene.selectedNodes.append(self)
self.scene.selectedNodeID = self.nid
QtGui.QGraphicsRectItem.mousePressEvent(self, e)
def mouseReleaseEvent(self, e):
#print("Square got mouse release event")
#print("Event came to us accepted: %s"%(e.isAccepted(),))
QtGui.QGraphicsRectItem.mouseReleaseEvent(self, e)
"""
This is where inputs and outputs will be created based on node type
"""
def iniNodeData(self):
print('making node data')
for j in range(5):
this = self
x = input(this,0, 0+(j*10),self.scene)
self.inputs.append(x)
for k in range(5):
this = self
x = output(this, self.width-10, 0+(k*10),self.scene)
self.outputs.append(x)
def mouseMoveEvent(self, event):
self.scene.updateConnections()
QtGui.QGraphicsRectItem.mouseMoveEvent(self, event)
def nid(self):
return self.nid
"""
Nodes will evaluate from the last node to the first node, therefore inputs are evaluted
"""
class input(QtGui.QGraphicsRectItem):
currentConnectedNode = None
currentConnectedOutput = None
parentNode = None
width = 10
height = 10
x = 1
y = 1
color = 1
drawItem = None
isOutput = False
isNode = False
scene = None
points = []
line = None
def __init__(self, pnode, posX, posY, n_scene):
self.parentNode = pnode
self.x = posX
self.y = posY
self.color = 1
self.scene = n_scene
QtGui.QGraphicsRectItem.__init__(self, self.x+self.parentNode.x, self.y+self.parentNode.y, self.width, self.height, self.parentNode)
self.setFlag(QtGui.QGraphicsItem.ItemIsSelectable, True)
'''
This will handel connections. It determins if a connection is allowed aswell as creates them
'''
def mousePressEvent(self, e):
#print("Square got mouse press event")
#print("Event came to us accepted: %s"%(e.isAccepted(),))
emptyArray = []
if len(self.scene.selectedNodes) > 0 or len(self.scene.selectedNodes) > 1:
a = self.scene.selectedNodes[0]
if a.isNode == False:
if a.parentNode != self.parentNode:
if a.isOutput == True and self.isOutput == False:
print('Output and Input! line test runnin....')
currentConnectedOutput = a
currentConnectedNode = a.parentNode
if self.line != None:
self.line = None
self.scene.addConnection(self, a)
elif a.isOutput == True and self.isOutput == True:
print('Output and Output')
elif a.isOutput == False and self.isOutput == False:
print('Input and Input')
self.scene.selectedNodes = emptyArray
else:
self.scene.selectedNodes = emptyArray
else:
self.scene.selectedNodes = emptyArray
self.scene.selectedNodes.append(self)
else:
self.scene.selectedNodes.append(self)
QtGui.QGraphicsRectItem.mousePressEvent(self, e)
'''
Output value from a node
'''
class output(QtGui.QGraphicsRectItem):
parentNode = None
width = 10
height = 10
x = 0
y = 0
isOutput = True
isNode = False
scene = None
def __init__(self, pnode, posX, posY, n_scene):
self.parentNode = pnode
self.x = posX
self.y = posY
self.color = 1
self.scene = n_scene
QtGui.QGraphicsRectItem.__init__(self, self.x+self.parentNode.x, self.y+self.parentNode.y, self.width, self.height, self.parentNode)
self.setFlag(QtGui.QGraphicsItem.ItemIsSelectable, True)
'''
This will handel connections. It determins if a connection is allowed aswell as creates them
'''
def mousePressEvent(self, e):
#print("Square got mouse press event")
#print("Event came to us accepted: %s"%(e.isAccepted(),))
emptyArray = []
if len(self.scene.selectedNodes) > 0 or len(self.scene.selectedNodes) > 1:
a = self.scene.selectedNodes[0]
if a.isNode == False:
if a.parentNode != self.parentNode:
if a.isOutput == False and self.isOutput == True:
print('Input and Output!')
a.currentConnectedOutput = self
a.currentConnectedNode = self.parentNode
elif a.isOutput == True and self.isOutput == False:
print('Output and Input!')
elif a.isOutput == True and self.isOutput == True:
print('Output and Output')
elif a.isOutput == False and self.isOutput == False:
print('Input and Input')
self.scene.selectedNodes = emptyArray
else:
self.scene.selectedNodes = emptyArray
else:
self.scene.selectedNodes = emptyArray
self.scene.selectedNodes.append(self)
else:
self.scene.selectedNodes.append(self)
QtGui.QGraphicsRectItem.mousePressEvent(self, e)
class connection(QtGui.QGraphicsLineItem):
usedNodeIDs = []
inputItem = None
outputItem = None
x1 = 0.0
y1 = 0.0
x2 = 0.0
y2 = 0.0
nid = None
scene = None
def __init__(self, n_inputItem, n_outputItemm, n_scene):
self.inputItem = n_inputItem
self.outputItem = n_outputItemm
self.usedNodeIDs.append(self.inputItem.parentNode.nid)
self.usedNodeIDs.append(self.outputItem.parentNode.nid)
self.updatePos()
QtGui.QGraphicsLineItem.__init__(self, self.x1, self.y1, self.x2, self.y2)
self.setFlag(QtGui.QGraphicsItem.ItemIsSelectable, True)
self.scene = n_scene
self.nid = uuid.uuid4()
def update(self):
self.updatePos()
self.setLine(self.x1, self.y1, self.x2, self.y2)
def updatePos(self):
scenePosInput = QtGui.QGraphicsItem.pos(self.inputItem)
scenePosOutput = QtGui.QGraphicsItem.pos(self.outputItem)
scenePosInputNode = QtGui.QGraphicsItem.pos(self.inputItem.parentNode)
scenePosOutputNode = QtGui.QGraphicsItem.pos(self.outputItem.parentNode)
self.x1 = (scenePosInputNode.x()+self.inputItem.parentNode.x)+(scenePosInput.x()+self.inputItem.x) + ns.inputWidth/2
self.y1 = (scenePosInputNode.y()+self.inputItem.parentNode.y)+(scenePosInput.y()+self.inputItem.y) + ns.inputHeight/2
self.x2 = (scenePosOutputNode.x()+self.outputItem.parentNode.x)+(scenePosOutput.x()+self.outputItem.x) + ns.outputWidth/2
self.y2 = (scenePosOutputNode.y()+self.outputItem.parentNode.y)+(scenePosOutput.y()+self.outputItem.y) + ns.outputHeight/2
def mousePressEvent(self, e):
self.scene.selectedNodeID = self.nid
QtGui.QGraphicsLineItem.mousePressEvent(self, e)
'''
Check Click events on the scene Object
Also Stores the node data
'''
class Scene(QtGui.QGraphicsScene):
nodes = []
connections = []
selectedNodeID = None
def __init__(self, x, y, w, h, p):
super(Scene, self).__init__()
self.width = w
self.height = h
def mousePressEvent(self, e):
#print("Scene got mouse press event")
#print("Event came to us accepted: %s"%(e.isAccepted(),))
QtGui.QGraphicsScene.mousePressEvent(self, e)
def mouseReleaseEvent(self, e):
#print("Scene got mouse release event")
#print("Event came to us accepted: %s"%(e.isAccepted(),))
QtGui.QGraphicsScene.mouseReleaseEvent(self, e)
def dragMoveEvent(self, e):
QtGui.QGraphicsScene.dragMoveEvent(self, e)
def updateConnections(self):
for connect in self.connections:
connect.update()
def addNode(self):
newNode = node(250,250,100,150, self)
self.addItem(newNode)
self.nodes.append(newNode)
def addPatternNode(self):
newNode = node(250,250,100,150, self)
self.addItem(newNode)
self.nodes.append(newNode)
def addConnection(self, n_inputItem, n_outputItem):
newConnection = connection(n_inputItem, n_outputItem, self)
self.addItem(newConnection)
self.connections.append(newConnection)
def keyPressEvent(self, e):
#Delete a node after it have been clicked on
#Use the node ID as the unique ID of the node to delete
if e.key() == QtCore.Qt.Key_Delete or e.key() == QtCore.Qt.Key_Backspace:
#If nothing selected
if self.selectedNodeID != None:
isConnection = False
for j in range(len(self.connections)):
if self.connections[j].nid == self.selectedNodeID:
isConnection = True
self.removeItem(self.connections[j])
self.connections.remove(self.connections[j])
if isConnection != True:
#first remove connections
rmItem = False
connectionsToRemove = []
for connect in self.connections:
rmItem = False
for nid in connect.usedNodeIDs:
if nid == self.selectedNodeID:
if rmItem == False:
connectionsToRemove.append(connect)
rmItem = True
for removeThis in connectionsToRemove:
self.connections.remove(removeThis)
self.removeItem(removeThis)
#now remove the nodes
for j in range(len(self.nodes)):
print(self.nodes[j].nid)
#figure out which node in our master node list must be deleted
if self.nodes[j].nid == self.selectedNodeID:
self.removeItem(self.nodes[j])
self.nodes.remove(self.nodes[j])
self.selectedNodeID = None
class mainWindowWidget(QtGui.QWidget):
grid = None
scene = None
def __init__(self):
self.initScene()
QtGui.QWidget.__init__(self)
def initScene(self):
self.grid = QtGui.QGridLayout()
'''Node Interface'''
self.scene = Scene(0, 0, 1280, 720, self)
self.view = QtGui.QGraphicsView()
self.view.setScene(self.scene)
self.grid.addWidget(self.view)
'''AttributeWindow'''
class MainWindowUi(QtGui.QMainWindow):
def __init__(self):
mainDataGraber = ind.dataGraber()
QtGui.QMainWindow.__init__(self)
self.setWindowTitle('RIS RIB Generator')
mainwindowwidget = mainWindowWidget()
self.setCentralWidget(mainwindowwidget)
exitAction = QtGui.QAction(QtGui.QIcon('exit24.png'), 'Exit', self)
exitAction.setShortcut('Ctrl+Q')
exitAction.setStatusTip('Exit application')
exitAction.triggered.connect(self.close)
newNodeAction = QtGui.QAction(QtGui.QIcon('exit24.png'), 'New Node', self)
newNodeAction.setStatusTip('Add a blank node')
newNodeAction.triggered.connect(mainwindowwidget.scene.addPatternNode)
nodeCreationActions = []
for nodeType in mainDataGraber.abstractNodeObjects:
nodeName = nodeType.nName
nodeType = nodeType.nType
#nodeStatusTip = nodeType.nhelp
newNodeAction = QtGui.QAction(QtGui.QIcon('exit24.png'), nodeName, self)
newNodeAction.setStatusTip('nodeType.nhelp')
if nodeType == 'pattern':
newNodeAction.triggered.connect(mainwindowwidget.scene.addPatternNode)
nodeCreationActions.append(newNodeAction)
newNodeAction = QtGui.QAction(QtGui.QIcon('exit24.png'), 'New Node', self)
newNodeAction.setStatusTip('Add a blank node')
newNodeAction.triggered.connect(mainwindowwidget.scene.addPatternNode)
self.statusBar()
menubar = self.menuBar()
fileMenu = menubar.addMenu('&File')
fileMenu.addAction(newNodeAction)
nodeMenu = menubar.addMenu('&Nodes')
for action in nodeCreationActions:
nodeMenu.addAction(action)
fileMenu.addAction(exitAction)
'''
Start Point
'''
if __name__ == '__main__':
app = QtGui.QApplication(sys.argv)
win = MainWindowUi()
win.show()
sys.exit(app.exec_())

Your issue is that you don't specify a parent for the QGridLayout (in the mainWindowWidget class), so it isn't attached to a widget. This results in the layout (and all widgets contained within it) not being visible. Adding a parent to the layout reveals a second problem in that you try and do things with the QWidget before calling __init__.
The corrected code is thus:
class mainWindowWidget(QtGui.QWidget):
grid = None
scene = None
def __init__(self):
QtGui.QWidget.__init__(self)
self.initScene()
def initScene(self):
self.grid = QtGui.QGridLayout(self)
'''Node Interface'''
self.scene = Scene(0, 0, 1280, 720, self)
self.view = QtGui.QGraphicsView()
self.view.setScene(self.scene)
self.grid.addWidget(self.view)
Note: For future questions requiring debugging help, please make a minimilistic example that is runnable. Don't just dump 90% of your code in a stack overflow post. It's not fun trying to dig through random code trying to cut out the missing imports so that it still reproduces the problem (fortunately it wasn't too difficult in this case). See How to create a Minimal, Complete, and Verifiable example.
Note 2: Why are you importing pygtk into a qt app?

Related

Widget selection problem with rubberband in 2 layouts

Currently, 2 layouts have been added, and buttons are added using FlowLayout at the top and below the label.
on Qrubberband
When the mouse is don't move, there is nothing wrong. When I drag, there is a problem with the selection.
Where did the problem come from?
# -*- coding: utf-8 -*-
from PySide2.QtCore import *
from PySide2.QtGui import *
from PySide2.QtWidgets import *
class QToolButton(QToolButton):
def __init__(self, label='', icon='', icon_size=100):
super(QToolButton, self).__init__()
self.label = label
self.icon = icon
self.icon_size = icon_size
self.create()
def create(self):
self.setText(self.label)
self.setIcon(QIcon(self.icon))
self.setIconSize(QSize(self.icon_size, self.icon_size))
self.setToolButtonStyle(Qt.ToolButtonTextUnderIcon)
self.setCheckable(True)
self.setFocusPolicy(Qt.NoFocus)
class Window(QMainWindow):
def __init__(self):
super(Window, self).__init__()
self.rubberBand = QRubberBand(QRubberBand.Rectangle, self)
self.ui()
self.add_thumbnail()
def ui(self):
self.setContentsMargins(2, 2, 2, 2)
self.resize(1000, 800)
self.centralwidget = QWidget(self)
self.gridLayout = QGridLayout(self.centralwidget)
self.label = QLabel(self.centralwidget)
self.label.setText('LABEL IMAGE')
self.label.setMinimumSize(QSize(0, 100))
self.label.setMaximumSize(QSize(16777215, 100))
self.label.setAlignment(Qt.AlignCenter)
self.gridLayout.addWidget(self.label, 0, 0, 1, 1)
self.frame = QFrame(self.centralwidget)
self.frame.setFrameShape(QFrame.StyledPanel)
self.frame.setFrameShadow(QFrame.Raised)
self.gridLayout.addWidget(self.frame, 1, 0, 1, 1)
self.setCentralWidget(self.centralwidget)
self.flow_layout = FlowLayout()
self.frame.setLayout(self.flow_layout)
def add_thumbnail(self):
for i in range(10):
button = QToolButton(label='test', icon='test')
self.flow_layout.addWidget(button)
button.installEventFilter(self)
def eventFilter(self, source, event):
if event.type() == event.MouseButtonPress:
self.origin = source.mapTo(self, event.pos())
elif event.type() == event.MouseMove and event.buttons():
if not self.rubberBand.isVisible():
distance = (source.mapTo(self, event.pos()) - self.origin).manhattanLength()
if distance > QApplication.startDragDistance():
if isinstance(source, QAbstractButton) and source.isDown():
source.setDown(False)
self.rubberBand.show()
if self.rubberBand.isVisible():
self.resizeRubberBand(source.mapTo(self, event.pos()))
event.accept()
return True
elif event.type() == event.MouseButtonRelease and self.rubberBand.isVisible():
self.closeRubberBand()
event.accept()
return True
return super(Window, self).eventFilter(source, event)
def startRubberBand(self, pos):
self.origin = pos
self.rubberBand.setGeometry(
QRect(self.origin, QSize()))
self.rubberBand.show()
def resizeRubberBand(self, pos):
if self.rubberBand.isVisible():
self.rubberBand.setGeometry(
QRect(self.origin, pos).normalized())
def closeRubberBand(self):
if self.rubberBand.isVisible():
self.rubberBand.hide()
selected = []
rect = self.rubberBand.geometry()
for child in self.findChildren(QToolButton):
if rect.intersects(child.geometry()):
selected.append(child)
if selected:
for i in selected:
if i.isChecked():
i.setChecked(False)
else:
i.setChecked(True)
def mousePressEvent(self, event):
self.startRubberBand(event.pos())
QMainWindow.mousePressEvent(self, event)
def mouseMoveEvent(self, event):
self.resizeRubberBand(event.pos())
QMainWindow.mouseMoveEvent(self, event)
def mouseReleaseEvent(self, event):
self.closeRubberBand()
QMainWindow.mouseReleaseEvent(self, event)
class FlowLayout(QLayout):
def __init__(self, parent=None, margin=0, spacing=-1):
super(FlowLayout, self).__init__(parent)
if parent is not None:
self.setContentsMargins(margin, margin, margin, margin)
self.setSpacing(spacing)
self.margin = margin
# spaces between each item
self.spaceX = 2
self.spaceY = 2
self.itemList = []
def __del__(self):
item = self.takeAt(0)
while item:
item = self.takeAt(0)
def addItem(self, item):
self.itemList.append(item)
def count(self):
return len(self.itemList)
def itemAt(self, index):
if index >= 0 and index < len(self.itemList):
return self.itemList[index]
return None
def takeAt(self, index):
if index >= 0 and index < len(self.itemList):
return self.itemList.pop(index)
return None
def expandingDirections(self):
return Qt.Orientations(Qt.Orientation(0))
def hasHeightForWidth(self):
return True
def heightForWidth(self, width):
height = self.doLayout(QRect(0, 0, width, 0), True)
return height
def setGeometry(self, rect):
super(FlowLayout, self).setGeometry(rect)
self.doLayout(rect, False)
def sizeHint(self):
return self.minimumSize()
def minimumSize(self):
size = QSize()
for item in self.itemList:
size = size.expandedTo(item.minimumSize())
size += QSize(2 * self.margin, 2 * self.margin)
return size
def doLayout(self, rect, testOnly):
x = rect.x()
y = rect.y()
lineHeight = 0
for item in self.itemList:
wid = item.widget()
nextX = x + item.sizeHint().width() + self.spaceX
if nextX - self.spaceX > rect.right() and lineHeight > 0:
x = rect.x()
y = y + lineHeight + self.spaceY
nextX = x + item.sizeHint().width() + self.spaceX
lineHeight = 0
if not testOnly:
item.setGeometry(QRect(QPoint(x, y), item.sizeHint()))
x = nextX
lineHeight = max(lineHeight, item.sizeHint().height())
return y + lineHeight - rect.y()
if __name__ == '__main__':
import sys
app = QApplication(sys.argv)
window = Window()
window.show()
sys.exit(app.exec_())
Where did the problem come from?
When left-clicking the mouse, when it was mousePressEvent, it worked normally, and when it was mouseReleaseEvent, there was a problem with widget selection.
I have solved it now
the code is
def closeRubberBand(self):
if self.rubberBand.isVisible():
self.rubberBand.hide()
selected = []
rect = self.rubberBand.geometry()
for child in self.findChildren(QToolButton):
if self.flow_layout_widget:
layout_x = self.flow_layout_widget.pos().toTuple()[0]
layout_y = self.flow_layout_widget.pos().toTuple()[1]
child_w = child.geometry().size().toTuple()[0]
child_h = child.geometry().size().toTuple()[1]
cx, cy, cw, ch = (child.geometry().x() + layout_x, child.geometry().y() + layout_y, child_w, child_h)
child_rect = QRect(cx, cy, cw, ch)
else:
child_rect = child.geometry()
if rect.intersects(child_rect):
selected.append(child)
if selected:
for i in selected:
if i.isChecked():
i.setChecked(False)
else:
i.setChecked(True)
Could there be an easier way?

How to have a scrollable context menu in PyQt

I would like to have a scrollable context menu so that I can place many Actions in it. I saw an answer from another post that setting menu.setStyleSheet('QMenu{menu-scrollable: 1;}') will enable scroll Bar but it doesn't seem to do the job.
Here is a demonstration of the context menu of the blender software.
How can this be achieved?
Dealing with menu customization (with any framework) is not an easy task, and from experience I can telly you that trying to use simple approaches such as toggling item visibility will most certainly result in unexpected behavior and issues in user experience.
Three aspects must be kept in mind:
what you see on your screen is never what the final user will see;
there's always a certain amount of scenarios that you didn't consider, mostly due to "lazy debugging" which leads you to always test just a handful of situations without being thorough enough;
menus have been around for decades, and users are very accustomed to them, they know very well how they work and behave (even if unconsciously), and unusual behavior or visual hint can easily bring confusion and get them annoyed;
From your given answer, I can see at least the following important issues:
there are serious problems in the way geometries and visibilities are dealt with, with the result that some items are visible even when they shouldn't;
menu items can (and should) be programmatically hidden, resulting in unexpected behavior (especially since you might restore the visibility on a previously hidden item);
items with very long text are not considered, and will be cropped;
keyboard navigation is not supported, so the user can navigate to an invisible item;
the arrows are misleading, since they overlap items and there's no hint about possible further scrolling (I know that this is also the way Qt normally behaves, but that's not the point);
no "hover" scrolling is implemented, so a partially hidden item will result in a "highlighted arrow", which will lead the user to think that clicking will result in scrolling;
The solution, "unfortunately", is to correctly implement everything is needed, starting from painting and, obviously, user interaction.
The following is an almost complete implementation of a scrollable menu; the scrolling can be enabled by setting a maximum height or a maxItemCount keyword argument that guesses the height based on a standard item; it is then activated by moving on the arrows (and/or clicking on them) as well as using keyboard arrows.
It's not perfect yet, there are probably some aspect I didn't consider (see the above "lazy debugging" note), but for what I can see it should work as expected.
And, yes, I know, it's really extended; but, as said, menus are not as simple as they look.
class ScrollableMenu(QtWidgets.QMenu):
deltaY = 0
dirty = True
ignoreAutoScroll = False
def __init__(self, *args, **kwargs):
maxItemCount = kwargs.pop('maxItemCount', 0)
super().__init__(*args, **kwargs)
self._maximumHeight = self.maximumHeight()
self._actionRects = []
self.scrollTimer = QtCore.QTimer(self, interval=50, singleShot=True, timeout=self.checkScroll)
self.scrollTimer.setProperty('defaultInterval', 50)
self.delayTimer = QtCore.QTimer(self, interval=100, singleShot=True)
self.setMaxItemCount(maxItemCount)
#property
def actionRects(self):
if self.dirty or not self._actionRects:
self._actionRects.clear()
offset = self.offset()
for action in self.actions():
geo = super().actionGeometry(action)
if offset:
geo.moveTop(geo.y() - offset)
self._actionRects.append(geo)
self.dirty = False
return self._actionRects
def iterActionRects(self):
for action, rect in zip(self.actions(), self.actionRects):
yield action, rect
def setMaxItemCount(self, count):
style = self.style()
opt = QtWidgets.QStyleOptionMenuItem()
opt.initFrom(self)
a = QtWidgets.QAction('fake action', self)
self.initStyleOption(opt, a)
size = QtCore.QSize()
fm = self.fontMetrics()
qfm = opt.fontMetrics
size.setWidth(fm.boundingRect(QtCore.QRect(), QtCore.Qt.TextSingleLine, a.text()).width())
size.setHeight(max(fm.height(), qfm.height()))
self.defaultItemHeight = style.sizeFromContents(style.CT_MenuItem, opt, size, self).height()
if not count:
self.setMaximumHeight(self._maximumHeight)
else:
fw = style.pixelMetric(style.PM_MenuPanelWidth, None, self)
vmargin = style.pixelMetric(style.PM_MenuHMargin, opt, self)
scrollHeight = self.scrollHeight(style)
self.setMaximumHeight(
self.defaultItemHeight * count + (fw + vmargin + scrollHeight) * 2)
self.dirty = True
def scrollHeight(self, style):
return style.pixelMetric(style.PM_MenuScrollerHeight, None, self) * 2
def isScrollable(self):
return self.height() < super().sizeHint().height()
def checkScroll(self):
pos = self.mapFromGlobal(QtGui.QCursor.pos())
delta = max(2, int(self.defaultItemHeight * .25))
if pos in self.scrollUpRect:
delta *= -1
elif pos not in self.scrollDownRect:
return
if self.scrollBy(delta):
self.scrollTimer.start(self.scrollTimer.property('defaultInterval'))
def offset(self):
if self.isScrollable():
return self.deltaY - self.scrollHeight(self.style())
return 0
def translatedActionGeometry(self, action):
return self.actionRects[self.actions().index(action)]
def ensureVisible(self, action):
style = self.style()
fw = style.pixelMetric(style.PM_MenuPanelWidth, None, self)
hmargin = style.pixelMetric(style.PM_MenuHMargin, None, self)
vmargin = style.pixelMetric(style.PM_MenuVMargin, None, self)
scrollHeight = self.scrollHeight(style)
extent = fw + hmargin + vmargin + scrollHeight
r = self.rect().adjusted(0, extent, 0, -extent)
geo = self.translatedActionGeometry(action)
if geo.top() < r.top():
self.scrollBy(-(r.top() - geo.top()))
elif geo.bottom() > r.bottom():
self.scrollBy(geo.bottom() - r.bottom())
def scrollBy(self, step):
if step < 0:
newDelta = max(0, self.deltaY + step)
if newDelta == self.deltaY:
return False
elif step > 0:
newDelta = self.deltaY + step
style = self.style()
scrollHeight = self.scrollHeight(style)
bottom = self.height() - scrollHeight
for lastAction in reversed(self.actions()):
if lastAction.isVisible():
break
lastBottom = self.actionGeometry(lastAction).bottom() - newDelta + scrollHeight
if lastBottom < bottom:
newDelta -= bottom - lastBottom
if newDelta == self.deltaY:
return False
self.deltaY = newDelta
self.dirty = True
self.update()
return True
def actionAt(self, pos):
for action, rect in self.iterActionRects():
if pos in rect:
return action
# class methods reimplementation
def sizeHint(self):
hint = super().sizeHint()
if hint.height() > self.maximumHeight():
hint.setHeight(self.maximumHeight())
return hint
def eventFilter(self, source, event):
if event.type() == event.Show:
if self.isScrollable() and self.deltaY:
action = source.menuAction()
self.ensureVisible(action)
rect = self.translatedActionGeometry(action)
delta = rect.topLeft() - self.actionGeometry(action).topLeft()
source.move(source.pos() + delta)
return False
return super().eventFilter(source, event)
def event(self, event):
if not self.isScrollable():
return super().event(event)
if event.type() == event.KeyPress and event.key() in (QtCore.Qt.Key_Up, QtCore.Qt.Key_Down):
res = super().event(event)
action = self.activeAction()
if action:
self.ensureVisible(action)
self.update()
return res
elif event.type() in (event.MouseButtonPress, event.MouseButtonDblClick):
pos = event.pos()
if pos in self.scrollUpRect or pos in self.scrollDownRect:
if event.button() == QtCore.Qt.LeftButton:
step = max(2, int(self.defaultItemHeight * .25))
if pos in self.scrollUpRect:
step *= -1
self.scrollBy(step)
self.scrollTimer.start(200)
self.ignoreAutoScroll = True
return True
elif event.type() == event.MouseButtonRelease:
pos = event.pos()
self.scrollTimer.stop()
if not (pos in self.scrollUpRect or pos in self.scrollDownRect):
action = self.actionAt(event.pos())
if action:
action.trigger()
self.close()
return True
return super().event(event)
def timerEvent(self, event):
if not self.isScrollable():
# ignore internal timer event for reopening popups
super().timerEvent(event)
def mouseMoveEvent(self, event):
if not self.isScrollable():
super().mouseMoveEvent(event)
return
pos = event.pos()
if pos.y() < self.scrollUpRect.bottom() or pos.y() > self.scrollDownRect.top():
if not self.ignoreAutoScroll and not self.scrollTimer.isActive():
self.scrollTimer.start(200)
return
self.ignoreAutoScroll = False
oldAction = self.activeAction()
if not pos in self.rect():
action = None
else:
y = event.y()
for action, rect in self.iterActionRects():
if rect.y() <= y <= rect.y() + rect.height():
break
else:
action = None
self.setActiveAction(action)
if action and not action.isSeparator():
def ensureVisible():
self.delayTimer.timeout.disconnect()
self.ensureVisible(action)
try:
self.delayTimer.disconnect()
except:
pass
self.delayTimer.timeout.connect(ensureVisible)
self.delayTimer.start(150)
elif oldAction and oldAction.menu() and oldAction.menu().isVisible():
def closeMenu():
self.delayTimer.timeout.disconnect()
oldAction.menu().hide()
self.delayTimer.timeout.connect(closeMenu)
self.delayTimer.start(50)
self.update()
def wheelEvent(self, event):
if not self.isScrollable():
return
self.delayTimer.stop()
if event.angleDelta().y() < 0:
self.scrollBy(self.defaultItemHeight)
else:
self.scrollBy(-self.defaultItemHeight)
def showEvent(self, event):
if self.isScrollable():
self.deltaY = 0
self.dirty = True
for action in self.actions():
if action.menu():
action.menu().installEventFilter(self)
self.ignoreAutoScroll = False
super().showEvent(event)
def hideEvent(self, event):
for action in self.actions():
if action.menu():
action.menu().removeEventFilter(self)
super().hideEvent(event)
def resizeEvent(self, event):
super().resizeEvent(event)
style = self.style()
l, t, r, b = self.getContentsMargins()
fw = style.pixelMetric(style.PM_MenuPanelWidth, None, self)
hmargin = style.pixelMetric(style.PM_MenuHMargin, None, self)
vmargin = style.pixelMetric(style.PM_MenuVMargin, None, self)
leftMargin = fw + hmargin + l
topMargin = fw + vmargin + t
bottomMargin = fw + vmargin + b
contentWidth = self.width() - (fw + hmargin) * 2 - l - r
scrollHeight = self.scrollHeight(style)
self.scrollUpRect = QtCore.QRect(leftMargin, topMargin, contentWidth, scrollHeight)
self.scrollDownRect = QtCore.QRect(leftMargin, self.height() - scrollHeight - bottomMargin,
contentWidth, scrollHeight)
def paintEvent(self, event):
if not self.isScrollable():
super().paintEvent(event)
return
style = self.style()
qp = QtGui.QPainter(self)
rect = self.rect()
emptyArea = QtGui.QRegion(rect)
menuOpt = QtWidgets.QStyleOptionMenuItem()
menuOpt.initFrom(self)
menuOpt.state = style.State_None
menuOpt.maxIconWidth = 0
menuOpt.tabWidth = 0
style.drawPrimitive(style.PE_PanelMenu, menuOpt, qp, self)
fw = style.pixelMetric(style.PM_MenuPanelWidth, None, self)
topEdge = self.scrollUpRect.bottom()
bottomEdge = self.scrollDownRect.top()
offset = self.offset()
qp.save()
qp.translate(0, -offset)
# offset translation is required in order to allow correct fade animations
for action, actionRect in self.iterActionRects():
actionRect = self.translatedActionGeometry(action)
if actionRect.bottom() < topEdge:
continue
if actionRect.top() > bottomEdge:
continue
visible = QtCore.QRect(actionRect)
if actionRect.bottom() > bottomEdge:
visible.setBottom(bottomEdge)
elif actionRect.top() < topEdge:
visible.setTop(topEdge)
visible = QtGui.QRegion(visible.translated(0, offset))
qp.setClipRegion(visible)
emptyArea -= visible.translated(0, -offset)
opt = QtWidgets.QStyleOptionMenuItem()
self.initStyleOption(opt, action)
opt.rect = actionRect.translated(0, offset)
style.drawControl(style.CE_MenuItem, opt, qp, self)
qp.restore()
cursor = self.mapFromGlobal(QtGui.QCursor.pos())
upData = (
False, self.deltaY > 0, self.scrollUpRect
)
downData = (
True, actionRect.bottom() - 2 > bottomEdge, self.scrollDownRect
)
for isDown, enabled, scrollRect in upData, downData:
qp.setClipRect(scrollRect)
scrollOpt = QtWidgets.QStyleOptionMenuItem()
scrollOpt.initFrom(self)
scrollOpt.state = style.State_None
scrollOpt.checkType = scrollOpt.NotCheckable
scrollOpt.maxIconWidth = scrollOpt.tabWidth = 0
scrollOpt.rect = scrollRect
scrollOpt.menuItemType = scrollOpt.Scroller
if enabled:
if cursor in scrollRect:
frame = QtWidgets.QStyleOptionMenuItem()
frame.initFrom(self)
frame.rect = scrollRect
frame.state |= style.State_Selected | style.State_Enabled
style.drawControl(style.CE_MenuItem, frame, qp, self)
scrollOpt.state |= style.State_Enabled
scrollOpt.palette.setCurrentColorGroup(QtGui.QPalette.Active)
else:
scrollOpt.palette.setCurrentColorGroup(QtGui.QPalette.Disabled)
if isDown:
scrollOpt.state |= style.State_DownArrow
style.drawControl(style.CE_MenuScroller, scrollOpt, qp, self)
if fw:
borderReg = QtGui.QRegion()
borderReg |= QtGui.QRegion(QtCore.QRect(0, 0, fw, self.height()))
borderReg |= QtGui.QRegion(QtCore.QRect(self.width() - fw, 0, fw, self.height()))
borderReg |= QtGui.QRegion(QtCore.QRect(0, 0, self.width(), fw))
borderReg |= QtGui.QRegion(QtCore.QRect(0, self.height() - fw, self.width(), fw))
qp.setClipRegion(borderReg)
emptyArea -= borderReg
frame = QtWidgets.QStyleOptionFrame()
frame.rect = rect
frame.palette = self.palette()
frame.state = QtWidgets.QStyle.State_None
frame.lineWidth = style.pixelMetric(style.PM_MenuPanelWidth)
frame.midLineWidth = 0
style.drawPrimitive(style.PE_FrameMenu, frame, qp, self)
qp.setClipRegion(emptyArea)
menuOpt.state = style.State_None
menuOpt.menuItemType = menuOpt.EmptyArea
menuOpt.checkType = menuOpt.NotCheckable
menuOpt.rect = menuOpt.menuRect = rect
style.drawControl(style.CE_MenuEmptyArea, menuOpt, qp, self)
class Test(QtWidgets.QWidget):
def __init__(self):
super().__init__()
self.menu = ScrollableMenu(maxItemCount=5)
self.menu.addAction('test action')
for i in range(10):
self.menu.addAction('Action {}'.format(i + 1))
if i & 1:
self.menu.addSeparator()
subMenu = self.menu.addMenu('very long sub menu')
subMenu.addAction('goodbye')
self.menu.triggered.connect(self.menuTriggered)
def menuTriggered(self, action):
print(action.text())
def contextMenuEvent(self, event):
self.menu.exec_(event.globalPos())
if __name__ == '__main__':
import sys
app = QtWidgets.QApplication(sys.argv)
test = Test()
test.show()
sys.exit(app.exec_())
For this, you will have to inherit the QMenu and override the wheelEvent.
Here is an example, which you may improve.
import sys
from PyQt5 import QtWidgets, QtCore, QtGui
class CustomMenu(QtWidgets.QMenu):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.setStyleSheet('QMenu{background: gray;} QMenu::item:selected {background: rgb(58, 145, 232);}')
self.setFixedHeight(100)
self.visible_lst = []
self.index = -1
self.visibleCount = None
self.maxHeightOfAction = 0
# -------- Remove Below code if you don't want the arrow ---------------#
self.topArrow = None
self.bottomArrow = None
self.painter = QtGui.QPainter()
# ---------------------------------------- #
def actionEvent(self, event):
if event.type() == QtCore.QEvent.ActionAdded:
if self.maxHeightOfAction < self.actionGeometry(self.actions()[-1]).height():
self.maxHeightOfAction = self.actionGeometry(self.actions()[-1]).height()
self.actions()[-1].setVisible(False)
self.updateActions()
if event.type() == QtCore.QEvent.ActionRemoved:
super(CustomMenu, self).actionEvent(event)
if self.index == len(self.actions()):
self.index -= 1
if event.action() in self.visible_lst:
self.visible_lst.remove(event.action())
self.removed()
super(CustomMenu, self).actionEvent(event)
def updateActions(self):
if self.actions():
if self.findVisibleCount() > len(self.actions()) and self.index == -1:
self.visible_lst = self.actions()
self.updateVisible()
elif self.findVisibleCount() < len(self.actions()) and self.index == -1:
self.index += 1
self.visible_lst = self.actions()[0: self.findVisibleCount()]
self.updateVisible()
self.setActiveAction(self.visible_lst[0])
def removed(self):
if len(self.actions()) > self.findVisibleCount():
if self.index < len(self.actions())-2:
index = self.findIndex(self.visible_lst, self.activeAction())
self.visible_lst.append(self.actions()[self.index + (index-self.findVisibleCount())-1])
elif self.index == len(self.actions())-1:
self.visible_lst.insert(0, self.actions()[-self.findVisibleCount()-1])
self.updateVisible()
def findVisibleCount(self): # finds how many QActions will be visible
visibleWidgets = 0
if self.actions():
try:
visibleWidgets = self.height()//self.maxHeightOfAction
except ZeroDivisionError:
pass
return visibleWidgets
def mousePressEvent(self, event) -> None:
if self.topArrow.containsPoint(event.pos(), QtCore.Qt.OddEvenFill) and self.index>0:
self.scrollUp()
elif self.bottomArrow.containsPoint(event.pos(), QtCore.Qt.OddEvenFill) and self.index < len(self.actions()) -1:
self.scrollDown()
else:
super(CustomMenu, self).mousePressEvent(event)
def keyPressEvent(self, event):
if self.actions():
if self.activeAction() is None:
self.setActiveAction(self.actions()[self.index])
if event.key() == QtCore.Qt.Key_Up:
self.scrollUp()
elif event.key() == QtCore.Qt.Key_Down:
self.scrollDown()
elif event.key() == QtCore.Qt.Key_Return:
super(CustomMenu, self).keyPressEvent(event)
def wheelEvent(self, event):
if self.actions():
if self.activeAction() is None:
self.setActiveAction(self.actions()[self.index])
delta = event.angleDelta().y()
if delta < 0: # scroll down
self.scrollDown()
elif delta > 0: # scroll up
self.scrollUp()
def scrollDown(self):
if self.index < len(self.actions())-1:
self.index = self.findIndex(self.actions(), self.activeAction()) + 1
try:
self.setActiveAction(self.actions()[self.index])
if self.activeAction() not in self.visible_lst and len(self.actions()) > self.findVisibleCount():
self.visible_lst[0].setVisible(False)
self.visible_lst.pop(0)
self.visible_lst.append(self.actions()[self.index])
self.visible_lst[-1].setVisible(True)
except IndexError:
pass
def scrollUp(self):
if self.findIndex(self.actions(), self.activeAction()) > 0:
self.index = self.findIndex(self.actions(), self.activeAction()) - 1
try:
self.setActiveAction(self.actions()[self.index])
if self.activeAction() not in self.visible_lst and len(self.actions()) > self.findVisibleCount():
self.visible_lst[-1].setVisible(False)
self.visible_lst.pop()
self.visible_lst.insert(0, self.actions()[self.index])
self.visible_lst[0].setVisible(True)
except IndexError:
pass
def updateVisible(self):
for item in self.visible_lst:
if not item.isVisible():
item.setVisible(True)
def findIndex(self, lst, element):
for index, item in enumerate(lst):
if item == element:
return index
return -1
def paintEvent(self, event): # remove this if you don't want the arrow
super(CustomMenu, self).paintEvent(event)
height = int(self.height())
width = self.width()//2
topPoints = [QtCore.QPoint(width-5, 7), QtCore.QPoint(width, 2), QtCore.QPoint(width+5, 7)]
bottomPoints = [QtCore.QPoint(width-5, height-7), QtCore.QPoint(width, height-2), QtCore.QPoint(width+5, height-7)]
self.topArrow = QtGui.QPolygon(topPoints)
self.bottomArrow = QtGui.QPolygon(bottomPoints)
self.painter.begin(self)
self.painter.setBrush(QtGui.QBrush(QtCore.Qt.white))
self.painter.setPen(QtCore.Qt.white)
if len(self.actions()) > self.findVisibleCount():
if self.index>0:
self.painter.drawPolygon(self.topArrow)
if self.index < len(self.actions()) -1:
self.painter.drawPolygon(self.bottomArrow)
self.painter.end()
class ExampleWindow(QtWidgets.QWidget):
def contextMenuEvent(self, event) -> None:
menu = CustomMenu()
menu.addAction('Hello0')
menu.addAction('Hello1')
menu.addAction('Hello2')
menu.addAction('Hello3')
menu.addAction('Hello4')
menu.addAction('Hello5')
menu.addAction('Hello6')
menu.addAction('Hello7')
menu.addAction('Hello8')
menu.exec_(QtGui.QCursor.pos())
def main():
app = QtWidgets.QApplication(sys.argv)
window = ExampleWindow()
window.setWindowTitle('PyQt5 App')
window.show()
app.exec_()
if __name__ == '__main__':
main()
Explanation of the above code:
Store all the visible actions in a list say visible_lst.
scrolling down:
increment the index when scrolling down
Use self.visible_lst[0].setVisible(False) which will make that action invisible and then pop the list from the front.
Append the next action to the visible_lst using self.actions()[self.index]
scrolling up:
decrement the index when scrolling up
use self.visible_lst[-1].setVisible(False) which will make the last item in the list hidden and pop the last element from the list
Insert the previous element to the 0th index of visible_lst and make it visible using self.actions()[index].setVisible(True)
Output of the scrolling context menu code:
Readers, if you have suggestions or queries leave a comment.

How to update a AnimationGroup without creating a new AnimationGroup every time?

I want to run a AnimationGroup with different changing animations.
But the problem is that the function clear()
self.group = QtCore.QSequentialAnimationGroup(self)
def anim(self):
if X == True:
self.group.addAnimation(self.animation_1)
self.group.addAnimation(self.animation_2)
elif X == False:
self.group.addAnimation(self.animation_3)
self.group.addAnimation(self.animation_4)
self.group.start()
self.group.clear()
displays an error
RuntimeError: wrapped C/C++ object of type QVariantAnimation has been deleted
I can’t constantly create a new group.
def anim(self):
self.group = QtCore.QSequentialAnimationGroup(self)
if X == True:
self.group.addAnimation(self.animation_1)
self.group.addAnimation(self.animation_2)
elif X == False:
self.group.addAnimation(self.animation_3)
self.group.addAnimation(self.animation_4)
self.group.start()
self.group.clear()
I tried to use removeAnimation
import sys
from PyQt5 import QtWidgets, QtGui, QtCore
class Rad(QtWidgets.QWidget):
def __init__(self, group, pos, parent=None):
super(Rad, self).__init__(parent)
self.resize(50, 50)
lay = QtWidgets.QVBoxLayout(self)
self.radio = QtWidgets.QRadioButton()
self.label = QtWidgets.QLabel()
self.label.setText('but-{}'.format(pos))
self.group = group
self.pos = pos
lay.addWidget(self.radio)
lay.addWidget(self.label)
self.radio.toggled.connect(self.fun)
self.animation = QtCore.QVariantAnimation()
self.animation.setDuration(1000)
self.animation.valueChanged.connect(self.value)
self.animation.setStartValue(100)
self.animation.setEndValue(0)
self.can = False
def value(self, val):
self.move(self.pos, val)
def fun(self):
self.animation.setStartValue(
100
if not self.can
else 0
)
self.animation.setEndValue(
0
if not self.can
else 100
)
if self.group.animationAt(1) == None :
print("Bad")
else :
print("Good")
self.group.removeAnimation(self.group.animationAt(0))
self.group.addAnimation(self.animation)
self.group.start()
self.can = not self.can
class Test(QtWidgets.QWidget):
def __init__(self):
super().__init__()
self.group = QtCore.QSequentialAnimationGroup(self)
self.buts = QtWidgets.QButtonGroup(self, exclusive=True)
wid_1 = Rad(self.group, 200, self)
wid_2 = Rad(self.group, 100, self)
wid_3 = Rad(self.group, 0, self)
self.buts.addButton(wid_1.radio, 0)
self.buts.addButton(wid_2.radio,1)
self.buts.addButton(wid_3.radio, 2)
wid_1.setStyleSheet('background:brown;')
wid_2.setStyleSheet('background:yellow;')
wid_3.setStyleSheet('background:green;')
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
w = Test()
w.resize(500, 500)
w.show()
sys.exit(app.exec_())
And now the animation has become sharp
And in the console output
QAnimationGroup::animationAt: index is out of bounds
From what I can deduce is that you want the pressed item to move down and if any item was in the low position then it must return to its position. If so then my answer should work.
You should not use the clear method since that removes and deletes the animation, instead use takeAnimation(0) until there are no animations, and just add the new animations, but that logic should not be inside "Rad" but in the Test class:
import sys
from PyQt5 import QtWidgets, QtGui, QtCore
class Rad(QtWidgets.QWidget):
def __init__(self, text, parent=None):
super(Rad, self).__init__(parent)
self.resize(50, 50)
self.radio = QtWidgets.QRadioButton()
self.label = QtWidgets.QLabel()
self.label.setText(text)
lay = QtWidgets.QVBoxLayout(self)
lay.addWidget(self.radio)
lay.addWidget(self.label)
self.animation = QtCore.QVariantAnimation()
self.animation.setDuration(1000)
self.animation.valueChanged.connect(self.on_value_changed)
self.animation.setStartValue(100)
self.animation.setEndValue(0)
#QtCore.pyqtSlot("QVariant")
def on_value_changed(self, val):
pos = self.pos()
pos.setY(val)
self.move(pos)
def swap_values(self):
self.animation.blockSignals(True)
start_value = self.animation.startValue()
end_value = self.animation.endValue()
self.animation.setStartValue(end_value)
self.animation.setEndValue(start_value)
self.animation.blockSignals(False)
class Test(QtWidgets.QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.buts = QtWidgets.QButtonGroup(self, exclusive=True)
wid_1 = Rad("but-200", self)
wid_1.setStyleSheet("background:brown;")
wid_1.move(200, 0)
wid_2 = Rad("but-100", self)
wid_2.setStyleSheet("background:yellow;")
wid_2.move(100, 0)
wid_3 = Rad("but-0", self)
wid_3.setStyleSheet("background:green;")
wid_3.move(0, 0)
self.buts.addButton(wid_1.radio, 0)
self.buts.addButton(wid_2.radio, 1)
self.buts.addButton(wid_3.radio, 2)
self.buts.buttonToggled.connect(self.on_button_toggled)
self.group = QtCore.QSequentialAnimationGroup(self)
self.last_widget = None
#QtCore.pyqtSlot(QtWidgets.QAbstractButton, bool)
def on_button_toggled(self, button, state):
if state:
wid = button.parent()
if self.group.animationCount() > 0:
self.group.takeAnimation(0)
if isinstance(self.last_widget, Rad):
self.last_widget.swap_values()
self.group.addAnimation(self.last_widget.animation)
wid.swap_values()
self.group.addAnimation(wid.animation)
self.group.start()
self.last_widget = wid
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
w = Test()
w.resize(500, 500)
w.show()
sys.exit(app.exec_())

QGraphicsItem position not updated (PyQt4)

I'm trying to guess how to update the position of the edge when the nodes are moved or why it's not been automatically updated. I have found that the position of the nodes is not updated if I remove the self.update() from the mouseReleaseEvent but I don't know which is the mecanism to get the same on the edge class. Can anybody help me with this?
EDIT: With "position" I mean the value got by getPos() or getScenePos() for the QEdgeGraphicItem. It's printed on the output for the nodes and the Edge.
#!/usr/bin/env python
import math
from PyQt4 import QtCore, QtGui
from PyQt4.QtGui import QGraphicsView, QGraphicsTextItem
class QEdgeGraphicItem(QtGui.QGraphicsItem):
Type = QtGui.QGraphicsItem.UserType + 2
def __init__(self, sourceNode, destNode, label=None):
super(QEdgeGraphicItem, self).__init__()
self.sourcePoint = QtCore.QPointF()
self.destPoint = QtCore.QPointF()
self.setAcceptedMouseButtons(QtCore.Qt.NoButton)
self.setFlag(QtGui.QGraphicsItem.ItemIsMovable)
self.setFlag(QtGui.QGraphicsItem.ItemSendsGeometryChanges)
# self.setCacheMode(QtGui.QGraphicsItem.DeviceCoordinateCache)
self.source = sourceNode
self.dest = destNode
self.label = QGraphicsTextItem("WHY IN THE HELL IS IT IN 0,0", self)
self.label.setParentItem(self)
self.label.setDefaultTextColor(QtCore.Qt.black)
self.source.addEdge(self)
self.dest.addEdge(self)
self.adjust()
def adjust(self):
if not self.source or not self.dest:
return
line = QtCore.QLineF(self.mapFromItem(self.source, 0, 0),
self.mapFromItem(self.dest, 0, 0))
self.prepareGeometryChange()
self.sourcePoint = line.p1()
self.destPoint = line.p2()
def boundingRect(self):
if not self.source or not self.dest:
return QtCore.QRectF()
extra = 2
return QtCore.QRectF(self.sourcePoint,
QtCore.QSizeF(self.destPoint.x() - self.sourcePoint.x(),
self.destPoint.y() - self.sourcePoint.y())).normalized().adjusted(-extra,
-extra,
extra,
extra)
def paint(self, painter, option, widget):
if not self.source or not self.dest:
return
# Draw the line itself.
line = QtCore.QLineF(self.sourcePoint, self.destPoint)
if line.length() == 0.0:
return
painter.setPen(QtGui.QPen(QtCore.Qt.black, 1, QtCore.Qt.SolidLine,
QtCore.Qt.RoundCap, QtCore.Qt.RoundJoin))
painter.drawLine(line)
painter.setBrush(QtCore.Qt.NoBrush)
painter.setPen(QtCore.Qt.red)
painter.drawRect(self.boundingRect())
print "Edge:"+str(self.scenePos().x()) + " " + str(self.scenePos().y())
class QNodeGraphicItem(QtGui.QGraphicsItem):
Type = QtGui.QGraphicsItem.UserType + 1
def __init__(self, label):
super(QNodeGraphicItem, self).__init__()
# self.graph = graphWidget
self.edgeList = []
self.newPos = QtCore.QPointF()
self.setFlag(QtGui.QGraphicsItem.ItemIsMovable)
self.setFlag(QtGui.QGraphicsItem.ItemSendsGeometryChanges)
# self.setCacheMode(QtGui.QGraphicsItem.DeviceCoordinateCache)
# self.setZValue(1)
self.size = 40
self.border_width = 4
def type(self):
return QNodeGraphicItem.Type
def addEdge(self, edge):
self.edgeList.append(edge)
edge.adjust()
def boundingRect(self):
x_coord = y_coord = (-1*(self.size/2)) - self.border_width
width = height = self.size+23+self.border_width
return QtCore.QRectF(x_coord, y_coord , width,
height)
def paint(self, painter, option, widget):
x_coord = y_coord = -(self.size / 2)
width = height = self.size
painter.save()
painter.setBrush(QtGui.QBrush(QtGui.QColor(100, 0, 200, 127)))
painter.setPen(QtCore.Qt.black)
painter.drawEllipse(x_coord, y_coord, width, height)
painter.restore()
print "Node: " + str(self.scenePos().x()) + " " + str(self.scenePos().y())
def itemChange(self, change, value):
if change == QtGui.QGraphicsItem.ItemPositionHasChanged:
for edge in self.edgeList:
edge.adjust()
return super(QNodeGraphicItem, self).itemChange(change, value)
def mousePressEvent(self, event):
self.update()
super(QNodeGraphicItem, self).mousePressEvent(event)
def mouseReleaseEvent(self, event):
self.update()
super(QNodeGraphicItem, self).mouseReleaseEvent(event)
if __name__ == '__main__':
import sys
app = QtGui.QApplication(sys.argv)
QtCore.qsrand(QtCore.QTime(0, 0, 0).secsTo(QtCore.QTime.currentTime()))
node1 = QNodeGraphicItem("Node1")
node2 = QNodeGraphicItem("Node2")
edge = QEdgeGraphicItem(node1,node2)
view = QGraphicsView()
view.setCacheMode(QtGui.QGraphicsView.CacheBackground)
view.setViewportUpdateMode(QtGui.QGraphicsView.BoundingRectViewportUpdate)
view.setRenderHint(QtGui.QPainter.Antialiasing)
view.setTransformationAnchor(QtGui.QGraphicsView.AnchorUnderMouse)
view.setResizeAnchor(QtGui.QGraphicsView.AnchorViewCenter)
view.scale(0.8, 0.8)
view.setMinimumSize(400, 400)
view.setWindowTitle("Example")
scene = QtGui.QGraphicsScene(view)
scene.setItemIndexMethod(QtGui.QGraphicsScene.NoIndex)
scene.setSceneRect(-400, -400, 800, 800)
view.setScene(scene)
scene.addItem(node1)
scene.addItem(node2)
scene.addItem(edge)
view.show()
sys.exit(app.exec_())
Thank you.

wxPython GetClientSize() returning (0, 0)

I am creating a small drawing application from a python book, "wxPython in Action", and it uses self.GetClientSize() to get the size of a window. For some reason this is return (0, 0) for me instead of the expected value (800, 600).
The program crashes when wx.EmptyBitmap is called with 0, 0 as its parameters. If I put
wx.EmptyBitmap(800, 600) the entire program runs fine, minus resizing.
Here is the relevant method
def InitBuffer(self):
size = self.GetClientSizeTuple()
print size
sys.exit(1)
self.buffer = wx.EmptyBitmap(size.width, size.height)
dc = wx.BufferedDC(None, self.buffer)
dc.SetBackground(wx.Brush(self.GetBackgroundColour()))
dc.Clear()
self.DrawLines(dc)
self.reInitBuffer = False
And this is the complete code
#!/usr/bin/arch -i386 /usr/bin/python2.6 -tt
import sys
import wx
class SketchWindow(wx.Window):
def __init__(self, parent, ID):
wx.Window.__init__(self, parent, ID)
self.SetBackgroundColour("White")
self.color = "Black"
self.thickness = 1
self.pen = wx.Pen(self.color, self.thickness, wx.SOLID)
self.lines = []
self.curLine = []
self.pos = (0, 0)
self.InitBuffer()
self.Bind(wx.EVT_LEFT_DOWN, self.OnLeftDown)
self.Bind(wx.EVT_LEFT_UP, self.OnLeftUp)
self.Bind(wx.EVT_SIZE, self.OnSize)
self.Bind(wx.EVT_MOTION, self.OnMotion)
self.Bind(wx.EVT_IDLE, self.OnIdle)
self.Bind(wx.EVT_PAINT, self.OnPaint)
def InitBuffer(self):
size = self.GetClientSizeTuple()
print size
sys.exit(1)
self.buffer = wx.EmptyBitmap(size.width, size.height)
dc = wx.BufferedDC(None, self.buffer)
dc.SetBackground(wx.Brush(self.GetBackgroundColour()))
dc.Clear()
self.DrawLines(dc)
self.reInitBuffer = False
def GetLinesData(self):
return self.lines[:]
def SetLinesData(self, lines):
self.lines = lines[:]
self.InitBuffer()
self.Refresh()
def OnLeftDown(self, event):
self.curLine = []
self.pos = event.GetPositionTuple()
self.CaptureMouse()
def OnLeftUp(self, event):
if self.HasCapture():
self.lines.append((self.color, self.thickness, self.curLine))
self.curLine = []
self.ReleaseMouse()
def OnMotion(self, event):
if event.Dragging() and event.LeftIsDown():
dc = wx.BufferedDC(wx.ClientDC(self), self.buffer)
self.drawMotion(dc, event)
event.Skip()
def drawMotion(self, dc, event):
dc.SetPen(self.pen)
newPos = event.GetPositionTuple()
coords = self.pos + newPos
self.curLine.append(coords)
dc.DrawLine(*coords)
self.pos = newPos
def OnSize(self, event):
self.reInitBuffer = True
def OnIdle(self, event):
if self.reInitBuffer:
self.InitBuffer()
self.Refresh(False)
def OnPaint(self, event):
dc = wx.BufferedPaintDC(self, self.buffer)
def DrawLines(self, dc):
for (colour, thickness, line) in self.lines:
pen = wx.Pen(colour, thickness, wx.SOLID)
dc.SetPen(pen)
for coord in line:
dc.DrawLine(*coord)
def SetColor(self, color):
self.color = color
self.pen = wx.Pen(self.color, self.thickness, wx.SOLID)
def GetColor(self):
return self.color
def SetThickness(self, thickness):
self.thickness = thickness
self.pen = wx.Pen(self.color, self.thickness, wx.SOLID)
class SketchFrame(wx.Frame):
def __init__(self, parent):
wx.Frame.__init__(self, parent, -1, 'Sketch Frame', size=(800, 600))
self.sketch = SketchWindow(self, -1)
def main():
app = wx.PySimpleApp()
frame = SketchFrame(None)
frame.Show(True)
app.MainLoop()
if __name__ == '__main__':
main()
It's because you're calling GetSize in the __init__() method - the window isn't fully created until this method has completed. Thus, it hasn't has its width and height set properly.
You could use wx.CallAfter/CallLater to postpone the calling of this function until window creation has fully completed.
I don't know if there is a better solution, but the problem is that when the object was being initialized it didn't have a parent yet, so it didn't know what size it should be. Thus it was width 0 and height 0. However, it needed to initialize the buffer. What I did to fix this was
if size == (0, 0):
size.width = 1
size.height = 1
Once it is added to the frame it gets a new size and the buffer is resized. So I guess that works!
I suppose another solution would be to pass a size parameter to the init method, but i'd prefer not to have to do that if it is not required.
Please post other solutions if you have them =)

Categories