1

There are many questions on how to center a python tkinter window on the screen and the answer works well. My problem is my so-called "screen" looks like this:

mmm multiple monitors.png

Although you can move windows partially (or entirely) to the grey areas they won't actually show up on any of my three monitors. Top left monitor is 1920x1080, top right monitor is 3840x2160 and bottom right monitor is 1920x1080.

A program can be started via desktop icon which could be on any monitor or via gnome-terminal which could be on any monitor. How does one discover:

  • Which monitor was active when python was invoked?
  • Coordinates of active monitor within the screen real estate?

Although I'm using Gnome Desktop I'd like support for all Linux flavors using X11 or Wayland. Additionally I tried out ChromeOS Linux Beta lately and support for it would also be nice. Furthermore support for Windows and OSX is highly desired.

I've already installed and used many tools gi, wnck, xdotool, wmctrl that hem me into a corner. I'm hoping their is a popular python library (preferably installed via apt-get and not pip or pip3) that can expose "screen", "desktop" and "monitors" to python.

WinEunuuchs2Unix
  • 1,801
  • 1
  • 17
  • 34

1 Answers1

1

I answered my own question. It was one of those answers that stops you from falling asleep Saturday night at midnight so you get up at 1:00 am on Sunday and code until 4:30 am.

Here's the code which you can adapt for non-Ubuntu environments (using the "future code" functions):

#!/usr/bin/env python
# -*- coding: utf-8 -*-

#==============================================================================
#
#       m - Wrapper for mserve.py
#
#==============================================================================

'''
        Splash screen for mserve.
        mserve has it's own list of required modules but this wrapper requires:
        
            Gnome Desktop Toolkit (Gdk)
'''

from __future__ import print_function       # Must be first import

try:
    import tkinter as tk
    PYTHON_VER="3"
except ImportError: # Python 2
    import Tkinter as tk
    PYTHON_VER="2"

import image as img                         # Routines for tk & photo images
import mserve                               # Script loaded as module for .pyc

# https://stackoverflow.com/a/36419702/6929343
import logging
logging.getLogger('PIL').setLevel(logging.WARNING)
import sys

logging.basicConfig(format='%(asctime)s %(levelname)s %(message)s',
                    level=logging.DEBUG,
                    stream=sys.stdout)

''' Future code '''
def get_active_window():
    """
    From: https://stackoverflow.com/a/36419702/6929343
    Get the currently active window.

    Returns
    -------
    string :
        Name of the currently active window.
    """
    import sys
    active_window_name = None
    logging.info('sys.platform: ' + sys.platform)
    print('sys.platform:', sys.platform)
    if sys.platform in ['linux', 'linux2']:
        # Alternatives: http://unix.stackexchange.com/q/38867/4784
        try:
            import wnck
        except ImportError:
            logging.info("wnck not installed")
            wnck = None
        if wnck is not None:
            screen = wnck.screen_get_default()
            screen.force_update()
            window = screen.get_active_window()
            if window is not None:
                pid = window.get_pid()
                with open("/proc/{pid}/cmdline".format(pid=pid)) as f:
                    active_window_name = f.read()
        else:
            try:
                # Next 3 limes from: https://stackoverflow.com/a/43349245/6929343
                import gi
                gi.require_version('Gtk', '3.0')
                gi.require_version('Wnck', '3.0')
                # Continue with original code:
                from gi.repository import Gtk, Wnck
                gi = "Installed"
            except ImportError:
                logging.info("gi.repository not installed")
                gi = None
            if gi is not None:
                Gtk.init([])  # necessary if not using a Gtk.main() loop
                screen = Wnck.Screen.get_default()
                screen.force_update()  # recommended per Wnck documentation
                active_window = screen.get_active_window()
                pid = active_window.get_pid()
                with open("/proc/{pid}/cmdline".format(pid=pid)) as f:
                    active_window_name = f.read()
    elif sys.platform in ['Windows', 'win32', 'cygwin']:
        # http://stackoverflow.com/a/608814/562769
        import win32gui
        window = win32gui.GetForegroundWindow()
        active_window_name = win32gui.GetWindowText(window)
    elif sys.platform in ['Mac', 'darwin', 'os2', 'os2emx']:
        # http://stackoverflow.com/a/373310/562769
        from AppKit import NSWorkspace
        active_window_name = (NSWorkspace.sharedWorkspace()
                              .activeApplication()['NSApplicationName'])
    else:
        print("sys.platform={platform} is unknown. Please report."
              .format(platform=sys.platform))
        print(sys.version)
    print("Active window: %s" % str(active_window_name))
    return active_window_name


''' Future code '''
def get_GtkWindow(w):

    # From: https://askubuntu.com/a/303754/307523
    import gi
    gi.require_version('Gdk', '3.0')
    gi.require_version('Gtk', '3.0')
    from gi.repository import Gdk, Gtk

    # Replace w with the GtkWindow of your application
    w = Gtk.Window()
    # Get the screen from the GtkWindow
    s = w.get_screen()
    # Using the screen of the Window, the monitor it's on can be identified
    m = s.get_monitor_at_window(s.get_active_window())
    # Then get the geometry of that monitor
    monitor = s.get_monitor_geometry(m)
    # This is an example output
    print("Height: %s, Width: %s, X: %s, Y: %s" % \
         (monitor.height, monitor.width, monitor.x, monitor.y))


