深圳幻海软件技术有限公司 欢迎您!

使用Python构建自己的Markdown编辑器

2023-02-27

Markdown编辑器大家应该都知道,很受程序员喜欢。许多人都在创建一个Markdown编辑器,有些很有创意,有些则很无聊。不过很多开发人员不希望使用Tkinter来构建Markdown编辑器,如果您已经熟悉Python和Tkinter,您可以轻松进入本指南。在我们开始之前,来解释一下为什么人们不想

Markdown编辑器大家应该都知道,很受程序员喜欢。许多人都在创建一个Markdown编辑器,有些很有创意,有些则很无聊。

不过很多开发人员不希望使用Tkinter来构建Markdown编辑器,如果您已经熟悉Python和Tkinter,您可以轻松进入本指南。

在我们开始之前,来解释一下为什么人们不想用tkinter来构建Markdown编辑器。这是因为没有默认的简单方法来显示markdown输入的html数据。甚至没有一个默认的tkinter组件来显示html数据。您可以简单地编写/编辑markdown,但是没有简单的方法在应用程序中显示输出。

但是,现在有了tk_html_widgets,它可以帮助我们显示html输出。

现在让我们能开始构建吧。

开始构建:

首先,请确保您已安装Python 3和Tkinter。如果没有,您可以从这里下载:

python.org/downloads(Tkinter已包含Python中)。

我们需要的其他东西是tkhtmlview和markdown2。您可以通过运行pip install tkhtmlview markdown2或pip3 install tkhtmlview markdown2来安装它们(如果您有多个Python版本)。

现在启动您喜欢的编辑器或IDE并创建一个新文件(例如www.linuxidc.com.py(我将其命名为linuxidc.com编辑器))。

我们将从导入必要的库开始。 

from tkinter import *  
from tkinter import font , filedialog  
from markdown2 import Markdown  
from tkhtmlview import HTMLLabel  
  • 1.
  • 2.
  • 3.
  • 4.

在第一行中,我们从tkinter包中导入(几乎)所有内容。

在第二行中,我们导入字体和文件对话框。需要使用font来设置输入字段的样式(例如Font,Font Size),并导入filedialog以打开markdown文件以进行编辑(和/或保存我们的markdown文件)。

在第三行中,导入了Markdown,以帮助我们将Markdown源转换为html,并使用HTMLLabel(在第四行中导入)将其显示在输出字段中。

之后,我们将创建一个名为Window的框架类,该框架类将从tkinters的Frame类继承。它将保存我们的输入和输出字段。 

