改进: 托盘图标状态管理和分类配置持久化- 添加活动状态图标(icon_active.png),提供更明显的视觉反馈- 优化图标切换逻辑,确保状态变化时图标正确更新- 增强图标刷新机制,解决某些环境下图标不更新的问题- 添加更全面的错误处理,提高系统托盘功能稳定性- 完善分类配置的持久化和管理机制- 更新README.md,添加v0.1.7版本更新内容

This commit is contained in:
zhukang 2025-03-02 20:05:33 +08:00
parent 9539cb6cb8
commit 9cf3cde62f
11 changed files with 950 additions and 167 deletions

View File

@ -0,0 +1,97 @@
# LLMClipboard 托盘图标状态管理改进总结
## 1. 问题背景
在之前的版本中LLMClipboard 应用程序的系统托盘图标在服务状态变化(启动/停止监听)时没有提供足够明显的视觉反馈。具体问题包括:
- 启动监听后托盘图标不变化,用户无法直观判断应用程序状态
- 缺少活动状态的专用图标文件
- 图标切换逻辑不完善,在某些环境下不能正确更新
## 2. 改进内容
### 2.1 活动状态图标创建
创建了专用的活动状态图标 `icon_active.png`,通过以下特点与非活动状态图标区分:
- 增加亮度 (30%)
- 增加对比度 (20%)
- 增加颜色饱和度 (50%)
这使得活动状态下的图标更加明亮、鲜艳,提供更明显的视觉反馈。
### 2.2 图标切换逻辑优化
优化了 `toggle_monitoring` 方法中的图标切换逻辑:
- 在启动监听时切换到活动状态图标
- 在停止监听时切换到非活动状态图标
- 添加了图标可见性检查,确保托盘图标始终可见
- 更新了图标提示文本,提供状态信息
### 2.3 图标刷新机制增强
添加了强制刷新图标的机制,解决某些环境下图标不更新的问题:
- 使用 `hide()``show()` 方法强制刷新图标
- 调用 `QApplication.processEvents()` 确保 UI 更新
- 在启动监听、停止监听和异常处理时都添加了图标刷新逻辑
### 2.4 错误处理增强
完善了图标操作的错误处理:
- 添加了 try-except 块捕获可能的异常
- 添加了详细的错误日志记录
- 实现了错误恢复机制,确保即使出现异常也不会影响应用程序整体功能
## 3. 技术实现
### 3.1 图标创建工具
创建了 `create_active_icon.py` 脚本,用于生成活动状态图标:
- 使用 PIL 库处理图像
- 通过 ImageEnhance 模块调整亮度、对比度和饱和度
- 自动检测资源目录和原始图标位置
### 3.2 GUI 类修改
修改了 `gui.py` 中的相关方法:
- `toggle_monitoring`: 添加了图标切换和刷新逻辑
- 异常处理部分: 添加了图标恢复和刷新逻辑
### 3.3 测试验证
通过以下场景验证了改进效果:
- 启动监听时图标切换到活动状态
- 停止监听时图标切换到非活动状态
- 异常情况下图标恢复到非活动状态
- 多次切换状态时图标正确更新
## 4. 效果对比
| 功能 | 改进前 | 改进后 |
|------|--------|--------|
| 视觉反馈 | 无明显区分 | 活动状态图标更明亮、鲜艳 |
| 状态指示 | 仅通过菜单文本 | 图标颜色和提示文本 |
| 更新可靠性 | 部分环境下不更新 | 强制刷新确保更新 |
| 错误处理 | 基础处理 | 全面的异常捕获和恢复 |
## 5. 后续优化方向
- 考虑添加动态图标,如活动状态下的动画效果
- 优化图标资源加载机制支持高DPI显示
- 添加更多状态指示(如错误状态、处理中状态等)
- 实现自定义图标主题支持
## 6. Commit 消息
```
改进: 托盘图标状态管理和分类配置持久化
- 添加活动状态图标(icon_active.png),提供更明显的视觉反馈
- 优化图标切换逻辑,确保状态变化时图标正确更新
- 增强图标刷新机制,解决某些环境下图标不更新的问题
- 添加更全面的错误处理,提高系统托盘功能稳定性
- 完善分类配置的持久化和管理机制
- 更新README.md添加v0.1.7版本更新内容
此次更新使系统托盘功能更加稳定可靠,用户可以通过图标颜色
直观判断应用程序状态,提升了整体用户体验。
```

View File

@ -326,7 +326,18 @@ cd project\llmclipboard ; pyinstaller --clean llmclipboard.spec
- 改进通知系统样式和错误处理 - 改进通知系统样式和错误处理
- 优化UI响应性能 - 优化UI响应性能
- 添加uv环境打包支持 - 添加uv环境打包支持
-
## v0.1.7 (2025-03-02)
- 改进托盘图标状态管理
- 添加活动状态图标,提供更明显的视觉反馈
- 优化图标切换逻辑,确保状态变化时图标正确更新
- 增强图标刷新机制,解决某些环境下图标不更新的问题
- 添加更全面的错误处理,提高系统托盘功能稳定性
- 完善分类配置的持久化和管理机制
- 支持开发和打包环境下的配置文件路径
- 确保配置文件正确保存和加载
- 添加更详细的日志记录
## 🤝 贡献指南 ## 🤝 贡献指南
1. Fork 项目 1. Fork 项目

View File

