How to let sibling windows accept event? - python-3.x

I write a simple window, when cursor in QLineEdit and press Enter Key, I want the QGraphicsRectItem, QGraphicsScene, QGraphicsView and QWidget also accept QKeyEvent or MyEvent(customize event).I have no idea to do it,Could someone have good method to do this?
Code Sample
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
from PyQt5.QtGui import *
class MyEvent(QEvent):
Type = QEvent.registerEventType()
def __init__(self):
super().__init__(MyEvent.Type)
self._data = "test"
class Item(QGraphicsRectItem):
def __init__(self):
super().__init__()
self.setRect(0 ,0, 100, 100)
self.setBrush(Qt.red)
self.setFlags(QGraphicsItem.ItemIsFocusable)
def keyPressEvent(self, event: QKeyEvent) -> None:
print("Item KeyPress", event.key())
return super().keyPressEvent(event)
class Scene(QGraphicsScene):
def keyPressEvent(self, event: QKeyEvent) -> None:
print("Scene KeyPress", event.key())
return super().keyPressEvent(event)
class View(QGraphicsView):
def keyPressEvent(self, event: QKeyEvent) -> None:
print("View KeyPress", "do something work here", event.key())
return super().keyPressEvent(event)
class MainWindow(QWidget):
def __init__(self):
super().__init__()
lay = QVBoxLayout()
view = View()
scene = Scene()
scene.addItem(Item())
view.setScene(scene)
lay.addWidget(view)
lay.addWidget(QLineEdit("Cursor In here, post Enter Event to QGraphicsView"))
self.setLayout(lay)
self.show()
self.view = view
def keyPressEvent(self, e: QKeyEvent) -> None:
print("QWidget KeyPress", e.key())
# myEvent = MyEvent()
# QApplication.postEvent(myEvent)
return super().keyPressEvent(e)
app = QApplication([])
m = MainWindow()
app.exec()
How let others item also get the event?

Related

How to get dockwiget splitter-handler?

I want to get handle in QDockWidget(red line show in picture),So that when handle was click, move, or mouse event and so on.I want to catch it and then do something else,But I can't find how to get it,Can someone know how to get it?
Code Sample
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
from PyQt5.QtGui import *
class View(QMainWindow):
def __init__(self):
super().__init__()
self.addDockWidget(Qt.LeftDockWidgetArea, QDockWidget("abc", self))
self.setCentralWidget(QPushButton("ABC"))
app = QApplication([])
v = View()
v.show()
app.exec()
Below Is New Update
The view in the QDockWidget,When mouse drag handle,I want to the view always show global mini map of the scene,but use re-szie event will be very slow.So I want to get mouse-release event,then use fitInView redraw mini map.
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
from PyQt5.QtGui import *
import random
import math
r = lambda : random.randint(0, 255)
r255 = lambda : (r(), r(), r())
class Scene(QGraphicsScene):
def __init__(self):
super().__init__()
for i in range(1000*300):
item = QGraphicsEllipseItem()
item.setRect(0, 0, r(), r())
item.setBrush(QColor(*r255()))
item.setPos(r()*100, r()*100)
self.addItem(item)
class MainView(QGraphicsView):
def wheelEvent(self, event: QWheelEvent) -> None:
factor = math.pow(2.7, event.angleDelta().y()/360)
self.scale(factor, factor)
super().wheelEvent(event)
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.scene = Scene()
self.main = MainView()
self.main.setDragMode(QGraphicsView.ScrollHandDrag)
self.side = QGraphicsView()
self.side.setBackgroundBrush(Qt.black)
self.main.setScene(self.scene)
self.side.setScene(self.scene)
self.setCentralWidget(self.main)
dock = QDockWidget()
dock.setWidget(self.side)
self.addDockWidget(Qt.LeftDockWidgetArea, dock)
#
dock.installEventFilter(self)
def eventFilter(self, w: 'QObject', e: 'QEvent') -> bool:
if isinstance(e, QResizeEvent):
self.side.fitInView(self.scene.itemsBoundingRect(), Qt.KeepAspectRatio)
return super().eventFilter(w, e)
app = QApplication([])
v = MainWindow()
v.show()
app.exec()

