Python Forum
Is there a way to call and focus any popup window outside of the main window app?
Thread Rating:
  • 0 Vote(s) - 0 Average
  • 1
  • 2
  • 3
  • 4
  • 5
Is there a way to call and focus any popup window outside of the main window app?
#1
Hi guys, I am trying to open a popup window when the user types something.

I read somewhere that Windows only allows you to focus a popup window if it has been called form the main window.

I search in the web and found this "work around", not very robust, because the methods to force focus were not working:



 def bring_window_to_foreground(self):
        self.top_window.update_idletasks()
        hwnd = win32gui.FindWindow(None, "Select Expansion")
        shell = win32com.client.Dispatch("WScript.Shell")
        shell.SendKeys("%")
        time.sleep(0.1)
        shell.SendKeys("%")
        win32gui.SetForegroundWindow(hwnd)
        # Send another Alt key to nullify the activation
        self.top_window.focus_force()
But sometimes this wont work and apps the use "alt" key to show menus like notepad will have problemas sometimes.

Is there any library or windows api I can use to send my tkinter window to foreground, always on top and focused, withou calling it from the main window app, that will be minimized?

And one more question? How do I show this popup exactly on the same place or closer to the text cursor? I am using the code below, but is not working. Thanks in advance.

def get_caret_position(self):
        class GUITHREADINFO(ctypes.Structure):
            _fields_ = [("cbSize", ctypes.c_ulong),
                        ("flags", ctypes.c_ulong),
                        ("hwndActive", ctypes.wintypes.HWND),
                        ("hwndFocus", ctypes.wintypes.HWND),
                        ("hwndCapture", ctypes.wintypes.HWND),
                        ("hwndMenuOwner", ctypes.wintypes.HWND),
                        ("hwndMoveSize", ctypes.wintypes.HWND),
                        ("hwndCaret", ctypes.wintypes.HWND),
                        ("rcCaret", ctypes.wintypes.RECT)]

        guiThreadInfo = GUITHREADINFO(cbSize=ctypes.sizeof(GUITHREADINFO))
    
        hwnd = win32gui.GetForegroundWindow()
    
        processID = ctypes.c_ulong()
        threadID = ctypes.windll.user32.GetWindowThreadProcessId(hwnd, ctypes.byref(processID))  # <-- Corrected line
    
        if ctypes.windll.user32.GetGUIThreadInfo(threadID, ctypes.byref(guiThreadInfo)):
            caret_x = guiThreadInfo.rcCaret.left
            caret_y = guiThreadInfo.rcCaret.top
            return caret_x, caret_y
        else:
            return None, None
Reply
#2
See Chapter 26. Universal widget methods Tkinter
specifically the part w.winfo_toplevel()
Valjean likes this post
Reply
#3
I think using a TopLevel window instead of the root window is making things more complicated. I know this is a common pattern, but a lot of common tkinter patterns are silly. Maybe they made sense at one time, but now they just look archaic. Why not put the buttons in the root and deiconify/withdraw the root window to make it pop up and disappear. Something like this:
import threading
import tkinter as tk
from string import ascii_lowercase
from pynput.keyboard import Listener


class Popup(tk.Tk):
    """Popup window for displaying option buttons."""
    def __init__(self, *args, command=None, **kwargs):
        super().__init__(*args, **kwargs)
        self.withdraw()
        self.command = command
        self.title("Popup")
        self.bind("<KeyPress>", self.keypress)
        self.protocol("WM_DELETE_WINDOW", lambda: self.select(None))
        self.shortcuts = {}  # Keyboard shortcuts to make selection.

    def open(self, options):
        """Add option buttons to window.  Show window."""
        # Clear the window
        for widget in self.winfo_children():
            widget.destroy()
        self.shortcuts.clear()

        # Add buttons
        for shortcut, option in zip(ascii_lowercase, options):
            button = tk.Button(
                self,
                text=f"{shortcut}: {option}",
                command=lambda arg=option: self.select(arg)
            )
            button.pack(expand=True, fill=tk.X)
            self.shortcuts[shortcut] = option
        self.deiconify()

    def keypress(self, event):
        """Key was pressed in window.  Check for shortcut."""
        if hasattr(event, "char") and event.char in self.shortcuts:
            self.select(self.shortcuts[event.char])

    def select(self, option):
        """Button press callback."""
        self.withdraw()
        if self.command:
            self.command(option)


