Any PyQt circular progress bar? - python

Does anybody know how I can implement circular progress bar on PyQt?
Also, I found an existing code:
http://sourceforge.net/projects/qroundprogressbar/
But, how it is in C++. How to use it for PyQt?
UPDATE: Using implementation of QRoundProgressBar below, I created a complete demo application with start button to demonstrate the QRoundProgressBar.
Save the QRoundProgressBar in circularprogressbar.py and create a new file in the same directory for the code below. Hope it helps others.
from circularprogressbar import QRoundProgressBar
import sys
from PyQt4.QtGui import *
from PyQt4 import QtCore, QtGui, Qt
from time import sleep
class TstWidget(QtGui.QWidget):
def __init__(self):
super(type(self), self).__init__()
self.bar = QRoundProgressBar()
self.bar.setFixedSize(300, 300)
self.bar.setDataPenWidth(3)
self.bar.setOutlinePenWidth(3)
self.bar.setDecimals(1)
self.bar.setFormat('%v | %p %')
# self.bar.resetFormat()
self.bar.setNullPosition(90)
self.bar.setBarStyle(QRoundProgressBar.StyleDonut)
self.bar.setDataColors([(0., QtGui.QColor.fromRgb(255,0,0)), (0.5, QtGui.QColor.fromRgb(255,255,0)), (1., QtGui.QColor.fromRgb(0,255,0))])
self.bar.setMaximun(100)
self.bar.setMinimun(0)
self.bar.setRange(0, 100)
self.bar.setValue(0)
button = QtGui.QPushButton("Start", self)
button.clicked.connect(self.on_start)
lay = QtGui.QVBoxLayout()
lay.addWidget(button)
lay.addWidget(self.bar)
self.setLayout(lay)
self.myLongTask = TaskThread()
self.myLongTask.notifyProgress.connect(self.on_progress)
def on_start(self):
self.myLongTask.start()
def on_progress(self, i):
self.bar.setValue(i)
class TaskThread(QtCore.QThread):
notifyProgress = QtCore.pyqtSignal(int)
def run(self):
for i in range(101):
self.notifyProgress.emit(i)
sleep(0.1)
def main():
app = QtGui.QApplication(sys.argv)
ex = TstWidget()
ex.show()
sys.exit(app.exec_())
if __name__ == '__main__':
main()

