Custom QSizeGrip to resize a QListWidget
Asked Answered
M

4

5

I want to make a QListWidget with a resize handle at the bottom (similar to text fields you see on webpages like this one). I've seen a few people asking the same question out there but did not find a complete example.

I have some code which is nearly there, but it flickers during resize so I'm guessing there is something I am missing about resize policies or layouts or something...

Here's my "working" example. The theory is pretty simple, you just measure the distance of the mouse move in the widget's mousePressEvent and resize/reposition accordingly. Unfortunately I'm missing something basic and I don't know what:

from PyQt4 import QtGui
import sys

class Grip(QtGui.QLabel):
    def __init__(self, parent, move_widget):
        super(Grip, self).__init__(parent)
        self.move_widget = move_widget
        self.setText("+")
        self.min_height = 50

        self.mouse_start = None
        self.height_start = self.move_widget.height()
        self.resizing = False
        self.setMouseTracking(True)


    def showEvent(self, event):
        super(Grip, self).showEvent(event)
        self.reposition()

    def mousePressEvent(self, event):
        super(Grip, self).mousePressEvent(event)
        self.resizing = True
        self.height_start = self.move_widget.height()
        self.mouse_start = event.pos()

    def mouseMoveEvent(self, event):
        super(Grip, self).mouseMoveEvent(event)
        if self.resizing:
            delta = event.pos() - self.mouse_start
            height = self.height_start + delta.y()
            if height > self.min_height:
                self.move_widget.setFixedHeight(height)
            else:
                self.move_widget.setFixedHeight(self.min_height)

            self.reposition()

    def mouseReleaseEvent(self, event):
        super(Grip, self).mouseReleaseEvent(event)
        self.resizing = False

    def reposition(self):
        rect = self.move_widget.geometry()
        self.move(rect.right(), rect.bottom())


class Dialog(QtGui.QDialog):
    def __init__(self):
        super(Dialog, self).__init__()
        layout = QtGui.QVBoxLayout()
        self.setLayout(layout)
        list_widget = QtGui.QListWidget()
        layout.addWidget(list_widget)
        gripper = Grip(self, list_widget)

        layout.addWidget(QtGui.QLabel("Test"))

        self.setGeometry(200, 500, 200, 500)
Milson answered 24/9, 2018 at 20:23 Comment(0)
M
2

Ok turns out I was really damn close. I post an answer here for anyone else looking to solve a similar problem!

All I really needed to change from my original code was to reference the globalPos() instead of the local pos(). Thanks for the help, and S. Nick in particular for spotting that the move event was causing the issue.

from PyQt4 import QtGui
import sys

class Grip(QtGui.QLabel):
    def __init__(self, parent, move_widget):
        super(Grip, self).__init__(parent)
        self.move_widget = move_widget
        self.setText("+")
        self.min_height = 50

        self.mouse_start = None
        self.height_start = self.move_widget.height()
        self.resizing = False
        self.setMouseTracking(True)

        self.setCursor(QtCore.Q.SizeVerCursor)


    def showEvent(self, event):
        super(Grip, self).showEvent(event)
        self.reposition()

    def mousePressEvent(self, event):
        super(Grip, self).mousePressEvent(event)
        self.resizing = True
        self.height_start = self.move_widget.height()
        self.mouse_start = event.globalPos()

    def mouseMoveEvent(self, event):
        super(Grip, self).mouseMoveEvent(event)
        if self.resizing:
            delta = event.globalPos() - self.mouse_start
            height = self.height_start + delta.y()
            if height > self.min_height:
                self.move_widget.setFixedHeight(height)
            else:
                self.move_widget.setFixedHeight(self.min_height)

            self.reposition()

    def mouseReleaseEvent(self, event):
        super(Grip, self).mouseReleaseEvent(event)
        self.resizing = False

    def reposition(self):
        rect = self.move_widget.geometry()
        self.move(rect.right(), rect.bottom())


class Dialog(QtGui.QDialog):
    def __init__(self):
        super(Dialog, self).__init__()
        layout = QtGui.QVBoxLayout()
        self.setLayout(layout)
        list_widget = QtGui.QListWidget()
        layout.addWidget(list_widget)
        gripper = Grip(self, list_widget)

        layout.addWidget(QtGui.QLabel("Test"))

        self.setGeometry(200, 500, 200, 500)
Milson answered 25/9, 2018 at 18:21 Comment(0)
F
3

Try it:

def mouseMoveEvent(self, event):
    super(Grip, self).mouseMoveEvent(event)
    if self.resizing:
        delta = event.pos() - self.mouse_start
        height = self.height_start + delta.y()
        if height > self.min_height:
            self.move_widget.setFixedHeight(height)
        else:
            self.move_widget.setFixedHeight(self.min_height)

        #self.reposition()                                       # <-  ---

def mouseReleaseEvent(self, event):
    super(Grip, self).mouseReleaseEvent(event)
    self.resizing = False

    self.reposition()                                             # <- +++
Fiducial answered 25/9, 2018 at 1:5 Comment(2)
Interesting! So why is it that when the widget moves it causes flicker? This does work but it looks a bit ugly, it nearly solves my problem but I'd like the widget to follow the mouse. But it does give me a direction to look...Milson
Ok figured it out, as always the solution was pretty simple. See my answer for my final solution!Milson
P
3

Alternatively to the other solutions, you could use QSizeGrip the way it comes out of the box:

from PyQt4 import QtCore, QtGui
import sys

