QPaint crashed in subclassed QStyledItemDelegate - qt

I subclassed a QStyledItemDelegate to change the highlight color of the selections, and if there is a QColor in ForegroundRole and BackgroundRole I will try to blend two colors.
However, when I'm trying to select a row in the tableview, following error message shows up:
TypeError: arguments did not match any overloaded call:
setColor(self, QPalette.ColorGroup, QPalette.ColorRole, Union[QColor, Qt.GlobalColor, QGradient]): argument 1 has unexpected type 'ColorRole'
setColor(self, QPalette.ColorRole, Union[QColor, Qt.GlobalColor, QGradient]): argument 2 has unexpected type 'NoneType'
and terminal shows:
QPainter::begin: A paint device can only be painted by one painter at a time.
QPainter::setCompositionMode: Painter not active
QBackingStore::endPaint() called with active painter; did you forget to destroy it or call QPainter::end() on it?
Here is my implementation:
class StyleDelegateForTable_List(QStyledItemDelegate):
"""
Customize highlight style for ListView & TableView
"""
def __init__(self, parent):
super().__init__(parent)
self.hightColor = QtGui.QColor("#0096ff")
def paint(self, painter, option: QtWidgets.QStyleOptionViewItem, index):
self.initStyleOption(option, index)
if (option.state & QtWidgets.QStyle.StateFlag.State_Selected and
option.state & QtWidgets.QStyle.StateFlag.State_Active):
# get foreground color,
# don't know where to set foreground color though...
fg = self.getColor(index, isBG = False)
# get background color
bg = self.getColor(index, isBG = True)
# set highlight color
option.palette.setColor(QtGui.QPalette.ColorRole.Highlight,
self.mixColors(bg))
QStyledItemDelegate.paint(self, painter, option, index)
def getColor(self, index: QModelIndex, isBG = True) -> QtGui.QColor:
parentWidget = self.parent()
model = parentWidget.model()
dataRole = Qt.ItemDataRole.BackgroundRole if isBG else Qt.ItemDataRole.ForegroundRole
# TableView
if isinstance(parentWidget, QtWidgets.QTableView):
if isinstance(model, QSortFilterProxyModel):
# proxy model
sourceIndex = model.mapToSource(index)
return model.sourceModel().data(sourceIndex, dataRole)
elif isinstance(model, TestDataTableModel):
# abstract table model
return model.data(index, dataRole)
# ListView
if isinstance(parentWidget, QtWidgets.QListView):
if isinstance(model, QSortFilterProxyModel):
# all of listView uses proxyModel
sourceIndex = model.mapToSource(index)
return model.sourceModel().data(sourceIndex, dataRole)
return QtGui.QColor("#000000")
def mixColors(self, src) -> QtGui.QColor:
if isinstance(src, QtGui.QColor):
r = int(src.red()*0.7 + self.hightColor.red()*0.3)
g = int(src.green()*0.7 + self.hightColor.green()*0.3)
b = int(src.blue()*0.7 + self.hightColor.blue()*0.3)
return QtGui.QColor(r, g, b)
else:
# I intended to mix default bg or fg color
# with highlight color but default is None.
# return hightlight color for now
self.hightColor
I tried to set a breakpoint in getColor, but my app just crashed, what did I do wrong here?
Edit:
This question is invalid, as # musicamante points out, mixColors returns None instead of hightlight color if the original Background/Foreground is None.
I will keep this question in case someone needs the same functionality.

Related

wxpython: howto design a BitmapToggleButton

