如何使用Python和Tkinter构建Toy Markdown编辑器

Markdown editors are trending these days. Everybody is creating a markdown editor, and some of them are innovative while some of them are boring.

Markdown编辑器近来呈趋势。 每个人都在创建降价编辑器,其中有些人很创新,而有些人很无聊。

As for myself, however, I have always been a fan of doing things which haven't been done by others. (I will explain below why other devs don't want to build a markdown editor with Tkinter.)

至于我自己,我一直热衷于做别人没有做过的事情。 (我将在下面解释为什么其他开发人员不想使用Tkinter构建markdown编辑器。)

If you already are familiar with Python and Tkinter you can easily get into this guide.

如果您已经熟悉Python和Tkinter,则可以轻松阅读本指南。

But If you are just starting out with Python and/or Tkinter, you can check out these: Python Tutorials: FreeCodeCamp Python Tutorial , Python 3 Playlist by sentdex , FreeCodeCamp Python for Beginners etc. (More can be found one Google search away) Tkinter Tutorials: Tkinter Basics , FreeCodeCamp Tkinter Course , TheNewBoston Tkinter Playlist etc. (More can be found one Google search away)

但是,如果您只是刚开始使用Python和/或Tkinter,则可以查看以下内容: Python教程: FreeCodeCamp Python教程senddex的 Python 3播放列表FreeCodeCamp Python初学者等。(更多内容可以从Google搜索中找到) Tkinter教程: Tkinter基础知识FreeCodeCamp Tkinter课程TheNewBoston Tkinter播放列表等。(可以在Google搜索中找到更多内容)

So, before we start, I want to explain why people don't want to build markdown editors with tkinter. It's because there's no default easy way to display the html output of the markdown input. There's not even a default tkinter widget to display html data. You can just simply write/edit markdown, but there's no easy way to display the output inside your application.

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

But one day, while I was roaming the streets of the Internet, I found something interesting: tk_html_widgets. It can display html output!

但是有一天,当我在互联网上漫游时,发现了一些有趣的东西: tk_html_widgets 。 它可以显示html输出!

But ofcourse it did have some problems: the fonts were too small, and it had no support for attaching remote photos. So as usual I created my own fork and fixed some issues and kinda improved the stability. I named it tkhtmlview. 😎

但是当然它确实存在一些问题:字体太小,并且不支持附加远程照片。 因此,像往常一样,我创建了自己的fork,并修复了一些问题,并提高了稳定性。 我将其命名为tkhtmlview 。 😎

Ugh, I think I'm boring you 😅, so let's stop talking and start building.

gh,我想我很无聊,所以让我们停止交谈,开始建设。

Start️开始建设: (🛠️ Start Building:)

First make sure you have Python 3 and Tkinter installed. If not you can download them from here: python.org/downloads (Tkinter is already packed with Python).

首先,请确保您已安装Python 3和Tkinter。 如果不是,您可以从以下位置下载它们: python.org/downloads (Tkinter已经包含Python)。

Other Things we will need are tkhtmlview and markdown2. You can install them by running pip install tkhtmlview markdown2 or pip3 install tkhtmlview markdown2 (if you have multiple versions of Python).

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

Now fire up your favorite editor or IDE and create a new file (eg. tdown.py (I named the editor tdown)). We will start by importing the necessary libraries.

现在启动您喜欢的编辑器或IDE并创建一个新文件(例如tdown.py (我将其命名为tdown编辑器))。 我们将从导入必要的库开始。

from tkinter import *
from tkinter import font , filedialog
from markdown2 import Markdown
from tkhtmlview import HTMLLabel

In the first line we import everything (almost) from the tkinter package.

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

In the second line we import the font and filedialog. font is needed to style (eg. Font , Font Size) our input field, and filedialog is imported to open markdown files for editing (and/or for saving our markdown file).

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

In the 3rd line, Markdown is imported to help us convert our markdown source to html and display it in the output field using HTMLLabel (which we import on the 4th line).

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

After that, we will create a frame class called Window which will be inherited from tkinters's Frame class. It will hold our input and output fields.

之后,我们将创建一个名为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("TDOWN")
        self.pack(fill=BOTH, expand=1)

Here in this codeblock, we first define a class called Window which inherits tkinter's Frame widget class.

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

Now in the initialization function we take master as a argument which will serve as the parent of the frame. In the next line we initialize a Frame.

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

Next we declare a custom font object called self.myfont with the font family Helvetica (you can choose any font family you want) and size 14 which will be used in our markdown input field.

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

