在很多应用场景中,例如 QQ 好友列表,我们都需要展示大量分组数据,同时希望在滚动时分组标题始终固定显示在顶部,提升用户体验。本文将详细介绍如何利用 PyQt5 实现类似效果——在滚动区域中,当前分组标题始终显示在最上面,当滚动到下一个分组时,自动切换为新的标题。

在实际项目中,我们经常会遇到展示大量控件的情况,尤其是需要按照分组进行展示时。如果直接将所有控件堆叠在一个固定高度的窗口中,很容易导致信息密集、查找困难。为此,我们常常采用 QScrollArea 来实现内容的滚动展示。
需求主要包括:
为此,我们需要:
整个实现主要分为两部分:
QListWidget(展示分组中的项)组成。按钮具备展开/折叠功能,通过调整 QListWidget 的高度实现显示或隐藏分组内容。QScrollArea 内。利用滚动条的 valueChanged 信号捕捉滚动事件,计算各分组控件在 viewport 中的位置,并判断哪个分组处于顶部。当检测到顶部分组后,更新一个单独的固定按钮(吸顶控件)的文本显示当前分组标题。这种设计既能满足吸顶效果,又兼顾了分组数据的展开与折叠需求。
下面对关键代码进行详细说明:
FriendGroupWidgetpythonclass FriendGroupWidget(QWidget):
    def __init__(self, group_name, friends):
        super().__init__()
        self.group_name = group_name
        self.friends = friends
        self.expanded = False  # 默认折叠状态
        # 布局包含一个按钮和一个列表
        self.layout = QVBoxLayout(self)
        self.layout.setContentsMargins(0, 0, 0, 0)
        self.button = QPushButton(group_name)
        self.button.setCheckable(True)
        self.button.setChecked(False)
        self.button.clicked.connect(self.toggle_list)
        # 创建 QListWidget 存储好友,并关闭其滚动条
        self.list_widget = QListWidget()
        self.list_widget.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        for friend in friends:
            self.list_widget.addItem(friend)
        self.list_widget.setFixedHeight(0)
        self.layout.addWidget(self.button)
        self.layout.addWidget(self.list_widget)
        self.list_widget.setVerticalScrollMode(QtWidgets.QAbstractItemView.ScrollPerPixel)
    def toggle_list(self):
        if self.expanded:
            # 折叠:高度设为 0
            self.list_widget.setFixedHeight(0)
            self.expanded = False
        else:
            # 展开:计算 QListWidget 展开所需的总高度
            count = self.list_widget.count()
            if count > 0:
                rowHeight = self.list_widget.sizeHintForRow(0)
                totalHeight = rowHeight * count + 2 * self.list_widget.frameWidth()
            else:
                totalHeight = 0
            self.list_widget.setFixedHeight(totalHeight)
            self.expanded = True
说明:
QListWidget。按钮点击时调用 toggle_list 方法,控制列表的展开和折叠。QListWidget 的高度。MainWindowpythonclass MainWindow(QWidget, Ui_Form):
    def __init__(self):
        super().__init__()
        self.setupUi(self)
        self.setWindowTitle("QQ好友分组")
        self.resize(300, 500)
        self.layout = QVBoxLayout(self)
        # 用于存储所有好友分组的容器 widget
        # 模拟多个好友分组,每个分组包含多个好友
        groups = {
            "家人": [f'家人{i}' for i in range(15)],
            "朋友": [f'朋友{i}' for i in range(15)],
            "同学": [f'同学{i}' for i in range(15)],
            "陌生人": [f'陌生人{i}' for i in range(15)],
        }
        self.group_widgets = []
        self.group_widgets_dic = {}
        # 为每个分组创建一个 FriendGroupWidget,并加入主窗口布局中
        for group_name, friends in groups.items():
            group_widget = FriendGroupWidget(group_name, friends)
            self.group_widgets.append(group_widget)
            self.verticalLayout_2.addWidget(group_widget)
            self.group_widgets_dic[group_name] = group_widget
        self.verticalLayout_2.addStretch()
        self.scrollArea.verticalScrollBar().valueChanged.connect(self.handleScroll)
        self.button.setVisible(False)
        self.button.clicked.connect(self.click_handel)
