Five years ago, I wrote about Clicking through a translucent window: using X11 input shapes and how I used a translucent image window that allows click-through, positioned on top of PyTopo, to trace an image of an old map and create tracks or waypoints.
But the transimageviewer.py app that I wrote then was based on GTK2, which is now obsolete and has been removed from most Linux distro repositories. So when I found myself wanting GIS to help investigate a growing trail controversy in Pueblo Canyon, I discovered I didn't have a usable click-through image viewer.
Searching for a GTK3 Solution
I've updated many of my older Python2/GTK2 scripts to Python3/GTK3.
That's not too hard for the simple stuff,
but when it comes to specialized features like
the Gnome/GTK team has a bad habit of removing features
without providing an updated equivalent. (Or, if there is a
modern equivalent, it's impossible to find because the documentation
is so scant.)
After struggling for an hour or so trying to find a way to accomplish the task in GTK3, I realized that with GTK4 is already out, any time I spent on GTK3 now will have to be repeated soon (and if you think it's hard to figure out how to do things in GTK3, just try GTK4!) I'm increasingly leaning toward TkInter when possible, because it doesn't gratuitously force everyone to rewrite everything every few years like GTK and Qt. TkInter isn't as full of bells and whistles as the other two, but at least it doesn't change all the time.
It was surprisingly easy to show a translucent image in TkInter. But as far as I can tell, there's no way within the toolkit to make a window that passes clicks through to the window underneath. I found plenty of people asking, and the answer was always no.
Somewhere along the line, though, I stumbled upon a nifty Python-Xlib demo: shapewin.py. Maybe I could take the translucent window I'd just created with TkInter and apply some Xlib to set the input shape mask?
Alas, I was never able to get that to work. The first problem was
getting the Window ID of the Xlib window. Not only does Tk offer no
way to get the ID directly, but Tk windows are also invisible in
lists of X clients, like
xroot.query_tree(), so you can't just search the X
tree for the right window.
But at least in most window managers, when you create a new window, it
immediately gets the input focus. So if you call Xlib's
get_input_focus().focus right after the window is mapped,
you get the Xlib handle for the window you just created. An ugly hack,
but probably good enough.
Unfortunately, even with the window ID, applying the input mask from Xlib did nothing to the Tk window. Oh, well.
The TkInter attempt, such as it is, can be found at transimageviewer_tk.py.
Images in Xlib
But that Xlib shapewin.pydemo worked nicely. If I could just display an image in a pure Xlib app, I could use the same techniques.
There are remarkably few examples (really, none) of how to show an image in a Python-Xlib window. Xlib programs in C tend to use the Xm or Xpm libraries, which have no Python equivalent. (The only image handling built into Xlib itself involves monochrome bitmaps and doesn't offer niceties like reading JPEG or PNG files.)
But if you call help() on an Xlib window object and look closely,
you'll discover the intriguingly named function
put_pil_image(). PIL is the Python Image Library;
taking a PIL.Image and displaying it sounded ideal, and it worked fine.
It doesn't work if you call it when setting up the window (that's
typical for X toolkits), so I call it from the main loop whenever a
ConfigureNotify or Expose event happens.
Translucency Required Some Bit-Fiddlng
Next: make it translucent. In Xlib, that turns out to involve calling
change_property with the X atom
_NET_WM_WINDOW_OPACITY. I found several C examples,
but I had to fiddle around a bit figuring out how to pass the
32-bit value that Xlib needed. This worked, presuming
self.opacity is a floating point number between 0 and 1:
onebyte = int(0xff * self.opacity) fourbytes = onebyte | (onebyte << 8) | (onebyte << 16) | (onebyte << 24) XA_NET_WM_WINDOW_OPACITY = \ self.dpy.intern_atom('_NET_WM_WINDOW_OPACITY') self.win.change_property(self.dpy.get_atom('_NET_WM_WINDOW_OPACITY'), Xatom.CARDINAL, 32, [fourbytes])There's probably a cleaner way to do that. I haven't done much with bit operations in Python.
Finally, the Input Shape
Then I wrote some code adapted from shapewin.py except that
shape.SK.Input instead of
input_pm = self.screen.root.create_pixmap(geom.width, geom.height, 1) gc = input_pm.create_gc(foreground=1, background=0) input_pm.fill_rectangle(gc, 0, 0, geom.width, 20) gc.change(foreground=0) input_pm.fill_rectangle(gc, 0, 20, geom.width, geom.height-20) # SO options are Intersect, Invert, Set, Subtract, Union # SK options are Bounding, Clip, Input self.win.shape_mask(shape.SO.Set, shape.SK.Input, 0, 0, input_pm)
That fills the image with black except for a white rectangle along the top of the window. That's because adding the input mask made the window's titlebar no longer visible to the window manager, so I can't move the window that way. By making a strip along the top that's exempted from the input mask, I can use that along with the window manager's "move window" key binding (in Openbox I hold down the "Windows" key while dragging).
It's a little flaky: I can W-drag from the top middle of the image most of the time, but sometimes not, and it never works from closer to the top two corners. I'm not sure why that is; but if I try a couple of times I can move the window where I want it, so this is at least usable even if it's not perfect.
The Xlib version: transimageviewer_x.py.
I wrote the preceding as though I went straight to Xlib, skipping over the frustrating most-of-a-day I spent pounding away at getting the Tk version to work and then trying and failing to find an example of showing an image in Python-Xlib. Such is programming.
But while I was hitting a wall with Tk and Xlib, I discovered that Qt5 makes it very easy to make a window translucent:
And Qt5 also offers the intriguingly named
Qt.WindowTransparentForInput, which sounded like exactly
what I wanted. Seemingly all I needed to do was
self.setWindowFlags(Qt.Popup|Qt.WindowDoesNotAcceptFocus | Qt.WindowTransparentForInput)
Except, sadly, that didn't work. Apparently TransparentForInput only means it will pass events down to a lower window from the same app, which didn't help me at all.
But it turns out there's another flag,
that actually does pass mouse events through. I needed both of them:
self.setWindowFlags(self.windowFlags() | Qt.WindowTransparentForInput | Qt.X11BypassWindowManagerHint )
This has the effect that the window manager doesn't manage that window at all, meaning it doesn't get a titlebar and you can't move it. I never found any way to move the window; so I added a commandline option for the coordinates at which the window should appear, and figured for fine adjustments I can move whatever window is underneath.
The Qt script: transimageviewer_qt5.py.
So now I have two, albeit somewhat hacky, ways of clicking through an
image. And meanwhile, other things I need to do have been piling up,
so it may be a while before I find time to actually use them
to make those Pueblo Canyon maps. Maybe this weekend ...
[ 19:08 Jun 23, 2022 More programming | permalink to this entry | ]