首页 > 解决方案 > “监视器”而不是“屏幕”或“桌面”上的 Python 居中窗口

问题描述

关于如何在屏幕上居中显示 python tkinter 窗口有很多问题,答案很有效。我的问题是我所谓的“屏幕”是这样的:

mmm 多显示器.png

尽管您可以将窗口部分(或全部)移动到灰色区域,但它们实际上不会出现在我的三台显示器上。左上显示器为 1920x1080,右上显示器为 3840x2160,右下显示器为 1920x1080。

可以通过桌面图标启动程序,该图标可以在任何监视器上,也可以通过可以在任何监视器上的图标启动gnome-terminal。如何发现:

虽然我使用的是 Gnome Desktop,但我希望支持使用 X11 或 Wayland 的所有 Linux 风格。此外,我最近试用了 ChromeOS Linux Beta,对它的支持也很好。此外,非常需要对 Windows 和 OSX 的支持。

我已经安装并使用了许多工具gi, wnck, xdotool,wmctrl让我陷入了困境。我希望他们是一个流行的 python 库(最好是通过apt-get而不是pipor安装pip3),可以将“屏幕”、“桌面”和“监视器”暴露给 python。

标签: pythonscreendesktopmonitor

解决方案


我回答了我自己的问题。这是阻止您在周六晚上午夜入睡的答案之一,因此您在周日凌晨 1:00 起床并编码到凌晨 4:30。

这是您可以适应非 Ubuntu 环境的代码(使用“未来代码”功能):

#!/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

推荐阅读