Finally we call the init_window function where we will put the heart of our application.

最后,我们调用init_window函数,将其放在应用程序的心脏中。

In the init_window function we first set the title of the window as TDOWN. In the next line self.pack(fill=BOTH, expand=1) , we tell our Frame to take the full space of our window.

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

We set the fill keyword argument to BOTH which is actually being imported from the tkinter library. It tells the frame to fill the window both horizontally and vertically, and the expand keyword argument is set to 1 (signifies True) which tells our Frame to be expandable. In simple terms the Frame will fill the window no matter how we stretch the window size or maximize it.

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

Now if you run the tdown.py script you'll not see anything because we have only defined the class but never called it.

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

To fix this we are gonna put this at the end of our script:

要解决此问题,我们将其放在脚本的末尾:

root = Tk()
root.geometry("700x600")
app = Window(root)
app.mainloop()

Here we create a Tk object and store it in the root variable which will serve as the root of our Window class.

在这里,我们创建一个Tk对象,并将其存储在root变量中,该变量将用作Window类的根。

Next we set the geometry of our window to 700x600 - 700 is the height and 600 is tthe width of the window. In the next line you can see that we are creating a Window object. We push the root variable as root of the frame and store it in a variable called app.

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

The next thing we do is just call the mainloop function which tells our app to run! 😊

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

Now run the tdown.py script. You will see a blank window like this if you did everything correctly:

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

Blank Tkinter Frame

But it's just a blank window. To write something in the window we need to add a Text Field where we will write our markdown. To do that we are gonna use the Text widget from tkinter.

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

...
def init_window(self):
    self.master.title("TDOWN")
    self.pack(fill=BOTH, expand=1)

    self.inputeditor = Text(self, width="1")
    self.inputeditor.pack(fill=BOTH, expand=1, side=LEFT)

Don't get confused with the ... (three dots) , I put them there only to signify that there are multiple lines of code before this code block.

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

Here we create a Text widget with a width of 1. Don't scratch your head - here sizes are done using ratios. You'll understand it more clearly in next few seconds when we put in the output box. 😁

在这里,我们创建一个宽度为1的Text小部件。 不要挠头-这里的尺寸是按比例确定的。 当我们将其放入输出框中时,您将在接下来的几秒钟内更清楚地了解它。 😁

We then pack it into the Frame and tell it to be both Horizontally and Vertically stretchable.

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

When you run the script, you'll see that a Multiline Input Field has taken over our whole World Window. If you start writing in it, you may notice that the characters are so tiny!

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

Input Field Has Taken Over the Whole Window!

I already knew this problem would arise. That's why I told you earlier to create a custom font object (self.myfont). Now if you do something like this:

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

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

(Here, we tell our Text widget to use our custom font instead of the default tiny one!)

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

...the font size of the input field will be increased to 14. Run the script to check if everything worked perfectly.

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

Font Size has been increased to 14

Now, I think it's time to add the outputbox where we will see the html output of our markdown source while we write.

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

To do that we are gonna add an HTMLLabel, something like this inside init_window function:

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

self.outputbox = HTMLLabel(self, width="1", background="white", html="<h1>Welcome</h1>")
self.outputbox.pack(fill=BOTH, expand=1, side=RIGHT)
self.outputbox.fit_height()

We use HTMLLabel from tkhtmlview with a width of 1 (again). We set the width to 1 because the window will be shared between Input Field and Output Box with ratio of 1:1 (You'll understand what I mean when you run the script).

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

The html keyword argument stores the value which will be shown the first time.

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

Then we pack it in the window with side as RIGHT to put it in the right side of the Input Field. The fit_height() makes the texts fit inside the widget (as far as I know... 😁)

然后,将其打包在窗口中,其sideRIGHT以将其置于输入字段的右侧。 fit_height()使文本适合小部件内(据我所知...😁)

Now run the code.

现在运行代码。

Output Box Added!

Now if you start writing in the input field, you may be disappointed (but don't be!) to see that the output is not getting updated as we type. That's because we have not told our program to do so yet.

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

To do that we are gonna first bind a event with our editor. Then whenever the text is modified the output will be updated, something like this:

为此,我们将首先将事件与编辑器绑定。 然后,无论何时修改文本,输出都会更新,如下所示:

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

Put that line inside the init_window() function.

将该行放入init_window()函数中。

So basically that line tells the inputeditor to call the onInputChange function whenever the text is changed. But as we don't have that function yet, we need to write it.

因此,基本上,该行告诉输入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)))