''' Future code '''
def get_monitors():
    """
        Get list of monitors in Gnome Desktop
    """
    import gi
    gi.require_version('Gdk', '3.0')
    from gi.repository import Gdk

    global NUMBER_OF_MONITORS, GNOME, ACTIVE_MONITOR, MONITOR_GEOMETRY

    display = Gdk.Display.get_default()
    screen = display.get_default_screen()
    window = screen.get_active_window()
    ACTIVE_MONITOR = screen.get_monitor_at_window(window)
    print('ACTIVE_MONITOR:', ACTIVE_MONITOR)

    # Gnome version 3.22 developed new monitor object
    try:
        # Gnome 3.22
        NUMBER_OF_MONITORS = display.get_n_monitors()
        monitor = display.get_monitor(ACTIVE_MONITOR)
        MONITOR_GEOMETRY = monitor.get_geometry()
        GNOME=3.22
    except:
        # Gnome 3.18
        NUMBER_OF_MONITORS = screen.get_n_monitors()
        MONITOR_GEOMETRY = screen.get_monitor_geometry(ACTIVE_MONITOR)
        GNOME=3.18

    # collect data about monitors
    for index in range(NUMBER_OF_MONITORS):

        if GNOME==3.22:
            monitor = display.get_monitor(index)
            geometry = monitor.get_geometry()
            name = monitor.get_monitor_plug_name()
        else:
            geometry = screen.get_monitor_geometry(index)
            name = screen.get_monitor_plug_name(index)

        print("Monitor {} = {}x{}+{}+{}".format(index, geometry.width, \
              geometry.height, geometry.x, geometry.y), name)


#get_monitors()
#print('ACTIVE_MONITOR:', ACTIVE_MONITOR, 'MONITOR_GEOMETRY:', MONITOR_GEOMETRY)

''' Start of REAL code used today (May 2, 2021) '''
def get_window_monitor(window):
    """
    Returns the Gdk monitor geometry rectangle tkinter window is on.
    If window is off screen force it into Monitor 1 (index 0).

    :param window: Tkinter root or Topleel
    """
    import gi
    gi.require_version('Gdk', '3.0')
    from gi.repository import Gdk

    # global variables that might be useful down the road but not on May 2, 2021
    global NUMBER_OF_MONITORS, GNOME

    display = Gdk.Display.get_default()
    screen = display.get_default_screen()

    # Gnome version 3.22 deprecated what used to work 3.18.
    # Gonme wasn't built in a day but, it was burned over night in next release!
    try:
        # Gnome 3.22
        NUMBER_OF_MONITORS = display.get_n_monitors()
        GNOME=3.22
    except:
        # Gnome 3.18
        NUMBER_OF_MONITORS = screen.get_n_monitors()
        GNOME=3.18

    x = window.winfo_x()                    # Window's left coordinate on screen
    y = window.winfo_y()                    # Window's top coordinate on screen
    if x < 0: x = 0                         # Window top left may be off screen!
    if y < 0: y = 0

    first_monitor = None

    for index in range (NUMBER_OF_MONITORS):
        if GNOME==3.22:
            # Gnome version 3.22 developed new monitor object
            monitor = display.get_monitor(index)
            mon_geom = monitor.get_geometry()
        else:
            # Gnome version 3.18 uses screen object for monitor properties
            mon_geom = screen.get_monitor_geometry(index)

        # Save first monitor if needed later
        if not first_monitor:
            first_monitor = mon_geom
        
        # Copmare to monitor's coordinates on screen and monitor width x height
        if x <  mon_geom.x: continue
        if x >= mon_geom.x + mon_geom.width: continue
        if y <  mon_geom.y: continue
        if y >= mon_geom.y + mon_geom.height: continue

        # Window is comletely on this monitor.
        return mon_geom

    # If window off of screen use first monitor
    return first_monitor


def center(window):
    """
    From: https://stackoverflow.com/a/10018670/6929343
    centers a tkinter window on monitor in multi-monitor setup
    :param win: the main window or Toplevel window to center
    """

    window.update_idletasks()               # Refresh window's current position
    mon_geom=get_window_monitor(window)     # Monitor geometry window is on

    if mon_geom is None: 
        logging.error("No monitors found!")
        return None

    # Calcuate X, Y of window to center within monitors X, Y, width and height
    x = mon_geom.width // 2 - window.winfo_width() // 2 + mon_geom.x
    y = mon_geom.height // 2 - window.winfo_height() // 2 + mon_geom.y
    if x < 0: x = 0                         # Window top left may be off screen!
    if y < 0: y = 0

    window.geometry('+{}+{}'.format(x, y))
    window.deiconify()                      # Forces window to appear

    return mon_geom


def main():
    """
    Create splash screen and invoke mserve.py which takes a second or more
    """
    splash = tk.Tk()                        # "very top" toplevel
    splash.title("Music Server - mserve")

    ''' Set font style for all fonts including tkSimpleDialog.py '''
    img.set_font_style()    # Make messagebox text larger for HDPI monitors

    ''' Get splash image '''
    splash_image = img.m_splash_image(300, 'white', 'lightskyblue', 'black')

    # create and pack the canvas. Then load image file
    canvas = tk.Canvas(width=300, height=300, bg='black')
    canvas.pack(expand=tk.YES, fill=tk.BOTH)
    canvas.create_image(0, 0, image=splash_image, anchor=tk.NW)
    splash.update_idletasks()               # This is required for visibility

    # Cemter splash screen on monitor and get monitors geometry
    mon_geom=center(splash)
    splash.update()                         # This is required for visibility

    # At this point make window undecorated, don't do it sooner!
    # From: https://stackoverflow.com/a/37199655/6929343
    splash.overrideredirect(True)           # Undecorated to prevent close

    # Call mserve module about 10k lines of code
    mserve.main(toplevel=splash, mon_geom=mon_geom)
    exit()                                  # Required to close mserve library
    splash.mainloop()


if __name__ == "__main__":
    main()

# End of m
WinEunuuchs2Unix
  • 1,801
  • 1
  • 17
  • 34