I've written a small script to display album art on USB display (on Raspberry Pi) by writing to the framebuffer with pygame. The script is working perfectly and the album art is displayed on the screen for 3 seconds:
def set_image(image):
""" Set the USB display image using pygame (320px x 240px) """
pygame.display.init()
size = (pygame.display.Info().current_w, pygame.display.Info().current_h)
black = 0, 0, 0
screen = pygame.display.set_mode(size)
pygame.mouse.set_visible(False)
pygame_image = pygame.image.fromstring(image.tobytes(), image.size, image.mode)
pygame_image_rect = pygame_image.get_rect()
screen.fill(black)
screen.blit(pygame_image, (40, 0))
pygame.font.init()
pygame.display.update()
time.sleep(3)
The problem is that when the script finishes, pygame (correctly) clears the framebuffer and my image disappears. Is there any way to tell it to leave the contents of the framebuffer when quitting?
Are you calling pygame.quit?
Generally:
- don't run window server (run from console)
- use flip to flip to a different surface
- don't call pygame.quit when exiting.
It is hard to guarantee what happens after that, but flip should flip the screen buffer to a different part of memory than the shell uses, so it won't get overwritten, and if you don't call pygame.quit, it should leave it in the (bad) state. Although there are scenarios this is useful.
Related
So I was trying to make a game with python and pygame but I noticed that I couldn't make a high resolution display because when I tried to make a display with more pixels, the pygame window was too big for my 4k (3840x2160) monitor. I should note that my monitor is connected to an old Dell laptop with a resolution of (1366x768). But when I entered this: print(pygame.display.list_modes()) it told me that I could use resolutions up to 4k and not just up to the resolution of my laptop. After a lot of searching and trying I accepted the fact that my game will be low resolution and moved on. As I continued coding the game I wanted to have a pop-up window so I imported pyautogui and my pygame window suddenly became much smaller. BOOM problem solved. I increased the resolution and I had no problems, my game was now running at a very high resolution! I was very confused so I made a very simple pygame program so I could test this and it actually worked. This is low quality and can't fit in my screen:
import pygame
import sys
pygame.init()
screen = pygame.display.set_mode((3000, 1500))
font = pygame.font.Font('font.otf', 50)
while True:
screen.fill((255, 255, 255))
txt = font.render("hello", True, (0, 0, 0))
screen.blit(txt, (100, 100))
pygame.display.update()
for event in pygame.event.get():
if event.type == pygame.QUIT:
sys.exit()
screenshot1
And this is high resolution and does fit in my screen:
import pygame
import sys
import pyautogui
pygame.init()
screen = pygame.display.set_mode((3000, 1500))
font = pygame.font.Font('font.otf', 50)
while True:
screen.fill((255, 255, 255))
txt = font.render("hello", True, (0, 0, 0))
screen.blit(txt, (100, 100))
pygame.display.update()
for event in pygame.event.get():
if event.type == pygame.QUIT:
sys.exit()
screenshot2
I don't even need to use pyautogui!
Can someone explain this to me?
Thanks
After a bunch of source diving I believe I have found the solution: pyautogui imports pyscreeze for the functions center, grab, pixel, pixelMatchesColor, screenshot. On lines 63 to 71 of pyscreeze/__init__.py is the following:
if sys.platform == 'win32':
# On Windows, the monitor scaling can be set to something besides normal 100%.
# PyScreeze and Pillow needs to account for this to make accurate screenshots.
# TODO - How does macOS and Linux handle monitor scaling?
import ctypes
try:
ctypes.windll.user32.SetProcessDPIAware()
except AttributeError:
pass # Windows XP doesn't support monitor scaling, so just do nothing.
The above code calls SetProcessDPIAware, which is equivalent to the following:
System DPI aware. This window does not scale for DPI changes. It will query for the DPI once and use that value for the lifetime of the process. If the DPI changes, the process will not adjust to the new DPI value. It will be automatically scaled up or down by the system when the DPI changes from the system value.
If want to get the same effect without pyautogui you can just include the above call to SetProcessDPIAware in your code.
I'm using pygame and updating to the screen every loop of the main loop. What I don't understand is nothing will update until I add a for loop looking for events, then suddenly all the updating does occur. Why is this?
def run(self):
two_pm = get_stand_up_timestamp()
pygame.init()
font = pygame.font.Font(None, 72)
screen = pygame.display.set_mode(self._dimensions)
before_two = True
while before_two:
# Blit the time to the window.
# Update Screen.
current_time = datetime.datetime.now()
text = font.render(f'{current_time.hour} : {current_time.minute} : {current_time.second}', True, (0, 0, 0))
blit_center = (
self._dimensions[0] // 2 - (text.get_width() // 2),
self._dimensions[1] // 2 - (text.get_height() // 2)
)
screen.fill((255, 255, 255))
screen.blit(text, blit_center)
pygame.display.flip()
# Get events.
for event in pygame.event.get():
if event.type == pygame.QUIT:
sys.exit()
elif event.type == pygame.KEYDOWN:
if event.key == pygame.K_q:
sys.exit()
When you call pygame.event.get() (or pump()), pygame processes all events that your window manager send to the window managed by pygame.
You don't see these events as they are not returned by get(), but pygame handles them internally. These events could be WM_PAINT on Windows or Expose on Linux (IIRC pygame uses Xlib), or other events (I guess you could look them up in pygame's source code).
E.g. if you run pygame on Windows, Pygame has to call Windows' GetMessage function, otherwise:
If a top-level window stops responding to messages for more than several seconds, the system considers the window to be not responding and replaces it with a ghost window that has the same z-order, location, size, and visual attributes. This allows the user to move it, resize it, or even close the application. However, these are the only actions available because the application is actually not responding.
So the typical behaviour if you don't let pygame process the events is that it will basically run, but the mouse cursor will change to the busy cursor and you can't move the window before it will eventually freeze.
If you run pygame on other systems, e.g. Linux, you only see a black screen. I don't know the internals of the message loop when pygame runs on Linux, but it's similiar to the Windows message loop: you have to process the events in the queue to have pygame call Xlib's XNextEvent function (IIRC) to give the window manager a chance to draw the window.
See e.g. Message loop in Microsoft Windows and/or Xlib for more information on that topic.
No idea why it doesn't work on your end, however when I run
def run():
width = 500
height = 500
pygame.init()
font = pygame.font.Font(None, 72)
screen = pygame.display.set_mode((width, height))
before_two = True
while before_two:
# Blit the time to the window.
# Update Screen.
current_time = datetime.datetime.now()
text = font.render(f'{current_time.hour} : {current_time.minute} : {current_time.second}', True, (0, 0, 0))
blit_center = (
width // 2 - (text.get_width() // 2),
height // 2 - (text.get_height() // 2)
)
screen.fill((255, 255, 255))
screen.blit(text, blit_center)
pygame.display.flip()
run()
Everything works fine update wise. The clock ticks every second so it may be something with your version of python or pygame. Try updating them both. Alternately it could be a problem with how you get pass pygame the dimensions of the window with the run(self) and self._dimensions. Trying using static dimensions like I did above and see if that works on your end. Sadly without more code to see how you call run() its difficult to fully debug whats wrong.
TL;DR
I am fiddling with a Raspberry Pi 2 and a 2.8" TFT touch screen attached to the Pi's GPIO. The Pi is also connected to a HDMI monitor.
My issue is that my Python3 pygame script is not able to use the TFT screen, but always displays on my HDMI screen instead.
Some background
I've installed the latest vanilla Raspbian ready-to-use distro and followed the TFT screen installation steps, everything works well: the TFT can display the console and X without issue. The touchscreen is calibrated and moves the cursor correctly. I can also see a new framebuffer device as /dev/fb1.
I've tried the following to test this new device:
sudo fbi -T 2 -d /dev/fb1 -noverbose -a my_picture.jpg
=> This successfully displays the pic on the TFT screen
while true; do sudo cat /dev/urandom > /dev/fb1; sleep .01; done
=> This successfully displays statics on the TFT screen
However, when I run this Python3/pygame script, the result appears in the HDMI screen consistently and not on the TFT screen:
#!/usr/bin/python3
import os, pygame, time
def setSDLVariables():
print("Setting SDL variables...")
os.environ["SDL_FBDEV"] = "/dev/fb1"
os.environ["SDL_VIDEODRIVER"] = driver
print("...done")
def printSDLVariables():
print("Checking current env variables...")
print("SDL_VIDEODRIVER = {0}".format(os.getenv("SDL_VIDEODRIVER")))
print("SDL_FBDEV = {0}".format(os.getenv("SDL_FBDEV")))
def runHW5():
print("Running HW5...")
try:
pygame.init()
except pygame.error:
print("Driver '{0}' failed!".format(driver))
size = (pygame.display.Info().current_w, pygame.display.Info().current_h)
print("Detected screen size: {0}".format(size))
lcd = pygame.display.set_mode(size)
lcd.fill((10,50,100))
pygame.display.update()
time.sleep(sleepTime)
print("...done")
driver = 'fbcon'
sleepTime= 0.1
printSDLVariables()
setSDLVariables()
printSDLVariables()
runHW5()
The script above runs as follow:
pi#raspberrypi:~/Documents/Python_HW_GUI $ ./hw5-ThorPy-fb1.py
Checking current env variables...
SDL_VIDEODRIVER = None
SDL_FBDEV = None
Setting SDL variables...
...done
Checking current env variables...
SDL_VIDEODRIVER = fbcon
SDL_FBDEV = /dev/fb1
Running HW5...
Detected screen size: (1920, 1080)
...done
I have tried different drivers (fbcon, directfb, svgalib...) without success.
Any help or idea would be greatly appreciated, I've been through a lot of doc, manuals and samples and just ran out of leads :/ Furthermore, it appears that a lot of people have succeeded in getting Python3/pygame to output to their TFT screen via /dev/fb1.
I have been fiddling around that for far too many hours now, but at least I have found what I'd call a decent workaround, if not a solution.
TL;DR
I've kept using pygame for building my graphics/GUI, and switched to evdev for handling the TFT touch events. The reason for using evdev rather than pygame's built-in input management (or pymouse, or any other high level stuff) is explained in the next section.
In a nutshell, this program builds some graphics in memory (RAM, not graphic) using pygame, and pushes the built graphics as bytes into the TFT screen framebuffer directly. This bypasses any driver so it is virtually compatible with any screen accessible through a framebuffer, however it also bypasses any potential optimizations coming along what would be a good driver.
Here is a code sample that makes the magic happen:
#!/usr/bin/python3
##
# Prerequisites:
# A Touchscreen properly installed on your system:
# - a device to output to it, e.g. /dev/fb1
# - a device to get input from it, e.g. /dev/input/touchscreen
##
import pygame, time, evdev, select, math
# Very important: the exact pixel size of the TFT screen must be known so we can build graphics at this exact format
surfaceSize = (320, 240)
# Note that we don't instantiate any display!
pygame.init()
# The pygame surface we are going to draw onto.
# /!\ It must be the exact same size of the target display /!\
lcd = pygame.Surface(surfaceSize)
# This is the important bit
def refresh():
# We open the TFT screen's framebuffer as a binary file. Note that we will write bytes into it, hence the "wb" operator
f = open("/dev/fb1","wb")
# According to the TFT screen specs, it supports only 16bits pixels depth
# Pygame surfaces use 24bits pixels depth by default, but the surface itself provides a very handy method to convert it.
# once converted, we write the full byte buffer of the pygame surface into the TFT screen framebuffer like we would in a plain file:
f.write(lcd.convert(16,0).get_buffer())
# We can then close our access to the framebuffer
f.close()
time.sleep(0.1)
# Now we've got a function that can get the bytes from a pygame surface to the TFT framebuffer,
# we can use the usual pygame primitives to draw on our surface before calling the refresh function.
# Here we just blink the screen background in a few colors with the "Hello World!" text
pygame.font.init()
defaultFont = pygame.font.SysFont(None,30)
lcd.fill((255,0,0))
lcd.blit(defaultFont.render("Hello World!", False, (0, 0, 0)),(0, 0))
refresh()
lcd.fill((0, 255, 0))
lcd.blit(defaultFont.render("Hello World!", False, (0, 0, 0)),(0, 0))
refresh()
lcd.fill((0,0,255))
lcd.blit(defaultFont.render("Hello World!", False, (0, 0, 0)),(0, 0))
refresh()
lcd.fill((128, 128, 128))
lcd.blit(defaultFont.render("Hello World!", False, (0, 0, 0)),(0, 0))
refresh()
##
# Everything that follows is for handling the touchscreen touch events via evdev
##
# Used to map touch event from the screen hardware to the pygame surface pixels.
# (Those values have been found empirically, but I'm working on a simple interactive calibration tool
tftOrig = (3750, 180)
tftEnd = (150, 3750)
tftDelta = (tftEnd [0] - tftOrig [0], tftEnd [1] - tftOrig [1])
tftAbsDelta = (abs(tftEnd [0] - tftOrig [0]), abs(tftEnd [1] - tftOrig [1]))
# We use evdev to read events from our touchscreen
# (The device must exist and be properly installed for this to work)
touch = evdev.InputDevice('/dev/input/touchscreen')
# We make sure the events from the touchscreen will be handled only by this program
# (so the mouse pointer won't move on X when we touch the TFT screen)
touch.grab()
# Prints some info on how evdev sees our input device
print(touch)
# Even more info for curious people
#print(touch.capabilities())
# Here we convert the evdev "hardware" touch coordinates into pygame surface pixel coordinates
def getPixelsFromCoordinates(coords):
# TODO check divide by 0!
if tftDelta [0] < 0:
x = float(tftAbsDelta [0] - coords [0] + tftEnd [0]) / float(tftAbsDelta [0]) * float(surfaceSize [0])
else:
x = float(coords [0] - tftOrig [0]) / float(tftAbsDelta [0]) * float(surfaceSize [0])
if tftDelta [1] < 0:
y = float(tftAbsDelta [1] - coords [1] + tftEnd [1]) / float(tftAbsDelta [1]) * float(surfaceSize [1])
else:
y = float(coords [1] - tftOrig [1]) / float(tftAbsDelta [1]) * float(surfaceSize [1])
return (int(x), int(y))
# Was useful to see what pieces I would need from the evdev events
def printEvent(event):
print(evdev.categorize(event))
print("Value: {0}".format(event.value))
print("Type: {0}".format(event.type))
print("Code: {0}".format(event.code))
# This loop allows us to write red dots on the screen where we touch it
while True:
# TODO get the right ecodes instead of int
r,w,x = select.select([touch], [], [])
for event in touch.read():
if event.type == evdev.ecodes.EV_ABS:
if event.code == 1:
X = event.value
elif event.code == 0:
Y = event.value
elif event.type == evdev.ecodes.EV_KEY:
if event.code == 330 and event.value == 1:
printEvent(event)
p = getPixelsFromCoordinates((X, Y))
print("TFT: {0}:{1} | Pixels: {2}:{3}".format(X, Y, p [0], p [1]))
pygame.draw.circle(lcd, (255, 0, 0), p , 2, 2)
refresh()
exit()
More details
A quick recap on what I wanted to achieve: my goal is to display content onto a TFT display with the following constraints:
Be able to display another content on the HDMI display without interference (e.g. X on HDMI, the output of a graphical app on the TFT);
be able to use the touch capability of the TFT display for the benefit of the graphical app;
make sure the point above would not interfere with the mouse pointer on the HDMI display;
leverage Python and Pygame to keep it very easy to build whatever graphics/GUI I'd fancy;
keep a less-than-decent-but-sufficient-for-me framerate, e.g. 10 FPS.
Why not using pygame/SDL1.2.x as instructed in many forums and the adafruit TFT manual?
First, it doesn't work, at all. I have tried a gazillion versions of libsdl and its dependencies and they all failed consistently. I've tried forcing some libsdl versions downgrades, same with pygame version, just to try to get back to what the software was when my TFT screen was released (~2014). Then I aslo tried switching to C and handle SDL2 primitives directly.
Furthermore, SDL1.2 is getting old and I believe it is bad practice to build new code on top of old one. That said, I am still using pygame-1.9.4...
So why not SDL2? Well, they have stopped (or are about to stop) supporting framebuffers. I have not tried their alternative to framebuffers, EGL, as it got more complex the further I digged and it did not look too engaging (so old it felt like necro-browsing). Any fresh help or advice on that would be greatly appreciated BTW.
What about the touchscreen inputs?
All the high level solutions that work in a conventional context are embedding a display. I've tried pygame events, pymouse and a couple others that would not work in my case as I got rid of the notion of display on purpose. That's why I had to go back to a generic and low level solution, and the internet introduced my to evdev, see the commented code above for more details.
Any comment on the above would be greatly appreciated, these are my first step with Raspbian, Python and TFT screens, I reckon I most probably have missed some pretty obvious stuff along the way.
This question already has answers here:
How can I convert pygame to exe?
(6 answers)
Pygame2Exe Errors that I can't fix
(3 answers)
Closed 1 year ago.
I want to be able to convert .py to .exe for python 3 pygame 1.9.4 projects (on my Windows 10 computer), and I have successfully converted some files by using auto py to exe (one file, console based):
https://nitratine.net/blog/post/auto-py-to-exe/.
For example, I tried to convert the code that is quite far up on this site:
http://openbookproject.net/thinkcs/python/english3e/pygame.html. That is:
import pygame
def main():
""" Set up the game and run the main game loop """
pygame.init() # Prepare the pygame module for use
surface_sz = 480 # Desired physical surface size, in pixels.
# Create surface of (width, height), and its window.
main_surface = pygame.display.set_mode((surface_sz, surface_sz))
# Set up some data to describe a small rectangle and its color
small_rect = (300, 200, 150, 90)
some_color = (255, 0, 0) # A color is a mix of (Red, Green, Blue)
while True:
ev = pygame.event.poll() # Look for any event
if ev.type == pygame.QUIT: # Window close button clicked?
break # ... leave game loop
# Update your game objects and data structures here...
# We draw everything from scratch on each frame.
# So first fill everything with the background color
main_surface.fill((0, 200, 255))
# Overpaint a smaller rectangle on the main surface
main_surface.fill(some_color, small_rect)
# Now the surface is ready, tell pygame to display it!
pygame.display.flip()
pygame.quit() # Once we leave the loop, close the window.
main()
The .exe file ran as it should when opened. However, some programs that I write are only partially functioning after the conversion.
One such program would be:
import pygame as pg
def main():
pg.init()
pg.display.set_caption('Some Caption')
screen = pg.display.set_mode((640, 480))
afont = pg.font.SysFont("some_font", 32)
done=False
while not done:
for event in pg.event.get():
if event.type == pg.QUIT:
done = True
screen.fill(0)
sometext = afont.render("Amazing text"
.format(0, 0), True, (250,200,250))
screen.blit(sometext, (10, 50))
pg.display.flip()
if __name__.endswith('__main__'):
main()
pg.quit()
After converted to exe, when I open it, some text about pygame appears and also the screen that is meant to pop up, showing the caption which I've set ('Some Caption'). The screen which popped up is however black and closes after a while, not showing what it should. However, I did get my exe file without any errors (which I could see anyway in the Output of the GUI which I used for the conversion i.e. auto py to exe). So in search of solutions I found this website:
https://nitratine.net/blog/post/issues-when-using-auto-py-to-exe/.
There were alot about error messages and general information but the thing I saw about program that opens but not doing what it should before closing was:
The Terminal Just Opens and Closes But There Are No Errors If you
double click to run your Python script, what happens? Does it open and
close also? That means this tool has done it's job correctly and the
script is finishing just like it should.
You most likely think the output should stay visible because you are
always using IDLE or an IDE and that's what those tools do. Add a
statement like input() at the end of your script to block execution
and wait for you to press enter before closing.
This, is not the solution for my problem since my program works if I double click on the python script. However, I think that it has a good point i.e that there might be something in my code that do not handle the conversion well, which is probable since the code from http://openbookproject.net/thinkcs/python/english3e/pygame.html did work after being converted to .exe when the same method was used.
My question is therefore, what is it that make my code not work after the conversion from .py to .exe?, and in that case, in what way might it be fixable?. The knowledge of what works and not when it comes to the converting .py to .exe would enable more programs to function correctly when they are .exe.
Thank you
I'm building a small game with pygame. I want the window of the game to be size of the monitors resolution. My computers screen's resolution is 1920x1080 and display.info says the window size is also 1920x1080 but when I run it, it creates a window roughly one and half times the size of my screen.
import pygame, sys
def main():
#set up pygame, main clock
pygame.init()
clock = pygame.time.Clock()
#creates an object with the computers display information
#current_h, current_w gives the monitors height and width
displayInfo = pygame.display.Info()
#set up the window
windowWidth = displayInfo.current_w
windowHeight = displayInfo.current_h
window = pygame.display.set_mode ((windowWidth, windowHeight), 0, 32)
pygame.display.set_caption('game')
#gameLoop
while True:
window.fill((0,0,0))
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()
#draw the window onto the screen
pygame.display.flip()
clock.tick(60)
main()
I've been having the same problem, and I managed to find the answer and posted it here. The answer I found is as follows:
I managed to find a commit on the Pygame BitBucket page here that explains the issue and gives an example on how to fix it.
What is happening is that some display environments can be configured to stretch windows so they don't look small on high PPI (Pixels Per Inch) displays. This stretching is what causes displays on larger resolutions to display larger than they actually are.
They provide an example code on the page I linked to showing how to fix this issue.
They fix the issue by importing ctypes and calling this:
ctypes.windll.user32.SetProcessDPIAware()
They also express that this is a Windows only solution and is available within base Python since Python 2.4. Before that it will need to be installed.
With that said, to make this work, put this bit of code anywhere before pygame.display.set_mode()
import ctypes
ctypes.windll.user32.SetProcessDPIAware()
#
# # # Anywhere Before
#
pygame.display.set_mode(resolution)
I hope this helps you and anyone else who finds they have this same issue.