class Window(Frame):  
    def __init__(self, master=None):  
        Frame.__init__(self, master)  
        self.master = master  
        self.myfont = font.Font(family="Helvetica"size=14 
        self.init_window()  
    def init_window(self):  
        self.master.title("linuxidc.com编辑器")  
        self.pack(fill=BOTHexpand=1 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.

在此代码块中,我们首先定义一个称为Window的类,该类继承tkinter的Frame小部件类。

现在,在初始化函数中,我们将master作为参数,用作框架的父级。在下一行中,我们初始化一个Frame。

接下来,我们声明一个名为self.myfont的自定义字体对象,其字体家族为Helvetica(您可以选择任何字体家族),大小为15,将在我们的markdown输入字段中使用。

最后,我们调用init_window函数,将我们的应用程序置于核心位置。

在init_window函数中,我们首先将窗口的标题设置为linuxidc.com编辑器。在下一行self.pack(fill=BOTH, expand=1)中,我们告诉Frame占用窗口的全部空间。

我们将fill关键字参数设置为BOTH,这实际上是从tkinter库导入的。它告诉框架在水平和垂直方向上都填充窗口,并且expand关键字参数设置为1(表示True),这告诉我们框架是可扩展的。简而言之,无论我们如何拉伸窗口大小或最大化窗口大小,框架都将填充窗口。

现在,如果您运行www.linuxidc.com.py脚本,您将看不到任何内容,因为我们仅定义了该类,但从未调用过它。

为了解决这个问题,我们将以下代码放在脚本的末尾: 

root = Tk()  
root.geometry("800x600")  
app = Window(root)  
app.mainloop()  
  • 1.
  • 2.
  • 3.
  • 4.

接下来,将窗口的几何形状设置为800x600的长方体,800是窗口的高度,600是窗口的宽度。在下一行中,您可以看到我们正在创建一个Window对象。我们将root变量推入框架的root,并将其存储在名为app的变量中。

接下来要做的就是调用mainloop函数,该函数告诉我们的应用程序运行!

现在运行www.linuxidc.com.py脚本。如果正确完成所有操作,您将看到一个空白窗口,如下所示:

但这只是一个空白窗口。要在窗口中写入内容,我们需要添加一个文本字段,在其中写入我们的markdown。为此,我们将使用tkinter中的Text小部件。 

...  
def init_window(self):  
    self.master.title("linuxidc.com编辑器")  
    self.pack(fill=BOTHexpand=1 
    self.inputeditor = Text(self, width="1" 
    self.inputeditor.pack(fill=BOTHexpand=1side=LEFT 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.

不要与...混淆(三个点),我把它们放在那里只是为了表示在此代码块之前有多行代码。

在这里,我们创建了一个宽度为1的Text小部件。不要误会,以为错了-这里的大小是使用比例来完成的。当我们将其放入输出框中时,您将在接下来的几秒钟内更清楚地了解它。

然后,我们将其包装到框架中,并使其在水平和垂直方向上均可拉伸。

运行脚本时,您会看到已接管了整个“窗口”。如果您开始写它,您可能会注意到字符太小了。

我已经知道会出现这个问题。这就是为什么我之前告诉过您创建自定义字体对象(self.myfont)的原因。现在,如果您执行以下操作: 

self.inputeditor = Text(self, width="1" , font=self.myfont) 
  • 1.

(这里,我们告诉Text小部件使用自定义字体,而不是默认的小字体!)

...输入字段的字体大小将增加到15。运行脚本以检查是否一切正常。

现在,我认为是时候添加outputbox了,我们在编写时将看到markdown源代码的html输出。

为此,我们要添加一个HTMLLabel,在init_window函数中是这样的: 

self.outputbox = HTMLLabel(self, width="1"background="white"html="<h1>linuxidc.com</h1>" 
self.outputbox.pack(fill=BOTHexpand=1side=RIGHT 
self.outputbox.fit_height()  
  • 1.
  • 2.
  • 3.

我们使用tkhtmlview中的HTMLLabel,宽度仍旧为1。我们将宽度设置为1,因为窗口将在输入字段和输出框之间以1:1的比例共享(运行脚本时您会明白我的意思)。

html关键字参数存储将在第一次显示的值。

然后,将其打包在窗口中,将side作为RIGHT置于输入字段的右侧。fit_height()使文本适合小部件。

现在运行代码,如下所示: 

现在,如果您开始在输入字段中书写,输入时输出不会得到更新。那是因为我们还没有告诉我们的程序这样做。

为此,我们首先要与编辑器绑定一个事件。然后,你进行修改文本,输出都会得到更新,如下所示: 

self.inputeditor.bind("<<Modified>>", self.onInputChange) 
  • 1.

将这一行放到init_window()函数中。

这一行告诉inputeditor在文本改变时调用onInputChange函数。但是因为我们还没有那个函数,我们需要把它写出来。 

...  
def onInputChange(self , event):  
    self.inputeditor.edit_modified(0)  
    md2html = Markdown()  
    self.outputbox.set_html(md2html.convert(self.inputeditor.get("1.0" , END)))  
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.

在第一行中,我们使用edit_modified(0)重置修改后的标志,以便重用它。否则,在第一次事件调用之后,它将不再工作。

接下来,我们创建一个名为md2html的Markdown对象。最后一行(上面标红那行),首先我们…等等!最后一行可能会让一些读者感到困惑。我把它分成三行。 

markdownText = self.inputeditor.get("1.0" , END)  
html = md2html.convert(markdownText) 
 self.outputbox.set_html(html)  
  • 1.
  • 2.
  • 3.

在第一行中,我们从输入字段的顶部到底部获取markdown文本。第一个参数,self.inputeditor.get,告诉它从第一行的第0个字符开始扫描(1.0 => [LINE_NUMBER].[CHARACTER_NUMBER]),最后一个参数告诉它在到达末尾时停止扫描。

然后,我们使用md2html.convert()函数将扫描的markdown文本转换为html,并将其存储在html变量中。

最后,我们告诉outputbox使用.set_html()函数来显示输出!

运行脚本。您将看到一个功能几乎正常的markdown编辑器。当您输入输入字段时,输出也将被更新。

但是…我们的工作还没有完成。用户至少需要能够打开和保存他们的文本。

为此,我们要在菜单栏中添加一个文件菜单。在这里,用户可以打开和保存文件,也可以退出应用程序。

在init_window函数中,我们将添加以下行:

self.mainmenu = Menu(self)  
self.filemenu = Menu(self.mainmenu)  
self.filemenu.add_command(label="打开"command=self.openfile)  
self.filemenu.add_command(label="另存为"command=self.savefile)  
self.filemenu.add_separator()  
self.filemenu.add_command(label="退出"command=self.quit)  
self.mainmenu.add_cascade(label="文件"menu=self.filemenu)  
self.master.config(menu=self.mainmenu)  
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.

简单说一下:

在这里,我们定义了一个新菜单,框架作为它的父菜单。

接下来,我们定义另一个菜单和上一个菜单作为其父菜单。它将作为我们的文件菜单。

然后使用add_command()和add_separator()函数添加3个子菜单(打开、另存为和退出)和分隔符。打开子菜单将执行openfile函数,另存为子菜单将执行savefile函数。最后,Exit将执行一个内建函数quit,该函数将关闭程序。

然后使用add_cascade()函数告诉第一个菜单对象包含filemenu变量。这包括标签文件中的所有子菜单。

最后,我们使用self.master.config()来告诉窗口使用主菜单作为窗口的菜单栏。

它看起来是这样的,但是现在还不要运行它。你会提示错误,openfile和savefile函数没有定义。

正如您现在看到的,我们必须在Window类中定义两个函数,我们将在其中使用tkinter的filedialog。

首先让我们定义打开文件的函数:

def openfile(self):  
    openfilename = filedialog.askopenfilename(filetypes=(("Markdown File", "*.md , *.mdown , *.markdown"),  
                                                                  ("Text File", "*.txt"),  
                                                                  ("All Files", "*.*")))  
    if openfilename:  
        try:  
            self.inputeditor.delete(1.0, END)  
            self.inputeditor.insert(END , open(openfilename).read())  
        except:  
            print("无法打开文件!")   
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.

在这里,首先我们向用户显示一个文件浏览器对话框,允许他们使用filedialog.askopenfilename()选择要打开的文件。与filetypes关键字参数,我们告诉对话框只打开这些类型的文件通过传递一个元组与支持的文件(基本上所有类型的文件):

  •  带 .md , .mdown , .markdown扩展名的文件
  •  扩展名为.txt的文本文件
  •  在使用通配符扩展的下一行中,我们告诉对话框打开任何扩展名的文件。

然后我们检查用户是否选择了一个文件。如果是,我们尝试打开文件。然后删除输入字段中从第一行的第0个字符到字段末尾的所有文本。

接下来,我们打开并读取所选文件的内容,并在输入字段中插入内容。

如果我们的程序不能打开一个文件,它将打印出错误。但是等等,这不是处理错误的好方法。我们在这里可以做的是向用户显示一个类似这样的错误消息:

为此,我们首先要从tkinter包中导入消息框messagebox。 

from tkinter import messagebox as mbox 
  • 1.

然后,不像上面那样只是打印一个错误消息,我们将用下面的行替换那一行,以便向用户显示正确的错误消息。

mbox.showerror(“打开选定文件时出错 " , "哎呀!,您选择的文件:{}无法打开!".format(openfilename))

这将创建一个错误消息,就像我上面显示的文件无法打开时的屏幕截图一样。

mbox.showerror函数,第一个参数是消息框的标题。第二个是要显示的消息。

现在,我们需要编写一个savefile函数来保存markdown输入。 

def savefile(self):  
        filedata = self.inputeditor.get("1.0" , END)  
        savefilename = filedialog.asksaveasfilename(filetypes = (("Markdown File", "*.md"),  
                                                                  ("Text File", "*.txt")) , title="保存 Markdown 文件" 
        if savefilename:  
            try:  
                f = open(savefilename , "w")  
                f.write(filedata)  
            except:  
                mbox.showerror("保存文件错误" , "哎呀!, 文件: {} 保存错误!".format(savefilename))  
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.

在这里,首先我们扫描输入字段的所有内容并将其存储在一个变量中。然后,我们通过为两种类型的文件类型(.md和.txt)。

如果用户选择一个文件名,我们将尝试保存存储在变量filedata中的输入字段的内容。如果发生异常,我们将向用户显示一条错误消息,说明程序无法保存文件。

不要忘记测试您的应用程序以检查任何bug !如果你的程序没有错误,运行完美应该是这样的:

OK,本文就这样,你学会了吗?