Connecting external threaded log to PyQt5 QPlainTextEdit

I'm new to PyQt and handlers in general, I tried to read from every repo I found but I can't figure out how to fix my issue, as you can see in my code, I'm executing logging thread in the background and I'm trying to show the logs in my QPlainTextEdit console - for some reason I can see the logs in my terminal and the text_box doesn't get the logs at all,
I will appreciate a lot your smart help.
import pandas as pd
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *
import time
import os
import threading
import json
class ConsolePanelHandler(logging.Handler):
def __init__(self, stream):
# super().__init__()
logging.Handler.__init__(self)
# logging.StreamHandler.__init__(self, stream)
self.stream = stream
def handle(self, record):
rv = self.filter(record)
if rv:
self.acquire()
try:
self.emit(record)
finally:
self.release()
return rv
def emit(self, record):
try:
stream = self.stream
stream(self.format(record))
except RecursionError:
raise
except Exception:
self.handleError(self.format(record))
def thread():
for index in range(20):
logging.warning('scheiBe '+str(index))
time.sleep(1)
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
layout = QVBoxLayout()
self.continue_ = QPushButton("Continue")
self.continue_.setStyleSheet("background-color: green")
self.continue_.setFont(QFont('SansSerif', 10))
self.continue_.setFixedSize(QSize(300, 22))
self.pause = QPushButton("Pause")
self.pause.setStyleSheet("background-color: orange")
self.pause.setFont(QFont('SansSerif', 10))
self.pause.setFixedSize(QSize(300, 22))
self.stop = QPushButton("Stop")
self.stop.setStyleSheet("background-color: #FD4B4B")
self.stop.setFont(QFont('SansSerif', 10))
self.stop.setFixedSize(QSize(300, 22))
self.text_box = QPlainTextEdit()
self.text_box.setPlaceholderText("Bugs will be printed here")
self.text_box.setReadOnly(True)
logging.getLogger().addHandler(self.text_box)
logging.getLogger().setLevel(logging.DEBUG)
ConsolePanelHandler(self.appendDebug , logging.DEBUG)
self.text_box.moveCursor(QTextCursor.End)
layout.addWidget(self.continue_)
layout.addWidget(self.pause)
layout.addWidget(self.stop)
layout.addWidget(self.text_box)
self.w = QWidget()
self.w.setLayout(layout)
self.setCentralWidget(self.w)
thread1 = threading.Thread(target=thread, args=(), daemon=True)
thread1.start()
self.show()
def closeEvent(self, event):
close = QMessageBox()
close.setText("Are you sure want to stop and exit?")
close.setStandardButtons(QMessageBox.Yes | QMessageBox.Cancel)
close = close.exec()
if close == QMessageBox.Yes:
sys.exit()
else:
event.ignore()
def appendDebug(self, string):
self.text_box.appendPlainText(string +'\n')
if __name__ == "__main__":
app = QApplication([])
window = MainWindow()
sys.exit(app.exec())
First, you should not pass the function that updates the text_area instead you should create a pyqtSignal which updates the text_area and pass it to ConsolePanelHandler.
Add ConsolePanelHandler as handler not text_area to logging.getLogger().addHandler()
and it is recommended to use QThread instead of threading.Thread
here is the complete updated code.
# import pandas as pd
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *
import time
import os
import sys
import threading
import json
import logging
class ConsolePanelHandler(logging.Handler):
def __init__(self, sig):
# super().__init__()
logging.Handler.__init__(self)
# logging.StreamHandler.__init__(self, stream)
self.stream = sig
def handle(self, record):
rv = self.filter(record)
if rv:
self.acquire()
try:
self.emit(record)
finally:
self.release()
return rv
def emit(self, record):
try:
self.stream.emit(self.format(record))
except RecursionError:
raise
except Exception:
self.handleError(record)
class thread(QThread):
def run(self) -> None:
for index in range(20):
logging.warning('scheiBe ' + str(index))
self.sleep(1)
class MainWindow(QMainWindow):
sig = pyqtSignal(str)
def __init__(self):
super().__init__()
self.layout = QVBoxLayout()
self.continue_ = QPushButton("Continue")
self.continue_.setStyleSheet("background-color: green")
self.continue_.setFont(QFont('SansSerif', 10))
self.continue_.setFixedSize(QSize(300, 22))
self.pause = QPushButton("Pause")
self.pause.setStyleSheet("background-color: orange")
self.pause.setFont(QFont('SansSerif', 10))
self.pause.setFixedSize(QSize(300, 22))
self.stop = QPushButton("Stop")
self.stop.setStyleSheet("background-color: #FD4B4B")
self.stop.setFont(QFont('SansSerif', 10))
self.stop.setFixedSize(QSize(300, 22))
self.c = ConsolePanelHandler(self.sig)
self.text_box = QPlainTextEdit()
self.text_box.setPlaceholderText("Bugs will be printed here")
self.text_box.setReadOnly(True)
logging.getLogger().addHandler(self.c)
logging.getLogger().setLevel(logging.DEBUG)
self.sig.connect(self.appendDebug)
self.text_box.moveCursor(QTextCursor.End)
self.layout.addWidget(self.continue_)
self.layout.addWidget(self.pause)
self.layout.addWidget(self.stop)
self.layout.addWidget(self.text_box)
self.w = QWidget()
self.w.setLayout(self.layout)
self.setCentralWidget(self.w)
self.thread1 = thread(self) # self is parent for Qthread so Qthread will be destroyed when it's parent no longer exist
self.thread1.start()
self.show()
def closeEvent(self, event):
close = QMessageBox()
close.setText("Are you sure want to stop and exit?")
close.setStandardButtons(QMessageBox.Yes | QMessageBox.Cancel)
close = close.exec()
if close == QMessageBox.Yes:
self.thread1.terminate()
sys.exit()
else:
event.ignore()
#pyqtSlot(str)
def appendDebug(self, string):
self.text_box.appendPlainText(string + '\n')
if __name__ == "__main__":
app = QApplication([])
window = MainWindow()
sys.exit(app.exec())