说明:
QScrollArea 展示所有分组,通过遍历模拟数据构建多个 FriendGroupWidget。verticalScrollBar().valueChanged 信号监听滚动事件,在 handleScroll 方法中实时计算当前处于顶部的分组。handleScrollpythondef handleScroll(self, value):
    """滚动时检测当前顶部可见的分组,并打印分组名称"""
    viewport = self.scrollArea.viewport()
    top_visible_group = None
    top_offset = None
    for group_widget in self.group_widgets:
        # 将每个分组的坐标转换到 viewport 坐标系中
        pos_y = group_widget.list_widget.mapTo(viewport, QPoint(0, 0)).y()
        print(pos_y, group_widget.list_widget.height())
        if not group_widget.expanded:
            continue
        # 判断分组是否至少部分可见(即 bottom > 0)
        if pos_y < 10:
            # 如果分组的顶部在 viewport 之上或正好位于顶部,则选择离顶部最近的
            if pos_y <= 0:
                if top_offset is None or pos_y > top_offset:
                    top_offset = pos_y
                    top_visible_group = group_widget
            else:
                # 如果当前没有处于顶部的分组,则选择顶部位置最小的
                if top_offset is None or pos_y < top_offset:
                    top_offset = pos_y
                    top_visible_group = group_widget
    if top_visible_group:
        print("当前最上面可见的分组:", top_visible_group.group_name)
        self.button.setText(top_visible_group.group_name)
        self.button.setVisible(True)
    else:
        self.button.setVisible(False)
说明:
mapTo 方法将每个分组的坐标转换为 viewport 坐标系,计算各分组控件相对于滚动区域的位置。viewport 中的垂直坐标,判断哪个分组处于顶部,并将吸顶控件更新为当前分组标题。通过以上实现,我们利用 PyQt5 的控件与信号机制,实现了一个分组列表的滚动吸顶效果。具体优势在于:
这种基于事件监听与控件坐标映射的方法,不仅适用于 QQ 好友分组,还可以推广到其他需要吸顶效果的场景。希望本文的讲解能为你的 PyQt5 项目开发提供新的思路和灵感!
欢迎大家留言讨论,如有任何问题或改进建议,也可以在评论区交流。Happy coding!
python"""
## PyQt5实现分组列表滚动吸顶效果
> 有时候一个展示页面内的控件数量展示较多,固定高度下放置不了,那么自然就会把控件放置到QScrollArea中,可能滚动区域中放置的好几块分类的数据,那么在滚动的时候我想知道当前属于哪个分类,我就需要往上滚动,交互体验不佳。
### 需求:
1. 滚动到详细内容的时候,我希望分类标题是一直展示的(类似列表和列表头的效果)
2. 一个QScrollArea中能支持多个标题吸顶,也就是滚动到第二个标题的时候,第一个标题自动消失,开始第二个标题吸顶
### 设计分析:
1. 如果要实现吸顶效果,需要知道QScrollArea当前左上角坐标、待吸顶控件坐标
2. 需要能监听到滚动发生后的事件,用于更新吸顶控件坐标
3. 用label显示标题、QListWidget显示分组项
3、准备一个和分组标题一样的label2控件,判断当前所在的分组,将label2的内容显示为当前分组。不需要显示的时候label2隐藏,需要显示的时候label2显示
"""
import sys
from PyQt5 import QtWidgets
from PyQt5.QtCore import Qt, QPoint
from PyQt5.QtWidgets import QApplication, QWidget, QVBoxLayout, QPushButton, QListWidget, QScrollArea
from test import Ui_Form
class FriendGroupWidget(QWidget):
    def __init__(self, group_name, friends):
        super().__init__()
        self.group_name = group_name
        self.friends = friends
        self.expanded = False  # 默认折叠状态
        # 布局包含一个按钮和一个列表
        self.layout = QVBoxLayout(self)
        self.layout.setContentsMargins(0, 0, 0, 0)
        self.button = QPushButton(group_name)
        self.button.setCheckable(True)
        self.button.setChecked(False)
        self.button.clicked.connect(self.toggle_list)
        # 创建 QListWidget 存储好友,并关闭其滚动条
        self.list_widget = QListWidget()
        self.list_widget.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        for friend in friends:
            self.list_widget.addItem(friend)
        self.list_widget.setFixedHeight(0)
        self.layout.addWidget(self.button)
        self.layout.addWidget(self.list_widget)
        self.list_widget.setVerticalScrollMode(QtWidgets.QAbstractItemView.ScrollPerPixel)
    def toggle_list(self):
        if self.expanded:
            # 折叠:高度设为 0
            self.list_widget.setFixedHeight(0)
            self.expanded = False
        else:
            # 展开:计算 QListWidget 展开所需的总高度
            count = self.list_widget.count()
            if count > 0:
                rowHeight = self.list_widget.sizeHintForRow(0)
                totalHeight = rowHeight * count + 2 * self.list_widget.frameWidth()
            else:
                totalHeight = 0
            self.list_widget.setFixedHeight(totalHeight)
            self.expanded = True
