Search code examples
pythondrawingpywin32updatingwin32gui

How to draw an empty rectangle on screen with Python


I am not an expert and I am trying to show a rectangle on screen which follows mouse movements from a settle starting point, just as when you select something in word or paint. I came with this code:

import win32gui
m=win32gui.GetCursorPos()
while True:
    n=win32gui.GetCursorPos()
    for i in range(n[0]-m[0]):
        win32gui.SetPixel(dc, m[0]+i, m[1], 0)
        win32gui.SetPixel(dc, m[0]+i, n[1], 0)
    for i in range(n[1]-m[1]):
        win32gui.SetPixel(dc, m[0], m[1]+i, 0)
        win32gui.SetPixel(dc, n[0], m[1]+i, 0)

As you can see, the code will draw the rectangle, but the previous ones will remain until the screen updates.

The only solution I've came with is to take the pixel values i will paint before set them black, and redraw them every time, but this makes my code pretty slow. Is there an easy way to update the screen faster to prevent this?

...

Edited with solution.

As suggested by @Torxed, using win32gui.InvalidateRect solved the updating problem. However, I found that setting only the color of the points I need to be set is cheaper than asking for a rectangle. The first solution renders quite clean, while the second remains a little glitchy. At the end, the code that worked the best for me is:

import win32gui

m=win32gui.GetCursorPos()
dc = win32gui.GetDC(0)

while True:
    n=win32gui.GetCursorPos()
    win32gui.InvalidateRect(hwnd, (m[0], m[1], GetSystemMetrics(0), GetSystemMetrics(1)), True)
    back=[]
    for i in range((n[0]-m[0])//4):
        win32gui.SetPixel(dc, m[0]+4*i, m[1], 0)
        win32gui.SetPixel(dc, m[0]+4*i, n[1], 0)
    for i in range((n[1]-m[1])//4):
        win32gui.SetPixel(dc, m[0], m[1]+4*i, 0)
        win32gui.SetPixel(dc, n[0], m[1]+4*i, 0)

The division and multiplication by four is necessary to avoid flickering, but is visually the same as using DrawFocusRect.

This will only work if you remain bellow and to the right from your initial position, but it is just what I needed. Not difficult to improve it to accept any secondary position.


Solution

  • In order to refresh the old drawn area, you need to either call win32gui.UpdateWindow or something similar to update your specific window, but since you're not technically drawing on a surface, but the entire monitor. You'll need to invalidate the entire region of your monitor in order to tell windows to re-draw anything on it (or so I understand it).

    And to overcome the slowness, instead of using for loops to create the boundary which will take X cycles to iterate over before completing the rectangle, you could use win32ui.Rectangle to draw it in one go:

    import win32gui, win32ui
    from win32api import GetSystemMetrics
    
    dc = win32gui.GetDC(0)
    dcObj = win32ui.CreateDCFromHandle(dc)
    hwnd = win32gui.WindowFromPoint((0,0))
    monitor = (0, 0, GetSystemMetrics(0), GetSystemMetrics(1))
    
    while True:
        m = win32gui.GetCursorPos()
        dcObj.Rectangle((m[0], m[1], m[0]+30, m[1]+30))
        win32gui.InvalidateRect(hwnd, monitor, True) # Refresh the entire monitor
    

    Further optimizations could be done here, like not update the entire monitor, only the parts where you've drawn on and so on. But this is the basic concept :)

    And to create a rectangle without the infill, you could swap Rectangle for DrawFocusRect for instance. Or for more control, even use win32gui.PatBlt

    And apparently setPixel is the fastest, so here's my final example with color and speed, altho it's not perfect as the RedrawWindow doesn't force a redraw, it simply asks windows to do it, then it's up to windows to honor it or not. InvalidateRect is a bit nicer on performance as it asks the event handler to clear the rect when there's free time to do so. But I haven't found a way more agressive than RedrawWindow, even tho that is still quite gentle. An example to this is, hide the desktop icons and the below code won't work.

    import win32gui, win32ui, win32api, win32con
    from win32api import GetSystemMetrics
    
    dc = win32gui.GetDC(0)
    dcObj = win32ui.CreateDCFromHandle(dc)
    hwnd = win32gui.WindowFromPoint((0,0))
    monitor = (0, 0, GetSystemMetrics(0), GetSystemMetrics(1))
    
    red = win32api.RGB(255, 0, 0) # Red
    
    past_coordinates = monitor
    while True:
        m = win32gui.GetCursorPos()
    
        rect = win32gui.CreateRoundRectRgn(*past_coordinates, 2 , 2)
        win32gui.RedrawWindow(hwnd, past_coordinates, rect, win32con.RDW_INVALIDATE)
    
        for x in range(10):
            win32gui.SetPixel(dc, m[0]+x, m[1], red)
            win32gui.SetPixel(dc, m[0]+x, m[1]+10, red)
            for y in range(10):
                win32gui.SetPixel(dc, m[0], m[1]+y, red)
                win32gui.SetPixel(dc, m[0]+10, m[1]+y, red)
    
        past_coordinates = (m[0]-20, m[1]-20, m[0]+20, m[1]+20)
    

    Issues with positions and resolution? Be aware that high DPI systems tend to cause a bunch of issues. And I haven't found many ways around this other than going over to a OpenGL solution or using frameworks such as wxPython or OpenCV other than this post: Marking Your Python Program as High DPI Aware Seamlessly Windows

    Or changing the Windows display scale to 100%:

    100%

    This causes the positioning issue to go away, perhaps take this to account by querying the OS for the scale and compensate.


    The only reference I could find on the "clearing the old drawings" was this post: win32 content changed but doesn't show update unless window is moved tagged c++ win winapi. Hopefully this saves some people from searching before finding a good example.