Using USB port data in PyQt - pyqt

The aim of my code is to create a window with labels, each representing a sensor. The data comes from the USB port in a table of 0s&1s and depending on the value it colours the labels accordingly.
The goal is supposed to look like this:
I am unsure of how to pass the data from the port to the function in real time without recreating the window as a whole each time, as I only want it to change the drawn labels (and their colours). Therefore, I was wondering if anyone could point me in the right direction or give me a suggestion of what I can do to make this work.
The code creating my main window & labels:
class MainWindow (qt.QMainWindow):
def __init__(self):
super().__init__()
self.count = 0
self.j = 0
self.i = 0
self.screen()
self.making()
def screen(self):
self.setWindowTitle("Bee Counter")
self.showMaximized()
def making(self):
for i in values: #Iterates over the list of data which comes from the port.
if (i == 1):
self.label = qt.QLabel(self)
self.label.setStyleSheet("background-color: green; border: 1px solid black;")
self.move_label() #Creates multiple labels with the colour green.
else:
self.label = qt.QLabel(self)
self.label.setStyleSheet("background-color: red; border: 1px solid black;")
self.move_label() #Creates multiple labels with the colour red.
self.count +=1
def move_label(self):
self.label.resize(A, A)
self.label.setAlignment(Qt.AlignmentFlag.AlignCenter)
if self.count%2==0:
k=20
self.label.move(X0+self.j,k)
self.j=self.j+X_STEP
self.label.setText(f"{self.count}")
else:
k=90
self.label.move(X0+self.i,k)
self.label.setText(f"{self.count}")
self.i=self.i+X_STEP
self.label.show()
if __name__ == '__main__':
app = qt.QApplication(sys.argv)
w = MainWindow()
w.show()
app.exec()
The code getting the data from the port:
ser = serial.Serial(
port='COM3',\
baudrate=115200,\
parity=serial.PARITY_NONE,\
stopbits=serial.STOPBITS_ONE,\
bytesize=serial.EIGHTBITS)
print("<Succesfully connected to: " + ser.portstr)
while 1:
if ser.inWaiting()>0:
line = ser.readline()
line = line.decode('utf-8')
line = [char for char in line if char=="1" or char=="0"] #Gets data in a form of a table of 0s & 1s.
print(line)
time.sleep(0.01)
ser.close()
P.S. Excuse my perhaps very obvious question, I simply can not wrap my head around it :)

The concept is based on the wrong premise: making should only create the labels (and keep a reference to them), while another function should be responsible for their update.
Since the data rate is quite fast and the display object very simple, it's probably better to use a custom widget instead of continuously set the style sheet.
class DisplayWidget(QWidget):
state = False
def __init__(self, index):
super().__init__()
self.index = str(index)
self.setFixedSize(32, 32)
def setState(self, state):
if self.state != state:
self.state = state
self.update()
def paintEvent(self, event):
qp = QPainter(self)
qp.setBrush(Qt.green if self.state else Qt.red)
qp.drawRect(self.rect().adjusted(0, 0, -1, -1))
qp.drawText(self.rect(), Qt.AlignCenter, self.index)
Now, the "viewer" is a custom widget that is able to create a predefined grid of display widgets and updates them when necessary.
You can provide a default field count, or just ignore that, since the function that updates the data is also capable to update the grid whenever the field count doesn't match.
class SerialViewer(QWidget):
def __init__(self, fieldCount=None):
super().__init__()
layout = QGridLayout(self)
layout.setAlignment(Qt.AlignCenter)
self.widgets = []
if isinstance(fieldCount, int) and fieldCount > 0:
self.createGrid(fieldCount)
def createGrid(self, fieldCount, rows=2):
while self.widgets:
self.widgets.pop(0).deleteLater()
rows = max(1, rows)
count = 0
columns, rest = divmod(fieldCount, rows)
if rest:
columns += 1
for column in range(columns):
for row in range(rows):
widget = DisplayWidget(count)
self.layout().addWidget(widget, row, column)
self.widgets.append(widget)
count += 1
if count == fieldCount:
break
def updateData(self, data):
if len(data) != len(self.widgets):
self.createGrid(len(data))
for widget, state in zip(self.widgets, data):
widget.setState(state)
As you can see, instead of using resize() or move(), I'm using a layout manager that is automatically able to place (and eventually resize) the widgets. Remember, fixed geometries are almost always discouraged. Also note that widgets should not be directly added to a QMainWindow, but set for its central widget.
The thread is implemented in a QThread subclass, using a custom signal that is emitted whenever new data is available:
class SerialThread(QThread):
dataReceived = pyqtSignal(object)
def run(self):
ser = serial.Serial(
port='COM3',
baudrate=115200,
parity=serial.PARITY_NONE,
stopbits=serial.STOPBITS_ONE,
bytesize=serial.EIGHTBITS)
self.keepRunning = True
while self.keepRunning:
if ser.inWaiting() > 0:
line = ser.readline()
line = line.decode('utf-8')
self.dataReceived.emit(
list(int(char) for char in line if char in '01')
)
# note that the indentation level of sleep() is *outside*
# of the "if" otherwise it may temporarily block the loop
# in case there is no data available
time.sleep(0.01)
def stop(self):
self.keepRunning = False
self.wait()
Finally, the main window, from which we can start or stop the serial communication:
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
central = QWidget()
self.setCentralWidget(central)
self.startButton = QPushButton('Start')
self.stopButton = QPushButton('Stop', enabled=False)
self.serialViewer = SerialViewer(32)
layout = QGridLayout(central)
layout.addWidget(self.startButton)
layout.addWidget(self.stopButton, 0, 1)
layout.addWidget(self.serialViewer, 1, 0, 1, 2)
self.serialThread = SerialThread()
self.startButton.clicked.connect(self.start)
self.stopButton.clicked.connect(self.serialThread.stop)
self.serialThread.dataReceived.connect(
self.serialViewer.updateData)
self.serialThread.finished.connect(self.stopped)
def start(self):
self.startButton.setEnabled(False)
self.stopButton.setEnabled(True)
self.serialThread.start()
def stopped(self):
self.startButton.setEnabled(True)
self.stopButton.setEnabled(False)
Notes:
Qt already provides an asynchronous class for serial communication that already supports signals, QSerialPort;
the above codes use the old enum syntax, for Qt6 you need the full enum names (Qt.GlobalColor.red, Qt.AlignmentFlag.AlignCenter, etc);
you will probably need a further check for the serial connection before starting the while loop in run();

