PyQt QHBoxLayout inside QPushButton text is being truncated - python

I'm trying to place a checkbox and some text inside a button, but I'm having trouble getting the button to expand wide enough to see the full text.
self.check = QtGui.QCheckBox("long text", self)
self.checkLayout = QtGui.QHBoxLayout()
self.checkLayout.addWidget(self.check)
self.checkButton = QtGui.QPushButton(None, self)
self.checkButton.setLayout(self.checkLayout)
I've tried various combinations of adding stretches, setting the size policy, setting margins and styles etc. but haven't had any luck so far.
Thanks

Add a layout to the button. Place the checkbox inside the layout. Looks like this is the only way for QWidget to automatically track a size of its subwidgets.
But QWidget doesn't automatically resize itself to fit its content. You must invoke adjustSize function to do it.
class TestWidget(QtGui.QWidget):
def __init__(self, *args):
QtGui.QWidget.__init__(self, *args)
self.check = QtGui.QCheckBox("very very long text")
self.checkLayout = QtGui.QHBoxLayout()
self.checkLayout.addWidget(self.check)
self.checkButton = QtGui.QPushButton()
self.checkButton.setLayout(self.checkLayout)
self.checkButton.adjustSize()
self.checkButton.setParent(self)
UPD: There is a problem with adjustSize. This function doesn't work for me if I call it after adding checkButton to the screen:
# works
self.checkButton.adjustSize()
self.checkButton.setParent(self)
# doesn't work
self.checkButton.setParent(self)
self.checkButton.adjustSize()
My solution is manual resizing instead of calling adjustSize:
self.checkButton.setFixedSize(self.checkLayout.sizeHint())

Related

How to delete QPushButtons from inside QtWidgets?

In PyQt5 I am dynamically adding QPushButtons, is there a way to delete them based on some label value.
I am dynamically adding buttons in the following manner:
for i in range(0, len(self.all_saved)):
self.button = QPushButton("X", self)
self.button.setStyleSheet("background-color: red")
self.button.resize(20, 20)
self.button.clicked.connect(lambda ch, i=i: self.future(i))
self.button.move(self.all_rect[i][0], self.all_rect[i][1])
self.button.show()
Once the user clicks the button 'X' it should delete itself, thats basically all I am trying to do here, as to why I cant use QVBoxLayout is because all the buttons would be placed on different x,y co ordinates please let me know if you have any suggestions?
I know we can do this easily with QVBoxLayout or QHBoxLayout but is there a way to do it directly on QtWidgets.QWidget
You just have to invoke the deleteLater() method that will delete the object and notify(using destroyed signal) the layout that the widget was deleted. Note: Don't use self.button as it is useless.
for i in range(0, len(self.all_saved)):
button = QPushButton("X", self)
button.setStyleSheet("background-color: red")
button.resize(20, 20)
button.clicked.connect(button.deleteLater)
button.move(self.all_rect[i][0], self.all_rect[i][1])
button.show()
If you want to delete certain buttons based on some condition then you must use the sender () method to get the button and call deleteLater:
button.clicked.connect(self.handle_clicked)
def handle_clicked(self):
button = self.sender()
if some_condition:
button.deleteLater()

How to prevent QComboBox from displaying unnecessary scrollbar