pyqt signal from a window not changing the main window widgets

I can pass values between two windows using signal,
and I want the plain text change, while I press a button from another window.
but the insertPlainText and even text.show() are not working
I've tried sending the signal to the init part of the Mainwindow,
tried update, repaint, but none of them works.
appreciate for any of your help, thanks!
the search_send method in class SearchWindow to MainWindow method test_
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
super(MainWindow, self).__init__()
self.ui = Ui_Dialog()
self.ui.setupUi(self)
self.ui.tag_box.hide()
def test_(self, i): # -------problem here------------------
print(i) // <- value here were right
self.ui.tag_box.insertPlainText(i) # -------Plain Text does not change-------
self.ui.tag_box.show()# -------Plain Text does not show either--------------
class SearchWindow(QtWidgets.QMainWindow):
signal=pyqtSignal(str)
def __init__(self, endpoint=None, user=None, password=None, points_link=None):
super(SearchWindow, self).__init__()
self.ui = Ui_Search()
self.ui.setupUi(self)
self.ui.pushButton_2.clicked.connect(self.search_send)
def search_send(self): # -------problem here------------------
tag_list = [tag1,tag2, tag3]
otherClass = MainWindow()
self.signal.connect(otherClass.test_)
for k in tag_list:
self.signal.emit(k)
if __name__ == '__main__':
app = QtWidgets.QApplication([])
window = MainWindow()
window.show()
sys.exit(app.exec_())
As we're unclear what Ui_Dialog contains, I can't tell what's wrong exactly, but can show you how Signal should be used.
This is example of Signal that's transferring text to Slot. Name may differ in PyQt5 - change accordingly.
from PySide2.QtWidgets import QWidget, QApplication, QPlainTextEdit, QTextEdit, QVBoxLayout
from PySide2.QtCore import Signal, Slot
import sys
class MainWindow(QWidget):
sig = Signal(str)
def __init__(self):
super(MainWindow, self).__init__()
self.setWindowTitle("Main")
self.layout = QVBoxLayout()
self.text = QTextEdit()
self.layout.addWidget(self.text)
self.setLayout(self.layout)
self.text.textChanged.connect(self.onChange)
def onChange(self):
self.sig.emit(self.text.toPlainText())
class SubWindow(QWidget):
def __init__(self, connect_target: MainWindow):
super(SubWindow, self).__init__()
self.setWindowTitle("Sub")
self.layout = QVBoxLayout()
self.text = QPlainTextEdit()
self.layout.addWidget(self.text)
self.setLayout(self.layout)
connect_target.sig.connect(self.onSignal)
#Slot(str)
def onSignal(self, text):
self.text.insertPlainText(text + '\r\n')
if __name__ == '__main__':
app = QApplication(sys.argv)
window = MainWindow()
window.show()
window_sub = SubWindow(window)
window_sub.show()
sys.exit(app.exec_())
Anything inside Main will be inserted to QPlainTextEdit as you wanted.

