I have a media player developed in Python and I have created a button that extracts the actual value of the slider's tick, the problem is it would be great if I can see it on the slider just to know the position of the mark I've already extracted. The idea is every time I push the button a mark should appear on the slider in that value's position. Anyone knows how to do it?
While QSlider offers the possibility to draw tick marks, they are only possible for constant intervals.
A possible solution is to create a custom widget that "embeds" the slider in a layout with an appropriate margin (on top, for example) that will be then painted over with the requested tickmarks. While this solution is effective, it might not be the most consistent, as it results in making the whole slider occupy a lot of space for the ticks.
class TickSlider(QtWidgets.QWidget):
valueChanged = QtCore.pyqtSignal(int)
def __init__(self):
super().__init__()
layout = QtWidgets.QVBoxLayout(self)
layout.setContentsMargins(0, 12, 0, 0)
layout.setSpacing(0)
self.slider = QtWidgets.QSlider(QtCore.Qt.Horizontal, maximum=101)
layout.addWidget(self.slider)
# link slider functions; be aware that this is not usually advised
# and you should better write specific functions that call the actual
# slider methods
self.value = self.slider.value
self.setValue = self.slider.setValue
self.minimum = self.slider.minimum
self.setMinimum = self.slider.setMinimum
self.maximum = self.slider.maximum
self.setMaximum = self.slider.setMaximum
self.ticks = set()
self.slider.valueChanged.connect(self.valueChanged)
def addTick(self, value=None):
if isinstance(value, bool) or value is None:
value = self.slider.value()
if not value in self.ticks and self.minimum() <= value <= self.maximum():
self.ticks.add(value)
self.update()
def removeTick(self, value=None):
if isinstance(value, bool) or value is None:
value = self.slider.value()
if value in self.ticks:
self.ticks.discard(value)
self.update()
def paintEvent(self, event):
if not self.ticks:
return
sliderMin = self.slider.minimum()
sliderMax = self.slider.maximum()
style = self.style()
opt = QtWidgets.QStyleOptionSlider()
self.slider.initStyleOption(opt)
sliderLength = style.pixelMetric(
style.PM_SliderLength, opt, self.slider)
span = style.pixelMetric(
style.PM_SliderSpaceAvailable, opt, self.slider)
qp = QtGui.QPainter(self)
qp.translate(opt.rect.x() + sliderLength / 2, 0)
y = self.slider.y() - 2
for value in sorted(self.ticks):
x = style.sliderPositionFromValue(
sliderMin, sliderMax, value, span)
qp.drawLine(x, 0, x, y)
Another possibility implies some "hacking" around the current style.
The major problem comes from the fact that in some systems and styles, Qt uses pixmaps to draw objects, and this "overpaints" any previous attempt to draw something "under" the actual slider.
The trick is to paint components of the slider individually, so that the ticks can be painted over the background (including the groove in which the handle moves) and before the handle is actually painted.
This approach offers the major benefit of directly using a QSlider instead of a container widget.
class TickOverride(QtWidgets.QSlider):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.ticks = set()
self.setTickPosition(self.TicksAbove)
def addTick(self, value=None):
if isinstance(value, bool) or value is None:
value = self.value()
if not value in self.ticks and self.minimum() <= value <= self.maximum():
self.ticks.add(value)
self.update()
def removeTick(self, value=None):
if isinstance(value, bool) or value is None:
value = self.value()
if value in self.ticks:
self.ticks.discard(value)
self.update()
def paintEvent(self, event):
qp = QtWidgets.QStylePainter(self)
opt = QtWidgets.QStyleOptionSlider()
style = self.style()
self.initStyleOption(opt)
# draw the groove only
opt.subControls = style.SC_SliderGroove
qp.drawComplexControl(style.CC_Slider, opt)
sliderMin = self.minimum()
sliderMax = self.maximum()
sliderLength = style.pixelMetric(style.PM_SliderLength, opt, self)
span = style.pixelMetric(style.PM_SliderSpaceAvailable, opt, self)
# if the tick option is set and ticks actually exist, draw them
if self.ticks and self.tickPosition():
qp.save()
qp.translate(opt.rect.x() + sliderLength / 2, 0)
grooveRect = style.subControlRect(
style.CC_Slider, opt, style.SC_SliderGroove)
grooveTop = grooveRect.top() - 1
grooveBottom = grooveRect.bottom() + 1
ticks = self.tickPosition()
bottom = self.height()
for value in sorted(self.ticks):
x = style.sliderPositionFromValue(
sliderMin, sliderMax, value, span)
if ticks & self.TicksAbove:
qp.drawLine(x, 0, x, grooveTop)
if ticks & self.TicksBelow:
qp.drawLine(x, grooveBottom, x, bottom)
qp.restore()
opt.subControls = style.SC_SliderHandle
opt.activeSubControls = style.SC_SliderHandle
if self.isSliderDown():
opt.state |= style.State_Sunken
qp.drawComplexControl(style.CC_Slider, opt)
Note: this answer obviously covers only horizontal sliders for simplicity, and considering the purpose required by the OP.
Related
How to animate geometry change inside QGridLayout. I have QLabel which will be placed inside the QGridlayout. It should expand when the mouse is inside the QLabel and shrink back to the normal state when outside. I managed to animate but, it doesn't expand from all four sides. It instead moves away from the grid.
MRE:
import sys
from PyQt5 import QtWidgets, QtCore
class Tile(QtWidgets.QLabel):
def __init__(self, *args, **kwargs):
super(Tile, self).__init__(*args, **kwargs)
# p = self.palette()
# p.setColor(self.backgroundRole(), QtCore.Qt.red)
# self.setPalette(p)
self.setText("hello")
self.setMinimumSize(100, 100)
self.setMaximumSize(125, 125)
def enterEvent(self, a0: QtCore.QEvent) -> None:
super(Tile, self).enterEvent(a0)
self.animation = QtCore.QPropertyAnimation(self, b"geometry")
self.animation.setStartValue(QtCore.QRect(self.geometry()))
self.animation.setEndValue(QtCore.QRect(self.geometry().adjusted(-25, -25, 25, 25)))
self.animation.setDuration(150)
self.animation.start(QtCore.QPropertyAnimation.DeleteWhenStopped)
def leaveEvent(self, a0: QtCore.QEvent) -> None:
super(Tile, self).leaveEvent(a0)
self.animation = QtCore.QPropertyAnimation(self, b"geometry")
self.animation.setStartValue(QtCore.QRect(self.geometry()))
self.animation.setEndValue(QtCore.QRect(self.geometry().adjusted(25, 25, -25, -25)))
self.animation.setDuration(150)
self.animation.start(QtCore.QPropertyAnimation.DeleteWhenStopped)
class ScrollView(QtWidgets.QWidget):
def __init__(self, *args, **kwargs):
super(ScrollView, self).__init__(*args, **kwargs)
self.setStyleSheet('border: 1px solid black')
self.setLayout(QtWidgets.QVBoxLayout())
widget = QtWidgets.QWidget()
self.grid_layout = QtWidgets.QGridLayout(widget)
self.scrollArea = QtWidgets.QScrollArea()
self.scrollArea.setWidget(widget)
self.scrollArea.setWidgetResizable(True)
self.grid_layout.setSpacing(50)
self.row_width = 4
self._row = 0
self._column = 0
self.layout().addWidget(self.scrollArea)
def addTile(self):
self.grid_layout.addWidget(Tile(), self._row, self._column)
if self._column == 3:
self._row += 1
self._column = 0
else:
self._column += 1
def main():
app = QtWidgets.QApplication(sys.argv)
win = ScrollView()
for x in range(30):
win.addTile()
win.show()
sys.exit(app.exec())
if __name__ == "__main__":
main()
The main problem is that you're setting a maximum size that is smaller than the end value: if you have a starting rectangle with size 100x100 and you expand it 25 pixels on each side, it will become 150x150, not 125x125. Since you've set that maximum value, once the geometry reaches 125x125 it will only change the coordinates while keeping that maximum size.
But there are also three other issues.
you're always using the current geometry as start value, which can become an issue: if you enter or leave while the other animation is in progress, you get a wrong reference for the target geometry;
if you enter/leave the widget very fast, you'll end up with two concurrent animations taking place; there's literally no benefit in continously creating animations every time;
the widget doesn't take into account resizing of the scroll area, which would alter the aspect ratio and create issues for the new positioning;
In order to avoid all this, you have to use one animation only, change its direction according to the enter/leave event, properly modify the start/end values according to the actual geometry changes, and also properly resize to the "default" size when an external resize happens; the last two points are done only if the animation is not active (since moveEvent and resizeEvent are called by the geometry change).
class Tile(QtWidgets.QLabel):
def __init__(self, *args, **kwargs):
super(Tile, self).__init__(*args, **kwargs)
self.setText("hello")
self.setMinimumSize(100, 100)
self.setMaximumSize(150, 150)
self.animation = QtCore.QPropertyAnimation(self, b"geometry")
self.animation.setDuration(150)
def animate(self, expand):
if expand:
self.animation.setDirection(self.animation.Forward)
else:
self.animation.setDirection(self.animation.Backward)
self.animation.start()
def enterEvent(self, a0: QtCore.QEvent) -> None:
super(Tile, self).enterEvent(a0)
self.animate(True)
def leaveEvent(self, a0: QtCore.QEvent) -> None:
super(Tile, self).leaveEvent(a0)
self.animate(False)
def updateAnimation(self):
if not self.animation.state():
center = self.geometry().center()
start = QtCore.QRect(QtCore.QPoint(), self.minimumSize())
start.moveCenter(center)
self.animation.setStartValue(start)
end = QtCore.QRect(QtCore.QPoint(), self.maximumSize())
end.moveCenter(center)
self.animation.setEndValue(end)
def moveEvent(self, event):
self.updateAnimation()
def resizeEvent(self, event):
self.updateAnimation()
if not self.animation.state():
rect = QtCore.QRect(QtCore.QPoint(),
self.maximumSize() if self.underMouse() else self.minimumSize())
rect.moveCenter(self.geometry().center())
self.setGeometry(rect)
I am designing a ui in qt-desginer where I have added dial. It looks like below:
It doesn't have any markings. Is it possible to add some markings to understand it better. Something like below:
Unfortunately, QDial is a widget that has never been really cared about, mostly because it's scarcely used, but also because many feature needs might change its behavior in inconsistent ways. In any case, this is not possible from Designer.
It can be partially done by subclassing QDial and promoting it in designer, but you'll end up having numbers drawn over the notches or the dial itself, which will look ugly.
In order to achieve what you want, you need to use a custom widget that contains the dial, and eventually promote that widget, but you'll not be able to see it in designer nor set its properties (unless you create a Designer plugin, which is not very easy). This is achieved by setting the container layout margins, so that there's always enough space to show the values as text.
This is a possible implementation. Consider that, for obvious reasons, if you have a big value range with a small dial, the numbers will overlap.
class ValueDial(QtWidgets.QWidget):
_dialProperties = ('minimum', 'maximum', 'value', 'singleStep', 'pageStep',
'notchesVisible', 'tracking', 'wrapping',
'invertedAppearance', 'invertedControls', 'orientation')
_inPadding = 3
_outPadding = 2
valueChanged = QtCore.pyqtSignal(int)
def __init__(self, *args, **kwargs):
# remove properties used as keyword arguments for the dial
dialArgs = {k:v for k, v in kwargs.items() if k in self._dialProperties}
for k in dialArgs.keys():
kwargs.pop(k)
super().__init__(*args, **kwargs)
layout = QtWidgets.QVBoxLayout(self)
self.dial = QtWidgets.QDial(self, **dialArgs)
layout.addWidget(self.dial)
self.dial.valueChanged.connect(self.valueChanged)
# make the dial the focus proxy (so that it captures focus *and* key events)
self.setFocusProxy(self.dial)
# simple "monkey patching" to access dial functions
self.value = self.dial.value
self.setValue = self.dial.setValue
self.minimum = self.dial.minimum
self.maximum = self.dial.maximum
self.wrapping = self.dial.wrapping
self.notchesVisible = self.dial.notchesVisible
self.setNotchesVisible = self.dial.setNotchesVisible
self.setNotchTarget = self.dial.setNotchTarget
self.notchSize = self.dial.notchSize
self.invertedAppearance = self.dial.invertedAppearance
self.setInvertedAppearance = self.dial.setInvertedAppearance
self.updateSize()
def inPadding(self):
return self._inPadding
def setInPadding(self, padding):
self._inPadding = max(0, padding)
self.updateSize()
def outPadding(self):
return self._outPadding
def setOutPadding(self, padding):
self._outPadding = max(0, padding)
self.updateSize()
# the following functions are required to correctly update the layout
def setMinimum(self, minimum):
self.dial.setMinimum(minimum)
self.updateSize()
def setMaximum(self, maximum):
self.dial.setMaximum(maximum)
self.updateSize()
def setWrapping(self, wrapping):
self.dial.setWrapping(wrapping)
self.updateSize()
def updateSize(self):
# a function that sets the margins to ensure that the value strings always
# have enough space
fm = self.fontMetrics()
minWidth = max(fm.width(str(v)) for v in range(self.minimum(), self.maximum() + 1))
self.offset = max(minWidth, fm.height()) / 2
margin = self.offset + self._inPadding + self._outPadding
self.layout().setContentsMargins(margin, margin, margin, margin)
def translateMouseEvent(self, event):
# a helper function to translate mouse events to the dial
return QtGui.QMouseEvent(event.type(),
self.dial.mapFrom(self, event.pos()),
event.button(), event.buttons(), event.modifiers())
def changeEvent(self, event):
if event.type() == QtCore.QEvent.FontChange:
self.updateSize()
def mousePressEvent(self, event):
self.dial.mousePressEvent(self.translateMouseEvent(event))
def mouseMoveEvent(self, event):
self.dial.mouseMoveEvent(self.translateMouseEvent(event))
def mouseReleaseEvent(self, event):
self.dial.mouseReleaseEvent(self.translateMouseEvent(event))
def paintEvent(self, event):
radius = min(self.width(), self.height()) / 2
radius -= (self.offset / 2 + self._outPadding)
invert = -1 if self.invertedAppearance() else 1
if self.wrapping():
angleRange = 360
startAngle = 270
rangeOffset = 0
else:
angleRange = 300
startAngle = 240 if invert > 0 else 300
rangeOffset = 1
fm = self.fontMetrics()
# a reference line used for the target of the text rectangle
reference = QtCore.QLineF.fromPolar(radius, 0).translated(self.rect().center())
fullRange = self.maximum() - self.minimum()
textRect = QtCore.QRect()
qp = QtGui.QPainter(self)
qp.setRenderHints(qp.Antialiasing)
for p in range(0, fullRange + rangeOffset, self.notchSize()):
value = self.minimum() + p
if invert < 0:
value -= 1
if value < self.minimum():
continue
angle = p / fullRange * angleRange * invert
reference.setAngle(startAngle - angle)
textRect.setSize(fm.size(QtCore.Qt.TextSingleLine, str(value)))
textRect.moveCenter(reference.p2().toPoint())
qp.drawText(textRect, QtCore.Qt.AlignCenter, str(value))
if __name__ == '__main__':
import sys
app = QtWidgets.QApplication(sys.argv)
dial = ValueDial(minimum=1, maximum=11)
dial.setNotchesVisible(True)
dial.show()
sys.exit(app.exec_())
i am modeling a chess board using pyqt5 graphicsscene.
th scene is populated with custom QGraphicsItem of 2 types: Case (not movable), Piece (movable)
i overrode the mousepressevent mousemoveevent and mousereleaseevent for the Piece object so that it always land in the center of a case. Not sure if it was the best choice as i could have customized the scene instead .
now i want to receive signals (info on source position, destination position) from the scene or the Piece object, what is a good way to go about that ?
Im new to Qt and not knowing C++ syntax makes it a little hard to follow the official documentation.
class Pawn(QGraphicsItem):
def __init__(self, x_coord, y_coord, color):
super().__init__()
self.x_coord = x_coord
self.y_coord = y_coord
self.color = color
self.setPos(x_coord*30 + 5, y_coord*30 + 5)
self.setFlag(QGraphicsItem.ItemIsMovable)
self.setZValue(2)
self.sourcePos = QPointF(x_coord*30 + 5, y_coord*30 + 5)
self.destinationPos = None
def paint(self, painter, option, widget):
painter.setBrush(QColor(self.color))
painter.drawEllipse(0, 0, 20, 20)
def boundingRect(self):
return QRectF(0, 0, 20, 20)
def mousePressEvent(self, event):
for item in self.scene().items():
if isinstance(item, Pawn) and item != self:
item.setZValue(2)
self.setZValue(3)
def mouseMoveEvent(self, event):
movePos = self.mapToScene(event.pos())
self.setPos(movePos.x() - 15, movePos.y() - 15)
def mouseReleaseEvent(self, event):
dropPos = self.mapToScene(event.pos())
dropCase = None
for item in self.scene().items(dropPos.x(), dropPos.y(), 0.0001, 0.0001,
Qt.IntersectsItemShape,
Qt.AscendingOrder):
if isinstance(item, Case):
dropCase = item
if dropCase:
newP = dropCase.scenePos()
self.setPos(newP.x()+5, newP.y()+5)
self.destinationPos = QPointF(newP.x()+5, newP.y()+5)
"""here i want to send the signal that the item changed position
from self.sourcePos to self.destinationPos"""
self.sourcePos = self.destinationPos
else:
self.setPos(self.sourcePos)
I'm adding a color parameter to the LineBand subclass of QWidget. I've found several examples of how to add additional parameters to a subclass in Python 3 and believe I've followed the advice. Yet, when I call the new version of the class using box = LineBand(self.widget2, color), I get the error File "C:/Users/...", line 63, in showBoxes ... box = LineBand(viewport, color) ... TypeError: __init__() takes 2 positional arguments but 3 were given. But, I'm only calling LineBand with 2 arguments, right? Below is the complete code. I've commented all the sections I've changed. I've also commented out the code that changes the background color of the text in order to see the colored lines more clearly (when they actually are drawn). The background color code works fine.
import sys
from PySide.QtCore import *
from PySide.QtGui import *
db = ((5,8,'A',Qt.darkMagenta),(20,35,'B',Qt.darkYellow),(45,60,'C',Qt.darkCyan)) # added color to db
class TextEditor(QTextEdit):
def __init__(self, parent=None):
super().__init__(parent)
text="This is example text that is several lines\nlong and also\nstrangely broken up and can be\nwrapped."
self.setText(text)
cursor = self.textCursor()
for n in range(0,len(db)):
row = db[n]
startChar = row[0]
endChar = row[1]
id = row[2]
color = row[3] # assign color from db to variable
cursor.setPosition(startChar)
cursor.movePosition(QTextCursor.NextCharacter, QTextCursor.KeepAnchor, endChar-startChar)
#charfmt = cursor.charFormat()
#charfmt.setBackground(QColor(color)) # assign color to highlight (background)
#cursor.setCharFormat(charfmt)
cursor.clearSelection()
self.setTextCursor(cursor)
def getBoundingRect(self, start, end):
cursor = self.textCursor()
cursor.setPosition(end)
last_rect = end_rect = self.cursorRect(cursor)
cursor.setPosition(start)
first_rect = start_rect = self.cursorRect(cursor)
if start_rect.y() != end_rect.y():
cursor.movePosition(QTextCursor.StartOfLine)
first_rect = last_rect = self.cursorRect(cursor)
while True:
cursor.movePosition(QTextCursor.EndOfLine)
rect = self.cursorRect(cursor)
if rect.y() < end_rect.y() and rect.x() > last_rect.x():
last_rect = rect
moved = cursor.movePosition(QTextCursor.NextCharacter)
if not moved or rect.y() > end_rect.y():
break
last_rect = last_rect.united(end_rect)
return first_rect.united(last_rect)
class Window(QWidget):
def __init__(self):
super(Window, self).__init__()
self.edit = TextEditor(self)
layout = QVBoxLayout(self)
layout.addWidget(self.edit)
self.boxes = []
def showBoxes(self):
while self.boxes:
self.boxes.pop().deleteLater()
viewport = self.edit.viewport()
for start, end, id, color in db: # get color too
rect = self.edit.getBoundingRect(start, end)
box = LineBand(viewport, color) # call LineBand with color as argument
box.setGeometry(rect)
box.show()
self.boxes.append(box)
def resizeEvent(self, event):
self.showBoxes()
super().resizeEvent(event)
class LineBand(QWidget):
def __init__(self, color): # define color within __init__
super().__init__(self)
self.color = color
def paintEvent(self, event):
painter = QPainter(self)
painter.setRenderHint(QPainter.Antialiasing)
painter.setPen(QPen(color, 1.8)) # call setPen with color
painter.drawLine(self.rect().topLeft(), self.rect().bottomRight())
if __name__ == '__main__':
app = QApplication(sys.argv)
window = Window()
window.show()
window.showBoxes()
app.exec_()
sys.exit(app.exec_())
When a method is not overwritten it will be the same as the implemented method of the parent so if you want it to work you must add those parameters, since these depend many times on the parent a simple way is to use *args and **kwargs and pass the new parameter as the first parameter. In addition you must use self.color instead of color since color only exists in the constructor.
class Window(QWidget):
[...]
def showBoxes(self):
while self.boxes:
self.boxes.pop().deleteLater()
viewport = self.edit.viewport()
for start, end, id, color in db: # get color too
rect = self.edit.getBoundingRect(start, end)
box = LineBand(color, viewport) # call LineBand with color as argument
box.setGeometry(rect)
box.show()
self.boxes.append(box)
[...]
class LineBand(QWidget):
def __init__(self, color, *args, **kwargs):
QWidget.__init__(self, *args, **kwargs)
self.color = color
def paintEvent(self, event):
painter = QPainter(self)
painter.setRenderHint(QPainter.Antialiasing)
painter.setPen(QPen(self.color, 1.8)) # call setPen with color
painter.drawLine(self.rect().topLeft(), self.rect().bottomRight())
Output:
I'm trying to make a custom text widget that is double buffered (In order to avoid flicker).
However, I'd like to be able to do a few things. Yet, I'm unsure of the exact methods I should use.
The first two are easy I simply want to change the background and foreground color.
So more or less I want to be able to change the text color for self.Text in self.Draw().
Snippet:
self.Text = mdc.DrawText(self.TextString, 10, 0)
As sell as the Background (fill) color for self.MemoryDC.
Next, does anyone know how I could center self.Text? Finally, how do I configure self.Text after it has been created?
The widget thus far:
class DynamicText (wx.Panel):
def __init__(self, par):
self.Par = par
wx.Panel.__init__(self, self.Par)
self.Time = Time(self, func=self.SetTime)
self.Dim = self.Par.GetClientSize()
self.SetSize(self.Dim)
self.Bind(wx.EVT_SIZE, self.Resize)
self.Bind(wx.EVT_ERASE_BACKGROUND, self.Erase)
self.Bind(wx.EVT_PAINT, self.Paint)
def Set (self, text) :
self.TextString = text
def SetTime (self, time) :
self.Set(str(time))
self.Resize(None)
def Resize(self, event):
self.Width, self.Height = self.GetSize()
bitmap = wx.EmptyBitmap(self.Width, self.Height)
self.MemoryDC = wx.MemoryDC(bitmap)
''' Redraws **self.MemoryDC** '''
mdc = self.MemoryDC
''' Deletes everything from widget. '''
mdc.Clear()
fs = 11
font = wx.Font( fs, wx.DEFAULT, wx.NORMAL, wx.NORMAL)
mdc.SetFont(font)
self.Draw()
self.Refresh()
def Draw (self) :
mdc = self.MemoryDC
self.Text = mdc.DrawText(self.TextString, 10, 0)
def Erase(self, event):
''' Does nothing, as to avoid flicker. '''
pass
def Paint(self, event):
pdc = wx.PaintDC(self)
w, h = self.MemoryDC.GetSize()
pdc.Blit(0, 0, w, h, self.MemoryDC, 0, 0)
I don't understand what you mean by configuring self.Text after it was created. If you want to change the text after you've drawn it - you can't. Once you've drawn it to the DC it's there, and the only way to change it would be to clear the DC and repaint it. In your case, it seems all you need to do when the text is updated is to call Resize() again, forcing a redraw. Note that DrawText() retruns nothing, so the value of your self.Text would be None. You definitely can't use that to refer to the drawn text. :D
As for the rest, here's an example of a Draw() method that centers the text and paints it blue:
def Draw(self) :
mdc = self.MemoryDC
dc_width, dc_height = mdc.GetSizeTuple()
text_width, text_height, descent, externalLeading = mdc.GetFullTextExtent(self.TextString)
x = (dc_width - text_width) / 2
y = (dc_height - text_height) / 2
mdc.SetTextForeground('Blue')
mdc.DrawText(self.TextString, x, y)