The code below, which is based on an example from zetcode.com, creates a single combo box. There are several issues with the resulting dialog, but the following are especially annoying:
PyQt displays a vertical scrollbar for the combo box, although there is plenty of space to display the entire list of options without a scrollbar.
I've tried to move the combo box to a position near the upper-left corner of the window, but this isn't working.
#!/usr/bin/python
import sys
from PyQt5.QtWidgets import QWidget, QLabel, QHBoxLayout, QComboBox, QApplication
class Example(QWidget):
def __init__(self):
super().__init__()
self.setFixedWidth(400)
self.setFixedHeight(500)
self.initUI()
def initUI(self):
hbox = QHBoxLayout()
self.lbl = QLabel('Animals', self)
self.lbl.setStyleSheet('font-size:11pt')
combo = QComboBox(self)
combo.addItem('bear')
combo.addItem('cat')
combo.addItem('dog')
combo.addItem('dolphin')
combo.addItem('elephant')
combo.addItem('fish')
combo.addItem('frog')
combo.addItem('horse')
combo.addItem('rabbit')
combo.addItem('rat')
combo.addItem('shark')
combo.addItem('snake')
combo.addItem('tiger')
combo.addItem('whale')
combo.activated[str].connect(self.onActivated)
hbox.addWidget(combo)
hbox.setSpacing(20)
hbox.addWidget(self.lbl)
self.setContentsMargins(20, 20, 20, 20)
self.setLayout(hbox)
combo.move(20, 60)
self.setWindowTitle('QComboBox')
self.show()
def onActivated(self, text):
self.lbl.setText(text)
self.lbl.adjustSize()
def main():
app = QApplication(sys.argv)
ex = Example()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
There are two wrong assumptions in the question.
the list of a QComboBox is a popup widget, it doesn't care (nor it should) about the available space the combobox might have: consider it as much as a context menu, which just pops out over the window, possibly going outside its boundaries if it requires more space (and that's just because those boundaries are meaningless to the menu);
the combo has been added to a layout manager, which takes care about resizing and positioning its (managed) child widgets, and that's why you cannot manually "move" them: the layout already sets the geometries automatically on its own everytime the widget is resized (which also happen when it's shown the first time), so any attempt to use move(), resize() or setGeometry() is completely useless;
When adding a widget to a layout, the default behavior is to make it occupy as much space as possible; since a QComboBox is one of those widgets that have a fixed size, the result is that it's actually centered (vertically and horizontally) in the space the layout is "assigning" to it, and this is clearly visible in your case because you set a fixed size for the container widget that is much bigger than what its contents would need.
There are two ways to align those widgets on top:
add the alignment arguments to addWidget:
hbox.addWidget(combo, alignment=QtCore.Qt.AlignTop)
hbox.addWidget(self.lbl, alignment=QtCore.Qt.AlignTop)
note that this won't give you good results in your case, because the label and the combo box have different heights, so the label might look "higher" than the combo;
use a QVBoxLayout layout as main layout for the widget, add the horizontal layout to it and then add a stretch after that (a stretch on a box layout is a "spacer" that tries to occupy as much space as possible)
# ...
mainLayout = QVBoxLayout()
mainLayout.addLayout(hbox)
mainLayout.addStretch()
self.setLayout(mainLayout)
PS: if you need to add lots of (string only) elements to a QComboBox, use addItems() instead of individually adding each of them.

Tkinter selected Entry() and Spinbox() field text background and foreground when not in focus