In the first line, using edit_modified(0) we reset the Modified flag so that it can be reused. Otherwise, after the first event call, it will not work anymore.

在第一行中,使用edit_modified(0)重置Modified标志,以便可以重用它。 否则,在第一个事件调用之后,它将不再起作用。

Next we create a Markdown object named md2html. On the last line, where first we.... wait! The last line may be confusing for some readers. So let me it break it down into three lines.

接下来,我们创建一个名为md2html的Markdown对象。 在最后一行,我们首先在哪里……等待! 最后一行可能会使某些读者感到困惑。 因此,让我将其分为三行。

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

Here in the first line we fetch the markdown text from top to bottom of the input field. The first argument, self.inputeditor.get, tells it to start scanning from the first line's 0th character (1.0 => [LINE_NUMBER].[CHARACTER_NUMBER]) , and the last argument tells it to stop scanning it has when reached the end.

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

Then we convert the scanned markdown text to html using the md2html.convert() function and store it in the html variable.

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

Finally we tell outputbox to display the output using the .set_html() function!

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

Run the script. You will see a functioning (almost) markdown editor. As you type in the input field, the output will also be updated!

运行脚本。 您将看到一个正常运行的(几乎)降价编辑器。 在输入字段中键入时,输出也将更新!

But...our work is not yet finished. Users need to be able to at least open and save their text.

但是...我们的工作尚未完成。 用户需要至少能够打开并保存其文本。

To do that, we are gonna add a File menu in the menubar. This is where users will be able to open and save files as well as quit the application.

为此,我们将在菜单栏中添加“ File ”菜单。 用户可以在此处打开和保存文件以及退出应用程序。

In the init_window function we will add these lines:

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

self.mainmenu = Menu(self)
self.filemenu = Menu(self.mainmenu)
self.filemenu.add_command(label="Open", command=self.openfile)
self.filemenu.add_command(label="Save as", command=self.savefile)
self.filemenu.add_separator()
self.filemenu.add_command(label="Exit", command=self.quit)
self.mainmenu.add_cascade(label="File", menu=self.filemenu)
self.master.config(menu=self.mainmenu)

I'll make this quick:

我将快速介绍一下:

Here we define a new menu with Frame as its parent.

在这里,我们定义了一个以Frame为父菜单的新菜单。

Next, we define another menu and previous menu as its parent. It will serve as our File menu.

接下来,我们定义另一个菜单,并将上一个菜单作为其父菜单。 它将用作我们的“ File菜单。

Then we add 3 sub menus (Open, Save as, and Exit) and a separator using the add_command() and add_separator() functions. The Open sub-menu will execute the openfile function, the Save as sub-menu will execute the savefile function. and finally Exit will execute a builtin function quit which will close the program.

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

Then using the add_cascade() function we tell the first Menu object to include the filemenu variable. This includes all our sub-menus with the label File.

然后使用add_cascade()函数,告诉第一个Menu对象包括filemenu变量。 这包括所有带有标签File子菜单。

At last we use self.master.config() to tell our window to use mainmenu as our window's menubar.

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

Menu Added

It will look something like this, but don't run it yet. You'll get errors saying that the openfile & savefile functions aren't defined.

它将看起来像这样,但请不要运行它。 您会得到错误消息,提示未定义openfilesavefile函数。

As you can see now, we have to define two functions inside the Window class where we will use tkinter's filedialog.

如您现在所见,我们必须在Window类中定义两个函数,在这里我们将使用tkinter的filedialog。

First Let's define the function to open files:

首先让我们定义打开文件的功能:

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("Cannot Open File!")

Here, at first we show the user a file browser dialog that allows them to choose a file to open using filedialog.askopenfilename(). With the filetypes keyword argument we tell the dialog to only open these types of files by passing a tuple with supported files (basically all types of files):

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

  • Markdown files with .md , .mdown , .markdown extensions,

    具有.md.mdown.markdown扩展名的Markdown文件,

  • Text Files with .txt extension,

    扩展名为.txt文本文件,

  • and in the next row using a wildcard extension, we tell the dialog to open files with any extension.

    然后在下一行使用通配符扩展名,告诉对话框打开具有任何扩展名的文件。

Then we check if the user has selected a file or not. If yes, we try to open the file. Then we delete all the text inside the input field from the first line's 0th character to the END of the field.

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

Next we open and read the content of the selected file and insert the contents in the input field.

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

