Opening new window while out of focus using "keyboard" - python

I am trying to track my keypresses, using the module "keyboard" while a PySide2 Widget is not in focus, which works fine. However when I try to create a new Widget using a "keyboard" shortcut the program crashes. Opening a window on a button press works fine. I can also call non UI functions using "keyboard" eg. the print function without any problem.
Do you know a way to fix this and open a new window using "keyboard" or any other method, while a PySide2 window is not in focus. In this example I want to open a new window on "CTRL+D". The Problem exists both for PySide2 and PyQt5.
This is my shortened code:
import sys
import json
import os
import keyboard
from PySide2.QtWidgets import QApplication, QWidget, QGridLayout, QKeySequenceEdit, QLabel, QPushButton, QShortcut
from PySide2.QtCore import Qt, QObject, Signal, Slot # Qt.Key_W beispielsweise
#from PyQt5.QtWidgets import QApplication, QWidget, QGridLayout, QKeySequenceEdit, QLabel, QPushButton, QShortcut
#from PyQt5.QtCore import Qt, QObject, pyqtSignal as Signal, pyqtSlot as Slot # Qt.Key_W beispielsweise
class ConfigWindow(QWidget):
def __init__(self):
super().__init__()
self.initUi()
self.init_shortcuts()
self.show()
def initUi(self):
self.setGeometry(300,300, 400, 250)
self.setWindowTitle("Settings")
grid = QGridLayout()
self.setLayout(grid)
self.keyseq = QKeySequenceEdit("CTRL+D")
grid.addWidget(self.keyseq, 0, 0)
s_button = QPushButton("Safe")
grid.addWidget(s_button, 1, 0)
cl_button = QPushButton("Close")
grid.addWidget(cl_button, 1, 1)
cl_button.clicked.connect(self.close)
open_button = QPushButton("openw")
grid.addWidget(open_button, 2, 0)
open_button.clicked.connect(self.call_item_parser)
def keyPressEvent(self, event): #event:PySide2.QtGui.QKeyEvent
if event.key() == Qt.Key_Escape:
self.close()
# shortcuts are listened to, while program is running
def init_shortcuts(self):
str_value = self.keyseq.keySequence().toString()
print("Binding _price_keyseq to {}".format(str_value))
keyboard.add_hotkey(str_value, self.call_item_parser)
# keyboard.add_hotkey(str_value, print, args=("this works")) # this would work
def call_item_parser(self):
self.h_w = ParseWindow()
self.h_w.setWindowTitle("New Window")
self.h_w.setGeometry(100, 100, 100, 100)
self.h_w.show()
class ParseWindow(QWidget):
def __init__(self):
super().__init__()
app = QApplication(sys.argv)
w = ConfigWindow()
sys.exit(app.exec_())