How to make "finish" button in QStackedWidget

I trying to create "Finish" button in QStackedWidget.
In a "checkButtons" function i checking current page index and set click events and text. I tried to check it by class name, but it doesn't work too.
Here is a code:
import sys
from PyQt5.QtGui import QIcon
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import (QApplication, QDialog, QComboBox, QStackedWidget, QWidget,
QPushButton, QLabel, QVBoxLayout, QHBoxLayout, QStyle)
class Main(QDialog):
def __init__(self, parent=None):
super(Main, self).__init__(parent)
# Main window setup
self.setWindowTitle("Stacked widget example")
self.setWindowIcon(self.style().standardIcon(QStyle.SP_FileDialogNewFolder))
self.setMinimumSize(400, 400)
self.setMaximumSize(640, 480)
self.rootVBox = QVBoxLayout()
self.rootHBox = QHBoxLayout()
self.rootHBox.addStretch()
self.rootVBox.addStretch()
self.pages = [FirstPage, SecondPage]
self.stacked = QStackedWidget(self)
for i in self.pages: self.stacked.addWidget(i(self))
self.pageState = True
self.buttonNext = QPushButton("Next")
self.buttonNext.clicked.connect(self.buttonNextConnect)
self.buttonBack = QPushButton("Back")
self.buttonBack.clicked.connect(self.buttonBackConnect)
self.rootHBox.addWidget(self.buttonBack)
self.rootHBox.addWidget(self.buttonNext)
self.rootVBox.addLayout(self.rootHBox)
self.setLayout(self.rootVBox)
def checkButtons(self):
print(self.stacked.currentIndex())
# I tried to check self.stacked.currentIndex() but it didn't work too
# if self.stacked.currentWidget().__class__ == self.pages[-1]:
if self.stacked.currentIndex() == len(self.pages) - 1:
self.buttonNext.setText("Finish")
self.buttonNext.clicked.connect(self.close)
elif self.stacked.currentIndex() < len(self.pages) - 1:
self.buttonNext.setText("Next")
self.buttonNext.clicked.connect(self.buttonNextConnect)
def buttonNextConnect(self):
self.stacked.setCurrentIndex(self.stacked.currentIndex() + 1)
self.checkButtons()
def buttonBackConnect(self):
self.stacked.setCurrentIndex(self.stacked.currentIndex() - 1)
self.checkButtons()
def finish(self):
self.close()
class FirstPage(QWidget):
def __init__(self, parent=None):
super(FirstPage, self).__init__(parent)
label = QLabel("First page")
rootVBox = QVBoxLayout()
rootHBox = QHBoxLayout()
rootHBox.addWidget(label)
rootVBox.addLayout(rootHBox)
self.setLayout(rootVBox)
class SecondPage(QWidget):
def __init__(self, parent=None):
super(SecondPage, self).__init__(parent)
label = QLabel("Second page")
rootVBox = QVBoxLayout()
rootHBox = QHBoxLayout()
rootHBox.addWidget(label)
rootVBox.addLayout(rootHBox)
self.setLayout(rootVBox)
if __name__ == '__main__':
app = QApplication(sys.argv)
main = Main()
main.show()
sys.exit(app.exec_())
If you try to press "next", "back" and then "next" again a program will be close. So, how can i fix it? Should i just make control buttons for each widget?
You must use the currentChanged signal of the QStackedWidget to know what page you are on and thus change the text, but in the buttonNextConnect slot you should check if you are already on the last page before switching to a new page, if you are then call to finish and if you do not change to another page
class Main(QDialog):
def __init__(self, parent=None):
super(Main, self).__init__(parent)
# Main window setup
self.setWindowTitle("Stacked widget example")
self.setWindowIcon(self.style().standardIcon(QStyle.SP_FileDialogNewFolder))
self.setMinimumSize(400, 400)
self.setMaximumSize(640, 480)
rootVBox = QVBoxLayout(self)
rootHBox = QHBoxLayout()
rootHBox.addStretch()
rootVBox.addStretch()
self.pages = [FirstPage, SecondPage]
self.stacked = QStackedWidget(self)
for i in self.pages: self.stacked.addWidget(i(self))
self.buttonNext = QPushButton("Next")
self.buttonNext.clicked.connect(self.buttonNextConnect)
self.buttonBack = QPushButton("Back")
self.buttonBack.clicked.connect(self.buttonBackConnect)
rootHBox.addWidget(self.buttonBack)
rootHBox.addWidget(self.buttonNext)
rootVBox.addLayout(rootHBox)
self.stacked.currentChanged.connect(self.on_currentChanged)
def buttonNextConnect(self):
if self.stacked.currentIndex() == self.stacked.count() -1:
self.finish()
if self.stacked.currentIndex() < self.stacked.count() -1:
self.stacked.setCurrentIndex(self.stacked.currentIndex() + 1)
def buttonBackConnect(self):
if self.stacked.currentIndex() > 0:
self.stacked.setCurrentIndex(self.stacked.currentIndex() - 1)
def on_currentChanged(self, index):
if index == self.stacked.count() -1:
self.buttonNext.setText("Finish")
else:
self.buttonNext.setText("Next")
def finish(self):
self.close()
Another option is to use QWizard and QWizardPage:
import sys
from PyQt5 import QtCore, QtGui, QtWidgets
class Main(QtWidgets.QWizard):
def __init__(self, parent=None):
super(Main, self).__init__(parent)
buttons = [
QtWidgets.QWizard.Stretch,
QtWidgets.QWizard.BackButton,
QtWidgets.QWizard.NextButton,
QtWidgets.QWizard.FinishButton
]
self.setButtonLayout(buttons)
self.addPage(FirstPage())
self.addPage(SecondPage())
class FirstPage(QtWidgets.QWizardPage):
def __init__(self, parent=None):
super(FirstPage, self).__init__(parent)
self.setTitle("First page")
class SecondPage(QtWidgets.QWizardPage):
def __init__(self, parent=None):
super(SecondPage, self).__init__(parent)
self.setTitle("Second page")
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
main = Main()
main.show()
sys.exit(app.exec_())