I'm using tkiner ttk widgets and configuring everything with ttk.Style().
When I use the 'clam' theme, widgets like ttk.Entry() and ttk.Spinbox() have this default setting that when text is selected but the widget is not in focus anymore the background of the text is gray and the foreground is white. Here a visual example when the cursor is already in entry line, but text in spinbox still has some custom color. The coloring of the text goes back to unselected style only when text in other widget is selected
I've been searching for a long time how this can be changed. The closest I have found is that tk.Text() widget actually has this option of inactiveselectbackground (there isn't inactiveselectforeground tho). But that is not something that works for ttk.Entry() or ttk.Spinbox().
Also when using regular tk.Entry() or tk.Spinbox() or using the default theme for ttk widgets, the background and foreground does not change to a new color. I suppose the options still might be there, but they are not set to anything specific.
Getting back to the specific problem - does anyone know if it is possible to change color of background and foreground of text in ttk.Entry() or ttk.Spinbox() when the widget is not in focus anymore? Maybe some workaround to this problem?
To change the foreground and background when the widget is inactive (out in focus) then with the help of and binds we can configure the widgets in such a way that they will change their foreground and background when they lose focus and when gain focus back.
Practically we can first save original foreground and background values of that widget and then use it and callbacks.
Here I've made a class Entry which does exactly you want. I added inactivebackground and inactiveforeground configure options.
class Entry(tk.Entry):
def __init__(self, master=None, **kw):
self.inactivebackground = kw.pop('inactivebackground', 'white')
self.inactiveforeground = kw.pop('inactiveforeground', 'black')
super().__init__(master=master, **kw)
self.org_bg = self['background']
self.org_fg = self['foreground']
self.bind('<FocusIn>', self._focusin, '+')
self.bind('<FocusOut>', self._focusout, '+')
self._focusout()
def _focusout(self, evt=None):
self['background'] = self.inactivebackground
self['foreground'] = self.inactiveforeground
def _focusin(self, evt=None):
self['background'] = self.org_bg
self['foreground'] = self.org_fg
Have a look at this example:-
import tkinter as tk
root = tk.Tk()
var = tk.StringVar(value="Hello! How are you doing! :)")
Entry(root, textvariable=var, inactivebackground='pink',
inactiveforeground='blue').pack()
Entry(root, textvariable=var, inactivebackground='orange',
inactiveforeground='red').pack()
root.mainloop()
Similarly, you can modify a Spinbox to do the same thing. Also just by replacing the inherited class tk.Entry with ttk.Entry will work with ttk style widgets as well but remember not everything is configurable directly with ttk style widgets.
Power of inheritance
There is one trick you can do to save time and space, by creating a support class that can be inherited along with the desired widget to have the same functionality.
class supportinactive(object):
def __init__(self, inactivebackground, inactiveforeground):
self.inactivebackground = inactivebackground
self.inactiveforeground = inactiveforeground
self.org_bg = self['background']
self.org_fg = self['foreground']
self.bind('<FocusIn>', self._focusin, '+')
self.bind('<FocusOut>', self._focusout, '+')
self._focusout()
def _focusout(self, evt=None):
self['background'] = self.inactivebackground
self['foreground'] = self.inactiveforeground
def _focusin(self, evt=None):
self['background'] = self.org_bg
self['foreground'] = self.org_fg
How to use it?
From the above supportinactive class we can add this functionality to widget like so
class Entry(tk.Entry, supportinactive):
def __init__(self, master=None, **kw):
inactivebg = kw.pop('inactivebackground', 'white')
inactivefg = kw.pop('inactiveforeground', 'black')
tk.Entry.__init__(self, master=master, **kw)
supportinactive.__init__(self, inactivebg, inactivefg)
# Spinbox will have the same functionality too.
class Spinbox(tk.Spinbox, supportinactive):
def __init__(self, master=None, **kw):
inactivebg = kw.pop('inactivebackground', 'white')
inactivefg = kw.pop('inactiveforeground', 'black')
tk.Spinbox.__init__(self, master=master, **kw)
supportinactive.__init__(self, inactivebg, inactivefg)
If you want to understand how this inheritance is working check out these answers:-
Multiple inheritance passing arguments to constructors.
A really good answer explaining how arguments can be passed with multiple inheritance.
I'm posting as an answer, so I can add pictures and be specific about the configuration that is not changing.
The above answer is a very nice approach to changing those configurations but will work only when a tk.Entry() widget is use. I'm using ttk.Entry() with 'clam' theme. And 'clam' theme is the one that introduces this additional configuration for background/foreground when text is selected and the widget goes out of focus.
What the above-given answer modifies is:
background/foreground of unselected text when out of focus.
What I want to modify (get rid off):
background/foreground of the selected text when out of focus when using ttk.Entry and 'clam'.
Here I have applied the above answer when using ttk.Entry() and 'clam' as theme.
import tkinter as tk
from tkinter import ttk
root = tk.Tk()
root.geometry('200x100')
style = ttk.Style()
style.theme_use('clam')
style.configure('New.TEntry',
fieldbackground='red',
foreground='green',
selectbackground='black',
selectforeground='yellow')
class NewEntry(ttk.Entry):
def __init__(self, parent, style):
super().__init__(parent)
self.style = style
self['style'] = 'New.TEntry'
self.bind('<FocusIn>', self._focusin, '+')
self.bind('<FocusOut>', self._focusout, '+')
def _focusout(self, evt=None):
self.style.configure('New.TEntry',
fieldbackground='red',
foreground='green',
selectbackground='black',
selectforeground='yellow')
def _focusin(self, evt=None):
self.style.configure('New.TEntry',
fieldbackground='blue',
foreground='pink',
selectbackground='black',
selectforeground='yellow')
entry1 = NewEntry(root, style).pack()
entry2 = tk.Entry(root).pack()
root.mainloop()
Here is a walkthrough:
entry1 is in focus, no text is selected. Color properties set by fieldbackground and foreground.
entry1 is in focus, half of the text is selected. Color properties of selected text are defined by selectbackground and selectforeground
entry1 is out of focus. The cursor is in entry2. The color of the text that is still selected is governed by some unknown properties to me. selectbackground and selectforeground out of focus have effect only on the part that was not selected.
When new text is selected somewhere else - the colors will be defined as they should by selectbackground and selectforeground
As I mentioned in the original posting that tk.Text() for instance has an option inactiveselectbackground. So I believe somewhere in the setting something like that exists. It is just a question of how to gain access to that property.
P.S.
This also made me wonder what is the easiest way to modify style within a class. I haven't seen anyone doing that. I figured passing the style inside the class would allow me to do it. Maybe there's a better way.

PyQt5 add custom QWidget to QLayout

