I have a column of auto-generated buttons which, if there are too many of, can squash UI elements in the window. Therefore, I want to automatically convert the single column of buttons - nominally inside of a QVBoxLayout referred to as self.main_layout - into a multi-column affair by:
Removing the buttons from self.main_layout
Adding them to alternating new columns represented by QVBoxLayouts
Changing self.main_layout to a QHBoxLayout
Adding the new columns to this layout
My attempt simply results in the buttons staying in a single column but now don't even resize to fill the QSplitter frame they occupy:
app = QApplication(sys.argv)
window = TestCase()
app.exec_()
class TestCase(QMainWindow):
def __init__(self):
super().__init__()
test = QWidget()
self.layout = QVBoxLayout()
test.setLayout(self.layout)
for i in range(10):
temp_btn = QPushButton(str(i))
temp_btn.pressed.connect(self.multi_col)
self.layout.addWidget(temp_btn)
self.setCentralWidget(test)
#pyqtSlot()
def multi_col(self):
cols = [QVBoxLayout(), QVBoxLayout()]
while self.layout.count():
child = self.layout.takeAt(0)
if child.widget():
self.layout.removeItem(child)
cols[0].addItem(child)
cols[1], cols[0] = cols[0], cols[1]
self.layout = QHBoxLayout()
self.layout.addLayout(cols[0])
self.layout.addLayout(cols[1])
Any glaringly obvious thing I'm doing wrong here?
Replacing a layout of a QWidget is not so simple with assigning another object to the variable that stored the reference of the other layout. In a few lines of code you are doing:
self.layout = Foo()
widget.setLayout(self.layout)
self.layout = Bar()
An object is not the same as a variable, the object itself is the entity that performs the actions but the variable is only a place where the reference of the object is stored. For example, objects could be people and variables our names, so if they change our name it does not imply that they change us as a person.
The solution is to remove the QLayout using sip.delete and then set the new layout:
import sys
from PyQt5.QtCore import pyqtSlot
from PyQt5.QtWidgets import (
QApplication,
QHBoxLayout,
QMainWindow,
QPushButton,
QVBoxLayout,
QWidget,
)
import sip
class TestCase(QMainWindow):
def __init__(self):
super().__init__()
test = QWidget()
self.setCentralWidget(test)
layout = QVBoxLayout(test)
for i in range(10):
temp_btn = QPushButton(str(i))
temp_btn.pressed.connect(self.multi_col)
layout.addWidget(temp_btn)
#pyqtSlot()
def multi_col(self):
cols = [QVBoxLayout(), QVBoxLayout()]
old_layout = self.centralWidget().layout()
while old_layout.count():
child = old_layout.takeAt(0)
widget = child.widget()
if widget is not None:
old_layout.removeItem(child)
cols[0].addWidget(widget)
cols[1], cols[0] = cols[0], cols[1]
sip.delete(old_layout)
lay = QHBoxLayout(self.centralWidget())
lay.addLayout(cols[0])
lay.addLayout(cols[1])
def main():
app = QApplication(sys.argv)
window = TestCase()
window.show()
app.exec_()
if __name__ == "__main__":
main()
I'd like to propose an alternative solution, which is to use a QGridLayout and just change the column of the widgets instead of setting a new layout everytime. The "trick" is that addWidget() always adds the widget at the specified position, even if it was already part of the layout, so you don't need to remove layout items.
Obviously, the drawback of this approach is that if the widgets have different heights, every row depends on the minimum required height of all widgets in that row, but since the OP was about using buttons, that shouldn't be the case.
This has the major benefit that the switch can be done automatically with one function, possibly by setting a maximum column number to provide further implementation.
In the following example the multi_col function actually increases the column count until the maximum number is reached, then it resets to one column again.
class TestCase(QMainWindow):
def __init__(self):
super().__init__()
test = QWidget()
self.layout = QGridLayout()
test.setLayout(self.layout)
for i in range(10):
temp_btn = QPushButton(str(i))
temp_btn.clicked.connect(self.multi_col)
self.layout.addWidget(temp_btn)
self.setCentralWidget(test)
self.multiColumns = 3
self.columns = 1
def multi_col(self):
maxCol = 0
widgets = []
for i in range(self.layout.count()):
item = self.layout.itemAt(i)
if item.widget():
widgets.append(item.widget())
row, col, rSpan, cSpan = self.layout.getItemPosition(i)
maxCol = max(col, maxCol)
if maxCol < self.multiColumns - 1:
self.columns += 1
else:
self.columns = 1
row = col = 0
for widget in widgets:
self.layout.addWidget(widget, row, col)
col += 1
if col > self.columns - 1:
col = 0
row += 1
Note: I changed to the clicked signal, as it's usually preferred against pressed due to the standard convention of buttons (which "accept" a click only if the mouse button is released inside it), and in your case it also creates two issues:
visually, the UI creates confusion, with the pressed button becoming unpressed in a different position (since the mouse button is released outside its actual and, at that point, different geometry);
conceptually, because if the user moves the mouse again inside the previously pressed button before releasing the mouse, it will trigger pressed once again;
Related
In the following code:
import sys
from PyQt5 import QtWidgets
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QApplication, QWidget, QHBoxLayout, QTableWidget, QPushButton
class Window(QtWidgets.QMainWindow):
def __init__(self, method):
super(Window, self).__init__()
mainWidget = QWidget()
mainLayout = QHBoxLayout(mainWidget)
table = QTableWidget(10, 3)
button1 = QPushButton("Play")
button2 = QPushButton("Cancel")
mainLayout.addWidget(table)
mainLayout.addWidget(button1)
mainLayout.addWidget(button2)
if (method == 1):
rtnValue = mainLayout.setAlignment(button1, Qt.AlignTop)
print("Method 1:", rtnValue)
elif (method == 2):
rtnValue = mainLayout.setAlignment(mainLayout, Qt.AlignTop)
print("Method 2:", rtnValue)
else:
rtnValue = mainLayout.setAlignment(Qt.AlignTop)
print("Method X:", rtnValue)
self.setCentralWidget(mainWidget)
self.show()
if __name__ == "__main__":
print("python QLayoutAlignment.py[ <MethodToUse=1>")
app = QApplication(sys.argv)
method = 1 if (len(sys.argv) < 2) else int(sys.argv[1])
GUI = Window(method)
sys.exit(app.exec_())
when I call the program like below with mainLayout.setAlignment(button1, Qt.AlignTop) being called, it works as expected with the "Play" button aligned at the top and "Cancel" button aligned at the center vertically. I also found the documentation for bool QLayout::setAlignment(QWidget *w, Qt::Alignment alignment) although in Qt.
python QLayoutAlignment.py 1
However when I call the the program like below with mainLayout.setAlignment(mainLayout, Qt.AlignTop) being called, it does not seem to work. All the buttons are vertically center aligned. I interpreted the Qt documentation of bool QLayout::setAlignment(QLayout *l, Qt::Alignment alignment)) as "it align all the added widget of the layout to the set alignment". So what does this function actually do (when is it used)?
python QLayoutAlignment.py 2
Lastly, I also saw another example from Center and top align a QVBoxLayout. When I call the program like below with mainLayout.setAlignment(Qt.AlignTop) being called, it also does not work with all the buttons vertically center aligned. For this one I could not find its documentation. So what does this function actually do (when is it used) and where can I find its documentation?
python QLayoutAlignment.py 3
The .setAlignment method which accepts a layout is used for aligning sub-layouts, that is child layouts you've added to the parent layout using .addLayout.
Below is a little demo based on your code.
import sys
from PyQt5 import QtWidgets
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QApplication, QWidget, QHBoxLayout, QTableWidget, QPushButton
class Window(QtWidgets.QMainWindow):
def __init__(self, method=0):
super(Window, self).__init__()
mainWidget = QWidget()
self.mainLayout = QHBoxLayout(mainWidget)
table = QTableWidget(10, 3)
self.button1 = QPushButton("Play")
self.button2 = QPushButton("Cancel")
self.subLayout = QHBoxLayout()
buttona1 = QPushButton("1")
buttona1.clicked.connect(self.clicked1)
buttona2 = QPushButton("2")
buttona2.clicked.connect(self.clicked2)
buttona3 = QPushButton("3")
buttona3.clicked.connect(self.clicked3)
buttona4 = QPushButton("4")
buttona4.clicked.connect(self.clicked4)
self.subLayout.addWidget(buttona1)
self.subLayout.addWidget(buttona2)
self.subLayout.addWidget(buttona3)
self.subLayout.addWidget(buttona4)
self.mainLayout.addWidget(table)
self.mainLayout.addWidget(self.button1)
self.mainLayout.addWidget(self.button2)
self.mainLayout.addLayout(self.subLayout)
self.setCentralWidget(mainWidget)
self.show()
def clicked1(self):
rtnValue = self.mainLayout.setAlignment(self.button1, Qt.AlignTop)
print("Method 1:", rtnValue)
def clicked2(self):
rtnValue = self.mainLayout.setAlignment(self.mainLayout, Qt.AlignTop)
print("Method 2:", rtnValue)
def clicked3(self):
rtnValue = self.mainLayout.setAlignment(Qt.AlignTop)
print("Method 3:", rtnValue)
def clicked4(self):
rtnValue = self.mainLayout.setAlignment(self.subLayout, Qt.AlignTop)
print("Method 4:", rtnValue)
if __name__ == "__main__":
print("python QLayoutAlignment.py[ <MethodToUse=1>")
app = QApplication(sys.argv)
method = 1 if (len(sys.argv) < 2) else int(sys.argv[1])
GUI = Window(method)
sys.exit(app.exec_())
You'll notice if you trigger this self.mainLayout.setAlignment(self.mainLayout, Qt.AlignTop) the return value is False. This is telling you that the layout you're aligning could not be found in the current layout. Since you're calling .setAlignment on mainLayout the layout you're affecting must be in that layout.
In the 4th method, I've added a sub-layout, and as you can see this ( rtnValue = self.mainLayout.setAlignment(self.subLayout, Qt.AlignTop)) works as expected and returns True.
First of all, it's important to understand that the Qt layout system works by using layout items (see QLayoutItem), which are abstract items that are used as virtual containers for objects: widgets, spacers and layouts (when nested layouts are used). Every QLayout is, in fact, a subclass of QLayoutItem.
Using setAlignment means setting the alignment of the specified layout item:
layout.setAlignment(item, alignment) sets the alignment of item, which has to be directly contained in layout;
layout.setAlignment(alignment) sets the alignment of layout related to its parent layout; note that this does not mean that items inside layout will use the specified alignment;
Your second case, mainLayout.setAlignment(mainLayout, Qt.AlignTop), does not work and returns False because mainLayout is, obviously, not "contained" in mainLayout. In fact, if you carefully read the documentation, it says:
returns true if w/l is found in this layout (not including child layouts);
In your third case, you don't see any result because mainLayout is the top layout for the widget, and since there's no parent layout the alignment seems to be ignored. As specified above, using layout.setAlignment(alignment) does not set the alignment of child items, but only of the layout item of layout. If you add that mainLayout to another layout, you will see that the alignment is respected for the layout.
Setting the layout alignment is rarely used, also because it's often counterintuitive: one might led to believe that setting the alignment of a layout will align its contents, but that's not the case.
To clarify, consider that setting the layout alignment is almost the same as creating a widget with that layout, and adding that widget with the specified alignment. With that in mind, it makes more sense: you're not aligning the contents of the widget, but the widget itself.
Consider the following example: besides the table on the left (used for comparison), I'm adding a layout on the left by specifying its alignment, then I'm adding a widget on the right by specifying the alignment of the widget for the main layout. As you can see, they appear exactly the same.
from PyQt5 import QtCore, QtWidgets
import sys
app = QtWidgets.QApplication(sys.argv)
test = QtWidgets.QWidget()
mainLayout = QtWidgets.QHBoxLayout(test)
# a very tall table to show the difference in alignment
mainLayout.addWidget(QtWidgets.QTableView(minimumHeight=300))
leftLayout = QtWidgets.QHBoxLayout()
# setting the alignment of leftLayout relative to mainLayout
leftLayout.setAlignment(QtCore.Qt.AlignTop)
leftLayout.addWidget(QtWidgets.QTableView(maximumHeight=100))
leftLayout.addWidget(QtWidgets.QPushButton())
mainLayout.addLayout(leftLayout)
rightWidget = QtWidgets.QWidget()
# adding the widget to mainLayout by aligning it on top as with leftLayout
mainLayout.addWidget(rightWidget, alignment=QtCore.Qt.AlignTop)
rightLayout = QtWidgets.QHBoxLayout(rightWidget)
rightLayout.addWidget(QtWidgets.QTableView(maximumHeight=100))
rightLayout.addWidget(QtWidgets.QPushButton())
test.show()
sys.exit(app.exec_())
Finally, if you want to align widgets, you either specify the alignment for each widget, or you add nested layout.
When many widgets are going to be added with the same alignment, the nested layout is usually the best solution: in your case, add a vertical layout to the main layout, then add horizontal layout to the vertical for the buttons, and add a stretch to the vertical to "push" the horizontal layout on top.
Alternatively, you can use a grid layout and eventually use spacers to ensure that the widgets are "aligned" as required.
from PyQt5 import QtCore, QtWidgets
import sys
app = QtWidgets.QApplication(sys.argv)
test = QtWidgets.QWidget()
mainLayout = QtWidgets.QHBoxLayout(test)
class Button(QtWidgets.QPushButton):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.setSizePolicy(
QtWidgets.QSizePolicy.Preferred,
QtWidgets.QSizePolicy.Preferred
)
noAlignGroup = QtWidgets.QGroupBox('no alignment')
mainLayout.addWidget(noAlignGroup)
noAlignLayout = QtWidgets.QHBoxLayout(noAlignGroup)
noAlignLayout.addWidget(QtWidgets.QTableView())
noAlignLayout.addWidget(Button())
noAlignLayout.addWidget(Button())
widgetAlignGroup = QtWidgets.QGroupBox('addWidget(widget, alignment)')
mainLayout.addWidget(widgetAlignGroup)
widgetAlignLayout = QtWidgets.QHBoxLayout(widgetAlignGroup)
widgetAlignLayout.addWidget(QtWidgets.QTableView())
widgetAlignLayout.addWidget(Button(), alignment=QtCore.Qt.AlignTop)
widgetAlignLayout.addWidget(Button(), alignment=QtCore.Qt.AlignTop)
layoutAlignGroup = QtWidgets.QGroupBox('nestedLayout.setAlignment()')
mainLayout.addWidget(layoutAlignGroup)
layoutAlignLayout = QtWidgets.QHBoxLayout(layoutAlignGroup)
layoutAlignLayout.addWidget(QtWidgets.QTableView())
buttonLayout = QtWidgets.QHBoxLayout()
layoutAlignLayout.addLayout(buttonLayout)
buttonLayout.setAlignment(QtCore.Qt.AlignTop)
buttonLayout.addWidget(Button())
buttonLayout.addWidget(Button())
stretchGroup = QtWidgets.QGroupBox('nestedLayout + stretch')
mainLayout.addWidget(stretchGroup)
stretchLayout = QtWidgets.QHBoxLayout(stretchGroup)
stretchLayout.addWidget(QtWidgets.QTableView())
rightLayout = QtWidgets.QVBoxLayout()
stretchLayout.addLayout(rightLayout)
buttonLayout = QtWidgets.QHBoxLayout()
rightLayout.addLayout(buttonLayout)
buttonLayout.addWidget(Button())
buttonLayout.addWidget(Button())
rightLayout.addStretch()
gridAlignGroup = QtWidgets.QGroupBox('grid + spacer')
mainLayout.addWidget(gridAlignGroup)
gridLayout = QtWidgets.QGridLayout(gridAlignGroup)
gridLayout.addWidget(QtWidgets.QTableView(), 0, 0, 2, 1)
gridLayout.addWidget(Button(), 0, 1)
gridLayout.addWidget(Button(), 0, 2)
spacer = QtWidgets.QSpacerItem(1, 50, vPolicy=QtWidgets.QSizePolicy.Expanding)
gridLayout.addItem(spacer, 1, 1)
test.show()
sys.exit(app.exec_())
I am implement my project using pyqt5. Currently, I have a window including many widget. Now, I want to remove some widgets. The window looks like:
Now, I want to remove the 'name1' widget including the QLabel and QPushButton.
However, after removing all 'name1' widgets, the 'name2' widgets including QLabel and QPushButton can not self-adapte with the window, like:
All my code is:
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
import sys
class Window(QDialog):
def __init__(self):
super().__init__()
self.initGUI()
self.show()
def initGUI(self):
layout = QVBoxLayout()
self.setLayout(layout)
removeLayout = QHBoxLayout()
self.__removeText = QLineEdit()
self.__removeBtn = QPushButton('Remove')
self.__removeBtn.clicked.connect(self.remove)
removeLayout.addWidget(self.__removeText)
removeLayout.addWidget(self.__removeBtn)
ROIsLayout = QVBoxLayout()
for name in ['name1', 'name2']:
subLayout = QHBoxLayout()
subText = QLabel(name)
subText.setObjectName(name)
subBtn = QPushButton(name)
subBtn.setObjectName(name)
subLayout.addWidget(subText)
subLayout.addWidget(subBtn)
ROIsLayout.addLayout(subLayout)
layout.addLayout(removeLayout)
layout.addLayout(ROIsLayout)
self.__ROIsLayout = ROIsLayout
def remove(self, checked=False):
name = self.__removeText.text()
while True:
child = self.__ROIsLayout.takeAt(0)
if child == None:
break
while True:
subChild = child.takeAt(0)
if subChild == None:
break
obName = subChild.widget().objectName()
if name == obName:
widget = subChild.widget()
widget.setParent(None)
child.removeWidget(widget)
self.__ROIsLayout.removeWidget(widget)
del widget
if __name__ == '__main__':
app = QApplication(sys.argv)
window = Window()
sys.exit(app.exec_())
update:
Actually, the issue may be the takeAt. The following code is workable:
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
import sys
class Window(QDialog):
def __init__(self):
super().__init__()
self.initGUI()
self.show()
def initGUI(self):
layout = QVBoxLayout()
self.setLayout(layout)
removeLayout = QHBoxLayout()
self.__removeText = QLineEdit()
self.__removeBtn = QPushButton('Remove')
self.__removeBtn.clicked.connect(self.remove)
removeLayout.addWidget(self.__removeText)
removeLayout.addWidget(self.__removeBtn)
ROIsLayout = QVBoxLayout()
for name in ['name1', 'name2']:
subLayout = QHBoxLayout()
subLayout.setObjectName(name)
subText = QLabel(name, parent=self)
subText.setObjectName(name)
subBtn = QPushButton(name, parent=self)
subBtn.setObjectName(name)
subLayout.addWidget(subText)
subLayout.addWidget(subBtn)
ROIsLayout.addLayout(subLayout)
print(name, subLayout, subText, subBtn)
layout.addLayout(removeLayout)
layout.addLayout(ROIsLayout)
self.__ROIsLayout = ROIsLayout
self.record = [subLayout, subText, subBtn]
def remove(self, checked=False):
layout = self.record[0]
txt = self.record[1]
btn = self.record[2]
layout.removeWidget(txt)
txt.setParent(None)
txt.deleteLater()
layout.removeWidget(btn)
btn.setParent(None)
btn.deleteLater()
if __name__ == '__main__':
app = QApplication(sys.argv)
window = Window()
sys.exit(app.exec_())
But, I have printed the QLabel/QPushButton in the self.record, and I find it is the same with that from child.takeAt(0).widget().
The main issue in your code is that you're constantly using takeAt(). The result is that all items in the __ROIsLayout layout will be removed from it (but not deleted), which, in your case, are the sub layouts. This is clearly not a good approach: only the widgets with the corresponding object name will be actually deleted, while the others will still be "owned" by their previous parent, will still be visible at their previous position and their geometries won't be updated since they're not managed by the layout anymore.
There are multiple solutions to your question, all depending on your needs.
If you need to remove rows from a layout, I'd consider setting the object name on the layout instead, and look for it using self.findChild().
Also consider that, while Qt allows setting the same object name for more than one object, that's not suggested.
Finally, while using del is normally enough, it's usually better to call deleteLater() for all Qt objects, which ensures that Qt correctly removes all objects (and related parentship/connections).
Another possibility, for this specific case, is to use a QFormLayout.
So, I'm working on an application and I'm using QTableWidget to generate a table. I want to put the table at the center of the window but I can't find a way to do this, it takes way too much space and its stucked at the top left of the window. I'm putting the table and a button in a QVBoxLayout, there's some blank space after the table (at the bottom and the right) and then the button, far away from the table.
Thats how its looking like
And thats how i want it to be
Right now my code is like this:
from PyQt5.QtWidgets import QHeaderView, QPushButton, QMainWindow, QApplication, QMenuBar, QAction, QFileDialog, QWidget, QTableView, QVBoxLayout, QHBoxLayout, QTableWidget, QTableWidgetItem
from PyQt5.QtCore import QAbstractTableModel, Qt
from PyQt5 import QtGui
import sys
class MyApp(QMainWindow):
def __init__(self):
super().__init__()
self.createWindow()
self.show()
def createWindow(self):
self.setWindowTitle('Pós Graduação')
self.setWindowIcon(QtGui.QIcon('icon.ico'))
self.setGeometry(300, 100, 700, 600)
self.table_widget = TableWidget(self)
self.setCentralWidget(self.table_widget)
class TableWidget(QWidget):
def __init__(self, parent):
super(TableWidget, self).__init__(parent)
self.layout = QVBoxLayout(self)
self.creatingTable(parent)
#self.setLayout(self.layout)
def creatingTable(self, parent):
tableWidget = QTableWidget()
tableWidget.setRowCount(6)
tableWidget.setColumnCount(4)
tableWidget.horizontalHeader().setVisible(False)
tableWidget.verticalHeader().setVisible(False)
header = tableWidget.horizontalHeader()
header.setSectionResizeMode(2, QHeaderView.ResizeToContents)
header.setSectionResizeMode(3, QHeaderView.ResizeToContents)
self.layout.addWidget(tableWidget)
self.button1 = QPushButton("Button 1")
self.layout.addWidget(self.button1)
self.setLayout(self.layout)
if __name__ == '__main__':
App = QApplication(sys.argv)
App.setStyle('Fusion')
window = MyApp()
sys.exit(App.exec())
An item view has a default size "hint" for Qt, which is the size that the widget suggests as the best to show its contents and make it as usable as possible. Also, each widget has a sizePolicy property, which tells how the widget behaves when it's part of a layout (should it shrink, grow, have a fixed size, etc).
Item views like QTableWidget (and its ancestor, QTableView) don't explicitly expose their contents, and that's for obvious reasons: a table could have thousands of rows and columns, and the layout shouldn't really care about them.
To be able to resize the table to its contents, the table has to cycle through all of them, and to do so it's better to do it whenever the contents change their sizes. The best approach is probably to connect to the signals the model and header provide, and set a fixed size each time they are fired, which is something that is better done with subclassing.
class TableWidgetSubclass(QTableWidget):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# update the size each time columns or rows are changed
self.model().columnsInserted.connect(self.updateSize)
self.model().columnsRemoved.connect(self.updateSize)
self.model().rowsInserted.connect(self.updateSize)
self.model().rowsRemoved.connect(self.updateSize)
# the same, when a section is resized; note that Qt requires some "time"
# to do so, so the call to the update function has to be delayed
self.horizontalHeader().sectionResized.connect(lambda: QTimer.singleShot(0, self.updateSize))
self.verticalHeader().sectionResized.connect(lambda: QTimer.singleShot(0, self.updateSize))
# ensure that the widget uses only the maximum required size
self.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum)
# and disable the scrollbars
self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
def updateSize(self):
width = 0
header = self.horizontalHeader()
# go through each column and add its size
for s in range(self.model().columnCount()):
width += header.sectionSize(s)
height = 0
header = self.verticalHeader()
# the same for rows
for s in range(self.model().rowCount()):
height += header.sectionSize(s)
size = QSize(width, height)
# since we've connected a lot of signals and the model could still
# be empty when calling this slot, ensure that the size is valid
if size.isValid():
self.setFixedSize(size)
Finally, when adding a widget to a layout, it usually uses as much space as possible. If it doesn't, it's aligned according to the default top-left. To avoid that, add the widget by specifying the alignment:
class TableWidget(QWidget):
def __init__(self, parent):
super(TableWidget, self).__init__(parent)
QVBoxLayout(self)
self.creatingTable(parent)
def creatingTable(self, parent):
tableWidget = TableWidgetSubclass()
# ...
self.layout().addWidget(tableWidget, alignment=Qt.AlignCenter)
self.button1 = QPushButton("Button 1")
self.layout().addWidget(self.button1, alignment=Qt.AlignCenter)
Note: as you can see, I didn't use self.layout =, and then I used self.layout(). That's for two reasons:
you should never overwrite basic class attributes (layout is a property of every QWidget)
when adding the target attribute to a layout constructor, that layout is automatically applied to the widget, and there's no need to use setLayout again, since it has already implicitly called.
I am using QTableWidget i have set 100 rows and 2 columns, now i need to table scrolling with pageup and pagedown buttons.
i'm trying to browse the table with PageUp/PageDown buttons, but the buttons go to the home or go to end the table. I wonder if there is a way to scroll the list partially?
from PyQt5.QtWidgets import QVBoxLayout, QPushButton, QHBoxLayout, QApplication, QWidget, QTableWidget, QTableWidgetItem
import sys
from PyQt5.QtCore import Qt
class Window(QWidget):
def __init__(self):
super().__init__()
self.title = "PyQt5 Tables"
self.top = 100
self.left = 100
self.width = 500
self.height = 400
self.qtd_rows = 100
self.table_widget = QTableWidget()
self.init_window()
def init_window(self):
self.setWindowTitle(self.title)
self.setGeometry(self.top, self.left, self.width, self.height)
self.creating_tables()
self.show()
def creating_tables(self):
self.table_widget = QTableWidget()
self.table_widget.setAutoScroll(True)
self.table_widget.setRowCount(self.qtd_rows)
self.table_widget.setColumnCount(2)
self.table_widget.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.table_widget.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
vbox = QVBoxLayout()
for text, slot in (("PageUp", self.btn_page_up), ("PageDown", self.btn_page_down)):
button = QPushButton(text)
vbox.addWidget(button)
button.clicked.connect(slot)
for i in range(0, self.qtd_rows):
self.table_widget.setItem(i, 0, QTableWidgetItem("Name_" + str(i)))
self.table_widget.setItem(i, 1, QTableWidgetItem("Email"))
vBoxLayout = QVBoxLayout()
vBoxLayout.addWidget(self.table_widget)
hBox = QHBoxLayout()
hBox.addLayout(vBoxLayout)
hBox.addLayout(vbox)
self.setLayout(hBox)
def btn_page_up(self):
self.table_widget.scrollToTop()
def btn_page_down(self):
self.table_widget.scrollToBottom()
App = QApplication(sys.argv)
window = Window()
sys.exit(App.exec())
I expect browsing all table items using PageUp/PageDown buttons.
While the answer provided by #eyllanesc works fine, I'd like to suggest another approach.
Even when the scroll bars are hidden, they still exist and are constantly updated according to the item view contents and position, making it possible to use them to scroll, since their valueChanged signal is still connected to the item view itself.
def btn_page_up(self):
scrollBar = self.table_widget.verticalScrollBar()
scrollBar.setValue(scrollBar.value() - scrollBar.pageStep())
def btn_page_down(self):
scrollBar = self.table_widget.verticalScrollBar()
scrollBar.setValue(scrollBar.value() + scrollBar.pageStep())
As long as you are using ScrollPerItem mode for verticalScrollMode, you can even scroll by a specific amount of items also.
For example, if you want to scroll down by 10 items each time:
def btn_scroll_down(self):
scrollBar = self.table_widget.verticalScrollBar()
scrollBar.setValue(scrollBar.value() + scrollBar.singleStep() * 10)
The problem comes if you're using ScrollPerPixel, as items can have different sizes; so, if you want to use the page keys to scroll by items you'll need to find your own method (as there's no "perfect" one) to scroll according to the currently shown items and their sizes. If that's the case, you should implement it using the code provided by eyllanesc, while checking for item positioning (and ensuring if the possibly shown items are near to the end).
You could use the scrollToItem() method but there is a drawback: Not every grid has a QTableWidgetItem associated because you could not move to those grids. Instead it is better to use the scrollTo() method that QModelIndex uses, and in that case every grid has a QModelIndex associated.
On the other hand, obtaining the QModelIndex is pure geometry, for this the rect of the viewport() and the indexAt() method are used, then the row and column of the grid are obtained, in the end it is obtained to the next or previous QModelIndex using the model.
def btn_up(self):
ix = self.table_widget.indexAt(self.table_widget.viewport().rect().topLeft())
previous_ix = self.table_widget.model().index(ix.row() - 1, ix.column())
self.table_widget.scrollTo(previous_ix)
def btn_down(self):
ix = self.table_widget.indexAt(self.table_widget.viewport().rect().bottomLeft())
next_ix = self.table_widget.model().index(ix.row() + 1, ix.column())
self.table_widget.scrollTo(next_ix)
My objective is to set multiple QPushButton as cellwidget in a QTableWidget and whenever the user click on any of those buttons, it will print out the Row and Column number of table it is in.
So, for 1 QPushButton,
import sys
from PyQt4 import QtGui,QtCore
def CurrentPos():
clickme = QtGui.qApp.focusWidget()
index = table.indexAt(clickme.pos())
if index.isValid():
print (index.row(), index.column())
def AddValues():
table.setRowCount(5)
for i in range(5):
button = QtGui.QPushButton('Click1')
table.setCellWidget(i,1,button)
button.clicked.connect(CurrentPos)
if __name__ == "__main__":
app = QtGui.QApplication(sys.argv)
table = QtGui.QTableWidget()
table.setFixedSize(QtCore.QSize(330,250))
table.setEditTriggers(QtGui.QTableWidget.NoEditTriggers)
table.setSelectionBehavior(QtGui.QTableWidget.SelectRows)
table.setSelectionMode(QtGui.QTableWidget.NoSelection)
table.horizontalHeader().setStretchLastSection(True)
table.setFocusPolicy(QtCore.Qt.NoFocus)
table.setColumnCount(3)
table.setHorizontalHeaderLabels(['A','B','C'])
table.setColumnWidth(0,50)
table.setColumnWidth(1,200)
table.show()
AddValues()
app.exec_()
When i click on each button, It will print out the rows and columns number just fine. But When I changed the function AddValues to accomodate multiple buttons:
def AddValues():
table.setRowCount(5)
for i in range(5):
button1 = QtGui.QPushButton('Click1')
button2 = QtGui.QPushButton('Click2')
button3 = QtGui.QPushButton('Click3')
button_layout = QtGui.QHBoxLayout()
button_layout.addWidget(button1)
button_layout.addWidget(button2)
button_layout.addWidget(button3)
buttons_widget = QtGui.QWidget()
buttons_widget.setLayout(button_layout)
table.setCellWidget(i,1,buttons_widget)
button1.clicked.connect(CurrentPos)
button2.clicked.connect(CurrentPos)
button3.clicked.connect(CurrentPos)
It no longer print out the correct row and column number anymore. Instead, It will just print either (0,0) or (0,1) depending on the position of the button.
I tried to print out the QtGui.qApp.focusWidget() and it gives me <PyQt4.QtGui.QPushButton object at 0x014B56F0> so as far as I understand, the focus widget is the actual QPushButton I am clicking instead of the QWidget named buttons_widget.
My only suspect is that .pos() method return the position relative to it's parent widget, which is QWidget (buttons_widget) rather than the QTableWidget itself. But I can't seems to find the documentation for .pos() method.
So, How can I modify my code so that whenever user click on any of 3 Pushbuttons, it print out the row and column number the button currently in.
I am using windows xp SP3 and Python 2.7.3
I found the solution.
index = table.indexAt(clickme.parent().pos())
I should be checking the position of the parent QWidget rather than the QPushButton
buttonClicked = self.sender()
index = self.table.indexAt(buttonClicked.pos())
print index.row()
Sams's answer works like a charm. RITZ XAVI, it works as follows:
buttonClicked = self.sender() - detects the widget that sends the signal
postitionOfWidget = buttonClicked.pos() - gets the x,y coordinate of this sender
index = self.table.indexAt(postitionOfWidget) - item in the qtablewidget with that coordinate
index.row() row of that item inside that qtablewidget
Sam, I can't thank you enough for this :-)