Skip to content

[Question] How to take a screenshot using only tkinter? #5208

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
3 of 7 tasks
PySimpleGUI opened this issue Feb 17, 2022 · 27 comments
Closed
3 of 7 tasks

[Question] How to take a screenshot using only tkinter? #5208

PySimpleGUI opened this issue Feb 17, 2022 · 27 comments
Labels
community input desired Port - TK PySimpleGUI Question Further information is requested

Comments

@PySimpleGUI
Copy link
Owner

Type of Issue (Enhancement, Error, Bug, Question)

Question


Environment

Operating System

Windows version 10

PySimpleGUI Port (tkinter, Qt, Wx, Web)

tkinter


Versions

Python version (sg.sys.version)

3.6.8 (tags/v3.6.8:3c6b436a57, Dec 24 2018, 00:16:47) [MSC v.1916 64 bit (AMD64)]

PySimpleGUI Version (sg.__version__)

4.57.0.1

GUI Version (tkinter (sg.tclversion_detailed), PySide2, WxPython, Remi)

8.6.6


Your Experience In Months or Years (optional)

4 Years 3 months Python programming experience
Several Years Programming experience overall
Have not used another Python GUI Framework except for PySimpleGUI


Troubleshooting

These items may solve your problem. Please check those you've done by changing - [ ] to - [X]

  • Searched main docs for your problem www.PySimpleGUI.org
  • Looked for Demo Programs that are similar to your goal Demos.PySimpleGUI.org
  • If not tkinter - looked for Demo Programs for specific port
  • For non tkinter - Looked at readme for your specific port if not PySimpleGUI (Qt, WX, Remi)
  • Run your program outside of your debugger (from a command line)
  • Searched through Issues (open and closed) to see if already reported Issues.PySimpleGUI.org
  • Tried using the PySimpleGUI.py file on GitHub. Your problem may have already been fixed but not released

Detailed Description

Adding the new "Gallery" feature to the PySimpleGUI project. There is a way of capturing a window in the Demo Program Demo_Graph_Drawing_And_Dragging_Figures_2_Windows.py, but it uses the PIL package. The idea is for PySimpleGUI to have this capability built right into the package so that you can get a screenshot of your application using a single keystroke or a function call.

I've looked around a bit and haven't come up with a tkinter-only solution, so turning to our awesome and talented community (and Jason of course ;-)

Code To Duplicate

Screenshot, Sketch, or Drawing


Whatcha Makin?

A gallery feature so that users can easily capture and upload their awesome creations for us all to marvel at!

image

@PySimpleGUI PySimpleGUI added Question Further information is requested community input desired Port - TK PySimpleGUI labels Feb 17, 2022
@PySimpleGUI
Copy link
Owner Author

The "fallback" position for this is likely to be a pip installable psgscreenshot utility that PySimpleGUI will attempt to execute. The psgscreenshot code will use PIL to do the capture since I can have the pip install of it also install PIL.

If the psgscreenshot execution fails then PySimpleGUI can offer to install the package for you and/or give you instructions to install it.

I'm trying to find solutions that are as seamless and "simple" as possible. Ideally would be nice to not have to install the extra stuff, but it may be necessary. PyPI is turning out to be much better at application distribution than I was aware of. It was only after @Chr0nicT wrote the psgcompiler program that I learned the techniques he researched & used. Now things like psgdemos are making it really easy to get the different "add-on" utilities from the PySimpleGUI project.

@jason990420
Copy link
Collaborator

It looks like all the libraries using Pillow to handle the image.

No idea to work it with only tkinter for

  • platform independent screenshot
  • image format processing

@resnbl
Copy link

resnbl commented Mar 8, 2022