class MainWindow(QWidget, Ui_Form):
    def __init__(self):
        super().__init__()
        self.setupUi(self)
        self.setWindowTitle("QQ好友分组")
        self.resize(300, 500)
        self.layout = QVBoxLayout(self)
        # 用于存储所有好友分组的容器 widget
        # 模拟多个好友分组,每个分组包含多个好友
        groups = {
            "家人": [f'家人{i}' for i in range(15)],
            "朋友": [f'朋友{i}' for i in range(15)],
            "同学": [f'同学{i}' for i in range(15)],
            "陌生人": [f'陌生人{i}' for i in range(15)],
        }
        self.group_widgets = []
        self.group_widgets_dic = {}
        # 为每个分组创建一个 FriendGroupWidget,并加入主窗口布局中
        for group_name, friends in groups.items():
            group_widget = FriendGroupWidget(group_name, friends)
            self.group_widgets.append(group_widget)
            self.verticalLayout_2.addWidget(group_widget)
            self.group_widgets_dic[group_name] = group_widget
        self.verticalLayout_2.addStretch()
        self.scrollArea.verticalScrollBar().valueChanged.connect(self.handleScroll)
        self.button.setVisible(False)
        self.button.clicked.connect(self.click_handel)
    def handleScroll(self, value):
        """滚动时检测当前顶部可见的分组,并打印分组名称"""
        viewport = self.scrollArea.viewport()
        top_visible_group = None
        top_offset = None
        for group_widget in self.group_widgets:
            # 将每个分组的坐标转换到 viewport 坐标系中
            pos_y = group_widget.list_widget.mapTo(viewport, QPoint(0, 0)).y()
            print(pos_y, group_widget.list_widget.height())
            if not group_widget.expanded:
                continue
            # 判断分组是否至少部分可见(即 bottom > 0)
            if pos_y<10:
                # 如果分组的顶部在 viewport 之上或正好位于顶部,则选择离顶部最近的
                if pos_y <= 0:
                    if top_offset is None or pos_y > top_offset:
                        top_offset = pos_y
                        top_visible_group = group_widget
                else:
                    # 如果当前没有处于顶部的分组,则选择顶部位置最小的
                    if top_offset is None or pos_y < top_offset:
                        top_offset = pos_y
                        top_visible_group = group_widget
        if top_visible_group:
            print("当前最上面可见的分组:", top_visible_group.group_name)
            self.button.setText(top_visible_group.group_name)
            self.button.setVisible(True)
        else:
            self.button.setVisible(False)
    def click_handel(self):
        group_name = self.button.text()
        self.group_widgets_dic[group_name].toggle_list()
if __name__ == '__main__':
    app = QApplication(sys.argv)
    window = MainWindow()
    window.show()
    sys.exit(app.exec_())
本文作者:司小远
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!