QTreeWidget and QPainter combination - python-3.x

I am a bit lost with the QTreeWidget and I were not able to suck the relevant information from the found topics (like: how to set QTreeView background image with QStyle::StandardPixmap in stylesheet method? or Python: PyQt QTreeview example - selection or Styling Qt QTreeView with CSS or Displaying tabular data in Qt5 ModelViews).
I have two files, one is the gui, one is the working class:
gui:
class Ui_Dialog(object):
def setupUi(self, Dialog):
Dialog.setObjectName("Dialog")
self.tree = QtWidgets.QTreeWidget(Dialog)
self.tree.setGeometry(QtCore.QRect(10, 60, 760, 480))
self.tree.setHeaderLabels(['circ', 'state', 'test'])
self.tree.setSortingEnabled(True)
worker:
class AppWindow(QDialog):
def __init__(self, fullscreen=False):
super().__init__()
self.ui = Ui_Dialog()
self.ui.setupUi(self)
self.show()
self.timer = QTimer()
self.timer.setInterval(500)
self.timer.timeout.connect(self.refresh_gui)
self.timer.start()
def refresh_gui(self):
self.painter = QPainter(self)
tmp = {0: {"state": 1, "info": "hello"}, 1: {"state": 0, "info": "world"}}
for i in tmp:
if tmp[i]["state"] == 0:
painter.setPen(QPen(Qt.red, 8, Qt.SolidLine))
else:
painter.setPen(QPen(Qt.green, 8, Qt.SolidLine))
circ = painter.drawEllipse(2,2,20,20)
item = QtWidgets.QTreeWidgetItem(self.ui.tree, [circ, tmp[i]["state"], "empty"])
item.setText(2, "circ painted")
I want to achive, that if state == 0 that a red circle is shown in the first column anf if state == 1 a green one. I do not know how to hand the QTreeWidgetItem a PyQt5.QtGui.QPainter object instead of a string.
Also, I do get the error:
QStandardPaths: XDG_RUNTIME_DIR not set, defaulting to '/tmp/runtime-root'
QWidget::paintEngine: Should no longer be called
QPainter::begin: Paint device returned engine == 0, type: 1
QPainter::setPen: Painter not active
QPainter::end: Painter not active, aborted
and some lines lower (because of it): QPainter::setPen: Painter not active
because I call self.painter = QPainter(self) which is discussed in this git issue from matplotlib but I fail to fix it in my code. I found this QPainter tutorial which paints on a QPixmap, which also works for me but is not what I am looking for here.

