How to create your own clipboard manager using python and Tkinter
I’m sure everyone at some point in their lives must have wanted to be able to copy multiple items from different places, and then pick which one to paste at leisure. Being a software developer, one of my main skills is in copy-pasting, and working for a restrictive company I was advised not to use the publicly available clipboard management applications. That’s because they could be using the data you copy for their own use, and if you are like me, I tend to copy a lot of passwords, which I do not want anyone to see (for reasons other than security ;) That’s why I thought about writing my own clipboard manager (I call it Clippy), and learning Tkinter in the process.
Tkinter is Python’s standard graphical user interface (GUI) package built on top of Tcl (Tool command Language). I did my research and I found 2 options for GUIs in python that are recommended by most: Tkinter and PyQt5. I will not go into the details about their differences, but the biggest ones that I found were that Tkinter is built-in with python, and has better documentation. PyQt5, on the other hand, is
- An external module that needs to be downloaded separately.
- Is still relatively new, and so most of the articles and videos that I searched for were for PyQt4.
- PyQt5 is not backwards compatible with PyQt4.
That was enough to convince me to use Tkinter.
Meet Clippy
Enough background, let’s jump straight into the code! (You can find the link to the entire code in my github repo here). This is what Clippy looks like in action:
The application mainly consists of 2 elements, a GUI handler(Tkinter
) and a clipboard handler(pyperclip
).
Let’s see both of them in detail.
Tkinter module
The Tkinter module contains everything we need to work with the toolkit. The first thing we need to do for a Tkinter application is import the tkinter
module (it’s named Tkinter
in python 2.x and tkinter
in python 3.x), and create the root element:
Once we do that, we create the root
element, which is a basic window with a title bar. The root element needs to be created before creating any other widget.
The next command is the mainloop()
function. This starts a never ending event loop, and the program stays in this loop until we close the main window. The way this works is that we create our application (define layout, callbacks etc.) and then call the mainloop function. Once the mainloop function is hit, our window becomes visible, and the application goes into Tkinter’s event loop, which can handle events from the user (through callbacks) or the windowing system (such as redraw events). It also includes display updates, which is why our application will not be visible until the mainloop function is executed.
It can be visualized through this diagram:
Once that is done, let’s talk about the different Tkinter widgets that we will use for our application.
Frame Widget
The first is the Frame
widget. It is mainly used as a base widget for other widgets, and it can group the widgets into layouts. Think of it as a convenient place holder to keep everything else that we will build on our application. For the example application in this post, we will build everything on top of root
itself, but Frame
will come handy when there are a lot more widgets to handle.
Label Widget
We will use this to create clickable buttons which will contain the value of the copied text. Wait what? If Tkinter offers a button widget, why not use that instead? Yes, Tkinter does offer a button widget, and trust me, I tried using buttons in the first release. Turns out, Tkinter has some ongoing issues with mac, and no matter what I tried, I could not get the button widget to resize. So I turned to the label widget instead, and created a button out of it. Easy right? (It’s not much different code-wise)
label = Label(text="Hello", cursor="plus", relief=RAISED, pady=5, wraplength=500)
This creates a simple label with Hello
as text, what cursor to show when the mouse is moved over the label, and some other aspects to make it look like a button. (relief=RAISED). We will talk later about how to make it behave like a button (we will animate the clicking). The wraplength
defines when the label’s text should be wrapped into multiple lines. It’s good aesthetics for longer copied text.
These are the basic widgets I will be using to create the application. For all other widgets, I would recommend checking out the official Tkinter documentation here.
Clipboard content
I used pyperclip
to manage the clipboard content. It has the simplest documentation of any module that I have come across:
>>> import pyperclip
>>> pyperclip.copy('The text to be copied to the clipboard.')
>>> pyperclip.paste()
'The text to be copied to the clipboard.'
That’s it! Now what we want to do is to keep calling thepyperclip.paste()
function indefinitely, and then handle any new text that it finds.
In order to avoid going through the pain of understanding multi-threading and making use of another thread to keep track of the clipboard content (which keeps calling the paste() function indefinitely in the background), Tkinter provides a neat functionality called
after
, which basically registers a callback function that is called after a given period of time. We can make the function call itself indefinitely by re-registering its callback inside itself. Example ahead.
Connecting it all together
Now we know the basics of Tkinter and pyperclip, we can see Clippy in action!
Creating a basic layout
For starters, we are going to create a label and put it on our application. It will reflect the content of the clipboard dynamically. We already saw how to create a basic label:
label = Label(root, text="", cursor="plus", relief=RAISED, pady=5, wraplength=500)label.pack()
The text
field is empty because we will populate it dynamically using the logic explained below.
The pack
command is to set the label widget onto the root
window. It basically packs widgets into rows or columns.
Checking for new clipboard value
We create a simple function, let’s call it updateClipboard
, which we are going to call indefinitely, and it will keep checking the clipboard to see if there is anything new:
The code should be self explanatory.
Once we have the cliptext
from our function, it’s time to process it.
Processing cliptext
We need to take care of one thing for now when we are processing clippings:
- Removing all characters that are outside the Tcl range (U+0000-U+FFFF). So basically all characters with unicode value > 65535 need to be removed. (Not a fun fact: Smileys have a higher unicode value than allowed by tcl <sad smiley>)
Once our cliptext
is nice and tidy, we can now add it to our label.
Updating label with value
Updating the label’s text is a piece of cake.
Copy label content to clipboard on clicking
Once the label is ready, we can bind a click
event to that label, so that once we click it, the label text is copied for us to paste! We bind a click event to the label like this:
label.bind("<Button-1>", lambda event, labelElem=label:
onClick(labelElem))
A little explanation needed here for those uncomfortable with lambda
functions. It took me some time as well coming from a java background, but trust me, they make your life so easy.
W3schools define lambda
functions to be small anonymous functions. You can refer to w3schools for more info. So technically, what we have written above is something like this:
label.bind("<Button-1>",function(event, labelElem=label):
onClick(labelElem))
Since this is a bind
event, Tkinter automatically adds an argument of an event
type, which holds metadata of the event that happened (for example, we can get the x and y coordinate of the position at which the click happened using event.x
and event.y)
. We need to account for this extra argument when we create our lambda function. For this use-case, we don’t care about that though, we just want to pass in the label element when we click on the label, so that we can extract the text from it in the onClick
function.
We define the onClick
function like this:
Let’s see what we have till now:
Before running this code, let’s copy the following line:
this is my first clipping
Once the code is run, we see a tiny window showing something like this:
Copy this line:
this is my second clipping
and now we see it change automatically:
Voila! Our label updates with whatever we copy! Now click on the label, and you will see this is my second clipping
printed on your console, and also copied onto your clipboard!
Enhancements (To be continued…)
There are infinite improvements that can be added onto this application. For starters, we can:
- Add test cases.
- Add animation to the
click
event so that the label feels like a button. - Add multiple labels which can hold multiple copied values.
- Refresh the label values using LRU replacement policies.
- Add a menu item to clear all labels.
- Add another menu item to make this application be on top of all other applications always.
These improvements already exist in my application hosted in GitHub, the link to which is here. I will walk you through all these enhancements in the second part of this tutorial link.
Thank you for reading.