The problem is caused because the callback registered in keyboard is executed in a secondary thread as can be verified by modifying the following part of the code and printing threading.current_thread(). In Qt it is forbidden to create any widget in another thread since they are not thread-safe.
def call_item_parser(self):
print(threading.current_thread())
self.h_w = ParseWindow()
self.h_w.setWindowTitle("New Window")
self.h_w.setGeometry(100, 100, 100, 100)
self.h_w.show()
print(threading.current_thread())
app = QApplication(sys.argv)
w = ConfigWindow()
sys.exit(app.exec_())
Output:
<_MainThread(MainThread, started 140144979916608)>
Binding _price_keyseq to ctrl+a
<Thread(Thread-10, started daemon 140144220817152)>
One possible solution is to use a signal to send the information to the main thread, and invoke the callback in the main thread.
import sys
from functools import partial
import platform
import threading
import keyboard
from PySide2.QtCore import Qt, QObject, Signal, Slot
from PySide2.QtGui import QKeySequence
from PySide2.QtWidgets import (
QApplication,
QWidget,
QGridLayout,
QKeySequenceEdit,
QPushButton,
)
class KeyBoardManager(QObject):
activated = Signal(str)
def __init__(self, parent=None):
super().__init__(parent)
self._callbacks = dict()
self.activated.connect(self._handle_activated)
#property
def callbacks(self):
return self._callbacks
def register(self, shortcut, callback, *, args=(), kwargs=None):
self.callbacks[shortcut] = (callback, args, kwargs or {})
keyboard.add_hotkey(shortcut, partial(self.activated.emit, shortcut))
#Slot(str)
def _handle_activated(self, shortcut):
values = self.callbacks.get(shortcut)
if values is not None:
callback, args, kwargs = self._callbacks[shortcut]
callback(*args, **kwargs)
class ConfigWindow(QWidget):
def __init__(self):
super().__init__()
self.initUi()
self.init_shortcuts()
self.show()
def initUi(self):
self.setGeometry(300, 300, 400, 250)
self.setWindowTitle("Settings")
grid = QGridLayout(self)
self.keyseq = QKeySequenceEdit("CTRL+A")
grid.addWidget(self.keyseq, 0, 0)
s_button = QPushButton("Safe")
grid.addWidget(s_button, 1, 0)
cl_button = QPushButton("Close")
grid.addWidget(cl_button, 1, 1)
cl_button.clicked.connect(self.close)
open_button = QPushButton("openw")
grid.addWidget(open_button, 2, 0)
open_button.clicked.connect(self.call_item_parser)
def keyPressEvent(self, event): # event:PySide2.QtGui.QKeyEvent
if event.key() == Qt.Key_Escape:
self.close()
# shortcuts are listened to, while program is running
def init_shortcuts(self):
self.keyboard_manager = KeyBoardManager()
str_value = self.keyseq.keySequence().toString()
if platform.system() == "Linux":
str_value = str_value.lower()
print("Binding _price_keyseq to {}".format(str_value))
self.keyboard_manager.register(str_value, self.call_item_parser)
def call_item_parser(self):
print(threading.current_thread())
self.h_w = ParseWindow()
self.h_w.setWindowTitle("New Window")
self.h_w.setGeometry(100, 100, 100, 100)
self.h_w.show()
class ParseWindow(QWidget):
pass
def main():
print(threading.current_thread())
app = QApplication(sys.argv)
w = ConfigWindow()
sys.exit(app.exec_())
if __name__ == "__main__":
main()
Output:
<_MainThread(MainThread, started 140037641176896)>
Binding _price_keyseq to ctrl+a
<_MainThread(MainThread, started 140037641176896)>

Related

PyQt5 - Detecting When Other Window Closes