I have ported QRoundProgressBar on PyQt (and fixed some minor bugs):
from PyQt4 import QtCore, QtGui, Qt
class QRoundProgressBar(QtGui.QWidget):
StyleDonut = 1
StylePie = 2
StyleLine = 3
PositionLeft = 180
PositionTop = 90
PositionRight = 0
PositionBottom = -90
UF_VALUE = 1
UF_PERCENT = 2
UF_MAX = 4
def __init__(self):
super().__init__()
self.min = 0
self.max = 100
self.value = 25
self.nullPosition = self.PositionTop
self.barStyle = self.StyleDonut
self.outlinePenWidth =1
self.dataPenWidth = 1
self.rebuildBrush = False
self.format = "%p%"
self.decimals = 1
self.updateFlags = self.UF_PERCENT
self.gradientData = []
self.donutThicknessRatio = 0.75
def setRange(self, min, max):
self.min = min
self.max = max
if self.max < self.min:
self.max, self.min = self.min, self.max
if self.value < self.min:
self.value = self.min
elif self.value > self.max:
self.value = self.max
if not self.gradientData:
self.rebuildBrush = True
self.update()
def setMinimun(self, min):
self.setRange(min, self.max)
def setMaximun(self, max):
self.setRange(self.min, max)
def setValue(self, val):
if self.value != val:
if val < self.min:
self.value = self.min
elif val > self.max:
self.value = self.max
else:
self.value = val
self.update()
def setNullPosition(self, position):
if position != self.nullPosition:
self.nullPosition = position
if not self.gradientData:
self.rebuildBrush = True
self.update()
def setBarStyle(self, style):
if style != self.barStyle:
self.barStyle = style
self.update()
def setOutlinePenWidth(self, penWidth):
if penWidth != self.outlinePenWidth:
self.outlinePenWidth = penWidth
self.update()
def setDataPenWidth(self, penWidth):
if penWidth != self.dataPenWidth:
self.dataPenWidth = penWidth
self.update()
def setDataColors(self, stopPoints):
if stopPoints != self.gradientData:
self.gradientData = stopPoints
self.rebuildBrush = True
self.update()
def setFormat(self, format):
if format != self.format:
self.format = format
self.valueFormatChanged()
def resetFormat(self):
self.format = ''
self.valueFormatChanged()
def setDecimals(self, count):
if count >= 0 and count != self.decimals:
self.decimals = count
self.valueFormatChanged()
def setDonutThicknessRatio(self, val):
self.donutThicknessRatio = max(0., min(val, 1.))
self.update()
def paintEvent(self, event):
outerRadius = min(self.width(), self.height())
baseRect = QtCore.QRectF(1, 1, outerRadius-2, outerRadius-2)
buffer = QtGui.QImage(outerRadius, outerRadius, QtGui.QImage.Format_ARGB32)
buffer.fill(0)
p = QtGui.QPainter(buffer)
p.setRenderHint(QtGui.QPainter.Antialiasing)
# data brush
self.rebuildDataBrushIfNeeded()
# background
self.drawBackground(p, buffer.rect())
# base circle
self.drawBase(p, baseRect)
# data circle
arcStep = 360.0 / (self.max - self.min) * self.value
self.drawValue(p, baseRect, self.value, arcStep)
# center circle
innerRect, innerRadius = self.calculateInnerRect(baseRect, outerRadius)
self.drawInnerBackground(p, innerRect)
# text
self.drawText(p, innerRect, innerRadius, self.value)
# finally draw the bar
p.end()
painter = QtGui.QPainter(self)
painter.drawImage(0, 0, buffer)
def drawBackground(self, p, baseRect):
p.fillRect(baseRect, self.palette().background())
def drawBase(self, p, baseRect):
bs = self.barStyle
if bs == self.StyleDonut:
p.setPen(QtGui.QPen(self.palette().shadow().color(), self.outlinePenWidth))
p.setBrush(self.palette().base())
p.drawEllipse(baseRect)
elif bs == self.StylePie:
p.setPen(QtGui.QPen(self.palette().base().color(), self.outlinePenWidth))
p.setBrush(self.palette().base())
p.drawEllipse(baseRect)
elif bs == self.StyleLine:
p.setPen(QtGui.QPen(self.palette().base().color(), self.outlinePenWidth))
p.setBrush(Qt.Qt.NoBrush)
p.drawEllipse(baseRect.adjusted(self.outlinePenWidth/2, self.outlinePenWidth/2, -self.outlinePenWidth/2, -self.outlinePenWidth/2))
def drawValue(self, p, baseRect, value, arcLength):
# nothing to draw
if value == self.min:
return
# for Line style
if self.barStyle == self.StyleLine:
p.setPen(QtGui.QPen(self.palette().highlight().color(), self.dataPenWidth))
p.setBrush(Qt.Qt.NoBrush)
p.drawArc(baseRect.adjusted(self.outlinePenWidth/2, self.outlinePenWidth/2, -self.outlinePenWidth/2, -self.outlinePenWidth/2),
self.nullPosition * 16,
-arcLength * 16)
return
# for Pie and Donut styles
dataPath = QtGui.QPainterPath()
dataPath.setFillRule(Qt.Qt.WindingFill)
# pie segment outer
dataPath.moveTo(baseRect.center())
dataPath.arcTo(baseRect, self.nullPosition, -arcLength)
dataPath.lineTo(baseRect.center())
p.setBrush(self.palette().highlight())
p.setPen(QtGui.QPen(self.palette().shadow().color(), self.dataPenWidth))
p.drawPath(dataPath)
def calculateInnerRect(self, baseRect, outerRadius):
# for Line style
if self.barStyle == self.StyleLine:
innerRadius = outerRadius - self.outlinePenWidth
else: # for Pie and Donut styles
innerRadius = outerRadius * self.donutThicknessRatio
delta = (outerRadius - innerRadius) / 2.
innerRect = QtCore.QRectF(delta, delta, innerRadius, innerRadius)
return innerRect, innerRadius
def drawInnerBackground(self, p, innerRect):
if self.barStyle == self.StyleDonut:
p.setBrush(self.palette().alternateBase())
cmod = p.compositionMode()
p.setCompositionMode(QtGui.QPainter.CompositionMode_Source)
p.drawEllipse(innerRect)
p.setCompositionMode(cmod)
def drawText(self, p, innerRect, innerRadius, value):
if not self.format:
return
text = self.valueToText(value)
# !!! to revise
f = self.font()
# f.setPixelSize(innerRadius * max(0.05, (0.35 - self.decimals * 0.08)))
f.setPixelSize(innerRadius * 1.8 / len(text))
p.setFont(f)
textRect = innerRect
p.setPen(self.palette().text().color())
p.drawText(textRect, Qt.Qt.AlignCenter, text)
def valueToText(self, value):
textToDraw = self.format
format_string = '{' + ':.{}f'.format(self.decimals) + '}'
if self.updateFlags & self.UF_VALUE:
textToDraw = textToDraw.replace("%v", format_string.format(value))
if self.updateFlags & self.UF_PERCENT:
percent = (value - self.min) / (self.max - self.min) * 100.0
textToDraw = textToDraw.replace("%p", format_string.format(percent))
if self.updateFlags & self.UF_MAX:
m = self.max - self.min + 1
textToDraw = textToDraw.replace("%m", format_string.format(m))
return textToDraw
def valueFormatChanged(self):
self.updateFlags = 0;
if "%v" in self.format:
self.updateFlags |= self.UF_VALUE
if "%p" in self.format:
self.updateFlags |= self.UF_PERCENT
if "%m" in self.format:
self.updateFlags |= self.UF_MAX
self.update()
def rebuildDataBrushIfNeeded(self):
if self.rebuildBrush:
self.rebuildBrush = False
dataBrush = QtGui.QConicalGradient()
dataBrush.setCenter(0.5,0.5)
dataBrush.setCoordinateMode(QtGui.QGradient.StretchToDeviceMode)
for pos, color in self.gradientData:
dataBrush.setColorAt(1.0 - pos, color)
# angle
dataBrush.setAngle(self.nullPosition)
p = self.palette()
p.setBrush(QtGui.QPalette.Highlight, dataBrush)
self.setPalette(p)
Usage example:
class TstWidget(QtGui.QWidget):
def __init__(self):
super(type(self), self).__init__()
self.bar = QRoundProgressBar()
self.bar.setFixedSize(300, 300)
self.bar.setDataPenWidth(3)
self.bar.setOutlinePenWidth(3)
self.bar.setDonutThicknessRatio(0.85)
self.bar.setDecimals(1)
self.bar.setFormat('%v | %p %')
# self.bar.resetFormat()
self.bar.setNullPosition(90)
self.bar.setBarStyle(QRoundProgressBar.StyleDonut)
self.bar.setDataColors([(0., QtGui.QColor.fromRgb(255,0,0)), (0.5, QtGui.QColor.fromRgb(255,255,0)), (1., QtGui.QColor.fromRgb(0,255,0))])
self.bar.setRange(0, 300)
self.bar.setValue(260)
lay = QtGui.QVBoxLayout()
lay.addWidget(self.bar)
self.setLayout(lay)