There has to be a way to handle repaint events with PyQt. I can't imagine there woulnd't be. I got carried away for a minute, but here is where I'm concluding my attempt. Hopefully this is of some help in anyway, I woulnd't expand on the queue. The main thread needs to come back to repaint it I think. The only issue is awaiting the serial's data, if there even is almost no wait time, I would expect it to hang, and leave the reciever flying through loops and burning up the CPU
import threading
import queue
q = queue.Queue()
def painter():
while True:
if ser.inWaiting()>0:
line = ser.readline()
line = line.decode('utf-8')
line = [char for char in line if char=="1" or char=="0"]
print(line)
time.sleep(0.01)
ser.close()
#stylesheet = "background-color: {}; border: 1px solid black;"
#color = "red" if i else "green"
#label.setStyleSheet(stylesheet.format(color))
class MainWindow (qt.QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("Bee Counter")
self.showMaximized()
def build_table(self, width=16, height=2):
labels = [qt.QLabel(self) for _ in range(width*height)]
for label in labels:
label.setStyleSheet(style.format("red"))
threading.Thread(target=painter).start()

Related

QHeaderView going out of viewport

I'm having couple of issues while trying to make a QHeaderView for my QTableView.
I want QHeaderView to be resizable by the user (Qt.ResizeMode.Interactive) while being able to stretch its sections proportionately when the window or QTableView is being resized. I found this problem online, and managed to mostly solve it but there is still some stuttering when the resizing begins and I think there should be a better solution than mine. Currently it's done by using QTimer to stop sections from going out of the viewport. Timer is being updated every millisecond. If the update interval is bigger, sections would go out of viewport and magically teleport back when the timer is updated, so once per millisecond in my case. There's still some stuttering visible if the user is dragging sections out of the viewport by dragging their mouse faster, not so visible when the mouse is slower, but visible none the less.
Every section should be resizable and movable, besides the first two. The first two sections should be immovable and fixed. I managed to make them fixed and they don't seem to have an effect on resizing of the sections, but I have no idea how to make them immovable while all the other sections are movable.
Sections should have text eliding, which I managed to make an item delegate for, but setting it on QHeaderView seems to do absolutely nothing (paint() method doesn't even get called). It's probably because item delegate isn't affecting sections, if so, how can I make a delegate that does affect them?
Here's my current code (it's a bit of a mess, but hopefully you'll get the idea):
import sys
import weakref
from typing import Any, Optional
from PyQt6 import QtWidgets, QtCore, QtGui
from PyQt6.QtCore import pyqtSlot, Qt
from PyQt6.QtGui import QFontMetrics
from PyQt6.QtWidgets import QHeaderView, QStyledItemDelegate, QStyleOptionViewItem
class MyItemDelegate(QStyledItemDelegate):
def __init__(self, parent=None):
super().__init__(parent)
def paint(self, painter: QtGui.QPainter, option: QStyleOptionViewItem, index: QtCore.QModelIndex) -> None:
text = index.data(Qt.ItemDataRole.DisplayRole)
# print(text)
if text:
elided_text = QFontMetrics(option.font).elidedText(str(text), Qt.TextElideMode.ElideRight, option.rect.width())
painter.drawText(option.rect, Qt.AlignmentFlag.AlignLeft, elided_text)
class HeaderView(QtWidgets.QHeaderView):
def __init__(self,
orientation: QtCore.Qt.Orientation = Qt.Orientation.Horizontal,
parent: Optional[QtWidgets.QWidget] = None):
super(HeaderView, self).__init__(orientation, parent)
item_delegate = MyItemDelegate(self)
self.setItemDelegate(item_delegate)
self.setMinimumSectionSize(5)
self.setStretchLastSection(True)
self.setCascadingSectionResizes(True)
self.setSectionsMovable(True)
self.fixed_section_indexes = (0, 1)
timer = QtCore.QTimer(self)
timer.setSingleShot(True)
timer.setTimerType(Qt.TimerType.PreciseTimer)
timer.timeout.connect(self._update_sizes)
resize_mode_timer = QtCore.QTimer(self)
resize_mode_timer.setTimerType(Qt.TimerType.PreciseTimer)
resize_mode_timer.setSingleShot(True)
resize_mode_timer.timeout.connect(lambda: self.setSectionResizeMode(QHeaderView.ResizeMode.Interactive))
self._resize_mode_timer = weakref.proxy(resize_mode_timer)
self._timer = weakref.proxy(timer)
self.sectionResized.connect(self._handle_resize)
self.setTextElideMode(Qt.TextElideMode.ElideLeft)
self.setDefaultAlignment(Qt.AlignmentFlag.AlignLeft)
self.proportions = []
self.mouse_pressed = False
def mouseReleaseEvent(self, e: QtGui.QMouseEvent) -> None:
self.mouse_pressed = False
super().mouseReleaseEvent(e)
self.proportions = [self.sectionSize(i) / self.width() for i in range(self.count())]
# print(self.mouse_pressed)
def init_sizes(self):
each = self.width() // self.count()
for i in range(self.count()):
self.resizeSection(self.logicalIndex(i), each)
#pyqtSlot(int, int, int)
def _handle_resize(self, logicalIndex: int, oldSize: int, newSize: int):
self._timer.start(1)
def resizeEvent(self, event: QtGui.QResizeEvent):
super().resizeEvent(event)
width = self.width()
# sizes = [self.sectionSize(self.logicalIndex(i)) for i in range(self.count())]
width_without_fixed = width - sum([self.sectionSize(i) for i in self.fixed_section_indexes])
for i in range(self.count()):
if not self.proportions:
break
if i not in self.fixed_section_indexes:
self.resizeSection(i, int(self.proportions[i] * width_without_fixed))
self._timer.start(1)
#pyqtSlot()
def _update_sizes(self):
width = self.width()
sizes = [self.sectionSize(self.logicalIndex(i)) for i in range(self.count())]
# width_without_fixed = width - sum([self.sectionSize(i) for i in self.fixed_section_indexes])
index = len(sizes) - 1
i = 0
while index >= 0 and sum(sizes) > width:
i += 1
if i > 100:
break
if sizes[index] > 5 and index not in self.fixed_section_indexes: # minimum width (5)
new_width = width - (sum(sizes) - sizes[index])
if new_width < 5:
new_width = 5
sizes[index] = new_width
index -= 1
for j, value in enumerate(sizes):
self.resizeSection(self.logicalIndex(j), value)
if not self.proportions:
self.proportions = [self.sectionSize(i) / width for i in range(self.count())]
class Model(QtCore.QAbstractTableModel):
def __init__(self, parent: Optional[QtWidgets.QWidget] = None) -> None:
super(Model, self).__init__(parent)
self.__headers = ["Column A", "Column B", "Column C", "Column D", "Column E", "Column F", "Column G"]
self.__data = []
for i in range(10):
row = [0, 1, 2, 3, 42222222222, 5, 6, 74444444]
self.__data.append(row)
def rowCount(self, index: Optional[QtCore.QModelIndex] = None) -> int:
return len(self.__data)
def columnCount(self, index: Optional[QtCore.QModelIndex] = None) -> int:
return len(self.__headers)
def headerData(self, section: int, orientation: QtCore.Qt.Orientation,
role: QtCore.Qt.ItemDataRole = Qt.ItemDataRole.DisplayRole) -> Any:
if role == Qt.ItemDataRole.DisplayRole:
if orientation == Qt.Orientation.Horizontal:
return self.__headers[section]
return f"{section}"
return None
def data(self, index: QtCore.QModelIndex,
role: QtCore.Qt.ItemDataRole = Qt.ItemDataRole.DisplayRole) -> Any:
if role in [Qt.ItemDataRole.DisplayRole, Qt.ItemDataRole.EditRole]:
return self.__data[index.row()][index.column()]
return None
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
view = QtWidgets.QTableView()
view.resize(600, 600)
header = HeaderView()
view.setHorizontalHeader(header)
model = Model()
view.setModel(model)
header.init_sizes()
view.horizontalHeader().resizeSection(0, 30)
view.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeMode.Fixed)
view.horizontalHeader().resizeSection(1, 30)
view.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeMode.Fixed)
view.show()
app.exec()