Catch final signal from QTimer object

I have a simple PyQt script. When I click a button, it starts a QTimer object and increments a progress bar. What I want is to change the label of my text when my progress bar reaches 100%. It worked for me once, but I can't get it to work anymore. What am I doing wrong?
Here's the main part of my code.
class MainWindow(QWidget):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
self.setWindowTitle('QProgressBar demo')
self.timerButton = QPushButton("Start", self)
self.timerButton.clicked.connect(self.timerStart)
self.timerObject = QTimer(self)
#self.timerObject.destroyed.connect(lambda:self.timerButton.setText("Finished") )
self.timerObject.destroyed.connect(lambda:print("Called" ))
self.progressBar = QProgressBar(self)
self.progressBar.setGeometry(10, 20, 290, 25)
self.timerButton.move(110,150)
self.progressBar.move(10,100)
self.increment = 0
self.resize(300, 300)
self.show()
#pyqtSlot()
def headsUp(self):
if(self.increment >= 100):
self.timerObject.stop()
else:
self.increment += 1
self.progressBar.setValue(self.increment)
return
def timerStart(self):
if (self.timerObject.isActive()):
self.timerObject.stop()
self.timerButton.setText("Resume")
else:
self.timerObject.timeout.connect(self.headsUp)
self.timerButton.setText("Pause")
self.timerObject.start(100)
destroyed is only issued when you delete the object, that a QTimer peer does not imply that it is deleted from memory, therefore it does not emit that signal, a possible solution is to create a signal for the QProgressBar when the value takes the maximum value as shown below:
import sys
from PyQt5 import QtCore, QtWidgets
class ProgressBar(QtWidgets.QProgressBar):
finished = QtCore.pyqtSignal()
def __init__(self, *args, **kwargs):
super(ProgressBar, self).__init__(*args, *kwargs)
self.valueChanged.connect(self.on_valueChanged)
#QtCore.pyqtSlot(int)
def on_valueChanged(self, val):
if val == self.maximum():
self.finished.emit()
class MainWindow(QtWidgets.QWidget):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
self.setWindowTitle('QProgressBar demo')
self.timerButton = QtWidgets.QPushButton("Start", self)
self.timerButton.clicked.connect(self.timerStart)
self.timerObject = QtCore.QTimer(self)
self.progressBar = ProgressBar(self)
self.progressBar.finished.connect(lambda: print("Called" ))
self.increment = 0
lay = QtWidgets.QVBoxLayout(self)
lay.addWidget(self.progressBar)
lay.addWidget(self.timerButton)
self.resize(300, 300)
#QtCore.pyqtSlot()
def headsUp(self):
if self.increment >= 100:
self.timerObject.stop()
else:
self.increment += 1
self.progressBar.setValue(self.increment)
#QtCore.pyqtSlot()
def timerStart(self):
if self.timerObject.isActive():
self.timerObject.stop()
self.timerButton.setText("Resume")
else:
self.timerObject.timeout.connect(self.headsUp)
self.timerButton.setText("Pause")
self.timerObject.start(100)
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
w = MainWindow()
w.show()
sys.exit(app.exec_())
Another best option is to use QTimeLine and its finished signal:
import sys
from PyQt5 import QtCore, QtWidgets
class MainWindow(QtWidgets.QWidget):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
self.setWindowTitle('QProgressBar demo')
self.timerButton = QtWidgets.QPushButton("Start", self)
self.timerButton.clicked.connect(self.timerStart)
self.timerObject = QtCore.QTimeLine(1000, self)
self.timerObject.setFrameRange(0, 100)
self.progressBar = QtWidgets.QProgressBar(self)
self.timerObject.frameChanged.connect(self.progressBar.setValue)
self.timerObject.finished.connect(lambda: print("Called" ))
lay = QtWidgets.QVBoxLayout(self)
lay.addWidget(self.progressBar)
lay.addWidget(self.timerButton)
self.resize(300, 300)
#QtCore.pyqtSlot()
def timerStart(self):
if self.timerObject.state() == QtCore.QTimeLine.Running:
self.timerObject.stop()
self.timerButton.setText("Resume")
else:
self.timerButton.setText("Pause")
self.timerObject.resume()
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
w = MainWindow()
w.show()
sys.exit(app.exec_())
This isn't working because you are connecting to the timers destoryed signal, but the timer is not being destroyed. To use this code as is, call self.timerObject.deleteLater() after you stop the timer.

Resources