基于 PyQt5 实现分组列表滚动吸顶效果

news/2025/2/24 4:53:12/

基于 PyQt5 实现分组列表滚动吸顶效果

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

示例效果


一、背景与需求

在实际项目中,我们经常会遇到展示大量控件的情况,尤其是需要按照分组进行展示时。如果直接将所有控件堆叠在一个固定高度的窗口中,很容易导致信息密集、查找困难。为此,我们常常采用 QScrollArea 来实现内容的滚动展示。

需求主要包括:

  1. 标题吸顶效果
    当用户向下滚动页面时,分组标题始终固定在窗口上方,让用户清楚当前所在分组。
  2. 分组切换交互
    当滚动到下一个分组时,当前分组的标题自动消失,替换为新分组的标题。

为此,我们需要:

  • 获取当前滚动区域的左上角坐标;
  • 监听滚动事件,动态判断各个分组的可见情况;
  • 根据可见性更新吸顶标题的内容和显示状态。

二、设计思路

整个实现主要分为两部分:

  1. 分组控件设计
    每个分组由一个按钮(显示分组标题)和一个 QListWidget(展示分组中的项)组成。按钮具备展开/折叠功能,通过调整 QListWidget 的高度实现显示或隐藏分组内容。
  2. 吸顶逻辑处理
    主窗口中,所有分组控件放置于一个 QScrollArea 内。利用滚动条的 valueChanged 信号捕捉滚动事件,计算各分组控件在 viewport 中的位置,并判断哪个分组处于顶部。当检测到顶部分组后,更新一个单独的固定按钮(吸顶控件)的文本显示当前分组标题。

这种设计既能满足吸顶效果,又兼顾了分组数据的展开与折叠需求。


三、代码解析

下面对关键代码进行详细说明:

1. 分组控件类 FriendGroupWidget

python">class FriendGroupWidget(QWidget):def __init__(self, group_name, friends):super().__init__()self.group_name = group_nameself.friends = friendsself.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:# 折叠:高度设为 0self.list_widget.setFixedHeight(0)self.expanded = Falseelse:# 展开:计算 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 = 0self.list_widget.setFixedHeight(totalHeight)self.expanded = True

说明:

  • 每个分组包含一个按钮和一个 QListWidget。按钮点击时调用 toggle_list 方法,控制列表的展开和折叠。
  • 为了更好的滚动体验,列表的垂直滚动条被关闭,通过计算列表项总高度来动态设置 QListWidget 的高度。

2. 主窗口类 MainWindow

python">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_widgetself.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 方法中实时计算当前处于顶部的分组。
  • 窗口中还存在一个独立的按钮控件(吸顶控件),用于在滚动时显示当前分组标题,点击该按钮可以切换对应分组的展开状态。

3. 滚动吸顶逻辑 handleScroll

python">def handleScroll(self, value):"""滚动时检测当前顶部可见的分组,并打印分组名称"""viewport = self.scrollArea.viewport()top_visible_group = Nonetop_offset = Nonefor 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_ytop_visible_group = group_widgetelse:# 如果当前没有处于顶部的分组,则选择顶部位置最小的if top_offset is None or pos_y < top_offset:top_offset = pos_ytop_visible_group = group_widgetif 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 sysfrom PyQt5 import QtWidgets
from PyQt5.QtCore import Qt, QPoint
from PyQt5.QtWidgets import QApplication, QWidget, QVBoxLayout, QPushButton, QListWidget, QScrollAreafrom test import Ui_Formclass FriendGroupWidget(QWidget):def __init__(self, group_name, friends):super().__init__()self.group_name = group_nameself.friends = friendsself.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:# 折叠:高度设为 0self.list_widget.setFixedHeight(0)self.expanded = Falseelse:# 展开:计算 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 = 0self.list_widget.setFixedHeight(totalHeight)self.expanded = Trueclass 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_widgetself.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 = Nonetop_offset = Nonefor 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_ytop_visible_group = group_widgetelse:# 如果当前没有处于顶部的分组,则选择顶部位置最小的if top_offset is None or pos_y < top_offset:top_offset = pos_ytop_visible_group = group_widgetif 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_())

http://www.ppmy.cn/news/1574550.html

相关文章

全局错误处理如何与Vue Router集成?

将全局错误处理与 Vue Router 集成可以确保在应用中处理错误的一致性&#xff0c;并在用户遇到未授权访问或其他错误时提供适当的反馈。以下是如何将全局错误处理与 Vue Router 集成的步骤和示例。 1. 设置全局错误处理 首先&#xff0c;您可以在 main.js 文件中设置全局错误…

【Bert】自然语言(Language Model)入门之---Bert

every blog every motto: Although the world is full of suffering&#xff0c; it is full also of the overcoming of it 0. 前言 对bert进行梳理 论文&#xff1a; BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding 时间&#xff1a;…

[Android]App生命周期

类似iOS的applicationWillEnterForeground:等方法 以下是使用 Application.ActivityLifecycleCallbacks 接口来监听应用启动和进入前台的示例代码。 创建一个自定义的 ActivityLifecycleCallbacks 首先&#xff0c;创建一个实现 Application.ActivityLifecycleCallbacks 的类…

前端面试之Flex布局:核心机制与高频考点全解析

目录 引言&#xff1a;弹性布局的降维打击 一、Flex布局的本质认知 1. 两大核心维度 2. 容器与项目的权力边界 二、容器属性深度剖析 1. 主轴控制三剑客 2. 交叉轴对齐黑科技 三、项目属性关键要点 1. flex复合属性解密 2. 项目排序魔法 四、六大高频面试场景 1. 经…

挑选出行数足够的excel文件

** 遍历文件夹下的所有excel文件&#xff0c;并将数据量超过指定标准的文件拷贝到指定文件夹中 import os.path import shutil import pandas as pddef copy_excel_files(source_folder, target_folder, row_threshold):if not os.path.exists(target_folder):os.makedirs(ta…

阅读《Vue.js设计与实现》 -- 01

菜鸟最近闲暇&#xff08;大的项目没开始&#xff0c;别的项目基本没事了&#xff09;&#xff0c;不知道干啥&#xff08;刷掘金刷多了&#xff0c;感觉文章都写得差不多&#xff0c;不知道学什么&#xff0c;原因如下&#xff1a;沸点&#xff09;&#xff0c;所以开始看《Vu…

3.Docker常用命令

1.Docker启动类命令 1.启动Docker systemctl start docker 2.停止Docker systemctl stop docker 3.重启Docker systemctl restart docker 4.查看Docker状态 systemctl status docker 5.设置开机自启(执行此命令后每次Linux重启后将自启动Docker) systemctl enable do…

Spring事务原理详解 三

通过《Spring事务原理 二》一文&#xff0c;我们知道了开启新事务的底层含义&#xff0c;就是从DataSource中获取一个connection&#xff0c;设置autocommit为false&#xff0c;并放到ThreadLocal中。 本文&#xff0c;我们继续来看事务的传播行为在源码中的实现&#xff1a; …