Currently I am trying to add a custom QWidget class to a QVBoxLayout. The problem I'm getting is that the widget doesn't appear at all in the layout. I even tried setting the minimum size of the QWidget because I thought that the widget wasn't showing because it's default size was set to zero.
This is a simplification of what the class looks like:
class myWidget(QWidget):
def __init__(self):
super().__init__()
# Slider
self.mySlider = QSlider(Qt.Horizontal)
self.mySlider.setRange(-360, 360)
# LCD Screen
self.lcd = QLCDNumber()
self.lcd.setMinimumHeight(45)
self.lcd.setMaximumHeight(75)
# set Size
self.setMinimumSize(QSize(400,300))
I removed the signals and slots made between the slider and the LCD screen because I am not worried here about the functionality. Only the fact that I get a gray area of QSize(400,300) directly between the two buttons in the following code:
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
#Create Widgets to be Added to Central Widget
self.w1 = QPushButton("First")
self.w2 = myWidget()
self.w3 = QPushButton("Third")
#Set Central Widget and VBox
self.central_widget = QWidget()
self.setCentralWidget(self.central_widget)
self.layout = QVBoxLayout()
self.central_widget.setLayout(self.layout)
#Add widgets
self.layout.addWidget(self.w1)
self.layout.addWidget(self.w2)
self.layout.addWidget(self.w3)
So what I'm simply doing is creating the 3 widgets, and placing them into the QVBoxLayout within the central widget. The 2 button widgets w1 and w3 appear but my custom widget doesn't appear and increasing the size of the widget via setMinimumSize only adds grey spacing between w1 and w3.
So the widget is there it just isn't visible for some reason. I am pretty new to PyQt so please explain why this has happened.
QWidgets are just containers for other widgets. A QWidget without any layout and subwidgets will just look like empty space unless you're doing some custom painting or styling.
In your example, you're not actually adding any sub-widgets to your custom widget. In order to add a sub-widget to another widget, you need to either set the parent of the subwidget, or add the subwidget to the layout of the parent widget (which automatically re-parents the subwidget)
class myWidget(QWidget):
def __init__(self):
super().__init__()
# Slider
self.mySlider = QSlider(Qt.Horizontal)
Here, you are creating a QSlider, but it's not actually owned by MyWidget, it's going to end up being owned by Qt, and I would expect it to be drawn in the upper left hand corner of your main window.
In order to make this a subwidget of MyWidget you need to set the parent and add it to a layout.
class myWidget(QWidget):
def __init__(self):
super().__init__()
self.myLay = QVBoxLayout()
self.setLayout(self.myLay)
# Notice self is being passed in to set the parent
self.mySlider = QSlider(Qt.Horizontal, self)
# You need to add widgets to a layout to position them properly
self.myLay.addWidget(self.mySlider)

How exactly does addStretch work in QBoxLayout?

I'm doing a PyQt4 tutorial about box layouts. But I dont understand how addStretch works.
If i use vbox.addStretch(1) and hbox.addStretch(1), the two buttons appear down-right. Why?
if i comment vbox.addStretch(1) and hbox.addStretch(1) out, the two buttons appear in the center of my window, and they're deformable horizontally, but not vertically. Why?
theres no difference if I change the value "1"... so what does the value do?
Below is the code I'm using:
import sys
from PyQt4 import QtGui
class BoxLayout(QtGui.QWidget):
def __init__(self, parent=None):
QtGui.QWidget.__init__(self, parent)
self.setWindowTitle('box layout')
ok = QtGui.QPushButton("OK")
cancel = QtGui.QPushButton("Cancel")
vbox = QtGui.QHBoxLayout()
vbox.addStretch(1)
vbox.addWidget(ok)
vbox.addWidget(cancel)
hbox = QtGui.QVBoxLayout()
hbox.addStretch(1)
hbox.addLayout(vbox)
self.setLayout(hbox)
self.resize(100, 100)
app = QtGui.QApplication(sys.argv)
qb = BoxLayout()
qb.show()
sys.exit(app.exec_())
The addStretch method adds a QSpacerItem to the end of a box layout. A QSpacerItem is an adjustable blank space.
Using vbox.addStretch(1) will add a zero-width spacer-item that
expands vertically from the top of the layout downwards.
Using hbox.addStretch(1) will add a zero-height spacer-item that
expands horizontally from the left of the layout rightwards.
Without stretch, the layout will be determined by the
sizePolicy
of the widgets. For a QPushButton, this is
QSizePolicy.Fixed
for the vertical dimension, and
QSizePolicy.Minimum
for the horizontal dimension. If you wanted the buttons to expand in
both directions, you could do something like this:
ok.setSizePolicy(QtGui.QSizePolicy.Minimum,
QtGui.QSizePolicy.Minimum)
cancel.setSizePolicy(QtGui.QSizePolicy.Minimum,
QtGui.QSizePolicy.Minimum)
The argument passed to addStretch changes the stretch factor. If you
add a second stretch after the ok button:
vbox = QtGui.QHBoxLayout()
vbox.addStretch(1)
vbox.addWidget(ok)
vbox.addStretch(2)
vbox.addWidget(cancel)
you will see that the second spacer item grows twice as fast as the
first. And if you set the first stretch to zero, it won't grow at
all.
If you want more information, see the Layout Management article in the Qt docs. It's also a good idea to use Qt Designer to experiment with stuff like this, as it gives you immediate visual feedback and shows you all the default values of the various properties involved.

Categories

Resources