前面一篇文章中我们进行了数据库设计,这一篇我们来实现主界面的布局。
首先回顾一下最终实现之后的界面,如下图所示。
接下来我们将实现这个三栏布局,首先设置一下开发环境。
开发环境准备
- 操作系统: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,可以发现界面美观了。
我们的笔记本列表页面已经实现了,接下来我们将实现笔记列表和文本编辑器,界面实现好了之后,我们将填充真实数据,并涉及到数据库读写和事件处理,后面将逐一介绍。