I'm trying to create in PyQt6 an input field (location: bottom right of the window) and next to It on the right an "Enter" button (I'm using pg.GraphicView()). I can't use the PySide library because of some interaction problems with the rest of my code. How can I achieve that?
I'm using the following code for generating a button but I can't figure out how to place It at the bottom right of the current window:
view = pg.GraphicsView()
l = pg.GraphicsLayout()
view.setCentralItem(l)
view.show()
proxy = QGraphicsProxyWidget()
button = QPushButton("ENTER")
proxy.setWidget(button)
view.addItem(proxy)
Regarding the input field I tried to implement different things without using PySide but they didn't worked.
The pg.GraphicsView class is actually a subclass of QGraphicsView, which is a standard QWidget that inherits from QAbstractScrollArea.
This means that we can potentially add any child widget to it without interfering with the contents of the scene and also ignoring possible transformations (scaling, rotation, etc.).
The solution, in fact, is quite simple: set the view as parent of the widget (either by using the view as argument in the constructor, or by calling setParent()). Then, what's left is to ensure that the widget geometry is always consistent with the view, so we need to wait for resize events and set the new geometry based on the new size. To achieve this, the simplest solution is to create a subclass.
For explanation purposes, I've implemented a system that allows to set widgets for any "corner" of the view, and defaults to the bottom right corner.
Note: since this question could also be valid for Qt5, in the following code I'm using the "old" enum style (Qt.Align...), for newer versions of Qt (and mandatory for PyQt6), you need to change to Qt.Alignment.Align....
class CustomGraphicsView(pg.GraphicsView):
toolWidgets = None
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.toolWidgets = {}
def setToolWidget(self, widget, position=Qt.AlignBottom|Qt.AlignRight):
position = Qt.AlignmentFlag(position)
current = self.toolWidgets.get(position)
if current:
current.deleteLater()
self.toolWidgets[position] = widget
widget.resize(widget.sizeHint())
# ensure that the widget is reparented
widget.setParent(self)
# setParent() automatically hides widgets, we need to
# explicitly call show()
widget.show()
self._updateToolWidgets()
def _updateToolWidgets(self):
if not self.toolWidgets:
return
viewGeo = self.rect()
for pos, widget in self.toolWidgets.items():
rect = widget.rect()
if pos & Qt.AlignLeft:
rect.moveLeft(0)
elif pos & Qt.AlignRight:
rect.moveRight(viewGeo.right())
elif pos & Qt.AlignHCenter:
rect.moveLeft(viewGeo.center().x() - rect.width() / 2)
if pos & Qt.AlignTop:
rect.moveTop(0)
elif pos & Qt.AlignBottom:
rect.moveBottom(viewGeo.bottom())
elif pos & Qt.AlignVCenter:
rect.moveTop(viewGeo.center().y() - rect.height() / 2)
widget.setGeometry(rect)
def resizeEvent(self, event):
super().resizeEvent(event)
self._updateToolWidgets()
# ...
view = CustomGraphicsView()
l = pg.GraphicsLayout()
view.setCentralItem(l)
view.show()
container = QFrame(autoFillBackground=True, objectName='container')
container.setStyleSheet('''
QFrame#container {
background: palette(window);
border: 1px outset palette(mid);
border-radius: 2px;
}
''')
lineEdit = QLineEdit()
button = QPushButton("ENTER")
frameLayout = QHBoxLayout(container)
frameLayout.setContentsMargins(0, 0, 0, 0)
frameLayout.addWidget(lineEdit)
frameLayout.addWidget(button)
view.setToolWidget(container)
bottomLeftButton = QPushButton('Something')
view.setToolWidget(bottomLeftButton, Qt.AlignLeft|Qt.AlignBottom)
i have a comboBox inside of a tableWidget and the verticalHeader DefaultSectionSize is 60.
class MainWindow(QtWidgets.QMainWindow):
def __init__(self, parent=None):
QtWidgets.QMainWindow.__init__(self,parent)
self.table = QTableWidget()
self.setCentralWidget(self.table)
self.table.verticalHeader().setDefaultSectionSize(60)
self.table.setColumnCount(2)
self.table.setRowCount(2)
data = ["1","2"]
for i in range(2):
item = QTableWidgetItem(data[i])
self.table.setItem(i,0,item)
self.combo_sell = QComboBox()
self.combo_sell.setMaximumHeight(30)
self.table.setCellWidget(i,1,self.combo_sell)
But since i set the maximum size of the comboBox to 30, it stays in the top of the item.
I want to know if there's a way to align it to the center.
When setting an index widget, the view tries to set the widget geometry based on the visualRect() of the index. Setting a fixed dimension forces the widget to align itself to the default origin, which is usually the top left corner.
The only way to vertically center a widget with a fixed height is to use a container with a vertical box layout and add the combo to it:
for i in range(2):
item = QTableWidgetItem(data[i])
item.setTextAlignment(Qt.AlignCenter)
self.table.setItem(i,0,item)
container = QWidget()
layout = QVBoxLayout(container)
combo_sell = QComboBox()
layout.addWidget(combo_sell)
combo_sell.setMaximumHeight(30)
self.table.setCellWidget(i, 1, container)
Note: setting instance attributes in a for loop is pointless, as the reference is lost every time the cycle loops.
If you need a simple reference to the combo, you can set it as an attribute of the widget:
container.combo_sell = QComboBox()
In this way you can easily access it when required:
widget = self.table.cellWidget(row, column)
if widget and hasattr(widget, 'combo'):
combo = widget.combo
print(combo.currentIndex())
Note that the reference is created for the python wrapper of the widget, and that behavior might change in future versions of Qt. A better and safer way to achieve this would be to use a subclass, which would also allow easier access to the combo:
class TableCombo(QWidget):
def __init__(self):
super().__init__()
layout = QVBoxLayout(self)
self.combo = QComboBox()
layout.addWidget(self.combo)
self.combo.setMaximumHeight(30)
self.currentIndex = self.combo.currentIndex
self.setCurrentIndex = self.combo.setCurrentIndex
self.addItems = self.combo.addItems
# ...
combo_sell = TableCombo()
self.table.setCellWidget(i, 1, combo_sell)
# ...
combo = self.table.cellWidget(row, column)
print(combo.currentIndex())
I want to put a QLineEdit and QLable to the same cell in QTableWidget. This cell widget has created the code below I modified from the internet.
class HiddenLabel(QLabel):
'''
QLable hide when mouse pressed
'''
def __init__(self, buddy, taskline, parent = None):
super(HiddenLabel, self).__init__(parent)
self.setFixedHeight(30)
self.buddy = buddy
self.taskline = taskline
# When it's clicked, hide itself and show its buddy
def mousePressEvent(self, event):
# left click to edit
if event.button() == QtCore.Qt.LeftButton:
self.hide()
self.buddy.setText(self.taskline.plain_text)
self.buddy.show()
self.buddy.setFocus() # Set focus on buddy so user doesn't have to click again
class EditableCell(QWidget):
'''
QLineEdit show when HiddenLabel is hidden
'''
def __init__(self, taskline, parent = None):
super(EditableCell, self).__init__(parent)
self.taskline = taskline
# Create ui
self.myEdit = QLineEdit()
self.myEdit.setFixedHeight(30)
self.myEdit.hide() # Hide line edit
self.myEdit.editingFinished.connect(self.textEdited)
# Create our custom label, and assign myEdit as its buddy
self.myLabel = HiddenLabel(self.myEdit, self.taskline)
self.myLabel.setText(self.taskline.enrich_text())
# Change vertical size policy so they both match and you don't get popping when switching
#self.myLabel.setSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed)
# Put them under a layout together
hLayout = QHBoxLayout()
hLayout.addWidget(self.myLabel)
hLayout.addWidget(self.myEdit)
self.setLayout(hLayout)
def textEdited(self):
# If the input is left empty, revert back to the label showing
print('edit finished')
print(self.myEdit.text())
taskline = TaskLine()
taskline.parser(self.myEdit.text())
self.taskline = taskline
self.myLabel.setText(taskline.enrich_text())
self.myEdit.hide()
self.myLabel.show()
The cell will change between QLineEdit and QLabel by left click.
As figured in the screenshot, I want to remove the blank space between the cell border and cell widget.
I think it can be adjusted by style setting, but I didn't find any useful documents about Qt style setting. I hope someone could give some use
You must set the layout margins to 0:
hLayout.setContentsMargins(0, 0, 0, 0)
since as the docs point out they are dependent on the style and platform:
void QLayout::setContentsMargins(int left, int top, int right, int
bottom)
Sets the left, top, right, and bottom margins to use around
the layout.
By default, QLayout uses the values provided by the style. On most
platforms, the margin is 11 pixels in all directions.
I am creating a form with some QTextEdit widgets.
The default height of the QTextEdit exceeds a single line of text and as the contents' height exceeds the QTextEdit's height, it creates a scroll-bar to scroll the content.
I would like to override this behaviour to create a QTextEdit that would rather wrap its height to its contents. This means that the default height would be one line and that on wrapping or entering a new line, the QTextEdit would increase its height automatically. Whenever the contents height exceeds the QTextEdit's height, the latter should not create a scroll bar but simply increase in height.
How can I go about doing this? Thanks.
This is almost exactly like a question I answer the other day about making a QTextEdit adjust its height in reponse to content changes: PySide Qt: Auto vertical growth for TextEdit Widget
I am answering instead of marking a duplicate as I suspect its possible you want a variation on this. Let me know if you want me to expand this answer:
The other question had multiple parts. Here is the excerpt of the growing height widget:
class Window(QtGui.QDialog):
def __init__(self):
super(Window, self).__init__()
self.resize(600,400)
self.mainLayout = QtGui.QVBoxLayout(self)
self.mainLayout.setMargin(10)
self.scroll = QtGui.QScrollArea()
self.scroll.setWidgetResizable(True)
self.scroll.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
self.mainLayout.addWidget(self.scroll)
scrollContents = QtGui.QWidget()
self.scroll.setWidget(scrollContents)
self.textLayout = QtGui.QVBoxLayout(scrollContents)
self.textLayout.setMargin(10)
for _ in xrange(5):
text = GrowingTextEdit()
text.setMinimumHeight(50)
self.textLayout.addWidget(text)
class GrowingTextEdit(QtGui.QTextEdit):
def __init__(self, *args, **kwargs):
super(GrowingTextEdit, self).__init__(*args, **kwargs)
self.document().contentsChanged.connect(self.sizeChange)
self.heightMin = 0
self.heightMax = 65000
def sizeChange(self):
docHeight = self.document().size().height()
if self.heightMin <= docHeight <= self.heightMax:
self.setMinimumHeight(docHeight)
the following code sets a QTextEdit widget to the height of the content:
# using QVBoxLayout in this example
grid = QVBoxLayout()
text_edit = QTextEdit('Some content. I make this a little bit longer as I want to see the effect on a widget with more than one line.')
# read-only
text_edit.setReadOnly(True)
# no scroll bars in this example
text_edit.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
text_edit.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
text_edit.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
# you can set the width to a specific value
# text_edit.setFixedWidth(400)
# this is the trick, we nee to show the widget without making it visible.
# only then the document is created and the size calculated.
# Qt.WA_DontShowOnScreen = 103, PyQt does not have this mapping?!
text_edit.setAttribute(103)
text_edit.show()
# now that we have a document we can use it's size to set the QTextEdit's size
# also we add the margins
text_edit.setFixedHeight(text_edit.document().size().height() + text_edit.contentsMargins().top()*2)
# finally we add the QTextEdit to our layout
grid.addWidget(text_edit)
I hope this helps.
I would like to add some text to the left end side, the right end side and on the slider as in the figure below
I don't understand how I can add text on top of a widget
here the minimal example of the Qscrollbar (without texts)
from PyQt5.QtGui import *
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
import sys
class Viewer(QMainWindow):
def __init__(self, parent=None):
super(Viewer, self).__init__()
self.parent = parent
self.centralWidget = QWidget()
self.setCentralWidget(self.centralWidget)
self.mainVBOX_param_scene = QVBoxLayout()
self.paramPlotV = QVBoxLayout()
self.horizontalSliders = QScrollBar(Qt.Horizontal)
self.horizontalSliders.setMinimum(0)
self.horizontalSliders.setMaximum(10)
self.horizontalSliders.setPageStep(1)
self.paramPlotV.addWidget(self.horizontalSliders)
self.centralWidget.setLayout(self.paramPlotV)
def main():
app = QApplication(sys.argv)
app.setStyle('Windows')
ex = Viewer(app)
ex.showMaximized()
sys.exit(app.exec())
if __name__ == '__main__':
main()
There are two possible approaches, and both of them use QStyle to get the geometry of the slider and the subPage/addPage rectangles (the "spaces" outside the slider and within its buttons, if they are visible).
Subclass QScrollBar and override paintEvent()
Here we override the paintEvent() of the scroll bar, call the base class implementation (which paints the scroll bar widget) and draw the text over it.
To get the rectangle where we're going to draw, we create a QStyleOptionSlider, which is a QStyleOption sub class used for any slider based widget (including scroll bars); a QStyleOption contains all the information QStyle needs to draw graphical elements, and its subclasses allow QStyle to find out how to draw complex elements such as scroll bars or control the behavior against any mouse event.
class PaintTextScrollBar(QScrollBar):
preText = 'pre text'
postText = 'post text'
sliderText = 'slider'
def paintEvent(self, event):
# call the base class paintEvent, which will draw the scrollbar
super().paintEvent(event)
# create a suitable styleoption and "init" it to this instance
option = QStyleOptionSlider()
self.initStyleOption(option)
painter = QPainter(self)
# get the slider rectangle
sliderRect = self.style().subControlRect(QStyle.CC_ScrollBar,
option, QStyle.SC_ScrollBarSlider, self)
# if the slider text is wider than the slider width, adjust its size;
# note: it's always better to add some horizontal margin for text
textWidth = self.fontMetrics().width(self.sliderText)
if textWidth > sliderRect.width():
sideWidth = (textWidth - sliderRect.width()) / 2
sliderRect.adjust(-sideWidth, 0, sideWidth, 0)
painter.drawText(sliderRect, Qt.AlignCenter,
self.sliderText)
# get the "subPage" rectangle and draw the text
subPageRect = self.style().subControlRect(QStyle.CC_ScrollBar,
option, QStyle.SC_ScrollBarSubPage, self)
painter.drawText(subPageRect, Qt.AlignLeft|Qt.AlignVCenter, self.preText)
# get the "addPage" rectangle and draw its text
addPageRect = self.style().subControlRect(QStyle.CC_ScrollBar,
option, QStyle.SC_ScrollBarAddPage, self)
painter.drawText(addPageRect, Qt.AlignRight|Qt.AlignVCenter, self.postText)
This approach is very effective and may be fine for most simple cases, but there will be problems whenever the text is wider than the size of the slider handle, since Qt decides the extent of the slider based on its overall size and the range between its minimum and maximum values.
While you can adjust the size of the rectangle you're drawing text (as I've done in the example), it will be far from perfect: whenever the slider text is too wide it might draw over the "pre" and "post" text, and make the whole scrollbar very ugly if the slider is near the edges, since the text might cover the arrow buttons:
Note: the result of a "non adjusted" text rectangle would be the same as the first scroll bar in the image above, with the text "clipped" to the slider geometry.
Use a proxy style
QProxyStyle is a QStyle descendant that makes subclassing easier by providing an easy way to override only methods of an existing style.
The function we're most interested in is drawComplexControl(), which is what Qt uses to draw complex controls like spin boxes and scroll bars. By implementing this function only, the behavior will be exactly the same as the paintEvent() method explained above, as long as you apply the custom style to a standard QScrollBar.
What a (proxy) style could really help with is being able to change the overall appearance and behavior of almost any widget.
To be able to take the most of its features, I've implemented another QScrollBar subclass, allowing much more customization, while overriding other important QProxyStyle functions.
class TextScrollBarStyle(QProxyStyle):
def drawComplexControl(self, control, option, painter, widget):
# call the base implementation which will draw anything Qt will ask
super().drawComplexControl(control, option, painter, widget)
# check if control type and orientation match
if control == QStyle.CC_ScrollBar and option.orientation == Qt.Horizontal:
# the option is already provided by the widget's internal paintEvent;
# from this point on, it's almost the same as explained above, but
# setting the pen might be required for some styles
painter.setPen(widget.palette().color(QPalette.WindowText))
margin = self.frameMargin(widget) + 1
sliderRect = self.subControlRect(control, option,
QStyle.SC_ScrollBarSlider, widget)
painter.drawText(sliderRect, Qt.AlignCenter, widget.sliderText)
subPageRect = self.subControlRect(control, option,
QStyle.SC_ScrollBarSubPage, widget)
subPageRect.setRight(sliderRect.left() - 1)
painter.save()
painter.setClipRect(subPageRect)
painter.drawText(subPageRect.adjusted(margin, 0, 0, 0),
Qt.AlignLeft|Qt.AlignVCenter, widget.preText)
painter.restore()
addPageRect = self.subControlRect(control, option,
QStyle.SC_ScrollBarAddPage, widget)
addPageRect.setLeft(sliderRect.right() + 1)
painter.save()
painter.setClipRect(addPageRect)
painter.drawText(addPageRect.adjusted(0, 0, -margin, 0),
Qt.AlignRight|Qt.AlignVCenter, widget.postText)
painter.restore()
def frameMargin(self, widget):
# a helper function to get the default frame margin which is usually added
# to widgets and sub widgets that might look like a frame, which usually
# includes the slider of a scrollbar
option = QStyleOptionFrame()
option.initFrom(widget)
return self.pixelMetric(QStyle.PM_DefaultFrameWidth, option, widget)
def subControlRect(self, control, option, subControl, widget):
rect = super().subControlRect(control, option, subControl, widget)
if (control == QStyle.CC_ScrollBar
and isinstance(widget, StyledTextScrollBar)
and option.orientation == Qt.Horizontal):
if subControl == QStyle.SC_ScrollBarSlider:
# get the *default* groove rectangle (the space in which the
# slider can move)
grooveRect = super().subControlRect(control, option,
QStyle.SC_ScrollBarGroove, widget)
# ensure that the slider is wide enough for its text
width = max(rect.width(),
widget.sliderWidth + self.frameMargin(widget))
# compute the position of the slider according to the
# scrollbar value and available space (the "groove")
pos = self.sliderPositionFromValue(widget.minimum(),
widget.maximum(), widget.sliderPosition(),
grooveRect.width() - width)
# return the new rectangle
return QRect(grooveRect.x() + pos,
(grooveRect.height() - rect.height()) / 2,
width, rect.height())
elif subControl == QStyle.SC_ScrollBarSubPage:
# adjust the rectangle based on the slider
sliderRect = self.subControlRect(
control, option, QStyle.SC_ScrollBarSlider, widget)
rect.setRight(sliderRect.left())
elif subControl == QStyle.SC_ScrollBarAddPage:
# same as above
sliderRect = self.subControlRect(
control, option, QStyle.SC_ScrollBarSlider, widget)
rect.setLeft(sliderRect.right())
return rect
def hitTestComplexControl(self, control, option, pos, widget):
if control == QStyle.CC_ScrollBar:
# check click events against the resized slider
sliderRect = self.subControlRect(control, option,
QStyle.SC_ScrollBarSlider, widget)
if pos in sliderRect:
return QStyle.SC_ScrollBarSlider
return super().hitTestComplexControl(control, option, pos, widget)
class StyledTextScrollBar(QScrollBar):
def __init__(self, sliderText='', preText='', postText=''):
super().__init__(Qt.Horizontal)
self.setStyle(TextScrollBarStyle())
self.preText = preText
self.postText = postText
self.sliderText = sliderText
self.sliderTextMargin = 2
self.sliderWidth = self.fontMetrics().width(sliderText) + self.sliderTextMargin + 2
def setPreText(self, text):
self.preText = text
self.update()
def setPostText(self, text):
self.postText = text
self.update()
def setSliderText(self, text):
self.sliderText = text
self.sliderWidth = self.fontMetrics().width(text) + self.sliderTextMargin + 2
def setSliderTextMargin(self, margin):
self.sliderTextMargin = margin
self.sliderWidth = self.fontMetrics().width(self.sliderText) + margin + 2
def sizeHint(self):
# give the scrollbar enough height for the font
hint = super().sizeHint()
if hint.height() < self.fontMetrics().height() + 4:
hint.setHeight(self.fontMetrics().height() + 4)
return hint
There's a lot of difference between using the basic paintEvent override, applying the style to a standard QScrollBar and using a full "style-enabled" scroll bar with a fully implemented subclass; as you can see it's always possible that the current style (or the baseStyle chosen for the custom proxy style) might not be very friendly in its appearance:
What changes between the two (three) approaches and what you will finally decide to use depends on your needs; if you need to add other features to the scroll bar (or add more control to text contents or their apparance) and the text is not very wide, you might want to go with subclassing; on the other hand, the QProxyStyle approach might be useful to control other aspects or elements too.
Remember that if the QStyle is not set before the QApplication constructor, it's possible that the applied style won't be perfect to work with: as opposed with QFont and QPalette, QStyle is not propagated to the children of the QWidget it's applied to (meaning that the new proxy style has to be notified about the parent style change and behave accordingly).
class HLine(QFrame):
def __init__(self):
super().__init__()
self.setFrameShape(self.HLine|self.Sunken)
class Example(QWidget):
def __init__(self):
QWidget.__init__(self)
layout = QVBoxLayout(self)
layout.addWidget(QLabel('Base subclass with paintEvent override, small text:'))
shortPaintTextScrollBar = PaintTextScrollBar(Qt.Horizontal)
layout.addWidget(shortPaintTextScrollBar)
layout.addWidget(QLabel('Same as above, long text (text rect adjusted to text width):'))
longPaintTextScrollBar = PaintTextScrollBar(Qt.Horizontal)
longPaintTextScrollBar.sliderText = 'I am a very long slider'
layout.addWidget(longPaintTextScrollBar)
layout.addWidget(HLine())
layout.addWidget(QLabel('Base QScrollBar with drawComplexControl override of proxystyle:'))
shortBasicScrollBar = QScrollBar(Qt.Horizontal)
layout.addWidget(shortBasicScrollBar)
shortBasicScrollBar.sliderText = 'slider'
shortBasicScrollBar.preText = 'pre text'
shortBasicScrollBar.postText = 'post text'
shortBasicScrollBar.setStyle(TextScrollBarStyle())
layout.addWidget(QLabel('Same as above, long text (text rectangle based on slider geometry):'))
longBasicScrollBar = QScrollBar(Qt.Horizontal)
layout.addWidget(longBasicScrollBar)
longBasicScrollBar.sliderText = 'I am a very long slider'
longBasicScrollBar.preText = 'pre text'
longBasicScrollBar.postText = 'post text'
longBasicScrollBar.setStyle(TextScrollBarStyle())
layout.addWidget(HLine())
layout.addWidget(QLabel('Subclasses with full proxystyle implementation, all available styles:'))
for styleName in QStyleFactory.keys():
scrollBar = StyledTextScrollBar()
layout.addWidget(scrollBar)
scrollBar.setSliderText('Long slider with {} style'.format(styleName))
scrollBar.setStyle(TextScrollBarStyle(QStyleFactory.create(styleName)))
scrollBar.valueChanged.connect(self.setScrollBarPreText)
scrollBar.setPostText('Post text')
for scrollBar in self.findChildren(QScrollBar):
scrollBar.setValue(7)
def setScrollBarPreText(self, value):
self.sender().setPreText(str(value))
if __name__ == '__main__':
app = QApplication(sys.argv)
example = Example()
example.show()
sys.exit(app.exec_())