class Dialog(QtGui.QDialog):
    def __init__(self):
        super(Dialog, self).__init__()
        layoutMain = QtGui.QVBoxLayout(self)
        listWidget = QtGui.QListWidget(self)
        gripper = QtGui.QSizeGrip(listWidget)
        l = QtGui.QHBoxLayout(listWidget)

        l.setContentsMargins(0, 0, 0, 0)
        l.addWidget(gripper, 0, QtCore.Qt.AlignRight | QtCore.Qt.AlignBottom)

        layoutMain.addWidget(listWidget)
        layoutMain.addWidget(QtGui.QLabel("Test", self))

        self.setGeometry(200, 500, 200, 500)

if __name__ == '__main__':
    import sys
    app = QtGui.QApplication(sys.argv)
    w = Dialog()
    w.show()
    sys.exit(app.exec_())
Pincince answered 25/9, 2018 at 1:58 Comment(8)
This is perfect! I thought from the documentation that SizeGrips resize the main window, but this works as expected.Milson
Slightly beyond the scope of the question but I'll ask anyway: any idea how to constrain the sizegrip to vertical scaling only?Milson
@Spencer, Let me test a solution and I will get back to you. Meanwhile, please consider upvoting the answer if you like it. By the way, the other answers are also very good and deserve to be upvoted.Pincince
@Spencer, after self.setGeometry(200, 500, 200, 500) add self.setFixedWidth(200).Pincince
That's a simple way to do it. I'm hoping to put some more work into this later tonight and I'll post up what I can figure out. I'm thinking it should be easy enough to subclass the mouseMoveEvent of the sizegrip and limit it to vertical movements...Milson
And to answer your earlier point: eyllanesc has helped me through many a dark moment working with Qt in the past, but in this case his answer does not really address this specific question so I can't quite accept it as a solution. And in terms of Nick's answer I'm hoping he gets back to me on my comment before I up-vote him ;)Milson
@Spencer, I strongly believe, that both S. Nick and eyllanesc have put enough effort to provide a legit solutions to your problem and definitely deserve your up-vote. So, no matter which solution you decide to accept, vote them all up. As for the mouse event, let me spare your time. It is more complicated than you think. I have tried hard to achieve that the last night w/o any spark of success. The fixed width seems not only like the simpliest, but also the only solution.Pincince
Sure, I gave them the up-vote! And you're right on the mouseMoveEvent being more complicated than I thought, I tried diving in and got nowhere. However I did manage to solve the issue, I was just referencing the local space which obviously stutters when the widget tries to move itself. See my posted answer! Hopefully this helps others out, because I saw a few people with the same question online.Milson
S
2

I think that instead of implementing your own QSizeGrip you should use QSplitter:

from PyQt4 import QtCore, QtGui
import sys


class Dialog(QtGui.QDialog):
    def __init__(self):
        super(Dialog, self).__init__()
        layout = QtGui.QVBoxLayout(self)

        splitter = QtGui.QSplitter(QtCore.Qt.Vertical)
        layout.addWidget(splitter)
        list_widget = QtGui.QListWidget()
        splitter.addWidget(list_widget)
        splitter.addWidget(QtGui.QLabel("Test"))


if __name__ == '__main__':
    import sys
    app = QtGui.QApplication(sys.argv)
    w = Dialog()
    w.show()
    sys.exit(app.exec_())
Settera answered 25/9, 2018 at 1:7 Comment(1)
Thanks eyllanesc, but in this case I do not want a splitter. Though I definitely use them from time to time ;)Milson
M
2

Ok turns out I was really damn close. I post an answer here for anyone else looking to solve a similar problem!

All I really needed to change from my original code was to reference the globalPos() instead of the local pos(). Thanks for the help, and S. Nick in particular for spotting that the move event was causing the issue.

from PyQt4 import QtGui
import sys

class Grip(QtGui.QLabel):
    def __init__(self, parent, move_widget):
        super(Grip, self).__init__(parent)
        self.move_widget = move_widget
        self.setText("+")
        self.min_height = 50

        self.mouse_start = None
        self.height_start = self.move_widget.height()
        self.resizing = False
        self.setMouseTracking(True)

        self.setCursor(QtCore.Q.SizeVerCursor)


    def showEvent(self, event):
        super(Grip, self).showEvent(event)
        self.reposition()

    def mousePressEvent(self, event):
        super(Grip, self).mousePressEvent(event)
        self.resizing = True
        self.height_start = self.move_widget.height()
        self.mouse_start = event.globalPos()

    def mouseMoveEvent(self, event):
        super(Grip, self).mouseMoveEvent(event)
        if self.resizing:
            delta = event.globalPos() - self.mouse_start
            height = self.height_start + delta.y()
            if height > self.min_height:
                self.move_widget.setFixedHeight(height)
            else:
                self.move_widget.setFixedHeight(self.min_height)

            self.reposition()

    def mouseReleaseEvent(self, event):
        super(Grip, self).mouseReleaseEvent(event)
        self.resizing = False

    def reposition(self):
        rect = self.move_widget.geometry()
        self.move(rect.right(), rect.bottom())


class Dialog(QtGui.QDialog):
    def __init__(self):
        super(Dialog, self).__init__()
        layout = QtGui.QVBoxLayout()
        self.setLayout(layout)
        list_widget = QtGui.QListWidget()
        layout.addWidget(list_widget)
        gripper = Grip(self, list_widget)

        layout.addWidget(QtGui.QLabel("Test"))

        self.setGeometry(200, 500, 200, 500)
Milson answered 25/9, 2018 at 18:21 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.