Pillow does have an ImageGrab class, but it is broken when used on Retina-screen Macs (that's all I have). I looked into the code, and it simply makes a subprocess call to the Mac "screencapture" command, then tries to clip the image down to the requested bbox size (but fails because the image data is pixel-doubled at 144 dpi and its 72 dpi pixel co-ordinates are off by x2 in each dimension). Also, if you use the sg.Window.size and sg.Window.current_location() to build the bbox for ImageGrab, you need to add 14 (low-res) or 28 (Retina) to the window height to account for the window title bar. (The nice thing about open-source software is I can see the problem and make my fixed version of it for my own use.)

I ran the Demo_Graph_Drawing_And_Dragging_Figures.py demo program, which was touted to perform screen captures, but Pillow 9.0.1 threw an exception on my Mac (OSError: cannot write mode RGBA as JPEG).

Evidently, cross-platform screen manipulation is a "hard computer science problem", and Pillow, like others, don't have the resources for platforms which only make up < 10% of the computer market (despite having the flashiest ads).

@PySimpleGUI
Copy link
Owner Author

PySimpleGUI commented Mar 8, 2022

Pillow, like others, don't have the resources for platforms which only make up < 10% of the computer market (despite having the flashiest ads).

I'm somewhat guilty too...

image

Macs are quite a ways lower in the size of the install base, but I'm determined to keep supporting it.

@PySimpleGUI
Copy link
Owner Author

I updated a couple of demo programs recently that do screencaptures. I state right at the top that it's Window-only, so there's the platform dependent problem that was hit right away. In this case it was because of my use of win32gui to get the bounding box for the window.

https://github.com/PySimpleGUI/PySimpleGUI/blob/master/DemoPrograms/Demo_Save_Any_Window_As_Image.py

@resnbl
Copy link

resnbl commented Mar 8, 2022

I'm used to being in the minority (I once had a 512K "Fat Mac" in the 80's...) and understand how projects will put their resources where they have the most benefit, so I'm not "dinging" PSG as much as just bemoaning the state of the world...

@PySimpleGUI
Copy link
Owner Author

I love Mac users...
image

I'm just not fond of some of the problems that trip up tkinter. You have totally valid points about support. I'm open to dinging when dinging is deserved and I've earned plenty of dings over the years.

@resnbl
Copy link

resnbl commented Mar 8, 2022

I think I can get you some limited Mac-specific code that doesn't require Pillow (but does something similar, so I guess I'd have to credit them for it). I have not tested it with multiple monitors yet (although I have two) so I'd need to hone it a bit before publication. Let me know if you are interested and I'lll look into it tomorrow...

@PySimpleGUI
Copy link
Owner Author

Lemme thinking ab... YES! Yes is the answer! Of course I'm interested!

@resnbl
Copy link

resnbl commented Mar 9, 2022

OK, I'll look into it tomorrow.

BTW: I got Demo_Graph_Drawing_And_Dragging_Figures.py to not crash on my Mac by simply changing the output filename from "test.jpg" to "test.png". However, just to show Pillow's shortcomings, here are:
a keyboard-initiated screenshot of a portion of my screen:

Screen Shot 2022-03-08 at 9 43 44 PM

and what Pillow captured:
test

and a gratuitous extra comment:
palm_56

@resnbl
Copy link

resnbl commented Mar 9, 2022

This doesn't usually happen, but I got lucky this time: it turns out that the Mac screencapture program uses exactly the same co-ordinate system that tkinter uses, so multi-monitor processing required no additional code. Attached is one of the demo files "slightly" modified to support captures on either the Mac or "other" platforms. Also, I added an extra bit of code so that Window objects can be captured as well as Element-based objects, so you don't really need to place everything into a single wrapper element just for screen capture purposes.

(filetype changed from '.py' to '.txt' so GitHub would accept it here)

Demo_Save_Window_As_Image.txt

And some example output of an element and the whole window:

Column@442_456

Window@427_420

@PySimpleGUI
Copy link
Owner Author

image

Oh WOW!

Thank you SO much... this is awesome! I've been concerned about how I was going to do this on platforms other than Windows and you've just solved that problem entirely.

Really appreciate the help. It's going to really help.

@resnbl
Copy link

resnbl commented Mar 10, 2022

You may want to hold up a minute on using my code...
As a programmer, I am always suspicious of "magic constants" like the "28" I have for the titlebar height in my code. I am working on code which will get the proper value from tkinter directly, but so far I've just hosed things up for the non-titlebar case. I should have it ready in the next day or so...

@PySimpleGUI
Copy link
Owner Author

I had a feeling I would see a warning 😉

I've got these kinds of constants in a number of the window-oriented code like this. I'm usually pretty OK with "Good Enough", but will take "Even Better" when it's available. Thank you very much for the help.