@ -18,6 +18,18 @@ if exist dist\LLMClipboard.exe (
if not exist dist\resources mkdir dist\resources if not exist dist\resources mkdir dist\resources
xcopy /s /y resources dist\resources\ xcopy /s /y resources dist\resources\
:: Create a default config.ini in the dist folder
echo Creating default config.ini in dist folder...
echo [Settings] > dist\config.ini
echo double_click_threshold = 0.3 >> dist\config.ini
echo save_location = C:\Users\Documents >> dist\config.ini
echo. >> dist\config.ini
echo [AI] >> dist\config.ini
echo model_type = none >> dist\config.ini
echo api_key = >> dist\config.ini
echo base_url = https://api.openai.com/v1 >> dist\config.ini
echo model = gpt-3.5-turbo >> dist\config.ini
:: Create a README.txt in the dist folder :: Create a README.txt in the dist folder
echo Creating README.txt in dist folder... echo Creating README.txt in dist folder...
echo LLMClipboard v0.1.6 > dist\README.txt echo LLMClipboard v0.1.6 > dist\README.txt
@ -29,7 +41,9 @@ if exist dist\LLMClipboard.exe (
echo 3. Double-click the system tray icon to open the main window >> dist\README.txt echo 3. Double-click the system tray icon to open the main window >> dist\README.txt
echo 4. Right-click the system tray icon to access the menu >> dist\README.txt echo 4. Right-click the system tray icon to access the menu >> dist\README.txt
echo. >> dist\README.txt echo. >> dist\README.txt
echo If you encounter any issues, please check error_log.txt and config_error.txt files >> dist\README.txt echo Configuration: >> dist\README.txt
echo - Settings are stored in config.ini in the same folder as the executable >> dist\README.txt
echo - If you encounter any issues, please check error_log.txt and config_error.txt files >> dist\README.txt
) else ( ) else (
echo Build failed, please check error messages. echo Build failed, please check error messages.
) )

View File

@ -0,0 +1,30 @@
{
"测试": [
"测试1",
"测试2",
"测试3"
],
"工作": [
"会议",
"项目",
"计划",
"报告",
"任务",
"进度"
],
"学习": [
"教程",
"课程",
"学习",
"笔记",
"知识",
"总结"
],
"生活": [
"购物",
"健康",
"旅行",
"美食",
"娱乐"
]
}

View File

@ -0,0 +1,58 @@
"""
创建活动状态图标
"""
from PIL import Image, ImageEnhance
import os
def create_active_icon(input_path, output_path):
"""
基于现有图标创建一个活动状态的图标
活动状态图标将更亮更鲜艳
"""
try:
# 打开原始图标
img = Image.open(input_path)
# 增加亮度
enhancer = ImageEnhance.Brightness(img)
brightened_img = enhancer.enhance(1.3) # 增加30%亮度
# 增加对比度
enhancer = ImageEnhance.Contrast(brightened_img)
contrasted_img = enhancer.enhance(1.2) # 增加20%对比度
# 增加颜色饱和度
enhancer = ImageEnhance.Color(contrasted_img)
colored_img = enhancer.enhance(1.5) # 增加50%饱和度
# 保存活动状态图标
colored_img.save(output_path)
print(f"活动状态图标已创建: {output_path}")
return True
except Exception as e:
print(f"创建活动状态图标失败: {e}")
return False
if __name__ == "__main__":
# 确定图标路径
script_dir = os.path.dirname(os.path.abspath(__file__))
resources_dir = os.path.join(script_dir, "resources")
# 确保资源目录存在
if not os.path.exists(resources_dir):
os.makedirs(resources_dir)
print(f"创建资源目录: {resources_dir}")
# 图标路径
icon_path = os.path.join(resources_dir, "icon.png")
active_icon_path = os.path.join(resources_dir, "icon_active.png")
# 检查原始图标是否存在
if not os.path.exists(icon_path):
print(f"错误: 原始图标不存在: {icon_path}")
else:
# 创建活动状态图标
if create_active_icon(icon_path, active_icon_path):
print("成功创建活动状态图标")
else:
print("创建活动状态图标失败")

View File