I want a BitMapButton Class with some toggle functionality:
first press ON, OFF next press etc.
This should be visible in the color of the Button.
I tried the following (needs some tiny 'off.gif', 'on.gif' files):
import wx
class BitmapToggleButton(wx.BitmapButton):
"""make a Button, inherits wx.BitmapButton, add a toggleState"""
#----------------------------------------------------------------------
def __init__(self, parent, filename1, filename2):
"""Constructor"""
self.state = False
self.image1 = wx.Image(filename1, wx.BITMAP_TYPE_ANY).ConvertToBitmap()
self.image2 = wx.Image(filename2, wx.BITMAP_TYPE_ANY).ConvertToBitmap()
#wx.BitmapButton.__init__(self, id=-1, bitmap=self.image2, pos=(10, 20), size = (300, 400))
self.image1 = wx.Image(filename1, wx.BITMAP_TYPE_ANY).ConvertToBitmap()
self.image2 = wx.Image(filename2, wx.BITMAP_TYPE_ANY).ConvertToBitmap()
self.button = wx.BitmapButton(parent, id=-1, bitmap=self.image1, pos=(10, 20), size = (self.image1.GetWidth()+5, self.image1.GetHeight()+5))
def OnClick(self, event):
"""does the toggling"""
if self.state:
self.state = False
self.button.SetBitmapLabel(self.image2)
else:
self.state = True
self.button.SetBitmapLabel(self.image1)
self.Refresh()
class MyFrame(wx.Frame):
"""make a frame, inherits wx.Frame, add a panel and button"""
def __init__(self):
# create a frame, no parent, default to wxID_ANY
wx.Frame.__init__(self, None, wx.ID_ANY, 'wxBitmapButton', pos=(300, 150), size=(300, 350))
#panel to display button
self.panel1 = wx.Panel(self, -1)
#test it
self.button = BitmapToggleButton(self, 'off.gif', 'on.gif')
self.Bind(wx.EVT_BUTTON, self.button.OnClick, self.button)
# show the frame
self.Show(True)
application = wx.PySimpleApp()
# call class MyFrame
f = MyFrame()
# start the event loop
application.MainLoop()
I confess to have some lost track, I have to inherit from BitmapButton class, right, What is wrong?
joh
I have wxPython 2.8. There is no GenBitmapToggleButton
or miss I something?
You don't. Just use GenBitmapToggleButton, see wxPython demo for an example (in Custom Controls/GenericButtons).
EDIT: Present since at least 2.8.9.1, see Link. Have a closer look at the wxPython demo if unsure how to use.

How to create a proxy model that would flatten nodes of a QAbstractItemModel into a list in PySide?

I have a hierarchy of nodes represented by a custom QAbstractItemModel. Is it possible to create a proxy model that would flatten the hierarchy into a list to allow me to present all the nodes/items in a QListView (without a proxy only the first level of the tree gets presented)?
A A
+---1 1
2 2
+--3 3
4 => 4
B B
+---5 5
6 6
+--7 7
8 8
Thanks,
FipS
It's way easier to just coerce a QTreeView to look like a list view:
view = QtGui.QTreeView()
view.setModel(model)
view.expandAll()
view.setIndentation(0)
view.header().hide()
If you really wish to do it, the proxy isn't the most trivial affair, since it needs to retain a structural model of the source model. For a source model that changes its structure, the proxy must also keep track of the structure of the source model.
As a starting point, below is a minimal implementation for a model with static structure. I've only tested it on Python 3.3. The changes are propagated between the views - you can edit the text of an item in either view, and the underlying tree model will be modified, and the other view appropriately notified.
The proxy should simply pass-through list models, as they are already flat. To demonstrate this transparency, the right pane is a list view of a proxy attached to the proxy viewed in the middle pane. The proxy viewed in the middle pane is attached to the tree model viewed in the left pane.
I gladly accept edits by those who actually know Python/PySide. My knowledge of Python is very recreational at the moment.
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import sys
from PySide import QtCore, QtGui
class FlatProxyModel(QtGui.QAbstractProxyModel):
#QtCore.Slot(QtCore.QModelIndex, QtCore.QModelIndex)
def sourceDataChanged(self, topLeft, bottomRight):
self.dataChanged.emit(self.mapFromSource(topLeft), \
self.mapFromSource(bottomRight))
def buildMap(self, model, parent = QtCore.QModelIndex(), row = 0):
if row == 0:
self.m_rowMap = {}
self.m_indexMap = {}
rows = model.rowCount(parent)
for r in range(rows):
index = model.index(r, 0, parent)
print('row', row, 'item', model.data(index))
self.m_rowMap[index] = row
self.m_indexMap[row] = index
row = row + 1
if model.hasChildren(index):
row = self.buildMap(model, index, row)
return row
def setSourceModel(self, model):
QtGui.QAbstractProxyModel.setSourceModel(self, model)
self.buildMap(model)
print(flush = True)
model.dataChanged.connect(self.sourceDataChanged)
def mapFromSource(self, index):
if index not in self.m_rowMap: return QtCore.QModelIndex()
#print('mapping to row', self.m_rowMap[index], flush = True)
return self.createIndex(self.m_rowMap[index], index.column())
def mapToSource(self, index):
if not index.isValid() or index.row() not in self.m_indexMap:
return QtCore.QModelIndex()
#print('mapping from row', index.row(), flush = True)
return self.m_indexMap[index.row()]
def columnCount(self, parent):
return QtGui.QAbstractProxyModel.sourceModel(self)\
.columnCount(self.mapToSource(parent))
def rowCount(self, parent):
#print('rows:', len(self.m_rowMap), flush=True)
return len(self.m_rowMap) if not parent.isValid() else 0
def index(self, row, column, parent):
#print('index for:', row, column, flush=True)
if parent.isValid(): return QtCore.QModelIndex()
return self.createIndex(row, column)
def parent(self, index):
return QtCore.QModelIndex()
def __init__(self, parent = None):
super(FlatProxyModel, self).__init__(parent)
if __name__ == "__main__":
app = QtGui.QApplication(sys.argv)
model = QtGui.QStandardItemModel()
names = ['Foo', 'Bar', 'Baz']
for first in names:
row = QtGui.QStandardItem(first)
for second in names:
row.appendRow(QtGui.QStandardItem(first+second))
model.appendRow(row)
proxy = FlatProxyModel()
proxy.setSourceModel(model)
nestedProxy = FlatProxyModel()
nestedProxy.setSourceModel(proxy)
w = QtGui.QWidget()
layout = QtGui.QHBoxLayout(w)
view = QtGui.QTreeView()
view.setModel(model)
view.expandAll()
view.header().hide()
layout.addWidget(view)
view = QtGui.QListView()
view.setModel(proxy)
layout.addWidget(view)
view = QtGui.QListView()
view.setModel(nestedProxy)
layout.addWidget(view)
w.show()
sys.exit(app.exec_())

