DEV Community

Cover image for Let's Create a Toy Markdown Editor with Python Tkinter!
Palash Bauri 👻
Palash Bauri 👻

Posted on • Edited on • Originally published at dev.palashbauri.in

Let's Create a Toy Markdown Editor with Python Tkinter!

Markdown Editor are one of the trending things nowadays. Everybody is creating a markdown editor, some of them are innovative but some of them are boring. In the meantime, I always been a fan of doing things which won't be done by others. (I will explain why other don't want to build markdown editor with Tkinter)

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

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 away)
Tkinter Tutorials: Tkinter Basics , FreeCodeCamp Tkinter Course , TheNewBoston Tkinter Playlist etc. (More can be found one google away)

So, before we start, I want to explain why people don't want to build markdown editors with tkinter. Because there's no default easy way to display 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.

But one day, while I was roaming the streets of Internet I found Something Interesting , tk_html_widgets which can display html output! But ofcourse it did have some problems: the fonts were too small, and had no support for attaching remote photos. So as usual I created my own fork and fixed some issues and kinda improved the stability and named it : tkhtmlview. 😎

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

🛠️ 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).

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)

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 necessary libraries.

from tkinter import *
from tkinter import font , filedialog
from markdown2 import Markdown
from tkhtmlview import HTMLLabel
Enter fullscreen mode Exit fullscreen mode

In the first line we import everything (almost) from tkinter package.
In the second line we import font and filedialog. font is needed to style (eg. Font , Font Size) our input field, and filedialog is imported to open files markdown files for editing and/or save our markdown file.
in 3rd line, Markdown is imported to help us convert our markdown source to html and display on output field using HTMLLabel which we import on 4th line.

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


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)

Enter fullscreen mode Exit fullscreen mode

Here in this codeblock, we first define a class called Window which inherits tkinter's Frame widget class. 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. Next we declare a custom font object called self.myfont with font family of Helvetica (You can choose any font family you want) and size of 14 which will be used in our markdown input field, finally we call the init_window function where we will put the heart of our application.

In the init_window function we first set the title of the window as TDOWN and in the next line self.pack(fill=BOTH, expand=1) , we tell your Frame to take the full space of our window. We set fill keyword argument to BOTH which is actually being import from 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) and tells our Frame to be expandable , in simple terms the Frame will be filling the window no matter if we stretch the window size , or maximize it.

Now if you run the tdown.py script you'll not see anything , because we have only defined the class but never called it.
To fix this we are gonna put this at the end of our script

root = Tk()
root.geometry("700x600")
app = Window(root)
app.mainloop()
Enter fullscreen mode Exit fullscreen mode

Here we create a Tk object and store it in root variable which will serve as the root of our Window class. Next we set the geometry of our window to 700x600, 700 is height and 600 is width of the window. In the next line you can see , we are creating a Window object, pushing root variable as root of the frame and storing it in a variable called app. Next thing we do is just call the mainloop function which tells our app to run! 😊

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

Blank Tkinter Frame

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

...
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)
Enter fullscreen mode Exit fullscreen mode

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 width of 1. Don't scratch your head, here sizings are done using ratios. You'll understand it more clearly in next few seconds when we will put the output box. 😁
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 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 will arise, that's why I told you earlier to create a custom font object (self.myfont), now if you do something like this:


self.inputeditor = Text(self, width="1" , font=self.myfont)
Enter fullscreen mode Exit fullscreen mode

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

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

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.
To do that we are gonna add a HTMLLabel something like this inside init_window function:

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()
Enter fullscreen mode Exit fullscreen mode

We use HTMLLabel from tkhtmlview with 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'll run the script).
html keyword argument stores the value which will be shown on first time.
Then we pack it in the window with side as RIGHT to put this in right side of the Input Field.
and the fit_height() makes the texts fit inside the widget (As far as I think 😁)

Run the code.

Output Box Added!

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

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

self.inputeditor.bind("<<Modified>>", self.onInputChange)
Enter fullscreen mode Exit fullscreen mode

Put that line inside the init_window() function

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 are gonna write it.

...
def onInputChange(self , event):
    self.inputeditor.edit_modified(0)
    md2html = Markdown()
    self.outputbox.set_html(md2html.convert(self.inputeditor.get("1.0" , END)))
Enter fullscreen mode Exit fullscreen mode

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.
Next we create a Markdown object named md2html and on the last line, where first we.... wait! the last line maybe confusing for some readers. So let me it break it down into three lines.

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

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

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

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

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 atleast open and save their texts.

To do that, we are gonna add a File menu in the menubar , where users will be able to Open and Save Files as well as Quit from the application!

In the init_window function we will add these:

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)
Enter fullscreen mode Exit fullscreen mode

I'll make this quick. Here we define a new Menu with Frame as its parent. Next, We define another Menu and previous menu as its parent. It will serve as our File menu. Then we add 3 sub menus (Open , Save as , 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 savefile function. and finally the Exit will execute a builtin function quit which will close the program.

Then using the add_cascade() function we tell the first Menu object to include the filemenu variable which includes all our sub-menus with the label File. At last we use self.master.config() to tell our window to use mainmenu as our window's menubar.

Menu Added

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

As you can understand now, we have to define two functions inside Window class , where we will use tkinter's 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!")     
Enter fullscreen mode Exit fullscreen mode

Here, at first we show the user a file browser dialog to choose a file to open using filedialog.askopenfilename(). With 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 file) : Markdown files with .md , .mdown , .markdown extensions, Text Files with .txt extension, and in the next row using a wildcard extension, we tell the dialog to open files with any extension.

Then we check if 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 first line's 0th character to the END of the field, 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, we can do here is show an Error Message to the user something like this:

Showing an Error Message

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

from tkinter import messagebox as mbox
Enter fullscreen mode Exit fullscreen mode

then instead of just printing a 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))
Enter fullscreen mode Exit fullscreen mode

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 , first argument is the title of the messagebox, and second one is the message to be displayed.

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

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))

Enter fullscreen mode Exit fullscreen mode

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

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

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 look something like this,

Final Product

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

We Finally Did it!

If you get into any problems regarding this article you can let me know in the comments or DM me on twitter at @bauripalash.

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 atleast has better html support (Full List).

  • 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.

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

So... I hope you enjoyed this article and learnt 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 did any mistakes above and your ideas or suggestions via comments or DM.

Thank You. 👻

Top comments (1)

Collapse
 
tcgumus profile image
Tuna Çağlar Gümüş • Edited

Man, this is a good effort. Thank you.