The painting of a widget is not done in any method, but the paintEvent method must be overridden, which is called whenever it is necessary by Qt or using update or repaint by the developer. But in the case of classes that handle a model (that is, a lot of organized information) that inherit from QAbstractItemView, a delegate must be used if an item is going to be painted.
Considering the above, the solution is:
class Delegate(QtWidgets.QStyledItemDelegate):
def paint(self, painter, option, index):
state = index.data(QtCore.Qt.UserRole)
color = (
QtGui.QColor(QtCore.Qt.red) if state == 0 else QtGui.QColor(QtCore.Qt.green)
)
painter.setPen(QtGui.QPen(color, 4, QtCore.Qt.SolidLine))
diameter = min(option.rect.width(), option.rect.height())
rect = QtCore.QRect(0, 0, diameter // 2, diameter // 2)
rect.moveCenter(option.rect.center())
painter.drawEllipse(rect)
class AppWindow(QtWidgets.QDialog):
def __init__(self, parent=None):
super().__init__(parent)
self.ui = Ui_Dialog()
self.ui.setupUi(self)
self.show()
delegate = Delegate(self.ui.tree)
self.ui.tree.setItemDelegateForColumn(0, delegate)
self.timer = QtCore.QTimer(interval=500, timeout=self.refresh_gui)
self.timer.start()
#QtCore.pyqtSlot()
def refresh_gui(self):
tmp = [{"state": 1, "info": "hello"}, {"state": 0, "info": "world"}]
for d in tmp:
item = QtWidgets.QTreeWidgetItem(self.ui.tree, ["", str(d["state"]), "empty"])
item.setData(0, QtCore.Qt.UserRole, d["state"])
item.setText(2, "circ painted")

Related

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

wxPython TreeCtrl very slow with millions of nodes (on multi-select tree control)

I am creating a tree with millions of nodes, but when I switched to using multiple-select on a tree control (wx.TR_MULTIPLE), actions on tree become slower, I only click to select a node and it takes me a few seconds. This does not happen when I use the single-select style (wx.TR_SINGLE).
I have tried to not set data for any node and did not use any event but it still slow.
Is there any way to use multiple-select on a tree control and the tree still fast as single-select?
I've pasted the modified demo code in below:
import wx
class MyTree(wx.TreeCtrl):
def __init__(self, parent, id, pos, size, style):
wx.TreeCtrl.__init__(self, parent, id, pos, size, style)
self.Bind(wx.EVT_TREE_SEL_CHANGED, self.item_changed)
def item_changed(self, evt):
print(self.GetItemData(evt.GetItem()))
class TreePanel(wx.Panel):
def __init__(self, parent):
wx.Panel.__init__(self, parent)
self.tree = MyTree(self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TR_HAS_BUTTONS | wx.TR_MULTIPLE)
self.root = self.tree.AddRoot('ROOT')
node1 = self.tree.InsertItem(self.root, 0, 'Node 1', data='node 1')
for i in range(1000000):
self.tree.PrependItem(node1, 'Sub node 1: ' + str(i), data='Sub node 1: ' + str(i))
self.tree.Expand(self.root)
sizer = wx.BoxSizer(wx.VERTICAL)
sizer.Add(self.tree, 1, wx.EXPAND)
self.SetSizer(sizer)
class MainFrame(wx.Frame):
def __init__(self):
wx.Frame.__init__(self, parent=None, title='TreeCtrl Demo')
panel = TreePanel(self)
self.Show()
if __name__ == '__main__':
app = wx.App(redirect=False)
frame = MainFrame()
app.MainLoop()
I also have the same problems. But when I change to the single choice it becomes fast again, but at this time we can not choose multiple node... hmmm.
I think this related with C-code inside the framework

How to detect mouse position on custom CellRenderer

It makes a while that I've been struggling to detect the mouse coordinates over a custom Gtk.CellRenderer.
I've been reading some good docs like:
RedHat doc
lazka.github
But I still not sure of how to do it. I've tried many things like trying to connect a signal with __gsignals__ or by trying to use the virtual methods.
I need to do this because I'm building a rating widget. The raiting widget contain starts like: ★★★★★ and I'd like that when the user pases the mouse over, the relative x position modifies the number of starts.
I also would like to connect a clicked signal in order to save the number of starts that the user selected.
Here below is my current working code. Any help on this is highly appreciated!
import gi
gi.require_version('Gtk', '3.0')
gi.require_version('PangoCairo', '1.0')
from gi.repository import Gtk, Gdk, cairo, Pango, PangoCairo, GObject
class CellRendererRating(Gtk.CellRenderer):
""" Cellrenderer to display ratings from 0 to 5: ★★★★★, ★★★☆☆, etc """
__gproperties__ = {
'rating': ( int, # type
"integer prop", # nick
"A property that contains an integer", # blurb
0, # min
5, # max
0, # default
GObject.PARAM_READWRITE # flags
),
}
def __init__(self):
super().__init__()
self.font_size=15
self.font="Sans Bold {}".format(self.font_size)
self.rating = 0
def activate(event, widget, path, background_area, cell_area, flags):
print(path)
def do_set_property(self, pspec, value):
setattr(self, pspec.name, value)
def do_get_property(self, pspec):
return getattr(self, pspec.name)
def do_get_size(self, widget, cell_area):
return (0, 0, self.font_size*5, self.font_size+5)
def do_start_editing(event, widget, path, background_area, cell_area, flags):
print('called')
def do_render(self, cr, treeview, background_area, cell_area, flags):
cr.translate (0, 0)
layout = PangoCairo.create_layout(cr)
#layout.set_font_description(FONT_CELLRATING_DESCRIPTION)
if 'GTK_CELL_RENDERER_FOCUSED' in str(flags) and self.rating < 5:
for i in range(5):
if i < self.rating:
layout.set_text("★", -1)
else:
layout.set_text("☆", -1)
cr.save()
PangoCairo.update_layout (cr, layout)
cr.move_to (cell_area.x+i*(self.font_size+1), cell_area.y)
PangoCairo.show_layout (cr, layout)
cr.restore()
else:
for i in range(self.rating):
layout.set_text("★", -1)
cr.save()
PangoCairo.update_layout (cr, layout)
cr.move_to (cell_area.x+i*(self.font_size+1), cell_area.y)
PangoCairo.show_layout (cr, layout)
cr.restore()
GObject.type_register(CellRendererRating)
if __name__ == '__main__':
class Window(Gtk.Window):
def __init__(self):
Gtk.Window.__init__(self)
self.connect('destroy', self.on_quit)
liststore = Gtk.ListStore(int)
for i in range(6):
liststore.append([i])
treeview = Gtk.TreeView(liststore)
treeviewcolumn = Gtk.TreeViewColumn("Rating")
treeviewcolumn.set_resizable(True)
cellrenderer = CellRendererRating()
treeviewcolumn.pack_start(cellrenderer, True)
treeviewcolumn.add_attribute(cellrenderer, 'rating', 0)
treeview.append_column(treeviewcolumn)
self.add(treeview)
self.show_all()
def on_quit(self, widget, data=None):
Gtk.main_quit()
w = Window()
Gtk.main()
It is possible to get the position of the cursor on a CellRenderer by doing:
def do_render(self, cr, treeview, background_area, cell_area, flags):
mouse_x, mouse_y = treeview.get_pointer()
cell_render_x = mouse_x - cell_area.x
cell_render_y = mouse_y - cell_area.y
Yet this might not be the optimal solution for your issue as there is already a Rating-CellRenderer that does exactly what you're trying to do namely RB.CellRendererRating. I would recommend using their implementation, either as is or as the basis for your custom version.
RB.CellRendererRating.new()
Create a cell renderer that will display some pixbufs for representing the rating of a song. It is also able to update the rating.
Source of RB.CellRendererRating

How to make one tab bar in QTabWidget expendable?

I need to customize my QTabWidget so that one of its tab bars (lets say there are 4 tabs overall) has expanding property and will fill the remaining space between other tabs. Any ideas?
you can subclass QTabBar, set it to the width of your tabwidget (the height depends on the fontsize) and overwrite tabSizeHint():
class tabBar(QTabBar):
def __init__(self, width, height, parent=None):
QTabBar.__init__(self, parent)
self.setFixedSize(width, height)
def tabSizeHint(self, i):
f = 3 # Tab3 shall be f times wider then the other tabs
tw = int(self.width()/(self.count() + f -1)) # width per Tab
if i == 2: # Tab3
# return QSize(tw*f, self.height()) edited -> rounding error possible
return QSize(self.width() - (self.count() - 1)*tw, self.height())
return QSize(tw, self.height()) # all other tabs
and set this tabBar to your tabwidget:
tb = tabBar(tabWidget.width(), 34) # tabBars height depends on fontSize
tabwidget..setTabBar(tb)
looks like this:
edit:
if the tabWidget is resized, a resizeEvent() occurs. In this moment the tabWidget already has its new size and is repainted immediatedly after the resizeEvent(),
see QT-Doc QTabWidget.resizeEvent
So if the width() of the tabBar is adapted in resizeEvent(), the tabBar will always have the same width as the tabwidget. Because the tabSizeHint() depends on the width, all tabs will have the correct width too. So You can subclass QTabWidget() and overwrite resizeEvent() for a dynamical solution:
class tabWidget(QTabWidget):
def __init__(self, parent=None):
QTabWidget.__init__(self, parent)
def resizeEvent(self, event):
self.tabBar().setFixedWidth(self.width())
QTabWidget.resizeEvent(self, event)
To do this correctly, it's necessary to work backwards from the existing sizes of the tabs. This is because the tab sizes are affected by the current style, and by other features such as tab close buttons. It's also important to set a minimum size for the tab which is exandable (otherwise it could be resized to nothing).
Here is a simple demo that does all that:
from PyQt4 import QtCore, QtGui
class TabBar(QtGui.QTabBar):
def __init__(self, expanded=-1, parent=None):
super(TabBar, self).__init__(parent)
self._expanded = expanded
def tabSizeHint(self, index):
size = super(TabBar, self).tabSizeHint(index)
if index == self._expanded:
offset = self.width()
for index in range(self.count()):
offset -= super(TabBar, self).tabSizeHint(index).width()
size.setWidth(max(size.width(), size.width() + offset))
return size
class TabWidget(QtGui.QTabWidget):
def __init__(self, expanded=-1, parent=None):
super(TabWidget, self).__init__(parent)
self.setTabBar(TabBar(expanded, self))
def resizeEvent(self, event):
self.tabBar().setMinimumWidth(self.width())
super(TabWidget, self).resizeEvent(event)
class Window(QtGui.QWidget):
def __init__(self):
super(Window, self).__init__()
self.tabs = TabWidget(2, self)
layout = QtGui.QVBoxLayout(self)
layout.addWidget(self.tabs)
for text in 'One Two Three Four'.split():
self.tabs.addTab(QtGui.QWidget(self), text)
if __name__ == '__main__':
import sys
app = QtGui.QApplication(sys.argv)
window = Window()
window.setGeometry(500, 300, 400, 200)
window.show()
sys.exit(app.exec_())

PyQt: QGraphicsItem added at a wrong position

I have a subclass of QGraphicsItem and I want to add instances of it to the scene on 'Control+LMB click'. The trouble is that the item is added at the position with coordinates that are two times larger than they should be. At the same time adding ellipses with scene.addEllipse(...) works fine.
#!/usr/bin/env python
import sys
from PyQt4.QtCore import (QPointF, QRectF, Qt, )
from PyQt4.QtGui import (QApplication, QMainWindow, QGraphicsItem,
QGraphicsScene, QGraphicsView, QPen, QStyle)
MapSize = (512, 512)
class DraggableMark(QGraphicsItem):
def __init__(self, position, scene):
super(DraggableMark, self).__init__(None, scene)
self.setFlags(QGraphicsItem.ItemIsSelectable | QGraphicsItem.ItemIsMovable)
self.rect = QRectF(position.x(), position.y(), 15, 15)
self.setPos(position)
scene.clearSelection()
def boundingRect(self):
return self.rect
def paint(self, painter, option, widget):
pen = QPen(Qt.SolidLine)
pen.setColor(Qt.black)
pen.setWidth(1)
if option.state & QStyle.State_Selected:
pen.setColor(Qt.blue)
painter.setPen(pen)
painter.drawEllipse(self.rect)
class GraphicsScene(QGraphicsScene):
def __init__(self, parent=None):
super(GraphicsScene, self).__init__(parent)
self.setSceneRect(0, 0, *MapSize)
def mousePressEvent(self, event):
super(GraphicsScene, self).mousePressEvent(event)
if event.button() != Qt.LeftButton:
return
modifiers = QApplication.keyboardModifiers()
pos = event.scenePos()
if modifiers == Qt.ControlModifier:
print("Control + Click: (%d, %d)" % (pos.x(), pos.y()))
DraggableMark(pos, self)
self.addEllipse(QRectF(pos.x(), pos.y(), 10, 10))
else:
print("Click: (%d, %d)" % (pos.x(), pos.y()))
class MainWindow(QMainWindow):
def __init__(self, parent=None):
super(MainWindow, self).__init__(parent)
self.scene = GraphicsScene(self)
self.scene.addRect(QRectF(0, 0, *MapSize), Qt.red)
self.view = QGraphicsView()
self.view.setScene(self.scene)
self.view.resize(self.scene.width(), self.scene.height())
self.setCentralWidget(self.view)
if __name__ == "__main__":
app = QApplication(sys.argv)
window = MainWindow()
rect = QApplication.desktop().availableGeometry()
window.resize(int(rect.width()), int(rect.height()))
window.show()
app.exec_()
I see you have answered your own question. However I would like to explain why this works.
Every QGraphicsItem has its own local coordinate system. So when you do
self.rect = QRectF(position.x(), position.y(), 15, 15)
you basically start from the (0, 0) of the item's local coordinate system and go to the given x and y which you take from position. This basically means that your rectangle will be drawn at position.x() + position.x() and position.y() + position.y() with the first position.x()/position.y() being the position of the QGraphicsItem inside your scene and the second position.x()/position.y() being the position inside the local coordinate system of your item.
If you want to start from the origin of the QGraphicsItem, you have to use
self.rect = QRectF(0, 0, 15, 15)
This ensures that you start from the origin of the local coordinate system.
This issue is particularly tricky due to the fact that by default objects are added to the (0, 0) of a scene. So position.x() + position.x() and position.y() + position.y() in this case will actually not show the issue at hand since 0+0 is always equal to 0. It is the moment you change the default position to something else when the problem will occur.
Here is a 3D figure that visualizes what I'm describing above (I was unable to find a 2D example but the principle is the same :P):
The world here is the scene while the object is the QGraphicsItem residing in that scene.
Changing
self.rect = QRectF(position.x(), position.y(), 15, 15)
to
self.rect = QRectF(0, 0, 15, 15)
solved the problem

Resources