#Alexandro, thank you for the code. It works nicely.
I found that it's best to set the min and max values as floats instead of int
self.min = 0.
self.max = 100.
self.value = 25.
Otherwise, the values won't update properly in python3 if the input value to valueToText() is also an integer, due to integer division

I have write this
class RoundProgress(QProgressBar):
def __init__(self,parent):
QProgressBar.__init__(self)
self.values = self.value()
self.values = (self.values*360)/100
self.parent = parent
self.setParent(parent)
self.n = self.value()
self.label = QLabel("<center>100%<center>")
self.label.setStyleSheet("color:red;")
self.label.setFont(QFont("courrier",math.sqrt(self.width())))
self.v = QVBoxLayout(self)
self.setLayout(self.v)
self.v.addWidget(self.label)
def setValue(self,n):
self.n = n
self.values = ((n*5650)/100)*(-1)
self.label.setText("<center>"+str(self.n)+"</center>")
def setNvalue(self,n):
self.n = n
self.values = ((n*5650)/100)*(-1)
self.label.setText("<center>"+str(self.n)+"</center>")
def paintEvent(self,event):
painter = QPainter(self)
painter.setRenderHint(QPainter.Antialiasing)
pen = QPen()
pen.setWidth(2)
pen.setColor(QColor("darkblue"))
painter.setPen(pen)
pen = QPen()
pen.setWidth(9)
pen.setColor(QColor("lightgrey"))
painter.setPen(pen)
painter.drawArc(5.1,5.1,self.width()-10,self.height()-10,1450,-5650)
#painter.drawEllipse(0,0,100,100)
painter.setBrush(QColor("lightblue"))
pen = QPen()
pen.setWidth(10)
pen.setColor(QColor("red"))
painter.setPen(pen)
painter.drawArc(5.1,5.1,self.width()-10,self.height()-10,1450,self.values)
self.update()

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.

