3

I've read through related answers and it seems that the accepted way to do this is by binding callbacks to <Map> and <Unmap> events in the Toplevel widget. I've tried the following but to no effect:

from Tkinter import *

tk = Tk()

def visible(event):
    print 'visible'

def invisible(event):
    print 'invisible'

tk.bind('<Map>', visible)
tk.bind('<Unmap>', invisible)

tk.mainloop()

I'm running python 2.7 on Linux. Could this be related to window manager code in different operating systems?

Calling tk.iconify() before tk.mainloop() has no effect either. In fact, the only command that produces the correct behavior is tk.withdraw() which is certainly not the same thing as minimizing the window. Additionally, if <Map> and <Unmap> events are triggered by calling pack(), grid(), or place(), why is <Map> triggered when the application window is minimized on Windows and/or Mac, as this and this answer suggest. And why would they be triggered when calling withdraw() and deiconify() on Linux?

Community
  • 1
  • 1
bzrr
  • 1,490
  • 3
  • 20
  • 39
  • 1
    Unmap on windows is quite different from Linux. On Linux it means making the window *disappear*, beyond iconified. It does even not show up in `wmctrl -l` anymore. I tried to Unmap your window with `xdotool windowunmap `, the interpreter correctly showed "invisible". Mapping it again with `xdotool windowmap` showed "visible" again (Ubuntu 16.04) – Jacob Vlijm Sep 20 '16 at 05:00
  • @JacobVlijm So there's no way, on Linux, of executing a callback when a Toplevel window is iconified/deiconified in Tkinter? – bzrr Sep 20 '16 at 05:18
  • I am searching my butt off, but so far, no luck. There is of course a workaround creating a thread / Queue to check with `tk.state()`, but a binding would be much more desirable. – Jacob Vlijm Sep 20 '16 at 05:21
  • Hi Jovito, see my updated answer. Although dirty, it works fine on at least Ubuntu 16.04, should work fine on other distro's as well. – Jacob Vlijm Sep 20 '16 at 08:50

3 Answers3

3

Unmapping on Linux

The term Unmap has a quite different meaning on Linux than it has on Windows. On Linux, Unmapping a window means making it (nearly) untraceable; It does not appear in the application's icon, nor is it listed anymore in the output of wmctrl -l. We can unmap / map a window by the commands:

xdotool windowunmap <window_id>

and:

xdotool windowmap <window_id>

To see if we can even possibly make tkinter detect the window's state minimized, I added a thread to your basic window, printing the window's state once per second, using:

root.state()

Minimized or not, the thread always printed:

normal

Workaround

Luckily, if you must be able to detect the window's minimized state, on Linux we have alternative tools like xprop and wmctrl. Although as dirty as it gets, it is very well scriptable reliably inside your application.

As requested in a comment, below a simplified example to create your own version of the bindings with external tools.

enter image description here

How it works

  • When the window appears (the application starts), We use wmctrl -lp to get the window's id by checking both name and pid (tkinter windows have pid 0).
  • Once we have the window id, we can check if the string _NET_WM_STATE_HIDDEN is in output of xprop -id <window_id>. If so, the window is minimized.

Then we can easily use tkinter's after() method to include a periodic check. In the example below, the comments should speak for themselves.

What we need

We need both wmctrl and xprop to be installed. On Dedian based systems:

sudo apt-get install wmctrl xprop

The code example

import subprocess
import time
from Tkinter import *

class TestWindow:

    def __init__(self, master):
        self.master = master
        self.wintitle = "Testwindow"
        self.checked = False
        self.state = None
        button = Button(self.master, text = "Press me")
        button.pack()
        self.master.after(0, self.get_state)
        self.master.title(self.wintitle)

    def get_window(self):
        """
        get the window by title and pid (tkinter windows have pid 0)
        """
        return [w.split() for w in subprocess.check_output(
            ["wmctrl", "-lp"]
            ).decode("utf-8").splitlines() if self.wintitle in w][-1][0]

    def get_state(self):
        """
        get the window state by checking if _NET_WM_STATE_HIDDEN is in the
        output of xprop -id <window_id>
        """
        try:
            """
            checked = False is to prevent repeatedly fetching the window id
            (saving fuel in the loop). after window is determined, it passes further checks.
            """
            self.match = self.get_window() if self.checked == False else self.match
            self.checked = True
        except IndexError:
            pass
        else:
            win_data = subprocess.check_output(["xprop", "-id", self.match]).decode("utf-8")
            if "_NET_WM_STATE_HIDDEN" in win_data:
                newstate = "minimized"
            else:
                newstate = "normal"
            # only take action if state changes
            if newstate != self.state:
                print newstate
                self.state = newstate
        # check once per half a second
        self.master.after(500, self.get_state)

