使用wxPython打造印象笔记(6)主界面布局

882 阅读7分钟
前面一篇文章中我们进行了数据库设计,这一篇我们来实现主界面的布局。
首先回顾一下最终实现之后的界面,如下图所示。
接下来我们将实现这个三栏布局,首先设置一下开发环境。

开发环境准备

  • 操作系统:macOS
  • Python版本:3.7.5
  • wxPython版本:4.0.7.post2
  • 开发工具:PyCharm
由于wxPython是跨平台开发框架,所以操作系统是Windows或者Linux也可以,但Python和wxPython的版本尽量保持最新,开发工具用主流的IDE都可以。
接下来新建一个文件夹,命名为note-app,进入到文件夹路径下,创建一个虚拟环境。
python3 -m venv .venv
创建好了以后,再使用当前虚拟环境:
source .venv/bin/activate
然后安装wxPython
pip install wxpython
这样操作之后,基本的开发环境就已经搭建好了。

布局的实现

对于这种三栏布局我们可以借助wxPython提供的aui组件来实现。aui全称是Advanced User Interface,即高级用户界面。
在根目录下创建一个views目录,里面将存放所有的页面组件。在views目录里面创建以下文件。
  • main_frame.py 主界面
  • nav_panel.py 第一栏,也就是笔记本列表面板
  • list_panel.py 中间的一栏,笔记列表面板
  • text_editor.py 第三栏,笔记本编辑器组件
我们接下来逐个实现上面的组件。

主界面

主界面也就是应用的主体,一个三栏结构的窗体。主界面里面会管理子页面,并处理事件。管理页面主要借助于AUI类,事件处理我们将会使用发布订阅模式来实现。
打开main_frame.py文件,开始引入aui和子组件(接下来会实现)。
import wx
import wx.aui
from .nav_panel import NavPanel
from .list_panel import ListPanel
from .text_editor import TextEditor
这里会涉及到NavPanel、ListPanel和TextEditor三个子组件,它们的详细实现我们会在后面逐个提及,这里暂时将它们设置成纯色背景的面板,便于观察。
打开nav_panel.py,构建一个黑色背景的面板,代码如下:
import wx

class NavPanel(wx.Panel):
    def __init__(self, parent):
        super().__init__(parent)
        self.SetBackgroundColour("#2a2a2a")
编辑 list_panel.py文件,构建一个红色背景的面板。
import wx

class ListPanel(wx.Panel):
    def __init__(self, parent):
        super().__init__(parent,style=wx.BORDER_NONE)
        self.SetBackgroundColour('red')
接着编辑text_editor.py文件,构建一个蓝色背景的面板。
import wx

class TextEditor(wx.Panel):
    def __init__(self, parent):
        super().__init__(parent)
        self.SetBackgroundColour('blue')
现在,三个面板已经简单的实现了。我们再回到主界面,使用aui来管理这个三个面板,打开main_frame.py文件,接着写入下面的代码。
class MainFrame(wx.Frame):
    def __init__(self):
        super().__init__(None, title='UltraNote',size=(800,600))
        self.aui_manager = wx.aui.AuiManager(self,wx.aui.AUI_MGR_TRANSPARENT_HINT)

        self.nav_panel = NavPanel(self)
        self.list_panel = ListPanel(self)
        self.detail_panel = TextEditor(self)

        self.aui_manager.AddPane(self.nav_panel, wx.aui.AuiPaneInfo().Left().Row(0).BestSize(300,-1))
        self.aui_manager.AddPane(self.list_panel, wx.aui.AuiPaneInfo().Left().Row(1).BestSize(250, -1).MinSize(150,-1))
        self.aui_manager.AddPane(self.detail_panel, wx.aui.AuiPaneInfo().CenterPane().Position(0).BestSize(400,-1))

        self.aui_manager.Update()

        self.Maximize(True)
        self._register_listeners()

    def _get_default_pane_info(self):
        return wx.aui.AuiPaneInfo().CaptionVisible(False).PaneBorder(False).CloseButton(False).PinButton(False).Gripper(
            False)

    def on_frame_closing(self, e):
        self.aui_manager.UnInit()
        del self.aui_manager
        self.Destroy()

    def _register_listeners(self):
        self.Bind(wx.EVT_CLOSE, self.on_frame_closing)