If our program can't open a file it will print out the error. But wait, that's not a good way to handle errors. What we can do here is show an Error Message to the user that looks like this:

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

Showing an Error Message

To do that, we are gonna first import messagebox from the tkinter package.

为此,我们首先要从tkinter包中导入messagebox

from tkinter import messagebox as mbox

Then instead of just printing an error message like we did above, we're gonna replace that line with the below line to show a proper error message to the user.

然后,我们不仅要像上面那样打印错误消息,还要用下面的行替换该行,以向用户显示正确的错误消息。

mbox.showerror("Error Opening Selected File" , "Oops!, The file you selected : {} can not be opened!".format(openfilename))

This will create an Error message like the above screenshot I showed you when the file can not be opened.

当无法打开文件时,这将产生一条错误消息,如上面的屏幕截图所示。

In the mbox.showerror function, the first argument is the title of the messagebox. The second one is the message to be displayed.

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

Now, we need to write a savefile function to save our markdown input.

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

def savefile(self):
        filedata = self.inputeditor.get("1.0" , END)
        savefilename = filedialog.asksaveasfilename(filetypes = (("Markdown File", "*.md"),
                                                                  ("Text File", "*.txt")) , title="Save Markdown File")
        if savefilename:
            try:
                f = open(savefilename , "w")
                f.write(filedata)
            except:
                mbox.showerror("Error Saving File" , "Oops!, The File : {} can not be saved!".format(savefilename))

Here at first we scan all the content of the input field and store it in a variable. Then we ask the user for the filename where they want to save the contents by giving the option for two types of file types (.md and .txt).

首先,我们在这里扫描输入字段的所有内容,并将其存储在变量中。 然后,我们通过为两种文件类型(.md和.txt)提供选项,要求用户输入文件名,并将其保存在文件中。

If the user chooses a filename we try to save the contents of the input field stored in the variable filedata. If an exception occurs, we show the user an error message stating that the program wasn't able to save the file.

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

Don't forget to test your application to check for any bugs! If you and I haven't made any mistakes, our programs should run perfectly and shouldlook something like this:

不要忘记测试您的应用程序以检查任何错误! 如果您和我没有犯任何错误,那么我们的程序应该可以完美运行并看起来像这样:

Final Product

Full source code for this 'tdown' markdown editor is available at GitHub and also at Repl.it where you can test the editor on your browser!

这个“ tdown”降价编辑器的完整源代码可以在GitHub上找到 ,也可以在Repl.it上找到 ,您可以在浏览器上测试该编辑器!

We Finally Did it!

If you get into any problems as you're going through this article you can let me know in the comments or DM me on twitter at @bauripalash.

如果您在阅读本文时遇到任何问题,可以在评论中让我知道,或者在Twitter上@bauripalash DM上发给我。

一些注意事项: (Some Notes:)

  • First, remember that this is just a toy editor. If you want to build more powerful editor you can use any other GUI libraries such as wxPython, PyQT , Kivy and many more which at least have better html support (Full List).

    首先,请记住,这只是一个玩具编辑器。 如果您想构建更强大的编辑器,则可以使用任何其他GUI库,例如wxPython,PyQT,Kivy等,它们至少具有更好的html支持( 完整列表 )。

  • In this article I only showed how to build a basic editor. You can also add many more cool features of your own such as exporting as HTML or PDF, adding buttons to simplify writing Markdown... etc etc.

    在本文中,我仅展示了如何构建基本编辑器。 您还可以添加自己的许多更酷的功能,例如导出为HTML或PDF,添加按钮以简化Markdown编写等。

  • The HTML Rendering modules tkhtmlview or tk_html_widgets are not fully stable and only support some basic html functionalities, so don't expect much.

    HTML渲染模块tkhtmlview或tk_html_widgets并不完全稳定,仅支持一些基本的html功能,因此不要期望太多。

So... I hope you enjoyed this article and learned some new things. Don't forget to let me know if you are stuck somewhere or can't understand something.

所以...希望您喜欢这篇文章并学到一些新东西。 如果您被困在某个地方或听不懂某事,请不要忘了告诉我。

Last but not Least, please Let me know if I made any mistakes above. And I'd love to hear your ideas or suggestions via comments or DM.

最后但并非最不重要,如果我在上面犯了任何错误,请告诉我。 我很乐意通过评论或DM听到您的想法或建议。

Thank You. 👻

谢谢。 👻

翻译自: https://www.freecodecamp.org/news/lets-create-a-toy-markdown-editor-with-python-tkinter/

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值