Tooltip message when hovering on cell with mouse in wx.grid wxpython

I have a wx.grid table, I want to set a tooltip when I hover on a cell, I tried Mike Driscoll's recommendation below, it works, but I can't select multiple cells with mouse drag anymore, it allows me to select only 1 cell max, please help:
self.grid_area.GetGridWindow().Bind(wx.EVT_MOTION, self.onMouseOver)
def onMouseOver(self, event):
'''
Method to calculate where the mouse is pointing and
then set the tooltip dynamically.
'''
# Use CalcUnscrolledPosition() to get the mouse position
# within the
# entire grid including what's offscreen
x, y = self.grid_area.CalcUnscrolledPosition(event.GetX(),event.GetY())
coords = self.grid_area.XYToCell(x, y)
# you only need these if you need the value in the cell
row = coords[0]
col = coords[1]
if self.grid_area.GetCellValue(row, col):
if self.grid_area.GetCellValue(row, col) == "ABC":
event.GetEventObject().SetToolTipString("Code is abc")
elif self.grid_area.GetCellValue(row, col) == "XYZ":
event.GetEventObject().SetToolTipString("code is xyz")
else:
event.GetEventObject().SetToolTipString("Unknown code")
OK, I found the solution, I have to skip the event:
def onMouseOver(self, event):
'''
Method to calculate where the mouse is pointing and
then set the tooltip dynamically.
'''
# Use CalcUnscrolledPosition() to get the mouse position
# within the
# entire grid including what's offscreen
x, y = self.grid_area.CalcUnscrolledPosition(event.GetX(),event.GetY())
coords = self.grid_area.XYToCell(x, y)
# you only need these if you need the value in the cell
row = coords[0]
col = coords[1]
if self.grid_area.GetCellValue(row, col):
if self.grid_area.GetCellValue(row, col) == "ABC":
event.GetEventObject().SetToolTipString("Code is abc")
elif self.grid_area.GetCellValue(row, col) == "XYZ":
event.GetEventObject().SetToolTipString("code is xyz")
else:
event.GetEventObject().SetToolTipString("Unknown code")
event.Skip()
Thanks
best regards
# GreenAsJade
Since I cannot comment due to reputation i am asnwering your question here!
why: what was going wrong, how does this fix it?
If you check the difference between your event Hanlder and #alwbtc's event Handler only difference is event.Skip()
Any time the wx.EVT_xx is bind with custom method in code, wxpython override default definition. For that reason event handling ends in onMouseOver. event.Skip() will propagate the event to _core of wxptyhon allowing it to execute default event handlers.
Hope this answers your question!

