I’m a software developer, and I’m also a gamer. Most of us who grew up with Lego love playing Minecraft, because you never run out of bricks to build within Minecraft. It’s an amazing game. But it can also be frustrating at times. This article explains how I solved one of those frustrations using Python.
When it comes to hobby projects in software development, the notion of “scratching that personal itch` often comes up — and this article is about precisely that. You see, when I play Minecraft, I often find myself building repeating patterns or decorations of various kinds. For example, if I’m building a house, each section of wall probably looks the same — a repeating pattern. To design it, I start by building a template. Then I revise it, change it, and work with it until I’m happy. Then, it’s time to copy the template to all sections of the wall where I want it. In Minecraft, this is done manually — each block is placed individually by the player. And if you’re building a large building like a castle or some such, that’s a lot of tedious, error-prone work that goes something like this:
This running back and forth inside the game between your template and your current building area takes a lot of time. So to avoid this, I started taking screenshots of the template (with the F2 key inside Minecraft), and then displaying the screenshot from a secondary screen which I can reference while I’m playing on my primary screen. This method works, but you still have to manually bring up the screenshot on your secondary screen every time you take a new one.
So, to solve this problem — to scratch this personal itch — I decided to write a very simple Python program called ImageMon, that would automatically display the last screenshot I took at all times. That way, I only have to start that program once on my secondary screen, and any time I take a new in-game screenshot, my program will display it automatically.
Note: ImageMon uses
pillow, the drop-in replacement for
PIL, the Python Image Library. It is best to install pillow in a virtual environment. However, an explanation of how this is done is beyond the scope of this article; go look up a virtualenv tutorial if you need assistance with this part, and then install
pillow in your virtual environment.
All we need to display images (such as screenshots) in Python is TkInter, which typically comes with Python when you install it. However, we also want to be able to resize these screenshots to make them fit nicely on our secondary screen, and for that we need the pillow module which you can install with pip.
Okay. Let’s start with the imports. Please note that the code in this article is in image format. However, should you wish to copy and paste, it is also available on GitHub.
#!/usr/bin/env python3 """ ImageMon by Christer Enfors 2021. Released under GPL version 3. Run this program while playing Minecraft Java Edition. If you make a screenshot from inside Minecraft with the F2 key, this program will display that screenshot in a separate window. """ from os import listdir from os.path import isfile, join, expanduser import time import tkinter as tk from PIL import Image, ImageTk, UnidentifiedImageError
Okay, good. Now, let’s continue with a function which finds all screenshot files in the screenshots folder:
def get_screenshot_list(path: str): files = [f for f in listdir(path) if isfile(join(path, f)) and f.endswith(".png")] return files
This function is very short thanks to the clever list comprehension functionality of Python, which allows us to create lists using a very compact syntax. But before we can understand how it works, there are a few functions that it uses that we first might need to explain:
listdir. This function was imported from
osabove, and it gives you a list of all files and directories inside a given path.
isfile. This was imported from
os.path, and it returns
Trueif we give it the name of a file as an argument. If we give it the name of a directory, it returns
False. This means that if there are directories inside Minecraft’s screenshots directory for some reason, we can weed them out.
join. This function was also imported from
os.path, and it allows us to concatenate a path with a file name in a portable way. For example, given the path “
screenshots” and the file name “
joinfunction will give us “
screenshots\screenshot-1.png” on Windows, but “
screenshot/screenshot-1.png” on Linux (since Windows uses backslashes and Linux uses slashes).
Now that we have an idea of what these functions do, we can attempt to undertand the list comprehension itself. Think of it this way:
“We want the files variable to be a list of each file in the given path if that file is in fact a file and not a directory and it ends with .png”.
List comprehensions can be a bit tricky to understand at first, so don’t worry if you’re not sure how this one works exactly. You can still learn from this article even if you don’t fully understand this part.
Next, we need a function which displays an image, after first having made sure we stop displaying the previous screenshot if we’re already displaying one:
def show_image(img_path: str, old_img: tk.Label): if old_img: old_img.pack_forget() image_loaded = False while image_loaded is not True: try: image = Image.open(img_path) image_loaded = True except UnidentifiedImageError: # We just noticed that the file is there, but loading it # fails. Why? Probably because it is still in the process # of being created, which could take a second. So, let's # give it exactly that - a second, then try again. time.sleep(1) # Change 'width' to make the window a suitable size for your # particular screen resolution. width = 1670 resized_image = image.resize((width, width*9//16)) img = ImageTk.PhotoImage(resized_image) label1 = tk.Label(image=img) label1.image = img label1.pack() return label1
Trueif we succeed, thus ending the loop. We load it using Image which we imported from
//”) means integer division — it always gives us an integer as a result. If you’re playing Minecraft on a screen with a different aspect ratio than 16:9, then change these numbers accordingly. For example, on an old-style 4:3 ratio screen, you’d multiply the width by 3 divided by 4.
Now that we have the supporting functions in place, we can implement our
def main(): root = tk.Tk() # Get the path to the directory where Minecraft stores screenshots. # In Linux, it's ~/.minecraft/screenshots, and in Windows it's # %appdata%\.minecraft\screenshots. # Using expanduser("~"), we get the first part - the home directory. # Using join, we can then concatenate that with the remaining parts # of the path to get the full path in a portable way. path = join(expanduser("~"), ".minecraft", "screenshots") old_list = get_screenshot_list(path) old_img = None try: while True: time.sleep(.1) new_list = get_screenshot_list(path) new_files = [f for f in new_list if f not in old_list] if new_files: old_img = show_image(join(path, new_files), old_img) old_list = new_list.copy() try: root.update() except tk.TclError: # The user has probably closed the window. Just exit. return except KeyboardInterrupt: return
expanduserfunction which we imported from
os.pathto find the directory inside the user’s home directory in which Minecraft stores its screenshots.
busyloop— a loop which frantically looks for new files as fast as it can, eating up CPU time and I/O resources needlessly. Looking once per second is enough.
get_screenshot_listfunction to get a list of screenshots.
new_list, and we want the resulting
new_fileslist to be a list of all files that exist in
new_listbut not in
old_list— that is to say, we want the
new_fileslist to (unsurprisingly) contain the new screenshots since last time we looked a second ago.
show_imagefunction on the first of them. If there are multiple new screenshots since last second, then only one of them will be displayed, that’s not a problem. Note that we’re also supplying
show_image, so that it can use it to stop showing the currently shown image, if any.
new_listis the old list for the next run through the loop.
rootobject, to update the display.
All we have left to do now is add the all-important code that calls our
if __name__ == "__main__": main()
And that’s it! We now have a working program that displays any new screenshots as soon as they’re taken in Minecraft.