diff --git a/garment_dialogs.py b/garment_dialogs.py index 0438b75..9ea69d3 100644 --- a/garment_dialogs.py +++ b/garment_dialogs.py @@ -10,142 +10,68 @@ from PyQt5.QtWidgets import ( QPushButton, QComboBox, QTableWidget, QTableWidgetItem, QHeaderView, QMessageBox, QFileDialog, QDoubleSpinBox, QWidget, QCompleter ) -from PyQt5.QtCore import Qt, QStringListModel, QTimer +from PyQt5.QtCore import Qt, QTimer, QSortFilterProxyModel from PyQt5.QtGui import QPixmap from database import get_db_connection class SearchableComboBox(QComboBox): - """支持模糊搜索的下拉框""" + """支持模糊搜索的下拉框 - 基于 QSortFilterProxyModel 和 QCompleter""" def __init__(self, parent=None): - super().__init__(parent) + super(SearchableComboBox, self).__init__(parent) + + self.setFocusPolicy(Qt.StrongFocus) self.setEditable(True) self.setInsertPolicy(QComboBox.NoInsert) - - # 存储所有选项 - self.all_items = [] - self.all_data = [] - self.is_filtering = False - - # 设置自动完成 - self.completer = QCompleter(self) - self.completer.setCompletionMode(QCompleter.PopupCompletion) - self.completer.setCaseSensitivity(Qt.CaseInsensitive) - self.completer.setFilterMode(Qt.MatchContains) - self.setCompleter(self.completer) - - # 连接信号 - self.lineEdit().textChanged.connect(self.on_text_changed) - - def addItem(self, text, userData=None): - """添加选项""" - # 临时断开信号连接,防止textChanged触发on_text_changed - try: - self.lineEdit().textChanged.disconnect() - except TypeError: - # 信号未连接或已被断开,忽略错误 - pass - super().addItem(text, userData) - if text not in self.all_items: - self.all_items.append(text) - self.all_data.append(userData) - self.update_completer() - # 重新连接信号 - try: - self.lineEdit().textChanged.connect(self.on_text_changed) - except: - # 如果连接失败,忽略错误 - pass - - def addItems(self, texts): - """批量添加选项""" - for text in texts: - self.addItem(text) - - def clear(self): - """清空所有选项""" - if not self.is_filtering: - super().clear() - self.all_items.clear() - self.all_data.clear() - self.update_completer() - - def reset_items(self): - """重置所有选项""" - # 临时断开信号连接,防止textChanged触发on_text_changed - try: - self.lineEdit().textChanged.disconnect() - except TypeError: - # 信号未连接或已被断开,忽略错误 - pass - self.is_filtering = True - super().clear() - for i, item in enumerate(self.all_items): - super().addItem(item, self.all_data[i] if i < len(self.all_data) else None) - self.is_filtering = False - # 重新连接信号 - try: - self.lineEdit().textChanged.connect(self.on_text_changed) - except: - # 如果连接失败,忽略错误 - pass - # # 确保当前索引设置为0(第一项) - # if self.count() > 0: - # super().setCurrentIndex(0) - - def update_completer(self): - """更新自动完成列表""" - model = QStringListModel(self.all_items) - self.completer.setModel(model) - - def on_text_changed(self, text): - """文本改变时的处理""" - if self.is_filtering: - return - - if not text or text in ["—— 选择型号 ——"]: - self.reset_items() - # 如果获得焦点且有选项,显示下拉列表 - if self.hasFocus() and self.count() > 0: - self.showPopup() - return - - # 模糊搜索匹配 - filtered_items = [] - filtered_data = [] - for i, item in enumerate(self.all_items): - if text.lower() in item.lower(): - filtered_items.append(item) - filtered_data.append(self.all_data[i] if i < len(self.all_data) else None) - - # 更新下拉列表 - self.is_filtering = True - super().clear() - for i, item in enumerate(filtered_items): - super().addItem(item, filtered_data[i]) - self.is_filtering = False - - # 如果有匹配项且获得焦点,显示下拉列表 - if filtered_items and self.hasFocus(): - self.showPopup() + # 添加过滤模型来过滤匹配项 + self.pFilterModel = QSortFilterProxyModel(self) + self.pFilterModel.setFilterCaseSensitivity(Qt.CaseInsensitive) + self.pFilterModel.setSourceModel(self.model()) + + # 添加自动完成器,使用过滤模型 + self.completer = QCompleter(self.pFilterModel, self) + # 始终显示所有(过滤后的)完成项 + self.completer.setCompletionMode(QCompleter.UnfilteredPopupCompletion) + self.setCompleter(self.completer) + + # 连接信号 + self.lineEdit().textEdited.connect(self.pFilterModel.setFilterFixedString) + self.completer.activated.connect(self.on_completer_activated) + + # 当从自动完成器中选择项时,从组合框中选择对应的项 + def on_completer_activated(self, text): + if text: + index = self.findText(text) + self.setCurrentIndex(index) + self.activated[str].emit(self.itemText(index)) + + # 当模型改变时,同时更新过滤器和自动完成器的模型 + def setModel(self, model): + super(SearchableComboBox, self).setModel(model) + self.pFilterModel.setSourceModel(model) + self.completer.setModel(self.pFilterModel) + + # 当模型列改变时,同时更新过滤器和自动完成器的模型列 + def setModelColumn(self, column): + self.completer.setCompletionColumn(column) + self.pFilterModel.setFilterKeyColumn(column) + super(SearchableComboBox, self).setModelColumn(column) + def setDefaultText(self, text): - """设置默认文本""" - # 临时断开信号连接,防止textChanged触发on_text_changed - try: - self.lineEdit().textChanged.disconnect() - except TypeError: - # 信号未连接或已被断开,忽略错误 - pass - self.lineEdit().setText(text) - self.update_completer() - # 重新连接信号 - try: - self.lineEdit().textChanged.connect(self.on_text_changed) - except: - # 如果连接失败,忽略错误 - pass + """设置默认文本 - 兼容方法""" + line_edit = self.lineEdit() + if line_edit: + line_edit.blockSignals(True) + line_edit.setText(text) + line_edit.blockSignals(False) + # 尝试找到匹配的项 + index = self.findText(text) + if index >= 0: + self.setCurrentIndex(index) + else: + self.setCurrentIndex(-1) class GarmentLibraryDialog(QDialog): """服装库管理对话框""" @@ -410,6 +336,7 @@ class GarmentEditDialog(QDialog): def add_material_row(self, category="", fabric_type="", usage=0.0, unit="米", model=""): """添加材料行""" + print("add_material_row", category, fabric_type, usage, unit, model) row = self.material_table.rowCount() self.material_table.insertRow(row) @@ -492,43 +419,14 @@ class GarmentEditDialog(QDialog): self.material_table.setCellWidget(row, 1, type_combo) - # 列2: 型号下拉框 - model_combo = QComboBox() - model_combo.setEditable(False) + # 列2: 型号下拉框(支持模糊搜索) + model_combo = SearchableComboBox() model_combo.addItem("—— 选择型号 ——") - - # 初始化时加载所有型号 - try: - with self.get_conn() as conn: - cursor = conn.execute(""" - SELECT DISTINCT model, color, unit - FROM fabrics - ORDER BY model - """) - models = cursor.fetchall() - for model_row in models: - model, color, unit = model_row - # 显示格式:型号-颜色(如果有颜色的话) - display_text = model - if color and color.strip(): - display_text = f"{model}-{color}" - model_combo.addItem(display_text, model) - except Exception as e: - pass - - if fabric_type: - model_combo.setCurrentText(fabric_type) - else: - # 如果没有指定类型,默认选择"—— 选择类型 ——"(索引0) - print("没有指定类型,默认选择'—— 选择类型 ——'(索引0)") - print(model_combo.count()) - print(model_combo.currentIndex()) - # 打印所有选项 - for i in range(model_combo.count()): - print(model_combo.itemText(i)) - model_combo.setCurrentIndex(0) - + # 先将下拉框添加到表格中,这样update_model_combo_range才能获取到类目和类型 self.material_table.setCellWidget(row, 2, model_combo) + + # 初始化时根据类目和类型加载对应的型号 + self.update_model_combo_range(model_combo, row) # 列3: 单件用量 usage_spin = QDoubleSpinBox() @@ -577,7 +475,91 @@ class GarmentEditDialog(QDialog): # break cat_combo.currentTextChanged.connect(lambda text, r=row: self.on_category_changed(text, r)) type_combo.currentTextChanged.connect(lambda text, r=row: self.on_type_changed(text, r)) - model_combo.currentTextChanged.connect(lambda text, r=row: self.on_model_selected(text, r)) + # 当用户真正选择了某个型号(包括通过模糊搜索补全选择),再联动类目和类型 + model_combo.activated[str].connect(lambda text, r=row: self.on_model_selected(text, r)) + def update_model_combo_range(self, model_combo, row): + print("update_model_combo_range", model_combo, row) + """更新型号下拉框的搜索范围""" + cat_combo = self.material_table.cellWidget(row, 0) + type_combo = self.material_table.cellWidget(row, 1) + + category_text = cat_combo.currentText() if cat_combo else "" + type_text = type_combo.currentText() if type_combo else "" + + # 阻止信号防止触发on_model_selected + model_combo.blockSignals(True) + line_edit = model_combo.lineEdit() + if line_edit: + line_edit.blockSignals(True) + + try: + with self.get_conn() as conn: + # 根据类目和类型构建查询条件 + if category_text and category_text != "—— 自定义类目 ——" and type_text and type_text != "—— 选择类型 ——": + # 同时根据类目和类型过滤 + cursor = conn.execute(""" + SELECT DISTINCT model, color, unit + FROM fabrics + WHERE category = ? AND fabric_type = ? + ORDER BY model + """, (category_text, type_text)) + elif category_text and category_text != "—— 自定义类目 ——": + # 只根据类目过滤 + cursor = conn.execute(""" + SELECT DISTINCT model, color, unit + FROM fabrics + WHERE category = ? + ORDER BY model + """, (category_text,)) + elif type_text and type_text != "—— 选择类型 ——": + # 只根据类型过滤 + cursor = conn.execute(""" + SELECT DISTINCT model, color, unit + FROM fabrics + WHERE fabric_type = ? + ORDER BY model + """, (type_text,)) + else: + # 显示所有型号 + cursor = conn.execute(""" + SELECT DISTINCT model, color, unit + FROM fabrics + ORDER BY model + """) + + # 保存当前用户输入的文本(如果有) + current_text = line_edit.text() if line_edit else "" + cursor_position = line_edit.cursorPosition() if line_edit else 0 + print("current_text", current_text) + print("cursor_position", cursor_position) + # 清空并重新填充型号下拉框 + model_combo.clear() + models = cursor.fetchall() + for model_row in models: + model, color, unit = model_row + # 显示格式:型号-颜色(如果有颜色的话) + display_text = model + if color and color.strip(): + display_text = f"{model}-{color}" + model_combo.addItem(display_text, model) + + # 恢复用户输入的文本,如果没有输入则设置默认文本 + if line_edit: + if current_text and current_text != "—— 选择型号 ——": + line_edit.setText(current_text) + line_edit.setCursorPosition(cursor_position) + else: + if isinstance(model_combo, SearchableComboBox): + model_combo.setDefaultText("—— 选择型号 ——") + else: + model_combo.setCurrentIndex(0) + except Exception as e: + pass + finally: + if line_edit: + line_edit.blockSignals(False) + model_combo.blockSignals(False) + def on_category_changed(self, category_text, row): """当类目改变时,更新类型下拉框""" print("on_category_changed", category_text, row) @@ -588,11 +570,6 @@ class GarmentEditDialog(QDialog): type_combo.clear() type_combo.addItem("—— 选择类型 ——") - # 重新初始化型号下拉框,显示所有型号(阻止信号防止触发on_model_selected) - model_combo.blockSignals(True) - model_combo.clear() - model_combo.addItem("—— 选择型号 ——") - try: with self.get_conn() as conn: # 加载所有类型 @@ -621,38 +598,10 @@ class GarmentEditDialog(QDialog): # 连接类型改变事件 type_combo.currentTextChanged.connect(lambda text, r=row: self.on_type_changed(text, r)) - # 加载该类目下的所有型号到型号下拉框 - if category_text and category_text != "—— 自定义类目 ——": - # 如果选择了具体类目,则只加载该类目下的型号 - cursor = conn.execute(""" - SELECT DISTINCT model, color, unit - FROM fabrics - WHERE category = ? - ORDER BY model - """, (category_text,)) - else: - # 如果是自定义类目,加载所有型号 - cursor = conn.execute(""" - SELECT DISTINCT model, color, unit - FROM fabrics - ORDER BY model - """) - models = cursor.fetchall() - for model_row in models: - model, color, unit = model_row - # 显示格式:型号-颜色(如果有颜色的话) - display_text = model - if color and color.strip(): - display_text = f"{model}-{color}" - model_combo.addItem(display_text, model) - - # 确保默认选中第一项("—— 选择型号 ——") - model_combo.setCurrentIndex(0) + # 更新型号下拉框的搜索范围 + self.update_model_combo_range(model_combo, row) except Exception as e: pass - finally: - # 恢复信号 - model_combo.blockSignals(False) def on_type_changed(self, type_text, row): """当类型改变时,更新类目和型号下拉框""" @@ -675,9 +624,9 @@ class GarmentEditDialog(QDialog): ORDER BY category LIMIT 1 """, (type_text,)) - row = cursor.fetchone() - if row and row[0]: - category = row[0].strip() + row_db = cursor.fetchone() + if row_db and row_db[0]: + category = row_db[0].strip() # 在类目下拉框中查找并选中该类目 index = cat_combo.findText(category) if index >= 0: @@ -687,66 +636,8 @@ class GarmentEditDialog(QDialog): except Exception as e: pass - # 重新初始化型号下拉框,显示该类型下的所有型号 - model_combo.blockSignals(True) - model_combo.clear() - model_combo.addItem("—— 选择型号 ——") - - # 根据类型和类目过滤型号 - try: - with self.get_conn() as conn: - # 获取当前选择的类目(可能已经更新) - category_text = cat_combo.currentText() if cat_combo else "" - - # 构建查询条件 - if type_text and type_text != "—— 选择类型 ——": - # 如果选择了具体类型 - if category_text and category_text != "—— 自定义类目 ——": - # 同时根据类目和类型过滤 - cursor = conn.execute(""" - SELECT DISTINCT model, color, unit - FROM fabrics - WHERE category = ? AND fabric_type = ? - ORDER BY model - """, (category_text, type_text)) - else: - # 只根据类型过滤 - cursor = conn.execute(""" - SELECT DISTINCT model, color, unit - FROM fabrics - WHERE fabric_type = ? - ORDER BY model - """, (type_text,)) - else: - # 如果没有选择类型,根据类目过滤(如果有类目) - if category_text and category_text != "—— 自定义类目 ——": - cursor = conn.execute(""" - SELECT DISTINCT model, color, unit - FROM fabrics - WHERE category = ? - ORDER BY model - """, (category_text,)) - else: - # 显示所有型号 - cursor = conn.execute(""" - SELECT DISTINCT model, color, unit - FROM fabrics - ORDER BY model - """) - models = cursor.fetchall() - for model_row in models: - model, color, unit = model_row - # 显示格式:型号-颜色(如果有颜色的话) - display_text = model - if color and color.strip(): - display_text = f"{model}-{color}" - model_combo.addItem(display_text, model) - except Exception as e: - pass - - # 确保默认选中第一项("—— 选择型号 ——") - model_combo.setCurrentIndex(0) - model_combo.blockSignals(False) + # 更新型号下拉框的搜索范围 + self.update_model_combo_range(model_combo, row) def on_model_selected(self, model_text, row): """当型号选择时,自动设置单位并填充类目和类型""" @@ -758,89 +649,53 @@ class GarmentEditDialog(QDialog): unit_combo = self.material_table.cellWidget(row, 4) if not model_text or model_text == "—— 选择型号 ——": - # 获取当前类目和类型下的所有型号,阻止信号防止死循环 - model_combo.blockSignals(True) - try: - model_combo.clear() - model_combo.addItem("—— 选择型号 ——") - - try: - with self.get_conn() as conn: - if cat_combo.currentText() != "—— 自定义类目 ——" and type_combo.currentText() != "—— 选择类型 ——": - cursor = conn.execute("SELECT DISTINCT model, color, unit FROM fabrics WHERE category = ? AND fabric_type = ? ORDER BY model", (cat_combo.currentText(), type_combo.currentText())) - elif cat_combo.currentText() != "—— 自定义类目 ——" and type_combo.currentText() == "—— 选择类型 ——": - cursor = conn.execute("SELECT DISTINCT model, color, unit FROM fabrics WHERE category = ? ORDER BY model", (cat_combo.currentText(),)) - else: - cursor = conn.execute("SELECT DISTINCT model, color, unit FROM fabrics ORDER BY model") - - models = cursor.fetchall() - for model_row in models: - model, color, unit = model_row - # 显示格式:型号-颜色(如果有颜色的话) - display_text = model - if color and color.strip(): - display_text = f"{model}-{color}" - model_combo.addItem(display_text, model) - except Exception as e: - pass - - model_combo.setCurrentIndex(0) - finally: - # 恢复信号 - model_combo.blockSignals(False) + # 空选择,不做联动 return - - # 获取选中项的数据 - current_index = model_combo.currentIndex() - if current_index > 0: - model = model_combo.itemData(current_index) - if model: - try: - with self.get_conn() as conn: - cursor = conn.execute("SELECT category, fabric_type, unit FROM fabrics WHERE model = ?", (model,)) - row_db = cursor.fetchone() - if row_db: - category, fabric_type, unit = row_db - - # 自动填充单位 - if unit: - unit_combo.setCurrentText(unit) - - # 自动填充类目和类型,阻止信号以避免触发默认更新逻辑 - if category: - # 阻止类目、类型和型号的信号,避免触发默认更新逻辑 - cat_combo.blockSignals(True) - type_combo.blockSignals(True) - model_combo.blockSignals(True) - - # 设置类目 - cat_combo.setCurrentText(category) - - # 更新类型下拉框选项(直接调用,不会触发信号因为已经阻止了) - self.on_category_changed(category, row) - - # 重新阻止信号(因为on_category_changed在finally中恢复了信号) - # 这可以防止死循环 - model_combo.blockSignals(True) - - # 重新设置选中的型号(因为on_category_changed会清空并重新加载型号下拉框) - # 查找并选中对应的型号 - for i in range(model_combo.count()): - item_data = model_combo.itemData(i) - if item_data == model: - model_combo.setCurrentIndex(i) - break - - # 设置类型 - if fabric_type: - type_combo.setCurrentText(fabric_type) - - # 恢复信号 - cat_combo.blockSignals(False) - type_combo.blockSignals(False) - model_combo.blockSignals(False) - except: - pass + + # 从显示文本中解析真实型号(例如 "M001-红" -> "M001") + display_text = model_text.strip() + base_model = display_text.split("-", 1)[0] if "-" in display_text else display_text + + try: + with self.get_conn() as conn: + cursor = conn.execute( + "SELECT category, fabric_type, unit FROM fabrics WHERE model = ?", + (base_model,) + ) + row_db = cursor.fetchone() + if row_db: + category, fabric_type, unit = row_db + + # 自动填充单位 + if unit: + unit_combo.setCurrentText(unit) + + # 自动填充类目和类型,阻止信号以避免触发默认更新逻辑 + if category: + # 阻止类目、类型和型号的信号,避免触发默认更新逻辑 + cat_combo.blockSignals(True) + type_combo.blockSignals(True) + model_combo.blockSignals(True) + + # 设置类目 + cat_combo.setCurrentText(category) + + # 更新类型下拉框选项(直接调用,不会触发信号因为已经阻止了) + self.on_category_changed(category, row) + + # 重新阻止信号(因为on_category_changed内部可能恢复了信号) + model_combo.blockSignals(True) + + # 设置类型 + if fabric_type: + type_combo.setCurrentText(fabric_type) + + # 恢复信号 + cat_combo.blockSignals(False) + type_combo.blockSignals(False) + model_combo.blockSignals(False) + except: + pass def save_garment(self): """保存服装"""