内容管理器 (支持代码片段和Markdown笔记)
项目简介
代码片段助手是一个功能强大的工具,用于管理和组织代码片段和Markdown笔记。该应用提供了简洁直观的界面,支持多种编程语言的代码高亮显示、Markdown实时预览、内容分类管理等功能,帮助用户更高效地管理和检索代码片段和笔记。
功能特性
核心功能
- 代码片段管理:创建、编辑、删除和搜索代码片段
- Markdown笔记支持:完整的Markdown编辑和实时预览功能
- 分类管理:通过分类系统组织不同类型的内容
- 标签系统:使用标签进一步细分和检索内容
- 搜索功能:快速查找代码片段和笔记
Markdown功能
- 支持标题、段落、加粗、斜体等基本格式
- 支持列表
- 支持代码块
- 实时预览Markdown渲染效果
用户体验
安装说明
1. 确保安装了Python
请确保您的系统已安装Python 3.7或更高版本。您可以通过以下命令检查Python版本:
python --version
2. 安装依赖
本项目主要依赖PyQt6,您可以使用pip安装:
pip install PyQt6
3. 克隆或下载项目
直接下载项目文件到您的本地目录即可。
4. 运行应用
进入项目目录,执行以下命令运行应用:
python main.py
在Windows环境下,也可以直接双击代码片段助.py文件运行。
使用指南
创建新的代码片段或笔记
- 点击界面底部的"新建"按钮
- 填写标题、选择类型(代码片段或Markdown笔记)
- 选择分类并添加标签(可选)
- 添加描述信息(可选)
- 在内容编辑区编写代码或Markdown内容
- 点击"保存"按钮保存内容
编辑现有内容
- 在左侧列表中选择要编辑的内容
- 修改相关信息或内容
- 点击"保存"按钮更新内容
Markdown编辑技巧
- 使用
#标记标题(最多支持6级标题)
- 使用
**文本**添加加粗文本
- 使用
*文本*添加斜体文本
字体缩放功能
- 在编辑区域按住Ctrl键并向上滚动鼠标滚轮放大字体
- 在编辑区域按住Ctrl键并向下滚动鼠标滚轮缩小字体
项目结构
-
main.py - 主程序入口
-
models.py - 数据模型
-
views.py - UI组件
-
controllers.py - 控制器逻辑
-
utils.py - 工具函数
-
highlighters.py - 语法高亮
-
dialogs.py - 对话框
-
snippets.json -存储数据的JSON文件
主要模块说明
controllers.py
控制器模块,负责连接模型和视图,处理用户交互和业务逻辑。包含保存、加载、删除代码片段和笔记的核心功能。
models.py
数据模型模块,定义了代码片段和笔记的数据结构,以及数据的存储和检索方法。
views.py
视图模块,定义了应用的UI组件,包括自定义的文本编辑框(支持字体缩放功能)等。
utils.py
工具模块,包含Markdown渲染器等辅助功能,支持将Markdown文本转换为HTML用于预览。
highlighters.py
语法高亮模块,为不同编程语言提供代码高亮显示功能。
数据存储
应用使用JSON文件(snippets.json)存储所有代码片段和笔记数据。数据以结构化的格式保存,便于加载和修改。
常见问题
Q: 如何添加新的编程语言支持?
A: 可以在highlighters.py中添加新的语法高亮规则。
Q: 保存时提示"标题和内容不能为空"怎么办?
A: 请确保您填写了标题,并且在对应类型的编辑区(代码编辑或Markdown编辑)中输入了内容。
Q: Markdown渲染格式不正确怎么办?
A: 当前支持基本的Markdown语法,请确保使用标准的Markdown格式进行编写。
[Python] 纯文本查看 复制代码
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from PyQt6.QtWidgets import (
QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QTreeWidget, QTreeWidgetItem,
QLineEdit, QPushButton, QFormLayout, QStatusBar, QComboBox, QMenu,
QMessageBox, QTabWidget, QTextEdit, QSplitter, QMenuBar, QToolBar, QLabel
)
from PyQt6.QtGui import QAction, QFont
from PyQt6.QtCore import Qt
from models import ContentManager
from highlighters import PythonSyntaxHighlighter
from utils import MarkdownRenderer
from dialogs import CategoryDialog
from views import ZoomableTextEdit, EnhancedTextEdit # 修改导入
class MainWindow(QMainWindow):
"""主应用程序窗口,包含所有UI控件和交互逻辑"""
def __init__(self):
super().__init__()
self.content_manager = ContentManager()
self.current_content_id = None
self.setWindowTitle("内容管理器 (支持代码片段和Markdown笔记)")
self.setGeometry(100, 100, 1200, 800)
self.init_ui()
self.populate_category_tree()
self.update_category_combo() # 添加这一行,确保分类下拉框初始化时有选项
def init_ui(self):
# 创建菜单栏
menubar = self.menuBar()
# 文件菜单
file_menu = menubar.addMenu('文件')
new_action = QAction('新建', self)
new_action.setShortcut('Ctrl+N')
new_action.triggered.connect(self.new_content)
file_menu.addAction(new_action)
save_action = QAction('保存', self)
save_action.setShortcut('Ctrl+S')
save_action.triggered.connect(self.save_content)
file_menu.addAction(save_action)
file_menu.addSeparator()
exit_action = QAction('退出', self)
exit_action.setShortcut('Ctrl+Q')
exit_action.triggered.connect(self.close)
file_menu.addAction(exit_action)
# 编辑菜单
edit_menu = menubar.addMenu('编辑')
search_action = QAction('搜索', self)
search_action.setShortcut('Ctrl+F')
search_action.triggered.connect(self.show_search_dialog)
edit_menu.addAction(search_action)
# 工具栏
toolbar = self.addToolBar('工具栏')
toolbar.addAction(new_action)
toolbar.addAction(save_action)
toolbar.addAction(search_action)
# 添加搜索框到工具栏
toolbar.addSeparator()
toolbar.addWidget(QLabel('搜索:'))
self.search_box = QLineEdit()
self.search_box.setPlaceholderText("输入关键词搜索...")
self.search_box.returnPressed.connect(self.perform_search)
toolbar.addWidget(self.search_box)
search_button = QPushButton("搜索")
search_button.clicked.connect(self.perform_search)
toolbar.addWidget(search_button)
central_widget = QWidget()
self.setCentralWidget(central_widget)
main_layout = QHBoxLayout(central_widget)
# --- 左侧面板 (分类树) ---
left_panel = QWidget()
left_layout = QVBoxLayout(left_panel)
self.category_tree = QTreeWidget()
self.category_tree.setHeaderLabel("内容分类")
self.category_tree.itemClicked.connect(self.on_tree_item_clicked)
self.category_tree.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
self.category_tree.customContextMenuRequested.connect(self.show_tree_context_menu)
manage_categories_btn = QPushButton("管理分类...")
manage_categories_btn.clicked.connect(self.open_category_manager)
left_layout.addWidget(self.category_tree)
left_layout.addWidget(manage_categories_btn)
# --- 右侧面板 (编辑区) ---
right_panel = QWidget()
right_layout = QVBoxLayout(right_panel)
form_layout = QFormLayout()
self.title_edit = QLineEdit()
self.category_combo = QComboBox()
self.category_combo.setEditable(True) # 允许用户输入新分类
self.tags_edit = QLineEdit()
self.description_edit = QLineEdit()
# 内容类型选择
self.content_type_combo = QComboBox()
self.content_type_combo.addItems(["代码片段", "Markdown笔记"])
self.content_type_combo.currentIndexChanged.connect(self.on_content_type_changed)
# 创建选项卡控件,用于代码编辑和Markdown编辑/预览
self.content_tabs = QTabWidget()
# 添加currentChanged信号处理,确保点击选项卡时同步更新类型下拉框
self.content_tabs.currentChanged.connect(self.on_tab_changed)
# 代码编辑选项卡
self.code_edit = EnhancedTextEdit(content_type='code') # 使用EnhancedTextEdit替代
self.code_edit.setFont(QFont("Consolas", 10))
self.code_highlighter = PythonSyntaxHighlighter(self.code_edit.document())
self.content_tabs.addTab(self.code_edit, "代码编辑")
# Markdown编辑和预览选项卡
markdown_widget = QWidget()
markdown_layout = QVBoxLayout(markdown_widget)
markdown_splitter = QSplitter(Qt.Orientation.Vertical)
# 只保留EnhancedTextEdit的赋值
self.markdown_edit = EnhancedTextEdit(content_type='markdown')
self.markdown_edit.setFont(QFont("SimHei", 10))
self.markdown_edit.setPlaceholderText("在此输入Markdown内容...")
self.markdown_edit.textChanged.connect(self.update_markdown_preview)
self.markdown_preview = QTextEdit()
self.markdown_preview.setReadOnly(True)
self.markdown_preview.setPlaceholderText("Markdown预览将显示在这里...")
markdown_splitter.addWidget(self.markdown_edit)
markdown_splitter.addWidget(self.markdown_preview)
markdown_splitter.setSizes([400, 400])
markdown_layout.addWidget(markdown_splitter)
self.content_tabs.addTab(markdown_widget, "Markdown")
form_layout.addRow("标题:", self.title_edit)
form_layout.addRow("类型:", self.content_type_combo)
form_layout.addRow("分类:", self.category_combo)
form_layout.addRow("标签 (逗号分隔):", self.tags_edit)
form_layout.addRow("描述:", self.description_edit)
form_layout.addRow("内容:", self.content_tabs)
button_layout = QHBoxLayout()
self.new_button = QPushButton("新建")
self.save_button = QPushButton("保存")
self.delete_button = QPushButton("删除")
self.new_button.clicked.connect(self.new_content)
self.save_button.clicked.connect(self.save_content)
self.delete_button.clicked.connect(self.delete_content)
button_layout.addWidget(self.new_button)
button_layout.addWidget(self.save_button)
button_layout.addWidget(self.delete_button)
right_layout.addLayout(form_layout)
right_layout.addLayout(button_layout)
# --- 使用分割器 ---
splitter = QSplitter(Qt.Orientation.Horizontal)
splitter.addWidget(left_panel)
splitter.addWidget(right_panel)
splitter.setSizes([300, 900])
main_layout.addWidget(splitter)
# --- 状态栏 ---
self.status_bar = QStatusBar()
self.setStatusBar(self.status_bar)
self.status_bar.showMessage("就绪")
def on_tab_changed(self, index):
"""处理选项卡切换,同步更新内容类型下拉框"""
# 当选项卡改变时,更新内容类型下拉框
self.content_type_combo.setCurrentIndex(index)
# 如果正在编辑,显示消息提示用户保存
if self.current_content_id:
content_type = "代码片段" if index == 0 else "Markdown笔记"
self.status_bar.showMessage(f"已切换到{content_type}模式,请确保保存更改")
def on_content_type_changed(self, index):
"""处理内容类型变化"""
# 切换到对应的选项卡
self.content_tabs.setCurrentIndex(index)
# 清空另一个编辑器的内容,避免内容混乱
if index == 0: # 切换到代码编辑
self.markdown_edit.clear()
self.markdown_preview.clear()
else: # 切换到Markdown编辑
self.code_edit.clear()
# 如果正在编辑,显示消息提示用户保存
if self.current_content_id:
content_type = "代码片段" if index == 0 else "Markdown笔记"
self.status_bar.showMessage(f"已切换到{content_type}模式,请确保保存更改")
def update_markdown_preview(self):
"""更新Markdown预览"""
markdown_text = self.markdown_edit.toPlainText()
html = MarkdownRenderer.render(markdown_text)
self.markdown_preview.setHtml(html)
def populate_category_tree(self):
"""根据内容数据填充分类树"""
self.category_tree.clear()
root_items = {} # 存储顶级分类项
all_contents_item = QTreeWidgetItem(self.category_tree, ["所有内容"])
all_contents_item.setData(0, Qt.ItemDataRole.UserRole, {"type": "all"})
for content in self.content_manager.contents:
path = content.category_path
parts = path.split('/')
parent_item = all_contents_item # 默认放在"所有内容"下
# 创建或查找分类节点
for i, part in enumerate(parts):
item_path = "/".join(parts[:i+1])
if item_path not in root_items:
# 如果是顶级分类,直接挂在"所有内容"下
if i == 0:
new_item = QTreeWidgetItem(all_contents_item, [part])
else:
# 否则挂在父分类下
parent_path = "/".join(parts[:i])
new_item = QTreeWidgetItem(root_items[parent_path], [part])
new_item.setData(0, Qt.ItemDataRole.UserRole, {"type": "category", "path": item_path})
root_items[item_path] = new_item
parent_item = root_items[item_path]
# 创建内容节点,并根据类型显示不同的图标或前缀
content_type_prefix = "[代码] " if content.type == 'code' else "[笔记] "
content_item = QTreeWidgetItem(parent_item, [content_type_prefix + content.title])
content_item.setData(0, Qt.ItemDataRole.UserRole, {"type": "content", "id": content.id})
self.category_tree.expandAll()
def update_category_combo(self):
"""更新分类下拉框的选项"""
self.category_combo.clear()
self.category_combo.addItems(sorted(self.content_manager.get_all_category_paths()))
def on_tree_item_clicked(self, item, column):
"""处理分类树项目点击事件"""
data = item.data(0, Qt.ItemDataRole.UserRole)
if isinstance(data, dict) and 'id' in data:
content = self.content_manager.get_content_by_id(data["id"])
self.load_content_into_editor(content)
else:
# 清空编辑器
self.clear_editor()
path = data.get('path', '所有内容') if isinstance(data, dict) else '所有内容'
self.status_bar.showMessage(f"已选择分类: {path}")
def load_content_into_editor(self, content):
"""将内容加载到编辑器中"""
if not content:
return
self.title_edit.setText(content.title)
self.description_edit.setText(content.description)
self.tags_edit.setText(', '.join(content.tags))
self.category_combo.setCurrentText(content.category_path)
# 根据内容类型设置编辑器
content_type_index = 0 if content.type == 'code' else 1
self.content_type_combo.setCurrentIndex(content_type_index)
# 根据类型显示对应内容
if content.type == 'code':
self.code_edit.setText(content.content_text)
self.content_tabs.setCurrentIndex(0) # 切换到代码编辑选项卡
else:
self.markdown_edit.setText(content.content_text)
self.update_markdown_preview() # 更新预览
self.content_tabs.setCurrentIndex(1) # 切换到Markdown选项卡
self.current_content_id = content.id
# 设置状态栏消息
content_type_text = "代码片段" if content.type == 'code' else "Markdown笔记"
self.status_bar.showMessage(f'已加载{content_type_text}: {content.title}')
def clear_editor(self):
"""清空编辑器"""
self.current_content_id = None
self.title_edit.clear()
self.tags_edit.clear()
self.description_edit.clear()
self.code_edit.clear()
self.markdown_edit.clear()
self.markdown_preview.clear()
self.code_edit.reset_zoom() # 重置字体大小
self.markdown_edit.reset_zoom() # 重置字体大小
self.title_edit.setFocus()
self.status_bar.showMessage("已清空编辑器")
def show_tree_context_menu(self, position):
"""显示树的右键菜单"""
item = self.category_tree.itemAt(position)
if not item:
return
data = item.data(0, Qt.ItemDataRole.UserRole)
if not data or data["type"] != "content":
return
menu = QMenu(self)
delete_action = QAction("删除此项", self)
delete_action.triggered.connect(lambda: self.delete_content_by_id(data["id"]))
menu.addAction(delete_action)
menu.exec(self.category_tree.mapToGlobal(position))
def delete_content_by_id(self, content_id):
"""根据ID删除内容项并刷新UI"""
reply = QMessageBox.question(
self,
'确认删除',
'你确定要删除这个内容项吗?',
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
)
if reply == QMessageBox.StandardButton.Yes:
content = self.content_manager.get_content_by_id(content_id)
if content:
self.content_manager.delete_content(content_id)
self.populate_category_tree()
self.new_content()
content_type_text = "代码片段" if content.type == 'code' else "Markdown笔记"
self.status_bar.showMessage(f"已删除{content_type_text}: {content.title}")
def open_category_manager(self):
"""打开分类管理对话框"""
dialog = CategoryDialog(self.content_manager.get_all_category_paths(), self)
if dialog.exec() == dialog.DialogCode.Accepted:
# 对话框关闭后,需要更新所有使用分类的内容
# 这里简化处理,只移除被删除分类下的内容
deleted_categories = set(self.content_manager.get_all_category_paths()) - dialog.existing_categories
contents_to_delete = []
for c in self.content_manager.contents:
if any(c.category_path.startswith(cat) for cat in deleted_categories):
contents_to_delete.append(c.id)
if contents_to_delete:
reply = QMessageBox.warning(
self,
"分类已删除",
f"以下分类被删除,其下的 {len(contents_to_delete)} 个内容项也将被删除,是否继续?\n{', '.join(deleted_categories)}",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
)
if reply == QMessageBox.StandardButton.Yes:
for cid in contents_to_delete:
self.content_manager.delete_content(cid)
# 重新加载UI
self.populate_category_tree()
self.update_category_combo()
self.new_content()
self.status_bar.showMessage("分类已更新。")
def new_content(self):
"""重置编辑器以创建新内容项"""
self.clear_editor()
self.status_bar.showMessage("正在创建新内容项...")
def save_content(self):
"""保存当前编辑的内容项(新建或更新)"""
title = self.title_edit.text()
category_path = self.category_combo.currentText()
tags = self.tags_edit.text()
description = self.description_edit.text()
# 根据当前选中的内容类型获取相应的内容文本
content_type = 'code' if self.content_type_combo.currentIndex() == 0 else 'markdown'
# 无论当前选中哪个标签页,都根据内容类型获取对应的编辑区内容
if content_type == 'code':
content_text = self.code_edit.toPlainText()
else:
content_text = self.markdown_edit.toPlainText()
if not title or not content_text.strip():
QMessageBox.warning(self, "警告", "标题和内容不能为空!")
return
if self.current_content_id is None:
self.content_manager.add_content(
title, description, content_text, tags, category_path, content_type
)
content_type_text = "代码片段" if content_type == 'code' else "Markdown笔记"
self.status_bar.showMessage(f"成功添加新{content_type_text}: {title}")
else:
self.content_manager.update_content(
self.current_content_id, title, description, content_text, tags, category_path, content_type
)
content_type_text = "代码片段" if content_type == 'code' else "Markdown笔记"
self.status_bar.showMessage(f"成功更新{content_type_text}: {title}")
self.populate_category_tree()
self.update_category_combo()
def delete_content(self):
"""删除当前在编辑器中加载的内容项"""
if self.current_content_id is None:
QMessageBox.warning(self, "警告", "请先从左侧列表选择一个要删除的内容项!")
return
self.delete_content_by_id(self.current_content_id)
def show_search_dialog(self):
"""显示搜索对话框"""
self.search_box.setFocus()
self.status_bar.showMessage("请输入搜索关键词...")
def perform_search(self):
"""执行搜索操作"""
keyword = self.search_box.text().strip()
if not keyword:
QMessageBox.warning(self, "警告", "请输入搜索关键词!")
return
# 搜索匹配的内容项
matched_items = []
for content in self.content_manager.contents:
# 在标题、描述、标签和内容中搜索
if (keyword.lower() in content.title.lower() or
keyword.lower() in content.description.lower() or
any(keyword.lower() in tag.lower() for tag in content.tags) or
keyword.lower() in content.content_text.lower()):
matched_items.append(content)
if not matched_items:
QMessageBox.information(self, "搜索结果", "未找到匹配的内容项。")
return
# 展开分类树并高亮显示匹配项
self.populate_category_tree_with_matches(matched_items)
self.status_bar.showMessage(f"找到 {len(matched_items)} 个匹配项")
def populate_category_tree_with_matches(self, matched_items):
"""根据搜索结果填充分类树并高亮匹配项"""
self.category_tree.clear()
root_items = {}
all_contents_item = QTreeWidgetItem(self.category_tree, ["搜索结果"])
all_contents_item.setData(0, Qt.ItemDataRole.UserRole, {"type": "search_results"})
# 创建匹配项的映射
matched_ids = {item.id for item in matched_items}
for content in self.content_manager.contents:
path = content.category_path
parts = path.split('/')
parent_item = all_contents_item
# 创建或查找分类节点
for i, part in enumerate(parts):
item_path = "/".join(parts[:i+1])
if item_path not in root_items:
# 如果是顶级分类,直接挂在"搜索结果"下
if i == 0:
new_item = QTreeWidgetItem(all_contents_item, [part])
else:
# 否则挂在父分类下
parent_path = "/".join(parts[:i])
new_item = QTreeWidgetItem(root_items[parent_path], [part])
new_item.setData(0, Qt.ItemDataRole.UserRole, {"type": "category", "path": item_path})
root_items[item_path] = new_item
parent_item = root_items[item_path]
# 创建内容节点,并根据类型显示不同的图标或前缀
content_type_prefix = "[代码] " if content.type == 'code' else "[笔记] "
content_title = content_type_prefix + content.title
# 如果是匹配项,添加特殊标记
if content.id in matched_ids:
content_title = "🔍 " + content_title
content_item = QTreeWidgetItem(parent_item, [content_title])
content_item.setData(0, Qt.ItemDataRole.UserRole, {"type": "content", "id": content.id})
# 如果是匹配项,设置为粗体显示
if content.id in matched_ids:
font = content_item.font(0)
font.setBold(True)
content_item.setFont(0, font)
self.category_tree.expandAll()