使用Python構建自己的Markdown編輯器
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
在第一行中,我們從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=BOTH, expand=1)
在此代碼塊中,我們首先定義一個稱為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()
接下來,將窗口的幾何形狀設置為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=BOTH, expand=1)
- self.inputeditor = Text(self, width="1")
- self.inputeditor.pack(fill=BOTH, expand=1, side=LEFT)
不要與...混淆(三個點),我把它們放在那里只是為了表示在此代碼塊之前有多行代碼。
在這里,我們創建了一個寬度為1的Text小部件。不要誤會,以為錯了-這里的大小是使用比例來完成的。當我們將其放入輸出框中時,您將在接下來的幾秒鐘內更清楚地了解它。
然后,我們將其包裝到框架中,并使其在水平和垂直方向上均可拉伸。
運行腳本時,您會看到已接管了整個“窗口”。如果您開始寫它,您可能會注意到字符太小了。
我已經知道會出現這個問題。這就是為什么我之前告訴過您創建自定義字體對象(self.myfont)的原因。現在,如果您執行以下操作:
- self.inputeditor = Text(self, width="1" , font=self.myfont)
(這里,我們告訴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=BOTH, expand=1, side=RIGHT)
- self.outputbox.fit_height()
我們使用tkhtmlview中的HTMLLabel,寬度仍舊為1。我們將寬度設置為1,因為窗口將在輸入字段和輸出框之間以1:1的比例共享(運行腳本時您會明白我的意思)。
html關鍵字參數存儲將在第一次顯示的值。
然后,將其打包在窗口中,將side作為RIGHT置于輸入字段的右側。fit_height()使文本適合小部件。
現在運行代碼,如下所示:
現在,如果您開始在輸入字段中書寫,輸入時輸出不會得到更新。那是因為我們還沒有告訴我們的程序這樣做。
為此,我們首先要與編輯器綁定一個事件。然后,你進行修改文本,輸出都會得到更新,如下所示:
- self.inputeditor.bind("<<Modified>>", self.onInputChange)
將這一行放到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)))
在第一行中,我們使用edit_modified(0)重置修改后的標志,以便重用它。否則,在第一次事件調用之后,它將不再工作。
接下來,我們創建一個名為md2html的Markdown對象。最后一行(上面標紅那行),首先我們…等等!最后一行可能會讓一些讀者感到困惑。我把它分成三行。
- markdownText = self.inputeditor.get("1.0" , END)
- html = md2html.convert(markdownText)
- self.outputbox.set_html(html)
在第一行中,我們從輸入字段的頂部到底部獲取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)
簡單說一下:
在這里,我們定義了一個新菜單,框架作為它的父菜單。
接下來,我們定義另一個菜單和上一個菜單作為其父菜單。它將作為我們的文件菜單。
然后使用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("無法打開文件!")
在這里,首先我們向用戶顯示一個文件瀏覽器對話框,允許他們使用filedialog.askopenfilename()選擇要打開的文件。與filetypes關鍵字參數,我們告訴對話框只打開這些類型的文件通過傳遞一個元組與支持的文件(基本上所有類型的文件):
- 帶 .md , .mdown , .markdown擴展名的文件
- 擴展名為.txt的文本文件
- 在使用通配符擴展的下一行中,我們告訴對話框打開任何擴展名的文件。
然后我們檢查用戶是否選擇了一個文件。如果是,我們嘗試打開文件。然后刪除輸入字段中從第一行的第0個字符到字段末尾的所有文本。
接下來,我們打開并讀取所選文件的內容,并在輸入字段中插入內容。
如果我們的程序不能打開一個文件,它將打印出錯誤。但是等等,這不是處理錯誤的好方法。我們在這里可以做的是向用戶顯示一個類似這樣的錯誤消息:
為此,我們首先要從tkinter包中導入消息框messagebox。
- from tkinter import messagebox as mbox
然后,不像上面那樣只是打印一個錯誤消息,我們將用下面的行替換那一行,以便向用戶顯示正確的錯誤消息。
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))
在這里,首先我們掃描輸入字段的所有內容并將其存儲在一個變量中。然后,我們通過為兩種類型的文件類型(.md和.txt)。
如果用戶選擇一個文件名,我們將嘗試保存存儲在變量filedata中的輸入字段的內容。如果發生異常,我們將向用戶顯示一條錯誤消息,說明程序無法保存文件。
不要忘記測試您的應用程序以檢查任何bug !如果你的程序沒有錯誤,運行完美應該是這樣的:
OK,本文就這樣,你學會了嗎?