QHeaderView going out of viewport - python-3.x

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()

Related

Using USB port data in 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()

Kivy Infinite Looping ScrollView

I am trying to create a ScrollView in Kivy with widgets that loop infinitely like in this demo I found online:
I have tried to remove widgets outside the ScrollView's bounds and add them back to the top or bottom, depending on their original location. However, this made the whole app flicker and glitch and ultimately did not work. If anyone has a method of achieving this, it would be greatly appreciated!
I created a custom class based on kivy's Stencilview and RelativeLayout.
It's used pretty much like kivy.uix.recycleview and supports looping. It's somewhat unoptimized but works pretty well. No x-axis scrolling (yet). Should be capable of processing an infinite amount of data without any performance loss. (Tested with 10.000.000 data entries => no lag)).
please don't quote me on anything inside on_touch_down, on_touch_move, on_touch_up, or any function called inside it (the only exception is scroll_y) cause I have no idea how they work.
from functools import partial
from kivy.app import App
from kivy.clock import Clock
from kivy.compat import iteritems
from kivy.lang import Builder
from kivy.metrics import sp
from kivy.properties import DictProperty, ListProperty, NumericProperty, BooleanProperty, ObjectProperty
from kivy.uix.label import Label
from kivy.uix.relativelayout import RelativeLayout
from kivy.uix.stencilview import StencilView
from kivy.uix.widget import Widget
class LoopEntry(Widget):
data_index = NumericProperty(0)
data = DictProperty(None, allow_none=True)
hidden = BooleanProperty(False)
def is_hidden(self):
"""
:return:
"""
return self.hidden
def hide(self):
"""
:return:
"""
self.opacity = 0
self.hidden = True
def show(self):
"""
:return:
"""
self.opacity = 1
self.hidden = False
def update(self, data):
"""
overwrite this function if values other than attributes are needed
:param data:
:return:
"""
assert isinstance(data, dict)
# assign data
self.data = data
# apply values
for key, value in iteritems(data):
setattr(self, key, value)
class LoopContainer(RelativeLayout, StencilView):
pass
class LoopContainerDebug(RelativeLayout):
pass
class LoopScrollView(RelativeLayout):
"""
Main data source. Contains the data that needs to be exchanged.
Data can be manipulated without a whole redraw. Might cause unwanted behaviour
like blank lines and incorrect orders if not properly updated.
Setting this value causes a complete refresh.
"""
data = ListProperty()
"""
Children height. All children need to be the same height else
unwanted behaviour might occur.
Altering this value causes a complete refresh.
"""
children_height = NumericProperty(44)
"""
Amount of widgets added to the minimum widgets. (Very) Big numbers may cause lag.
Altering this value causes a complete refresh
"""
protection_amount = NumericProperty(4)
"""
viewclass is used to set the class type the widgets should be
future version might support manual adding of widgets
Altering this value causes a complete refresh.
"""
viewclass = ObjectProperty(LoopEntry)
"""
controls looping behaviour.
"""
loop = BooleanProperty(True)
"""
debugging option. shows hidden entries. not possible to switch
while running (yet)
"""
debug = BooleanProperty(False)
"""
scroll timeout. If the mouse has not been moved 'scroll_distance' within this time, dispatch the touch to children
in milliseconds
"""
scroll_timeout = NumericProperty(200)
"""
touch distance. Distance mouse needs to be moved
in pixel
"""
scroll_distance = NumericProperty('20dp')
def __init__(self, **kwargs):
"""
:param kwargs:
"""
# minimum widgets controls min/max amount of widgets on screen. Readonly.
self.__minimum_widgets = 0
# controls overscroll blocking if loop is disabled
self.__overscroll_block_y = "free"
# container
_kwargs = {
"size_hint": (None, None),
"size": (0, 0)
}
self.container = LoopContainer(**_kwargs) if not kwargs.get('debug', False) else LoopContainerDebug(**_kwargs)
# init super values
super(LoopScrollView, self).__init__(**kwargs)
# add container
self.add_widget(self.container)
# create widgets
self.__create_widgets()
# no idea
self._drag_touch = None
def on_pos(self, widget, value) -> None:
"""
:param widget:
:param value:
:return:
"""
def on_size(self, widget, value) -> None:
"""
:param widget:
:param value:
:return:
"""
# set container size
self.container.size = self.size
# recreate widgets
self.__create_widgets()
def on_data(self, widget, value) -> None:
"""
called if new data is set. Forces complete refresh. Use with care.
:param widget: widget event belongs to
:param value: event value
:return: None
"""
self.__create_widgets()
def on_protection_amount(self, widget, value) -> None:
"""
Forces complete refresh. Use with care.
:param widget: widget event belongs to
:param value: event value
:return: None
"""
self.__create_widgets()
def on_viewclass(self, widget, value) -> None:
"""
sets the viewclass used to entries
Forces complete refresh
:param widget: widget
:param value: value
:return: None
"""
self.__create_widgets()
def on_children_height(self, widget, value) -> None:
"""
Changes the children height.
Forces complete refresh.
:param widget: widget
:param value: value
:return: None
"""
self.__create_widgets()
def __create_widgets(self) -> None:
"""
clear all widgets and recreate
:return: None
"""
# remove all widgets
self.container.clear_widgets()
# calculate the minimum amount of required widgets
self.minimum_widgets = round(self.height / self.children_height) + self.protection_amount
# adding entries to the stencil view in reversed order to start with the smallest value (index 0) at top
for entry in range(self.minimum_widgets, 0, -1):
# create widget instance
_tmp_entry = self.viewclass(
size_hint=(1, None),
height=self.children_height,
pos=(0, self.height - self.children_height * entry)
)
# add to container
self.container.add_widget(_tmp_entry)
# refresh all widgets from given index and apply data values
self.__refresh_from_index(0)
def __refresh_from_index(self, index=0) -> None:
"""
refreshes widgets from given index
:param index: index to start with (very top entry)
:return: None
"""
# return if data is empty
if not self.data:
return
# reset widget positions to prevent weird behaviour
self.__reset_widget_positions(brute=True)
# reduce overhead. Slightly.
_data_length = len(self.data)
# loop through children and set values from given index
for child in self.container.children:
# if the current index exceeds the lengths and looping is disabled hide the widget
if index >= _data_length and not self.loop:
# Note : I dislike direct changing of values -_-
if not child.is_hidden():
child.hide() # hide child
child.data_index = index
else:
_normalized_index = index % _data_length
# get the new data value for the widget
_data_value = self.data[_normalized_index]
child.update(_data_value)
child.data_index = _normalized_index
if child.is_hidden():
child.show()
# increase index
index += 1
def __reset_widget_positions(self, brute=False) -> None:
"""
resets widget positions. Does not take into account values or
value positions.
If brute is True positions will be reset forcefully meaning data may mix up.
If brute is False positions will be scrolled meaning values will remain ordered.
:param brute: boolean
:return: None
"""
if brute:
# forcefully reset children
for child in self.container.children:
child.y = self.height - (child.height * (self.get_child_index(child) + 1))
else:
# get top child
_top_child = self.container.children[0]
# loop until child's y value matches the top threshold
while _top_child.y != self.height - _top_child.height:
# scroll by up to ensure proper order
self.scroll_y(1)
def __trigger_overscroll(self, entry: (LoopEntry, None), state):
"""
:param entry:
:param state:
:return:
"""
# trigger overscroll for down
if state == "bottom" and entry is not None:
# reset child to a proper spot
while entry.y != 0:
# scroll in the fastest direction
self.scroll_y(1 if entry.y < 0 else -1)
# set overscroll AFTER scrolling
self.__overscroll_block_y = "bottom"
# for up
elif state == "top" and entry is not None:
# reset child to proper spot
while entry.y != self.height - entry.height:
self.scroll_y(1 if entry.y < self.height - entry.height else 1)
# set overscroll AFTER scrolling
self.__overscroll_block_y = "top"
# reset else
else:
# free scrolling
self.__overscroll_block_y = "free"
def __update_entry(self, entry: LoopEntry, direction) -> None:
"""
:param entry:
:param direction:
:return:
"""
# get data length
_data_length = len(self.data)
# check direction
if direction == "down":
# get new index
_data_index = entry.data_index + self.minimum_widgets
if self.loop:
# normalize data index
_normalized_data_index = _data_index % _data_length
# update entry
entry.update(self.data[_normalized_data_index])
# set data index
entry.data_index = _normalized_data_index
# show entry
if entry.is_hidden():
entry.show()
else:
# if loop is disabled and data index exceeds either direction
if _data_index >= _data_length or _data_index < 0:
# hide children
if not entry.is_hidden():
entry.hide()
else:
# update entry from data index
entry.update(self.data[_data_index])
# show entry
if entry.is_hidden():
entry.show()
# set data index
entry.data_index = _data_index
elif direction == "up":
# get new data index
_data_index = entry.data_index - self.minimum_widgets
# if looping is enabled
if self.loop:
# normalize index
_normalized_data_index = _data_index % _data_length
# update entry
entry.update(self.data[_normalized_data_index])
# set data index
entry.data_index = _normalized_data_index
# show entry
if entry.is_hidden():
entry.show()
else:
if _data_index < 0 or _data_index >= _data_length:
# hide children
if not entry.is_hidden():
entry.hide()
else:
# update entry from data index
entry.update(self.data[_data_index])
# show entry
if entry.is_hidden():
entry.show()
# set data index
entry.data_index = _data_index
else:
# error
raise Exception
def get_child_index(self, child) -> (int, None):
"""
returns the index if the child exists in list else None
:param child: child instance
:return: int,None
"""
return self.container.children.index(child) if child in self.container.children else None
def scroll_y(self, delta_y) -> None:
"""
scroll by given amount
:param delta_y: delta value in pixels
:return: None
"""
# set highest and lowest children (needed for rotation)
_highest, _lowest = self.container.children[0], self.container.children[0]
# round delta y
delta_y = round(delta_y)
# get data length
data_length = len(self.data)
# control var
_free_block = True
# loop through children
for child in self.container.children:
# increase/decrease children y position
child.y += delta_y
# update highest and lowest children
_highest = child if child.y > _highest.y else _highest
_lowest = child if child.y < _lowest.y else _lowest
# check for loop condition
if not self.loop:
# if current child's index exceeds or evens data length and is higher than given
# threshold trigger overscroll event for bottom
if child.data_index >= data_length - 1 and child.y >= 0:
self.__trigger_overscroll(child, 'bottom')
_free_block = False
# if current child's index is smaller or evens 0 and is higher than the given
# threshold trigger overscroll event for top
elif child.data_index <= 0 and child.y <= self.height - child.height:
self.__trigger_overscroll(child, 'top')
_free_block = False
# unblock if block is free to be unblocked
if _free_block:
self.__trigger_overscroll(None, 'reset')
# check if swap is needed
if _highest.y > self.height + _highest.height and delta_y > 0:
# set new y for highest if it exceeds max height
_highest.y = _lowest.y - _highest.height
self.__update_entry(_highest, direction="down")
elif _lowest.y < 0 - _lowest.height - _lowest.height and delta_y < 0:
_lowest.y = _highest.y + _highest.height
self.__update_entry(_lowest, direction="up")
def _get_uid(self, prefix='sv'):
return '{0}.{1}'.format(prefix, self.uid)
def on_touch_down(self, touch):
x, y = touch.pos
if not self.collide_point(x, y):
touch.ud[self._get_uid('svavoid')] = True
return super(LoopScrollView, self).on_touch_down(touch)
if self._drag_touch or ('button' in touch.profile and touch.button.startswith('scroll')):
return super(LoopScrollView, self).on_touch_down(touch)
# no mouse scrolling, so the user is going to drag with this touch.
self._drag_touch = touch
uid = self._get_uid()
touch.grab(self)
touch.ud[uid] = {
'mode': 'unknown',
'dx': 0,
'dy': 0
}
Clock.schedule_once(self._change_touch_mode, self.scroll_timeout / 1000.)
return True
def on_touch_move(self, touch):
if self._get_uid('svavoid') in touch.ud or self._drag_touch is not touch:
return super(LoopScrollView, self).on_touch_move(touch) or self._get_uid() in touch.ud
if touch.grab_current is not self:
return True
uid = self._get_uid()
ud = touch.ud[uid]
mode = ud['mode']
if mode == 'unknown':
ud['dx'] += abs(touch.dx)
ud['dy'] += abs(touch.dy)
if ud['dx'] > sp(self.scroll_distance):
mode = 'drag'
if ud['dy'] > sp(self.scroll_distance):
mode = 'drag'
ud['mode'] = mode
if mode == 'drag':
if (touch.dy > 0 and self.__overscroll_block_y == "bottom" or
touch.dy < 0 and self.__overscroll_block_y == "top"):
pass
else:
self.scroll_y(touch.dy)
return True
def on_touch_up(self, touch):
if self._get_uid('svavoid') in touch.ud:
return super(LoopScrollView, self).on_touch_up(touch)
if self._drag_touch and self in [x() for x in touch.grab_list]:
touch.ungrab(self)
self._drag_touch = None
ud = touch.ud[self._get_uid()]
if ud['mode'] == 'unknown':
super(LoopScrollView, self).on_touch_down(touch)
Clock.schedule_once(partial(self._do_touch_up, touch), .1)
else:
if self._drag_touch is not touch:
super(LoopScrollView, self).on_touch_up(touch)
return self._get_uid() in touch.ud
def _do_touch_up(self, touch, *largs):
super(LoopScrollView, self).on_touch_up(touch)
# don't forget about grab event!
for x in touch.grab_list[:]:
touch.grab_list.remove(x)
x = x()
if not x:
continue
touch.grab_current = x
super(LoopScrollView, self).on_touch_up(touch)
touch.grab_current = None
def _change_touch_mode(self, *largs):
if not self._drag_touch:
return
uid = self._get_uid()
touch = self._drag_touch
ud = touch.ud[uid]
if ud['mode'] != 'unknown':
return
touch.ungrab(self)
self._drag_touch = None
touch.push()
touch.apply_transform_2d(self.parent.to_widget)
super(LoopScrollView, self).on_touch_down(touch)
touch.pop()
return
# ------------------ Showcase ------------------ #
from kivy.uix.button import Button
class LoopLabel(LoopEntry, Label):
pass
class LoopButton(LoopEntry, Button):
pass
__style = ("""
<LoopLabel>:
color : 1,1,1
text: "test"
canvas:
Color:
rgb : 1,1,1
Line:
rectangle: (*self.pos,self.width ,self.height )
<LoopContainer,LoopContainerDebug>:
canvas:
Color:
rgb : 1,0,0
Line:
rectangle: (0+1,0+1,self.width - 1,self.height -1 )
width:5
""")
class InfiniteScrollingScrollView(App):
def build(self):
root = RelativeLayout()
root.bind(on_touch_down=lambda x, y: print("-" * 10))
sv = LoopScrollView(
size_hint=(0.5, 0.5), pos_hint={'center': (0.5, 0.5)}, viewclass=LoopLabel, debug=False
)
sv.data = [{'text': str(x)} for x in range(10000000)]
root.add_widget(sv)
return root
if __name__ == "__main__":
Builder.load_string(__style)
InfiniteScrollingScrollView().run()
i think this helpful but not the Answer
and this will help https://youtu.be/l8Imtec4ReQ in minute 59:30
from kivy.app import App
from kivy.uix.label import Label
from kivy.lang.builder import Builder
kv=Builder.load_string('''
ScrollView:
do_scroll_x:False
scroll_type:['bars', 'content']
bar_width:'20dp'
GridLayout:
id:g
cols:1
size_hint:(1,None)
height:self.minimum_height
''')
class a(App):
def build(self):
return kv
def on_start(self):
for i in range(100):self.root.ids.g.add_widget(Label(text=str(i),size_hint=(1, None),size=(1, 20)))
a().run()

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