Python3: How to dynamically resize button text in tkinter/ttk?

I want to know how to arrange for the text on a ttk widget (a label or button, say) to resize automatically.
Changing the size of the text is easy, it is just a matter of changing the font in the style. However, hooking it into changes in the size of the window is a little more tricky. Looking on the web I found some hints, but there was nowhere a complete answer was posted.
So, here below is a complete working example posted as an answer to my own question. I hope someone finds it useful. If anyone has further improvements to suggest, I will be delighted to see them!
The example below shows two techniques, one activated by re-sizing the window (see the resize() method, bound to the <Configure> event), and the other by directly changing the size of the font (see the mutate() method).
Other code necessary to get resizing working is the grid configuration code in the __init__() method.
When running the example, there is some interaction between the two methods, but I think in a 'real' situation one technique would be sufficient, so that issue won't arise.
from tkinter import *
from tkinter.ttk import *
class ButtonApp(Frame):
"""Container for the buttons."""
def __init__(self, master=None):
"""Initialize the frame and its children."""
super().__init__(master)
self.createWidgets()
# configure the frame's resize behaviour
master.columnconfigure(0, weight=1)
master.rowconfigure(0, weight=1)
self.grid(sticky=(N,S,E,W))
# configure resize behaviour for the frame's children
self.columnconfigure(0, weight=1)
self.rowconfigure(0, weight=1)
self.rowconfigure(0, weight=1)
# bind to window resize events
self.bind('<Configure>', self.resize)
def createWidgets(self):
"""Make the widgets."""
# this button mutates
self.mutantButton = Button(self, text='Press Me',
style='10.TButton')
self.mutantButton.grid(column=0, row=0, sticky=(N,S,E,W))
self.mutantButton['command'] = self.mutate
# an ordinary quit button for comparison
self.quitButton = Button(self, text='Quit', style='TButton')
self.quitButton.grid(column=0, row=1, sticky=(N,S,E,W))
self.quitButton['command'] = self.quit
def mutate(self):
"""Rotate through the styles by hitting the button."""
style = int(self.mutantButton['style'].split('.')[0])
newStyle = style + 5
if newStyle > 50: newStyle = 10
print('Choosing font '+str(newStyle))
self.mutantButton['style'] = fontStyle[newStyle]
# resize the frame
# get the current geometries
currentGeometry = self._root().geometry()
w, h, x, y = self.parseGeometry(currentGeometry)
reqWidth = self.mutantButton.winfo_reqwidth()
reqHeight = self.mutantButton.winfo_reqheight()
# note assume height of quit button is constant at 20.
w = max([w, reqWidth])
h = 20 + reqHeight
self._root().geometry('%dx%d+%d+%d' % (w, h, x, y))
def parseGeometry(self, geometry):
"""Geometry parser.
Returns the geometry as a (w, h, x, y) tuple."""
# get w
xsplit = geometry.split('x')
w = int(xsplit[0])
rest = xsplit[1]
# get h, x, y
plussplit = rest.split('+')
h = int(plussplit[0])
x = int(plussplit[1])
y = int(plussplit[2])
return w, h, x, y
def resize(self, event):
"""Method bound to the <Configure> event for resizing."""
# get geometry info from the root window.
wm, hm = self._root().winfo_width(), self._root().winfo_height()
# choose a font height to match
# note subtract 30 for the button we are NOT scaling.
# note we assume optimal font height is 1/2 widget height.
fontHeight = (hm - 20) // 2
print('Resizing to font '+str(fontHeight))
# calculate the best font to use (use int rounding)
bestStyle = fontStyle[10] # use min size as the fallback
if fontHeight < 10: pass # the min size
elif fontHeight >= 50: # the max size
bestStyle = fontStyle[50]
else: # everything in between
bestFitFont = (fontHeight // 5) * 5
bestStyle = fontStyle[bestFitFont]
# set the style on the button
self.mutantButton['style'] = bestStyle
root = Tk()
root.title('Alice in Pythonland')
# make a dictionary of sized font styles in the range of interest.
fontStyle = {}
for font in range(10, 51, 5):
styleName = str(font)+'.TButton'
fontName = ' '.join(['helvetica', str(font), 'bold'])
fontStyle[font] = styleName
Style().configure(styleName, font=fontName)
# run the app
app = ButtonApp(master=root)
app.mainloop()
root.destroy()

PySide/PyQt truncate text in QLabel based on minimumSize

I am wondering how to best truncate text in a QLabel based on it's maximum width/height.
The incoming text could be any length, but in order to keep a tidy layout I'd like to truncate long strings to fill a maximum amount of space (widget's maximum width/height).
E.g.:
'A very long string where there should only be a short one, but I can't control input to the widget as it's a user given value'
would become:
'A very long string where there should only be a short one, but ...'
based on the required space the current font needs.
How can I achieve this best?
Here is a simple example of what I'm after, though this is based on word count, not available space:
import sys
from PySide.QtGui import *
from PySide.QtCore import *
def truncateText(text):
maxWords = 10
words = text.split(' ')
return ' '.join(words[:maxWords]) + ' ...'
app = QApplication(sys.argv)
mainWindow = QWidget()
layout = QHBoxLayout()
mainWindow.setLayout(layout)
text = 'this is a very long string, '*10
label = QLabel(truncateText(text))
label.setWordWrap(True)
label.setFixedWidth(200)
layout.addWidget(label)
mainWindow.show()
sys.exit(app.exec_())
Even easier - use the QFontMetrics.elidedText method and overload the paintEvent, here's an example:
from PyQt4.QtCore import Qt
from PyQt4.QtGui import QApplication,\
QLabel,\
QFontMetrics,\
QPainter
class MyLabel(QLabel):
def paintEvent( self, event ):
painter = QPainter(self)
metrics = QFontMetrics(self.font())
elided = metrics.elidedText(self.text(), Qt.ElideRight, self.width())
painter.drawText(self.rect(), self.alignment(), elided)
if ( __name__ == '__main__' ):
app = None
if ( not QApplication.instance() ):
app = QApplication([])
label = MyLabel()
label.setText('This is a really, long and poorly formatted runon sentence used to illustrate a point')
label.setWindowFlags(Qt.Dialog)
label.show()
if ( app ):
app.exec_()
I found that #Eric Hulser's answer, while great, didn't work when the label was put into another widget.
I came up with this by hacking together Eric's response with the Qt Elided Label Example. It should behave just like a regular label, yet elide horizontally when the text width exceeds the widget width. It has an extra argument for different elide modes. I also wrote some tests for fun :)
If you want to use PyQt5...
Change "PySide2" to "PyQt5"
Change "Signal" to "pyqtSignal"
Enjoy!
Eliding Label
# eliding_label.py
from PySide2 import QtCore, QtWidgets, QtGui
class ElidingLabel(QtWidgets.QLabel):
"""Label with text elision.
QLabel which will elide text too long to fit the widget. Based on:
https://doc-snapshots.qt.io/qtforpython-5.15/overviews/qtwidgets-widgets-elidedlabel-example.html
Parameters
----------
text : str
Label text.
mode : QtCore.Qt.TextElideMode
Specify where ellipsis should appear when displaying texts that
don’t fit.
Default is QtCore.Qt.ElideMiddle.
Possible modes:
QtCore.Qt.ElideLeft
QtCore.Qt.ElideMiddle
QtCore.Qt.ElideRight
parent : QWidget
Parent widget. Default is None.
f : Qt.WindowFlags()
https://doc-snapshots.qt.io/qtforpython-5.15/PySide2/QtCore/Qt.html#PySide2.QtCore.PySide2.QtCore.Qt.WindowType
"""
elision_changed = QtCore.Signal(bool)
def __init__(self, text='', mode=QtCore.Qt.ElideMiddle, **kwargs):
super().__init__(**kwargs)
self._mode = mode
self.is_elided = False
self.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Preferred)
self.setText(text)
def setText(self, text):
self._contents = text
# This line set for testing. Its value is the return value of
# QFontMetrics.elidedText, set in paintEvent. The variable
# must be initialized for testing. The value should always be
# the same as contents when not elided.
self._elided_line = text
self.update()
def text(self):
return self._contents
def paintEvent(self, event):
super().paintEvent(event)
did_elide = False
painter = QtGui.QPainter(self)
font_metrics = painter.fontMetrics()
text_width = font_metrics.horizontalAdvance(self.text())
# layout phase
text_layout = QtGui.QTextLayout(self._contents, painter.font())
text_layout.beginLayout()
while True:
line = text_layout.createLine()
if not line.isValid():
break
line.setLineWidth(self.width())
if text_width >= self.width():
self._elided_line = font_metrics.elidedText(self._contents, self._mode, self.width())
painter.drawText(QtCore.QPoint(0, font_metrics.ascent()), self._elided_line)
did_elide = line.isValid()
break
else:
line.draw(painter, QtCore.QPoint(0, 0))
text_layout.endLayout()
if did_elide != self.is_elided:
self.is_elided = did_elide
self.elision_changed.emit(did_elide)
if __name__ == '__main__':
app = QtWidgets.QApplication([])
long_text = "this is some long text, wouldn't you say?"
elabel = ElidingLabel(long_text)
elabel.show()
app.exec_()
Test Eliding Label
# test_eliding_label.py.py
#
# Run tests with
#
# python3 -m unittest test_eliding_label.py --failfast --quiet
import unittest
import unittest.mock
from PySide2 import QtCore, QtWidgets, QtGui, QtTest
import eliding_label
if not QtWidgets.QApplication.instance():
APP = QtWidgets.QApplication([]) # pragma: no cover
class TestElidingLabelArguments(unittest.TestCase):
def test_optional_text_argument(self):
elabel = eliding_label.ElidingLabel()
self.assertEqual(elabel.text(), "")
def test_text_argument_sets_label_text(self):
elabel = eliding_label.ElidingLabel(text="Test text")
self.assertEqual(elabel.text(), "Test text")
def test_optional_elision_mode_argument(self):
elabel = eliding_label.ElidingLabel()
self.assertEqual(elabel._mode, QtCore.Qt.ElideMiddle)
class TestElidingLabel(unittest.TestCase):
def setUp(self):
self.elabel = eliding_label.ElidingLabel()
def test_elabel_is_a_label(self):
self.assertIsInstance(self.elabel, QtWidgets.QLabel)
def test_has_elision_predicate(self):
self.assertEqual(self.elabel.is_elided, False)
def test_elision_predicate_changes_when_text_width_exceeds_widget_width(self):
# NOTE: This is a bit of a stretch, inducing a paint event
# when the event loop isn't running. Throws a bunch of C++
# sourced text which can't be (easily) caught.
self.elabel.setFixedWidth(25)
self.assertEqual(self.elabel.width(), 25)
long_text = "This is line is definely longer than 25 pixels."
painter = QtGui.QPainter()
font_metrics = painter.fontMetrics()
long_text_width = font_metrics.horizontalAdvance(long_text)
self.assertGreater(long_text_width, 25)
self.elabel.setText(long_text)
x = self.elabel.x()
y = self.elabel.y()
w = self.elabel.width()
h = self.elabel.height()
paint_event = QtGui.QPaintEvent(QtGui.QRegion(x, y, w, h))
self.elabel.paintEvent(paint_event)
self.assertEqual(self.elabel.is_elided, True)
def test_text_is_elided_when_text_width_exceeds_widget_width(self):
# NOTE: This is a bit of a stretch, inducing a paint event
# when the event loop isn't running. Throws a bunch of C++
# sourced text which can't be (easily) caught.
self.elabel.setFixedWidth(25)
self.assertEqual(self.elabel.width(), 25)
long_text = "This is line is definely longer than 25 pixels."
painter = QtGui.QPainter()
font_metrics = painter.fontMetrics()
long_text_width = font_metrics.horizontalAdvance(long_text)
self.assertGreater(long_text_width, 25)
self.elabel.setText(long_text)
x = self.elabel.x()
y = self.elabel.y()
w = self.elabel.width()
h = self.elabel.height()
paint_event = QtGui.QPaintEvent(QtGui.QRegion(x, y, w, h))
self.elabel.paintEvent(paint_event)
# PySide2.QtGui.QFontMetrics.elidedText states, "If the string
# text is wider than width , returns an elided version of the
# string (i.e., a string with '…' in it). Otherwise, returns
# the original string."
self.assertEqual(self.elabel._elided_line, '…')
def test_text_is_not_elided_when_text_width_is_less_than_widget_width(self):
# NOTE: This is a bit of a stretch, inducing a paint event
# when the event loop isn't running. Throws a bunch of C++
# sourced text which can't be (easily) caught.
self.elabel.setFixedWidth(500)
self.assertEqual(self.elabel.width(), 500)
short_text = "Less than 500"
painter = QtGui.QPainter()
font_metrics = painter.fontMetrics()
short_text_width = font_metrics.horizontalAdvance(short_text)
self.assertLess(short_text_width, 500)
self.elabel.setText(short_text)
x = self.elabel.x()
y = self.elabel.y()
w = self.elabel.width()
h = self.elabel.height()
paint_event = QtGui.QPaintEvent(QtGui.QRegion(x, y, w, h))
self.elabel.paintEvent(paint_event)
# PySide2.QtGui.QFontMetrics.elidedText states, "If the string
# text is wider than width , returns an elided version of the
# string (i.e., a string with '…' in it). Otherwise, returns
# the original string."
self.assertEqual(self.elabel._elided_line, short_text)
def test_stores_full_text_even_when_elided(self):
# NOTE: This is a bit of a stretch, inducing a paint event
# when the event loop isn't running. Throws a bunch of C++
# sourced text which can't be (easily) caught.
self.elabel.setFixedWidth(25)
self.assertEqual(self.elabel.width(), 25)
long_text = "This is line is definely longer than 25 pixels."
painter = QtGui.QPainter()
font_metrics = painter.fontMetrics()
long_text_width = font_metrics.horizontalAdvance(long_text)
self.assertGreater(long_text_width, 25)
self.elabel.setText(long_text)
x = self.elabel.x()
y = self.elabel.y()
w = self.elabel.width()
h = self.elabel.height()
paint_event = QtGui.QPaintEvent(QtGui.QRegion(x, y, w, h))
self.elabel.paintEvent(paint_event)
# PySide2.QtGui.QFontMetrics.elidedText states, "If the string
# text is wider than width , returns an elided version of the
# string (i.e., a string with '…' in it). Otherwise, returns
# the original string."
self.assertEqual(self.elabel._elided_line, '…')
self.assertEqual(self.elabel.text(), long_text)
def test_has_elision_changed_signal(self):
self.assertIsInstance(self.elabel.elision_changed, QtCore.Signal)
def test_elision_changed_signal_emits_on_change_to_is_elided_predicate(self):
mock = unittest.mock.Mock()
self.elabel.elision_changed.connect(mock.method)
# NOTE: This is a bit of a stretch, inducing a paint event
# when the event loop isn't running. Throws a bunch of C++
# sourced text which can't be (easily) caught.
# Induce elision
self.elabel.setFixedWidth(150)
self.assertEqual(self.elabel.width(), 150)
long_text = "This line is definitely going to be more than 150 pixels"
painter = QtGui.QPainter()
font_metrics = painter.fontMetrics()
long_text_width = font_metrics.horizontalAdvance(long_text)
self.assertGreater(long_text_width, 150)
self.elabel.setText(long_text)
self.assertEqual(self.elabel.is_elided, False) # no elide until painting
x = self.elabel.x()
y = self.elabel.y()
w = self.elabel.width()
h = self.elabel.height()
paint_event = QtGui.QPaintEvent(QtGui.QRegion(x, y, w, h))
self.elabel.paintEvent(paint_event)
self.assertEqual(self.elabel.is_elided, True)
mock.method.assert_called_once()
# Remove elision
short_text = "Less than 150"
painter = QtGui.QPainter()
font_metrics = painter.fontMetrics()
short_text_width = font_metrics.horizontalAdvance(short_text)
self.assertLess(short_text_width, 150)
self.elabel.setText(short_text)
self.assertEqual(self.elabel.is_elided, True) # still elided until painting
x = self.elabel.x()
y = self.elabel.y()
w = self.elabel.width()
h = self.elabel.height()
paint_event = QtGui.QPaintEvent(QtGui.QRegion(x, y, w, h))
self.elabel.paintEvent(paint_event)
self.assertEqual(self.elabel.is_elided, False)
self.assertEqual(mock.method.call_count, 2)
You can achieves this through determining the width with QFontMetrics, see this answer.
You would probably want to use or create some algorithm which finds the place to cut quickly, unless doing it in a simple for loop would be sufficient.
simpler solution if you want show QLabel in center in provided area
label.setAlignment(Qt.AlignmentFlag.AlignCenter)
label.minimumSizeHint = lambda self=label: QSize(0, QLabel.minimumSizeHint(self).height() )

Resources