Get Clicked Point Along QPainterPath

How can I get the percentage representing the point clicked along a QPainterPath. For example say I have a line, like the image below, and a user clicks on the QPainterPath, represented by the red dot. I would like to log what percentage the point falls along the path. In this case it would print 0.75 since the point is located around 75%.
These are the known variables:
# QPainterPath
path = QPainterPath()
path.moveTo( QPointF(10.00, -10.00) )
path.cubicTo(
QPointF(114.19, -10.00),
QPointF(145.80, -150.00),
QPointF(250.00, -150.00)
)
# User Clicked Point
QPointF(187.00, -130.00)
Updated!
My goal is to give the user the ability to click on the path and insert a point. Below is the code i have so far. You'll see in the video that it appears to fail when adding points between points. simply click on the path to insert a point.
Video Link to Watch bug:
https://youtu.be/nlWyZUIa7II
import sys
from PySide.QtGui import *
from PySide.QtCore import *
import random, math
class MyGraphicsView(QGraphicsView):
def __init__(self):
super(MyGraphicsView, self).__init__()
self.setDragMode(QGraphicsView.RubberBandDrag)
self.setCacheMode(QGraphicsView.CacheBackground)
self.setHorizontalScrollBarPolicy( Qt.ScrollBarAlwaysOff )
self.setVerticalScrollBarPolicy( Qt.ScrollBarAlwaysOff )
def mousePressEvent(self, event):
item = self.itemAt(event.pos())
if event.button() == Qt.LeftButton and isinstance(item, ConnectionItem):
percentage = self.percentageByPoint(item.shape(), self.mapToScene(event.pos()))
item.addKnotByPercent(percentage)
event.accept()
elif event.button() == Qt.MiddleButton:
super(MyGraphicsView, self).mousePressEvent(event)
# connection methods
def percentageByPoint(self, path, point, precision=0.5, width=3.0):
percentage = -1.0
if path.contains(point):
t = 0.0
d = []
while t <=100.0:
d.append(QVector2D(point - path.pointAtPercent(t/100.0)).length())
t += precision
percentage = d.index(min(d))*precision
return percentage
class MyGraphicsScene(QGraphicsScene):
def __init__(self, parent):
super(MyGraphicsScene, self).__init__()
self.setBackgroundBrush(QBrush(QColor(50,50,50)))
class KnotItem(QGraphicsEllipseItem):
def __init__(self, parent=None,):
super(self.__class__, self).__init__(parent)
self.setAcceptHoverEvents(True)
self.setFlag(self.ItemSendsScenePositionChanges, True)
self.setFlag(self.ItemIsSelectable, True) # false
self.setFlag(self.ItemIsMovable, True) # false
self.setRect(-6, -6, 12, 12)
# Overrides
def paint(self, painter, option, widget=None):
painter.save()
painter.setRenderHint(QPainter.Antialiasing)
painter.setPen(QPen(QColor(30,30,30), 2, Qt.SolidLine))
painter.setBrush(QBrush(QColor(255,30,30)))
painter.drawEllipse(self.rect())
painter.restore()
def itemChange(self, change, value):
if change == self.ItemScenePositionHasChanged:
if self.parentItem():
self.parentItem().update()
return super(self.__class__, self).itemChange(change, value)
def boundingRect(self):
rect = self.rect()
rect.adjust(-1,-1,1,1)
return rect
class ConnectionItem(QGraphicsPathItem):
def __init__(self, startPoint, endPoint, parent=None):
super(ConnectionItem, self).__init__()
self._hover = False
self.setAcceptHoverEvents(True)
self.setFlag( QGraphicsItem.ItemIsSelectable )
self.setFlag(QGraphicsItem.ItemSendsScenePositionChanges, True)
self.setZValue(-100)
self.startPoint = startPoint
self.endPoint = endPoint
self.knots = []
self.update()
def getBezierPath(self, points=[], curving=1.0):
# Calculate Bezier Line
path = QPainterPath()
curving = 1.0 # range 0-1
if len(points) < 2:
return path
path.moveTo(points[0])
for i in range(len(points)-1):
startPoint = points[i]
endPoint = points[i+1]
# use distance as mult, closer the nodes less the bezier
dist = math.hypot(endPoint.x() - startPoint.x(), endPoint.y() - startPoint.y())
# multiply distance by 0.375
offset = dist * 0.375 * curving
ctrlPt1 = startPoint + QPointF(offset,0);
ctrlPt2 = endPoint + QPointF(-offset,0);
# print startPoint, ctrlPt1, ctrlPt2, endPoint
path.cubicTo(ctrlPt1, ctrlPt2, endPoint)
return path
def drawPath(self, pos=None):
# Calculate Bezier Line
points = [self.startPoint]
for k in self.knots:
points.append(k.scenePos())
points.append(self.endPoint)
path = self.getBezierPath(points)
self.setPath(path)
def update(self):
super(self.__class__, self).update()
self.drawPath()
def paint(self, painter, option, widget):
painter.setRenderHints( QPainter.Antialiasing | QPainter.SmoothPixmapTransform | QPainter.HighQualityAntialiasing, True )
pen = QPen(QColor(170,170,170), 2, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin)
if self.isSelected():
pen.setColor(QColor(255, 255, 255))
elif self.hover:
pen.setColor(QColor(255, 30, 30))
painter.setPen(pen)
painter.drawPath(self.path())
def shape(self):
'''
Description:
This is super important for creating a more accurate path used for
collision detection by cursor.
'''
qp = QPainterPathStroker()
qp.setWidth(15)
qp.setCapStyle(Qt.SquareCap)
return qp.createStroke(self.path())
def hoverEnterEvent(self, event):
self.hover = True
self.update()
super(self.__class__, self).hoverEnterEvent(event)
def hoverLeaveEvent(self, event):
self.hover = False
self.update()
super(self.__class__, self).hoverEnterEvent(event)
def addKnot(self, pos=QPointF(0,0)):
'''
Description:
Add not based on current location of cursor or inbetween points on path.
'''
knotItem = KnotItem(parent=self)
knotItem.setPos(pos)
self.knots.append(knotItem)
self.update()
def addKnotByPercent(self, percentage=0.0):
'''
Description:
The percentage value should be between 0.0 and 100.0. This value
determines the location of the point and it's index in the knots list.
'''
if percentage < 0.0 or percentage > 100.0:
return
# add item
pos = self.shape().pointAtPercent(percentage*.01)
knotItem = KnotItem(parent=self)
knotItem.setPos(pos)
index = int(len(self.knots) * (percentage*.01))
print len(self.knots), (percentage), index
self.knots.insert(index, knotItem)
self.update()
# properties
#property
def hover(self):
return self._hover
#hover.setter
def hover(self, value=False):
self._hover = value
self.update()
class MyMainWindow(QMainWindow):
def __init__(self):
super(MyMainWindow, self).__init__()
self.setWindowTitle("Test")
self.resize(800,600)
self.gv = MyGraphicsView()
self.gv.setScene(MyGraphicsScene(self))
self.btnReset = QPushButton('Reset')
lay_main = QVBoxLayout()
lay_main.addWidget(self.btnReset)
lay_main.addWidget(self.gv)
widget_main = QWidget()
widget_main.setLayout(lay_main)
self.setCentralWidget(widget_main)
self.populate()
# connect
self.btnReset.clicked.connect(self.populate)
def populate(self):
scene = self.gv.scene()
for x in scene.items():
scene.removeItem(x)
del x
con = ConnectionItem(QPointF(-150,150), QPointF(250,-150))
scene.addItem(con)
def main():
app = QApplication(sys.argv)
ex = MyMainWindow()
ex.show()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
A possible solution is to use pointAtPercent () that returns a given point a percentage and calculate the distance to the point and find the minimum index and multiply it by the step. But for this the search must be refined because the previous algorithm works for any point even if it is outside the path. The idea in this case is to use a QPainterPath with a certain area using QPainterPathStroker and verify if the point belongs, and if not the value is outside the QPainterPath.
C++
#include <QtGui>
static qreal percentageByPoint(const QPainterPath & path, const QPointF & p, qreal precision=0.5, qreal width=3.0){
qreal percentage = -1;
QPainterPathStroker stroker;
stroker.setWidth(width);
QPainterPath strokepath = stroker.createStroke(path);
if(strokepath.contains(p)){
std::vector<qreal> d;
qreal t=0.0;
while(t<=100.0){
d.push_back(QVector2D(p - path.pointAtPercent(t/100)).length());
t+= precision;
}
std::vector<qreal>::iterator result = std::min_element(d.begin(), d.end());
int j= std::distance(d.begin(), result);
percentage = j*precision;
}
return percentage;
}
int main(int argc, char *argv[])
{
Q_UNUSED(argc)
Q_UNUSED(argv)
QPainterPath path;
path.moveTo( QPointF(10.00, -10.00) );
path.cubicTo(
QPointF(114.19, -10.00),
QPointF(145.80, -150.00),
QPointF(250.00, -150.00)
);
// User Clicked Point
QPointF p(187.00, -130.00);
qreal percentage = percentageByPoint(path, p);
qDebug() << percentage;
return 0;
}
python:
def percentageByPoint(path, point, precision=0.5, width=3.0):
percentage = -1.0
stroker = QtGui.QPainterPathStroker()
stroker.setWidth(width)
strokepath = stroker.createStroke(path)
if strokepath.contains(point):
t = 0.0
d = []
while t <=100.0:
d.append(QtGui.QVector2D(point - path.pointAtPercent(t/100)).length())
t += precision
percentage = d.index(min(d))*precision
return percentage
if __name__ == '__main__':
path = QtGui.QPainterPath()
path.moveTo(QtCore.QPointF(10.00, -10.00) )
path.cubicTo(
QtCore.QPointF(114.19, -10.00),
QtCore.QPointF(145.80, -150.00),
QtCore.QPointF(250.00, -150.00)
)
point = QtCore.QPointF(187.00, -130.00)
percentage = percentageByPoint(path, point)
print(percentage)
Output:
76.5
Instead of implementing the logic in QGraphicsView, you must do it in the item, and then when you update the path, the points must be ordered with respect to the percentage.
import math
from PySide import QtCore, QtGui
from functools import partial
class MyGraphicsView(QtGui.QGraphicsView):
def __init__(self):
super(MyGraphicsView, self).__init__()
self.setDragMode(QtGui.QGraphicsView.RubberBandDrag)
self.setCacheMode(QtGui.QGraphicsView.CacheBackground)
self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
scene = QtGui.QGraphicsScene(self)
scene.setBackgroundBrush(QtGui.QBrush(QtGui.QColor(50,50,50)))
self.setScene(scene)
class KnotItem(QtGui.QGraphicsEllipseItem):
def __init__(self, parent=None,):
super(self.__class__, self).__init__(parent)
self.setAcceptHoverEvents(True)
self.setFlag(self.ItemSendsScenePositionChanges, True)
self.setFlag(self.ItemIsSelectable, True)
self.setFlag(self.ItemIsMovable, True)
self.setRect(-6, -6, 12, 12)
self.setPen(QtGui.QPen(QtGui.QColor(30,30,30), 2, QtCore.Qt.SolidLine))
self.setBrush(QtGui.QBrush(QtGui.QColor(255,30,30)))
def itemChange(self, change, value):
if change == self.ItemScenePositionHasChanged:
if isinstance(self.parentItem(), ConnectionItem):
self.parentItem().updatePath()
# QtCore.QTimer.singleShot(60, partial(self.parentItem().setSelected,False))
return super(self.__class__, self).itemChange(change, value)
class ConnectionItem(QtGui.QGraphicsPathItem):
def __init__(self, startPoint, endPoint, parent=None):
super(ConnectionItem, self).__init__(parent)
self._start_point = startPoint
self._end_point = endPoint
self._hover = False
self.setAcceptHoverEvents(True)
self.setFlag(QtGui.QGraphicsItem.ItemIsSelectable )
self.setFlag(QtGui.QGraphicsItem.ItemSendsScenePositionChanges)
self.setZValue(-100)
self.updatePath()
def updatePath(self):
p = [self._start_point]
for children in self.childItems():
if isinstance(children, KnotItem):
p.append(children.pos())
p.append(self._end_point)
v = sorted(p, key=partial(ConnectionItem.percentageByPoint, self.path()))
self.setPath(ConnectionItem.getBezierPath(v))
def paint(self, painter, option, widget):
painter.setRenderHints(QtGui.QPainter.Antialiasing | QtGui.QPainter.SmoothPixmapTransform | QtGui.QPainter.HighQualityAntialiasing, True )
pen = QtGui.QPen(QtGui.QColor(170,170,170), 2, QtCore.Qt.SolidLine, QtCore.Qt.RoundCap, QtCore.Qt.RoundJoin)
if self.isSelected():
pen.setColor(QtGui.QColor(255, 255, 255))
elif self._hover:
pen.setColor(QtGui.QColor(255, 30, 30))
painter.setPen(pen)
painter.drawPath(self.path())
def mousePressEvent(self, event):
if event.button() == QtCore.Qt.LeftButton:
item = KnotItem(parent=self)
item.setPos(event.pos())
def hoverEnterEvent(self, event):
self._hover = True
self.update()
super(self.__class__, self).hoverEnterEvent(event)
def hoverLeaveEvent(self, event):
self._hover = False
self.update()
super(self.__class__, self).hoverEnterEvent(event)
def shape(self):
qp = QtGui.QPainterPathStroker()
qp.setWidth(15)
qp.setCapStyle(QtCore.Qt.SquareCap)
return qp.createStroke(self.path())
#staticmethod
def getBezierPath(points=[], curving=1.0):
# Calculate Bezier Line
path = QtGui.QPainterPath()
curving = 1.0 # range 0-1
if len(points) < 2:
return path
path.moveTo(points[0])
for i in range(len(points)-1):
startPoint = points[i]
endPoint = points[i+1]
# use distance as mult, closer the nodes less the bezier
dist = math.hypot(endPoint.x() - startPoint.x(), endPoint.y() - startPoint.y())
# multiply distance by 0.375
offset = dist * 0.375 * curving
ctrlPt1 = startPoint + QtCore.QPointF(offset,0);
ctrlPt2 = endPoint + QtCore.QPointF(-offset,0);
# print startPoint, ctrlPt1, ctrlPt2, endPoint
path.cubicTo(ctrlPt1, ctrlPt2, endPoint)
return path
#staticmethod
def percentageByPoint(path, point, precision=0.5):
t = 0.0
d = []
while t <=100.0:
d.append(QtGui.QVector2D(point - path.pointAtPercent(t/100.0)).length())
t += precision
percentage = d.index(min(d))*precision
return percentage
class MyMainWindow(QtGui.QMainWindow):
def __init__(self):
super(MyMainWindow, self).__init__()
central_widget = QtGui.QWidget()
self.setCentralWidget(central_widget)
button = QtGui.QPushButton("Reset")
self._view = MyGraphicsView()
button.clicked.connect(self.reset)
lay = QtGui.QVBoxLayout(central_widget)
lay.addWidget(button)
lay.addWidget(self._view)
self.resize(640, 480)
self.reset()
#QtCore.Slot()
def reset(self):
self._view.scene().clear()
it = ConnectionItem(QtCore.QPointF(-150,150), QtCore.QPointF(250,-150))
self._view.scene().addItem(it)
def main():
import sys
app =QtGui.QApplication(sys.argv)
ex = MyMainWindow()
ex.show()
sys.exit(app.exec_())
main()

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.

Pyqt Embedding Graphics object in Widget

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?

Categories