class AdvancedMultiFilterWidget(QWidget):
"""高级多条件筛选器组件 - 参考图片设计"""
filter_changed = pyqtSignal()
def __init__(self, parent=None):
super().__init__(parent)
self.dataframe = None
self.filtered_data = None
self.filter_conditions = []
self.is_collapsed = False
self.init_ui()
def init_ui(self):
self.main_layout = QVBoxLayout()
self.main_layout.setContentsMargins(10, 5, 10, 5)
# 标题栏和工具栏
self.create_header()
# 筛选条件容器
self.filters_container = QWidget()
self.filters_layout = QGridLayout(self.filters_container)
self.filters_layout.setSpacing(10)
# 滚动区域
self.scroll_area = QScrollArea()
self.scroll_area.setWidgetResizable(True)
self.scroll_area.setWidget(self.filters_container)
self.scroll_area.setMaximumHeight(300)
self.scroll_area.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
self.scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
self.main_layout.addWidget(self.scroll_area)
# 操作按钮栏
self.create_action_buttons()
# 状态栏
self.status_label = QLabel("筛选器就绪")
self.status_label.setStyleSheet("color: #666; font-size: 10px; padding: 5px;")
self.main_layout.addWidget(self.status_label)
self.setLayout(self.main_layout)
# 设置样式
self.setStyleSheet("""
QWidget {
font-size: 12px;
}
QGroupBox {
font-weight: bold;
border: 2px solid #cccccc;
border-radius: 5px;
margin: 5px 0px;
padding-top: 10px;
}
QGroupBox::title {
subcontrol-origin: margin;
left: 8px;
padding: 0 5px 0 5px;
}
""")
def create_header(self):
"""创建标题栏"""
header_frame = QFrame()
header_frame.setFrameStyle(QFrame.StyledPanel | QFrame.Raised)
header_layout = QHBoxLayout(header_frame)
# 标题
title_label = QLabel("🔍 高级筛选器")
title_label.setFont(QFont("微软雅黑", 12, QFont.Bold))
header_layout.addWidget(title_label)
header_layout.addStretch()
# 折叠/展开按钮
self.collapse_btn = QPushButton("📁 折叠")
self.collapse_btn.setMaximumWidth(80)
self.collapse_btn.clicked.connect(self.toggle_collapse)
header_layout.addWidget(self.collapse_btn)
# 清除所有筛选
clear_all_btn = QPushButton("🗑️ 清除全部")
clear_all_btn.setMaximumWidth(100)
clear_all_btn.clicked.connect(self.clear_all_filters)
header_layout.addWidget(clear_all_btn)
self.main_layout.addWidget(header_frame)
def create_action_buttons(self):
"""创建操作按钮栏"""
button_frame = QFrame()
button_layout = QHBoxLayout(button_frame)
# 应用筛选按钮
apply_btn = QPushButton("✅ 应用筛选")
apply_btn.setStyleSheet("QPushButton { background-color: #4CAF50; color: white; font-weight: bold; padding: 6px 12px; }")
apply_btn.clicked.connect(self.apply_filters)
button_layout.addWidget(apply_btn)
# 重置按钮
reset_btn = QPushButton("🔄 重置")
reset_btn.setStyleSheet("QPushButton { background-color: #f44336; color: white; font-weight: bold; padding: 6px 12px; }")
reset_btn.clicked.connect(self.reset_filters)
button_layout.addWidget(reset_btn)
# 导出筛选结果
export_btn = QPushButton("📤 导出结果")
export_btn.setStyleSheet("QPushButton { background-color: #2196F3; color: white; font-weight: bold; padding: 6px 12px; }")
export_btn.clicked.connect(self.export_filtered_data)
button_layout.addWidget(export_btn)
button_layout.addStretch()
# 筛选结果统计
self.result_label = QLabel("结果: 0 行")
self.result_label.setStyleSheet("font-weight: bold; color: #2196F3;")
button_layout.addWidget(self.result_label)
self.main_layout.addWidget(button_frame)
def set_dataframe(self, df):
"""设置数据源"""
self.dataframe = df.copy()
self.filtered_data = df.copy()
self.setup_default_filters()
self.update_result_count()
def setup_default_filters(self):
"""设置默认筛选条件 - 参考图片布局"""
if self.dataframe is None:
return
# 清除现有筛选器
self.clear_layout(self.filters_layout)
columns = self.dataframe.columns.tolist()
# 第一行筛选器
row = 0
col = 0
# Project ID 筛选 (文本输入)
if len(columns) > 0:
self.add_text_filter("Project ID", columns[0], row, col)
col += 1
# Project Owner 筛选 (下拉选择)
if len(columns) > 1:
self.add_dropdown_filter("Project Owner", columns[1], row, col)
col += 1
# Project Type 筛选 (下拉选择)
if len(columns) > 2:
self.add_dropdown_filter("Project Type", columns[2], row, col)
col += 1
# OEM 筛选 (下拉选择)
if len(columns) > 3:
self.add_dropdown_filter("OEM", columns[3], row, col)
# 第二行筛选器
row = 1
col = 0
# PR/JJ/SF# 筛选 (文本输入)
if len(columns) > 4:
self.add_text_filter("PR/JJ/SF#", columns[4], row, col)
col += 1
# Sold to Customer 筛选 (下拉选择)
if len(columns) > 5:
self.add_dropdown_filter("Sold to Customer", columns[5], row, col)
col += 1
# Project Status 筛选 (下拉选择)
if len(columns) > 6:
self.add_dropdown_filter("Project Status", columns[6], row, col)
col += 1
# Approval Status 筛选 (下拉选择)
if len(columns) > 7:
self.add_dropdown_filter("Approval Status", columns[7], row, col)
# 第三行筛选器
row = 2
col = 0
# Lost Reason 筛选 (下拉选择)
if len(columns) > 8:
self.add_dropdown_filter("Lost Reason", columns[8], row, col)
col += 1
# POS Customer 筛选 (下拉选择)
if len(columns) > 9:
self.add_dropdown_filter("POS Customer", columns[9], row, col)
col += 1
# Won Date 筛选 (日期范围)
if len(columns) > 10:
self.add_date_range_filter("Won Date", columns[10], row, col)
col += 1
# Project Property 筛选 (下拉选择)
if len(columns) > 11:
self.add_dropdown_filter("Project Property", columns[11], row, col)
def add_text_filter(self, label, column, row, col):
"""添加文本筛选器"""
group_box = QGroupBox(label)
group_box.setMaximumWidth(180)
layout = QVBoxLayout(group_box)
# 文本输入框
text_input = QLineEdit()
text_input.setPlaceholderText(f"输入{label}")
text_input.setObjectName(f"text_filter_{column}")
layout.addWidget(text_input)
# 筛选选项
options_layout = QHBoxLayout()
contains_cb = QCheckBox("包含")
contains_cb.setChecked(True)
contains_cb.setObjectName(f"contains_{column}")
options_layout.addWidget(contains_cb)
exact_cb = QCheckBox("精确")
exact_cb.setObjectName(f"exact_{column}")
options_layout.addWidget(exact_cb)
layout.addLayout(options_layout)
self.filters_layout.addWidget(group_box, row, col)
def add_dropdown_filter(self, label, column, row, col):
"""添加下拉筛选器"""
group_box = QGroupBox(label)
group_box.setMaximumWidth(180)
layout = QVBoxLayout(group_box)
# 下拉选择框
combo_box = QComboBox()
combo_box.setObjectName(f"dropdown_filter_{column}")
# 添加选项
combo_box.addItem("全部")
if self.dataframe is not None and column in self.dataframe.columns:
unique_values = self.dataframe[column].dropna().unique()
for value in sorted(unique_values.astype(str)):
combo_box.addItem(str(value))
layout.addWidget(combo_box)
# 多选支持
multi_select_btn = QPushButton("📋 多选")
multi_select_btn.setMaximumHeight(25)
multi_select_btn.clicked.connect(lambda: self.show_multi_select_dialog(column, label))
layout.addWidget(multi_select_btn)
self.filters_layout.addWidget(group_box, row, col)
def add_date_range_filter(self, label, column, row, col):
"""添加日期范围筛选器"""
group_box = QGroupBox(label)
group_box.setMaximumWidth(200)
layout = QVBoxLayout(group_box)
# 开始日期
start_layout = QHBoxLayout()
start_layout.addWidget(QLabel("从:"))
start_date = QDateEdit()
start_date.setDate(QDate.currentDate().addMonths(-1))
start_date.setCalendarPopup(True)
start_date.setObjectName(f"start_date_{column}")
start_layout.addWidget(start_date)
layout.addLayout(start_layout)
# 结束日期
end_layout = QHBoxLayout()
end_layout.addWidget(QLabel("到:"))
end_date = QDateEdit()
end_date.setDate(QDate.currentDate())
end_date.setCalendarPopup(True)
end_date.setObjectName(f"end_date_{column}")
end_layout.addWidget(end_date)
layout.addLayout(end_layout)
# 启用复选框
enable_cb = QCheckBox("启用日期筛选")
enable_cb.setObjectName(f"enable_date_{column}")
layout.addWidget(enable_cb)
self.filters_layout.addWidget(group_box, row, col)
def show_multi_select_dialog(self, column, label):
"""显示多选对话框"""
if self.dataframe is None or column not in self.dataframe.columns:
return
dialog = MultiSelectDialog(self.dataframe[column].dropna().unique(), label, self)
if dialog.exec_() == QDialog.Accepted:
selected_values = dialog.get_selected_values()
# 这里可以保存选中的值,用于筛选
print(f"Selected values for {column}: {selected_values}")
def clear_layout(self, layout):
"""清除布局中的所有控件"""
while layout.count():
child = layout.takeAt(0)
if child.widget():
child.widget().deleteLater()
def toggle_collapse(self):
"""切换折叠状态"""
self.is_collapsed = not self.is_collapsed
if self.is_collapsed:
self.scroll_area.hide()
self.collapse_btn.setText("📂 展开")
else:
self.scroll_area.show()
self.collapse_btn.setText("📁 折叠")
def clear_all_filters(self):
"""清除所有筛选条件"""
# 重置所有筛选控件
for i in range(self.filters_layout.count()):
item = self.filters_layout.itemAt(i)
if item and item.widget():
widget = item.widget()
self.reset_filter_widget(widget)
# 重置数据
if self.dataframe is not None:
self.filtered_data = self.dataframe.copy()
self.update_result_count()
self.filter_changed.emit()
def reset_filter_widget(self, widget):
"""重置单个筛选控件"""
if isinstance(widget, QGroupBox):
for child in widget.findChildren(QLineEdit):
child.clear()
for child in widget.findChildren(QComboBox):
child.setCurrentIndex(0)
for child in widget.findChildren(QCheckBox):
if "contains" in child.objectName():
child.setChecked(True)
else:
child.setChecked(False)
def apply_filters(self):
"""应用所有筛选条件"""
if self.dataframe is None:
return
filtered_df = self.dataframe.copy()
# 应用文本筛选
filtered_df = self.apply_text_filters(filtered_df)
# 应用下拉筛选
filtered_df = self.apply_dropdown_filters(filtered_df)
# 应用日期筛选
filtered_df = self.apply_date_filters(filtered_df)
self.filtered_data = filtered_df
self.update_result_count()
self.filter_changed.emit()
# 显示筛选结果消息
QMessageBox.information(self, "筛选完成", f"筛选完成!结果: {len(filtered_df)} 行数据")
def apply_text_filters(self, df):
"""应用文本筛选"""
for i in range(self.filters_layout.count()):
item = self.filters_layout.itemAt(i)
if item and item.widget():
widget = item.widget()
text_input = widget.findChild(QLineEdit)
if text_input and text_input.text().strip():
column = text_input.objectName().replace("text_filter_", "")
if column in df.columns:
text_value = text_input.text().strip()
# 检查筛选模式
contains_cb = widget.findChild(QCheckBox, f"contains_{column}")
exact_cb = widget.findChild(QCheckBox, f"exact_{column}")
if exact_cb and exact_cb.isChecked():
df = df[df[column].astype(str) == text_value]
elif contains_cb and contains_cb.isChecked():
df = df[df[column].astype(str).str.contains(text_value, case=False, na=False)]
return df
def apply_dropdown_filters(self, df):
"""应用下拉筛选"""
for i in range(self.filters_layout.count()):
item = self.filters_layout.itemAt(i)
if item and item.widget():
widget = item.widget()
combo_box = widget.findChild(QComboBox)
if combo_box and combo_box.currentText() != "全部":
column = combo_box.objectName().replace("dropdown_filter_", "")
if column in df.columns:
selected_value = combo_box.currentText()
df = df[df[column].astype(str) == selected_value]
return df
def apply_date_filters(self, df):
"""应用日期筛选"""
for i in range(self.filters_layout.count()):
item = self.filters_layout.itemAt(i)
if item and item.widget():
widget = item.widget()
enable_cb = widget.findChild(QCheckBox)
if enable_cb and "enable_date" in enable_cb.objectName() and enable_cb.isChecked():
column = enable_cb.objectName().replace("enable_date_", "")
if column in df.columns:
start_date_widget = widget.findChild(QDateEdit, f"start_date_{column}")
end_date_widget = widget.findChild(QDateEdit, f"end_date_{column}")
if start_date_widget and end_date_widget:
start_date = start_date_widget.date().toPyDate()
end_date = end_date_widget.date().toPyDate()
# 转换列为日期类型
try:
df[column] = pd.to_datetime(df[column])
df = df[(df[column].dt.date >= start_date) & (df[column].dt.date <= end_date)]
except:
continue
return df
def reset_filters(self):
"""重置所有筛选"""
self.clear_all_filters()
def update_result_count(self):
"""更新结果计数"""
if self.filtered_data is not None:
count = len(self.filtered_data)
total = len(self.dataframe) if self.dataframe is not None else 0
self.result_label.setText(f"结果: {count}/{total} 行")
self.status_label.setText(f"筛选完成 - 显示 {count} 行,共 {total} 行")
else:
self.result_label.setText("结果: 0 行")
self.status_label.setText("筛选器就绪")
def export_filtered_data(self):
"""导出筛选结果"""
if self.filtered_data is None or len(self.filtered_data) == 0:
QMessageBox.warning(self, "警告", "没有数据可导出!")
return
file_path, _ = QFileDialog.getSaveFileName(self, "导出筛选结果", "", "CSV files (*.csv);;Excel files (*.xlsx)")
if file_path:
try:
if file_path.endswith('.csv'):
self.filtered_data.to_csv(file_path, index=False, encoding='utf-8-sig')
else:
self.filtered_data.to_excel(file_path, index=False)
QMessageBox.information(self, "成功", f"筛选结果已导出到: {file_path}")
except Exception as e:
QMessageBox.warning(self, "错误", f"导出失败: {str(e)}")
深度解释上述代码,解释的时候需要将使用到的函数全部解释,并定义代码进行注释,返回注释后的代码