Another area that I'm looking at is how to get the bounding box for the Windows. I'm using win32gui on Windows. @Chr0nicT is doing some research on other solutions. If you've got suggestions, I'm all ears.

psgscreenshot is going to be an application rather than fully integrated into PySimpleGUI because it uses PIL. I can pass in the coordinates if launched through PySimpleGUI, but for standalone launches I'll need to either have the user mark the location of the window or get the location using another package.

@resnbl
Copy link

resnbl commented Mar 11, 2022

Nerts! Ran into a problem with both the existing code and my "improved" version:
It works fine for both a Window or an Element when the main Window has a title.
It works fine for both a Window or an Element when the main Window has no title, if the window is the only PSG window active.
But when there is no title, and I precede the capture call with the popup_get_file() dialog to collect an output filename, the captured image is at the proper screen co-ords and size, but the image contains portions of the previous file save dialog window which should have already disappeared!

Here is the "untitled window" output w/o a preceding popup_get_file():
no-popup-Window@867_533

Here it is with a preceding popup_get_file():
popup-Window@867_533

Could I need to get tkinter (or Python) to garbage collect the old window before the capture? Any idea how to do that?

@resnbl
Copy link

resnbl commented Mar 11, 2022

As far as getting screen co-ords for another program's window goes, this is difficult in the Mac world without PyObjC/Appkit stuff (PyObjC loads a bazillion packages into pip). Some older packages exist, but they relied on the Python 2.x + extras that Apple stopped pre-installing on Macs several OS releases ago...

@PySimpleGUI
Copy link
Owner Author

PySimpleGUI commented Mar 11, 2022

Could I need to get tkinter (or Python) to garbage collect the old window before the capture? Any idea how to do that?

You can force a garbage collect using the technique found here:
https://pysimplegui.readthedocs.io/en/latest/#multiple-threads

Basically

import gc

# Then later when you want to force a garbage collect
gc.collect()

It's not clear why the framebuffer would have old information in it if you're not seeing it also on your screen.

Tanay has been making progress on Linux too so it's coming together better than expected. I'm sure there will be some fudging going on with the window size, etc, It'll be much better than nothing at all. I'm also going to have a "drag to capture" so that a manual screenshot can happen.

@resnbl
Copy link

resnbl commented Mar 11, 2022

I think I have a handle on this, and it is not a gc issue.

It's not clear why the framebuffer would have old information in it if you're not seeing it also on your screen.

And there's the rub: the popup_get_file() exits and then we immediately try to capture the screen. It happens rather fast, so "what I see" isn't relevant. I think the issue is that tkinter has not run its idle tasks yet (nor has psg), so technically the dialog window actually still is on the screen when the capture occurs. Note the I tried this with no_window=True on the popup and captured a portion of the standard Mac save dialog.

I need to let tkinter do its thing before I do the capture. Might be some combination of a short sleep, root.update() or root.update_idletasks(), or...

Why this only appears to be an issue for non-titled windows is beyond me, but I suspect tkinter is doing something funny to make a title-less window.

@PySimpleGUI
Copy link
Owner Author

Call window.refresh() if you've made changes that you want to make sure tkinter sees prior to the next window.read() call.

@resnbl
Copy link

resnbl commented Mar 12, 2022

window.refresh() by itself isn't cutting it. However, if I precede it with time.sleep(0.01) things appear to work (even after doing several back-to-back captures to insure all the Python code is loaded/compiled/cached and running at full speed). Interestingly, using these 2 commands in reverse order fails. Also, the sleep alone doesn't work.

I was wondering if maybe the OS needs to run in order to reset the screen buffer, as well as tkinter.

@resnbl
Copy link

resnbl commented Mar 12, 2022

Well, I dislike inserting a timed sleep (even if it is short), but here is an updated version. You may wish to check out how I got the main Window from an Element - you may know a better way.

new_Demo_Save_Window_As_Image.txt

@Chr0nicT
Copy link
Collaborator

Chr0nicT commented Mar 12, 2022

This is what I wrote up for Mac using AppleScript. Unsure if it works on Retina displays. I suppose ImageGrab could be swapped for the screencapture command as well.

(Nothing pretty about it, hardcoded for 'Terminal'. In a specific app, I could change the code around to grab a specific window, of course)

