基于PANDAS的QAbstractTableModel实现高级TableView详细解析(六、基于PANDS的写入、交互控制,基于撤销栈的修改数据生成) 一、写入、交互原理之前我们有说过QAbstractTableModel的setData可以控制数值是否可以被写入flags控制单元格是否可以被交互、编辑在初始刷新视图的时候模型会跑data和flags这个时候cell的访问、编辑权限就确定好了我们常用的两种权限要实现显示多选框也是在这里实现这个我们后面讲Qt.ItemFlag.ItemIsEditable #编辑Qt.ItemFlag.ItemIsEnabled #交互实现输入的检查则需要在setData中设置检查函数若符合条件则可以写入setData(self, index: QModelIndex, value: Any, role: int Qt.ItemDataRole.EditRole) - bool:True - 可以写入False - 拒绝写入二、权限flags我们可以实现的访问限制类型有很多种比如控制那些列、单元格可以编辑从第一列开始可以编辑那些列可以交互但是不能编辑等等def flags(self, index: QModelIndex) - Qt.ItemFlag: 根据单元格位置设置交互标志 if not index.isValid(): return Qt.ItemFlag.NoItemFlags flag super().flags(index) row index.row() col index.column() if row len(self._current_slice_index): return Qt.ItemFlag.NoItemFlags #定义can_edit方法将你要限制的方法写进去 if self.permission.can_edit(self._current_slice_index[row], self._full_data.columns[col]): # 允许编辑 flag | Qt.ItemFlag.ItemIsEditable flag | Qt.ItemFlag.ItemIsEnabled # 确保启用交互 else: # 禁止编辑 flag ~Qt.ItemFlag.ItemIsEditable if col 0 and not self.checkbox_status:# 对于非复选框的首列禁用交互 flag ~Qt.ItemFlag.ItemIsEnabled return flag三、写入限制写入的限制也是多种多样比如控制类型只能写入数字、文本、时间或者只能写入指定范围内的值或者定义正则匹配只能写入固定格式的数据这个就是看你要实现什么样的要求了方法在setData中实现def setData(self, index: QModelIndex, value: Any, role: int Qt.ItemDataRole.EditRole) - bool: 处理数据编辑操作,位置自动切换至真实点位 if not index.isValid() or self._full_data.empty: return False row,col index.row(),index.column() if row len(self._current_slice_index): return False idx self._current_slice_index[row] real_row self._visible_row_map[row] old_value self._full_data.iat[real_row, col] col_name self._columns[col] try: if role Qt.ItemDataRole.EditRole: if not self.permission.can_edit(idx, col_name): return False #自定义验证函数即可 verification_value self.permission.validate(col_name, value) if verification_value[0] is False: return False if self._should_update_value(old_value, verification_value[1]): self.log_modification(edit, idx, col_name, old_value, verification_value[1]) self._full_data.iat[real_row, col] verification_value[1] self.dataChanged.emit(index, index) self.valueChanged.emit(row, col, verification_value[1]) return True return False except Exception as e: self.errorOccurred.emit(fsetData error: {str(e)}) return False return False四、验证方式分享def validate(self, col_name, value) - Tuple[bool, Any]: value pd.NA if value is None or not str(value).strip() else value # ---- edit limit ---- if not pd.isna(value) and col_name in self.edit_limit_dict: if value not in self.edit_limit_dict[col_name]: return (False, None) # ---- type limit ---- if not pd.isna(value) and col_name in self.type_limit_dict : limit_type self.type_limit_dict[col_name] validator_fun self.type_validators.get(limit_type) if validator_fun: result validator_fun(value) if result is False: return (False, None) elif result is not True: value result return (True, value) def _validate_number(self, value): try: f float(value) return int(f) if f.is_integer() else f except: return False def _validate_time(self, value): pattern r^([0-9]|[01]?\d|2[0-3])[:.]([0-5]\d|[0-9])$ m re.match(pattern, str(value)) if not m: return False return f{m.group(1).zfill(2)}:{m.group(2).zfill(2)} def _validate_time_to_now_hm(self, value): from datetime import datetime import re pattern r^([0-9]|[01]?\d|2[0-3])[:.]([0-5]\d|[0-9])$ m re.match(pattern, str(value)) if not m: return False h m.group(1).zfill(2) m_ m.group(2).zfill(2) return f{datetime.now().strftime(%Y-%m-%d)} {h}:{m_} def _validate_date_yearmonth(self, value): import re pattern r^(20\d\d)-(0[1-9]|1[0-2]|[1-9])$ m re.match(pattern, str(value)) if not m: return False return f{m.group(1)}-{m.group(2).zfill(2)} def _validate_date(self, value): from datetime import datetime import re pattern r^(20\d\d)-(0[1-9]|1[0-2]|[1-9])-(0[1-9]|[12][0-9]|3[01]|[1-9])$ m re.match(pattern, str(value)) if not m: return False try: d f{m.group(1)}-{m.group(2).zfill(2)}-{m.group(3).zfill(2)} datetime.strptime(d, %Y-%m-%d) return d except: return False五、基于撤销栈的修改数据生成这部分是上一篇没有介绍完成的数据生成我们用撤销栈存储的信息中包含很多的信息再次基础上我们可以直接生成一个SQL生成器SqlUpdateBuilder不过因为可能有重复编辑我们需要先做去重def collect_modified_cells(self,model) - Dict[tuple, Any]: 获取修改变动去重 modified {} for record in self.get_undo_stack(): if record.get(type) batch: for change in record[changes]: if change[column] model._checkbox_col_name: continue key (change[index], change[column]) modified[key] change[new] else: if record[column] model._checkbox_col_name: continue key (record[index], record[column]) modified[key] record[new] return modified然后在生成参数直接贴代码class SqlUpdateBuilder: 根据 撤销栈 生成参数字典,数值参为必填项其他可选用于辅助定位更新字段 返回格式: [{参数1, 数值1}...],出现异常会返回空列表 参数 - update_value_param 更新值的字段,也可使用positional_param更新 - fixed_param 固定参数字典格式为{参数固定字段} - splicing_param 拼接参数字典格式为{参数(拼接字段,idx/col)} - positional_param 位置参数字典,根据 idx、列名 定位相应的值,格式为{参数:列名} - mapping_param 映射参数字典,根据 idx、列名 定位相应的值,格式为{参数:列名},再将获取到的值作为键,取得真正的值 - mapping_dict 映射字典 - derived_param 派生参数 形式为{循序字段:(拼接1,拼接2)} def __init__(self,model,update_value_paramNone,fixed_paramNone,splicing_paramNone,positional_paramNone, mapping_paramNone,mapping_dictNone,derived_paramNone): self.model model self._value_field update_value_param self._fixed fixed_param or {} self._positional positional_param or {} self._mapping mapping_param or {} self._mapping_dict mapping_dict or {} self._splicing splicing_param or {} self._derived derived_param or {} self._validate() def _validate(self): if self._value_field is not None and not isinstance(self._value_field, str): raise TypeError(numerical_param 必须为 str) if not isinstance(self._fixed, dict): raise TypeError(fixed_param 必须为 dict) for k, v in self._positional.items(): if not isinstance(k, str): raise TypeError(positional_param 的键必须为 str) if not isinstance(v, str): raise TypeError(positional_param 的值必须为 str) for k, v in self._derived.items(): if not isinstance(v, (tuple,list)): raise TypeError(derived_param 的值必须为 tuple或list) if self._mapping and not self._mapping_dict: raise TypeError(映射参数启用的时候映射字典不能为空) def build(self): updates [] modified_cells self.model.collect_modified_cells() df self.model.get_data() derived_idx 0 for (idx, column), new_value in modified_cells.items(): params {} # value if self._value_field: params[self._value_field] ( None if pd.isna(new_value) else new_value ) # fixed params.update(self._fixed) # positional for param_name, col_name in self._positional.items(): if col_name not in df.columns: continue value df.loc[idx, col_name] if isinstance(value, (np.integer, np.floating)): value value.item() elif pd.isna(value): value None params[param_name] value # mapping for param_name, col_name in self._mapping.items(): if col_name not in df.columns: continue key df.loc[idx, col_name] if pd.isna(key): params[param_name] None continue if key not in self._mapping_dict: raise ValueError( f{key} 不在 mapping_dict 中 ) params[param_name] ( self._mapping_dict[key] ) # splicing for name, (prefix, mode) in self._splicing.items(): value ( str(idx) if mode idx else str(column) ) params[name] prefix value # derived for field, source_fields in self._derived.items(): values [] for source_field in source_fields: values.append(str(source_field)) values.append(str(derived_idx)) derived_idx 1 params[field] _.join(values) updates.append(params) return updates六、下期这期提到了复选框多选框因为篇幅问题没有展开介绍下一篇文章介绍怎么在QAbstractTableModel中实现