What causes a nested QRubberband to move unexpectedly?

I am just curious if I can make a nested QRubberband. (I or someone might find a use to it). I managed to edit the code from this answer to make a nested QRubberband. It is all fine and working until I move the QRubberband inside its parent QRubberband. I was very confused as it moves wildly when I'm dragging it.
This is the sample code:
import sys
from PyQt5.QtGui import *
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
class ResizableRubberBand(QRubberBand):
moving = False
def __init__(self, parent=None):
super(ResizableRubberBand, self).__init__(QRubberBand.Rectangle, parent)
self.setAttribute(Qt.WA_TransparentForMouseEvents, False)
self.draggable = True
self.dragging = False
self.is_dragging = False
self.dragging_threshold = 5
self.mousePressPos = None
self.borderRadius = 5
self.setWindowFlags(Qt.SubWindow)
layout = QHBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
layout.addWidget(
QSizeGrip(self), 0,
Qt.AlignLeft | Qt.AlignTop)
layout.addWidget(
QSizeGrip(self), 0,
Qt.AlignRight | Qt.AlignBottom)
self.show()
def resizeEvent(self, event):
self.clearMask()
def paintEvent(self, event):
super().paintEvent(event)
qp = QPainter(self)
qp.setRenderHint(QPainter.Antialiasing)
qp.translate(.5, .5)
qp.drawRoundedRect(self.rect().adjusted(0, 0, -1, -1),
self.borderRadius, self.borderRadius)
def mousePressEvent(self, event):
if self.draggable and event.button() == Qt.RightButton:
self.mousePressPos = event.pos()
if event.button() == Qt.LeftButton:
self.first_mouse_location = (event.x(), event.y())
self.band = ResizableRubberBand(self)
self.band.setGeometry(event.x(), event.y(), 0, 0)
super(ResizableRubberBand, self).mousePressEvent(event)
def mouseMoveEvent(self, event):
if self.draggable and event.buttons() & Qt.RightButton:
diff = event.pos() - self.mousePressPos
if not self.dragging:
if diff.manhattanLength() > self.dragging_threshold:
self.dragging = True
if self.dragging:
geo = self.geometry()
parentRect = self.parent().rect()
geo.translate(diff)
if not parentRect.contains(geo):
if geo.right() > parentRect.right():
geo.moveRight(parentRect.right())
elif geo.x() < parentRect.x():
geo.moveLeft(parentRect.x())
if geo.bottom() > parentRect.bottom():
geo.moveBottom(parentRect.bottom())
elif geo.y() < parentRect.y():
geo.moveTop(parentRect.y())
self.move(geo.topLeft())
if event.buttons() & Qt.LeftButton:
first_mouse_location_x = self.first_mouse_location[0]
first_mouse_location_y = self.first_mouse_location[1]
new_x, new_y = event.x(), event.y()
difference_x = new_x - first_mouse_location_x
difference_y = new_y - first_mouse_location_y
self.band.resize(difference_x, difference_y)
super(ResizableRubberBand, self).mouseMoveEvent(event)
def mouseReleaseEvent(self, event):
if self.mousePressPos is not None:
if event.button() == Qt.RightButton and self.dragging:
event.ignore()
self.dragging = False
self.mousePressPos = None
super(ResizableRubberBand, self).mouseReleaseEvent(event)
class mQLabel(QLabel):
def __init__(self, parent=None):
QLabel.__init__(self, parent)
self.setContentsMargins(0,0,0,0)
self.setAlignment(Qt.AlignTop | Qt.AlignLeft)
self.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.MinimumExpanding)
def mousePressEvent(self, event):
if event.button() == Qt.LeftButton:
self.first_mouse_location = (event.x(), event.y())
self.band = ResizableRubberBand(self)
self.band.setGeometry(event.x(), event.y(), 0, 0)
def mouseMoveEvent(self, event):
if event.buttons() & Qt.LeftButton:
first_mouse_location_x = self.first_mouse_location[0]
first_mouse_location_y = self.first_mouse_location[1]
new_x, new_y = event.x(), event.y()
difference_x = new_x - first_mouse_location_x
difference_y = new_y - first_mouse_location_y
self.band.resize(difference_x, difference_y)
class App(QWidget):
def __init__(self):
super().__init__()
## Set main window attributes
self.setFixedSize(1000,600)
# Add Label
self.label = mQLabel()
self.label.setStyleSheet("border: 1px solid black;")
self.label_layout = QHBoxLayout(self)
self.label_layout.addWidget(self.label)
self.show()
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = App()
sys.exit(app.exec_())
I'm trying to figure it out for 2 hours but I can't really seem to figure out what causes the unnecessary movement. My best guess is it is coming from the mouseMoveEvent but I'm not quite sure if it is from the parent QRubberband or from the QRubberband inside. I hope someone can figure out what is happening here.
The problem is the call to the base implementation of mouse events, which by default gets propagated to the parent for widgets that do not directly implement them, including QRubberBand, which normally doesn't intercept mouse events at all (which we restored by disabling the relative window attribute).
Since the parent itself is a rubber band, it will be moved itself too, making the movement recursive for the child, since it receives a mouse move exactly due to the fact that its been moved: remember that if a widget is moved and the mouse doesn't directly follow the same movement, it will potentially receive a mouse move event relative to its new position.
You can either return before calling it when you handle it, or not call it at all, depending on your needs.
The important thing is that it's consistent (especially for press and move), otherwise a widget could receive a mouse move without receiving the mouse press, which will crash as the variables have not been set yet.
Be aware that if you're in the process of making a more advanced editor for clipping/selections, drawing, etc, you should really consider using the Graphics View Framework: while much more complex and with a more steep learning curve, you'll soon find out that continuing development on basic QWidgets becomes gradually much more convoluted and difficult, to a point where it is really hard to fix things, especially if you're going to deal with image scaling or even basic scroll and zoom.
QWidget and QLabel implementations are not intended for image management, not even simple editing, and custom placed/painted/nested widgets are often difficult to deal with. Consider that doing a similar selection tool would have been much more easy in a graphics scene: for instance, the moving implementation would be almost completely unnecessary, as it's enough to set a simple flag to make an item movable.

