PyQt5: A Label Within A Label? - python

I'd like to make part of the text of a label clickable, like an inline hyperlink on a website. I know how to make an individual label clickable, but I'm not sure how to only make part of the label clickable and still maintain a consistent format.
I've placed the code for my first attempt below and included an image of the output.
The two issues I see are the noticeable space between the labels (which even a QStretchItem at the end doesn't fix) and the issues with word wrapping.
Any help would be greatly appreciated. Thank you!
from PyQt5.QtWidgets import *
app = QApplication([])
class MainWindow(QWidget):
def __init__(self, *args, **kwargs):
super(MainWindow, self).__init__(*args, **kwargs)
self.setWindowTitle('Title')
self.setGeometry(1200, 200, 350, 500)
self.layout = QVBoxLayout()
self.setLayout(self.layout)
# Dummy list to print
place_list = { '2000': 'An event happened.',
'2005': 'An event at {this place} happened long ago.',
'2010': 'Another event happened at {a different place}, but it was not fun.' }
# Initialize Grid of Notes
grid = QGridLayout()
# Create Headers for each column
grid.addWidget(QLabel('Date'), 0, 0)
grid.addWidget(QLabel('Note'), 0, 1)
index = 1
# Iterate through each entry in place_list
for year in place_list:
# Add index of entry (by year)
grid.addWidget(QLabel(year), index, 0)
# Get text of entry
note = place_list[year]
# Look for "{}" to indicate link
if '{' in note:
# Get location of link within the entry
start = note.find('{')
end = note.find('}')
# Create a label for the text before the link
lab_1 = QLabel(note[:start])
lab_1.setWordWrap(True)
# Create a label for the link
# NOTE: It's a QLabel for formatting purposes only
lab_2 = QLabel(note[start+1:end])
lab_2.setWordWrap(True)
# Create a label for the text after the link
lab_3 = QLabel(note[end+1:])
lab_3.setWordWrap(True)
# Combine the labels in one layout
note_lab = QHBoxLayout()
note_lab.addWidget(lab_1)
note_lab.addWidget(lab_2)
note_lab.addWidget(lab_3)
# Add the layout as the entry
grid.addLayout(note_lab, index, 1)
else:
# Create the label for the whole entry if no link indicator is found
note_lab = QLabel(note)
note_lab.setWordWrap(True)
grid.addWidget(note_lab, index, 1)
# Go to next row in grid
index += 1
self.layout.addLayout(grid)
window = MainWindow()
window.show()
app.exec_()

The best solution I believe is to subclass QLabel and override the mousePressEvent method.
def mousePressEvent(event):
# event.pos() or .x() and .y() to find the position of the click.
If you create a QRect in the area that you want in the initialization of your custom QLabel, you can easily check if the click is inside the rectangle by using the QRect.contains() method as well.
Other useful methods for this would be mouseReleaseEvent and mouseDoubleClickEvent.
And in general, when you are adding/changing functionality to widgets, look to subclass first.

Related

How to word wrap the header contents of QTableWidget in PyQt5 Python

I am working on PyQt5 where I have a QTableWidget. It has a header column which I want to word wrap. Below is how the table looks like:
As we can see that the header label like Maximum Variation Coefficient has 3 words, thus its taking too much column width. How can wrap the words in the header.
Below is the code:
import sys
from PyQt5 import QtWidgets
from PyQt5.QtWidgets import *
# Main Window
class App(QWidget):
def __init__(self):
super().__init__()
self.title = 'PyQt5 - QTableWidget'
self.left = 0
self.top = 0
self.width = 300
self.height = 200
self.setWindowTitle(self.title)
self.setGeometry(self.left, self.top, self.width, self.height)
self.createTable()
self.layout = QVBoxLayout()
self.layout.addWidget(self.tableWidget)
self.setLayout(self.layout)
# Show window
self.show()
# Create table
def createTable(self):
self.tableWidget = QTableWidget()
# Row count
self.tableWidget.setRowCount(3)
# Column count
self.tableWidget.setColumnCount(2)
self.tableWidget.setHorizontalHeaderLabels(["Maximum Variation Coefficient", "Maximum Variation Coefficient"])
self.tableWidget.setSizeAdjustPolicy(QtWidgets.QAbstractScrollArea.AdjustToContents)
self.tableWidget.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeToContents)
self.tableWidget.setItem(0, 0, QTableWidgetItem("3.44"))
self.tableWidget.setItem(0, 1, QTableWidgetItem("5.3"))
self.tableWidget.setItem(1, 0, QTableWidgetItem("4.6"))
self.tableWidget.setItem(1, 1, QTableWidgetItem("1.2"))
self.tableWidget.setItem(2, 0, QTableWidgetItem("2.2"))
self.tableWidget.setItem(2, 1, QTableWidgetItem("4.4"))
# Table will fit the screen horizontally
self.tableWidget.horizontalHeader().setStretchLastSection(True)
self.tableWidget.horizontalHeader().setSectionResizeMode(
QHeaderView.Stretch)
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = App()
sys.exit(app.exec_())
I tried adding this self.tableWidget.setWordWrap(True) but this doesnt make any change. Can anyone give some good solution. Please help. Thanks
EDIT:
Also tried this :
self.tableWidget.horizontalHeader().setDefaultAlignment(QtCore.Qt.AlignHCenter | Qt.Alignment(QtCore.Qt.TextWordWrap))
But it didnt worked
In order to achieve what you need, you must set your own header and proceed with the following two assumptions:
the header must provide the correct size hint height according to the section contents in case the width of the column is not sufficient;
the text alignment must include the QtCore.Qt.TextWordWrap flag, so that the painter knows that it can wrap text;
Do note that, while the second aspect might be enough in some situations (as headers are normally tall enough to fit at least two lines), the first point is mandatory as the text might require more vertical space, otherwise some text would be cut out.
The first point requires to subclass QHeaderView and reimplement sectionSizeFromContents():
class WrapHeader(QtWidgets.QHeaderView):
def sectionSizeFromContents(self, logicalIndex):
# get the size returned by the default implementation
size = super().sectionSizeFromContents(logicalIndex)
if self.model():
if size.width() > self.sectionSize(logicalIndex):
text = self.model().headerData(logicalIndex,
self.orientation(), QtCore.Qt.DisplayRole)
if not text:
return size
# in case the display role is numeric (for example, when header
# labels are not defined yet), convert it to a string;
text = str(text)
option = QtWidgets.QStyleOptionHeader()
self.initStyleOption(option)
alignment = self.model().headerData(logicalIndex,
self.orientation(), QtCore.Qt.TextAlignmentRole)
if alignment is None:
alignment = option.textAlignment
# get the default style margin for header text and create a
# possible rectangle using the current section size, then use
# QFontMetrics to get the required rectangle for the wrapped text
margin = self.style().pixelMetric(
QtWidgets.QStyle.PM_HeaderMargin, option, self)
maxWidth = self.sectionSize(logicalIndex) - margin * 2
rect = option.fontMetrics.boundingRect(
QtCore.QRect(0, 0, maxWidth, 10000),
alignment | QtCore.Qt.TextWordWrap,
text)
# add vertical margins to the resulting height
height = rect.height() + margin * 2
if height >= size.height():
# if the height is bigger than the one provided by the base
# implementation, return a new size based on the text rect
return QtCore.QSize(rect.width(), height)
return size
class App(QWidget):
# ...
def createTable(self):
self.tableWidget = QTableWidget()
self.tableWidget.setHorizontalHeader(
WrapHeader(QtCore.Qt.Horizontal, self.tableWidget))
# ...
Then, to set the word wrap flag, there are two options:
set the alignment flag on the underlying model with setHeaderData() for each existing column:
# ...
model = self.tableWidget.model()
default = self.tableWidget.horizontalHeader().defaultAlignment()
default |= QtCore.Qt.TextWordWrap
for col in range(self.tableWidget.columnCount()):
alignment = model.headerData(
col, QtCore.Qt.Horizontal, QtCore.Qt.TextAlignmentRole)
if alignment:
alignment |= QtCore.Qt.TextWordWrap
else:
alignment = default
model.setHeaderData(
col, QtCore.Qt.Horizontal, alignment, QtCore.Qt.TextAlignmentRole)
Use a QProxyStyle to override the painting of the header, by applying the flag on the option:
# ...
class ProxyStyle(QtWidgets.QProxyStyle):
def drawControl(self, control, option, painter, widget=None):
if control in (self.CE_Header, self.CE_HeaderLabel):
option.textAlignment |= QtCore.Qt.TextWordWrap
super().drawControl(control, option, painter, widget)
if __name__ == '__main__':
app = QApplication(sys.argv)
app.setStyle(ProxyStyle())
ex = App()
sys.exit(app.exec_())
Finally, consider that:
using setSectionResizeMode with ResizeToContents or Stretch, along with setStretchLastSection, will always cause the table trying to use as much space as required by the headers upon showing the first time;
by default, QHeaderView sections are not clickable (which is a mandatory requirement for sorting) and the highlightSections property is also False; both QTableView and QTableWidget create their headers with those values as True, so when a new header is set you must explicitly change those aspects if sorting and highlighting are required:
self.tableWidget.setHorizontalHeader(
WrapHeader(QtCore.Qt.Horizontal, self.tableWidget))
self.tableWidget.horizontalHeader().setSectionsClickable(True)
self.tableWidget.horizontalHeader().setHighlightSections(True)
both sorting and section highlighting can create some issues, as the sort indicator requires further horizontal space and highlighted sections are normally shown with a bold font (but are shown normally while the mouse is pressed); all this might create some flickering and odd behavior; unfortunately, there's no obvious solution for these problems, but when using the QProxyStyle it's possible to avoid some flickering by overriding the font style:
def drawControl(self, control, option, painter, widget=None):
if control in (self.CE_Header, self.CE_HeaderLabel):
option.textAlignment |= QtCore.Qt.TextWordWrap
if option.state & self.State_Sunken:
option.state |= self.State_On
super().drawControl(control, option, painter, widget)

