#!/usr/bin/env python # -*- coding: utf-8 -*- """ Validators - 输入验证框架 提供统一的输入验证机制,用于 GUI 表单验证 """ import os import re from datetime import datetime from typing import Any, Callable, List, Optional from tkinter import ttk class ValidationResult: """ 验证结果 表示验证操作的结果,包含是否成功和错误消息 """ def __init__(self, is_valid: bool, error_message: str = ""): """ 初始化验证结果 Args: is_valid: 是否验证通过 error_message: 错误消息(验证失败时) """ self.is_valid = is_valid self.error_message = error_message def __bool__(self) -> bool: """允许直接在 if 语句中使用""" return self.is_valid def __str__(self) -> str: """返回错误消息或"验证通过" """ return self.error_message if not self.is_valid else "验证通过" class Validator: """ 输入验证器基类 提供常用的静态验证方法 """ @staticmethod def not_empty(value: Any, field_name: str = "字段") -> ValidationResult: """ 非空验证 Args: value: 要验证的值 field_name: 字段名称(用于错误消息) Returns: 验证结果 """ if value is None or value == "": return ValidationResult(False, f"{field_name}不能为空") if isinstance(value, str) and not value.strip(): return ValidationResult(False, f"{field_name}不能为空或仅包含空格") return ValidationResult(True) @staticmethod def file_exists(path: str, field_name: str = "文件") -> ValidationResult: """ 文件存在性验证 Args: path: 文件路径 field_name: 字段名称 Returns: 验证结果 """ if not path: return ValidationResult(False, f"{field_name}路径不能为空") if not os.path.exists(path): return ValidationResult(False, f"{field_name}不存在: {path}") return ValidationResult(True) @staticmethod def dir_exists(path: str, field_name: str = "目录") -> ValidationResult: """ 目录存在性验证 Args: path: 目录路径 field_name: 字段名称 Returns: 验证结果 """ if not path: return ValidationResult(False, f"{field_name}路径不能为空") if not os.path.exists(path): return ValidationResult(False, f"{field_name}不存在: {path}") if not os.path.isdir(path): return ValidationResult(False, f"{field_name}不是有效的目录: {path}") return ValidationResult(True) @staticmethod def date_format(value: str, format_str: str = "%Y-%m-%d", field_name: str = "日期") -> ValidationResult: """ 日期格式验证 Args: value: 日期字符串 format_str: 期望的日期格式 field_name: 字段名称 Returns: 验证结果 """ if not value: return ValidationResult(False, f"{field_name}不能为空") try: datetime.strptime(value, format_str) return ValidationResult(True) except ValueError: return ValidationResult(False, f"{field_name}格式错误,期望格式: {format_str}") @staticmethod def numeric(value: Any, field_name: str = "数值", min_value: Optional[float] = None, max_value: Optional[float] = None) -> ValidationResult: """ 数值验证 Args: value: 要验证的值 field_name: 字段名称 min_value: 最小值(可选) max_value: 最大值(可选) Returns: 验证结果 """ if value is None or value == "": return ValidationResult(False, f"{field_name}不能为空") try: num = float(value) except (ValueError, TypeError): return ValidationResult(False, f"{field_name}必须是有效的数字") if min_value is not None and num < min_value: return ValidationResult(False, f"{field_name}不能小于 {min_value}") if max_value is not None and num > max_value: return ValidationResult(False, f"{field_name}不能大于 {max_value}") return ValidationResult(True) @staticmethod def integer(value: Any, field_name: str = "整数") -> ValidationResult: """ 整数验证 Args: value: 要验证的值 field_name: 字段名称 Returns: 验证结果 """ if value is None or value == "": return ValidationResult(False, f"{field_name}不能为空") try: int(value) return ValidationResult(True) except (ValueError, TypeError): return ValidationResult(False, f"{field_name}必须是有效的整数") @staticmethod def length(value: str, min_length: int = 0, max_length: Optional[int] = None, field_name: str = "字段") -> ValidationResult: """ 字符串长度验证 Args: value: 要验证的字符串 min_length: 最小长度 max_length: 最大长度(可选) field_name: 字段名称 Returns: 验证结果 """ if not isinstance(value, str): return ValidationResult(False, f"{field_name}必须是字符串") length = len(value) if length < min_length: return ValidationResult(False, f"{field_name}长度不能少于 {min_length} 个字符") if max_length is not None and length > max_length: return ValidationResult(False, f"{field_name}长度不能超过 {max_length} 个字符") return ValidationResult(True) @staticmethod def regex(value: str, pattern: str, field_name: str = "字段") -> ValidationResult: """ 正则表达式验证 Args: value: 要验证的字符串 pattern: 正则表达式模式 field_name: 字段名称 Returns: 验证结果 """ if not isinstance(value, str): return ValidationResult(False, f"{field_name}必须是字符串") if not re.match(pattern, value): return ValidationResult(False, f"{field_name}格式不正确") return ValidationResult(True) @staticmethod def email(value: str, field_name: str = "邮箱") -> ValidationResult: """ 邮箱格式验证 Args: value: 邮箱地址 field_name: 字段名称 Returns: 验证结果 """ if not value: return ValidationResult(False, f"{field_name}不能为空") # 简单的邮箱正则表达式 pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$' return Validator.regex(value, pattern, field_name) @staticmethod def phone(value: str, field_name: str = "手机号") -> ValidationResult: """ 手机号验证(中国大陆) Args: value: 手机号 field_name: 字段名称 Returns: 验证结果 """ if not value: return ValidationResult(False, f"{field_name}不能为空") # 中国大陆手机号正则表达式 pattern = r'^1[3-9]\d{9}$' return Validator.regex(value, pattern, field_name) @staticmethod def in_range(value: Any, allowed_values: List[Any], field_name: str = "字段") -> ValidationResult: """ 值范围验证 Args: value: 要验证的值 allowed_values: 允许的值列表 field_name: 字段名称 Returns: 验证结果 """ if value not in allowed_values: return ValidationResult( False, f"{field_name}必须是以下值之一: {', '.join(str(v) for v in allowed_values)}" ) return ValidationResult(True) @staticmethod def custom(value: Any, validator_func: Callable[[Any], bool], error_message: str = "验证失败") -> ValidationResult: """ 自定义验证函数 Args: value: 要验证的值 validator_func: 验证函数,返回 True 表示验证通过 error_message: 错误消息 Returns: 验证结果 """ try: if validator_func(value): return ValidationResult(True) return ValidationResult(False, error_message) except Exception as e: return ValidationResult(False, f"验证过程出错: {str(e)}") class ValidatedWidget: """ 带验证功能的 Widget 基类 为任何支持 get_value() 方法的组件添加验证功能 """ def __init__(self, widget: ttk.Widget, error_label: Optional[ttk.Label] = None): """ 初始化验证组件 Args: widget: 要验证的组件(必须支持 get_value() 方法) error_label: 用于显示错误消息的 Label(可选) """ self.widget = widget self.error_label = error_label self.validators: List[Callable[[Any], ValidationResult]] = [] self._last_result: Optional[ValidationResult] = None def add_validator(self, validator: Callable[[Any], ValidationResult]): """ 添加验证器 Args: validator: 验证函数,接收值并返回 ValidationResult """ self.validators.append(validator) def add_not_empty_validator(self, field_name: str = "字段"): """ 添加非空验证器 Args: field_name: 字段名称 """ self.add_validator(lambda v: Validator.not_empty(v, field_name)) def add_file_exists_validator(self, field_name: str = "文件"): """ 添加文件存在性验证器 Args: field_name: 字段名称 """ self.add_validator(lambda v: Validator.file_exists(v, field_name)) def add_date_format_validator(self, format_str: str = "%Y-%m-%d", field_name: str = "日期"): """ 添加日期格式验证器 Args: format_str: 日期格式 field_name: 字段名称 """ self.add_validator(lambda v: Validator.date_format(v, format_str, field_name)) def add_custom_validator(self, validator_func: Callable[[Any], bool], error_message: str = "验证失败"): """ 添加自定义验证器 Args: validator_func: 验证函数 error_message: 错误消息 """ self.add_validator(lambda v: Validator.custom(v, validator_func, error_message)) def validate(self) -> bool: """ 执行所有验证 Returns: True 如果所有验证都通过,False 否则 """ value = self._get_value() for validator in self.validators: result = validator(value) self._last_result = result if not result.is_valid: self._show_error(result.error_message) return False self._clear_error() return True def _get_value(self) -> Any: """ 获取组件的值 Returns: 组件的当前值 """ if hasattr(self.widget, 'get'): return self.widget.get() elif hasattr(self.widget, 'cget'): # 对于某些组件,尝试获取配置值 return self.widget.cget('text') else: raise AttributeError(f"Widget {type(self.widget).__name__} 不支持获取值") def _show_error(self, message: str): """ 显示错误消息 Args: message: 错误消息 """ if self.error_label: self.error_label.config(text=message, foreground="red") # 也可以添加其他错误显示方式,比如改变组件边框颜色 def _clear_error(self): """清除错误消息""" if self.error_label: self.error_label.config(text="") def get_last_result(self) -> Optional[ValidationResult]: """ 获取最后一次验证结果 Returns: 最后一次验证的 ValidationResult """ return self._last_result