subclassed QGraphicsItem not getting mouse events when used as module [closed]

This question is unlikely to help any future visitors; it is only relevant to a small geographic area, a specific moment in time, or an extraordinarily narrow situation that is not generally applicable to the worldwide audience of the internet. For help making this question more broadly applicable, visit the help center.
Closed 10 years ago.
I'm using Pyside to create an interactive subclassed QGraphicsView - containing some subclassed QGraphicsItems.
It works well in its own module and receives mouse events.
But when used as a module from another file, and incorporated into another Layout - my mouse events are not being triggered.
However .itemChange is working.
Everything except the mouse events.
I am not using tracking events such as hover. I am using mousePressEvent and mouseReleaseEvent.
I have seen c++ responses about setting "setMouseTracking" but this is for widgets and my QGraphicsItems have been added as items not widgets. So when I call this funcion it tells me it doesn't exist for items. Also that seems to be for hover type of events - which I am not needing.
I believe I am subclassing properly and I pass on the events to the parent class. As I said at the start - my code works fine in a standalone file.
Any ideas what I've forgotten to do ?
Here is the standalone - working program:
Save as test_subclass_module.py
import sys
import weakref
import math
from PySide import QtCore, QtGui
###
class Edge(QtGui.QGraphicsItem):
Type = QtGui.QGraphicsItem.UserType + 2
def __init__(self, sourceNode, destNode):
QtGui.QGraphicsItem.__init__(self)
#
self.sourcePoint = QtCore.QPointF()
self.destPoint = QtCore.QPointF()
self.setAcceptedMouseButtons(QtCore.Qt.NoButton)
self.source = weakref.ref(sourceNode)
self.dest = weakref.ref(destNode)
self.source().addEdge(self)
self.dest().addEdge(self)
self.set_index()
self.adjust()
def type(self):
return Edge.Type
def sourceNode(self):
return self.source()
def setSourceNode(self, node):
self.source = weakref.ref(node)
self.adjust()
def destNode(self):
return self.dest()
def setDestNode(self, node):
self.dest = weakref.ref(node)
self.adjust()
def set_index(self):
self.setToolTip(self.source().label)
def adjust(self):
# do we have a line to draw ?
if self.source() and self.dest():
line = QtCore.QLineF(self.mapFromItem(self.source(), 0, 0), self.mapFromItem(self.dest(), 0, 0))
length = line.length()
if length > 20:
edgeOffset = QtCore.QPointF((line.dx() * 10) / length, (line.dy() * 10) / length)
self.prepareGeometryChange()
self.sourcePoint = line.p1() + edgeOffset
self.destPoint = line.p2() - edgeOffset
else: # want to make sure line not drawn
self.prepareGeometryChange()
self.sourcePoint = self.destPoint
def boundingRect(self):
# do we have a line to draw ?
if not self.source() or not self.dest():
return QtCore.QRectF()
else:
extra = 1
return QtCore.QRectF(self.sourcePoint,
QtCore.QSizeF(self.destPoint.x() - self.sourcePoint.x(),
self.destPoint.y() - self.sourcePoint.y())).normalized().adjusted(-extra, -extra, extra, extra)
def paint(self, painter, option, widget):
if self.source() and self.dest():
# Draw the line itself.
line = QtCore.QLineF(self.sourcePoint, self.destPoint)
if line.length() > 0.0:
painter.setPen(QtGui.QPen(QtCore.Qt.black, 1, QtCore.Qt.SolidLine, QtCore.Qt.RoundCap, QtCore.Qt.RoundJoin))
painter.drawLine(line)
###
class Node(QtGui.QGraphicsItem):
Type = QtGui.QGraphicsItem.UserType + 1
def __init__(self, graphWidget, time, temp, pos):
QtGui.QGraphicsItem.__init__(self)
self.graph = weakref.ref(graphWidget)
self.edgeList = []
self.set_index(pos)
self.newPos = QtCore.QPointF()
self.setFlag(QtGui.QGraphicsItem.ItemIsMovable)
self.setFlag(QtGui.QGraphicsItem.ItemSendsGeometryChanges)
self.setCacheMode(self.DeviceCoordinateCache)
self.setZValue(-1)
#
self.temp = temp
self.time = time
x,y = self.map_temptime_to_pos()
self.setPos(x,y)
self.marker = False
def type(self):
return Node.Type
def addEdge(self, edge):
self.edgeList.append(weakref.ref(edge))
def set_index(self, index):
self.index = index
self.label = "Step %d" % index
self.setToolTip(self.label)
def get_prev_edge(self):
index = 1000
edge = False
for e in self.edgeList:
sn = e().source().index
dn = e().dest().index
if sn < index:
index = sn
edge = e
if dn < index:
index = dn
edge = e
return edge
def get_next_edge(self):
index = -1
edge = False
for e in self.edgeList:
sn = e().source().index
dn = e().dest().index
if sn > index:
index = sn
edge = e
if dn > index:
index = dn
edge = e
return edge
def map_temptime_to_pos(self):
x = self.time * self.graph().graph_width_ratio
y = self.graph().size[3] - self.temp * self.graph().graph_height_ratio
return (x,y)
def boundingRect(self):
adjust = 2.0
return QtCore.QRectF(-10 - adjust, -10 - adjust,
22 + adjust, 23 + adjust)
def paint(self, painter, option, widget):
painter.drawLine(QtCore.QLineF(6,-40,6,-2))
painter.setPen(QtCore.Qt.NoPen)
painter.setBrush(QtCore.Qt.lightGray)
painter.drawEllipse(-10, -10, 20, 20)
gradient = QtGui.QRadialGradient(0, 0, 22)
if option.state & QtGui.QStyle.State_Sunken: # selected
gradient.setColorAt(0, QtGui.QColor(QtCore.Qt.darkGreen).lighter(120))
else:
gradient.setColorAt(1, QtCore.Qt.blue)
painter.setBrush(QtGui.QBrush(gradient))
painter.setPen(QtGui.QPen(QtCore.Qt.black, 0))
painter.drawEllipse(-6, -6, 12, 12)
def itemChange(self, change, value):
if change == QtGui.QGraphicsItem.ItemPositionChange:
for edge in self.edgeList:
edge().adjust()
return QtGui.QGraphicsItem.itemChange(self, change, value)
def mousePressEvent(self, event):
if not self.graph().inhibit_edit:
self.update()
print "Node pressed"
QtGui.QGraphicsItem.mousePressEvent(self, event)
def mouseReleaseEvent(self, event):
if not self.graph().inhibit_edit:
self.update()
print "Node released"
#
QtGui.QGraphicsItem.mouseReleaseEvent(self, event)
###
class GraphWidget(QtGui.QGraphicsView):
def __init__(self):
QtGui.QGraphicsView.__init__(self)
self.size = (-30, 30, 600, 400)
#
scene = QtGui.QGraphicsScene(self)
scene.setItemIndexMethod(QtGui.QGraphicsScene.NoIndex)
scene.setSceneRect(self.size[0],self.size[1],self.size[2],self.size[3])
self.setScene(scene)
self.setCacheMode(QtGui.QGraphicsView.CacheBackground)
self.setRenderHint(QtGui.QPainter.Antialiasing)
self.setTransformationAnchor(QtGui.QGraphicsView.AnchorUnderMouse)
self.setResizeAnchor(QtGui.QGraphicsView.AnchorViewCenter)
#
self.maxtemp = 300
self.maxtime = 160
self.nodecount = 0
self.calc_upper_limits()
#
self.scale(0.8, 0.8)
self.setMinimumSize(600, 400)
self.setWindowTitle(self.tr("Elastic Nodes"))
self.inhibit_edit = False
def calc_upper_limits(self):
self.toptemp = (self.maxtemp / 100 + 1) * 100
self.toptime = (int(self.maxtime) / 30 + 1) * 30
self.graph_width_ratio = float(self.size[2]) /self.toptime
self.graph_height_ratio = float(self.size[3]) / self.toptemp
def add_node(self, time, temp, marker=False, pos=-1):
self.nodecount += 1
scene = self.scene()
# Insert Node into scene
node = Node(self, time, temp, self.nodecount)
scene.addItem(node)
# Insert new edges
nodes = self.get_ordered_nodes()
if len(nodes) > 1:
e = Edge(nodes[-2], node)
scene.addItem(e)
# cleanup edge tooltips
for n in self.get_ordered_nodes():
edges = n.edgeList
for e in edges:
e().set_index()
def get_ordered_nodes(self):
nodes = [item for item in self.scene().items() if isinstance(item, Node)]
nodes.sort(key=lambda n: n.index)
return nodes
def keyPressEvent(self, event):
key = event.key()
if key == QtCore.Qt.Key_Plus:
self.scaleView(1.2)
elif key == QtCore.Qt.Key_Minus:
self.scaleView(1 / 1.2)
else:
QtGui.QGraphicsView.keyPressEvent(self, event)
def mousePressEvent(self, event):
print "GraphWidget mouse"
QtGui.QGraphicsView.mousePressEvent(self, event)
def wheelEvent(self, event):
self.scaleView(math.pow(2.0, -event.delta() / 240.0))
def scaleView(self, scaleFactor):
factor = self.matrix().scale(scaleFactor, scaleFactor).mapRect(QtCore.QRectF(0, 0, 1, 1)).width()
if factor < 0.07 or factor > 100:
return
self.scale(scaleFactor, scaleFactor)
def drawBackground(self, painter, rect):
sceneRect = self.sceneRect()
if __name__ == "__main__":
app = QtGui.QApplication(sys.argv)
widget = GraphWidget()
widget.add_node(0, 25)
widget.add_node(30, 100)
widget.add_node(60, 200)
widget.show()
sys.exit(app.exec_())
Here is the parent program - which does not get the mouse events:
Called test_toplevel.py
# import user interface etc
from PySide import QtCore, QtGui
from test_tabs_ui import Ui_Form
from test_subclass_module import *
import sys
Programs = {"Gen13": {"steps": [[0, 0, 0], [0, 30, 30], [0, 60, 60], [0, 77, 77]]
}}
###-----------------------------------------------------------
### The dialog
class Nusku_tab_Add_kiln(QtGui.QWidget):
""" Create dialog to add/delete kilns from controlled kilns """
def __init__(self, parent=None):
# Get the UI loaded
super(Nusku_tab_Add_kiln, self).__init__(parent)
self.ui = Ui_Form()
self.ui.setupUi(self)
self.current = Programs['Gen13']
# draw program in graphicsView
# swap out the standin
self.ui.graphLayout.removeWidget(self.ui.graphicsView)
self.ui.graphicsView.setParent(None)
self.ui.graphicsView.deleteLater()
self.graph = GraphWidget()
self.ui.graphLayout.addWidget(self.graph)
self.draw_graph()
def choose_program(self):
pass
def draw_graph(self):
graph = self.graph
graph.inhibit_edit = True
steps = self.current['steps']
for s in steps:
print s
graph.add_node(s[1],s[2])
###------------------------------------------------
if __name__ == '__main__':
app = QtGui.QApplication(sys.argv)
prog = Nusku_tab_Add_kiln()
prog.show()
sys.exit(app.exec_())
For sake of completeness. Here is the ui file it imports:
test_tabs_ui.py
# -*- coding: utf-8 -*-
# Form implementation generated from reading ui file 'test_tabs.ui'
#
# Created: Wed Dec 05 15:20:02 2012
# by: pyside-uic 0.2.14 running on PySide 1.1.1
#
# WARNING! All changes made in this file will be lost!
from PySide import QtCore, QtGui
class Ui_Form(object):
def setupUi(self, Form):
Form.setObjectName("Form")
Form.resize(595, 540)
self.verticalLayout = QtGui.QVBoxLayout(Form)
self.verticalLayout.setObjectName("verticalLayout")
self.tabWidget = QtGui.QTabWidget(Form)
self.tabWidget.setObjectName("tabWidget")
self.Tab_Program = QtGui.QWidget()
self.Tab_Program.setObjectName("Tab_Program")
self.verticalLayout_2 = QtGui.QVBoxLayout(self.Tab_Program)
self.verticalLayout_2.setObjectName("verticalLayout_2")
self.frame_graph_status = QtGui.QFrame(self.Tab_Program)
self.frame_graph_status.setFrameShape(QtGui.QFrame.StyledPanel)
self.frame_graph_status.setFrameShadow(QtGui.QFrame.Raised)
self.frame_graph_status.setObjectName("frame_graph_status")
self.horizontalLayout_7 = QtGui.QHBoxLayout(self.frame_graph_status)
self.horizontalLayout_7.setSpacing(0)
self.horizontalLayout_7.setContentsMargins(0, 0, 0, 0)
self.horizontalLayout_7.setObjectName("horizontalLayout_7")
self.frame_program = QtGui.QFrame(self.frame_graph_status)
self.frame_program.setFrameShape(QtGui.QFrame.StyledPanel)
self.frame_program.setFrameShadow(QtGui.QFrame.Raised)
self.frame_program.setObjectName("frame_program")
self.graphLayout = QtGui.QVBoxLayout(self.frame_program)
self.graphLayout.setContentsMargins(-1, 0, -1, 0)
self.graphLayout.setObjectName("graphLayout")
self.graphicsView = QtGui.QGraphicsView(self.frame_program)
self.graphicsView.setObjectName("graphicsView")
self.graphLayout.addWidget(self.graphicsView)
self.horizontalLayout_7.addWidget(self.frame_program)
self.verticalLayout_2.addWidget(self.frame_graph_status)
self.widget_prog = QtGui.QWidget(self.Tab_Program)
self.widget_prog.setObjectName("widget_prog")
self.prog_layout = QtGui.QGridLayout(self.widget_prog)
self.prog_layout.setContentsMargins(4, 4, 4, 4)
self.prog_layout.setSpacing(0)
self.prog_layout.setContentsMargins(0, 0, 0, 0)
self.prog_layout.setObjectName("prog_layout")
self.verticalLayout_2.addWidget(self.widget_prog)
self.tabWidget.addTab(self.Tab_Program, "")
self.Tab_alarms = QtGui.QWidget()
self.Tab_alarms.setObjectName("Tab_alarms")
self.alarms_tab_layout = QtGui.QVBoxLayout(self.Tab_alarms)
self.alarms_tab_layout.setObjectName("alarms_tab_layout")
self.tabWidget.addTab(self.Tab_alarms, "")
self.Tab_settings = QtGui.QWidget()
self.Tab_settings.setObjectName("Tab_settings")
self.settings_tab_layout = QtGui.QVBoxLayout(self.Tab_settings)
self.settings_tab_layout.setObjectName("settings_tab_layout")
self.tabWidget.addTab(self.Tab_settings, "")
self.Tab_pid = QtGui.QWidget()
self.Tab_pid.setObjectName("Tab_pid")
self.verticalLayout_8 = QtGui.QVBoxLayout(self.Tab_pid)
self.verticalLayout_8.setSpacing(0)
self.verticalLayout_8.setContentsMargins(0, 0, 0, 0)
self.verticalLayout_8.setObjectName("verticalLayout_8")
self.scrollArea_2 = QtGui.QScrollArea(self.Tab_pid)
self.scrollArea_2.setWidgetResizable(True)
self.scrollArea_2.setObjectName("scrollArea_2")
self.scrollAreaWidgetContents_2 = QtGui.QWidget()
self.scrollAreaWidgetContents_2.setGeometry(QtCore.QRect(0, 0, 569, 494))
self.scrollAreaWidgetContents_2.setObjectName("scrollAreaWidgetContents_2")
self.PID_tab_layout = QtGui.QVBoxLayout(self.scrollAreaWidgetContents_2)
self.PID_tab_layout.setSpacing(0)
self.PID_tab_layout.setContentsMargins(0, 0, 0, 0)
self.PID_tab_layout.setObjectName("PID_tab_layout")
self.scrollArea_2.setWidget(self.scrollAreaWidgetContents_2)
self.verticalLayout_8.addWidget(self.scrollArea_2)
self.tabWidget.addTab(self.Tab_pid, "")
self.verticalLayout.addWidget(self.tabWidget)
self.retranslateUi(Form)
self.tabWidget.setCurrentIndex(0)
QtCore.QMetaObject.connectSlotsByName(Form)
Form.setTabOrder(self.tabWidget, self.graphicsView)
Form.setTabOrder(self.graphicsView, self.scrollArea_2)
def retranslateUi(self, Form):
Form.setWindowTitle(QtGui.QApplication.translate("Form", "Form", None, QtGui.QApplication.UnicodeUTF8))
self.graphicsView.setToolTip(QtGui.QApplication.translate("Form", "decimal point", None, QtGui.QApplication.UnicodeUTF8))
self.graphicsView.setStatusTip(QtGui.QApplication.translate("Form", "Accuracy can be increased at lower temperatures", None, QtGui.QApplication.UnicodeUTF8))
self.tabWidget.setTabText(self.tabWidget.indexOf(self.Tab_Program), QtGui.QApplication.translate("Form", "Program", None, QtGui.QApplication.UnicodeUTF8))
self.tabWidget.setTabText(self.tabWidget.indexOf(self.Tab_alarms), QtGui.QApplication.translate("Form", "Alarms", None, QtGui.QApplication.UnicodeUTF8))
self.tabWidget.setTabText(self.tabWidget.indexOf(self.Tab_settings), QtGui.QApplication.translate("Form", "Settings", None, QtGui.QApplication.UnicodeUTF8))
self.tabWidget.setTabText(self.tabWidget.indexOf(self.Tab_pid), QtGui.QApplication.translate("Form", "PID", None, QtGui.QApplication.UnicodeUTF8))
because there is an answer - which reasonably asks me to delete my answer - I have to answer the question with my own answer to close it. After waiting 3 days... hopefully I can delete it - but probably not as it has an answer..
I downloaded and tried your application and saw it more or less working
(although the cursor position gets wrong in test_toplevel.py).
After removing graph.inhibit_edit = False also the cursor stays right.
So is the problem already solved? Then it would be very helpful to remove your
question or mak it as resolved. Or please clarify the question.

