How to display text on the screen without a window using Python

Question:

The Problem:

I need to write text directly to the screen without a window. The text needs to appear above all other windows and full-screen applications and should not be clickable or interactable in any way.

Example:
Screenshot of text on the screen.
The text doesn’t need to have a transparent background as seen in the example. I can use either Python 2 or 3 on Windows 7.

My Attempt at a Solution:

I tried making a standalone label using Tkinter:

Edit: Improved with the help of Christian Rapp

import Tkinter
label = Tkinter.Label(text='Text on the screen', font=('Times','30'), fg='black', bg='white')
label.master.overrideredirect(True)
label.master.geometry("+250+250")
label.master.lift()
label.master.wm_attributes("-topmost", True)
label.master.wm_attributes("-disabled", True)
label.master.wm_attributes("-transparentcolor", "white")
label.pack()
label.mainloop()

What works:

  • The text is shown without a window
  • The text remains above all other windows
  • The background can be transparent

What doesn’t:

  • The text does not show above fullscreen applications
  • The text blocks click events that occur over it
  • Background transparency isn’t alpha, so there’s hard edges
Asked By: dln385

||

Answers:

It turns out there are two entirely different problems here. To show text over windows, you’ll need to create an undecorated topmost window and chroma key the background. However, this won’t work when there’s a full-screen application running (such as a game). The only reliable way to show text over a full-screen application is to use a Direct3D hook.

I haven’t written up a Direct3D hook example, but I’ll give two different solutions to the first problem.

Solution 1: Tkinter + pywin32

In this example, I do the majority of the work with Tkinter and use win32api to prevent the text from blocking mouse clicks. If win32api isn’t available to you, then you can just remove that part of the code.

import Tkinter, win32api, win32con, pywintypes

label = Tkinter.Label(text='Text on the screen', font=('Times New Roman','80'), fg='black', bg='white')
label.master.overrideredirect(True)
label.master.geometry("+250+250")
label.master.lift()
label.master.wm_attributes("-topmost", True)
label.master.wm_attributes("-disabled", True)
label.master.wm_attributes("-transparentcolor", "white")

hWindow = pywintypes.HANDLE(int(label.master.frame(), 16))
# http://msdn.microsoft.com/en-us/library/windows/desktop/ff700543(v=vs.85).aspx
# The WS_EX_TRANSPARENT flag makes events (like mouse clicks) fall through the window.
exStyle = win32con.WS_EX_COMPOSITED | win32con.WS_EX_LAYERED | win32con.WS_EX_NOACTIVATE | win32con.WS_EX_TOPMOST | win32con.WS_EX_TRANSPARENT
win32api.SetWindowLong(hWindow, win32con.GWL_EXSTYLE, exStyle)

label.pack()
label.mainloop()

Solution 2: pywin32

This example does everything through pywin32. This makes it more complicated and less portable, but considerably more powerful. I’ve included links to the relevant parts of the Windows API throughout the code.

import win32api, win32con, win32gui, win32ui

