ASSISTANT
创建一个功能完善的安卓笔记应用需要整合多个功能模块。以下是使用 Python 和 [Flet](https://flet.dev/) 库构建该应用的详细步骤和示例代码。此示例涵盖了以下功能:
1. **添加新文件夹**
2. **在文件夹内添加、编辑、保存、删除笔记**
3. **搜索笔记和关键词**
4. **导出所有笔记为 PDF**
5. **撤销编辑操作**
### 前置条件
1. **安装 Flet**:
```bash
pip install flet
```
2. **安装 SQLite3**:Python 自带 SQLite3,无需额外安装。
3. **安装 ReportLab**(用于生成 PDF):
```bash
pip install reportlab
```
### 数据库设计
使用 SQLite 数据库来存储文件夹和笔记信息。
```python
import sqlite3
def init_db():
conn = sqlite3.connect('notes.db')
cursor = conn.cursor()
# 创建文件夹表
cursor.execute('''
CREATE TABLE IF NOT EXISTS folders (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT UNIQUE
)
''')
# 创建笔记表
cursor.execute('''
CREATE TABLE IF NOT EXISTS notes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
folder_id INTEGER,
title TEXT,
content TEXT,
FOREIGN KEY(folder_id) REFERENCES folders(id)
)
''')
conn.commit()
conn.close()
init_db()
```
### 应用实现
下面是使用 Flet 构建安卓笔记应用的示例代码:
```python
import flet
from flet import (
Page, Text, TextField, ElevatedButton, Column, Row, ListView,
Dialog, IconButton, Icon, AppBar, NavigationRail, NavigationRailDestination,
Tabs, Tab, TabView, TabViewPage, Snackbar
)
import sqlite3
from reportlab.lib.pagesizes import letter
from reportlab.pdfgen import canvas
import os
from flet import FilePicker, FilePickerResultEvent
# 初始化数据库(确保在应用启动时调用)
def init_db():
conn = sqlite3.connect('notes.db')
cursor = conn.cursor()
# 创建文件夹表
cursor.execute('''
CREATE TABLE IF NOT EXISTS folders (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT UNIQUE
)
''')
# 创建笔记表
cursor.execute('''
CREATE TABLE IF NOT EXISTS notes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
folder_id INTEGER,
title TEXT,
content TEXT,
FOREIGN KEY(folder_id) REFERENCES folders(id)
)
''')
conn.commit()
conn.close()
# 获取所有文件夹
def get_folders():
conn = sqlite3.connect('notes.db')
cursor = conn.cursor()
cursor.execute('SELECT id, name FROM folders')
folders = cursor.fetchall()
conn.close()
return folders
# 获取笔记夹下所有笔记
def get_notes(folder_id, search_query=None):
conn = sqlite3.connect('notes.db')
cursor = conn.cursor()
if search_query:
cursor.execute('''
SELECT id, title FROM notes
WHERE folder_id=? AND (title LIKE ? OR content LIKE ?)
''', (folder_id, f'%{search_query}%', f'%{search_query}%'))
else:
cursor.execute('SELECT id, title FROM notes WHERE folder_id=?', (folder_id,))
notes = cursor.fetchall()
conn.close()
return notes
# 添加文件夹
def add_folder(name):
conn = sqlite3.connect('notes.db')
cursor = conn.cursor()
try:
cursor.execute('INSERT INTO folders (name) VALUES (?)', (name,))
conn.commit()
return True
except sqlite3.IntegrityError:
return False
finally:
conn.close()
# 添加或更新笔记
def save_note(folder_id, title, content, note_id=None):
conn = sqlite3.connect('notes.db')
cursor = conn.cursor()
if note_id:
cursor.execute('''
UPDATE notes SET title=?, content=? WHERE id=?
''', (title, content, note_id))
else:
cursor.execute('''
INSERT INTO notes (folder_id, title, content) VALUES (?, ?, ?)
''', (folder_id, title, content))
conn.commit()
conn.close()
# 删除笔记
def delete_note(note_id):
conn = sqlite3.connect('notes.db')
cursor = conn.cursor()
cursor.execute('DELETE FROM notes WHERE id=?', (note_id,))
conn.commit()
conn.close()
# 导出为 PDF
def export_to_pdf(page, search_query=None):
conn = sqlite3.connect('notes.db')
cursor = conn.cursor()
cursor.execute('SELECT title, content FROM notes')
notes = cursor.fetchall()
conn.close()
pdf_path = os.path.join(os.path.expanduser('~/Download'), 'notes_export.pdf')
c = canvas.Canvas(pdf_path, pagesize=letter)
width, height = letter
y = height - 50
for title, content in notes:
c.setFont("Helvetica-Bold", 14)
c.drawString(50, y, title)
y -= 20
c.setFont("Helvetica", 12)
for line in content.split('\n'):
c.drawString(60, y, line)
y -= 15
if y < 50:
c.showPage()
y = height - 50
y -= 10
c.save()
page.snack_bar = Snackbar(Text(f"Exported to {pdf_path}"))
page.snack_bar.open = True
page.update()
# 主应用
def main(page: Page):
page.title = "笔记应用"
page.padding = 20
page.vertical_alignment = "start"
init_db()
selected_folder = None
current_note = None
# 文件夹列表
folder_list = ListView(expand=True)
# 笔记列表
notes_list = ListView(expand=True)
# 搜索框
search_field = TextField(label="搜索笔记", on_change=lambda e: load_notes())
# 笔记内容编辑器
title_field = TextField(label="标题")
content_field = TextField(label="内容", multiline=True, expand=True, keyboard_type="text", can_revert=True)
def load_folders():
folder_list.controls.clear()
folders = get_folders()
for folder in folders:
folder_id, name = folder
folder_list.controls.append(
Row(
[Text(name)],
vertical_alignment="center",
on_click=lambda e, fid=folder_id: select_folder(fid)
)
)
page.update()
def load_notes():
if not selected_folder:
return
search_query = search_field.value.strip()
notes = get_notes(selected_folder, search_query if search_query else None)
notes_list.controls.clear()
for note in notes:
note_id, title = note
notes_list.controls.append(
Row(
[Text(title)],
vertical_alignment="center",
on_click=lambda e, nid=note_id: open_note(nid)
)
)
page.update()
def select_folder(folder_id):
nonlocal selected_folder, current_note
selected_folder = folder_id
current_note = None
load_notes()
title_field.value = ""
content_field.value = ""
page.update()
def open_note(note_id):
nonlocal current_note
conn = sqlite3.connect('notes.db')
cursor = conn.cursor()
cursor.execute('SELECT title, content FROM notes WHERE id=?', (note_id,))
note = cursor.fetchone()
conn.close()
if note:
title, content = note
title_field.value = title
content_field.value = content
current_note = note_id
page.update()
def add_folder_dialog(e):
dialog = Dialog(
content=Column([
Text("添加新文件夹"),
TextField(label="文件夹名称", id="new_folder_name"),
]),
actions=[
ElevatedButton("取消", on_click=lambda e: page.dialog.close()),
ElevatedButton("添加", on_click=lambda e: save_new_folder())
]
)
page.dialog = dialog
dialog.open = True
page.update()
def save_new_folder():
folder_name = page.get_control("new_folder_name").value.strip()
if folder_name:
success = add_folder(folder_name)
if success:
page.dialog.close()
load_folders()
else:
page.snack_bar = Snackbar(Text("文件夹名称已存在"))
page.snack_bar.open = True
page.update()
def save_note_action(e):
if not selected_folder:
page.snack_bar = Snackbar(Text("请选择一个文件夹首先"))
page.snack_bar.open = True
page.update()
return
title = title_field.value.strip()
content = content_field.value.strip()
if not title:
page.snack_bar = Snackbar(Text("标题不能为空"))
page.snack_bar.open = True
page.update()
return
save_note(selected_folder, title, content, current_note)
load_notes()
page.snack_bar = Snackbar(Text("保存成功"))
page.snack_bar.open = True
page.update()
def delete_note_action(e):
if current_note:
delete_note(current_note)
current_note = None
title_field.value = ""
content_field.value = ""
load_notes()
page.snack_bar = Snackbar(Text("笔记已删除"))
page.snack_bar.open = True
page.update()
def export_pdf(e):
export_to_pdf(page)
# UI 布局
page.add(
Column([
AppBar(
title=Text("笔记应用"),
actions=[
IconButton(icon=icons.ADD, tooltip="添加文件夹", on_click=add_folder_dialog),
IconButton(icon=icons.DOWNLOAD, tooltip="导出为 PDF", on_click=export_pdf),
]
),
Row([
Column([
Text("文件夹", weight="bold"),
folder_list
], width=200),
VerticalDivider(),
Column([
Row([
search_field,
IconButton(icon=icons.SEARCH, on_click=lambda e: load_notes())
]),
Text("笔记", weight="bold"),
notes_list
], width=300),
VerticalDivider(),
Column([
TextField(label="标题", expand=True, on_change=lambda e: None),
TextField(label="内容", multiline=True, expand=True, can_revert=True),
Row([
ElevatedButton("保存", on_click=save_note_action),
ElevatedButton("删除", on_click=delete_note_action),
])
], expand=True)
])
])
)
load_folders()
flet.app(target=main, view=flet.app_views.WEB_BROWSER)
```
### 代码说明
1. **数据库初始化**:
- `init_db()` 函数用于创建 `folders` 和 `notes` 表。
2. **数据操作函数**:
- `get_folders()`:获取所有文件夹。
- `get_notes()`:获取指定文件夹下的所有笔记,支持搜索查询。
- `add_folder()`:添加新文件夹,防止重复名称。
- `save_note()`:添加或更新笔记。
- `delete_note()`:删除指定笔记。
- `export_to_pdf()`:将所有笔记导出为 PDF,并保存到用户的 `Download` 文件夹中。
3. **UI 组件**:
- **文件夹列表**:左侧显示所有文件夹,点击选择文件夹后显示对应的笔记。
- **笔记列表**:中间区域显示选中文件夹下的所有笔记,支持搜索。
- **笔记编辑区域**:右侧用于查看和编辑笔记,支持撤销操作。
- **顶部 AppBar**:包含添加文件夹和导出 PDF 的按钮。
4. **功能实现**:
- **添加文件夹**:点击 AppBar 中的添加按钮,弹出对话框输入文件夹名称并保存。
- **添加/编辑笔记**:选择文件夹后,可以在右侧编辑笔记的标题和内容,点击保存按钮保存到数据库。
- **删除笔记**:选中笔记后,点击删除按钮从数据库中删除该笔记。
- **导出为 PDF**:点击顶栏的导出按钮,将所有笔记内容生成 PDF 并保存到 `Download` 文件夹。
- **搜索笔记**:在搜索框中输入关键词,实时过滤笔记列表。
- **撤销操作**:`TextField` 组件自带撤销功能,用户可以使用快捷键或者控件自带的按钮进行撤销。
### 打包为安卓应用
要将此 Flet 应用打包为安卓应用,可以使用 [Flet for Mobile](https://flet.dev/docs/getting-started/flutter) 相关指南。以下是基本步骤:
1. **安装 Flutter**:因为 Flet 基于 Flutter 来构建移动应用,确保已安装 Flutter SDK。
2. **设置 Flet for Mobile**:参考 Flet 官方文档,使用 `flet` 命令行工具生成安卓项目。
3. **构建安卓应用**:
```bash
flet build android
```
这将生成一个可安装的 APK 文件。
4. **安装 APK**:将生成的 APK 文件传输到安卓设备并安装。
### 注意事项
- **权限**:确保应用具有访问文件系统的权限,尤其是写入 `Download` 文件夹。可能需要在 `AndroidManifest.xml` 中声明相关权限。
- **性能优化**:对于大量笔记,考虑分页加载或者优化数据库查询以提升性能。
- **错误处理**:添加更多的错误处理和用户反馈,以提升用户体验。
- **UI 设计**:上述示例提供了基本的布局,您可以根据需要进一步美化界面和优化交互。
### 结论
以上示例提供了一个基于 Python 和 Flet 的安卓笔记应用的基本实现。您可以根据具体需求进行功能扩展和优化,例如添加用户认证、云同步功能等。Flet 的跨平台特点使其成为快速开发多平台应用的有力工具。