@ -35,39 +35,185 @@ class StreamToLogger:
class TextCaptureService: class TextCaptureService:
def __init__(self): def __init__(self):
# 设置日志记录
self.setup_logging()
# 加载配置
self.load_config() self.load_config()
self.running = True self.running = True
self.last_right_click_time = 0 self.last_right_click_time = 0
self.setup_logging()
self._mouse_listener = None self._mouse_listener = None
self._keyboard_listener = None self._keyboard_listener = None
# 加载AI配置 # 加载AI配置
self.ai_config = self.load_ai_config() self.ai_config = self.load_ai_config()
self.doc_processor = DocumentProcessor(ai_config=self.ai_config)
# 确定配置文件路径
if getattr(sys, 'frozen', False):
# 打包环境 - 使用可执行文件所在目录
config_dir = os.path.dirname(sys.executable)
else:
# 开发环境 - 使用项目根目录
config_dir = os.path.dirname(os.path.dirname(__file__))
# 初始化文档处理器,传入配置路径
self.doc_processor = DocumentProcessor(
ai_config=self.ai_config,
config_path=os.path.join(config_dir, 'categories.json')
)
self.logger.info(f"文档处理器已初始化,分类配置路径: {os.path.join(config_dir, 'categories.json')}")
def load_config(self): def load_config(self):
self.config = configparser.ConfigParser() try:
config_file = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'config.ini') # 使用 ConfigParser 的 BasicInterpolation 而不是 ExtendedInterpolation
if os.path.exists(config_file): # 这样可以避免 % 符号的问题
self.config.read(config_file) self.config = configparser.ConfigParser(interpolation=configparser.BasicInterpolation())
self.double_click_threshold = self.config.getfloat('Settings', 'double_click_threshold', fallback=0.3)
self.save_location = self.config.get('Settings', 'save_location', # 确定配置文件路径
fallback=os.path.expanduser('~/Documents')) if getattr(sys, 'frozen', False):
# 打包环境 - 使用可执行文件所在目录
config_dir = os.path.dirname(sys.executable)
else:
# 开发环境 - 使用项目根目录
config_dir = os.path.dirname(os.path.dirname(__file__))
self.config_file = os.path.join(config_dir, 'config.ini')
self.logger.info(f"配置文件路径: {self.config_file}")
if os.path.exists(self.config_file):
self.config.read(self.config_file, encoding='utf-8')
self.logger.info(f"成功读取配置文件")
else:
self.logger.warning(f"配置文件不存在,将使用默认配置")
# 使用默认配置
if not self.config.has_section('Settings'):
self.config.add_section('Settings')
self.config.set('Settings', 'double_click_threshold', '0.3')
self.config.set('Settings', 'save_location', os.path.expanduser('~/Documents'))
# 保存默认配置
try:
with open(self.config_file, 'w', encoding='utf-8') as f:
self.config.write(f)
self.logger.info(f"已创建默认配置文件: {self.config_file}")
except Exception as save_error:
self.logger.error(f"创建默认配置文件失败: {save_error}")
# 获取设置值,处理可能的插值语法错误
try:
self.double_click_threshold = self.config.getfloat('Settings', 'double_click_threshold', fallback=0.3)
except (configparser.InterpolationError, ValueError) as e:
self.logger.error(f"读取double_click_threshold设置时出错: {e}")
self.double_click_threshold = 0.3
try:
save_location = self.config.get('Settings', 'save_location', fallback=os.path.expanduser('~/Documents'))
# 检查路径是否存在,如果不存在则使用默认路径
if not os.path.exists(save_location):
self.logger.warning(f"保存路径不存在: {save_location},将使用默认路径")
save_location = os.path.expanduser('~/Documents')
self.save_location = save_location
except (configparser.InterpolationError, ValueError) as e:
self.logger.error(f"读取save_location设置时出错: {e}")
self.save_location = os.path.expanduser('~/Documents')
except Exception as e:
self.logger.error(f"加载配置时出错: {e}")
import traceback
traceback.print_exc()
# 使用默认值
self.double_click_threshold = 0.3
self.save_location = os.path.expanduser('~/Documents')
def load_ai_config(self): def load_ai_config(self):
"""加载AI配置""" try:
ai_config = {} if not self.config.has_section('AI'):
if self.config.has_section('AI'): self.config.add_section('AI')
for key, value in self.config.items('AI'): self.config.set('AI', 'model_type', 'none')
ai_config[key] = value self.config.set('AI', 'api_key', '')
self.config.set('AI', 'base_url', 'https://api.openai.com/v1')
self.config.set('AI', 'model', 'gpt-3.5-turbo')
# 保存默认AI配置
try:
with open(self.config_file, 'w', encoding='utf-8') as f:
self.config.write(f)
except Exception as e:
self.logger.error(f"保存默认AI配置失败: {e}")
# 读取AI配置处理可能的插值语法错误
try:
self.ai_model_type = self.config.get('AI', 'model_type', fallback='none')
except configparser.InterpolationError as e:
self.logger.error(f"读取model_type设置时出错: {e}")
self.ai_model_type = 'none'
try:
self.ai_api_key = self.config.get('AI', 'api_key', fallback='')
except configparser.InterpolationError as e:
self.logger.error(f"读取api_key设置时出错: {e}")
self.ai_api_key = ''
try:
self.ai_base_url = self.config.get('AI', 'base_url', fallback='https://api.openai.com/v1')
except configparser.InterpolationError as e:
self.logger.error(f"读取base_url设置时出错: {e}")
self.ai_base_url = 'https://api.openai.com/v1'
try:
self.ai_model = self.config.get('AI', 'model', fallback='gpt-3.5-turbo')
except configparser.InterpolationError as e:
self.logger.error(f"读取model设置时出错: {e}")
self.ai_model = 'gpt-3.5-turbo'
self.logger.info(f"AI配置已加载: 模型类型={self.ai_model_type}, 模型={self.ai_model}")
except Exception as e:
self.logger.error(f"加载AI配置时出错: {e}")
import traceback
traceback.print_exc()
# 使用默认值
self.ai_model_type = 'none'
self.ai_api_key = ''
self.ai_base_url = 'https://api.openai.com/v1'
self.ai_model = 'gpt-3.5-turbo'
ai_config = {
'model_type': self.ai_model_type,
'api_key': self.ai_api_key,
'base_url': self.ai_base_url,
'model': self.ai_model
}
return ai_config return ai_config
def update_ai_config(self, new_config): def update_ai_config(self, new_config):
"""更新AI配置""" """更新AI配置"""
self.ai_config = new_config try:
if hasattr(self, 'doc_processor'): if not self.config.has_section('AI'):
self.doc_processor.update_ai_config(new_config) self.config.add_section('AI')
# 更新配置
for key, value in new_config.items():
self.config.set('AI', key, str(value))
# 保存到文件
with open(self.config_file, 'w', encoding='utf-8') as f:
self.config.write(f)
# 更新实例变量
self.ai_model_type = new_config.get('model_type', 'none')
self.ai_api_key = new_config.get('api_key', '')
self.ai_base_url = new_config.get('base_url', 'https://api.openai.com/v1')
self.ai_model = new_config.get('model', 'gpt-3.5-turbo')
self.logger.info(f"AI配置已更新: 模型类型={self.ai_model_type}, 模型={self.ai_model}")
return True
except Exception as e:
self.logger.error(f"更新AI配置时出错: {e}")
import traceback
traceback.print_exc()
return False
def setup_logging(self): def setup_logging(self):
try: try:

View File

