31

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
dln385
  • 11,630
  • 12
  • 48
  • 58
  • Questions asking us to recommend or find a tool, library or favorite off-site resource are off-topic for Stack Overflow as they tend to attract opinionated answers and spam. Instead, describe the problem and what has been done so far to solve it. – jonrsharpe Feb 17 '14 at 22:27
  • @jon I noticed that you quoted point 5 from [this page](http://stackoverflow.com/help/on-topic), which contains a link with more information. I have edited my question and feel that it now meets the stackoverflow criteria for an on-topic question. – dln385 Feb 17 '14 at 23:27
  • Have you tried label.master.wm_attributes("-alpha", 1.0) for a transparent background? – Christian Rapp Feb 18 '14 at 20:12
  • @Christian That makes the whole label transparent, and it seems like it may be [impossible to make only the background transparent](http://stackoverflow.com/questions/19080499). As for making it show over fullscreen, I'm currently looking into [hardware overlay](http://msdn.microsoft.com/en-us/library/dd797814(VS.85).aspx) options. – dln385 Feb 18 '14 at 20:48
  • 1
    Oh sorry, I thought it could be used for a transparent background. But you can misuse the canvas widget --> http://stackoverflow.com/questions/17039481/how-to-create-transparent-widgets-using-tkinter – Christian Rapp Feb 18 '14 at 20:55
  • Canvas allows for text with a transparent background to be drawn on it, but the canvas itself can't have a transparent background. Or so I thought, until I found `label.master.wm_attributes("-transparentcolor", "white")`, which surprisingly works on both canvas and labels! I've updated the example in the question. – dln385 Feb 18 '14 at 21:49
  • @dln385 Hi, considering your problem and the answers you got, do you know if tkinter has a way to draw lines around the mouse cursor without canvas / windows, etc... ? – Romain Cendre Feb 05 '21 at 17:33

2 Answers2

43

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()
dln385
  • 11,630
  • 12
  • 48
  • 58
  • Awesome examples. Do you know if it's possible to use `LWA_COLORKEY` with a window that already exists? I managed to get full transparency working with `LWA_ALPHA`, but so far `LWA_COLORKEY | LWA_ALPHA` has been ineffective for partial transparency. – Enteleform Sep 27 '17 at 07:20
  • @dln385 - thanks for those solutions! Is it possible to somehow display text on constantly changing place? For example on an area of a particular window? – Jakub Bielan Jan 17 '19 at 18:48
  • 1
    @dln385 Hey, this might be a bit late, but how can you then exit this standalone label? How can you make it disappear within your code without having to terminate the script? – Alteros Jul 29 '20 at 02:27
  • How to remove the written text from the screen after writing it? – Vinayak Mikkal Feb 16 '21 at 10:01
  • If you want to remove the text, you should take a look at what win32gui.PumpMessages() is doing... basically it's waiting to process messages. In my case I just commented that line.... Now to send the close message, you can use the function win32gui.PostQuitMessage(hwnd). And you can get that hwnd at this point: win32gui.CreateWindowEx. – kuhi May 03 '22 at 09:55
  • Is it possible to draw a rectangle instead of text? – Carlos Porta Dec 19 '22 at 11:47
1

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.!

PaulD
  • 29
  • 1