我们首先初始化一个AuiManager,接着通过AddPane方法将三个面板添加到manager里面,注意此方法的第二个参数是一个AuiPaneInfo实例,参数的含义是什么意思呢?以下面代码为例
self.aui_manager.AddPane(self.nav_panel, wx.aui.AuiPaneInfo().Left().Row(0).BestSize(300,-1))
Left表示靠左边排列,通过Row来指定次序,Row(0)表示排在第一,BestSize用来指定面板的理想大小,这里的(300,-1)表示长度为300像素,高度为默认高度。
调用了AddPanel之后,再调用Update方法来更新界面。
然后调用Maxmize让主界面启动之后立即最大化,最后添加事件监听逻辑,使得主界面关闭的时候,能够正常的释放掉aui_manager这个对象,这是必须的一步,如果不释放掉,主界面关闭时将抛出异常。

应用入口

实现了main_frame.py之后,我们想要启动程序,看看效果如何。在项目的根目录下,也就是views的上级目录,新建一个main.py文件作为应用的入口。
import wx
from views import MainFrame

class NoteApp(wx.App):
    def OnInit(self):
        MainFrame().Show()
        return True

if __name__ == "__main__":
    app = NoteApp()
    app.MainLoop()
运行main.py,执行:
python main.py
此时会出现错误,提示MainFrame无法导入,这是因为views目录缺少 __init__.py文件,导致views没有被视为模块。我们在views目录下新建一个 __init__.py文件,来引入MainFrame。
from .main_frame import MainFrame
再次运行main.py文件,可以看到,主界面已经显示了,界面如下所示。

面板界面优化

从上面的截图可以看到,显示了三栏布局。但每个面板上面都有一个关闭按钮并且边框很宽,不是很美观,这是AUI补充的界面特征,我们将之优化一下,打开 main_frame.py文件,进行如下修改。
self.aui_manager.AddPane(self.nav_panel, self._get_default_pane_info().Left().Row(0).BestSize(300,-1))
self.aui_manager.AddPane(self.list_panel, self._get_default_pane_info().Left().Row(1).BestSize(250, -1).MinSize(150,-1))
self.aui_manager.AddPane(self.detail_panel, self._get_default_pane_info().CenterPane().Position(0).BestSize(400,-1))
AddPane的第二个参数控制了面板的关闭按钮、边框、标题等属性,接着定义涉及到的方法。
def _get_default_pane_info(self):
        return wx.aui.AuiPaneInfo().CaptionVisible(False).PaneBorder(False).CloseButton(False).PinButton(False).Gripper(False)
接着将面板之间拖动指示器的尺寸设置为1。
self.aui_manager.GetArtProvider().SetMetric(wx.aui.AUI_DOCKART_SASH_SIZE, 1)
main_frame.py的完整代码如下。
import wx
import wx.aui
from .nav_panel import NavPanel
from .list_panel import ListPanel
from .text_editor import TextEditor

class MainFrame(wx.Frame):
    def __init__(self):
        super().__init__(None, title='UltraNote',size=(800,600))
        self.aui_manager = wx.aui.AuiManager(self,wx.aui.AUI_MGR_TRANSPARENT_HINT)

        self.nav_panel = NavPanel(self)
        self.list_panel = ListPanel(self)
        self.detail_panel = TextEditor(self)

        self.aui_manager.AddPane(self.nav_panel, self._get_default_pane_info().Left().Row(0).BestSize(300,-1))
        self.aui_manager.AddPane(self.list_panel, self._get_default_pane_info().Left().Row(1).BestSize(250, -1).MinSize(150,-1))
        self.aui_manager.AddPane(self.detail_panel, self._get_default_pane_info().CenterPane().Position(0).BestSize(400,-1))

        self.aui_manager.GetArtProvider().SetMetric(wx.aui.AUI_DOCKART_SASH_SIZE, 1)
        self.aui_manager.Update()

        self.Maximize(True)
        self._register_listeners()

    def _get_default_pane_info(self):
        return wx.aui.AuiPaneInfo().CaptionVisible(False).PaneBorder(False).CloseButton(False).PinButton(False).Gripper(
            False)

    def on_frame_closing(self, e):
        self.aui_manager.UnInit()
        del self.aui_manager
        self.Destroy()

    def _register_listeners(self):
        self.Bind(wx.EVT_CLOSE, self.on_frame_closing)
再次运行main.py文件,面板上面的按钮消失了,边框也美观了。

笔记本列表的实现

主界面成功运行之后,我们开始实现子组件了。首先来实现笔记本列表页面,如下图所示。
可以看到,最上面是一个按钮,下面是一个树形控件。这两个组件可以使用BoxSizer来垂直布局,考虑到下面的笔记列表会涉及到很多的事件触发,我们可以将其封装成一个NoteTree类,马上会提到。
打开nav_panel.py文件,开始实现上面的界面。
import wx
from .note_tree import NoteTree