class KeyListener:
    """Listens to key presses.  Executes command if key event is mapped."""
    def __init__(self):
        self.listening = True
        self.popup = Popup(command=self.popup_callback)

    def popup_callback(self, option):
        """Popup window callback."""
        print("Popup callback", option)
        self.listening = True
        if option == "Quit":
            self.stop()

    def open_popup(self, options):
        self.listening = False
        self.popup.open(options)

    def on_press(self, key):
        if not (self.listening and hasattr(key, "char")):
            return
        match(key.char):
            case "1":
                self.open_popup(("This", "That", "The other thing", "Quit"))
            case "2":
                self.open_popup(("Here", "There", "Everywhere", "Quit"))
            case "3":
                self.open_popup(("Up", "Down", "All around", "Quit"))
        
    def listen(self):
        """Start the event pynput Listener."""
        self.listener = Listener(on_press=self.on_press)
        self.listener.start()
        self.listener.join()

    def start(self):
        """Start listening to keypress events."""
        # Run pynput listener in different thread.
        # tkinter likes to run in main thread.
        threading.Thread(target=self.listen).start()
        self.popup.mainloop()

    def stop(self):
        """Stop application."""
        self.listener.stop()
        self.popup.quit()


KeyListener().start()
As for drawing the window at the text cursor, are you sure you want that? I think I'd prefer if I could place the popup and have it in the same place each time it appears.
Valjean likes this post
Reply
#4
I looked back at some of your other posts. I think you switched over to using the keyboard module to catch keystrokes. I modified my earlier code to use the same module and to fit better with the code you have posted. While doing this I noticed there is no reason for the popup to capture keystrokes. The KeyListener knows when the popup is visible, and it knows what keys are pressed. It can forward key presses to the popup when visible. That fixes your focus problem.
import tkinter as tk
import string
import keyboard


class Popup(tk.Tk):
    """Popup window for displaying option buttons."""

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.withdraw()
        self.command = None
        self.shurtcuts = {}
        self.title("Popup")
        self.protocol("WM_DELETE_WINDOW", lambda: self.select(None))

    def configure(self, options=None, command=None, **kwargs):
        """Adds options and command to configure."""
        super().configure(**kwargs)
        self.command = command or self.command
        if options:
            for widget in self.winfo_children():
                widget.destroy()
            self.shortcuts = dict(zip(string.ascii_lowercase, options))
            for key, op in self.shortcuts.items():
                button = tk.Button(
                    self, text=f"{key}: {op}", command=lambda x=op: self.select(x)
                )
                button.pack(expand=True, fill=tk.X)

    def open(self):
        """Draw window."""
        self.deiconify()

    def shortcut(self, key):
        """Check if key is shortcut."""
        if key in self.shortcuts:
            self.select(self.shortcuts[key])

    def select(self, option):
        """Button press callback."""
        self.withdraw()
        self.command(option)


class KeyListener:
    """Listens to key presses.  Executes command if key event is mapped."""

    keychars = {"space": " ", "tab": "\t", "enter": "\n"}

    def __init__(self):
        self.word = []
        self.popup = Popup()
        self.commands = {
            "Do this": lambda: print("Doing this"),
            "Do that": lambda: print("Doing that"),
            "Clear": self.word.clear,
            "Quit": self.popup.destroy
        }
        self.popup.configure(
            command=self.popup_callback, options=list(self.commands)
        )
        keyboard.on_press(self.on_press)
        self.popup.mainloop()

    def popup_callback(self, option):
        """Popup window callback."""
        self.commands[option]()

    def on_press(self, key):
        """Called when keyboard key is pressed."""
        key = self.keychars.get(key.name, key.name)
        if self.popup.state() == "normal":
            # Redirect to popup.
            self.popup.shortcut(key)
        elif key == "esc":
            # Open command popup.
            self.popup.open()
        else:
            # Do some input processing thing.
            if key in string.printable:
                self.word.append(key)
                print("".join(self.word))


KeyListener()
Not having to run the keyboard listener in another threads simplifies things. You don't have to create a thread, and you don't have to stop the thread. Other changes are asking the popup window if it is visible instead of using a variable to keep track and using a dictionary to hold the popup commands
Valjean likes this post
Reply
#5
(Sep-30-2023, 01:59 PM)deanhystad Wrote: I looked back at some of your other posts. I think you switched over to using the keyboard module to catch keystrokes. I modified my earlier code to use the same module and to fit better with the code you have posted. While doing this I noticed there is no reason for the popup to capture keystrokes. The KeyListener knows when the popup is visible, and it knows what keys are pressed. It can forward key presses to the popup when visible. That fixes your focus problem.
import tkinter as tk
import string
import keyboard


class Popup(tk.Tk):
    """Popup window for displaying option buttons."""


