这篇文章我们来实现笔记列表的排序和笔记本编辑删除功能,最终实现效果如下:
笔记排序功能
用户从排序菜单中点击一个排序选项之后,笔记列表将清空,然后列表中的笔记执行排序操作,最后笔记列表会加载已排序的笔记。
需要注意的地方:
- 如果排序前笔记A高亮(预览面板的背景色加深),那么排序之后,笔记A依然需要高亮,并且文本编辑器不发生变化
- 用户选择的排序选项需要记录下来,如果用户选择了按字母排序,那么当用户再次点击排序按钮时,按字母排序选项应该是勾选状态。并且切换笔记本时,笔记列表也需要按字母排序。
构造排序菜单
修改 views / header_panel.py,初始化的时候添加几个成员变量:
from models import Note
# 修改 __init__
self._sort_menu_ids = wx.NewIdRef(6)
self._checked_menu_id = self._sort_menu_ids[0]
self.sort_option = Note.updated_at.desc()
如果需要将全部笔记按更新时间逆序排列,则代码如下:
Note.select().order_by(Note.updated_at.desc())
sort_option 就是排序选项,同理按标题顺序排列时,sort_option 就等于 Note.title.asc()。
接下来定义一个创建排序菜单的方法:
def _build_sort_menu(self):
menu = wx.Menu()
sub_menu1 = wx.Menu()
sub_menu1.AppendCheckItem(self._sort_menu_ids[0], '最新到最旧')
sub_menu1.AppendCheckItem(self._sort_menu_ids[1], '最旧到最新')
menu.AppendSubMenu(sub_menu1, '按更新时间')
sub_menu2 = wx.Menu()
sub_menu2.AppendCheckItem(self._sort_menu_ids[2], '最新到最旧')
sub_menu2.AppendCheckItem(self._sort_menu_ids[3], '最旧到最新')
menu.AppendSubMenu(sub_menu2, '按创建时间')
sub_menu3 = wx.Menu()
sub_menu3.AppendCheckItem(self._sort_menu_ids[4], '逆字母排序')
sub_menu3.AppendCheckItem(self._sort_menu_ids[5], '字母排序')
menu.AppendSubMenu(sub_menu3, '按标题')
return menu
定义一个 _init_event 方法,用来处理事件绑定;定义 _show_sort_menu 来显示排序菜单。
def _init_event(self):
self.btn_display_order_options.Bind(wx.EVT_BUTTON, self._show_sort_menu)
def _show_sort_menu(self, _):
menu = self._build_sort_menu()
menu.Check(self._checked_menu_id, True)
self.PopupMenu(menu)
menu.Destroy()
运行主程序,点击排序按钮,可以看到排序菜单了。
实现排序逻辑
排序菜单可以正常显示了,接下来在菜单上添加事件绑定,可以看到,不同的排序选项菜单实际上对应不同的排序参数,所以事件处理方法中需要接收排序参数和必要的事件对象。
修改 header_panel.py:
from functools import partial
from pubsub import pub
def _on_note_sorting(self, e, sort_param):
if self._checked_menu_id != e.GetId():
self._checked_menu_id = e.GetId()
self.sort_option = sort_param
pub.sendMessage('note.sorting', sort_param=sort_param)
# 修改 _init_event
def _init_event(self):
# ....
self.Bind(wx.EVT_MENU, partial(self._on_note_sorting, sort_param=Note.updated_at.desc()), id=self._sort_menu_ids[0])
self.Bind(wx.EVT_MENU, partial(self._on_note_sorting, sort_param=Note.updated_at.asc()), id=self._sort_menu_ids[1])
self.Bind(wx.EVT_MENU, partial(self._on_note_sorting, sort_param=Note.created_at.desc()), id=self._sort_menu_ids[2])
self.Bind(wx.EVT_MENU, partial(self._on_note_sorting, sort_param=Note.created_at.asc()), id=self._sort_menu_ids[3])
self.Bind(wx.EVT_MENU, partial(self._on_note_sorting, sort_param=Note.title.desc()), id=self._sort_menu_ids[4])
self.Bind(wx.EVT_MENU, partial(self._on_note_sorting, sort_param=Note.title.asc()), id=self._sort_menu_ids[5])
self.Bind(wx.EVT_MENU, partial(self._on_note_sorting, sort_param=Note.title.asc()), id=self._sort_menu_ids[5])
点击排序选项时,将传进来的排序参数保存下来,然后发送 note.sorting 消息。为了让代码简洁,我们使用了partial高阶函数。
现在点击排序菜单时,我们可以得到对应的排序参数了,编辑 list_panel.py,订阅 note.sorting :
# 编辑 __init__
pub.subscribe(self._on_note_sorting, 'note.sorting')
def _on_note_sorting(self, sort_param):
notes = list(Note.select().where(Note.id.in_(self._note_ids)).order_by(sort_param))
self._load(notes)
运行主程序,测试之后发现了一个问题,如下图所示:
可以看到,排序之前笔记 11111 选中,排序之后却是笔记 test 被选中了,并且文本编辑器也随之变化了。预期效果应该是:选中的笔记和文本编辑器都不会发生变化。
排序操作不影响已选中的笔记
ListPanel 中的 _load 方法实际上调用了 NoteListPanel 的 replace 方法,默认会选中第一条笔记,所以我们需要将 replace 方法重构一下,让它能够保存已选中的笔记。
编辑 note_list_panel.py,作如下修改:
编辑 list_panel.py,给 _load 方法添加 preserve_select 参数:
def _load(self, notes, preserve_select=False):
if len(notes):
self.note_list_panel.replace(notes, preserve_select)
# ...
def _on_note_sorting(self, sort_param):
# ...
self._load(notes, True)
再来测试一下,和预期一致,在排序前后,选中的都是同一条笔记。
排序选项全局范围起作用
修改 list_panel.py,应用已经选中的排序选项:
def _on_notebook_selected(self, notebook):
# ...
notes = list(notebook.notes.order_by(self.header_panel.sort_option))
# ...
def _on_root_selected(self):
# ...
notes = list(Note.select().order_by(self.header_panel.sort_option))
# ...
笔记本编辑和删除功能
构造菜单
修改 header_panel.py,和上面的排序菜单构造方法类似:
# 修改 __init__
self._rename_notebook_menu_id = wx.NewIdRef()
self._delete_notebook_menu_id = wx.NewIdRef()
def _show_action_menu(self, _):
menu = self._build_action_menu()
self.PopupMenu(menu)
menu.Destroy()
def _init_event(self):
# ...
self.btn_display_notebook_options.Bind(wx.EVT_BUTTON, self._show_action_menu)
self.Bind(wx.EVT_MENU, lambda _: pub.sendMessage('notebook.editing'), id=self._rename_notebook_menu_id)
self.Bind(wx.EVT_MENU, lambda _: pub.sendMessage('notebook.deleting'), id=self._delete_notebook_menu_id)
实现编辑笔记本功能
编辑和删除笔记本功能之前已经实现过了,当时相关逻辑放在了 NoteTree 组件中,参考:
ac2190:使用wxPython打造印象笔记(14)笔记本管理zhuanlan.zhihu.com所以我们只需要复用这部分逻辑就行了,关键点在于让笔记列表和笔记本导航栏数据同步:笔记本重命名之后,笔记本导航栏的节点文字会变化,笔记列表的顶部文字也需要变化。
最好是将这类逻辑放在主窗体 MainFrame 中,便于代码复用。
修改 note_tree.py:
def _edit_notebook(self, _):
pub.sendMessage('notebook.editing')
def set_text(self, text):
self.SetItemText(self.GetSelection(), text)
修改 main_frame.py:
from pubsub import pub
from .notebook_dialog import NotebookDialog
def _register_listeners(self):
# ...
pub.subscribe(self._on_notebook_editing, 'notebook.editing')
def _on_notebook_editing(self):
notebook = self.nav_panel.note_tree.notebook
with NotebookDialog(self, notebook) as dialog:
if dialog.ShowModal() == wx.ID_OK:
notebook.name = dialog.get_name()
notebook.description = dialog.get_description()
notebook.save()
self.nav_panel.note_tree.set_text(notebook.name)
self.list_panel.header_panel.set_title(notebook.name)
实现删除笔记本功能
和上面的方法类似,将 NoteTree 里面的删除笔记本逻辑转移到 MainFrame 中,并提供一个删除节点的方法。
修改 note_tree.py:
def _delete_notebook(self, _):
pub.sendMessage('notebook.deleting')
def delete_selection(self):
self.GetSelection().GetData().delete_instance()
self.Delete(self.GetSelection())
修改 main_frame.py:
def _register_listeners(self):
# ...
pub.subscribe(self._on_notebook_deleting, 'notebook.deleting')
def _on_notebook_deleting(self):
dialog = wx.MessageDialog(self, '此笔记本中的任何笔记都将被删除,这个操作不能恢复。', '确定要删除吗?',style=wx.OK|wx.CANCEL|wx.CANCEL_DEFAULT)
dialog.SetOKCancelLabels('确定', '取消')
if dialog.ShowModal() == wx.ID_OK:
self.nav_panel.note_tree.delete_selection()
总结
笔记列表顶部的两个右键菜单功能已经实现,后面的文章中将开发笔记搜索功能。