Search code examples
pythonwxpythonscreenshot

How can I select an area on the screen and dim the unseleceted area with Python?


I'm building a screen capture application and I'm using wxPython, but feel free to suggest another library for this task.

I can place a full-screen, semi-transparent frame on top of the screen and I can draw a rectangle on top of it. But how do I make this rectangle fully transparent while the rest of the frame remains semi-transparent?

I've had this problem before when cropping an image. I had to use two images, one hidden and the other one was where the user selected an area to crop. Once the user made a selection, I computed the coordinates, clipped the hidden image and displayed it on top of the semi-transparent image. I would like to avoid that here if possible. What if the user wants to capture the screen of a video or moving object?


Solution

  • I can place a full-screen, semi-transparent frame on top of the screen and I can draw a rectangle on top of it. But how do I make this rectangle fully transparent while the rest of the frame remains semi-transparent?

    You can't. A fully-transparent rectangle is just going to show whatever's behind it, which is a semi-transparent rectangle.

    But you can easily just turn it around. Create a fully-transparent frame, then draw a semi-transparent region—that is, everything but your rectangle—on top of the frame.

    The reason the first way doesn't work is that your fully-transparent rectangle is just going to show whatever's behind it, which is a semi-transparent rectangle.

    There are a number of ways to draw a region like that.

    The simplest is to break it down into four rectangles, like this pseudocode:

    drawrect(0, 0, top, -1)
    drawrect(top, 0, bottom, left)
    drawrect(top, 0, bottom, right)
    drawrect(bottom, 0, -1, -1)
    

    Building a region is probably more readable (just build a whole-frame region, then subtract the rect), but drawing a region is more painful. Alternatively, you could draw an unfilled rect one pixel around your rect, then another one around the window borders, then flood from (1,1), but then you have to deal with off-by-one errors. Or you could create a polygon with 8 pixels and trick the winding rule. And so on. But I think this is easiest.


    Let's say you build a "polygon" with these points: TL, TR, BR, BL, TL, tl, bl, br, tr, tl, TL (where caps means outer rectangle edges, and lowercase inner rectangle edges). That's not really a polygon at all, but if you ask what pixels are inside that polygon (as you do when you tell it to draw the polygon with a fill), wx has to do something. wx has two rules that you can choose: the even-odd rule counts how many times a line from a pixel to some point at infinity crosses the polygon, while the winding-number rule counts how many loops the polygon makes around a pixel.

    With the even-odd rule: Every pixel inside both rectangles has either 2 or 4 crossings (depending on whether the point at infinity is across the line). Every pixel outside both has 0, 2, 4, or 6 (because it may cross just the outer rectangle or both, and may cross the line in either case). Every pixel between them but not on the line has 1, 3, or 5 (because it will cross the outer rectangle exactly once, and may cross the inner rectangle and/or the line). Every pixel on the line… that's where it gets tricky. wx leaves it up to each platform backend to implement the rule, and on some of them, some points on the line may be counted as starting on one line and crossing the other, meaning they end up with 2 (or 4) crossings, and therefore aren't filled. So, you can end up with a set of gaps that together make up a 1-pixel-wide dashed line. Drawing a stroke as well as a fill should fix it, but I'm not sure it does.

    With the winding rule: In theory, every pixel anywhere on the screen should be considered outside, and not filled. However, the way it's actually implemented, pixels inside both rectangles or outside both see half the curve exactly canceling the other half, while pixels between the rectangles only see the line canceling itself, while the rectangles both show up as clockwise. Even if the platform does something weird and doesn't see the line canceling itself out properly, that's still fine; it can only make the pixel even less canceled out.