PyQt - 使用 QListWidget 自定义滚动

PyQt - Custom scrolling with QListWidget

我正在尝试找出一种方法来自定义 QListWidget 的滚动条,使滚动条位于 QListWidget 的上方和下方,而不是正常的垂直和水平滚动条。

如果您不明白我的意思,请查看我下面的示例。
在下面的示例中,我使用 QPushButtonsQTimers 控制滚动来代替滚动条,但我正在寻找的是启用菜单滚动时像 QMenu 中那样的滚动条。

如果这不是一个选项,我想知道是否有滚动条信号或我可以尝试使用的东西来了解滚动条何时正常激活?这样我就可以根据需要 show/hide 按钮。谢谢。

import sys
from PyQt5.QtCore import pyqtSignal, QTimer, Qt
from PyQt5.QtGui import QPainter
from PyQt5.QtWidgets import QWidget, QPushButton, QVBoxLayout, \
    QApplication, QStyle, QListWidget, QStyleOptionButton, QListWidgetItem

class UpBtn(QPushButton):
    mouseHover = pyqtSignal()
    def __init__(self):
        QPushButton.__init__(self)
        self.setMouseTracking(True)
        self.timer = QTimer()

    def paintEvent(self, event):
        painter = QPainter()
        painter.begin(self)
        opt = QStyleOptionButton()
        self.initStyleOption(opt)
        self.style().drawControl(QStyle.CE_ScrollBarSubLine, opt, painter, self)
        painter.end()

    def startScroll(self):
        self.mouseHover.emit()

    def enterEvent(self, event):
        self.timer.timeout.connect(self.startScroll)
        self.timer.start(120)

    def leaveEvent(self, event):
        self.timer.stop()

class DwnBtn(QPushButton):
    mouseHover = pyqtSignal()
    def __init__(self):
        QPushButton.__init__(self)
        self.setMouseTracking(True)
        self.timer = QTimer()

    def paintEvent(self, event):
        painter = QPainter()
        painter.begin(self)
        opt = QStyleOptionButton()
        self.initStyleOption(opt)
        self.style().drawControl(QStyle.CE_ScrollBarAddLine, opt, painter, self)
        painter.end()

    def startScroll(self):
        self.mouseHover.emit()

    def enterEvent(self, event):
        self.timer.timeout.connect(self.startScroll)
        self.timer.start(120)

    def leaveEvent(self, event):
        self.timer.stop()

class Window(QWidget):
    def __init__(self):
        super(Window, self).__init__()
        self.layout = QVBoxLayout()
        self.layout.setContentsMargins(0, 0, 0, 0)
        self.layout.setSpacing(0)
        self.upBtn = UpBtn()
        self.upBtn.setFixedWidth(230)
        self.layout.addWidget(self.upBtn)

        self.listWidget = QListWidget()
        self.listWidget.setFixedWidth(230)
        self.listWidget.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        self.listWidget.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        self.layout.addWidget(self.listWidget)

        self.downBtn = DwnBtn()
        self.downBtn.setFixedWidth(230)
        self.layout.addWidget(self.downBtn)

        self.setLayout(self.layout)
        self.upBtn.clicked.connect(self.upBtnClicked)
        self.upBtn.mouseHover.connect(self.upBtnClicked)
        self.downBtn.clicked.connect(self.downBtnClicked)
        self.downBtn.mouseHover.connect(self.downBtnClicked)

        for i in range(100):
            item = QListWidgetItem()
            item.setText("list item " + str(i))
            self.listWidget.addItem(item)

    def upBtnClicked(self):
        cur = self.listWidget.currentRow()
        self.listWidget.setCurrentRow(cur - 1)

    def downBtnClicked(self):
        cur = self.listWidget.currentRow()
        self.listWidget.setCurrentRow(cur + 1)

if __name__ == '__main__':
    app = QApplication(sys.argv)
    window = Window()
    window.show()
    sys.exit(app.exec_())

编辑: 这是我正在谈论的示例图像。这是一个可滚动的 QMenu.

编辑:
可滚动 QMenu 代码。
取消注释注释部分以获得图像中的固定大小。通常 Qmenu 滚动仅在菜单项超过屏幕高度时才起作用。我只是在寻找顶部和底部悬停样式滚动,但要在 QListWidget.

中使用
import sys
from PyQt5.QtCore import QPoint, QEvent
from PyQt5.QtWidgets import QWidget, QPushButton, QVBoxLayout, \
    QApplication, QAction, QMenu, QProxyStyle, QStyle

class MyMenu(QMenu):
    def event(self, event):
        if event.type() == QEvent.Show:
            self.move(self.parent().mapToGlobal(QPoint(-108, 0)))
        return super(MyMenu, self).event(event)

# class CustomStyle(QProxyStyle):
#     def pixelMetric(self, QStyle_PixelMetric, option=None, widget=None):
#         if QStyle_PixelMetric == QStyle.PM_MenuScrollerHeight:
#             return 15
#         if QStyle_PixelMetric == QStyle.PM_MenuDesktopFrameWidth:
#             return 290
#         else:
#             return QProxyStyle.pixelMetric(self, QStyle_PixelMetric, option, widget)

