使用Python構(gòu)建自己的Markdown編輯器
Markdown編輯器大家應(yīng)該都知道,很受程序員喜歡。許多人都在創(chuàng)建一個(gè)Markdown編輯器,有些很有創(chuàng)意,有些則很無(wú)聊。
不過(guò)很多開(kāi)發(fā)人員不希望使用Tkinter來(lái)構(gòu)建Markdown編輯器,如果您已經(jīng)熟悉Python和Tkinter,您可以輕松進(jìn)入本指南。
在我們開(kāi)始之前,來(lái)解釋一下為什么人們不想用tkinter來(lái)構(gòu)建Markdown編輯器。這是因?yàn)闆](méi)有默認(rèn)的簡(jiǎn)單方法來(lái)顯示markdown輸入的html數(shù)據(jù)。甚至沒(méi)有一個(gè)默認(rèn)的tkinter組件來(lái)顯示html數(shù)據(jù)。您可以簡(jiǎn)單地編寫/編輯markdown,但是沒(méi)有簡(jiǎn)單的方法在應(yīng)用程序中顯示輸出。
但是,現(xiàn)在有了tk_html_widgets,它可以幫助我們顯示html輸出。
現(xiàn)在讓我們能開(kāi)始構(gòu)建吧。
開(kāi)始構(gòu)建:
首先,請(qǐng)確保您已安裝Python 3和Tkinter。如果沒(méi)有,您可以從這里下載:
python.org/downloads(Tkinter已包含Python中)。
我們需要的其他東西是tkhtmlview和markdown2。您可以通過(guò)運(yùn)行pip install tkhtmlview markdown2或pip3 install tkhtmlview markdown2來(lái)安裝它們(如果您有多個(gè)Python版本)。
現(xiàn)在啟動(dòng)您喜歡的編輯器或IDE并創(chuàng)建一個(gè)新文件(例如www.linuxidc.com.py(我將其命名為linuxidc.com編輯器))。
我們將從導(dǎo)入必要的庫(kù)開(kāi)始。
- from tkinter import *
- from tkinter import font , filedialog
- from markdown2 import Markdown
- from tkhtmlview import HTMLLabel
在第一行中,我們從tkinter包中導(dǎo)入(幾乎)所有內(nèi)容。
在第二行中,我們導(dǎo)入字體和文件對(duì)話框。需要使用font來(lái)設(shè)置輸入字段的樣式(例如Font,F(xiàn)ont Size),并導(dǎo)入filedialog以打開(kāi)markdown文件以進(jìn)行編輯(和/或保存我們的markdown文件)。
在第三行中,導(dǎo)入了Markdown,以幫助我們將Markdown源轉(zhuǎn)換為html,并使用HTMLLabel(在第四行中導(dǎo)入)將其顯示在輸出字段中。
之后,我們將創(chuàng)建一個(gè)名為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=BOTH, expand=1)
在此代碼塊中,我們首先定義一個(gè)稱為Window的類,該類繼承tkinter的Frame小部件類。
現(xiàn)在,在初始化函數(shù)中,我們將master作為參數(shù),用作框架的父級(jí)。在下一行中,我們初始化一個(gè)Frame。
接下來(lái),我們聲明一個(gè)名為self.myfont的自定義字體對(duì)象,其字體家族為Helvetica(您可以選擇任何字體家族),大小為15,將在我們的markdown輸入字段中使用。
最后,我們調(diào)用init_window函數(shù),將我們的應(yīng)用程序置于核心位置。
在init_window函數(shù)中,我們首先將窗口的標(biāo)題設(shè)置為linuxidc.com編輯器。在下一行self.pack(fill=BOTH, expand=1)中,我們告訴Frame占用窗口的全部空間。
我們將fill關(guān)鍵字參數(shù)設(shè)置為BOTH,這實(shí)際上是從tkinter庫(kù)導(dǎo)入的。它告訴框架在水平和垂直方向上都填充窗口,并且expand關(guān)鍵字參數(shù)設(shè)置為1(表示True),這告訴我們框架是可擴(kuò)展的。簡(jiǎn)而言之,無(wú)論我們?nèi)绾卫齑翱诖笮』蜃畲蠡翱诖笮?,框架都將填充窗口?/p>
現(xiàn)在,如果您運(yùn)行www.linuxidc.com.py腳本,您將看不到任何內(nèi)容,因?yàn)槲覀儍H定義了該類,但從未調(diào)用過(guò)它。
為了解決這個(gè)問(wèn)題,我們將以下代碼放在腳本的末尾:
- root = Tk()
- root.geometry("800x600")
- app = Window(root)
- app.mainloop()
接下來(lái),將窗口的幾何形狀設(shè)置為800x600的長(zhǎng)方體,800是窗口的高度,600是窗口的寬度。在下一行中,您可以看到我們正在創(chuàng)建一個(gè)Window對(duì)象。我們將root變量推入框架的root,并將其存儲(chǔ)在名為app的變量中。
接下來(lái)要做的就是調(diào)用mainloop函數(shù),該函數(shù)告訴我們的應(yīng)用程序運(yùn)行!
現(xiàn)在運(yùn)行www.linuxidc.com.py腳本。如果正確完成所有操作,您將看到一個(gè)空白窗口,如下所示:
但這只是一個(gè)空白窗口。要在窗口中寫入內(nèi)容,我們需要添加一個(gè)文本字段,在其中寫入我們的markdown。為此,我們將使用tkinter中的Text小部件。
- ...
- def init_window(self):
- self.master.title("linuxidc.com編輯器")
- self.pack(fill=BOTH, expand=1)
- self.inputeditor = Text(self, width="1")
- self.inputeditor.pack(fill=BOTH, expand=1, side=LEFT)
不要與...混淆(三個(gè)點(diǎn)),我把它們放在那里只是為了表示在此代碼塊之前有多行代碼。
在這里,我們創(chuàng)建了一個(gè)寬度為1的Text小部件。不要誤會(huì),以為錯(cuò)了-這里的大小是使用比例來(lái)完成的。當(dāng)我們將其放入輸出框中時(shí),您將在接下來(lái)的幾秒鐘內(nèi)更清楚地了解它。
然后,我們將其包裝到框架中,并使其在水平和垂直方向上均可拉伸。
運(yùn)行腳本時(shí),您會(huì)看到已接管了整個(gè)“窗口”。如果您開(kāi)始寫它,您可能會(huì)注意到字符太小了。
我已經(jīng)知道會(huì)出現(xiàn)這個(gè)問(wèn)題。這就是為什么我之前告訴過(guò)您創(chuàng)建自定義字體對(duì)象(self.myfont)的原因?,F(xiàn)在,如果您執(zhí)行以下操作:
- self.inputeditor = Text(self, width="1" , font=self.myfont)
(這里,我們告訴Text小部件使用自定義字體,而不是默認(rèn)的小字體!)
...輸入字段的字體大小將增加到15。運(yùn)行腳本以檢查是否一切正常。
現(xiàn)在,我認(rèn)為是時(shí)候添加outputbox了,我們?cè)诰帉憰r(shí)將看到markdown源代碼的html輸出。
為此,我們要添加一個(gè)HTMLLabel,在init_window函數(shù)中是這樣的:
- self.outputbox = HTMLLabel(self, width="1", background="white", html="<h1>linuxidc.com</h1>")
- self.outputbox.pack(fill=BOTH, expand=1, side=RIGHT)
- self.outputbox.fit_height()
我們使用tkhtmlview中的HTMLLabel,寬度仍舊為1。我們將寬度設(shè)置為1,因?yàn)榇翱趯⒃谳斎胱侄魏洼敵隹蛑g以1:1的比例共享(運(yùn)行腳本時(shí)您會(huì)明白我的意思)。
html關(guān)鍵字參數(shù)存儲(chǔ)將在第一次顯示的值。
然后,將其打包在窗口中,將side作為RIGHT置于輸入字段的右側(cè)。fit_height()使文本適合小部件。
現(xiàn)在運(yùn)行代碼,如下所示:
現(xiàn)在,如果您開(kāi)始在輸入字段中書寫,輸入時(shí)輸出不會(huì)得到更新。那是因?yàn)槲覀冞€沒(méi)有告訴我們的程序這樣做。
為此,我們首先要與編輯器綁定一個(gè)事件。然后,你進(jìn)行修改文本,輸出都會(huì)得到更新,如下所示:
- self.inputeditor.bind("<<Modified>>", self.onInputChange)
將這一行放到init_window()函數(shù)中。
這一行告訴inputeditor在文本改變時(shí)調(diào)用onInputChange函數(shù)。但是因?yàn)槲覀冞€沒(méi)有那個(gè)函數(shù),我們需要把它寫出來(lái)。
- ...
- def onInputChange(self , event):
- self.inputeditor.edit_modified(0)
- md2html = Markdown()
- self.outputbox.set_html(md2html.convert(self.inputeditor.get("1.0" , END)))
在第一行中,我們使用edit_modified(0)重置修改后的標(biāo)志,以便重用它。否則,在第一次事件調(diào)用之后,它將不再工作。
接下來(lái),我們創(chuàng)建一個(gè)名為md2html的Markdown對(duì)象。最后一行(上面標(biāo)紅那行),首先我們…等等!最后一行可能會(huì)讓一些讀者感到困惑。我把它分成三行。
- markdownText = self.inputeditor.get("1.0" , END)
- html = md2html.convert(markdownText)
- self.outputbox.set_html(html)
在第一行中,我們從輸入字段的頂部到底部獲取markdown文本。第一個(gè)參數(shù),self.inputeditor.get,告訴它從第一行的第0個(gè)字符開(kāi)始掃描(1.0 => [LINE_NUMBER].[CHARACTER_NUMBER]),最后一個(gè)參數(shù)告訴它在到達(dá)末尾時(shí)停止掃描。
然后,我們使用md2html.convert()函數(shù)將掃描的markdown文本轉(zhuǎn)換為html,并將其存儲(chǔ)在html變量中。
最后,我們告訴outputbox使用.set_html()函數(shù)來(lái)顯示輸出!
運(yùn)行腳本。您將看到一個(gè)功能幾乎正常的markdown編輯器。當(dāng)您輸入輸入字段時(shí),輸出也將被更新。
但是…我們的工作還沒(méi)有完成。用戶至少需要能夠打開(kāi)和保存他們的文本。
為此,我們要在菜單欄中添加一個(gè)文件菜單。在這里,用戶可以打開(kāi)和保存文件,也可以退出應(yīng)用程序。
在init_window函數(shù)中,我們將添加以下行:
- self.mainmenu = Menu(self)
- self.filemenu = Menu(self.mainmenu)
- self.filemenu.add_command(label="打開(kāi)", 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)
簡(jiǎn)單說(shuō)一下:
在這里,我們定義了一個(gè)新菜單,框架作為它的父菜單。
接下來(lái),我們定義另一個(gè)菜單和上一個(gè)菜單作為其父菜單。它將作為我們的文件菜單。
然后使用add_command()和add_separator()函數(shù)添加3個(gè)子菜單(打開(kāi)、另存為和退出)和分隔符。打開(kāi)子菜單將執(zhí)行openfile函數(shù),另存為子菜單將執(zhí)行savefile函數(shù)。最后,Exit將執(zhí)行一個(gè)內(nèi)建函數(shù)quit,該函數(shù)將關(guān)閉程序。
然后使用add_cascade()函數(shù)告訴第一個(gè)菜單對(duì)象包含filemenu變量。這包括標(biāo)簽文件中的所有子菜單。
最后,我們使用self.master.config()來(lái)告訴窗口使用主菜單作為窗口的菜單欄。
它看起來(lái)是這樣的,但是現(xiàn)在還不要運(yùn)行它。你會(huì)提示錯(cuò)誤,openfile和savefile函數(shù)沒(méi)有定義。
正如您現(xiàn)在看到的,我們必須在Window類中定義兩個(gè)函數(shù),我們將在其中使用tkinter的filedialog。
首先讓我們定義打開(kāi)文件的函數(shù):
- 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("無(wú)法打開(kāi)文件!")
在這里,首先我們向用戶顯示一個(gè)文件瀏覽器對(duì)話框,允許他們使用filedialog.askopenfilename()選擇要打開(kāi)的文件。與filetypes關(guān)鍵字參數(shù),我們告訴對(duì)話框只打開(kāi)這些類型的文件通過(guò)傳遞一個(gè)元組與支持的文件(基本上所有類型的文件):
- 帶 .md , .mdown , .markdown擴(kuò)展名的文件
- 擴(kuò)展名為.txt的文本文件
- 在使用通配符擴(kuò)展的下一行中,我們告訴對(duì)話框打開(kāi)任何擴(kuò)展名的文件。
然后我們檢查用戶是否選擇了一個(gè)文件。如果是,我們嘗試打開(kāi)文件。然后刪除輸入字段中從第一行的第0個(gè)字符到字段末尾的所有文本。
接下來(lái),我們打開(kāi)并讀取所選文件的內(nèi)容,并在輸入字段中插入內(nèi)容。
如果我們的程序不能打開(kāi)一個(gè)文件,它將打印出錯(cuò)誤。但是等等,這不是處理錯(cuò)誤的好方法。我們?cè)谶@里可以做的是向用戶顯示一個(gè)類似這樣的錯(cuò)誤消息:
為此,我們首先要從tkinter包中導(dǎo)入消息框messagebox。
- from tkinter import messagebox as mbox
然后,不像上面那樣只是打印一個(gè)錯(cuò)誤消息,我們將用下面的行替換那一行,以便向用戶顯示正確的錯(cuò)誤消息。
mbox.showerror(“打開(kāi)選定文件時(shí)出錯(cuò) " , "哎呀!,您選擇的文件:{}無(wú)法打開(kāi)!".format(openfilename))
這將創(chuàng)建一個(gè)錯(cuò)誤消息,就像我上面顯示的文件無(wú)法打開(kāi)時(shí)的屏幕截圖一樣。
mbox.showerror函數(shù),第一個(gè)參數(shù)是消息框的標(biāo)題。第二個(gè)是要顯示的消息。
現(xiàn)在,我們需要編寫一個(gè)savefile函數(shù)來(lái)保存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("保存文件錯(cuò)誤" , "哎呀!, 文件: {} 保存錯(cuò)誤!".format(savefilename))
在這里,首先我們掃描輸入字段的所有內(nèi)容并將其存儲(chǔ)在一個(gè)變量中。然后,我們通過(guò)為兩種類型的文件類型(.md和.txt)。
如果用戶選擇一個(gè)文件名,我們將嘗試保存存儲(chǔ)在變量filedata中的輸入字段的內(nèi)容。如果發(fā)生異常,我們將向用戶顯示一條錯(cuò)誤消息,說(shuō)明程序無(wú)法保存文件。
不要忘記測(cè)試您的應(yīng)用程序以檢查任何bug !如果你的程序沒(méi)有錯(cuò)誤,運(yùn)行完美應(yīng)該是這樣的:
OK,本文就這樣,你學(xué)會(huì)了嗎?