It worked!!! Thanks so much!!! **dance** 

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.withdraw()
        self.command = None
        self.shurtcuts = {}
        self.title("Popup")
        self.protocol("WM_DELETE_WINDOW", lambda: self.select(None))

    def configure(self, options=None, command=None, **kwargs):
        """Adds options and command to configure."""
        super().configure(**kwargs)
        self.command = command or self.command
        if options:
            for widget in self.winfo_children():
                widget.destroy()
            self.shortcuts = dict(zip(string.ascii_lowercase, options))
            for key, op in self.shortcuts.items():
                button = tk.Button(
                    self, text=f"{key}: {op}", command=lambda x=op: self.select(x)
                )
                button.pack(expand=True, fill=tk.X)

    def open(self):
        """Draw window."""
        self.deiconify()

    def shortcut(self, key):
        """Check if key is shortcut."""
        if key in self.shortcuts:
            self.select(self.shortcuts[key])

    def select(self, option):
        """Button press callback."""
        self.withdraw()
        self.command(option)


class KeyListener:
    """Listens to key presses.  Executes command if key event is mapped."""

    keychars = {"space": " ", "tab": "\t", "enter": "\n"}

    def __init__(self):
        self.word = []
        self.popup = Popup()
        self.commands = {
            "Do this": lambda: print("Doing this"),
            "Do that": lambda: print("Doing that"),
            "Clear": self.word.clear,
            "Quit": self.popup.destroy
        }
        self.popup.configure(
            command=self.popup_callback, options=list(self.commands)
        )
        keyboard.on_press(self.on_press)
        self.popup.mainloop()

    def popup_callback(self, option):
        """Popup window callback."""
        self.commands[option]()

    def on_press(self, key):
        """Called when keyboard key is pressed."""
        key = self.keychars.get(key.name, key.name)
        if self.popup.state() == "normal":
            # Redirect to popup.
            self.popup.shortcut(key)
        elif key == "esc":
            # Open command popup.
            self.popup.open()
        else:
            # Do some input processing thing.
            if key in string.printable:
                self.word.append(key)
                print("".join(self.word))


KeyListener()
Not having to run the keyboard listener in another threads simplifies things. You don't have to create a thread, and you don't have to stop the thread. Other changes are asking the popup window if it is visible instead of using a variable to keep track and using a dictionary to hold the popup commands
Reply
#6
(Oct-01-2023, 02:56 PM)Valjean Wrote:
(Sep-30-2023, 01:59 PM)deanhystad Wrote: I looked back at some of your other posts. I think you switched over to using the keyboard module to catch keystrokes. I modified my earlier code to use the same module and to fit better with the code you have posted. While doing this I noticed there is no reason for the popup to capture keystrokes. The KeyListener knows when the popup is visible, and it knows what keys are pressed. It can forward key presses to the popup when visible. That fixes your focus problem.
import tkinter as tk
import string
import keyboard


class Popup(tk.Tk):
    """Popup window for displaying option buttons."""


It worked!!! Thanks so much!!! **dance** 

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.withdraw()
        self.command = None
        self.shurtcuts = {}
        self.title("Popup")
        self.protocol("WM_DELETE_WINDOW", lambda: self.select(None))

    def configure(self, options=None, command=None, **kwargs):
        """Adds options and command to configure."""
        super().configure(**kwargs)
        self.command = command or self.command
        if options:
            for widget in self.winfo_children():
                widget.destroy()
            self.shortcuts = dict(zip(string.ascii_lowercase, options))
            for key, op in self.shortcuts.items():
                button = tk.Button(
                    self, text=f"{key}: {op}", command=lambda x=op: self.select(x)
                )
                button.pack(expand=True, fill=tk.X)

    def open(self):
        """Draw window."""
        self.deiconify()

    def shortcut(self, key):
        """Check if key is shortcut."""
        if key in self.shortcuts:
            self.select(self.shortcuts[key])

    def select(self, option):
        """Button press callback."""
        self.withdraw()
        self.command(option)


class KeyListener:
    """Listens to key presses.  Executes command if key event is mapped."""

    keychars = {"space": " ", "tab": "\t", "enter": "\n"}

    def __init__(self):
        self.word = []
        self.popup = Popup()
        self.commands = {
            "Do this": lambda: print("Doing this"),
            "Do that": lambda: print("Doing that"),
            "Clear": self.word.clear,
            "Quit": self.popup.destroy
        }
        self.popup.configure(
            command=self.popup_callback, options=list(self.commands)
        )
        keyboard.on_press(self.on_press)
        self.popup.mainloop()

    def popup_callback(self, option):
        """Popup window callback."""
        self.commands[option]()

    def on_press(self, key):
        """Called when keyboard key is pressed."""
        key = self.keychars.get(key.name, key.name)
        if self.popup.state() == "normal":
            # Redirect to popup.
            self.popup.shortcut(key)
        elif key == "esc":
            # Open command popup.
            self.popup.open()
        else:
            # Do some input processing thing.
            if key in string.printable:
                self.word.append(key)
                print("".join(self.word))