How to set selecting slider for a QTableWidget?

I'm looking for solution of merge discrete slider and QTableWidget (see attached screenshot). Slider is used as selecting pointer(instead of default selecting highlighter). How it can be implemented using Qt (PyQt)?
Small premise. Technically, according to StackOverflow standards, your question is not a very good one. I'll explain it at the end of this answer.
Getting what you are asking for is not easy, most importantly because sliders are not built for that purpose (and there are many UX reasons for which you should not do that, go to User Experience to ask about them).
The trick is to create a QSlider that has the table widget as a parent. Creating a widget with a parent ensures that the child widget will always be enclosed within the parent boundaries (this is only false for QMainWindow and QDialog descendants), as long as the widget is not added to the parent layout. This allows you to freely set its geometry (position and size).
In the following example I'm adding an internal QSlider, but the main issue about this widget is aligning it in such a way that its value positions are aligned with the table contents.
class GhostHeader(QtWidgets.QHeaderView):
'''
A "fake" vertical header that does not paint its sections
'''
def __init__(self, parent):
super().__init__(QtCore.Qt.Vertical, parent)
self.setSectionResizeMode(self.Fixed)
def paintEvent(self, event):
pass
class SliderTable(QtWidgets.QTableWidget):
def __init__(self, rows=0, columns=0, parent=None):
super().__init__(rows, columns, parent)
self.horizontalHeader().setStretchLastSection(True)
self.setHorizontalHeaderLabels(['Item table'])
self.setVerticalHeader(GhostHeader(self))
# create a slider that is a child of the table; there is no layout, but
# setting the table as its parent will cause it to be shown "within" it.
self.slider = QtWidgets.QSlider(QtCore.Qt.Vertical, self)
# by default, a slider has its maximum on the top, let's invert this
self.slider.setInvertedAppearance(True)
self.slider.setInvertedControls(True)
# show tick marks at each slider value, on both sides
self.slider.setTickInterval(1)
self.slider.setTickPosition(self.slider.TicksBothSides)
self.slider.setRange(0, max(0, self.rowCount() - 1))
# not necessary, but useful for wheel and click interaction
self.slider.setPageStep(1)
# disable focus on the slider
self.slider.setFocusPolicy(QtCore.Qt.NoFocus)
self.slider.valueChanged.connect(self.selectRowFromSlider)
self.slider.valueChanged.connect(self.updateSlider)
self.verticalScrollBar().valueChanged.connect(self.updateSlider)
self.model().rowsInserted.connect(self.modelChanged)
self.model().rowsRemoved.connect(self.modelChanged)
def selectRowFromSlider(self, row):
if self.currentIndex().isValid():
column = self.currentIndex().column()
else:
column = 0
self.setCurrentIndex(self.model().index(row, column))
def modelChanged(self):
self.slider.setMaximum(max(0, self.rowCount() - 1))
self.updateSlider()
def updateSlider(self):
slider = self.slider
option = QtWidgets.QStyleOptionSlider()
slider.initStyleOption(option)
style = slider.style()
# get the available extent of the slider
available = style.pixelMetric(style.PM_SliderSpaceAvailable, option, slider)
# compute the space between the top of the slider and the position of
# the minimum value (0)
deltaTop = (slider.height() - available) // 2
# do the same for the maximum
deltaBottom = slider.height() - available - deltaTop
# the vertical center of the first item
top = self.visualRect(self.model().index(0, 0)).center().y()
# the vertical center of the last
bottom = self.visualRect(self.model().index(self.model().rowCount() - 1, 0)).y()
# get the slider width and adjust the size of the "ghost" vertical header
width = self.slider.sizeHint().width()
left = self.frameWidth() + 1
self.verticalHeader().setFixedWidth(width // 2 + left)
viewGeo = self.viewport().geometry()
headerHeight = viewGeo.top()
# create the rectangle for the slider geometry
rect = QtCore.QRect(0, headerHeight + top, width, headerHeight + bottom - top // 2)
# adjust to the values computed above
rect.adjust(0, -deltaTop + 1, 0, -deltaBottom)
# translate it so that its center will be between the vertical header and
# the table contents
rect.translate(left, 0)
self.slider.setGeometry(rect)
# set the mask, in case the item view is scrolled, so that the top of the
# slider won't be shown in the horizontal header
visible = self.rect().adjusted(0, viewGeo.top(), 0, 0)
mask = QtGui.QPainterPath()
topLeft = slider.mapFromParent(visible.topLeft())
bottomRight = slider.mapFromParent(visible.bottomRight() + QtCore.QPoint(1, 1))
mask.addRect(QtCore.QRectF(topLeft, bottomRight))
self.slider.setMask(QtGui.QRegion(mask.toFillPolygon(QtGui.QTransform()).toPolygon()))
def currentChanged(self, current, previous):
super().currentChanged(current, previous)
if current.isValid():
self.slider.setValue(current.row())
def resizeEvent(self, event):
# whenever the table is resized (even when first shown) call the base
# implementation (which is required for correct drawing of items and
# selections), then update the slider
super().resizeEvent(event)
self.updateSlider()
class Test(QtWidgets.QWidget):
def __init__(self):
super().__init__()
layout = QtWidgets.QVBoxLayout(self)
self.table = SliderTable()
self.table.setRowCount(4)
self.table.setColumnCount(1)
self.table.setHorizontalHeaderLabels(['Item table'])
layout.addWidget(self.table)
for row in range(self.table.rowCount()):
item = QtWidgets.QTableWidgetItem('item {}'.format(row + 1))
item.setTextAlignment(QtCore.Qt.AlignCenter)
self.table.setItem(row, 0, item)
Why this question is not that good?
Well, it's dangerously close to the "I don't know how to do this, can you do it for me?" limit. You should provide any minimal, reproducible example (it doesn't matter if it doesn't work, you should do some research and show your efforts), and the question is a bit vague, even after some clarifications in the comment sections.
Long story short: if it's too hard and you can't get it working, you probably still need some studying and exercise before you can achieve it. Be patient, study the documentation: luckily, Qt docs are usually well written, so it's just a matter of time.

PyQt Image(pixmap) gets cropped when other content changes width in a widget

I'm making a table-like widget that displays an image, the file name, and two box-selection areas. I have two objects 'grid_row' & 'grid_table' (both using QGridLayout), grid_row being a single row and grid_table containing x number of grid_rows (I'm designing it like this because it's simply easier to keep track of my custom properties).
The tool looks like this
The final layout is a QVBoxLayout, then from top to bottom, I have QHBoxLayout(the one with a label and combobox), grid_row(for the headers 1,2,3), a scroll_area that contains the grid_table with each one being grid_rows. Lastly another QHBoxLayout for the buttons.
Each grid_row contains a 'image-widget', and two region labels(QLabel). The image widget contains a label(I used setPixmap for display) and a pushbutton. Here are my grid_row and image_widget classes:
class grid_row(QWidget):
def __init__(self, parent=None):
super().__init__()
#self.frame = frame_main()
self.grid_layout = QGridLayout()
self.grid_layout.setSpacing(50)
self.image_widget = image_widget()
self.grid_layout.addWidget(self.image_widget, 0, 0, 1, 1, Qt.AlignHCenter)
self.region_2 = QLabel('null')
self.grid_layout.addWidget(self.region_2, 0, 2, 1, 1, Qt.AlignHCenter)
self.setLayout(self.grid_layout)
self.region_1 = QLabel('null')
self.grid_layout.addWidget(self.region_1, 0, 1, 1, 1, Qt.AlignHCenter)
class image_widget(QWidget):
def __init__(self, parent=None):
super().__init__()
self.initUI()
def initUI(self):
self.setAcceptDrops(True)
self.image_widget_layout = QHBoxLayout()
self.image_widget_label = QLabel()
self.image_widget_label.setPixmap(QPixmap('default.png').scaled(96, 54))
self.image_widget_layout.addWidget(self.image_widget_label)
self.img_btn = QPushButton()
self.img_btn.setEnabled(False)
self.img_btn.setText('Drag Here!')
self.image_widget_layout.addWidget(self.img_btn)
self.setLayout(self.image_widget_layout)
if __name__ == '__main__':
app = QApplication(sys.argv)
widget = QWidget()
layout = QVBoxLayout()
grid_row = grid_row()
layout.addWidget(grid_row)
btn = QPushButton('press')
btn.clicked.connect(lambda: grid_row.region_1.setText('[0,0,1920,1080]'))
layout.addWidget(btn)
widget.setLayout(layout)
scroll_area = QScrollArea()
scroll_area.setWidget(widget)
scroll_area.show()
sys.exit(app.exec_())
So currently, I've implemented events that allow me to drag images into the image_widget and click the push button to modify the two regions that are framed (format: [x1, y1, x2, y2]). The problem is that when I do that(e.g. region values go from 'null' to say '[20,20, 500, 500]', the image gets squished because now the labels are taking up more width.
I realize that some size policy needs to be set (and maybe other properties) but I don't know which property to use and on which widget. I want the image to remain the same. Maybe stretch out the width of each column for the grid_row?
To clarify, I want the label containing the pixmap to remain the same size (always 96*54) and fully displayed(not cropped or stretched) at all times.
I've provided the a simplified executable code to display my problem, the classes are the same as my code, I just only put grid_row inside the scroll_area and added a button to change one of the values of the region to simulate the situation. Can provide additional code if needed. Thanks in advance!
Wow sometimes the answer is really one extra line of code...
So the documentation mentions that QScrollArea by default honors the size of its widget. Which is why when I changed the region (to a value that's wider/ more text) the widget does not auto adjust.
I needed to add
scroll_area.setWidgetResizable(True)
to allow the widget to resize wider thus prompting the scroll bars to appear. This way my pixmap image doesn't get cropped from not having enough space.
The easiest way would be to add size constraints to the label before adding to the layout
self.image_widget_label.adjustSize()
self.image_widget_label.setFixedSize(self.image_widget_label.size())
self.image_widget_layout.addWidget(self.image_widget_label)
adjustSize would resize the label depending on the contents.
The more difficult way is to answer the questions :
"when I change the size of the overall window, how do I want this
particular item to behave? When the window is at its minimal size,
which items do I want hidden or out of view? When the window is full
size, where do I want empty spots?"
To answer these better read a bit on Qt Layout management

Empty spaces between Widgets even with setSpacing(0)

from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
import sys
data = {'dia': 23, 'mes': 8, 'ano': 2017}
class FormConfigBkpBD(QWidget):
senha = None
pathLine = None
def __init__(self, parent=None,instancia=None):
super(FormConfigBkpBD, self).__init__(parent)
mainLayout = QVBoxLayout()
mainLayout.setSpacing(0)
mainLayout.setContentsMargins(0, 0, 0, 0)
self.senha = QLineEdit()
self.senha.setEchoMode(QLineEdit.Password)
self.senha.setFixedSize(385, 25)
self.pathLine = QLineEdit()
self.pathLine.setFixedSize(355,25)
self.senha.setContentsMargins(0,0,0,0)
self.pathLine.setContentsMargins(0,0,0,0)
mainLayout.addWidget(self.pathLine,0,Qt.AlignTop)
mainLayout.addWidget(self.senha,0,Qt.AlignTop)
self.setWindowTitle("ConfiguraĆ§Ć£o de Backup - Banco de Dados")
self.setFixedSize(460,640)
self.setLayout(mainLayout)
def main():
app = QApplication(sys.argv)
bd = FormConfigBkpBD()
bd.show()
app.exec()
if __name__ == "__main__" :
main()
The above code shows only two widgets (QLineEdit), the original window has much more widgets, this is only for post here.
Why, even with setSpacing(0) and setContentsMargins(0,0,0,0) there's a space between widgets?
It's because of the combination between the fixed size of your window and the layout you are using. The vertical layout tries to distribute evenly (unless told otherwise) all of its children vertically and fill the whole window.
Here you have two widgets so the first one goes in the first cell and the second one goes in the second cell. Since you haven't set any alignment the default behaviour is present that is top left corner of each cell is where the widget is positioned. The rest is just filling the free space of the parent window which the layout is part of.
If you want not space between the widgets
either remove the fixed size of the window
or put more widgets in the layout until it's "full".
The first option might not be what you want to do (otherwise you would have probably not set it to fixed size in the first place). The second can be achieved by simply using a QSpacerItem (vertical one) that will take care of the empty space for you and push the widgets to the top where you want these to be:
mainLayout.addStretch(1)
You can also add the spacer manually if you need more control over its behaviour:
self.spacer = QSpacerItem(20, 40, QSizePolicy.Minimum, QSizePolicy.Expanding) # or Fixed
mainLayout.addItem(self.spacer)

Layout not adding correctly, seems that its not erasing previous widgets

After every click on a "Add Item..." button, I want a row(label, button) to be appended to the layout (below that same button).
So, it should add one row per click.
Problem is it adds the following:
1st click: 1 row added (total item rows = 1) (correct)
2nd click: 2 rows added (total item rows = 3) (should be 2)
3rd click: 3 rows added (total item rows = 6) (should be 3)
Here's the relevant code:
from PySide import QtCore
from PySide import QtGui
import sys
class Form(QtGui.QDialog):
items = []
def __init__(self, parent = None):
super(Form, self).__init__(parent)
self.btn = QtGui.QPushButton("Add Item...")
self.btn.clicked.connect(self.item_toggle)
self.layout = self.initial_view()
self.setLayout(self.layout)
def item_toggle(self, add = True):
layout = self.layout
if add:
string = ("25468 5263.35 54246") #####random text
self.items.append(string)
for item in self.items:
rem_btn = QtGui.QPushButton("X")
rem_btn.clicked.connect(self.remove_item)
layout.addRow(item, rem_btn)
self.setLayout(layout)
def remove_item(self, ):
#self.items.pop() #something to delete that item
self.add_item("False") #redraw items part
def initial_view(self, ):
layout = QtGui.QFormLayout()
#adding to layout
layout.addRow(self.btn)
return layout
app = QtGui.QApplication(sys.argv)
form = Form()
form.show()
app.exec_()
I figure its not erasing the previous widgets, but I can't quiet figure it out. Also, a way to to remove the items(remove_item function), would also help me out.
I hope I explained well and you get what I'm trying to do...
Any help will be appreciated. Thanks in advance
To prevent adding additional items to your list just remove the for loop and just do the following:
rem_btn = QtGui.QPushButton("X")
rem_btn.clicked.connect(self.remove_item)
layout.addRow(string, rem_btn)
What you have to know about the addRow call, is that this add your QPushButton in the second column, and auto-creates a QLabel for the first column. So when you want to remove the row, you will have to remove both the button and the label.
Now about the remove. I guess the easiest way to start would be to find out which button is asking to be removed.
sending_button = self.sender()
At this point you will need to get access to the QLabel. Luckily there is a call on the layout called labelForField which will return the QLabel associated with your QPushButton
labelWidget = self.layout.labelForField(sending_button)
Then to remove the actual widgets
sending_button.deleteLater()
if labelWidget:
labelWidget.deleteLater()

Categories

Resources