def main():
    hInstance = win32api.GetModuleHandle()
    className = 'MyWindowClassName'

    # http://msdn.microsoft.com/en-us/library/windows/desktop/ms633576(v=vs.85).aspx
    # win32gui does not support WNDCLASSEX.
    wndClass                = win32gui.WNDCLASS()
    # http://msdn.microsoft.com/en-us/library/windows/desktop/ff729176(v=vs.85).aspx
    wndClass.style          = win32con.CS_HREDRAW | win32con.CS_VREDRAW
    wndClass.lpfnWndProc    = wndProc
    wndClass.hInstance      = hInstance
    wndClass.hCursor        = win32gui.LoadCursor(None, win32con.IDC_ARROW)
    wndClass.hbrBackground  = win32gui.GetStockObject(win32con.WHITE_BRUSH)
    wndClass.lpszClassName  = className
    # win32gui does not support RegisterClassEx
    wndClassAtom = win32gui.RegisterClass(wndClass)

    # http://msdn.microsoft.com/en-us/library/windows/desktop/ff700543(v=vs.85).aspx
    # Consider using: WS_EX_COMPOSITED, WS_EX_LAYERED, WS_EX_NOACTIVATE, WS_EX_TOOLWINDOW, WS_EX_TOPMOST, WS_EX_TRANSPARENT
    # The WS_EX_TRANSPARENT flag makes events (like mouse clicks) fall through the window.
    exStyle = win32con.WS_EX_COMPOSITED | win32con.WS_EX_LAYERED | win32con.WS_EX_NOACTIVATE | win32con.WS_EX_TOPMOST | win32con.WS_EX_TRANSPARENT

    # http://msdn.microsoft.com/en-us/library/windows/desktop/ms632600(v=vs.85).aspx
    # Consider using: WS_DISABLED, WS_POPUP, WS_VISIBLE
    style = win32con.WS_DISABLED | win32con.WS_POPUP | win32con.WS_VISIBLE

    # http://msdn.microsoft.com/en-us/library/windows/desktop/ms632680(v=vs.85).aspx
    hWindow = win32gui.CreateWindowEx(
        exStyle,
        wndClassAtom,
        None, # WindowName
        style,
        0, # x
        0, # y
        win32api.GetSystemMetrics(win32con.SM_CXSCREEN), # width
        win32api.GetSystemMetrics(win32con.SM_CYSCREEN), # height
        None, # hWndParent
        None, # hMenu
        hInstance,
        None # lpParam
    )

    # http://msdn.microsoft.com/en-us/library/windows/desktop/ms633540(v=vs.85).aspx
    win32gui.SetLayeredWindowAttributes(hWindow, 0x00ffffff, 255, win32con.LWA_COLORKEY | win32con.LWA_ALPHA)

    # http://msdn.microsoft.com/en-us/library/windows/desktop/dd145167(v=vs.85).aspx
    #win32gui.UpdateWindow(hWindow)

    # http://msdn.microsoft.com/en-us/library/windows/desktop/ms633545(v=vs.85).aspx
    win32gui.SetWindowPos(hWindow, win32con.HWND_TOPMOST, 0, 0, 0, 0,
        win32con.SWP_NOACTIVATE | win32con.SWP_NOMOVE | win32con.SWP_NOSIZE | win32con.SWP_SHOWWINDOW)

    # http://msdn.microsoft.com/en-us/library/windows/desktop/ms633548(v=vs.85).aspx
    #win32gui.ShowWindow(hWindow, win32con.SW_SHOW)

    win32gui.PumpMessages()

def wndProc(hWnd, message, wParam, lParam):
    if message == win32con.WM_PAINT:
        hdc, paintStruct = win32gui.BeginPaint(hWnd)

        dpiScale = win32ui.GetDeviceCaps(hdc, win32con.LOGPIXELSX) / 60.0
        fontSize = 80

        # http://msdn.microsoft.com/en-us/library/windows/desktop/dd145037(v=vs.85).aspx
        lf = win32gui.LOGFONT()
        lf.lfFaceName = "Times New Roman"
        lf.lfHeight = int(round(dpiScale * fontSize))
        #lf.lfWeight = 150
        # Use nonantialiased to remove the white edges around the text.
        # lf.lfQuality = win32con.NONANTIALIASED_QUALITY
        hf = win32gui.CreateFontIndirect(lf)
        win32gui.SelectObject(hdc, hf)

        rect = win32gui.GetClientRect(hWnd)
        # http://msdn.microsoft.com/en-us/library/windows/desktop/dd162498(v=vs.85).aspx
        win32gui.DrawText(
            hdc,
            'Text on the screen',
            -1,
            rect,
            win32con.DT_CENTER | win32con.DT_NOCLIP | win32con.DT_SINGLELINE | win32con.DT_VCENTER
        )
        win32gui.EndPaint(hWnd, paintStruct)
        return 0

    elif message == win32con.WM_DESTROY:
        print 'Closing the window.'
        win32gui.PostQuitMessage(0)
        return 0

    else:
        return win32gui.DefWindowProc(hWnd, message, wParam, lParam)


if __name__ == '__main__':
    main()
Answered By: dln385

I had a similar need and discovered that the pygame library did a REALLY good job for what I was seeking. I was able to generate very large text that would update very fast and without flicker. See this topic below (first code ‘solution’ is it):

Simple way to display text on screen in Python?

I sped it up and it is fast. Also made the font much larger and that didn’t impact speed much at all. All of that running on a little Orange Pi Lite board (< $20). You can launch it from the command line w/o GUI (or from a windowed desktop) and in either case it is full-screen and doesn’t appear as a “windowed” app.

My assumption, as I’m new to Python and Pygame, is that you could load an image file as the background and then put text atop of that.

Oh, and I tried the same using the Tkinter example and it was slower, flickered, and the font just didn’t look “right”. Pygame was clearly the winner. It’s designed to blit things to the screen without “tearing” as games need to not flicker when images update. I was surprised that it wasn’t dependent on OpenGL because it was fast. I don’t think OpenGL is supported on the Orange Pi (can’t figure out a clear answer on that). So for 2D stuff, wow, pygame is impressive.!

Answered By: PaulD