I would like to send multiple POST requests using single QNetworkAccessManager, but it works only once. The second and further requests got an empty reply.
I know I can use multiple instances of manager, but I read, that it's an architectural flaw.
I have also found this: using QNetworkAccessManager GET multiple times, but I do not understand the eventual solution.
Here's my code:
class Networker(QObject):
gotResponse = pyqtSignal(str)
def proc(self, reply):
s = bytes(reply.readAll()).decode('utf-8')
self.gotResponse.emit(s)
def __init__(self, parent=None):
super(Networker, self).__init__(parent)
self.manager = QNetworkAccessManager()
def send(self):
req = QNetworkRequest(QUrl("http://127.0.0.1:8000/"))
req.setHeader(QNetworkRequest.ContentTypeHeader, "text/json;charset=utf-8")
data = QByteArray('{"id":1}')
self.manager.finished.connect(self.proc)
self.manager.post(req, data)
Main (imports excluded):
def main():
app = QtWidgets.QApplication(sys.argv)
n = Networker(app)
w = QtWidgets.QWidget()
w.setWindowTitle("main")
layout = QBoxLayout(QBoxLayout.LeftToRight, w)
l1 = QTextEdit("Result", w)
l1.resize(300, 600)
n.gotResponse.connect(l1.setText)
layout.addWidget(l1)
w.show()
l2 = QTextEdit("Idle", w)
l2.resize(250, 600)
layout.addWidget(l2)
w.resize(500, 600)
btn = QPushButton("Send", w)
layout.addWidget(btn)
btn.clicked.connect(n.send)
w.show()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
Related
I got the emit signal working when it is in the Worker class - def run(self) method. Everything runs fine, the while loop is able to cycle and emit a signal every 1 second. Then the label will be updated after receiving the signal.
I decided to try out by placing the while loop in another class named loop, methodA. This is to try see if the emitted signal will be picked up by the MainWindow. Unfortunately, the signal is not picked up and the program hung up thereafter.
Did I miss any step that prevented the while loop's emitting signals from being picked up? Kindly point me in the right direction by making changes to my script.
Thanks.
import sys
import time
from PyQt5 import QtWidgets
from PyQt5.QtCore import QThread, pyqtSignal
from mydialog import Ui_mydialog
class Mainwindow(QtWidgets.QMainWindow, Ui_mydialog):
def __init__(self, *args, obj=None, **kwargs):
super(Mainwindow, self).__init__(*args, **kwargs)
self.setupUi(self)
self.thread = Worker()
self.loop = loop()
self.thread.sig.connect(self.updatelabel)
self.mypushbutton.clicked.connect(self.mypushbuttonclicked)
def mypushbuttonclicked(self):
self.thread.start()
def updatelabel(self, text):
self.mylabel.setText(text)
class Worker(QThread):
sig = pyqtSignal(str)
def __init__(self, parent=None):
super(Worker, self).__init__(parent)
# self.count = 0
self.loop = loop()
def run(self):
self.loop.methodA()
## Original code without being in class loop and method loopA
# while True:
# time.sleep(1)
# self.count += 1
# if (self.count % 1 == 0):
# self.sig.emit(f"Timer: {self.count} s")
# Newly added class with method "methodA"
class loop(object):
sig = pyqtSignal(str)
def __init__(self):
self.count = 0
def methodA(self):
while True:
time.sleep(1)
self.count += 1
if (self.count % 1 == 0):
self.sig.emit(f"Timer: {self.count} s")
app = QtWidgets.QApplication(sys.argv)
window = Mainwindow()
app.setStyle("Fusion")
window.show()
app.exec()
I had a similar problem.
I solved it by following this: http://zetcode.com/gui/pyqt5/eventssignals/
The idea is to create a class that hold all the signals, and pass the same communication class to all the classes as a parameter.
So your code might become:
import sys
import time
from PyQt5 import QtWidgets
from PyQt5.QtCore import QThread, pyqtSignal, QObject
from mydialog import Ui_mydialog
class Communicate(QObject):
sig = pyqtSignal(str)
class Mainwindow(QtWidgets.QMainWindow, Ui_mydialog):
def __init__(self, *args, obj=None, **kwargs):
super(Mainwindow, self).__init__(*args, **kwargs)
self.setupUi(self)
self.communicate = Communicate()
self.communicate.sig[str].connect(self.updatelabel)
self.thread = Worker(communicate = self.communicate)
#self.loop = loop() # this seems useless to me here
self.mypushbutton.clicked.connect(self.mypushbuttonclicked)
def mypushbuttonclicked(self):
self.thread.start()
def updatelabel(self, text):
self.mylabel.setText(text)
class Worker(QThread):
def __init__(self, parent=None, communicate=Communicate()):
super(Worker, self).__init__(parent)
self.communicate = communicate
# self.count = 0
self.loop = loop(communicate= self.communicate)
def run(self):
self.loop.methodA()
## Original code without being in class loop and method loopA
# while True:
# time.sleep(1)
# self.count += 1
# if (self.count % 1 == 0):
# self.sig.emit(f"Timer: {self.count} s")
# Newly added class with method "methodA"
class loop(object):
def __init__(self, communicate=Communicate()):
self.count = 0
self.communicate = communicate
def methodA(self):
while True:
time.sleep(1)
self.count += 1
if (self.count % 1 == 0):
self.communicate.sig.emit(f"Timer: {self.count} s")
app = QtWidgets.QApplication(sys.argv)
window = Mainwindow()
app.setStyle("Fusion")
window.show()
app.exec()
I haven't tested this code, but I hope that you've got the idea.
After reading about finite state machines I found the the QState/QStateMachine API in QT.
But now I can't find out how to use it to run different code based on which state I'm currently in.
Let's take the following example:
import sys
from PySide2 import QtCore, QtWidgets
class Form(QtWidgets.QDialog):
def action_a(self):
print("I'm in mode A")
def action_b(self):
print("Mode B is the current mode")
def __init__(self, parent=None):
super(Form, self).__init__(parent)
self.button1 = QtWidgets.QPushButton("Run action")
self.button2 = QtWidgets.QPushButton("Change State")
self.layout = QtWidgets.QVBoxLayout()
self.layout.addWidget(self.button1)
self.layout.addWidget(self.button2)
self.setLayout(self.layout)
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
form = Form()
form.show()
state_a = QtCore.QState()
state_b = QtCore.QState()
state_a.assignProperty(form.button2, "text", "To state B")
state_b.assignProperty(form.button2, "text", "To state A")
state_a.addTransition(form.button2, QtCore.SIGNAL("clicked()"), state_b)
state_b.addTransition(form.button2, QtCore.SIGNAL("clicked()"), state_a)
machine = QtCore.QStateMachine()
machine.addState(state_a)
machine.addState(state_b)
machine.setInitialState(state_a)
machine.start()
sys.exit(app.exec_())
What would I have to add to this code so that button1.clicked connects to action_a when in state_a, but to action_b when in state_b?
With reconfigurable model execution it is possible to resize inputs and outputs of components. How are the connections updated, when reconfigured outputs and inputs are connected?
In the example below the output c2.y and c3.y is resized at each model run. This input and output is supposed to be connected, as shown in the N2 chart. However, after the reconfiguration the connection size seems to be not updated automatically, it throws the following error:
ValueError: The source and target shapes do not match or are ambiguous for the connection 'c2.y' to 'c3.y'. Expected (1,) but got (2,).
I included below 3 tests, with promoted connection, absolute connection, and the last one with reconfiguration but without the connection (which works).
The last chance would be to declare the connection in the parent group of the comps, which I did not try yet.
The tests:
Promoted connection
Absolute connection
No connection
Reconfigurable component classes and tests:
from __future__ import division
import logging
import numpy as np
import unittest
from openmdao.api import Problem, Group, IndepVarComp, ExplicitComponent
from openmdao.utils.assert_utils import assert_rel_error
class ReconfComp(ExplicitComponent):
def initialize(self):
self.size = 1
self.counter = 0
def reconfigure(self):
logging.info('reconf started {}'.format(self.pathname))
self.counter += 1
logging.info('reconf ended {}'.format(self.pathname))
if self.counter % 2 == 0:
self.size += 1
return True
else:
return False
def setup(self):
logging.info('setup started {}'.format(self.pathname))
self.add_input('x', val=1.0)
self.add_output('y', val=np.zeros(self.size))
# All derivatives are defined.
self.declare_partials(of='*', wrt='*')
logging.info('setup ended {}'.format(self.pathname))
def compute(self, inputs, outputs):
logging.info('compute started {}'.format(self.pathname))
outputs['y'] = 2 * inputs['x']
logging.info('compute ended {}'.format(self.pathname))
def compute_partials(self, inputs, jacobian):
jacobian['y', 'x'] = 2 * np.ones((self.size, 1))
class ReconfComp2(ReconfComp):
"""The size of the y input changes the same as way as in ReconfComp"""
def setup(self):
logging.info('setup started {}'.format(self.pathname))
self.add_input('y', val=np.zeros(self.size))
self.add_output('f', val=np.zeros(self.size))
# All derivatives are defined.
self.declare_partials(of='*', wrt='*')
logging.info('setup ended {}'.format(self.pathname))
def compute(self, inputs, outputs):
logging.info('compute started {}'.format(self.pathname))
outputs['f'] = 2 * inputs['y']
logging.info('compute ended {}'.format(self.pathname))
def compute_partials(self, inputs, jacobian):
jacobian['f', 'y'] = 2 * np.ones((self.size, 1))
class TestReconfConnections(unittest.TestCase):
def test_reconf_comp_promoted_connections(self):
p = Problem()
p.model = Group()
p.model.add_subsystem('c1', IndepVarComp('x', 1.0), promotes_outputs=['x'])
p.model.add_subsystem('c2', ReconfComp(), promotes_inputs=['x'], promotes_outputs=['y'])
p.model.add_subsystem('c3', ReconfComp2(), promotes_inputs=['y'],
promotes_outputs=['f'])
p.setup()
p['x'] = 3.
# First run the model once; counter = 1, size of y = 1
p.run_model()
totals = p.compute_totals(wrt=['x'], of=['y'])
assert_rel_error(self, p['x'], 3.0)
assert_rel_error(self, p['y'], 6.0)
assert_rel_error(self, totals['y', 'x'], [[2.0]])
print(p['x'], p['y'], totals['y', 'x'].flatten())
# Run the model again, which will trigger reconfiguration; counter = 2, size of y = 2
p.run_model() # FIXME Fails with ValueError
def test_reconf_comp_connections(self):
p = Problem()
p.model = Group()
p.model.add_subsystem('c1', IndepVarComp('x', 1.0), promotes_outputs=['x'])
p.model.add_subsystem('c2', ReconfComp(), promotes_inputs=['x'])
p.model.add_subsystem('c3', ReconfComp2(), promotes_outputs=['f'])
p.model.connect('c2.y', 'c3.y')
p.setup()
p['x'] = 3.
# First run the model once; counter = 1, size of y = 1
p.run_model()
# Run the model again, which will trigger reconfiguration; counter = 2, size of y = 2
p.run_model() # FIXME Fails with ValueError
def test_reconf_comp_not_connected(self):
p = Problem()
p.model = Group()
p.model.add_subsystem('c1', IndepVarComp('x', 1.0), promotes_outputs=['x'])
p.model.add_subsystem('c2', ReconfComp(), promotes_inputs=['x'])
p.model.add_subsystem('c3', ReconfComp2(), promotes_outputs=['f'])
# c2.y not connected to c3.y
p.setup()
p['x'] = 3.
# First run the model once; counter = 1, size of y = 1
p.run_model()
# Run the model again, which will trigger reconfiguration; counter = 2, size of y = 2
fail, _, _ = p.run_model()
self.assertFalse(fail)
if __name__ == '__main__':
unittest.main()
UPDATE:
It seems, that in Group._var_abs2meta only the source size is updated, but not the target. The setup of the connections starts, before the setup of the parent group or the setup of the other component would be called.
UPDATE 2:
This happens with the default NonlinearRunOnce solver, with a NewtonSolver of NonlinearBlockGS there is no error, but the variable sizes also don't change.
As of OpenMDAO V2.5 reconfigurable model variables is not an officially supported feature in the framework. The bare bones of the capability has been in the code since that research was done, but it wasn't something that was high priority enough for us to finalize. A recent major refactor in V2.4 re-worked how some underlying data-structures worked and must have broken this functionality.
It is on our development priority list to get this working again, but its not super high on that list. We focus development mainly on features that have a direct in-house applications, and we don't have one of those yet.
If you could provide a decently complete set of tests for it, we could take a look at getting the functionality working.
I have a folder TreeView on the left layout and I want to drag a .txt file from there and drop it to the other layout. Hopefully, I want to load the data of that dropped file on a variable.
For the code’s needs, I used till now (to my “real” code) the np.loadtxt() to load the data, so I’d like to use it here too.
In case it matters, the .txt file contains 4 columns (coordinates).
I post my code. The program closes when I drop the file.
Thanks in advance!
import sys, time, os
from PyQt5.QtWidgets import *
from PyQt5.QtGui import *
from PyQt5.QtCore import *
import numpy as np
import pylab as pl
import random
class Example(QMainWindow):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
self.central_widget = QWidget()
self.setCentralWidget(self.central_widget)
self.folderLayout = QWidget();
self.pathRoot = QDir.rootPath()
self.dirmodel = QFileSystemModel(self)
self.dirmodel.setRootPath(QDir.currentPath())
self.indexRoot = self.dirmodel.index(self.dirmodel.rootPath())
self.folder_view = QTreeView();
self.folder_view.setDragEnabled(True)
self.folder_view.setModel(self.dirmodel)
self.folder_view.setRootIndex(self.indexRoot)
self.selectionModel = self.folder_view.selectionModel()
self.left_layout = QVBoxLayout()
self.left_layout.addWidget(self.folder_view)
self.folderLayout.setLayout(self.left_layout)
splitter_filebrowser = QSplitter(Qt.Horizontal)
splitter_filebrowser.addWidget(self.folderLayout)
splitter_filebrowser.addWidget(Figure_Canvas(self))
splitter_filebrowser.setStretchFactor(1, 1)
hbox = QHBoxLayout(self)
hbox.addWidget(splitter_filebrowser)
self.centralWidget().setLayout(hbox)
self.setWindowTitle('Simple drag & drop')
self.setGeometry(750, 100, 600, 500)
class Figure_Canvas(QWidget):
def __init__(self, parent):
super().__init__(parent)
self.setAcceptDrops(True)
blabla = QLineEdit()
self.right_layout = QVBoxLayout()
self.right_layout.addWidget(blabla)
self.buttonLayout = QWidget()
self.buttonLayout.setLayout(self.right_layout)
def dragEnterEvent(self, e):
if e.mimeData().hasFormat('text/uri-list'):
e.accept()
else:
e.ignore()
def dropEvent(self, e):
print("something")
data = np.loadtxt(e.mimeData())
print(data)
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = Example()
ex.show()
app.exec_()
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() )