def main(): 
    root = Tk()
    app = TestWindow(root)
    root.mainloop()

if __name__ == '__main__':
    main()
Community
  • 1
  • 1
Jacob Vlijm
  • 3,099
  • 1
  • 21
  • 37
  • @Jovito sure, either in a minute or late tonight. Have to run in a minute. – Jacob Vlijm Sep 20 '16 at 07:18
  • Any idea why state isn't updated immediately when maximizing the window even if a very low amount of time is given to `after()`? – bzrr Sep 20 '16 at 09:29
  • What are your settings? It updates instantly on my system. – Jacob Vlijm Sep 20 '16 at 09:37
  • @Jovito Just s question, but if it solves your issue, wiuld you consider accepting? I cannot help things are this way :) – Jacob Vlijm Sep 20 '16 at 10:01
  • I'll wait a couple more days to see how people vote. If your answer is favored by others, I'll be sure to accept it. – bzrr Sep 20 '16 at 10:05
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/123789/discussion-between-jovito-and-jacob-vlijm). – bzrr Sep 20 '16 at 12:58
3

My own implementation of the hack suggested by Jacob.

from Tkinter import Tk, Toplevel
import subprocess


class CustomWindow(Toplevel):

    class State(object):
        NORMAL = 'normal'
        MINIMIZED = 'minimized'

    def __init__(self, parent, **kwargs):
        Toplevel.__init__(self, parent, **kwargs)
        self._state = CustomWindow.State.NORMAL
        self.protocol('WM_DELETE_WINDOW', self.quit)
        self.after(50, self._poll_window_state)

    def _poll_window_state(self):
        id = self.winfo_id() + 1
        winfo = subprocess.check_output(
            ['xprop', '-id', str(id)]).decode('utf-8')

        if '_NET_WM_STATE_HIDDEN' in winfo:
            state = CustomWindow.State.MINIMIZED
        else:
            state = CustomWindow.State.NORMAL

        if state != self._state:
            sequence = {
                CustomWindow.State.NORMAL: '<<Restore>>',
                CustomWindow.State.MINIMIZED: '<<Minimize>>'
            }[state]
            self.event_generate(sequence)
            self._state = state

        self.after(50, self._poll_window_state)


if __name__ == '__main__':
    root = Tk()
    root.withdraw()
    window = CustomWindow(root)

    def on_restore(event):
        print 'restore'

    def on_minimize(event):
        print 'minimize'

    window.bind('<<Restore>>', on_restore)
    window.bind('<<Minimize>>', on_minimize)

    root.mainloop()
Community
  • 1
  • 1
bzrr
  • 1,490
  • 3
  • 20
  • 39
1

For me, on Win10, your code works perfectly, with the caveat that the middle frame button produces 'visible' whether it means 'maximize' or 'restore'. So maximize followed by restore results in 2 new 'visibles' becoming visible.

I did not particularly expect this because this reference says Map is produced when

A widget is being mapped, that is, made visible in the application. This will happen, for example, when you call the widget's .grid() method.

Toplevels are not managed by geometry. The more authoritative tk doc says

Map, Unmap

The Map and Unmap events are generated whenever the mapping state of a window changes.

Windows are created in the unmapped state. Top-level windows become mapped when they transition to the normal state, and are unmapped in the withdrawn and iconic states.

Try adding tk.iconify() before the mainloop call. This should be doing the same as the minimize button. If it does not result in 'invisible', then there appears to be a tcl/tk bug on Linux.

Community
  • 1
  • 1
Terry Jan Reedy
  • 18,414
  • 3
  • 40
  • 52