import subprocess
from PIL import ImageGrab

def get_all_windows():
    """
    Solely for testing purposes.

    :return: string of open window titles seperated by newlines.
    """

    cmd = """osascript -s 's' -e 'tell application "System Events"
                                      set processList to get the name of every process whose background only is false
                                  end tell
                                  return processList'"""
    ret = subprocess.check_output(cmd, shell=True).decode(encoding='utf-8').replace('"', '').replace(", ", "\n").replace('{', '').replace('}', '').strip()
    return ret

def capture_image(window_title):
    """
    Captures screenshot using PIL imagegrab from a Window.
    """

    cmd = """on run {arg1}
                 set win_title to arg1
                 try
                     tell application arg1
                         set win_bounds to get bounds of first window
                     end tell
                 end try
                 return win_bounds
             end run"""
    proc = subprocess.Popen(['osascript', '-', window_title], stdin=subprocess.PIPE, stdout=subprocess.PIPE, encoding='utf-8')
    ret, err = proc.communicate(cmd)
    if not err:
        bnd_res = ret.replace(", ", ",")
        geom = bnd_res.split(',')
        arg1 = int(geom[0])
        arg2 = int(geom[1])
        arg3 = int(geom[2])
        arg4 = int(geom[3])
        print(f"Capturing BBOX: {arg1}, {arg2}, {arg3}, {arg4}")
        im = ImageGrab.grab(bbox=(arg1, arg2, arg3, arg4))
        im.save('test.png')
        
print(f"-------\nList of windows:\n{get_all_windows()}\n-------")
capture_image("Terminal")

test
test

@PySimpleGUI
Copy link
Owner Author

I dislike inserting a timed sleep (even if it is short)

I'm with you on this. Rather than sleep, if it's the mainthread, then I use a window.read with a timeout instead of a sleep. This keeps the GUI alive and active during that timeframe. Instead of time.sleep(1) try using window.read(timeout=1000) and see if you get the results you're after.

Another technique would be to use a thread that does the sleep. Here's a short example of using a thread to sleep for 2 seconds.

import PySimpleGUI as sg
import time

def sleep_thread(window:sg.Window):
    time.sleep(2)
    # If you want to explicitly send an event use this
    # window.write_event_value('-SLEEP DONE-', None)

layout = [[sg.T('Sleep test')],
          [sg.Button('Sleep'), sg.Button('Exit')]]

window = sg.Window('Window Title', layout, finalize=True)

while True:
    event, values = window.read()
    print(event, values)
    if event == sg.WIN_CLOSED or event == 'Exit':
        break
    if event == 'Sleep':
        window.perform_long_operation(lambda: sleep_thread(window), '-THEAD DONE-')
    elif event == '-THEAD DONE-':
        sg.popup('Sleep compeleted')
        
window.close()

@resnbl
Copy link

resnbl commented Mar 12, 2022

@Chr0nicT I like your mechanism for getting a list of windows, although it does list apps which are not actually visible when the command is run. But see my first post in this thread on why ImageGrab doesn't work on Retina displays. To wit, here is a keyboard-initiated partial screen capture of my desktop:

Screen Shot 2022-03-12 at 12 01 09 PM

Here is what your code actually captured:
test

Also, it created a blank .png when the Terminal window was located on my second monitor.

BTW: ImageGrab uses screencapture under the covers, it just doesn't deal with the bbox of the captured screen correctly.

@resnbl
Copy link

resnbl commented Mar 12, 2022

@PySimpleGUI I have updated to the timed read and it appears to work.
Demo_Save_Window_As_Image.txt

@resnbl
Copy link

resnbl commented Mar 24, 2022

FYI: after I submitted a bug report to Pillow, they have created a PR to fix the issue with PIL.ImageGrab.grab() on the Mac.

@PySimpleGUI
Copy link
Owner Author

I really appreciate you pushing all this forward Bob! I'll be returning to this project shortly. I tooks a bit of a detour with the new scrollbar work. Sometimes the roadmap of features takes a sudden detour. I really appreciate all the help you've provided!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
community input desired Port - TK PySimpleGUI Question Further information is requested
Projects
None yet
Development

No branches or pull requests

5 participants