I am currently making some experiments applying the following RangeSlider (done by another user) in a bigger program. Right now, is used to choose a time interval. I wanted to adapt the code, in order to see a vertical line in the point I am interested to. Here there are some examples I did with an image editor.
What I have:
What I would like:
In my current code, when I add a line by using QLine and QPainter.drawLine, I get the line, but it does not behave in the way that I want. For example, when I move the green range, the line also moves along with it (it should be static). It also create problems with the user-interaction, because if, for example, the line is just above the grey part of the range, then I am not able to select it with the mouse pointer (it´s like if the line overlays, and disable the interaction of the range)
After doing some attempts, I am not able to create something like that above the range.
This is my code (I adapted to not show any lines on it).
import os
import sys
from PyQt4 import QtCore
from PyQt4 import QtGui
from PyQt4 import uic
from PyQt4.QtGui import QGridLayout, QSplitter, QGroupBox, QApplication, HBoxLayout, QWidget, QPainter, QColor, QFont
try:
_fromUtf8 = QtCore.QString.fromUtf8
except AttributeError:
_fromUtf8 = lambda s: s
__all__ = ['QRangeSlider']
DEFAULT_CSS = """
QRangeSlider * {
border: 0px;
padding: 0px;
}
QRangeSlider #Head {
background: #a7adba;
}
QRangeSlider #Span {
background: #343d46;
}
QRangeSlider #Span:active {
background: #343d46;
}
QRangeSlider #Tail {
background: #a7adba;
}
QRangeSlider > QSplitter::handle {
background: #4f5b66;
}
QRangeSlider > QSplitter::handle:vertical {
height: 4px;
}
QRangeSlider > QSplitter::handle:pressed {
background: #ca5;
}
"""
def scale(val, src, dst):
"""
Scale the given value from the scale of src to the scale of dst.
"""
return int(((val - src[0]) / float(src[1]-src[0])) * (dst[1]-dst[0]) + dst[0])
class Ui_Form(object):
"""default range slider form"""
def setupUi(self, Form):
Form.setObjectName(_fromUtf8("QRangeSlider"))
Form.resize(300, 30)
Form.setStyleSheet(_fromUtf8(DEFAULT_CSS))
self.gridLayout = QGridLayout(Form)
self.gridLayout.setContentsMargins(0, 0, 0, 0)
self.gridLayout.setSpacing(0)
self.gridLayout.setObjectName(_fromUtf8("gridLayout"))
self._splitter = QSplitter(Form)
self._splitter.setMinimumSize(QtCore.QSize(0, 0))
self._splitter.setMaximumSize(QtCore.QSize(16777215, 16777215))
self._splitter.setOrientation(QtCore.Qt.Horizontal)
self._splitter.setObjectName(_fromUtf8("splitter"))
self._head = QGroupBox(self._splitter)
self._head.setTitle(_fromUtf8(""))
self._head.setObjectName(_fromUtf8("Head"))
self._handle = QGroupBox(self._splitter)
self._handle.setTitle(_fromUtf8(""))
self._handle.setObjectName(_fromUtf8("Span"))
self._tail = QGroupBox(self._splitter)
self._tail.setTitle(_fromUtf8(""))
self._tail.setObjectName(_fromUtf8("Tail"))
self.gridLayout.addWidget(self._splitter, 0, 0, 1, 1)
QtCore.QMetaObject.connectSlotsByName(Form)
class Element(QGroupBox):
def __init__(self, parent, main):
super(Element, self).__init__(parent)
self.main = main
def setStyleSheet(self, style):
"""redirect style to parent groupbox"""
self.parent().setStyleSheet(style)
def textColor(self):
"""text paint color"""
return getattr(self, '__textColor', QColor(125, 125, 125))
def setTextColor(self, color):
"""set the text paint color"""
if type(color) == tuple and len(color) == 3:
color = QColor(color[0], color[1], color[2])
elif type(color) == int:
color = QColor(color, color, color)
setattr(self, '__textColor', color)
def paintEvent(self, event):
"""overrides paint event to handle text"""
qp = QPainter()
qp.begin(self)
if self.main.drawValues():
self.drawText(event, qp)
qp.end()
class Head(Element):
"""area before the handle"""
def __init__(self, parent, main):
super(Head, self).__init__(parent, main)
def drawText(self, event, qp):
# Adding a line
qp.setPen(QColor(255, 0, 0))
qp.drawLine(25, 0, 25, 50)
qp.setPen(self.textColor())
qp.setFont(QFont('Arial', 10))
qp.drawText(event.rect(), QtCore.Qt.AlignLeft, str(self.main.min()))
class Tail(Element):
"""area after the handle"""
def __init__(self, parent, main):
super(Tail, self).__init__(parent, main)
def drawText(self, event, qp):
# Adding a line
qp.setPen(QColor(255, 0, 0))
qp.drawLine(50, 0, 50, 50)
qp.setPen(self.textColor())
qp.setFont(QFont('Arial', 10))
qp.drawText(event.rect(), QtCore.Qt.AlignRight, str(self.main.max()))
class Handle(Element):
"""handle area"""
def __init__(self, parent, main):
super(Handle, self).__init__(parent, main)
def drawText(self, event, qp):
# Adding a line
qp.setPen(QColor(255, 0, 0))
qp.drawLine(75, 0, 75, 50)
qp.setPen(self.textColor())
qp.setFont(QFont('Arial', 10))
qp.drawText(event.rect(), QtCore.Qt.AlignLeft, str(self.main.start()))
qp.drawText(event.rect(), QtCore.Qt.AlignRight, str(self.main.end()))
def mouseMoveEvent(self, event):
event.accept()
mx = event.globalX()
_mx = getattr(self, '__mx', None)
vrange = self.main.max() - self.main.min()
size = self.main.width()
step = vrange/size
if not _mx:
setattr(self, '__mx', mx)
dx = 0
else:
dx = mx - _mx
dx *= step
if -1 < dx < 1:
event.ignore()
return
dx = round(dx)
setattr(self, '__mx', mx)
s = self.main.start() + dx
e = self.main.end() + dx
if s >= self.main.min() and e <= self.main.max():
self.main.setRange(s, e)
def mousePressEvent(self, event):
setattr(self, '__mx', event.globalX())
class QRangeSlider(QWidget, Ui_Form):
"""
The QRangeSlider class implements a horizontal range slider widget.
Inherits QWidget.
Methods
* __init__ (self, QWidget parent = None)
* bool drawValues (self)
* int end (self)
* (int, int) getRange (self)
* int max (self)
* int min (self)
* int start (self)
* setBackgroundStyle (self, QString styleSheet)
* setDrawValues (self, bool draw)
* setEnd (self, int end)
* setStart (self, int start)
* setRange (self, int start, int end)
* setSpanStyle (self, QString styleSheet)
Signals
* endValueChanged (int)
* maxValueChanged (int)
* minValueChanged (int)
* startValueChanged (int)
Customizing QRangeSlider
You can style the range slider as below:
::
QRangeSlider * {
border: 0px;
padding: 0px;
}
QRangeSlider #Head {
background: #222;
}
QRangeSlider #Span {
background: #393;
}
QRangeSlider #Span:active {
background: #282;
}
QRangeSlider #Tail {
background: #222;
}
Styling the range slider handles follows QSplitter options:
::
QRangeSlider > QSplitter::handle {
background: #393;
}
QRangeSlider > QSplitter::handle:vertical {
height: 4px;
}
QRangeSlider > QSplitter::handle:pressed {
background: #ca5;
}
"""
endValueChanged = QtCore.pyqtSignal(int)
maxValueChanged = QtCore.pyqtSignal(int)
minValueChanged = QtCore.pyqtSignal(int)
startValueChanged = QtCore.pyqtSignal(int)
# define splitter indices
_SPLIT_START = 1
_SPLIT_END = 2
def __init__(self, parent=None):
"""Create a new QRangeSlider instance.
:param parent: QWidget parent
:return: New QRangeSlider instance.
"""
super(QRangeSlider, self).__init__(parent)
self.setupUi(self)
self.setMouseTracking(False)
self._splitter.splitterMoved.connect(self._handleMoveSplitter)
# head layout
self._head_layout = QHBoxLayout()
self._head_layout.setSpacing(0)
self._head_layout.setContentsMargins(0, 0, 0, 0)
self._head.setLayout(self._head_layout)
self.head = Head(self._head, main=self)
self._head_layout.addWidget(self.head)
# handle layout
self._handle_layout = QHBoxLayout()
self._handle_layout.setSpacing(0)
self._handle_layout.setContentsMargins(0, 0, 0, 0)
self._handle.setLayout(self._handle_layout)
self.handle = Handle(self._handle, main=self)
self.handle.setTextColor((150, 255, 150))
self._handle_layout.addWidget(self.handle)
# tail layout
self._tail_layout = QHBoxLayout()
self._tail_layout.setSpacing(0)
self._tail_layout.setContentsMargins(0, 0, 0, 0)
self._tail.setLayout(self._tail_layout)
self.tail = Tail(self._tail, main=self)
self._tail_layout.addWidget(self.tail)
# defaults
self.setMin(0)
self.setMax(99)
self.setStart(0)
self.setEnd(99)
self.setDrawValues(True)
def min(self):
""":return: minimum value"""
return getattr(self, '__min', None)
def max(self):
""":return: maximum value"""
return getattr(self, '__max', None)
def setMin(self, value):
"""sets minimum value"""
value = float(value)
assert type(value) is float
setattr(self, '__min', value)
self.minValueChanged.emit(value)
def setMax(self, value):
"""sets maximum value"""
value = float(value)
assert type(value) is float
setattr(self, '__max', value)
self.maxValueChanged.emit(value)
def start(self):
""":return: range slider start value"""
return getattr(self, '__start', None)
def end(self):
""":return: range slider end value"""
return getattr(self, '__end', None)
def _setStart(self, value):
"""stores the start value only"""
setattr(self, '__start', value)
self.startValueChanged.emit(value)
def setStart(self, value):
"""sets the range slider start value"""
value = float(value)
assert type(value) is float
v = self._valueToPos(value)
self._splitter.splitterMoved.disconnect()
self._splitter.moveSplitter(v, self._SPLIT_START)
self._splitter.splitterMoved.connect(self._handleMoveSplitter)
self._setStart(value)
def _setEnd(self, value):
"""stores the end value only"""
setattr(self, '__end', value)
self.endValueChanged.emit(value)
def setEnd(self, value):
"""set the range slider end value"""
value = float(value)
assert type(value) is float
v = self._valueToPos(value)
self._splitter.splitterMoved.disconnect()
self._splitter.moveSplitter(v, self._SPLIT_END)
self._splitter.splitterMoved.connect(self._handleMoveSplitter)
self._setEnd(value)
def drawValues(self):
""":return: True if slider values will be drawn"""
return getattr(self, '__drawValues', None)
def setDrawValues(self, draw):
"""sets draw values boolean to draw slider values"""
assert type(draw) is bool
setattr(self, '__drawValues', draw)
def getRange(self):
""":return: the start and end values as a tuple"""
return (self.start(), self.end())
def setRange(self, start, end):
"""set the start and end values"""
self.setStart(start)
self.setEnd(end)
def keyPressEvent(self, event):
"""overrides key press event to move range left and right"""
key = event.key()
if key == QtCore.Qt.Key_Left:
s = self.start()-1
e = self.end()-1
elif key == QtCore.Qt.Key_Right:
s = self.start()+1
e = self.end()+1
else:
event.ignore()
return
event.accept()
if s >= self.min() and e <= self.max():
self.setRange(s, e)
def setBackgroundStyle(self, style):
"""sets background style"""
self._tail.setStyleSheet(style)
self._head.setStyleSheet(style)
def setSpanStyle(self, style):
"""sets range span handle style"""
self._handle.setStyleSheet(style)
def _valueToPos(self, value):
"""converts slider value to local pixel x coord"""
return scale(value, (self.min(), self.max()), (0, self.width()))
def _posToValue(self, xpos):
"""converts local pixel x coord to slider value"""
return scale(xpos, (0, self.width()), (self.min(), self.max()))
def _handleMoveSplitter(self, xpos, index):
"""private method for handling moving splitter handles"""
hw = self._splitter.handleWidth()
def _lockWidth(widget):
width = widget.size().width()
widget.setMinimumWidth(width)
widget.setMaximumWidth(width)
def _unlockWidth(widget):
widget.setMinimumWidth(0)
widget.setMaximumWidth(16777215)
v = self._posToValue(xpos)
if index == self._SPLIT_START:
_lockWidth(self._tail)
if v >= self.end():
return
offset = -20
w = xpos + offset
self._setStart(v)
elif index == self._SPLIT_END:
_lockWidth(self._head)
if v <= self.start():
return
offset = -40
w = self.width() - xpos + offset
self._setEnd(v)
_unlockWidth(self._tail)
_unlockWidth(self._head)
_unlockWidth(self._handle)
app = QtGui.QApplication(sys.argv)
rs1 = QRangeSlider()
rs1.show()
rs1.setRange(15, 35)
rs1.setBackgroundStyle('background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #222, stop:1 #333);')
rs1.setSpanStyle('background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #282, stop:1 #393);')
app.exec_()
Edit: I modified the code to show 3 lines and the different effects when I move the range.
I guess the problem comes when I create the line in the same widget, so I should do a separate one for the lines. Am I right?
Related
I am attempting to design a label class that inherits from the PyQt5 base QLabel class that is able to track another widget. Here is the current code for my class:
class AttachedLabel(QLabel):
def __init__(self, attachedTo, *args, side="left", ** kwargs):
super().__init__(*args, **kwargs) # Run parent initialization
# Define instance variables
self.attached = attachedTo
self.side = side
# Update label position
self.updatePos()
def updatePos(self):
# Get "attached widget" position and dimensions
x = self.attached.geometry().x()
y = self.attached.geometry().y()
aWidth = self.attached.geometry().width()
aHeight = self.attached.geometry().height()
# Get own dimensions
width = self.geometry().width()
height = self.geometry().height()
if self.side == "top": # Above of attached widget
self.setGeometry(x, y-height, width, height)
elif self.side == "bottom": # Below attached widget
self.setGeometry(x, y+height+aHeight, width, height)
elif self.side == "right": # Right of attached widget
self.setGeometry(x + width + aWidth, y, width, height)
else: # Left of attached widget
self.setGeometry(x - width, y, width, height)
I want to be able to instantiate the label like so:
AttachedLabel(self.pushButton, self.centralwidget)
where self.pushButton is the widget it is supposed to be following. The issue is that I don't know how to detect when the widget moves in order to run my updatePos() function. I would ideally only update the label position when the other widget moves, but I want to refrain from havign to add extra code to the class of the widget that is being tracked. I have tried overriding the paintEvent, but that only triggers when the object itself needs to be redrawn, so it doesn't even function as a sub-optimal solution.
Is there some built-in method I can use/override to detect when the widget moves or when the screen itself is updated?
You have to use an eventFilter intersecting the QEvent::Move event and you should also track the resize through the QEvent::Resize event.
from dataclasses import dataclass, field
import random
from PyQt5 import QtCore, QtWidgets
class GeometryTracker(QtCore.QObject):
geometryChanged = QtCore.pyqtSignal()
def __init__(self, widget):
super().__init__(widget)
self._widget = widget
self.widget.installEventFilter(self)
#property
def widget(self):
return self._widget
def eventFilter(self, source, event):
if self.widget is source and event.type() in (
QtCore.QEvent.Move,
QtCore.QEvent.Resize,
):
self.geometryChanged.emit()
return super().eventFilter(source, event)
#dataclass
class TrackerManager:
widget1: field(default_factory=QtWidgets.QWidget)
widget2: field(default_factory=QtWidgets.QWidget)
alignment: QtCore.Qt.Alignment = QtCore.Qt.AlignLeft
enabled: bool = True
valid_alignments = (
QtCore.Qt.AlignLeft,
QtCore.Qt.AlignRight,
QtCore.Qt.AlignHCenter,
QtCore.Qt.AlignTop,
QtCore.Qt.AlignBottom,
QtCore.Qt.AlignVCenter,
)
def __post_init__(self):
self._traker = GeometryTracker(self.widget1)
self._traker.geometryChanged.connect(self.update)
if not any(self.alignment & flag for flag in self.valid_alignments):
raise ValueError("alignment is not valid")
def update(self):
if not self.enabled:
return
r = self.widget1.rect()
p1 = r.center()
c1 = r.center()
if self.alignment & QtCore.Qt.AlignLeft:
p1.setX(r.left())
if self.alignment & QtCore.Qt.AlignRight:
p1.setX(r.right())
if self.alignment & QtCore.Qt.AlignTop:
p1.setY(r.top())
if self.alignment & QtCore.Qt.AlignBottom:
p1.setY(r.bottom())
p2 = self.convert_position(p1)
c2 = self.convert_position(c1)
g = self.widget2.geometry()
g.moveCenter(c2)
if self.alignment & QtCore.Qt.AlignLeft:
g.moveRight(p2.x())
if self.alignment & QtCore.Qt.AlignRight:
g.moveLeft(p2.x())
if self.alignment & QtCore.Qt.AlignTop:
g.moveBottom(p2.y())
if self.alignment & QtCore.Qt.AlignBottom:
g.moveTop(p2.y())
self.widget2.setGeometry(g)
def convert_position(self, point):
gp = self.widget1.mapToGlobal(point)
if self.widget2.isWindow():
return gp
return self.widget2.parent().mapFromGlobal(gp)
class MainWindow(QtWidgets.QMainWindow):
def __init__(self, parent=None):
super().__init__(parent)
self.button = QtWidgets.QPushButton("Press me", self)
self.label = QtWidgets.QLabel(
"Tracker\nLabel", self, alignment=QtCore.Qt.AlignCenter
)
self.label.setAttribute(QtCore.Qt.WA_TransparentForMouseEvents, True)
self.label.setFixedSize(200, 200)
self.label.setStyleSheet(
"background-color: salmon; border: 1px solid black; font-size: 40pt;"
)
self.resize(640, 480)
self.manager = TrackerManager(
widget1=self.button,
widget2=self.label,
alignment=QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter,
)
self.move_button()
def move_button(self):
pos = QtCore.QPoint(*random.sample(range(400), 2))
animation = QtCore.QPropertyAnimation(
targetObject=self.button,
parent=self,
propertyName=b"pos",
duration=1000,
startValue=self.button.pos(),
endValue=pos,
)
animation.finished.connect(self.move_button)
animation.start(QtCore.QAbstractAnimation.DeleteWhenStopped)
def main():
import sys
app = QtWidgets.QApplication(sys.argv)
w = MainWindow()
w.show()
sys.exit(app.exec_())
if __name__ == "__main__":
main()
How can I add a button to the right side of QListViewItems? I'm trying to recreate a similar tag editor like the one you see here on Stackoverflow.
Current:
Goal:
import os, sys, re, glob, pprint
from Qt import QtCore, QtGui, QtWidgets
class TagsEditorWidget(QtWidgets.QWidget):
def __init__(self, *args, **kwargs):
QtWidgets.QWidget.__init__(self)
self.setWindowTitle('Tags')
self.resize(640,400)
# privates
self._tags = []
# controls
self.uiLineEdit = QtWidgets.QLineEdit('df, df,d , dfd d, ')
self.uiAdd = QtWidgets.QToolButton()
self.uiAdd.setText('+')
self.uiAdd.setIcon(QtGui.QIcon(StyleUtils.getIconFilepath('add.svg')))
self.uiAdd.setIconSize(QtCore.QSize(16,16))
self.uiAdd.setFixedSize(24,24)
self.uiAdd.setFocusPolicy(QtCore.Qt.ClickFocus)
self.uiListView = QtWidgets.QListView()
self.uiListView.setViewMode(QtWidgets.QListView.IconMode)
self.uiListView.setMovement(QtWidgets.QListView.Static)
self.uiListView.setResizeMode(QtWidgets.QListView.Adjust)
self.uiListView.setSelectionMode(QtWidgets.QAbstractItemView.NoSelection)
self.uiListView.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers)
model = QtCore.QStringListModel()
self.uiListView.setModel(model)
# layout
self.hLayout = QtWidgets.QHBoxLayout()
self.hLayout.setContentsMargins(0,0,0,0)
self.hLayout.setSpacing(5)
self.hLayout.addWidget(self.uiLineEdit)
self.hLayout.addWidget(self.uiAdd)
self.layout = QtWidgets.QVBoxLayout(self)
self.layout.setContentsMargins(0,0,0,0)
self.layout.setSpacing(0)
self.layout.addLayout(self.hLayout)
self.layout.addWidget(self.uiListView)
self.layout.setStretch(0,1.0)
# Signals
self.uiAdd.clicked.connect(self.slotEnteredTags)
self.uiLineEdit.returnPressed.connect(self.slotEnteredTags)
self.setStyleSheet('''
QListView:item {
background: rgb(200,225,240);
margin: 2px;
padding: 2px;
border-radius: 2px;
}
''')
# Methods
def setTags(self, tags=None):
tags = [] if tags == None else tags
tags = self.getUniqueList(tags)
self._tags = tags
# Update ui
model = self.uiListView.model()
model.setStringList(self._tags)
def getTags(self):
return self._tags
def getUniqueList(self, lst):
result=[]
marker = set()
for l in lst:
lc = l.lower()
if lc not in marker:
marker.add(lc)
result.append(l)
return result
def appendTag(self, tag):
# split by comma and remove leading/trailing spaces and empty strings
tags = filter(None, [x.strip() for x in tag.split(',')])
self.setTags(self.getTags() + tags)
def appendTags(self, tags):
for t in tags:
self.appendTag(t)
# Slots
def slotEnteredTags(self):
self.appendTag(self.uiLineEdit.text())
self.uiLineEdit.clear()
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
ex = TagsEditorWidget()
ex.setTags(["Paper", "Plastic", "Aluminum", "Paper", "Tin", "Glass", "Tin", "Polypropylene Plastic"])
ex.show()
sys.exit(app.exec_())
UPDATE #1
I attempted to use the ItemDelegate, however it appears to produce more problems, like spacing and padding around the text within the list. I don't understand why the SizeHint i override doesn't appear to properly work. The X button still overlap when they shouldn't.
import os, sys, re, glob, pprint
from Qt import QtCore, QtGui, QtWidgets
class TagDelegate(QtWidgets.QItemDelegate):
def __init__(self, parent=None, *args):
QtWidgets.QItemDelegate.__init__(self, parent, *args)
def paint(self, painter, option, index):
painter.save()
isw, ish = option.decorationSize.toTuple()
x, y = option.rect.topLeft().toTuple()
dx, dy = option.rect.size().toTuple()
value = index.data(QtCore.Qt.DisplayRole)
rect = QtCore.QRect(x, y, dx, dy)
painter.setPen(QtGui.QPen(QtCore.Qt.NoPen))
painter.setBrush(QtGui.QBrush(QtCore.Qt.blue))
painter.drawRect(rect)
painter.setPen(QtGui.QPen(QtCore.Qt.black))
painter.drawText(rect, QtCore.Qt.AlignLeft, value)
rect = QtCore.QRect(x+dx-5, y, 16, dy)
painter.setPen(QtGui.QPen(QtCore.Qt.NoPen))
painter.setBrush(QtGui.QBrush(QtCore.Qt.gray))
painter.drawRect(rect)
painter.setPen(QtGui.QPen(QtCore.Qt.black))
painter.drawText(rect, QtCore.Qt.AlignCenter, 'x')
painter.restore()
def sizeHint(self, option, index):
if index.data():
font = QtGui.QFontMetrics(option.font)
text = index.data(QtCore.Qt.DisplayRole)
rect = font.boundingRect(option.rect, QtCore.Qt.TextSingleLine, text)
# rect.adjust(0, 0, 15, 0)
return QtCore.QSize(rect.width(), rect.height())
return super(TagDelegate, self).sizeHint(option, index)
class TagListView(QtWidgets.QListView):
def __init__(self, *arg, **kwargs):
super(TagListView, self).__init__()
self.setViewMode(QtWidgets.QListView.IconMode)
self.setMovement(QtWidgets.QListView.Static)
self.setResizeMode(QtWidgets.QListView.Adjust)
self.setSelectionMode(QtWidgets.QAbstractItemView.NoSelection)
self.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers)
self.setMouseTracking(True)
self.setItemDelegate(TagDelegate())
class TagsEditorWidget(QtWidgets.QWidget):
def __init__(self, *args, **kwargs):
QtWidgets.QWidget.__init__(self)
self.setWindowTitle('Tags')
self.resize(640,400)
# privates
self._tags = []
# controls
self.uiLineEdit = QtWidgets.QLineEdit('df, df,d , dfd d, ')
self.uiAdd = QtWidgets.QToolButton()
self.uiAdd.setText('+')
# self.uiAdd.setIcon(QtGui.QIcon(StyleUtils.getIconFilepath('add.svg')))
self.uiAdd.setIconSize(QtCore.QSize(16,16))
self.uiAdd.setFixedSize(24,24)
self.uiAdd.setFocusPolicy(QtCore.Qt.ClickFocus)
self.uiListView = TagListView()
model = QtCore.QStringListModel()
self.uiListView.setModel(model)
# layout
self.hLayout = QtWidgets.QHBoxLayout()
self.hLayout.setContentsMargins(0,0,0,0)
self.hLayout.setSpacing(5)
self.hLayout.addWidget(self.uiLineEdit)
self.hLayout.addWidget(self.uiAdd)
self.layout = QtWidgets.QVBoxLayout(self)
self.layout.setContentsMargins(0,0,0,0)
self.layout.setSpacing(0)
self.layout.addLayout(self.hLayout)
self.layout.addWidget(self.uiListView)
self.layout.setStretch(0,1.0)
# Signals
self.uiAdd.clicked.connect(self.slotEnteredTags)
self.uiLineEdit.returnPressed.connect(self.slotEnteredTags)
self.setStyleSheet('''
QListView:item {
background: rgb(200,225,240);
margin: 2px;
padding: 2px;
border-radius: 2px;
}
''')
# Methods
def setTags(self, tags=None):
tags = [] if tags == None else tags
tags = self.getUniqueList(tags)
self._tags = tags
# Update ui
model = self.uiListView.model()
model.setStringList(self._tags)
def getTags(self):
return self._tags
def getUniqueList(self, lst):
result=[]
marker = set()
for l in lst:
lc = l.lower()
if lc not in marker:
marker.add(lc)
result.append(l)
return result
def appendTag(self, tag):
# split by comma and remove leading/trailing spaces and empty strings
tags = filter(None, [x.strip() for x in tag.split(',')])
self.setTags(self.getTags() + tags)
def appendTags(self, tags):
for t in tags:
self.appendTag(t)
# Slots
def slotEnteredTags(self):
self.appendTag(self.uiLineEdit.text())
self.uiLineEdit.clear()
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
ex = TagsEditorWidget()
ex.setTags(["Paper", "Plastic", "Aluminum", "Paper", "Tin", "Glass", "Tin", "Polypropylene Plastic"])
ex.show()
sys.exit(app.exec_())
The size returned by sizeHint() has to cover the whole item size. In your paintEvent method, QtCore.QRect(x+dx-5, y, 16, dy) defines a rect with 11 pixels outside the items bounding rect (16 - 5).
The easiest way to calculate the right size is to consider the button size in sizeHint:
class TagDelegate(QStyledItemDelegate):
def __init__(self, parent=None):
super().__init__(parent)
self._buttonSize = QSize(16, 16)
def buttonRect(self, boundingRect):
return boundingRect.adjusted(boundingRect.width() - self._buttonSize.width(), 0, 0, 0)
def labelRect(self, boundingRect):
return boundingRect.adjusted(0, 0, -self._buttonSize.width(), 0)
def paint(self, painter, option, index):
text = index.data(QtCore.Qt.DisplayRole)
painter.save()
painter.drawText(self.labelRect(option.rect), QtCore.Qt.AlignLeft, text)
rect = self.buttonRect(option.rect)
painter.setPen(QtGui.QPen(QtCore.Qt.NoPen))
painter.setBrush(QtGui.QBrush(QtCore.Qt.gray))
painter.drawRect(rect)
painter.setPen(QtGui.QPen(QtCore.Qt.black))
painter.drawText(rect, QtCore.Qt.AlignCenter, 'x')
painter.restore()
def sizeHint(self, option, index):
if index.data():
font = QtGui.QFontMetrics(option.font)
text = index.data(QtCore.Qt.DisplayRole)
rect = font.boundingRect(option.rect, QtCore.Qt.TextSingleLine, text)
return QSize(rect.width() + self._buttonSize.width(), max(rect.height(), self._buttonSize.height()))
return super(TagDelegate, self).sizeHint(option, index)
I used two methods (buttonRect and labelRect) to get the position of each part of the item. It will make easier to handle the click on the close button:
def editorEvent(self, event, model, option, index):
if not isinstance(event, QMouseEvent) or event.type() != QEvent.MouseButtonPress:
return super().editorEvent(event, model, option, index)
if self.buttonRect(option.rect).contains(event.pos()):
print("Close button on ", model.data(index, Qt.DisplayRole))
return True
elif self.labelRect(option.rect).contains(event.pos()):
print("Label clicked on ", model.data(index, Qt.DisplayRole))
return True
return False
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()
I couldn't get through some problem for 2 days in my app, so I post here.
I have UI written in pyqt5 (Qt designer), and part of main window consists QML objects (inside QQuickWidget).
My problem is refreshing gauge object inside QQuickWidget.
For example, if I run application with QML file as ApplicationWindow, everything is ok and I am able to manipulate data:
Gauge is refreshing
But when I place this object (and change object in QML to Rectangle) into the QQuickWidget, I am not able to update the state of this object.
gauge inside python UI application - not refreshing
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
window = GUI_MainWindow() #Main window written in pyqt5
qmlRegisterType(RadialBar, "SDK", 1,0, "RadialBar")
# Setting source for QML Widget
# batteryCWidget is the QQUickWidget object (4 of them are on main window)
window.batteryCWidget.setSource(QUrl('qml_widget.qml'))
batteryWidget = MyClass() # Class with function to update data in QML
engine = QQmlApplicationEngine()
context = engine.rootContext()
context.setContextProperty("batteryWidget", batteryWidget)
engine.load('qml_widget.qml')
root = engine.rootObjects()[0]
timer = QTimer()
timer.start(200)
#Every 200ms I generate new number in function random_value
timer.timeout.connect(batteryWidget.random_value)
#and then update value in QML
batteryWidget.randomValue.connect(root.setValue)
Is it possible to update/repaint/refresh state of object inside QQuickWidget?
This is qml_widget.qml:
import QtQuick 2.4
import SDK 1.0
import QtQuick.Layouts 1.1
Rectangle {
id: root
Layout.alignment: Layout.Center
width: 160
height: 145
color: "#181818"
property var suffix: "A"
property int minVal: 0
property int maxVal: 100
property var actVal: 0
function setValue(v) {
actVal = v
}
Rectangle {
Layout.alignment: Layout.Center
width: 160
height: 145
color: "#1d1d35"
border.color: "#000000"
border.width: 3
Text {
id: name
text: "Battery Current"
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
anchors.topMargin: 5
font.pointSize: 13
color: "#6affcd"
}
RadialBar {
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.bottom
width: parent.width / 1.4
height: width - (0.001)*actVal
penStyle: Qt.RoundCap
progressColor: "#6affcd"
foregroundColor: "#191a2f"
dialWidth: 11
minValue: minVal
maxValue: maxVal
value: actVal
suffixText: suffix
textFont {
family: "Halvetica"
italic: false
pointSize: 18
}
textColor: "#00ffc1"
}}
MyClass:
class MyClass(QObject):
randomValue = pyqtSignal(float)
def __init__(self, parent=None):
super(MyClass, self).__init__(parent)
def random_value(self):
v = float(randrange(1, 100))
self.randomValue.emit(v)
RadialBar class:
class RadialBar(QQuickPaintedItem):
class DialType():
FullDial = 0
MinToMax = 1
NoDial = 2
sizeChanged = pyqtSignal()
startAngleChanged = pyqtSignal()
spanAngleChanged = pyqtSignal()
minValueChanged = pyqtSignal()
maxValueChanged = pyqtSignal()
valueChanged = pyqtSignal()
dialWidthChanged = pyqtSignal()
backgroundColorChanged = pyqtSignal()
foregroundColorChanged = pyqtSignal()
progressColorChanged = pyqtSignal()
textColorChanged = pyqtSignal()
suffixTextChanged = pyqtSignal()
showTextChanged = pyqtSignal()
penStyleChanged = pyqtSignal()
dialTypeChanged = pyqtSignal()
textFontChanged = pyqtSignal()
def __init__(self, parent=None):
super(RadialBar, self).__init__(parent)
self.setWidth(200)
self.setHeight(200)
self.setSmooth(True)
self.setAntialiasing(True)
self._Size = 200
self._StartAngle = 40
self._SpanAngle = 280
self._MinValue = 0
self._MaxValue = 100
self._Value = 50
self._DialWidth = 25
self._BackgroundColor = Qt.transparent
self._DialColor = QColor(80,80,80)
self._ProgressColor = QColor(135,26,50)
self._TextColor = QColor(0, 0, 0)
self._SuffixText = ""
self._ShowText = True
self._PenStyle = Qt.FlatCap
self._DialType = RadialBar.DialType.MinToMax
self._TextFont = QFont()
def paint(self, painter):
painter.save()
size = min(self.width(), self.height())
self.setWidth(size)
self.setHeight(size)
rect = QRectF(0, 0, self.width(), self.height()) #self.boundingRect()
painter.setRenderHint(QPainter.Antialiasing)
pen = painter.pen()
pen.setCapStyle(self._PenStyle)
startAngle = -90 - self._StartAngle
if RadialBar.DialType.FullDial != self._DialType:
spanAngle = 0 - self._SpanAngle
else:
spanAngle = -360
#Draw outer dial
painter.save()
pen.setWidth(self._DialWidth)
pen.setColor(self._DialColor)
painter.setPen(pen)
offset = self._DialWidth / 2
if self._DialType == RadialBar.DialType.MinToMax:
painter.drawArc(rect.adjusted(offset, offset, -offset, -offset), startAngle * 16, spanAngle * 16)
elif self._DialType == RadialBar.DialType.FullDial:
painter.drawArc(rect.adjusted(offset, offset, -offset, -offset), -90 * 16, -360 * 16)
else:
pass
#do not draw dial
painter.restore()
#Draw background
painter.save()
painter.setBrush(self._BackgroundColor)
painter.setPen(self._BackgroundColor)
inner = offset * 2
painter.drawEllipse(rect.adjusted(inner, inner, -inner, -inner))
painter.restore()
#Draw progress text with suffix
painter.save()
painter.setFont(self._TextFont)
pen.setColor(self._TextColor)
painter.setPen(pen)
if self._ShowText:
painter.drawText(rect.adjusted(offset, offset, -offset, -offset), Qt.AlignCenter,str(self._Value) + self._SuffixText)
else:
painter.drawText(rect.adjusted(offset, offset, -offset, -offset), Qt.AlignCenter, self._SuffixText)
painter.restore()
#Draw progress bar
painter.save()
pen.setWidth(self._DialWidth)
pen.setColor(self._ProgressColor)
valueAngle = float(float(self._Value - self._MinValue)/float(self._MaxValue - self._MinValue)) * float(spanAngle) #Map value to angle range
painter.setPen(pen)
painter.drawArc(rect.adjusted(offset, offset, -offset, -offset), startAngle * 16, valueAngle * 16)
painter.restore()
#QtCore.pyqtProperty(str, notify=sizeChanged)
def size(self):
return self._Size
#size.setter
def size(self, size):
if self._Size == size:
return
self._Size = size
self.sizeChanged.emit()
#QtCore.pyqtProperty(int, notify=startAngleChanged)
def startAngle(self):
return self._StartAngle
#startAngle.setter
def startAngle(self, angle):
if self._StartAngle == angle:
return
self._StartAngle = angle
self.startAngleChanged.emit()
#QtCore.pyqtProperty(int, notify=spanAngleChanged)
def spanAngle(self):
return self._SpanAngle
#spanAngle.setter
def spanAngle(self, angle):
if self._SpanAngle == angle:
return
self._SpanAngle = angle
self.spanAngleChanged.emit()
#QtCore.pyqtProperty(int, notify=minValueChanged)
def minValue(self):
return self._MinValue
#minValue.setter
def minValue(self, value):
if self._MinValue == value:
return
self._MinValue = value
self.minValueChanged.emit()
#QtCore.pyqtProperty(int, notify=maxValueChanged)
def maxValue(self):
return self._MaxValue
#maxValue.setter
def maxValue(self, value):
if self._MaxValue == value:
return
self._MaxValue = value
self.maxValueChanged.emit()
#QtCore.pyqtProperty(float, notify=valueChanged)
def value(self):
return self._Value
#value.setter
def value(self, value):
if self._Value == value:
return
self._Value = value
self.valueChanged.emit()
#QtCore.pyqtProperty(float, notify=dialWidthChanged)
def dialWidth(self):
return self._DialWidth
#dialWidth.setter
def dialWidth(self, width):
if self._DialWidth == width:
return
self._DialWidth = width
self.dialWidthChanged.emit()
#QtCore.pyqtProperty(QColor, notify=backgroundColorChanged)
def backgroundColor(self):
return self._BackgroundColor
#backgroundColor.setter
def backgroundColor(self, color):
if self._BackgroundColor == color:
return
self._BackgroundColor = color
self.backgroundColorChanged.emit()
#QtCore.pyqtProperty(QColor, notify=foregroundColorChanged)
def foregroundColor(self):
return self._ForegrounColor
#foregroundColor.setter
def foregroundColor(self, color):
if self._DialColor == color:
return
self._DialColor = color
self.foregroundColorChanged.emit()
#QtCore.pyqtProperty(QColor, notify=progressColorChanged)
def progressColor(self):
return self._ProgressColor
#progressColor.setter
def progressColor(self, color):
if self._ProgressColor == color:
return
self._ProgressColor = color
self.progressColorChanged.emit()
#QtCore.pyqtProperty(QColor, notify=textColorChanged)
def textColor(self):
return self._TextColor
#textColor.setter
def textColor(self, color):
if self._TextColor == color:
return
self._TextColor = color
self.textColorChanged.emit()
#QtCore.pyqtProperty(str, notify=suffixTextChanged)
def suffixText(self):
return self._SuffixText
#suffixText.setter
def suffixText(self, text):
if self._SuffixText == text:
return
self._SuffixText = text
self.suffixTextChanged.emit()
#QtCore.pyqtProperty(str, notify=showTextChanged)
def showText(self):
return self._ShowText
#showText.setter
def showText(self, show):
if self._ShowText == show:
return
self._ShowText = show
#QtCore.pyqtProperty(Qt.PenCapStyle, notify=penStyleChanged)
def penStyle(self):
return self._PenStyle
#penStyle.setter
def penStyle(self, style):
if self._PenStyle == style:
return
self._PenStyle = style
self.penStyleChanged.emit()
#QtCore.pyqtProperty(int, notify=dialTypeChanged)
def dialType(self):
return self._DialType
#dialType.setter
def dialType(self, type):
if self._DialType == type:
return
self._DialType = type
self.dialTypeChanged.emit()
#QtCore.pyqtProperty(QFont, notify=textFontChanged)
def textFont(self):
return self._TextFont
#textFont.setter
def textFont(self, font):
if self._TextFont == font:
return
self._TextFont = font
self.textFontChanged.emit()
When you have an object created in Python/C++ and you want to connect it to an object created in QML, the correct option is to do it on the QML side using Connections, but for this you must create a property in MyClass.
main.py
import sys
from random import randrange
from PyQt5 import QtCore, QtGui, QtWidgets, QtQml, QtQuick, QtQuickWidgets
from RadialBar import RadialBar
class MyClass(QtCore.QObject):
randomValueChanged = QtCore.pyqtSignal(float)
def __init__(self, parent=None):
super(MyClass, self).__init__(parent)
self.m_randomValue = 0
#QtCore.pyqtProperty(float, notify=randomValueChanged)
def randomValue(self):
return self.m_randomValue
#randomValue.setter
def randomValue(self, v):
if self.m_randomValue == v:
return
self.m_randomValue = v
self.randomValueChanged.emit(v)
def random_value(self):
v = float(randrange(1, 100))
self.randomValue = v
class GUI_MainWindow(QtWidgets.QMainWindow):
def __init__(self, parent=None):
QtWidgets.QMainWindow.__init__(self, parent)
self.batteryCWidget = QtQuickWidgets.QQuickWidget()
self.setCentralWidget(self.batteryCWidget)
self.batteryCWidget.setResizeMode(QtQuickWidgets.QQuickWidget.SizeRootObjectToView)
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
window = GUI_MainWindow() #Main window written in pyqt5
QtQml.qmlRegisterType(RadialBar, "SDK", 1,0, "RadialBar")
batteryWidget = MyClass() # Class with function to update data in QML
context = window.batteryCWidget.rootContext()
context.setContextProperty("batteryWidget",batteryWidget)
window.batteryCWidget.setSource(QtCore.QUrl.fromLocalFile('qml_widget.qml'))
timer = QtCore.QTimer()
timer.timeout.connect(batteryWidget.random_value)
timer.start(200)
window.show()
sys.exit(app.exec_())
qml_widget.qml
import QtQuick 2.4
import SDK 1.0
import QtQuick.Layouts 1.1
Rectangle {
id: root
Layout.alignment: Layout.Center
width: 160
height: 145
color: "#181818"
property string suffix: "A"
property int minVal: 0
property int maxVal: 100
property real actVal: 0
Connections{
target: batteryWidget
onRandomValueChanged: root.actVal = batteryWidget.randomValue
}
Rectangle {
Layout.alignment: Layout.Center
width: 160
height: 145
color: "#1d1d35"
border.color: "#000000"
border.width: 3
Text {
id: name
text: "Battery Current"
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
anchors.topMargin: 5
font.pointSize: 13
color: "#6affcd"
}
RadialBar {
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.bottom
width: parent.width / 1.4
height: width - (0.001)*actVal
penStyle: Qt.RoundCap
progressColor: "#6affcd"
foregroundColor: "#191a2f"
dialWidth: 11
minValue: minVal
maxValue: maxVal
value: actVal
suffixText: suffix
textFont {
family: "Halvetica"
italic: false
pointSize: 18
}
textColor: "#00ffc1"
}
}
}
You can find the complete code in the following link.
As #GrecKo says, a much simpler way is to make a binding.
...
property real actVal: batteryWidget.randomValue
Rectangle {
...
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.