I’m using PyQt5 and I need to have my main window detect when a different window closes. I read here Emit a signal from another class to main class that creating a signal class to serve as an intermediary should work. However I haven’t gotten my example to work.
In my example, clicking the button opens a QWidget window. When the QWidget is closed, the main window is supposed to change from a blue background to a red background. However, the main window remains blue using the script below.
What am I doing wrong?
from PyQt5.QtWidgets import QPushButton
from PyQt5.QtCore import QObject, pyqtSignal
import os, sys
class MySignal(QObject):
signal = pyqtSignal()
class MyMainWindow(QMainWindow):
def __init__(self):
super().__init__()
#Make mainwindow
self.setGeometry(100,100,300,200)
self.setStyleSheet('background-color: blue')
# Make widget and button objects and set connection
self.widget = MyWidget()
self.btn = QPushButton(self)
self.btn.setText('Click')
self.btn.move(175, 150)
self.btn.setStyleSheet('background-color: white')
self.btn.clicked.connect(self.widget.showWidget)
# Make signal object and set connection
self.mySignal = MySignal()
self.mySignal.signal.connect(self.changeToRed)
# Let's start
self.show()
def changeToRed(self):
self.setStyleSheet('background-color: red')
def closeEvent(self, event):
os._exit(0)
class MyWidget(QWidget):
def __init__(self):
super().__init__()
self.setGeometry(500, 100, 200, 200)
self.sig = MySignal()
def showWidget(self):
self.show()
def closeEvent(self, event):
self.sig.signal.emit()
self.close()
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = MyMainWindow()
app.exec()```
The reason your code fails is that the connected signal object in MyMainWindow is not the same as the one you create in MyWidget, so the signal is never emitted. Here is a modified solution using signals in the normal way:
from PyQt5.QtWidgets import QPushButton, QMainWindow, QWidget, QApplication
from PyQt5.QtCore import QObject, pyqtSignal
import os, sys
class MyMainWindow(QMainWindow):
def __init__(self):
super().__init__()
#Make mainwindow
self.setGeometry(100,100,300,200)
self.setStyleSheet('background-color: blue')
# Make widget and button objects and set connection
self.widget = MyWidget()
self.btn = QPushButton(self)
self.btn.setText('Click')
self.btn.move(175, 150)
self.btn.setStyleSheet('background-color: white')
self.btn.clicked.connect(self.widget.showWidget)
self.widget.sig.connect(self.changeToRed)
# Let's start
self.show()
def changeToRed(self):
self.setStyleSheet('background-color: red')
class MyWidget(QWidget):
sig = pyqtSignal()
def __init__(self):
super().__init__()
self.setGeometry(500, 100, 200, 200)
def showWidget(self):
self.show()
def closeEvent(self, event):
self.sig.emit()
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = MyMainWindow()
app.exec()
All you need is to add the connection between the close event and the function that turn your main screen red:
self.widget.closeEvent = self.changeToRed
this line should be in your main Window class
change your changeToRed function so it will except the event too:
def changeToRed(self, e):

Disable elements when running QThread with PySide/PyQt

This is what I get when I click on the OK button.
But I would like to disable the two buttons while the run_action() is still running and finally reset the bar to 0.
This is my current code:
import sys
import time
from PySide6.QtCore import QThread, Signal
from PySide6.QtWidgets import (
QApplication,
QHBoxLayout,
QProgressBar,
QPushButton,
QWidget,
)
class External(QThread):
progressChanged = Signal(int)
def run(self):
progress = 0
while progress < 100:
progress += 10
time.sleep(1)
self.progressChanged.emit(progress)
class Window(QWidget):
"""The main application Window."""
def __init__(self):
super().__init__()
self.setWindowTitle("Example")
self.layout = QHBoxLayout()
self.layout.setContentsMargins(6, 6, 6, 6)
self.bar = QProgressBar()
self.bar.setTextVisible(False)
self.bar.setValue(0)
self.layout.addWidget(self.bar)
self.cancel_btn = QPushButton("Cancel")
self.cancel_btn.clicked.connect(self.close)
self.layout.addWidget(self.cancel_btn)
self.ok_btn = QPushButton("OK")
self.ok_btn.clicked.connect(self.run_action)
self.layout.addWidget(self.ok_btn)
self.setLayout(self.layout)
def run_action(self):
self.ok_btn.setEnabled(False)
self.cancel_btn.setEnabled(False)
self.calc = External()
self.calc.progressChanged.connect(self.onProgressChanged)
self.calc.start()
self.cancel_btn.setEnabled(True)
self.ok_btn.setEnabled(True)
self.bar.setValue(0)
def onProgressChanged(self, value):
self.bar.setValue(value)
if __name__ == "__main__":
app = QApplication(sys.argv)
window = Window()
window.show()
sys.exit(app.exec_())
void QThread::finished()
This signal is emitted from the associated thread right before it finishes executing.
When this signal is emitted, the event loop has already stopped running.
No more events will be processed in the thread, except for deferred deletion events.
This signal can be connected to QObject::deleteLater(), to free objects in that thread.
import sys
#import time
#from PySide6.QtCore import QThread, Signal
from PyQt5.QtCore import QThread, pyqtSignal
#from PySide6.QtWidgets import (
from PyQt5.QtWidgets import (
QApplication,
QHBoxLayout,
QProgressBar,
QPushButton,
QWidget,
)
class External(QThread):
# progressChanged = Signal(int)
progressChanged = pyqtSignal(int)
def run(self):
progress = 0
while progress <= 100:
self.progressChanged.emit(progress)
self.msleep(500)
progress += 10
class Window(QWidget):
"""The main application Window."""
def __init__(self):
super().__init__()
self.setWindowTitle("Example")
self.layout = QHBoxLayout()
self.layout.setContentsMargins(6, 6, 6, 6)
self.bar = QProgressBar()
self.bar.setTextVisible(False)
self.bar.setValue(0)
self.layout.addWidget(self.bar)
self.cancel_btn = QPushButton("Cancel")
self.cancel_btn.clicked.connect(self.close)
self.layout.addWidget(self.cancel_btn)
self.ok_btn = QPushButton("OK")
self.ok_btn.clicked.connect(self.run_action)
self.layout.addWidget(self.ok_btn)
self.setLayout(self.layout)
def run_action(self):
self.ok_btn.setEnabled(False)
self.cancel_btn.setEnabled(False)
self.calc = External()
self.calc.progressChanged.connect(self.onProgressChanged)
self.calc.finished.connect(self.onFinished) # +++
self.calc.start()
# self.cancel_btn.setEnabled(True)
# self.ok_btn.setEnabled(True)
# self.bar.setValue(0)
def onProgressChanged(self, value):
self.bar.setValue(value)
def onFinished(self): # +++
self.cancel_btn.setEnabled(True)
self.ok_btn.setEnabled(True)
self.bar.setValue(0)
if __name__ == "__main__":
app = QApplication(sys.argv)
window = Window()
window.show()
sys.exit(app.exec_())

What is the signal for user clicks the down arrow on the QComboBox?

I need to execute a method whenever the user clicks the down arrow on the combo box. I've tried the signals listed in the documentations but none of the worked.
from PyQt5.QtWidgets import *
import sys
class Window(QWidget):
def __init__(self):
super().__init__()
self.combo = QComboBox(self)
self.combo.signal.connect(self.mymethod)
self.show()
def mymethod(self):
print('hello world')
app = QApplication(sys.argv)
win = Window()
sys.exit(app.exec_())
There is no signal that is emitted when the down arrow is pressed but you can create override the mousePressEvent method and verify that this element was pressed:
import sys
from PyQt5.QtCore import pyqtSignal, Qt
from PyQt5.QtWidgets import (
QApplication,
QComboBox,
QStyle,
QStyleOptionComboBox,
QVBoxLayout,
QWidget,
)
class ComboBox(QComboBox):
arrowClicked = pyqtSignal()
def mousePressEvent(self, event):
super().mousePressEvent(event)
opt = QStyleOptionComboBox()
self.initStyleOption(opt)
sc = self.style().hitTestComplexControl(
QStyle.CC_ComboBox, opt, event.pos(), self
)
if sc == QStyle.SC_ComboBoxArrow:
self.arrowClicked.emit()
class Window(QWidget):
def __init__(self):
super().__init__()
self.combo = ComboBox()
self.combo.arrowClicked.connect(self.mymethod)
lay = QVBoxLayout(self)
lay.addWidget(self.combo)
lay.setAlignment(Qt.AlignTop)
def mymethod(self):
print("hello world")
if __name__ == "__main__":
app = QApplication(sys.argv)
win = Window()
win.show()
sys.exit(app.exec_())

How to embed a cmd to pyqt5 application

I tried codes below, but it seemed not work as normal. I cannot use embedded cmd whatever I did. It looks like decorations. I mean I just want to use the cmd like the ordinary one. Here, I paste the code, and any advice would be greatly appreciated.
I develop this app on Python 3.7.3 conda environment, Window10.
import sys
import subprocess
import time
import win32gui
from PyQt5.QtCore import QProcess, Qt
from PyQt5.QtGui import QWindow, QIcon, QFont
from PyQt5.QtWidgets import QMainWindow
from PyQt5.QtWidgets import QMdiArea, QSplitter, QTextBrowser
from PyQt5.QtWidgets import QWidget, QApplication, QVBoxLayout
from win32com import client
from win32gui import GetWindowText, EnumWindows,SetForegroundWindow
class Example(QMainWindow):
def __init__(self):
super().__init__()
self.p = QProcess()
self.layout = QVBoxLayout()
self.mdi = QMdiArea()
self.mainSplitter = QSplitter(Qt.Vertical)
self.setCentralWidget(self.mainSplitter)
self.mainSplitter.addWidget(QTextBrowser())
self.initUI()
def initUI(self):
self.runExe()
EnumWindows(self.set_cmd_to_foreground, None)
hwnd1 = win32gui.GetForegroundWindow()
#hwnd1 = win32gui.FindWindow(None, "C:\\Windows\\system32\\calc.exe")
print(hwnd1)
window = QWindow.fromWinId(hwnd1)
container_widge = self.createWindowContainer(window, self)
container_widge.setFocusPolicy(Qt.TabFocus)
container_widge.setFocus()
container_widge.setWindowTitle("ain")
container_widge.setFont(QFont("Times New Roman"))
container_widge.setGeometry(500, 500, 450, 400)
#container_widge.setFocusPolicy()
container_widge.activateWindow()
container_widge.acceptDrops()
container_widge.grabMouse()
container_widge.setMouseTracking(True)
self.mainSplitter.addWidget(container_widge)
self.showMaximized()
#self.setGeometry(200, 200, 700, 700)
#self.show()
def runExe(self):
shell.run("cmd.exe")
time.sleep(1)
def set_cmd_to_foreground(self, hwnd, extra):
"""sets first command prompt to forgeround"""
if "cmd.exe" in GetWindowText(hwnd):
print(hwnd)
SetForegroundWindow(hwnd)
return
def run_script(self, shell, scripts):
"""runs the py script"""
shell.SendKeys(scripts+"{ENTER}")
if __name__ == '__main__':
shell = client.Dispatch("WScript.Shell")
app = QApplication(sys.argv)
ex = Example()
#ex.run_script(shell, "python -m pip list")
#ex.show()
sys.exit(app.exec_())
you can try this I got it from this blog:
import sys
from PyQt5.QtCore import QProcess, QTextStream
from PyQt5.QtWidgets import QApplication, QMainWindow, QTextEdit,
QLineEdit, QVBoxLayout, QHBoxLayout, QWidget, QPushButton
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.process = QProcess(self)
self.output = QTextEdit(self)
self.input = QLineEdit(self)
self.run_command_button = QPushButton("Run Command", self)
layout = QVBoxLayout()
input_layout = QHBoxLayout()
input_layout.addWidget(self.input)
input_layout.addWidget(self.run_command_button)
layout.addLayout(input_layout)
layout.addWidget(self.output)
central_widget = QWidget(self)
central_widget.setLayout(layout)
self.setCentralWidget(central_widget)
self.process.readyReadStandardOutput.connect(self.read_output)
self.run_command_button.clicked.connect(self.run_command)
self.process.start("cmd.exe")
def read_output(self):
stream = QTextStream(self.process)
self.output.append(stream.readAll())
def run_command(self):
command = self.input.text() + "\n"
self.process.write(command.encode())
if __name__ == '__main__':
app = QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec_())

QWebEngineView history canGoForward/canGoBack works only after three items

I have a simple application where the back and forward buttons are enabled/disabled based on the history of the visited web pages. For this, I have found the canGoForward and canGoBack functions
of the QWebEngineHistory. However, the functions return True only after there are at least three items in the history. Normally, browsers work right after visiting the second different page.
Is this supposed to be working like that? I there any way to change it to the 2 web pages? I have looked at the QWebEngineSettings, but there is nothing related to this.
Here is a working example:
#!/usr/bin/python
import sys
from PyQt5.QtCore import QUrl
from PyQt5.QtGui import QIcon
from PyQt5.QtWidgets import (QApplication, QLineEdit, QMainWindow,
QPushButton, QToolBar)
from PyQt5.QtWebEngineWidgets import QWebEnginePage, QWebEngineView
class Example(QMainWindow):
def __init__(self):
super(Example, self).__init__()
self.initUI()
def initUI(self):
self.toolBar = QToolBar(self)
self.addToolBar(self.toolBar)
self.backBtn = QPushButton(self)
self.backBtn.setEnabled(False)
self.backBtn.setIcon(QIcon(':/qt-project.org/styles/commonstyle/images/left-32.png'))
# self.backBtn.setIcon(QIcon('stock_left.png'))
self.backBtn.clicked.connect(self.back)
self.toolBar.addWidget(self.backBtn)
self.forBtn = QPushButton(self)
self.forBtn.setEnabled(False)
# self.forBtn.setIcon(QIcon('stock_right.png'))
self.forBtn.setIcon(QIcon(':/qt-project.org/styles/commonstyle/images/right-32.png'))
self.forBtn.clicked.connect(self.forward)
self.toolBar.addWidget(self.forBtn)
self.address = QLineEdit(self)
self.address.returnPressed.connect(self.load)
self.toolBar.addWidget(self.address)
self.webEngineView = QWebEngineView(self)
self.setCentralWidget(self.webEngineView)
self.webEngineView.page().urlChanged.connect(self.onLoadFinished)
print(self.webEngineView.history().backItem().url())
print(self.webEngineView.history().forwardItem().url())
self.setGeometry(300, 300, 500, 400)
self.setWindowTitle('QWebEnginePage')
self.show()
# self.webEngineView.page().urlChanged.connect(self.urlChanged)
def onLoadFinished(self):
print(dir(self.webEngineView.history()))
print('load finished')
# print(self.webEngineView.history().backItem().url())
# print(self.webEngineView.history().forwardItem().url())
# print(self.webEngineView.history().backItem())
# print(self.webEngineView.history().forwardItem())
# print(self.webEngineView.history().count())
# print(self.webEngineView.history().items())
# print(self.webEngineView.history().canGoForward())
# print(self.webEngineView.history().canGoBack())
if self.webEngineView.history().canGoBack():
self.backBtn.setEnabled(True)
else:
self.backBtn.setEnabled(False)
if self.webEngineView.history().canGoForward():
self.forBtn.setEnabled(True)
else:
self.forBtn.setEnabled(False)
def load(self):
url = QUrl.fromUserInput(self.address.text())
if url.isValid():
self.webEngineView.load(url)
def back(self):
self.webEngineView.page().triggerAction(QWebEnginePage.Back)
def forward(self):
self.webEngineView.page().triggerAction(QWebEnginePage.Forward)
def urlChanged(self, url):
self.address.setText(url.toString())
def main():
app = QApplication(sys.argv)
ex = Example()
sys.exit(app.exec_())
if __name__ == '__main__':
main()

Categories

Resources