一、为啥用PANDAS来存储数据
原始的内部模型是二维数组模型,具有以下特点:
- 最简单
- 无筛选/排序
- 无缓存
使用pandas就不一样了,作为一个成熟的库筛选、排序、切片都是他的拿手好戏,所以我们去实现以上功能借助pandas就可以简单便捷的完成以上功能了
二、怎么用PANDAS
我们要在__init__中定义_full_data
self._full_data: pd.DataFrame = pd.DataFrame() #正常状态下的总数据
然后在添加一个setDataFrame方法,并修改data方法就可以替换掉默认的二维数组显示方案
def setDataFrame(self, df:pd.DataFrame) -> None:
"""设置并更新模型的主数据框架"""
if not isinstance(df, pd.DataFrame):
raise TypeError("Expected a pandas DataFrame")
self._full_data = df.copy()
if df.empty:
self._full_data = pd.DataFrame(columns=col)
self.layoutChanged.emit() #告诉模型要刷新显示了
def data(self, index: QModelIndex, role: int = Qt.ItemDataRole.DisplayRole) -> Any:
"""核心数据获取方法,处理不同数据角色的展示逻辑"""
#验证index是否超出显示范围,显示数据是否为空
if not index.isValid() or self._full_data.empty:
return None
row = index.row()
col = index.column()
value = self._full_data.iloc[row , col]
if role == Qt.ItemDataRole.DisplayRole:
if not (self.checkbox_status and col == 0):
return value
return None
三、为什么要懒加载
单元格要渲染的内容不单单是DisplayRole,还有ForegroundRole(字体)、BackgroundRole(背景色)、TextAlignmentRole(对其方式),当数据量很大的时会消耗大量内存,造成界面卡死的情况,而且我们在日常操作时也不会同时去看上万的数据所以懒加载就很重要了,那怎么实现呢?
答案上面已经告诉你了,切片。但是怎么切还是有讲究的,pandas的切片有很多方法loc,iloc,at,不同的方案占用内存的大小也不一样,我研究出来的最优方案是存储索引,这样我们就需要添加相关的初始化信息了。
self._display_start = 0
self._display_end = 0
self._chunk_size = 50 # 默认加载块大小
self._current_slice_index: Optional[pd.Index] = None # 当前显示的索引
添加完成后我们需要做两个函数1.局部加载 2.索引获取
def _current_index(self):
"获取当前数据的索引"
return self._full_data.index
def _load_data_chunk(self, start: int, end: int) -> None:
"""懒加载,通过变更_current_slice_index内容使data读取到指定的数据"""
current_index = self._current_index()
end = min(end, len(current_index))
start = max(0, start)
new_slice = current_index[start:end]
#若显示与当前相同则不做变动
if self._current_slice_index is not None and new_slice.equals(self._current_slice_index):
return
self.layoutAboutToBeChanged.emit()
self._current_slice_index = new_slice
self._display_start = start
self._display_end = end
self._update_headers()
self.layoutChanged.emit()
if len(new_slice) > 0:
top_left = self.index(0, 0)
bottom_right = self.index(len(new_slice)-1, self.columnCount()-1)
self.dataChanged.emit(top_left, bottom_right)
self.updateCount.emit(len(new_slice))
因为现在数据的起始位置出现了变化,所以data也需要做相应的修改
def data(self, index: QModelIndex, role: int = Qt.ItemDataRole.DisplayRole) -> Any:
"""核心数据获取方法,处理不同数据角色的展示逻辑"""
if not index.isValid() or self._full_data.empty:
return None
row = index.row()
col = index.column()
if row >= len(self._current_slice_index):
return None
real_row = self._display_start + row
value = self._full_data.iloc[real_row, col]
if role == Qt.ItemDataRole.DisplayRole:#其他正常显示
if not (self.checkbox_status and col == 0):
return value
return None
四、实现筛选功能
首先在init创建条件和索引
self.condition_dict: dict = {} #记录筛选条件,格式为{字段:条件}
self._filtered_index: Optional[pd.Index] = None # 筛选后的索引
添加筛选的功能,并修改_current_index就可以实现了
def _current_index(self):
"获取当前数据的索引,筛选 > 完整"
if self.filter_flag and self._filtered_index is not None:
return self._filtered_index
return self._full_data.index
def set_filter(self, condition_dict: dict, match_type: str = 'fuzzy'):
"""
基于数据框,根据条件字典筛选对应的数据
参数:
– condition_dict: 字典结构{搜索字段:条件},fuzzy条件为str,精准为list,bool为布尔列表搜索字段随意
– match_type: 匹配类型,'fuzzy' 、 'precise' 或 'bool'
"""
if self._full_data.empty:
return
self.condition_dict = condition_dict
df = self._full_data
mask = pd.Series(True, index=df.index)
for column, values in condition_dict.items():
if column not in self._full_data.columns:
continue
col_data = df[column]
# 模糊匹配
if match_type == 'fuzzy':
if not isinstance(values, str):
raise ValueError(f"模糊匹配的筛选条件必须为str")
keywords = values.split()
cond = pd.Series(False, index=df.index)
for kw in keywords:
cond |= col_data.astype(str).str.contains(kw, na=False)
# 精准匹配
elif match_type == 'precise':
if not isinstance(values, list):
raise ValueError(f"精准匹配的筛选条件必须为 list ")
cond = col_data.isin(values)
# 布尔列表筛选
elif match_type == 'bool':
if not isinstance(values, list):
raise ValueError(f"布尔匹配的筛选内容必须为list ")
if len(values) != len(mask):
raise ValueError(f"布尔匹配筛选列表长度需要与数据匹配")
if not all(isinstance(v, bool) for v in values):
raise TypeError("布尔匹配的筛选内容必须都是布尔类型")
cond = pd.Series(values, index=df.index)
else:
raise ValueError("未知匹配类型")
mask &= cond
self.filter_flag = True
self._filtered_index = df.index[mask]
self.total_recrod_count_change.emit(len(self._filtered_index))
self._display_start = 0
self._display_end = min(self._chunk_size, len(self._filtered_index))
self._load_data_chunk(self._display_start, self._display_end)
def clear_filter(self) -> None:
"""清除筛选状态,显示数据的初始状态"""
self.filter_flag = False
self._filtered_index = None
self.condition_dict = {}
self.total_recrod_count_change.emit(len(self._full_data))
self._display_start = min(self._display_start, len(self._full_data))
self._display_end = min(self._chunk_size,self._full_data.shape[0])
self._load_data_chunk(self._display_start, self._display_end)
五、实现排序功能
实现排序功能也是一个道理的,初始化_sorted_index,修改_current_index这个位置请自行添加我就不占用篇幅了
self._sorted_index: Optional[pd.Index] = None
def sort_data(self,by,ascending=True):
"排序"
current_index = self._current_index()
df = self._full_data.loc[current_index]
sorted_df = df.sort_values(by=by,ascending=ascending,kind="mergesort") #稳定排序
self._sorted_index = sorted_df.index
self._load_data_chunk(self._display_start,self._display_end)
def clear_sort(self):
"清除排序"
self._sorted_index = None
self._load_data_chunk(self._display_start,self._display_end)
总结
做完以上操作一个基础的展示模型就完成了,下一个篇章我们来将这个基础模型应用到TableView,并做一些简单的优化