class MainWindow(QWidget):
    def __init__(self, parent=None):
        super(MainWindow, self).__init__(parent)
        self.layout = QVBoxLayout()
        self.btn = QPushButton("Button")
        self.btn.setFixedHeight(30)
        self.btn.setFixedWidth(100)
        self.myMenu = MyMenu("Menu", self.btn)
        self.btn.setMenu(self.myMenu)
        self.layout.addWidget(self.btn)
        self.setLayout(self.layout)
        menus = []
        for _ in range(5):
            myMenus = QMenu("Menu"+str(_+1), self.btn)
            # myMenus.setFixedHeight(120)
            myMenus.setStyleSheet("QMenu{menu-scrollable: 1; }")
            menus.append(myMenus)
        for i in menus:
            self.btn.menu().addMenu(i)
            for item in range(100):
                action = QAction("item" + str(item), self)
                i.addAction(action)

if __name__ == '__main__':
    app = QApplication(sys.argv)
    # app.setStyle(CustomStyle())
    w = MainWindow()
    w.show()
    app.exec_()

思路是获取决定按钮是否隐藏的上下元素的行,为此我们使用itemAt()方法returns给定几何坐标的item .另一方面,我改进了每次他们更改 QListView 中的项目数时必须进行的计算,因为我们使用内部模型的信号。

import sys
from PyQt5 import QtCore, QtGui, QtWidgets


class Button(QtWidgets.QPushButton):
    moveSignal = QtCore.pyqtSignal()
    def __init__(self, *args, **kwargs):
        super(Button, self).__init__(*args, **kwargs)
        self.m_timer = QtCore.QTimer(self, interval=120)
        self.m_timer.timeout.connect(self.moveSignal)
        self.setMouseTracking(True)
        self.setFixedHeight(20)

    def mouseReleaseEvent(self, e):
        super(Button, self).mousePressEvent(e)
        self.setDown(True)

    def enterEvent(self, e):
        self.setDown(True)
        self.m_timer.start()
        super(Button, self).enterEvent(e)

    def leaveEvent(self, e):
        self.setDown(False)
        self.m_timer.stop()
        super(Button, self).leaveEvent(e)


class Window(QtWidgets.QWidget):
    def __init__(self):
        super(Window, self).__init__()
        self.setFixedWidth(230)

        icon = self.style().standardIcon(QtWidgets.QStyle.SP_ArrowUp)
        self.upBtn = Button(icon=icon)
        self.upBtn.moveSignal.connect(self.moveUp)
        icon = self.style().standardIcon(QtWidgets.QStyle.SP_ArrowDown)
        self.downBtn = Button(icon=icon)
        self.downBtn.moveSignal.connect(self.moveDown)

        self.listWidget = QtWidgets.QListWidget()
        self.listWidget.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
        self.listWidget.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)

        layout = QtWidgets.QVBoxLayout(self)
        layout.setContentsMargins(0, 0, 0, 0)
        layout.setSpacing(0)
        layout.addWidget(self.upBtn)
        layout.addWidget(self.listWidget)
        layout.addWidget(self.downBtn)
        self.adjust_buttons()
        self.create_connections()

    def create_connections(self):
        self.listWidget.currentItemChanged.connect(self.adjust_buttons)
        model = self.listWidget.model()
        model.rowsInserted.connect(self.adjust_buttons)
        model.rowsRemoved.connect(self.adjust_buttons)
        model.rowsMoved.connect(self.adjust_buttons)
        model.modelReset.connect(self.adjust_buttons)
        model.layoutChanged.connect(self.adjust_buttons)

    @QtCore.pyqtSlot()
    def adjust_buttons(self):
        first = self.listWidget.itemAt(QtCore.QPoint())
        r = self.listWidget.row(first)
        self.upBtn.setVisible(r != 0 and  r!= -1)
        last = self.listWidget.itemAt(self.listWidget.viewport().rect().bottomRight())
        r = self.listWidget.row(last)
        self.downBtn.setVisible( r != (self.listWidget.count() -1) and r != -1)

    @QtCore.pyqtSlot()
    def moveUp(self):
        ix = self.listWidget.moveCursor(QtWidgets.QAbstractItemView.MoveUp, QtCore.Qt.NoModifier)
        self.listWidget.setCurrentIndex(ix)

    @QtCore.pyqtSlot()
    def moveDown(self):
        ix = self.listWidget.moveCursor(QtWidgets.QAbstractItemView.MoveDown, QtCore.Qt.NoModifier)
        self.listWidget.setCurrentIndex(ix)

    @QtCore.pyqtSlot(str)
    def add_item(self, text):
        item = QtWidgets.QListWidgetItem(text)
        self.listWidget.addItem(item)


if __name__ == '__main__':
    app = QtWidgets.QApplication(sys.argv)
    window = Window()
    for i in range(100):
        window.add_item("item {}".format(i))
    window.show()
    sys.exit(app.exec_())