KeyListener()
Not having to run the keyboard listener in another threads simplifies things. You don't have to create a thread, and you don't have to stop the thread. Other changes are asking the popup window if it is visible instead of using a variable to keep track and using a dictionary to hold the popup commands


Thanks. I t worked. And yes.. I need the popup window to be place a little below the current caret position...this is my acutal code:


def create_popup(tk_queue, key_listener_instance):
    print("Entered create_popup")  # Debugging
    while True:
        queue_data = tk_queue.get()
        msg, data = queue_data[:2]  # Only take the first two values

        current_window = getActiveWindow()
        current_win_title = current_window.title if current_window else "Unknown Window"
        
        if msg == "create_popup":
            print("About to stop listener and create popup")  # Debugging
            key_listener_instance.stop_listener()

            windows = gw.getWindowsWithTitle(current_win_title)
            if windows:
                main_win = windows[0]
                pyautogui.click(main_win.left + 10, main_win.top + 10)
            else:
                print(f"No window with title '{current_win_title}' found.")

            popup = ctk.CTk()  # Use ctk instead of tk
            popup.title("Select Expansion")

            for i, option in enumerate(key_listener_instance.expansions_list):
                raw_button_text = option['expansion'] if 'expansion' in option else "Undefined"
                button_text = truncate_text(raw_button_text, 60)
                button = ctk.CTkButton(
                    popup,
                    text=button_text,
                    command=partial(key_listener_instance.make_selection, i, popup),
                    font=("Work Sans", 12),
                    anchor="w"
                )
                button.pack(fill=ctk.X, padx=10, pady=5)

            # Update idle tasks to get updated dimensions
            popup.update_idletasks()

            # Get the content width and height
            content_width = 400
            content_height = popup.winfo_height()

            # Set the geometry of the popup to fit the content
            popup.geometry(f"{content_width}x{content_height}")
          
         
            def on_closing():
                try:
                    print("Trying to restart the listener...")  # Debugging
                    key_listener_instance.start_listener()  # Start the listener
                except Exception as e:
                    print(f"Failed to restart listener. Exception: {e}")
                finally:
                    popup.destroy()

            popup.protocol("WM_DELETE_WINDOW", on_closing)
            print("Setting WM_DELETE_WINDOW protocol")  # Debugging

            popup.attributes("-topmost", True)
            popup.focus_force()

            print("Entering Tkinter mainloop")  # Debugging
            popup.mainloop()
Reply
#7
Thanks. I t worked.
Not sure what you are talking about. Looking at your code I don't see anything that looks like my code or uses any of the ideas I mentioned in my posts. For example, my last post showed a way where you don't need to stop the keyboard listener when the popup is visible, you just redirect keyboard input to the popup. You also don't have to force focus on the popup, nor do you have to make it topmost. In my post before that I showed that you can (should) use the same root window for the popup. That post mentioned that tkinter does not like it when you create root windows or call mainloop from outside the main thread. You didn't follow those suggestions either.
Reply


Possibly Related Threads…
Thread Author Replies Views Last Post
  Hide CMD call window tester_V 8 308 Apr-16-2024, 08:26 PM
Last Post: deanhystad
  Open files in an existing window instead of new Kostov 2 320 Apr-13-2024, 07:22 AM
Last Post: Kostov
  How to Minimize ADB window OomKoos 0 395 Dec-29-2023, 12:41 PM
Last Post: OomKoos
  add entries and labels to the window tkinter jacksfrustration 3 634 Oct-10-2023, 06:41 PM
Last Post: buran
  Can't stop keyboard listener to grab chars typed inside CTk window Valjean 9 1,370 Sep-25-2023, 08:07 PM
Last Post: deanhystad
  read active document name - other than from the window title ineuw 0 541 Sep-11-2023, 09:06 AM
Last Post: ineuw
  how to open a popup window in tkinter with entry,label and button lunacy90 1 896 Sep-01-2023, 12:07 AM
Last Post: lunacy90
Bug tkinter.TclError: bad window path name "!button" V1ber 2 803 Aug-14-2023, 02:46 PM
Last Post: V1ber
  Howto do motion event on solely window and not the widgets on it? janeik 3 860 Jul-11-2023, 12:10 AM
Last Post: deanhystad
  What is all the info in the info window in Idle? Pedroski55 3 708 Jul-08-2023, 11:26 AM
Last Post: DeaD_EyE

Forum Jump:

User Panel Messages

Announcements
Announcement #1 8/1/2020
Announcement #2 8/2/2020
Announcement #3 8/6/2020