@ -33,7 +33,20 @@ logger.addHandler(console_handler)
class DocumentProcessor: class DocumentProcessor:
def __init__(self, config_path=None, ai_config=None): def __init__(self, config_path=None, ai_config=None):
self.config_path = config_path or os.path.join(os.path.dirname(__file__), 'categories.json') # 确定配置文件路径
if config_path:
self.config_path = config_path
else:
# 如果没有提供配置路径,使用默认路径
if getattr(sys, 'frozen', False):
# 打包环境 - 使用可执行文件所在目录
config_dir = os.path.dirname(sys.executable)
else:
# 开发环境 - 使用项目根目录
config_dir = os.path.dirname(os.path.dirname(__file__))
self.config_path = os.path.join(config_dir, 'categories.json')
logger.info(f"使用分类配置文件路径: {self.config_path}")
self.load_categories() self.load_categories()
# 初始化AI处理器 # 初始化AI处理器
@ -45,10 +58,29 @@ class DocumentProcessor:
def load_categories(self): def load_categories(self):
"""加载预定义的分类规则""" """加载预定义的分类规则"""
if os.path.exists(self.config_path): try:
with open(self.config_path, 'r', encoding='utf-8') as f: logger.info(f"尝试从 {self.config_path} 加载分类规则")
self.categories = json.load(f) if os.path.exists(self.config_path):
else: with open(self.config_path, 'r', encoding='utf-8') as f:
self.categories = json.load(f)
logger.info(f"成功加载分类规则,包含 {len(self.categories)} 个分类")
for category, keywords in self.categories.items():
logger.debug(f"分类 '{category}' 包含 {len(keywords)} 个关键词")
else:
logger.warning(f"分类规则文件 {self.config_path} 不存在,使用默认分类")
self.categories = {
"技术": ["编程", "开发", "代码", "框架", "算法", "数据库", "API"],
"学习": ["教程", "课程", "学习", "笔记", "知识", "总结"],
"工作": ["会议", "项目", "计划", "报告", "任务", "进度"],
"想法": ["想法", "创意", "思考", "灵感", "观点", "建议"],
"资源": ["工具", "资源", "链接", "参考", "文档", "书籍"]
}
self._save_categories()
except Exception as e:
logger.error(f"加载分类规则时出错: {e}")
import traceback
traceback.print_exc()
# 使用默认分类
self.categories = { self.categories = {
"技术": ["编程", "开发", "代码", "框架", "算法", "数据库", "API"], "技术": ["编程", "开发", "代码", "框架", "算法", "数据库", "API"],
"学习": ["教程", "课程", "学习", "笔记", "知识", "总结"], "学习": ["教程", "课程", "学习", "笔记", "知识", "总结"],
@ -56,50 +88,31 @@ class DocumentProcessor:
"想法": ["想法", "创意", "思考", "灵感", "观点", "建议"], "想法": ["想法", "创意", "思考", "灵感", "观点", "建议"],
"资源": ["工具", "资源", "链接", "参考", "文档", "书籍"] "资源": ["工具", "资源", "链接", "参考", "文档", "书籍"]
} }
self._save_categories()
def _save_categories(self): def _save_categories(self):
"""保存分类规则""" """保存分类规则"""
os.makedirs(os.path.dirname(self.config_path), exist_ok=True) try:
with open(self.config_path, 'w', encoding='utf-8') as f: logger.info(f"保存分类规则到 {self.config_path}")
json.dump(self.categories, f, ensure_ascii=False, indent=2) os.makedirs(os.path.dirname(self.config_path), exist_ok=True)
with open(self.config_path, 'w', encoding='utf-8') as f:
json.dump(self.categories, f, ensure_ascii=False, indent=2)
logger.info(f"分类规则已保存,包含 {len(self.categories)} 个分类")
except Exception as e:
logger.error(f"保存分类规则时出错: {e}")
import traceback
traceback.print_exc()
def update_categories(self, new_categories): def update_categories(self, new_categories):
"""更新分类规则""" """更新分类规则"""
self.categories = new_categories try:
self._save_categories() logger.info(f"更新分类规则,新分类包含 {len(new_categories)} 个分类")
logger.info("分类规则已更新") self.categories = new_categories
self._save_categories()
def clean_content(self, content): logger.info("分类规则已更新")
"""清理内容,移除多余的换行符和空白""" except Exception as e:
if not content or content.isspace(): logger.error(f"更新分类规则时出错: {e}")
logger.debug("收到空内容或纯空白内容") import traceback
return "" traceback.print_exc()
logger.debug("原始内容长度: %d", len(content))
logger.debug("原始内容前100个字符: %s", content[:100])
logger.debug("原始内容包含的换行符数量: %d", content.count('\n'))
# 移除文件名
content = re.sub(r'captured_text_\d{8}_\d{6}', '', content)
logger.debug("已移除文件名")
logger.debug("移除文件名后的内容长度: %d", len(content))
# 清理内容,保持原有的换行格式
lines = content.splitlines()
cleaned_lines = []
for line in lines:
line = line.strip()
if line or cleaned_lines: # 如果当前行非空或已经有内容,保留这一行
cleaned_lines.append(line)
# 使用原始换行符重新组合内容
content = '\n'.join(cleaned_lines)
logger.debug("清理后内容长度: %d", len(content))
logger.debug("清理后内容包含的换行符数量: %d", content.count('\n'))
logger.debug("清理后内容的前100个字符: %s", content[:100])
return content
def update_ai_config(self, ai_config): def update_ai_config(self, ai_config):
"""更新AI配置""" """更新AI配置"""
@ -301,3 +314,34 @@ category: {category}
'tags': tags, 'tags': tags,
'category': category 'category': category
} }
def clean_content(self, content):
"""清理内容,移除多余的换行符和空白"""
if not content or content.isspace():
logger.debug("收到空内容或纯空白内容")
return ""
logger.debug("原始内容长度: %d", len(content))
logger.debug("原始内容前100个字符: %s", content[:100])
logger.debug("原始内容包含的换行符数量: %d", content.count('\n'))
# 移除文件名
content = re.sub(r'captured_text_\d{8}_\d{6}', '', content)
logger.debug("已移除文件名")
logger.debug("移除文件名后的内容长度: %d", len(content))
# 清理内容,保持原有的换行格式
lines = content.splitlines()
cleaned_lines = []
for line in lines:
line = line.strip()
if line or cleaned_lines: # 如果当前行非空或已经有内容,保留这一行
cleaned_lines.append(line)
# 使用原始换行符重新组合内容
content = '\n'.join(cleaned_lines)
logger.debug("清理后内容长度: %d", len(content))
logger.debug("清理后内容包含的换行符数量: %d", content.count('\n'))
logger.debug("清理后内容的前100个字符: %s", content[:100])
return content

View File

