3

I want to create my own virtual keyboard. However, it always happens that the focus is taken from another window as soon as a button is pressed or the window is opened. How can I work around this problem?

My goal

Below I have created a small example with which you can reproduce the problem.

import sys
from PyQt5.QtWidgets import QApplication, QWidget, QPushButton
from PyQt5.QtGui import QIcon
from PyQt5.QtCore import pyqtSlot


class App(QWidget):

    def __init__(self):
        super().__init__()
        self.title = 'Keyboard'
        self.left = 10
        self.top = 10
        self.width = 320
        self.height = 200
        self.initUI()

    def initUI(self):
        self.setWindowTitle(self.title)
        self.setGeometry(self.left, self.top, self.width, self.height)

        button = QPushButton('1', self)
        button.move(100, 70)
        button.clicked.connect(self.on_click)

        self.show()

    @pyqtSlot()
    def on_click(self):
        print('PyQt5 button click')


if __name__ == '__main__':
    app = QApplication(sys.argv)
    ex = App()
    sys.exit(app.exec_())

So far I've tried changing the focus settings of the button button.setFocusPolicy(Qt.NoFocus). As well as to set the window flags or the attribute:

self.setWindowFlags(Qt.WindowDoesNotAcceptFocus|Qt.WindowStaysOnTopHint).
self.setAttribute(Qt.WA_ShowWithoutActivating)

I may also need the User32 dll to solve the problem (https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getwindowlonga). How do I get handle to the window (=hwnd)?

What is the best and easiest way to reach my goal? Maybe this is already integrated in PYQT5, but I couldn't find anything about it.

For tkinter I found the following. And I'm trying the exact same thing with Pyqt5!

import tkinter as tk
from ctypes import windll, wintypes

GWL_STYLE = -16
GWL_EXSTYLE = -20
WS_CHILD = 0x40000000
WS_EX_APPWINDOW = 0x00040000
WS_EX_TOOLWINDOW = 0x00000080
WS_EX_NOACTIVATE = 0x08000000

SWP_FRAMECHANGED = 0x0020
SWP_NOACTIVATE = 0x0010
SWP_NOMOVE = 0x0002
SWP_NOSIZE = 0x0001

# write short names for functions and specify argument and return types
GetWindowLong = windll.user32.GetWindowLongW
GetWindowLong.restype = wintypes.ULONG
GetWindowLong.argtpes = (wintypes.HWND, wintypes.INT)

SetWindowLong = windll.user32.SetWindowLongW
SetWindowLong.restype = wintypes.ULONG
SetWindowLong.argtpes = (wintypes.HWND, wintypes.INT, wintypes.ULONG)

SetWindowPos = windll.user32.SetWindowPos

def find_root_window(win): # takes tkinter window ref
    w_id = win.winfo_id() # gets handle
    style = GetWindowLong(w_id, GWL_STYLE) # get existing style
    newstyle = style & ~WS_CHILD # remove child style
    res = SetWindowLong(w_id, GWL_STYLE, newstyle) # set new style
    res = SetWindowPos(w_id, 0, 0,0,0,0, SWP_FRAMECHANGED | SWP_NOACTIVATE | SWP_NOMOVE | SWP_NOSIZE)
    hwnd = int(root.wm_frame(), 16) # find handle of parent
    res = SetWindowLong(w_id, GWL_STYLE, style) # set back to old style
    res = SetWindowPos(w_id, 0, 0,0,0,0, SWP_FRAMECHANGED | SWP_NOACTIVATE | SWP_NOMOVE | SWP_NOSIZE)
    return hwnd # return parents handle

def set_no_focus(hwnd):
    style = GetWindowLong(hwnd, GWL_EXSTYLE) # get existing style
    style = style & ~WS_EX_TOOLWINDOW # remove toolwindow style
    style = style | WS_EX_NOACTIVATE | WS_EX_APPWINDOW
    res = SetWindowLong(hwnd, GWL_EXSTYLE, style)
    res = SetWindowPos(hwnd, 0, 0,0,0,0, SWP_FRAMECHANGED | SWP_NOACTIVATE | SWP_NOMOVE | SWP_NOSIZE)

def push_me():
    print('you pushed me!')

def focus_me(event):
    root.focus_force()

root = tk.Tk()
root.wm_attributes("-topmost", 1)
tk.Button(root, text="Push me", command=push_me).pack()
e = tk.Entry(root)
e.pack()
e.bind('<Button-1>', focus_me) # if we have a widget that must have focus it needs to be bound
root.update() # for some reason, window style is messed with after window creation, update to get past this
hwnd = find_root_window(root)
if hwnd:
    set_no_focus(hwnd)
root.mainloop()
Robin Fauser
  • 111
  • 1
  • 13
  • Try: `self.setWindowFlags(Qt.Tool | Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint)` (i.e. with `WA_ShowWithoutActivating`). – ekhumoro Mar 13 '22 at 17:52
  • Unfortunately does not work. The cursor disappears from its position. – Robin Fauser Mar 13 '22 at 17:54
  • On Linux, `Qt.WindowDoesNotAcceptFocus` is all that's needed. It seems each platform may require it's own magic combination of flags, so further experimentation may be needed. Some of the answers/comments [here](https://stackoverflow.com/q/966688/984421) suggest it did at least work on Windows with Qt4. – ekhumoro Mar 13 '22 at 18:15
  • It doesn't work on windows. Interestingly the first time the window is opened, it doesn't take the focus. Only when the window is reopened or resized it takes the focus from other Windows-applications. – Robin Fauser Mar 13 '22 at 21:19

1 Answers1

3

I solved the problem in Windows by disabling the window via User32.dll. To do this, I simply pass the 'HWND' handler to the 'SetWindowLongW' function and then set a new extended window style. All information can be seen on the following link:

https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-setwindowlongw

Update: Below is the working example:

import sys
from PyQt5.QtWidgets import QApplication, QWidget, QPushButton
from PyQt5.QtGui import QIcon
from PyQt5.QtCore import pyqtSlot,Qt
import ctypes
User32 = ctypes.WinDLL('User32.dll')

class App(QWidget):

    def __init__(self):
        super().__init__()
        self.title = 'Keyboard'
        self.left = 10
        self.top = 10
        self.width = 320
        self.height = 200
        self.initUI()
        self.setWindowFlags(Qt.WindowDoesNotAcceptFocus | Qt.WindowStaysOnTopHint)
        self.setAttribute(Qt.WA_ShowWithoutActivating)
        print(int(self.winId()))
        self.update()
        self.show()
        print(User32.SetWindowLongW(int(self.winId()), -20, 134217728))

    def initUI(self):
        self.setWindowTitle(self.title)
        self.setGeometry(self.left, self.top, self.width, self.height)

        button = QPushButton('1', self)
        button.move(100, 70)
        button.clicked.connect(self.on_click)

        self.show()

    @pyqtSlot()
    def on_click(self):
        print('PyQt5 button click')


if __name__ == '__main__':
    app = QApplication(sys.argv)
    ex = App()
    sys.exit(app.exec_())
Robin Fauser
  • 111
  • 1
  • 13