Capture keys with TKinter with this scenario

I would either like to capture all key strokes, or associate a key stroke to a button. At this time, there is no user input in this game other than the user clicking buttons. I would like to assign a single keyboard letter to each button. I was also playing with pynput, but since the program is already using TKinter, seems like I should be able to accomplish it with its features.
I can either have an on_press method in the main Game class that then calls the appropriate function for each key (same as user click the key), or perhaps there is a better way.
Most of the examples I've seen deal with the object created from tkinter class, but in this case, it's removed from my main program several levels.
This is a game I got from GitHub, and adapting to my preferences. So I'm trying to change it as little as possibly structurally.
In Graphics.py, I see this code:
class GraphWin(tk.Canvas):
def __init__(self, title="Graphics Window", width=200, height=200):
master = tk.Toplevel(_root)
master.protocol("WM_DELETE_WINDOW", self.close)
tk.Canvas.__init__(self, master, width=width, height=height)
self.master.title(title)
self.pack()
master.resizable(0,0)
self.foreground = "black"
self.items = []
self.mouseX = None
self.mouseY = None
self.bind("<Button-1>", self._onClick) #original code
self.height = height
self.width = width
self._mouseCallback = None
self.trans = None
def _onClick(self, e):
self.mouseX = e.x
self.mouseY = e.y
if self._mouseCallback:
self._mouseCallback(Point(e.x, e.y))
The main program is basically something like this:
def main():
# first number is width, second is height
screenWidth = 800
screenHeight = 500
mainWindow = GraphWin("Game", screenWidth, screenHeight)
game = game(mainWindow)
mainWindow.bind('h', game.on_press()) #<---- I added this
#listener = keyboard.Listener(on_press=game.on_press, on_release=game.on_release)
#listener.start()
game.go()
#listener.join()
mainWindow.close()
if __name__ == '__main__':
main()
I added a test function in the Game class, and it currently is not firing.
def on_press(self):
#print("key=" + str(key))
print( "on_press")
#if key == keyboard.KeyCode(char='h'):
# self.hit()
Buttons are setup like this:
def __init__( self, win ):
# First set up screen
self.win = win
win.setBackground("dark green")
xmin = 0.0
xmax = 160.0
ymax = 220.0
win.setCoords( 0.0, 0.0, xmax, ymax )
self.engine = MouseTrap( win )
then later...
self.play_button = Button( win, Point(bs*8.5,by), bw, bh, 'Play')
self.play_button.setRun( self.play )
self.engine.registerButton( self.play_button )
And finally, the Button code is in guiengine.py
class Button:
"""A button is a labeled rectangle in a window.
It is activated or deactivated with the activate()
and deactivate() methods. The clicked(p) method
returns true if the button is active and p is inside it."""
def __init__(self, win, center, width, height, label):
""" Creates a rectangular button, eg:
qb = Button(myWin, Point(30,25), 20, 10, 'Quit') """
self.runDef = False
self.setUp( win, center, width, height, label )
def setUp(self, win, center, width, height, label):
""" set most of the Button data - not in init to make easier
for child class methods inheriting from Button.
If called from child class with own run(), set self.runDef"""
w,h = width/2.0, height/2.0
x,y = center.getX(), center.getY()
self.xmax, self.xmin = x+w, x-w
self.ymax, self.ymin = y+h, y-h
p1 = Point(self.xmin, self.ymin)
p2 = Point(self.xmax, self.ymax)
self.rect = Rectangle(p1,p2)
self.rect.setFill('lightgray')
self.rect.draw(win)
self.label = Text(center, label)
self.label.draw(win)
self.deactivate()
def clicked(self, p):
"Returns true if button active and p is inside"
return self.active and \
self.xmin <= p.getX() <= self.xmax and \
self.ymin <= p.getY() <= self.ymax
def getLabel(self):
"Returns the label string of this button."
return self.label.getText()
def activate(self):
"Sets this button to 'active'."
self.label.setFill('black')
self.rect.setWidth(2)
self.active = True
def deactivate(self):
"Sets this button to 'inactive'."
self.label.setFill('darkgrey')
self.rect.setWidth(1)
self.active = False
def setRun( self, function ):
"set a function to be the mouse click event handler"
self.runDef = True
self.runfunction = function
def run( self ):
"""The default event handler. It either runs the handler function
set in setRun() or it raises an exception."""
if self.runDef:
return self.runfunction()
else:
#Neal change for Python3
#raise RuntimeError, 'Button run() method not defined'
raise RuntimeError ('Button run() method not defined')
return False # exit program on error
Extra code requested:
class Rectangle(_BBox):
def __init__(self, p1, p2):
_BBox.__init__(self, p1, p2)
def _draw(self, canvas, options):
p1 = self.p1
p2 = self.p2
x1,y1 = canvas.toScreen(p1.x,p1.y)
x2,y2 = canvas.toScreen(p2.x,p2.y)
return canvas.create_rectangle(x1,y1,x2,y2,options)
def clone(self):
other = Rectangle(self.p1, self.p2)
other.config = self.config
return other
class Point(GraphicsObject):
def __init__(self, x, y):
GraphicsObject.__init__(self, ["outline", "fill"])
self.setFill = self.setOutline
self.x = x
self.y = y
def _draw(self, canvas, options):
x,y = canvas.toScreen(self.x,self.y)
return canvas.create_rectangle(x,y,x+1,y+1,options)
def _move(self, dx, dy):
self.x = self.x + dx
self.y = self.y + dy
def clone(self):
other = Point(self.x,self.y)
other.config = self.config
return other
def getX(self): return self.x
def getY(self): return self.y
Update
Some of the notes I put in the comments:
It is using this: http://mcsp.wartburg.edu/zelle/python/graphics.py John Zelle's graphic.py.
http://mcsp.wartburg.edu/zelle/python/graphics/graphics.pdf - See class _BBox(GraphicsObject): for common methods.
I see class GraphWin has an anykey - where it captures the keys. But how would I get that back in my main program, especially as an event that would fire as soon as the user typed it?
Do I need to write my own listener - see also python graphics win.getKey() function?…. That post has a while loop waiting on the keys. I'm not sure where I would put in such a while loop, and how that would trigger into the "Game" class to fire an event. Do I need to write my own listener?
The reason the on_press() command not firing is due to the fact that the .bind() call is bound to an instance of Canvas. This means the canvas widget must have the focus for the keypress to register.
Use bind_all instead of bind.
Alternatives to fixing this:
Using mainWindow.bind_all("h", hit) - to bind the letter h directly to the "hit" button handler function (just make sure the hit function has the signature as follows:
def hit(self, event='')
Using mainWindow.bind_all("h", game.on_press) - binds the keypress to the whole application
Using root.bind("h", game.on_press) - binds the keypress to the root window (maybe a toplevel is more accurate here depending on if there are multiple windows)
Related to catching any key, there are some examples over here about doing that, using the "<Key>" event specifier: https://tkinterexamples.com/events/keyboard
whenever you want to combine your mouse and keyboard input with widgets I strongly suggest you to use the built-in .bind() method. .bind() can have two values:
Type of input
Name of callback function
An example:
entry_name.bind("<Return>", function_name_here)
This will call the function_name_here() function whenever the Return or the Enter key on your keyboard is pressed. This method is available for almost all tk widgets. You can also state key combinations as well as mouse controls.(Ctrl-K, Left-click etc.). If you want to bind multiple key strokes to a certain callback function, then just bind the both with same callback function like this:
entry_name.bind("<Return>", function_name_here)
entry_name.bind("<MouseButton-1>", function_name_here)
This will allow you to call the function when either of Return or Left-click of the mouse are pressed.

Is there an equivalent of Toastr for PyQt?

I am working on my first PyQt project and I would like to come up with a way to provide the user with success or error messages when they complete tasks. With Javascript in the past, I used Toastr and I was curious if there is anything like it for Python applications. I considered using the QDialog class in PyQt, but I would rather not have separate windows as popups if possible since even modeless dialog windows would be distracting for the user.
UPDATE: I've updated the code, making it possible to show desktop-wise notifications (see below).
Implementing a desktop-aware toaster like widget is not impossible, but presents some issues that are platform dependent. On the other hand, a client-side one is easier.
I've created a small class that is able to show a notification based on the top level window of the current widget, with the possibility to set the message text, the icon, and if the notification is user-closable. I also added a nice opacity animation, that is common in such systems.
Its main use is based on a static method, similarly to what QMessageBox does, but it can also be implemented in a similar fashion by adding other features.
UPDATE
I realized that making a desktop-wise notification is not that hard (but some care is required for cross-platform development, I'll leave that up to the programmer).
The following is the updated code that allows using None as a parent for the class, making the notification a desktop widget instead of a child widget of an existing Qt one. If you're reading this and you're not interested in such a feature, just check the editing history for the original (and slightly simpler) code.
from PyQt5 import QtCore, QtGui, QtWidgets
import sys
class QToaster(QtWidgets.QFrame):
closed = QtCore.pyqtSignal()
def __init__(self, *args, **kwargs):
super(QToaster, self).__init__(*args, **kwargs)
QtWidgets.QHBoxLayout(self)
self.setSizePolicy(QtWidgets.QSizePolicy.Maximum,
QtWidgets.QSizePolicy.Maximum)
self.setStyleSheet('''
QToaster {
border: 1px solid black;
border-radius: 4px;
background: palette(window);
}
''')
# alternatively:
# self.setAutoFillBackground(True)
# self.setFrameShape(self.Box)
self.timer = QtCore.QTimer(singleShot=True, timeout=self.hide)
if self.parent():
self.opacityEffect = QtWidgets.QGraphicsOpacityEffect(opacity=0)
self.setGraphicsEffect(self.opacityEffect)
self.opacityAni = QtCore.QPropertyAnimation(self.opacityEffect, b'opacity')
# we have a parent, install an eventFilter so that when it's resized
# the notification will be correctly moved to the right corner
self.parent().installEventFilter(self)
else:
# there's no parent, use the window opacity property, assuming that
# the window manager supports it; if it doesn't, this won'd do
# anything (besides making the hiding a bit longer by half a second)
self.opacityAni = QtCore.QPropertyAnimation(self, b'windowOpacity')
self.opacityAni.setStartValue(0.)
self.opacityAni.setEndValue(1.)
self.opacityAni.setDuration(100)
self.opacityAni.finished.connect(self.checkClosed)
self.corner = QtCore.Qt.TopLeftCorner
self.margin = 10
def checkClosed(self):
# if we have been fading out, we're closing the notification
if self.opacityAni.direction() == self.opacityAni.Backward:
self.close()
def restore(self):
# this is a "helper function", that can be called from mouseEnterEvent
# and when the parent widget is resized. We will not close the
# notification if the mouse is in or the parent is resized
self.timer.stop()
# also, stop the animation if it's fading out...
self.opacityAni.stop()
# ...and restore the opacity
if self.parent():
self.opacityEffect.setOpacity(1)
else:
self.setWindowOpacity(1)
def hide(self):
# start hiding
self.opacityAni.setDirection(self.opacityAni.Backward)
self.opacityAni.setDuration(500)
self.opacityAni.start()
def eventFilter(self, source, event):
if source == self.parent() and event.type() == QtCore.QEvent.Resize:
self.opacityAni.stop()
parentRect = self.parent().rect()
geo = self.geometry()
if self.corner == QtCore.Qt.TopLeftCorner:
geo.moveTopLeft(
parentRect.topLeft() + QtCore.QPoint(self.margin, self.margin))
elif self.corner == QtCore.Qt.TopRightCorner:
geo.moveTopRight(
parentRect.topRight() + QtCore.QPoint(-self.margin, self.margin))
elif self.corner == QtCore.Qt.BottomRightCorner:
geo.moveBottomRight(
parentRect.bottomRight() + QtCore.QPoint(-self.margin, -self.margin))
else:
geo.moveBottomLeft(
parentRect.bottomLeft() + QtCore.QPoint(self.margin, -self.margin))
self.setGeometry(geo)
self.restore()
self.timer.start()
return super(QToaster, self).eventFilter(source, event)
def enterEvent(self, event):
self.restore()
def leaveEvent(self, event):
self.timer.start()
def closeEvent(self, event):
# we don't need the notification anymore, delete it!
self.deleteLater()
def resizeEvent(self, event):
super(QToaster, self).resizeEvent(event)
# if you don't set a stylesheet, you don't need any of the following!
if not self.parent():
# there's no parent, so we need to update the mask
path = QtGui.QPainterPath()
path.addRoundedRect(QtCore.QRectF(self.rect()).translated(-.5, -.5), 4, 4)
self.setMask(QtGui.QRegion(path.toFillPolygon(QtGui.QTransform()).toPolygon()))
else:
self.clearMask()
#staticmethod
def showMessage(parent, message,
icon=QtWidgets.QStyle.SP_MessageBoxInformation,
corner=QtCore.Qt.TopLeftCorner, margin=10, closable=True,
timeout=5000, desktop=False, parentWindow=True):
if parent and parentWindow:
parent = parent.window()
if not parent or desktop:
self = QToaster(None)
self.setWindowFlags(self.windowFlags() | QtCore.Qt.FramelessWindowHint |
QtCore.Qt.BypassWindowManagerHint)
# This is a dirty hack!
# parentless objects are garbage collected, so the widget will be
# deleted as soon as the function that calls it returns, but if an
# object is referenced to *any* other object it will not, at least
# for PyQt (I didn't test it to a deeper level)
self.__self = self
currentScreen = QtWidgets.QApplication.primaryScreen()
if parent and parent.window().geometry().size().isValid():
# the notification is to be shown on the desktop, but there is a
# parent that is (theoretically) visible and mapped, we'll try to
# use its geometry as a reference to guess which desktop shows
# most of its area; if the parent is not a top level window, use
# that as a reference
reference = parent.window().geometry()
else:
# the parent has not been mapped yet, let's use the cursor as a
# reference for the screen
reference = QtCore.QRect(
QtGui.QCursor.pos() - QtCore.QPoint(1, 1),
QtCore.QSize(3, 3))
maxArea = 0
for screen in QtWidgets.QApplication.screens():
intersected = screen.geometry().intersected(reference)
area = intersected.width() * intersected.height()
if area > maxArea:
maxArea = area
currentScreen = screen
parentRect = currentScreen.availableGeometry()
else:
self = QToaster(parent)
parentRect = parent.rect()
self.timer.setInterval(timeout)
# use Qt standard icon pixmaps; see:
# https://doc.qt.io/qt-5/qstyle.html#StandardPixmap-enum
if isinstance(icon, QtWidgets.QStyle.StandardPixmap):
labelIcon = QtWidgets.QLabel()
self.layout().addWidget(labelIcon)
icon = self.style().standardIcon(icon)
size = self.style().pixelMetric(QtWidgets.QStyle.PM_SmallIconSize)
labelIcon.setPixmap(icon.pixmap(size))
self.label = QtWidgets.QLabel(message)
self.layout().addWidget(self.label)
if closable:
self.closeButton = QtWidgets.QToolButton()
self.layout().addWidget(self.closeButton)
closeIcon = self.style().standardIcon(
QtWidgets.QStyle.SP_TitleBarCloseButton)
self.closeButton.setIcon(closeIcon)
self.closeButton.setAutoRaise(True)
self.closeButton.clicked.connect(self.close)
self.timer.start()
# raise the widget and adjust its size to the minimum
self.raise_()
self.adjustSize()
self.corner = corner
self.margin = margin
geo = self.geometry()
# now the widget should have the correct size hints, let's move it to the
# right place
if corner == QtCore.Qt.TopLeftCorner:
geo.moveTopLeft(
parentRect.topLeft() + QtCore.QPoint(margin, margin))
elif corner == QtCore.Qt.TopRightCorner:
geo.moveTopRight(
parentRect.topRight() + QtCore.QPoint(-margin, margin))
elif corner == QtCore.Qt.BottomRightCorner:
geo.moveBottomRight(
parentRect.bottomRight() + QtCore.QPoint(-margin, -margin))
else:
geo.moveBottomLeft(
parentRect.bottomLeft() + QtCore.QPoint(margin, -margin))
self.setGeometry(geo)
self.show()
self.opacityAni.start()
class W(QtWidgets.QWidget):
def __init__(self):
QtWidgets.QWidget.__init__(self)
layout = QtWidgets.QVBoxLayout(self)
toasterLayout = QtWidgets.QHBoxLayout()
layout.addLayout(toasterLayout)
self.textEdit = QtWidgets.QLineEdit('Ciao!')
toasterLayout.addWidget(self.textEdit)
self.cornerCombo = QtWidgets.QComboBox()
toasterLayout.addWidget(self.cornerCombo)
for pos in ('TopLeft', 'TopRight', 'BottomRight', 'BottomLeft'):
corner = getattr(QtCore.Qt, '{}Corner'.format(pos))
self.cornerCombo.addItem(pos, corner)
self.windowBtn = QtWidgets.QPushButton('Show window toaster')
toasterLayout.addWidget(self.windowBtn)
self.windowBtn.clicked.connect(self.showToaster)
self.screenBtn = QtWidgets.QPushButton('Show desktop toaster')
toasterLayout.addWidget(self.screenBtn)
self.screenBtn.clicked.connect(self.showToaster)
# a random widget for the window
layout.addWidget(QtWidgets.QTableView())
def showToaster(self):
if self.sender() == self.windowBtn:
parent = self
desktop = False
else:
parent = None
desktop = True
corner = QtCore.Qt.Corner(self.cornerCombo.currentData())
QToaster.showMessage(
parent, self.textEdit.text(), corner=corner, desktop=desktop)
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
w = W()
w.show()
sys.exit(app.exec_())
Try it:
import sys
from PyQt5.QtCore import (QRectF, Qt, QPropertyAnimation, pyqtProperty,
QPoint, QParallelAnimationGroup, QEasingCurve)
from PyQt5.QtGui import QPainter, QPainterPath, QColor, QPen
from PyQt5.QtWidgets import (QLabel, QWidget, QVBoxLayout, QApplication,
QLineEdit, QPushButton)
class BubbleLabel(QWidget):
BackgroundColor = QColor(195, 195, 195)
BorderColor = QColor(150, 150, 150)
def __init__(self, *args, **kwargs):
text = kwargs.pop("text", "")
super(BubbleLabel, self).__init__(*args, **kwargs)
self.setWindowFlags(
Qt.Window | Qt.Tool | Qt.FramelessWindowHint |
Qt.WindowStaysOnTopHint | Qt.X11BypassWindowManagerHint)
# Set minimum width and height
self.setMinimumWidth(200)
self.setMinimumHeight(58)
self.setAttribute(Qt.WA_TranslucentBackground, True)
layout = QVBoxLayout(self)
# Top left and bottom right margins (16 below because triangles are included)
layout.setContentsMargins(8, 8, 8, 16)
self.label = QLabel(self)
layout.addWidget(self.label)
self.setText(text)
# Get screen height and width
self._desktop = QApplication.instance().desktop()
def setText(self, text):
self.label.setText(text)
def text(self):
return self.label.text()
def stop(self):
self.hide()
self.animationGroup.stop()
self.close()
def show(self):
super(BubbleLabel, self).show()
# Window start position
startPos = QPoint(
self._desktop.screenGeometry().width() - self.width() - 100,
self._desktop.availableGeometry().height() - self.height())
endPos = QPoint(
self._desktop.screenGeometry().width() - self.width() - 100,
self._desktop.availableGeometry().height() - self.height() * 3 - 5)
self.move(startPos)
# Initialization animation
self.initAnimation(startPos, endPos)
def initAnimation(self, startPos, endPos):
# Transparency animation
opacityAnimation = QPropertyAnimation(self, b"opacity")
opacityAnimation.setStartValue(1.0)
opacityAnimation.setEndValue(0.0)
# Set the animation curve
opacityAnimation.setEasingCurve(QEasingCurve.InQuad)
opacityAnimation.setDuration(4000)
# Moving up animation
moveAnimation = QPropertyAnimation(self, b"pos")
moveAnimation.setStartValue(startPos)
moveAnimation.setEndValue(endPos)
moveAnimation.setEasingCurve(QEasingCurve.InQuad)
moveAnimation.setDuration(5000)
# Parallel animation group (the purpose is to make the two animations above simultaneously)
self.animationGroup = QParallelAnimationGroup(self)
self.animationGroup.addAnimation(opacityAnimation)
self.animationGroup.addAnimation(moveAnimation)
# Close window at the end of the animation
self.animationGroup.finished.connect(self.close)
self.animationGroup.start()
def paintEvent(self, event):
super(BubbleLabel, self).paintEvent(event)
painter = QPainter(self)
painter.setRenderHint(QPainter.Antialiasing) # Antialiasing
rectPath = QPainterPath() # Rounded Rectangle
triPath = QPainterPath() # Bottom triangle
height = self.height() - 8 # Offset up 8
rectPath.addRoundedRect(QRectF(0, 0, self.width(), height), 5, 5)
x = self.width() / 5 * 4
triPath.moveTo(x, height) # Move to the bottom horizontal line 4/5
# Draw triangle
triPath.lineTo(x + 6, height + 8)
triPath.lineTo(x + 12, height)
rectPath.addPath(triPath) # Add a triangle to the previous rectangle
# Border brush
painter.setPen(QPen(self.BorderColor, 1, Qt.SolidLine,
Qt.RoundCap, Qt.RoundJoin))
# Background brush
painter.setBrush(self.BackgroundColor)
# Draw shape
painter.drawPath(rectPath)
# Draw a line on the bottom of the triangle to ensure the same color as the background
painter.setPen(QPen(self.BackgroundColor, 1,
Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin))
painter.drawLine(x, height, x + 12, height)
def windowOpacity(self):
return super(BubbleLabel, self).windowOpacity()
def setWindowOpacity(self, opacity):
super(BubbleLabel, self).setWindowOpacity(opacity)
# Since the opacity property is not in QWidget, you need to redefine one
opacity = pyqtProperty(float, windowOpacity, setWindowOpacity)
class TestWidget(QWidget):
def __init__(self, *args, **kwargs):
super(TestWidget, self).__init__(*args, **kwargs)
layout = QVBoxLayout(self)
self.msgEdit = QLineEdit(self, returnPressed=self.onMsgShow)
self.msgButton = QPushButton("Display content", self, clicked=self.onMsgShow)
layout.addWidget(self.msgEdit)
layout.addWidget(self.msgButton)
def onMsgShow(self):
msg = self.msgEdit.text().strip()
if not msg:
return
if hasattr(self, "_blabel"):
self._blabel.stop()
self._blabel.deleteLater()
del self._blabel
self._blabel = BubbleLabel()
self._blabel.setText(msg)
self._blabel.show()
if __name__ == "__main__":
app = QApplication(sys.argv)
w = TestWidget()
w.show()
sys.exit(app.exec_())
There is nothing like that even in Qt 6.
Anyways, you said "but I would rather not have separate windows as popups if possible since even modeless dialog windows would be distracting for the user.".
Yes, there are two things necessary for the toast, and there is a solution.
Should not be a separated window - Qt.SubWindow
self.setWindowFlags(Qt.SubWindow)
Should ignore the mouse event, be unable to focus - Qt.WA_TransparentForMouseEvents
self.setAttribute(Qt.WA_TransparentForMouseEvents, True)
Based on those rules, i made the toast that user can set the text, font, color(text or background) of it.
Here is my repo if you want to check the detail: https://github.com/yjg30737/pyqt-toast

cocos2d process events randomly while animating

This problem is so weird that I think I'm doing a huge mistake somewhere.
I'm using cocos2d in python3. I created a simple example, which basically is a merge of samples/hello_world.py and samples/handling_events.py, and just visualize a moving text while also checking for events.
Problem is: events are processed basically randomly while the animation is in progress: sometimes, pressing ESC, the program stops after few instants, sometimes it doesn't stop. Pressing the keyboard usually doesn't show anything in the text, but if you press a lot of keys maybe you get a couple to get visualized. Same with the mouse: if you move or click a lot, sometimes you see the event processed.
I can't understand what's happening: isn't cocos supposed to process the events before every rendered frame? Am I missing something?
Source code
import cocos
import pyglet
from cocos.actions import *
class HelloWorld(cocos.layer.ColorLayer):
def __init__(self):
super(HelloWorld, self).__init__(64, 64, 200, 255)
label = cocos.text.Label(
'Hello, World!',
font_name='Times New Roman',
font_size=32,
anchor_x='center',
anchor_y='center')
label.position = (320, 240)
# label.scale = 2 # Pixel scale
self.add(label)
scale = ScaleBy(3, duration=2)
# Unlimited repeat
#label.do(Repeat(scale + Reverse(scale)))
# Limited repeat
label.do((scale + Reverse(scale)) * 3)
class KeyDisplay(cocos.layer.Layer):
# This is necessary to receive events
is_event_handler = True
def __init__(self):
super(KeyDisplay, self).__init__()
self.text = cocos.text.Label('', x=100, y=200)
self.down_keys_ = set()
self.update_text()
self.add(self.text)
def update_text(self):
key_names = [pyglet.window.key.symbol_string(k) for k in self.down_keys_]
self.text.element.text = 'Keys: ' + ' '.join(key_names)
def on_key_press(self, key, modifiers):
self.down_keys_.add(key)
self.update_text()
def on_key_release(self, key, modifiers):
self.down_keys_.remove(key)
self.update_text()
class MouseDisplay(cocos.layer.Layer):
is_event_handler = True
def __init__(self):
super(MouseDisplay, self).__init__()
self.text = cocos.text.Label('', x=100, y=150)
self.add(self.text)
def on_mouse_motion(self, x, y, dx, dy):
self.text.element.text = 'Moving'
def on_mouse_drag(self, x, y, dx, dy, buttons, modifiers):
self.text.element.text = 'Dragging'
def on_mouse_press(self, x, y, buttons, modifiers):
self.text.element.text = 'Pressed'
def on_mouse_release(self, x, y, buttons, modifiers):
self.text.element.text = 'Released'
if __name__ == '__main__':
cocos.director.director.init()
# A layer with text
hello_layer = HelloWorld()
hello_layer.do(RotateBy(360 * 2, duration=5))
# A layer for key
key_layer = KeyDisplay()
# A layer for mouse
mouse_layer = MouseDisplay()
main_scene = cocos.scene.Scene(hello_layer, key_layer, mouse_layer)
cocos.director.director.run(main_scene)
This issue was due to a bug in pyglet.
For details:
https://groups.google.com/forum/#!topic/cocos-discuss/6uviIoGzII8
https://groups.google.com/forum/#!topic/pyglet-users/uG_qgulsixI

Resources