@ -1,15 +1,16 @@
import sys import sys
import os import os
import time import time
from configparser import ConfigParser from configparser import ConfigParser, BasicInterpolation
import darkdetect import darkdetect
from PyQt6.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, from PyQt6.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout,
QHBoxLayout, QPushButton, QLabel, QFileDialog, QHBoxLayout, QPushButton, QLabel, QFileDialog,
QSystemTrayIcon, QMenu, QSpinBox, QStyle, QLineEdit, QListWidget, QLineEdit, QSpinBox, QGroupBox,
QDialog, QTabWidget, QGroupBox, QComboBox, QCheckBox, QSystemTrayIcon, QMenu, QStyle, QMessageBox, QDialog, QTabWidget, QComboBox, QCheckBox,
QDialogButtonBox, QMessageBox, QListWidget) QDialogButtonBox)
from PyQt6.QtCore import Qt from PyQt6.QtCore import Qt
from PyQt6.QtGui import QIcon, QAction from PyQt6.QtGui import QIcon, QAction
from llmclipboard.category_manager import CategoryManagerDialog
class NotificationDialog(QDialog): class NotificationDialog(QDialog):
"""自定义通知对话框类""" """自定义通知对话框类"""
@ -180,7 +181,13 @@ class AISettingsDialog(QDialog):
# 按钮 # 按钮
button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Save | QDialogButtonBox.StandardButton.Cancel) button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Save | QDialogButtonBox.StandardButton.Cancel)
button_box.accepted.connect(self.accept)
# 添加调试信息
def on_accepted():
print("AI设置对话框保存按钮被点击")
self.accept()
button_box.accepted.connect(on_accepted)
button_box.rejected.connect(self.reject) button_box.rejected.connect(self.reject)
layout.addWidget(button_box) layout.addWidget(button_box)
@ -317,8 +324,19 @@ class MainWindow(QMainWindow):
self.service = llmclipboard.app.TextCaptureService() self.service = llmclipboard.app.TextCaptureService()
# 加载配置 # 加载配置
self.config = ConfigParser() self.config = ConfigParser(interpolation=BasicInterpolation())
self.config_file = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'config.ini')
# 确定配置文件路径
if getattr(sys, 'frozen', False):
# 打包环境 - 使用可执行文件所在目录
config_dir = os.path.dirname(sys.executable)
else:
# 开发环境 - 使用项目根目录
config_dir = os.path.dirname(os.path.dirname(__file__))
self.config_file = os.path.join(config_dir, 'config.ini')
print(f"GUI配置文件路径: {self.config_file}")
self.load_config() self.load_config()
# 创建主窗口部件 # 创建主窗口部件
@ -450,8 +468,10 @@ class MainWindow(QMainWindow):
def load_config(self): def load_config(self):
try: try:
if os.path.exists(self.config_file): if os.path.exists(self.config_file):
self.config.read(self.config_file) self.config.read(self.config_file, encoding='utf-8')
print(f"成功读取GUI配置文件: {self.config_file}")
else: else:
print(f"GUI配置文件不存在将使用默认配置: {self.config_file}")
self.config['Settings'] = { self.config['Settings'] = {
'save_location': os.path.expanduser('~/Documents'), 'save_location': os.path.expanduser('~/Documents'),
'double_click_threshold': '0.3' 'double_click_threshold': '0.3'
@ -470,11 +490,20 @@ class MainWindow(QMainWindow):
# 更新UI组件如果存在 # 更新UI组件如果存在
if hasattr(self, 'save_path_edit'): if hasattr(self, 'save_path_edit'):
self.save_path_edit.setText(self.config.get('Settings', 'save_location', fallback=os.path.expanduser('~/Documents'))) try:
save_location = self.config.get('Settings', 'save_location', fallback=os.path.expanduser('~/Documents'))
self.save_path_edit.setText(save_location)
except (configparser.InterpolationError, ValueError) as e:
print(f"读取save_location设置时出错: {e}")
self.save_path_edit.setText(os.path.expanduser('~/Documents'))
if hasattr(self, 'threshold_spin'): if hasattr(self, 'threshold_spin'):
threshold = int(float(self.config.get('Settings', 'double_click_threshold', fallback='0.3')) * 1000) try:
self.threshold_spin.setValue(threshold) threshold = int(float(self.config.get('Settings', 'double_click_threshold', fallback='0.3')) * 1000)
self.threshold_spin.setValue(threshold)
except (configparser.InterpolationError, ValueError) as e:
print(f"读取double_click_threshold设置时出错: {e}")
self.threshold_spin.setValue(300) # 默认值0.3秒,转换为毫秒
except Exception as e: except Exception as e:
import traceback import traceback
@ -489,31 +518,62 @@ class MainWindow(QMainWindow):
def save_config(self): def save_config(self):
try: try:
# 检查属性是否存在 print("开始保存配置...")
if hasattr(self, 'save_path_edit') and hasattr(self, 'threshold_spin'): # 确保配置目录存在
config_dir = os.path.dirname(self.config_file)
if not os.path.exists(config_dir):
os.makedirs(config_dir)
# 更新配置对象
if hasattr(self, 'save_path_edit'):
self.config['Settings']['save_location'] = self.save_path_edit.text() self.config['Settings']['save_location'] = self.save_path_edit.text()
print(f"保存路径设置为: {self.save_path_edit.text()}")
if hasattr(self, 'threshold_spin'):
self.config['Settings']['double_click_threshold'] = str(self.threshold_spin.value() / 1000) self.config['Settings']['double_click_threshold'] = str(self.threshold_spin.value() / 1000)
else: print(f"双击阈值设置为: {self.threshold_spin.value() / 1000}")
# 如果属性不存在,使用默认值
if not self.config.has_section('Settings'): # 写入配置文件
self.config['Settings'] = {} with open(self.config_file, 'w', encoding='utf-8') as f:
self.config['Settings'].setdefault('save_location', os.path.expanduser('~/Documents'))
self.config['Settings'].setdefault('double_click_threshold', '0.3')
print("警告: GUI组件未初始化使用默认配置值")
with open(self.config_file, 'w') as f:
self.config.write(f) self.config.write(f)
# 重新加载服务配置 print(f"配置已保存到: {self.config_file}")
if hasattr(self.service, 'load_config'): # 显示保存成功的提示
self.service.load_config() print("准备显示保存成功消息...")
if hasattr(self, 'status_label'): # 直接调用show_tray_message方法
self.show_message("设置已保存") self.show_tray_message(
"LLMClipboard",
"配置已保存成功",
"info",
2000
)
# 更新状态栏
self.status_label.setStyleSheet("color: green; font-weight: bold;")
self.status_label.setText("配置已保存成功")
# 5秒后清除消息
from PyQt6.QtCore import QTimer
QTimer.singleShot(5000, lambda: self.status_label.setText("就绪"))
print("已显示保存成功消息")
# 更新服务的配置
if hasattr(self, 'service'):
try:
print("准备更新服务配置...")
self.service.load_config()
print("服务配置已更新")
except Exception as service_error:
print(f"更新服务配置时出错: {service_error}")
except Exception as e: except Exception as e:
import traceback import traceback
print(f"保存配置时出错: {e}") print(f"保存配置时出错: {e}")
traceback.print_exc() traceback.print_exc()
# 显示保存失败的提示
self.show_message(f"保存配置失败: {e}", error=True)
# 在打包环境中,确保错误信息被记录 # 在打包环境中,确保错误信息被记录
if getattr(sys, 'frozen', False): if getattr(sys, 'frozen', False):
error_log_path = os.path.join(os.path.dirname(sys.executable), 'config_error.txt') error_log_path = os.path.join(os.path.dirname(sys.executable), 'config_error.txt')
@ -528,39 +588,103 @@ class MainWindow(QMainWindow):
def show_ai_settings(self): def show_ai_settings(self):
"""显示AI设置对话框""" """显示AI设置对话框"""
dialog = AISettingsDialog(self) try:
dialog = AISettingsDialog(self)
# 设置当前配置
ai_config = {}
if self.config.has_section('AI'):
for key, value in self.config.items('AI'):
ai_config[key] = value
dialog.set_config(ai_config)
if dialog.exec():
# 获取新配置
new_config = dialog.get_config()
# 保存到配置文件 # 设置当前配置
if not self.config.has_section('AI'): ai_config = {}
self.config.add_section('AI') if self.config.has_section('AI'):
try:
for key, value in new_config.items(): for key in ['model_type', 'api_key', 'base_url', 'model']:
self.config.set('AI', key, str(value)) try:
value = self.config.get('AI', key, fallback='')
self.save_config() ai_config[key] = value
except configparser.InterpolationError as e:
print(f"读取AI配置项 {key} 时出错: {e}")
# 设置默认值
if key == 'model_type':
ai_config[key] = 'none'
elif key == 'api_key':
ai_config[key] = ''
elif key == 'base_url':
ai_config[key] = 'https://api.openai.com/v1'
elif key == 'model':
ai_config[key] = 'gpt-3.5-turbo'
except Exception as e:
print(f"读取AI配置时出错: {e}")
import traceback
traceback.print_exc()
dialog.set_config(ai_config)
# 更新服务的AI配置 if dialog.exec():
if hasattr(self.service, 'update_ai_config'): print("AI设置对话框已接受")
self.service.update_ai_config(new_config) # 获取新配置
new_config = dialog.get_config()
# 保存到配置文件
if not self.config.has_section('AI'):
self.config.add_section('AI')
for key, value in new_config.items():
self.config.set('AI', key, str(value))
self.save_config()
# 更新服务的AI配置
if hasattr(self.service, 'update_ai_config'):
self.service.update_ai_config(new_config)
# 直接调用show_tray_message方法
self.show_tray_message(
"LLMClipboard",
"AI设置已保存并应用",
"info",
2000
)
# 更新状态栏
self.status_label.setStyleSheet("color: green; font-weight: bold;")
self.status_label.setText("AI设置已保存并应用")
# 5秒后清除消息
from PyQt6.QtCore import QTimer
QTimer.singleShot(5000, lambda: self.status_label.setText("就绪"))
except Exception as e:
import traceback
print(f"显示AI设置对话框失败: {e}")
traceback.print_exc()
self.show_message(f"显示AI设置对话框失败: {str(e)}", error=True)
def show_category_manager(self): def show_category_manager(self):
"""显示分类管理器对话框""" """显示分类管理器对话框"""
# 获取当前分类 # 获取当前分类
categories = {} categories = {}
if hasattr(self.service, 'doc_processor'): try:
categories = self.service.doc_processor.categories if hasattr(self.service, 'doc_processor'):
categories = self.service.doc_processor.categories
else:
# 如果doc_processor不存在尝试从文件加载分类
import json
import os
categories_file = os.path.join(os.path.dirname(__file__), 'categories.json')
if os.path.exists(categories_file):
with open(categories_file, 'r', encoding='utf-8') as f:
categories = json.load(f)
else:
# 使用默认分类
categories = {
"技术": ["编程", "开发", "代码", "框架", "算法", "数据库", "API"],
"学习": ["教程", "课程", "学习", "笔记", "知识", "总结"],
"工作": ["会议", "项目", "计划", "报告", "任务", "进度"],
"想法": ["想法", "创意", "思考", "灵感", "观点", "建议"],
"资源": ["工具", "资源", "链接", "参考", "文档", "书籍"]
}
except Exception as e:
import traceback
traceback.print_exc()
self.show_message(f"加载分类失败: {e}", error=True)
return
# 创建分类管理器对话框 # 创建分类管理器对话框
dialog = CategoryManagerDialog(categories, self) dialog = CategoryManagerDialog(categories, self)
@ -573,10 +697,73 @@ class MainWindow(QMainWindow):
def update_categories(self, new_categories): def update_categories(self, new_categories):
"""更新分类""" """更新分类"""
if hasattr(self.service, 'doc_processor'): try:
self.service.doc_processor.update_categories(new_categories) print(f"更新分类: {new_categories}")
self.show_message("分类已更新")
# 更新doc_processor的分类
if hasattr(self.service, 'doc_processor'):
print("使用doc_processor更新分类")
self.service.doc_processor.update_categories(new_categories)
print("分类更新成功")
else:
# 如果doc_processor不存在直接保存到文件
print("直接保存分类到文件")
import json
import os
# 确保使用正确的配置文件路径
if getattr(sys, 'frozen', False):
# 打包环境 - 使用可执行文件所在目录
config_dir = os.path.dirname(sys.executable)
else:
# 开发环境 - 使用项目根目录
config_dir = os.path.dirname(os.path.dirname(__file__))
categories_file = os.path.join(config_dir, 'categories.json')
print(f"保存分类到文件: {categories_file}")
os.makedirs(os.path.dirname(categories_file), exist_ok=True)
with open(categories_file, 'w', encoding='utf-8') as f:
json.dump(new_categories, f, ensure_ascii=False, indent=2)
print(f"分类已保存到文件: {categories_file}")
# 尝试重新加载服务配置
try:
print("尝试重新加载服务配置")
self.service.load_config()
print("服务配置重新加载成功")
except Exception as e:
print(f"重新加载服务配置失败: {e}")
import traceback
traceback.print_exc()
# 显示成功消息
self.show_tray_message(
"LLMClipboard",
"分类已更新并保存",
"info",
2000
)
# 更新状态栏
self.status_label.setStyleSheet("color: green; font-weight: bold;")
self.status_label.setText("分类已更新")
# 5秒后清除消息
from PyQt6.QtCore import QTimer
QTimer.singleShot(5000, lambda: self.status_label.setText("就绪"))
except Exception as e:
print(f"更新分类失败: {e}")
import traceback
traceback.print_exc()
self.show_tray_message(
"LLMClipboard",
f"更新分类失败: {e}",
"error",
3000
)
def add_recent_file(self, file_path): def add_recent_file(self, file_path):
"""添加最近保存的文件到列表""" """添加最近保存的文件到列表"""
# 检查是否已存在 # 检查是否已存在
@ -624,11 +811,18 @@ class MainWindow(QMainWindow):
if hasattr(self, 'status_action') and self.status_action is not None: if hasattr(self, 'status_action') and self.status_action is not None:
self.status_action.setText("状态: 正在运行") self.status_action.setText("状态: 正在运行")
if hasattr(self, 'tray_icon') and hasattr(self, 'active_icon') and self.tray_icon is not None: if hasattr(self, 'tray_icon') and hasattr(self, 'active_icon') and self.tray_icon is not None:
print("设置活动状态托盘图标")
self.tray_icon.setIcon(self.active_icon) self.tray_icon.setIcon(self.active_icon)
# 确保托盘图标可见 # 确保托盘图标可见
if not self.tray_icon.isVisible(): if not self.tray_icon.isVisible():
print("确保托盘图标可见") print("确保托盘图标可见")
self.tray_icon.show() self.tray_icon.show()
# 强制更新图标
self.tray_icon.setToolTip("LLMClipboard - 正在运行")
# 强制刷新图标
self.tray_icon.hide()
self.tray_icon.show()
QApplication.processEvents()
# 保存配置并启动服务 # 保存配置并启动服务
try: try:
@ -664,11 +858,21 @@ class MainWindow(QMainWindow):
if hasattr(self, 'status_action') and self.status_action is not None: if hasattr(self, 'status_action') and self.status_action is not None:
self.status_action.setText("状态: 已停止") self.status_action.setText("状态: 已停止")
if hasattr(self, 'tray_icon') and hasattr(self, 'inactive_icon') and self.tray_icon is not None: if hasattr(self, 'tray_icon') and hasattr(self, 'inactive_icon') and self.tray_icon is not None:
print("设置非活动状态托盘图标")
self.tray_icon.setIcon(self.inactive_icon) self.tray_icon.setIcon(self.inactive_icon)
# 确保托盘图标可见 # 确保托盘图标可见
if not self.tray_icon.isVisible(): if not self.tray_icon.isVisible():
print("确保托盘图标可见") print("确保托盘图标可见")
self.tray_icon.show() self.tray_icon.show()
# 强制更新图标
self.tray_icon.setToolTip("LLMClipboard - 已停止")
# 强制刷新图标
try:
self.tray_icon.hide()
self.tray_icon.show()
except Exception as refresh_error:
print(f"刷新托盘图标失败: {str(refresh_error)}")
QApplication.processEvents()
# 停止服务 # 停止服务
try: try:
@ -700,6 +904,9 @@ class MainWindow(QMainWindow):
self.tray_icon.show() self.tray_icon.show()
except Exception as icon_error: except Exception as icon_error:
print(f"恢复托盘图标可见性失败: {str(icon_error)}") print(f"恢复托盘图标可见性失败: {str(icon_error)}")
# 强制更新图标
self.tray_icon.setToolTip("LLMClipboard - 已停止")
QApplication.processEvents()
# 显示错误消息 # 显示错误消息
self.show_message(f"切换监听状态失败: {str(e)}", error=True) self.show_message(f"切换监听状态失败: {str(e)}", error=True)
@ -783,11 +990,20 @@ class MainWindow(QMainWindow):
# 设置托盘图标 # 设置托盘图标
print("设置托盘图标") print("设置托盘图标")
icon_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "resources", "icon.png") icon_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), "resources")
if os.path.exists(icon_path): active_icon_path = os.path.join(icon_dir, "icon_active.png")
print(f"使用自定义图标: {icon_path}") inactive_icon_path = os.path.join(icon_dir, "icon.png")
self.active_icon = QIcon(icon_path)
self.inactive_icon = QIcon(icon_path) # 检查自定义图标是否存在
if os.path.exists(active_icon_path) and os.path.exists(inactive_icon_path):
print(f"使用自定义图标: 活动={active_icon_path}, 非活动={inactive_icon_path}")
self.active_icon = QIcon(active_icon_path)
self.inactive_icon = QIcon(inactive_icon_path)
elif os.path.exists(inactive_icon_path):
# 只有一个图标文件,但仍使用不同的系统图标区分状态
print(f"使用自定义非活动图标和系统活动图标")
self.inactive_icon = QIcon(inactive_icon_path)
self.active_icon = self.style().standardIcon(QStyle.StandardPixmap.SP_DialogApplyButton)
else: else:
# 使用系统默认图标 # 使用系统默认图标
print("使用系统默认图标") print("使用系统默认图标")
@ -1079,49 +1295,23 @@ class MainWindow(QMainWindow):
if error: if error:
print(f"错误: {message}") print(f"错误: {message}")
self.status_label.setStyleSheet("color: red; font-weight: bold;") self.status_label.setStyleSheet("color: red; font-weight: bold;")
if hasattr(self, 'tray_icon') and self.tray_icon is not None: # 使用改进的show_tray_message方法显示错误消息
try: self.show_tray_message(
self.tray_icon.showMessage( "LLMClipboard - 错误",
"LLMClipboard - 错误", message,
message, "error",
QSystemTrayIcon.MessageIcon.Critical, 3000
3000 )
)
except Exception as e:
print(f"显示错误消息失败: {str(e)}")
# 尝试使用旧版API
try:
self.tray_icon.showMessage(
"LLMClipboard - 错误",
message,
QSystemTrayIcon.Critical,
3000
)
except Exception as e2:
print(f"使用旧版API显示错误消息也失败: {str(e2)}")
else: else:
print(f"消息: {message}") print(f"消息: {message}")
self.status_label.setStyleSheet("color: green; font-weight: bold;") self.status_label.setStyleSheet("color: green; font-weight: bold;")
if hasattr(self, 'tray_icon') and self.tray_icon is not None: # 使用改进的show_tray_message方法显示信息消息
try: self.show_tray_message(
self.tray_icon.showMessage( "LLMClipboard",
"LLMClipboard", message,
message, "info",
QSystemTrayIcon.MessageIcon.Information, 2000
2000 )
)
except Exception as e:
print(f"显示信息消息失败: {str(e)}")
# 尝试使用旧版API
try:
self.tray_icon.showMessage(
"LLMClipboard",
message,
QSystemTrayIcon.Information,
2000
)
except Exception as e2:
print(f"使用旧版API显示信息消息也失败: {str(e2)}")
self.status_label.setText(message) self.status_label.setText(message)
@ -1380,7 +1570,14 @@ class MainWindow(QMainWindow):
# 添加确定和取消按钮 # 添加确定和取消按钮
button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel) button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)
button_box.accepted.connect(dialog.accept)
# 连接确定按钮到保存配置方法
def on_accepted():
print("设置对话框确定按钮被点击")
self.save_config() # 保存配置
dialog.accept() # 关闭对话框
button_box.accepted.connect(on_accepted)
button_box.rejected.connect(dialog.reject) button_box.rejected.connect(dialog.reject)
layout.addWidget(button_box) layout.addWidget(button_box)

