Operating System: macOS High Sierra
Python version: Python 3.7
PySimpleGUI Version: 4.19.0 Released 5-May-2020
5 years. Python programming experience
5 years. Programming experience overall
no. Have used another Python GUI Framework (tkinter, Qt, etc) previously (yes/no is fine)?
First I'd just like to say thank you for this package. I really like the way that the layout works.
I'm wondering if there's any way to get the text logged using python's logging library to print to (preferably) the Output element.
I have a fairly big project's worth of code that is full of many logging statements. I need to share this project with lots of Windows users who would get on much better with a GUI. The logging statements are quite important to show to the users, so I'd prefer them to be on the same window as the main GUI.
import PySimpleGUI as sg
import logging
log_file = 'run_log.txt'
# Logging setup to send one format of logs to a log file and one to stdout:
logging.basicConfig(
level=logging.DEBUG,
format='%(name)s, %(asctime)s, [%(levelname)s], %(message)s',
filename=log_file,
filemode='w')
ch = logging.StreamHandler()
ch.setLevel(logging.INFO)
ch.setFormatter(logging.Formatter('%(name)s, [%(levelname)s], %(message)s'))
logging.getLogger('').addHandler(ch)
layout = [
[sg.Text('Number:'), sg.Input(key='input')],
[sg.Button('Run')],
[sg.Output(size=(100,10), key='log')],
[sg.Button('Exit')],
]
window = sg.Window('Window Title', layout)
while True: # Event Loop
event, values = window.read()
if event == sg.WIN_CLOSED or event == 'Exit':
break
if event == 'Run':
logging.info('Running...')
window.close()
This outputs root, [INFO], Running... to the command line/output in PyCharm (and similar text is sent to the log file), (rather than to the output window, which is what I'd like). I was wondering if there was a way to reroute the stdout to the Output window?
I did look in the documentation, but couldn't see anything. Sorry if I missed something.
It is not processed mainly under PySimpleGUI, and you need to build a handler by yourself. To update output, I create a buffer for all output content. Format of output is defined in emit().
You can do it like this:
class Handler(logging.StreamHandler):
def __init__(self):
logging.StreamHandler.__init__(self)
def emit(self, record):
global buffer
buffer = f'{buffer}\n{str(record)}'.strip()
window['log'].update(value=buffer)
buffer = ''
ch = Handler()
Thanks this worked perfectly 馃槃. I've ever looked at the inner workings of handlers before. It was easy to update to get the formatting I wanted.
class Handler(logging.StreamHandler):
def __init__(self):
logging.StreamHandler.__init__(self)
def emit(self, record):
global buffer
record = f'{record.name}, [{record.levelname}], {record.message}'
buffer = f'{buffer}\n{record}'.strip()
window['log'].update(value=buffer)
log_file = 'run_log.txt'
logging.basicConfig(
level=logging.DEBUG,
format='%(name)s, %(asctime)s, [%(levelname)s], %(message)s',
filename=log_file,
filemode='w')
buffer = ''
ch = Handler()
ch.setLevel(logging.INFO)
logging.getLogger('').addHandler(ch)
layout = [
[sg.Text('Number:'), sg.Input(key='input')],
[sg.Button('Run')],
[sg.Output(size=(100,10), key='log')],
[sg.Button('Exit')],
]
window = sg.Window('Window Title', layout)
while True: # Event Loop
event, values = window.read()
if event == sg.WIN_CLOSED or event == 'Exit':
break
if event == 'Run':
logging.info('Running...')
window.close()
I want to keep this opened. Jason came up with something important. Maybe I should add to the Cookbook? Or it could be turned into a demo program. I think it's certainly worth keeping and not losing.
@jason990420
I have a similar problem, but I am not familiar with handlers.
I would like to display scaled images (png, jpg, ,,,) with an image element. The way I do it now is that I save a temporary image to disk and read it from there. Is it possible to write it to a buffer and get it back from there. Should be faster.
A demo code modified from the cookbook looks like this. I do some other stuff with my images, therefore the special intermediate image format.
Thanks for your help, your last advice was excellent!
#!/usr/bin/env python
import PySimpleGUI as sg
import os
import numpy as np
from skimage import io, img_as_float
from skimage import transform as tf
import time
def main():
# Get the folder containing the images from the user
folder = sg.popup_get_folder('Image folder to open')
if folder is None:
sg.popup_cancel('Cancelling')
return
# get list of PNG, JPG or GIF files in folder
image_files = [folder + '\\' + f for f in os.listdir(folder)
if '.png' in f or '.jpg' in f or '.gif' in f]
if len(image_files) == 0:
sg.popup('No PNG images in folder')
return
# define layout, show and read the window
col = [[sg.Text(image_files[0], size=(80, 3), key='filename')],
[sg.Button('Next', size=(8, 2)), sg.Button('Prev', size=(8, 2)),
sg.Text('File 1 of {}'.format(len(image_files)),
size=(15, 1), key='filenum')],
[sg.Image(filename=image_files[0], key='image', size=(500,500)),
sg.Sizer(0,500)]]
window = sg.Window('Image Browser', [[sg.Col(col)]],
return_keyboard_events=True,
location=(0, 0),
use_default_focus=False)
# loop reading the user input and displaying image, filename
i = 0
while True:
event, values = window.read()
# --------------------- Button & Keyboard ---------------------
if event is None:
break
elif event in ('Next', 'MouseWheel:Down', 'Down:40', 'Next:34') and i < len(image_files)-1:
i += 1
elif event in ('Prev', 'MouseWheel:Up', 'Up:38', 'Prior:33') and i > 0:
i -= 1
elif event == 'Exit':
break
if event == 'Read':
filename = folder + '/' + values['listbox'][0]
else:
t = time.time()
filename = image_files[i]
image = img_as_float(io.imread(filename))
(y,x) = image.shape[:2]
print(x,y)
scale = 500.0/max(y, x, 500)
if len(image.shape) == 3:
multichannel = True
else:
multichannel = False
im = tf.rescale(image, scale, multichannel=multichannel)
im = np.clip(im, 0.0, 1.0)
t1 = time.time() - t
# I save a temporary scaled png image for showing it and load it again
io.imsave('tmp.png', (im * 255).astype(np.uint8))
# update window with new image
window['image'].update(filename='tmp.png')
t2 = time.time() - t - t1
print(scale, t1, t2)
# update window with filename
window['filename'].update(filename)
# update page display
window['filenum'].update('File {} of {}'.format(i+1, len(image_files)))
window.close()
if __name__ == '__main__':
main()
Over the weekend there was a new Demo Program:
https://github.com/PySimpleGUI/PySimpleGUI/blob/master/DemoPrograms/Demo_Graph_Elem_Image_Album.py
I was wondering the same thing.... how to scale images down to a particular size without writing to disk. This demo contains 2 functions that help do this. One is for files, the other for the bytes objects that are Base64 encoded.
Take a look at this function in particular:
def get_img_filename(f, resize=None):
"""Generate image data using PIL
"""
img = PIL.Image.open(f)
cur_width, cur_height = img.size
if resize:
new_width, new_height = resize
if cur_width > cur_height:
new_height = int(new_height * cur_height/cur_width)
else:
new_width = int(new_width * cur_width/cur_height)
img = img.resize((new_width, new_height), PIL.Image.ANTIALIAS)
bio = io.BytesIO()
img.save(bio, format="PNG")
del img
return bio.getvalue()
It should return to you a bytes object that you can pass to the image.update as a data parameter.
This is what I call fast response! I was trying something similar without success. I looked at the demo. I am sure I can use this for my program. This should simplify my code. Next I have to see how I can get my FITS images into the same format (https://docs.astropy.org/en/stable/io/fits/). Thank you very much.
I was able to flip through folders of images and also used the same function to resize things like the Base64 icon that's built into PySimpleGUI. I dunno if it's a good function / technique or not, but it seems to work.
The function shown above for resizing images works only for square graphics. I suggest to change it the following way:
G_SIZE = (800,600)
def get_img_data(data, resize=None):
"""Generate PIL.Image data using PIL
"""
img = PIL.Image.open(io.BytesIO(base64.b64decode(data)))
cur_width, cur_height = img.size
if resize:
new_width, new_height = resize
scale = min(new_height/cur_height, new_width/cur_width)
img = img.resize((cur_width*scale, cur_height*scale), PIL.Image.ANTIALIAS)
bio = io.BytesIO()
img.save(bio, format="PNG")
del img
return bio.getvalue()
Thanks for your tip
Yes, as you said. I think it's a bug in old code and only correct when width = height.
for example, I want to size (3, 5) to (6, 10), it will get (3, 10), or (5, 3) to (10, 6), it will get (10, 3).
It means size ration changed, not 2 times as request.
I have no idea why to fix the ratio for it, not kept it unchanged and decided by user, like resize work in Image package. Sometime we may need to stretch the image with different ratio, and after that we can excatly know the size of image after resize.
Ah, I see what you mean. It's only working when the output size is square?
I hacked it together without testing a non-square output format. Sorry about that.
I'll try out your algorithm today! Thank you!
I ended up splitting out the resize. Needed to cast as int before calling resize.
Here's the new code for the image part. Will check in the new demo shortly.
def get_img_filename(f, resize=None):
"""
Resizes an image file and returns the result as a byte string which can be used to update a PySimpleGUI Element
"""
img = PIL.Image.open(f)
return resize_pil_image(img, resize)
def get_img_data(data, resize=None):
"""Generate PIL.Image data using PIL
"""
img = PIL.Image.open(io.BytesIO(base64.b64decode(data)))
return resize_pil_image(img, resize)
def resize_pil_image(img, resize=None):
"""
Resizes a PIL image to a new size or just converts to PNG as a byte string
:param img:
:param resize:
:return:
"""
cur_width, cur_height = img.size
if resize:
new_width, new_height = resize
scale = min(new_height/cur_height, new_width/cur_width)
img = img.resize((int(cur_width*scale), int(cur_height*scale)), PIL.Image.ANTIALIAS)
bio = io.BytesIO()
img.save(bio, format="PNG")
del img
return bio.getvalue()
my fault. I corrected get_image_filename to int image size when I noticed the bug, but copied the uncorrected und untested get_image_data in my comment. :-(
Using one function for resizing avoids this mistake. As you would say: keep it simple
Well, now that you went and tossed out that "Simple" word, that meant time to go back in and "do it right".
There should be one function for this and it should take either a filename or a bytes object. So, I made this function to replace the entire lot of functions:
def convert_to_bytes(file_or_bytes, resize=None):
'''
Will convert into bytes and optionally resize an image that is a file or a base64 bytes object.
:param file_or_bytes: either a string filename or a bytes base64 image object
:type file_or_bytes: (Union[str, bytes])
:param resize: optional new size
:type resize: (Tuple[int, int] or None)
:return: (bytes) a byte-string object
:rtype: (bytes)
'''
if isinstance(file_or_bytes, str):
img = PIL.Image.open(file_or_bytes)
else:
img = PIL.Image.open(io.BytesIO(base64.b64decode(file_or_bytes)))
cur_width, cur_height = img.size
if resize:
new_width, new_height = resize
scale = min(new_height/cur_height, new_width/cur_width)
img = img.resize((int(cur_width*scale), int(cur_height*scale)), PIL.Image.ANTIALIAS)
bio = io.BytesIO()
img.save(bio, format="PNG")
del img
return bio.getvalue()
You can pass in a filename or a bytes string and it'll return a bytes string that you can use to update an Image element or draw on a Graph element.
Now I should make sure this thing get put somewhere useful, like maybe in the Cookbook.
However, I still don't quite know what I'm doing, so until I get a little more verification that it actually works I'll hold off.
It is not processed mainly under PySimpleGUI, and you need to build a handler by yourself. To update output, I create a buffer for all output content. Format of output is defined in emit().
You can do it like this:
class Handler(logging.StreamHandler): def __init__(self): logging.StreamHandler.__init__(self) def emit(self, record): global buffer buffer = f'{buffer}\n{str(record)}'.strip() window['log'].update(value=buffer) buffer = '' ch = Handler()
Could you explain if and why this is better practice then simply calling an existing StreamHandler class with .TKOut like I've seen other PSG apps (like mine) do? For example, with an output element referenced by key "_output_":
viewHandler= logging.StreamHandler(window["_output_"].TKOut)
formatter= logging.Formatter("%(levelname)s: %(message)s")
viewHandler.setFormatter(formatter)
viewHandler.setLevel(logging.ERROR)
parentLogger.addHandler(viewHandler)
Sorry about that I just find a working solution and provide a suggestion. Not sure which one is best, anyway, I am not good at PySimpleGUI, also at logging. :-) So I cannot discuss other issue with you in detail. Just try to find solution or answer about PySimpleGUI issues here.
Thanks this worked perfectly 馃槃. I've ever looked at the inner workings of handlers before. It was easy to update to get the formatting I wanted.
I tried this solution, but it does not seem to work ... It just exists and says: Process finished with exit code 1
after a while. Does someone have an idea?
Here's how my code looks:
import logging
import PySimpleGUI as sg
import crawler as crawler
import iocParser
import mispParser
import postProcessing
# MISP KEY
MISP_URL = ""
MISP_KEY = ""
class Handler(logging.StreamHandler):
def __init__(self):
logging.StreamHandler.__init__(self)
def emit(self, record):
global buffer
record = f'{record.name}, [{record.levelname}], {record.message}'
buffer = f'{buffer}\n{record}'.strip()
window['log'].update(value=buffer)
def generate(misp_url: str, misp_key: str, misp_cert: bool, numberOfArticlesPerBlog: int, features: int) -> None:
""" starts the import client
Args:
misp_url: the URL of the local MISP instance
misp_key: a valid API key for the current MISP instance
misp_cert: specifies if the certificate of the server should be validated
numberOfArticlesPerBlog: specifies how many articles we need to crawl from each blog
features: specifies how many features the IOC objects need to have
Returns:
returns nothing
"""
logging.basicConfig(level=logging.DEBUG)
# get article URLs for every blog
logging.info(f"Crawling {numberOfArticlesPerBlog} articles from each blog")
crawled_urls = crawler.crawl_blogs(numberOfArticlesPerBlog)
# parse articles
logging.info("Parsing the blog posts...")
api_filename = iocParser.parse(crawled_urls, parser="api")
# post process api result
logging.info("Filtering...")
postProcessing.postProcess(api_filename, features)
# create Events and Attributes
logging.info("Creating events and attributes...")
mispParser.processIOC(api_filename, misp_url, misp_key, misp_cert)
logging.info("Finished.")
# logging
log_file = 'run_log.txt'
logging.basicConfig(
level=logging.DEBUG,
format='%(name)s, %(asctime)s, [%(levelname)s], %(message)s',
filename=log_file,
filemode='w')
buffer = ''
ch = Handler()
ch.setLevel(logging.INFO)
logging.getLogger('').addHandler(ch)
# GUI
sg.theme('DarkBlue3') # Add some color to the window
# Very basic window.
layout = [
[sg.Text('MISP infos')],
[sg.Text('MISP URL', size=(15, 1)), sg.InputText(default_text=MISP_URL)],
[sg.Text('MISP KEY', size=(15, 1)), sg.InputText(MISP_KEY)],
[sg.Checkbox('Verify certificates?', default=False)],
[sg.Text('Generator info')],
[sg.Text('Number of articles per source'), sg.Spin([i for i in range(1, 1000)], initial_value=1)],
[sg.Text('Number of min. features per post'), sg.Spin([i for i in range(1, 15)], initial_value=3)],
[sg.Text('Log:')],
[sg.Output(size=(100, 10), key='log')],
[sg.Button('Run'), sg.Button('Exit')]
]
window = sg.Window('IOC Generator', layout)
# Event Loop
while True:
event, values = window.read()
if event == sg.WIN_CLOSED or event == 'Exit':
break
if event == 'Run':
logging.info('Running...')
generate(values[0], values[1], values[2], values[3], values[4])
logging.info('Finished...')
window.close()
I should document this better, but you may find that the Multiline's recent changes make it superior to the Output element. You can now redirect stdout when creating the Multiline. That means it will work just like the Output Element in terms of print statements going to it. The Multiline has a lot more other features that you can then apply to it. Perhaps try swapping out the Output with a Multiline.
Can you post the entire error that's output and your environment (basically the stuff in the form) since nothing is known about your setup?
@PySimpleGUI thanks for the fast responds :)
I filled out the form below. Also i replaced Output with Multiline and used the print command.
Now it works and i can see the printed statements!
Still i got two things related to that:
MacOS BigSur Beta
3.8
4.28.0 Released 3-Aug-2020
1 years Python programming experience
5 years Programming experience overall
Have used another Python GUI Framework (tkinter, Qt, etc) previously (yes/no is fine)?
yes, a little Qt
Also here's now the code:
import logging
import PySimpleGUI as sg
import crawler as crawler
import iocParser
import mispParser
import postProcessing
# MISP KEY
MISP_URL = ""
MISP_KEY = ""
# logging
print = lambda *args, **kwargs: window['log'].print(*args, **kwargs)
def generate(misp_url: str, misp_key: str, misp_cert: bool, numberOfArticlesPerBlog: int, features: int) -> None:
[...]
# GUI
sg.theme('DarkBlue3') # Add some color to the window
# Very basic window. Return values using auto numbered keys
layout = [
[sg.Text('MISP infos')],
[sg.Text('MISP URL', size=(15, 1)), sg.InputText(default_text=MISP_URL)],
[sg.Text('MISP KEY', size=(15, 1)), sg.InputText(MISP_KEY)],
[sg.Checkbox('Verify certificates?', default=False)],
[sg.Text('Generator info')],
[sg.Text('Number of articles per source'), sg.Spin([i for i in range(1, 1000)], initial_value=1)],
[sg.Text('Number of min. features per post'), sg.Spin([i for i in range(1, 15)], initial_value=3)],
[sg.Text('Log:')],
[sg.Multiline(size=(100, 10), key='log')],
[sg.Button('Run'), sg.Button('Exit')]
]
window = sg.Window('IOC Generator', layout)
# Event Loop
while True:
event, values = window.read()
if event == sg.WIN_CLOSED or event == 'Exit':
break
if event == 'Run':
print('Running...')
generate(values[0], values[1], values[2], values[3], values[4])
print('Finished...')
window.close()
If you make a change to a window or its elements, you need to either call window.read or window.refresh in order to see those changes. There should be something in the docs about updating elements and the need to perform these kinds of calls.
I don't know what to tell you about other modules. In theory, all stdout should go to that window. Maybe they're logging to stderr and you need to re-route that too? Not sure on this one.
Ok, thank you very much :) seems i did a sloppy job reading the docs 馃槄
I don't see in your code that you're re-routing stdout. Take a look at the definition of the Multiline definition for those parameters you need to set. They will show up in the docstrings if you're using PyCharm. If not, they're in the docs:
https://pysimplegui.readthedocs.io/en/latest/call%20reference/#multiline-element
Most helpful comment
Over the weekend there was a new Demo Program:
https://github.com/PySimpleGUI/PySimpleGUI/blob/master/DemoPrograms/Demo_Graph_Elem_Image_Album.py
I was wondering the same thing.... how to scale images down to a particular size without writing to disk. This demo contains 2 functions that help do this. One is for files, the other for the bytes objects that are Base64 encoded.
Take a look at this function in particular:
It should return to you a bytes object that you can pass to the image.update as a data parameter.