0

This picture is a pretty good representation of what I'm trying to emulate.

The goal is to create items or widgets, looking like the example above, that a user could create on a QSlider by a MouseDoubleClicked event, and which would remain at the Tick position it was originally created (it would remain immobile). I've already made a few attempts using either QLabels with Pixmaps or a combination of QGraphicsItems and QGraphicsView, in vain.

Still, I have the feeling that I'm most likely over complicating things, and that there might be a simpler way to achieve that.

What would be your approach to make those "markers"?

EDIT: I've tried my best to edit one of my previous attempts, in order to make it a Minimal Reproducible Example. Might still be too long though, but here it goes.

import random

from PySide2 import QtCore, QtGui, QtWidgets


class Marker(QtWidgets.QLabel):
    def __init__(self, parent=None):
        super(Marker, self).__init__(parent)
        self._slider = None
        self.setAcceptDrops(True)
        pix = QtGui.QPixmap(30, 30)
        pix.fill(QtGui.QColor("transparent"))
        paint = QtGui.QPainter(pix)
        slider_color = QtGui.QColor(random.randint(130, 180), random.randint(130, 180), random.randint(130, 180))
        handle_pen = QtGui.QPen(QtGui.QColor(slider_color.darker(200)))
        handle_pen.setWidth(3)
        paint.setPen(handle_pen)
        paint.setBrush(QtGui.QBrush(slider_color, QtCore.Qt.SolidPattern))
        points = QtGui.QPolygon([
            QtCore.QPoint(5, 5),
            QtCore.QPoint(5, 19),
            QtCore.QPoint(13, 27),
            QtCore.QPoint(21, 19),
            QtCore.QPoint(21, 5),

        ])
        paint.drawPolygon(points)
        del paint
        self.setPixmap(pix)


class myTimeline(QtWidgets.QWidget):
    def __init__(self, parent=None):
        super(myTimeline, self).__init__(parent)
        layout = QtWidgets.QGridLayout(self)
        self.slider = QtWidgets.QSlider(QtCore.Qt.Horizontal)
        self.slider.setMinimum(0)
        self.slider.setMaximum(50)
        self.slider.setTickPosition(QtWidgets.QSlider.TicksAbove)
        self.slider.setTickInterval(1)
        self.slider.setSingleStep(1)
        self.slider.setAcceptDrops(True)
        self.resize(self.width(), 50)
        layout.addWidget(self.slider)

    def create_marker(self):
        bookmark = Marker(self)
        opt = QtWidgets.QStyleOptionSlider()
        self.slider.initStyleOption(opt)
        rect = self.slider.style().subControlRect(
            QtWidgets.QStyle.CC_Slider,
            opt,
            QtWidgets.QStyle.SC_SliderHandle,
            self.slider
        )
        bookmark.move(rect.center().x(), 0)
        bookmark.show()

    def mouseDoubleClickEvent(self, event):
        self.create_marker()