Binary file not shown.

After

Width:  |  Height:  |  Size: 862 KiB

View File

@ -0,0 +1,91 @@
"""
测试分类配置的加载和保存功能
"""
import os
import sys
import json
import logging
# 配置日志记录
logging.basicConfig(level=logging.DEBUG,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
# 添加项目根目录到Python路径
project_root = os.path.dirname(os.path.abspath(__file__))
if project_root not in sys.path:
sys.path.append(project_root)
from llmclipboard.document_processor import DocumentProcessor
def test_categories_persistence():
"""测试分类配置的持久化功能"""
logger.info("开始测试分类配置的持久化功能")
# 确定配置文件路径
if getattr(sys, 'frozen', False):
# 打包环境 - 使用可执行文件所在目录
config_dir = os.path.dirname(sys.executable)
else:
# 开发环境 - 使用项目根目录
config_dir = project_root
categories_file = os.path.join(config_dir, 'categories.json')
logger.info(f"分类配置文件路径: {categories_file}")
# 1. 检查配置文件是否存在
if os.path.exists(categories_file):
logger.info(f"分类配置文件已存在: {categories_file}")
with open(categories_file, 'r', encoding='utf-8') as f:
current_categories = json.load(f)
logger.info(f"当前分类: {current_categories}")
else:
logger.info(f"分类配置文件不存在: {categories_file}")
current_categories = {}
# 2. 创建文档处理器实例
doc_processor = DocumentProcessor(config_path=categories_file)
# 3. 获取当前分类
logger.info(f"文档处理器加载的分类: {doc_processor.categories}")
# 4. 修改分类
new_categories = {
"测试": ["测试1", "测试2", "测试3"],
"工作": ["会议", "项目", "计划", "报告", "任务", "进度"],
"学习": ["教程", "课程", "学习", "笔记", "知识", "总结"],
"生活": ["购物", "健康", "旅行", "美食", "娱乐"]
}
# 5. 更新分类
logger.info(f"更新分类: {new_categories}")
doc_processor.update_categories(new_categories)
# 6. 检查配置文件是否已更新
if os.path.exists(categories_file):
with open(categories_file, 'r', encoding='utf-8') as f:
updated_categories = json.load(f)
logger.info(f"更新后的分类: {updated_categories}")
# 验证更新是否成功
if updated_categories == new_categories:
logger.info("分类更新成功!")
else:
logger.error("分类更新失败!")
else:
logger.error(f"分类配置文件不存在: {categories_file}")
# 7. 创建新的文档处理器实例,验证配置是否能被正确加载
new_doc_processor = DocumentProcessor(config_path=categories_file)
logger.info(f"新文档处理器加载的分类: {new_doc_processor.categories}")
# 验证加载是否成功
if new_doc_processor.categories == new_categories:
logger.info("分类加载成功!")
else:
logger.error("分类加载失败!")
return True
if __name__ == "__main__":
test_categories_persistence()

View File

@ -0,0 +1,95 @@
"""
测试LLMClipboard分类配置管理功能
"""
import os
import sys
import json
import logging
from PyQt6.QtWidgets import QApplication
from PyQt6.QtCore import QTimer
# 配置日志记录
logging.basicConfig(level=logging.DEBUG,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
# 添加项目根目录到Python路径
project_root = os.path.dirname(os.path.abspath(__file__))
if project_root not in sys.path:
sys.path.append(project_root)
from llmclipboard.document_processor import DocumentProcessor
from llmclipboard.category_manager import CategoryManagerDialog
from llmclipboard.gui import MainWindow
def test_category_manager_dialog():
"""测试分类管理器对话框"""
logger.info("开始测试分类管理器对话框")
# 创建应用程序实例
app = QApplication(sys.argv)
# 确定配置文件路径
if getattr(sys, 'frozen', False):
# 打包环境 - 使用可执行文件所在目录
config_dir = os.path.dirname(sys.executable)
else:
# 开发环境 - 使用项目根目录
config_dir = project_root
categories_file = os.path.join(config_dir, 'categories.json')
logger.info(f"分类配置文件路径: {categories_file}")
# 加载当前分类
if os.path.exists(categories_file):
logger.info(f"分类配置文件已存在: {categories_file}")
with open(categories_file, 'r', encoding='utf-8') as f:
categories = json.load(f)
logger.info(f"当前分类: {categories}")
else:
logger.info(f"分类配置文件不存在: {categories_file}")
categories = {}
# 创建分类管理器对话框
dialog = CategoryManagerDialog(categories)
# 连接信号
def on_categories_updated(new_categories):
logger.info(f"分类已更新: {new_categories}")
# 保存到文件
with open(categories_file, 'w', encoding='utf-8') as f:
json.dump(new_categories, f, ensure_ascii=False, indent=2)
logger.info(f"分类已保存到文件: {categories_file}")
dialog.categories_updated.connect(on_categories_updated)
# 显示对话框
dialog.show()
# 5秒后自动关闭
QTimer.singleShot(60000, dialog.close)
# 运行应用程序
sys.exit(app.exec())
def test_main_window_category_management():
"""测试主窗口的分类管理功能"""
logger.info("开始测试主窗口的分类管理功能")
# 创建应用程序实例
app = QApplication(sys.argv)
# 创建主窗口
window = MainWindow()
# 显示窗口
window.show()
# 运行应用程序
sys.exit(app.exec())
if __name__ == "__main__":
# 选择要运行的测试
test_category_manager_dialog()
# test_main_window_category_management()