How can I align a right-click context menu properly in PyQt?

With the sample code below (heavily influenced from here) the right-click context menu is not really aligned properly.
As can be seen in the screenshot, the resulting menu is above the mouse cursor quite a bit. I would expect the menu's top left corner to be exactly aligned with the mouse pointer.
Is there any way to adjust for this?
import re
import operator
import os
import sys
import sqlite3
import cookies
from PyQt4.QtCore import *
from PyQt4.QtGui import *
def main():
app = QApplication(sys.argv)
w = MyWindow()
w.show()
sys.exit(app.exec_())
class MyWindow(QWidget):
def __init__(self, *args):
QWidget.__init__(self, *args)
self.tabledata = [('apple', 'red', 'small'),
('apple', 'red', 'medium'),
('apple', 'green', 'small'),
('banana', 'yellow', 'large')]
self.header = ['fruit', 'color', 'size']
# create table
self.createTable()
# layout
layout = QVBoxLayout()
layout.addWidget(self.tv)
self.setLayout(layout)
def popup(self, pos):
for i in self.tv.selectionModel().selection().indexes():
print i.row(), i.column()
menu = QMenu()
quitAction = menu.addAction("Quit")
action = menu.exec_(self.mapToGlobal(pos))
if action == quitAction:
qApp.quit()
def createTable(self):
# create the view
self.tv = QTableView()
self.tv.setStyleSheet("gridline-color: rgb(191, 191, 191)")
self.tv.setContextMenuPolicy(Qt.CustomContextMenu)
self.tv.customContextMenuRequested.connect(self.popup)
# set the table model
tm = MyTableModel(self.tabledata, self.header, self)
self.tv.setModel(tm)
# set the minimum size
self.tv.setMinimumSize(400, 300)
# hide grid
self.tv.setShowGrid(True)
# set the font
font = QFont("Calibri (Body)", 12)
self.tv.setFont(font)
# hide vertical header
vh = self.tv.verticalHeader()
vh.setVisible(False)
# set horizontal header properties
hh = self.tv.horizontalHeader()
hh.setStretchLastSection(True)
# set column width to fit contents
self.tv.resizeColumnsToContents()
# set row height
nrows = len(self.tabledata)
for row in xrange(nrows):
self.tv.setRowHeight(row, 18)
# enable sorting
self.tv.setSortingEnabled(True)
return self.tv
class MyTableModel(QAbstractTableModel):
def __init__(self, datain, headerdata, parent=None, *args):
""" datain: a list of lists
headerdata: a list of strings
"""
QAbstractTableModel.__init__(self, parent, *args)
self.arraydata = datain
self.headerdata = headerdata
def rowCount(self, parent):
return len(self.arraydata)
def columnCount(self, parent):
return len(self.arraydata[0])
def data(self, index, role):
if not index.isValid():
return QVariant()
elif role != Qt.DisplayRole:
return QVariant()
return QVariant(self.arraydata[index.row()][index.column()])
def headerData(self, col, orientation, role):
if orientation == Qt.Horizontal and role == Qt.DisplayRole:
return QVariant(self.headerdata[col])
return QVariant()
def sort(self, Ncol, order):
"""Sort table by given column number.
"""
self.emit(SIGNAL("layoutAboutToBeChanged()"))
self.arraydata = sorted(self.arraydata, key=operator.itemgetter(Ncol))
if order == Qt.DescendingOrder:
self.arraydata.reverse()
self.emit(SIGNAL("layoutChanged()"))
if __name__ == "__main__":
main()
the position is in viewport coordinate, so if you are using
self.tableView.setContextMenuPolicy(Qt.CustomContextMenu)
so you don't have event passed to popup, you can do the following
action = menu.exec_(self.tableView.viewport().mapToGlobal(pos))
instead.
This was a bit tricky, but following the subclassing example in this wiki example and replacing
15 action = menu.exec_(self.mapToGlobal(event.pos()))
with
15 action = menu.exec_(event.globalPos())
will make the popup menu's top left corner match the mouse click exactly.
This will work for maximized/minified windows.
Menu will be generated at right-bottom position of mouse.
menu.exec_(self.mapToGlobal(self.mapFromGlobal(QtGui.QCursor.pos())))

Resources