hugodurmer
  • 11
  • 3
  • 2
    Can you provide a [minimal, reproducible example](https://stackoverflow.com/help/minimal-reproducible-example) of what you've got so far? Possibly with the code of the thing that is closer to what you want to achieve (even if it doesn't work as expected). – musicamante Dec 27 '20 at 19:41
  • Thanks for the comment, but that's actually the point of my question here: I can't. While I could certainly put one of my previous attempts here, it definitely wouldn't be "minimal", hence this question. On top of that, even if I did, it would probably go against Stack Overflow's rules, and I'd like to avoid problems with moderators here. – hugodurmer Dec 27 '20 at 22:19
  • 1
    I understand your concerns, but QSliders are not easy to map, and without an even minimal MRE it's a bit hard to provide an answer based on a "guess". We could try to give an answer, but it would probably be very generic, and it might not be a proper solution (and answer). Don't worry about possible "problems" with moderators, try to get an example as minimal as possible to start with, if it has problems they will be notified by you. – musicamante Dec 28 '20 at 03:51
  • Thanks again for your comment. I ended up taking the plunge and I just edited my question to add what I think would the closest thing to a Minimal Reproducible Example. – hugodurmer Dec 28 '20 at 18:51
  • Don't worry, your code is fine and is not too long. From what I can see, you almost got it working, except from a small issue with the QLabel geometry and the fact that it makes the slider almost unusable if the handle is near the marker. Besides that (and the marker position slightly off with certain styles), I cannot see any other issues: what is wrong with your implementation? – musicamante Dec 28 '20 at 19:09
  • Well I'm probably making a lot of fuss for nothing, but I thought it might be too convoluted and was trying to seek for easier ways to produce this kind of results. Also, I didn't mention this, but ultimately I'd like to have an option for the user to zoom into the slider horizontally. So far I've found that what worked best for me was to link a WheelEvent to a function updating the range, but then I would have to reupdate the position through the QStyle, hence why that looked too complicated to my taste. – hugodurmer Dec 28 '20 at 19:32
  • As said, mapping the actual handle position is not immediate with the default QSlider, as the styles add some "virtual" margins and the position is not always accurate (for instance, certain styles do *not* map the mouse cursor position correctly, especially if the slider is relatively small and the range is big); so, your solution is actually correct. Also, if you want to add that "zoom" feature, I strongly suggest you to use a modifier (ctrl or alt), otherwise the control would become unintuitive. – musicamante Dec 28 '20 at 23:16

1 Answers1

0

Your approach is indeed correct, it only has a few issues:

  1. The geometry of the marker should be updated to reflect its contents. This is required as QLabel is a very special type of widget, and usually adapts its size only when added to a layout or is a top level window.

  2. The marker pixmap is not correctly aligned (it's slightly on the left of its center).

  3. The marker positioning should not only use the rect center, but also the marker width and the slider position (since you are adding the slider to the parent and there's a layout, the slider is actually off by the size of the layout's contents margins).

  4. The markers should be repositioned everytime the widget is resized, so they should keep a reference to their value.

  5. Markers should be transparent for mouse events, otherwise they would block mouse control on the slider.

Given the above, I suggest you the following:

class Marker(QtWidgets.QLabel):
    def __init__(self, value, parent=None):
        super(Marker, self).__init__(parent)
        self.value = value
        # ...
        # correctly centered polygon
        points = QtGui.QPolygon([
            QtCore.QPoint(7, 5),
            QtCore.QPoint(7, 19),
            QtCore.QPoint(15, 27),
            QtCore.QPoint(22, 19),
            QtCore.QPoint(22, 5),

        ])
        paint.drawPolygon(points)
        del paint
        self.setPixmap(pix)

        # this is important!
        self.adjustSize()


class myTimeline(QtWidgets.QWidget):
    def __init__(self, parent=None):
        # ...
        self.markers = []

    def mouseDoubleClickEvent(self, event):
        self.create_marker()

    def create_marker(self):
        bookmark = Marker(self.slider.value(), self)
        bookmark.show()
        bookmark.installEventFilter(self)
        self.markers.append(bookmark)
        self.updateMarkers()

    def updateMarkers(self):
        opt = QtWidgets.QStyleOptionSlider()
        self.slider.initStyleOption(opt)
        for marker in self.markers:
            opt.sliderValue = opt.sliderPosition = marker.value
            rect = self.slider.style().subControlRect(
                QtWidgets.QStyle.CC_Slider,
                opt,
                QtWidgets.QStyle.SC_SliderHandle,
            )
            marker.move(rect.center().x() - marker.width() / 2 + self.slider.x(), 0)

    def eventFilter(self, source, event):
        if event.type() == QtCore.QEvent.MouseButtonPress:
            return True
        return super().eventFilter(source, event)

    def resizeEvent(self, event):
        super().resizeEvent(event)
        self.updateMarkers()
musicamante
  • 41,230
  • 6
  • 33
  • 58