class NavPanel(wx.Panel):
    def __init__(self, parent):
        super().__init__(parent)

        v_sizer = wx.BoxSizer(wx.VERTICAL)

        self.btn_new_note = wx.Button(self,style=wx.NO_BORDER)
        self.btn_new_note.SetLabelMarkup('<span fgcolor="white" weight="bold" size="large">新建笔记</span>')

        v_sizer.Add(self.btn_new_note, flag=wx.ALIGN_CENTER|wx.TOP, border=40)
        v_sizer.AddSpacer(20)

        self.note_tree = NoteTree(self)

        v_sizer.Add(self.note_tree, proportion=1,flag=wx.EXPAND)
        self.SetSizer(v_sizer)

        self.SetBackgroundColour("#2a2a2a")
可以看到NavPanel有两个子控件:
  • btn_new_note 即新建笔记按钮,button的缩写是btn,所以命名为btn_new_note
  • note_tree 下方的树形列表,这个接下来会实现
然后将这两个子控件放在一个BoxSizer里面,竖直排列。

NoteTree的实现

上面提到了NoteTree,实际上就是一个树形控件,用来展示所有的笔记本。在views目录下新建一个note_tree.py文件。
import wx.lib.agw.customtreectrl as customtreectrl

class NoteTree(customtreectrl.CustomTreeCtrl):
    def __init__(self, parent):
        super().__init__(parent,agwStyle=customtreectrl.TR_HAS_BUTTONS|customtreectrl.TR_FULL_ROW_HIGHLIGHT|customtreectrl.TR_ELLIPSIZE_LONG_ITEMS|customtreectrl.TR_TOOLTIP_ON_LONG_ITEMS)

        self.root = self.AddRoot("所有笔记")
        self._load_note_books()

    def _load_note_books(self):
        root_note_books = ['使用','wxPython','打造','自己的','印象笔记']

        for note_book in root_note_books:
            root_node = self.AppendItem(self.root, note_book)
            self.AppendItem(root_node, '笔记本')
        self.ExpandAll()
这里使用了wx.lib.agw.customtreectrl这个树形组件,比原生的wx.TreeCtrl增加了很多功能,我们后面会提到。
wx.lib.agw模块包含了很多自绘控件,如果某些原生控件不够理想,可以考虑去这个模块里面查找对应控件。
通过AddRoot添加一个根节点,接下来调用_load_note_books方法来添加子节点。可以看到添加节点使用了AppendItem方法,第一个参数是父节点,第二个参数是节点文字。这里我们使用了模拟数据,后面会使用真实的数据。
我们再来运行main.py文件,此时左侧显示了树形列表,如图所示。
但是不够美观,我们将字体大小、间距和颜色调整一下,note_tree.py完整的代码如下。
import wx.lib.agw.customtreectrl as customtreectrl

class NoteTree(customtreectrl.CustomTreeCtrl):
    def __init__(self, parent):
        super().__init__(parent,agwStyle=customtreectrl.TR_HAS_BUTTONS|customtreectrl.TR_FULL_ROW_HIGHLIGHT|customtreectrl.TR_ELLIPSIZE_LONG_ITEMS|customtreectrl.TR_TOOLTIP_ON_LONG_ITEMS)

        self.root = self.AddRoot("所有笔记")
        self._load_note_books()
        self._init_ui()

    def _load_note_books(self):
        root_note_books = ['使用','wxPython','打造','自己的','印象笔记']

        for note_book in root_note_books:
            root_node = self.AppendItem(self.root, note_book)
            self.AppendItem(root_node, '笔记本')
        self.ExpandAll()

    def _init_ui(self):
        panel_font = self.GetFont()
        panel_font.SetPointSize(panel_font.GetPointSize() + 1)
        self.SetFont(panel_font)

        self.EnableSelectionGradient(False)
        self.EnableSelectionGradient(False)

        self.SetForegroundColour("#ececec")
        self.SetBackgroundColour("#2a2a2a")
        self.SetHilightFocusColour("#646464")
        self.SetHilightNonFocusColour("#646464")

        self.SetSpacing(20)
        self.SetIndent(10)
再次运行main.py,可以发现界面美观了。
我们的笔记本列表页面已经实现了,接下来我们将实现笔记列表和文本编辑器,界面实现好了之后,我们将填充真实数据,并涉及到数据库读写和事件处理,后面将逐一介绍。