I am writing a simple program to display the contents of a SQL database table in a QDialog (PySide). The goal is to have a method that expands the window to show all of the columns, so the user doesn't have to resize to see everything. This problem was addressed in a slightly different context:
Fit width of TableView to width of content
Based on that, I wrote the following method:
def resizeWindowToColumns(self):
frameWidth = self.view.frameWidth() * 2
vertHeaderWidth = self.view.verticalHeader().width()
horizHeaderWidth =self.view.horizontalHeader().length()
vertScrollWidth = self.view.style().pixelMetric(QtGui.QStyle.PM_ScrollBarExtent)
fudgeFactor = 6 #not sure why this is needed
newWidth = frameWidth + vertHeaderWidth + horizHeaderWidth + vertScrollWidth + fudgeFactor
It works great. But notice that I have had to add a fudgeFactor. With fudge, it works perfectly. But it suggests I have lost track of six pixels, and am very curious where they are coming from. It doesn't seem to matter how many columns are displayed, or their individual widths: the fudgeFactor 6 always seems to work.
System details
Python 2.7 (Spyder/Anaconda). PySide version 1.2.2, Qt version 4.8.5. Windows 7 laptop with a touch screen (touch screens sometimes screw things up in PySide).
Full working example
# -*- coding: utf-8 -*-
import os
import sys
from PySide import QtGui, QtCore, QtSql
class DatabaseInspector(QtGui.QDialog):
def __init__(self, tableName, parent = None):
QtGui.QDialog.__init__(self, parent)
#define model
self.model = QtSql.QSqlTableModel(self)
self.model.setTable(tableName)
self.model.select()
#View of model
self.view = QtGui.QTableView()
self.view.setModel(self.model)
#Sizing
self.view.resizeColumnsToContents() #Resize columns to fit content
self.resizeWindowToColumns() #resize window to fit columns
#Quit button
self.quitButton = QtGui.QPushButton("Quit");
self.quitButton.clicked.connect(self.reject)
#Layout
layout = QtGui.QVBoxLayout()
layout.addWidget(self.view) #table view
layout.addWidget(self.quitButton) #pushbutton
self.setLayout(layout)
self.show()
def resizeEvent(self, event):
#This is just to see what's going on
print "Size set to ({0}, {1})".format(event.size().width(), event.size().height())
def resizeWindowToColumns(self):
#Based on: https://stackoverflow.com/a/20807145/1886357
frameWidth = self.view.frameWidth() * 2
vertHeaderWidth = self.view.verticalHeader().width()
horizHeaderWidth =self.view.horizontalHeader().length()
vertScrollWidth = self.view.style().pixelMetric(QtGui.QStyle.PM_ScrollBarExtent)
fudgeFactor = 6 #not sure why this is needed
newWidth = frameWidth + vertHeaderWidth + horizHeaderWidth + vertScrollWidth + fudgeFactor
if newWidth <= 500:
self.resize(newWidth, self.height())
else:
self.resize(500, self.height())
def populateDatabase():
print "Populating table in database..."
query = QtSql.QSqlQuery()
if not query.exec_("""CREATE TABLE favorites (
id INTEGER PRIMARY KEY AUTOINCREMENT UNIQUE NOT NULL,
category VARCHAR(40) NOT NULL,
number INTEGER NOT NULL,
shortdesc VARCHAR(20) NOT NULL,
longdesc VARCHAR(80))"""):
print "Failed to create table"
return False
categories = ("Apples", "Chocolate chip cookies", "Favra beans")
numbers = (1, 2, 3)
shortDescs = ("Crispy", "Yummy", "Clarice?")
longDescs = ("Healthy and tasty", "Never not good...", "Awkward beans for you!")
query.prepare("""INSERT INTO favorites (category, number, shortdesc, longdesc)
VALUES (:category, :number, :shortdesc, :longdesc)""")
for category, number, shortDesc, longDesc in zip(categories, numbers, shortDescs, longDescs):
query.bindValue(":category", category)
query.bindValue(":number", number)
query.bindValue(":shortdesc", shortDesc)
query.bindValue(":longdesc", longDesc)
if not query.exec_():
print "Failed to populate table"
return False
return True
def main():
import site
app = QtGui.QApplication(sys.argv)
#Connect to/initialize database
dbName = "food.db"
tableName = "favorites"
site_pack_path = site.getsitepackages()[1]
QtGui.QApplication.addLibraryPath('{0}\\PySide\\plugins'.format(site_pack_path))
db = QtSql.QSqlDatabase.addDatabase("QSQLITE")
fullFilePath = os.path.join(os.path.dirname(__file__), dbName) #;print fullFilePath
dbExists = QtCore.QFile.exists(fullFilePath) #does it already exist in directory?
db.setDatabaseName(fullFilePath)
db.open()
if not dbExists:
populateDatabase()
#Display database
dataTable = DatabaseInspector(tableName)
sys.exit(app.exec_())
#Close and delete database (not sure this is needed)
db.close()
del db
if __name__ == "__main__":
main()
The difference between the linked question and your example, is that the former is resizing a widget within a layout, whereas the latter is resizing a top-level window.
A top-level window is usually decorated with a frame. On your system, the width of this frame seems to be three pixels on each side, making six pixels in all.
You can calculate this value programmatically with:
self.frameSize().width() - self.width()
where self is the top-level window.
However, there may be an extra issue to deal with, and that is in choosing when to calculate this value. On my Linux system, the frame doesn't get drawn until the window is fully shown - so calculating during __init__ doesn't work.
I worked around that problem like this:
dataTable = DatabaseInspector(tableName)
dataTable.show()
QtCore.QTimer.singleShot(10, dataTable.resizeWindowToColumns)
but I'm not sure whether that's portable (or even necessarily the best way to do it).
PS:
It seems that the latter issue may be specific to X11 - see the Window Geometry section in the Qt docs.
UPDATE:
The above explanation and calculation is not correct!
The window decoration is only relevant when postioning windows. The resize() and setGeometry() functions always exclude the window frame, so it doesn't need to be factored in when calculating the total width.
The difference between resizing a widget within a layout versus resizing a top-level window, is that the latter needs to take account of the layout margin.
So the correct calculation is this:
margins = self.layout().contentsMargins()
self.resize((
margins.left() + margins.right() +
self.view.frameWidth() * 2 +
self.view.verticalHeader().width() +
self.view.horizontalHeader().length() +
self.view.style().pixelMetric(QtGui.QStyle.PM_ScrollBarExtent)
), self.height())
But note that this always allows room for a vertical scrollbar.
The example script doesn't add enough rows to show the vertical scrollbar, so it is misleading in that respect - if more rows are added, the total width is exactly right.
Related
I want to create a "details" view for books I have downloaded.
With the attached image as an example, imagine the red block to the left is the book's cover page, and metadata related to it is displayed to the right.
With the way I have it done right now:
from PySide6 import QtWidgets as qtw
from PySide6 import QtGui as qtg
from PySide6 import QtCore as qtc
class Details:
def __init__(self):
self.location = "/home/user/Desktop/Untitled.png"
self.title = "Some title"
self.subtitle = "Sub title"
self.id = 123124
def to_html(self):
return """
<p>
<b>Author =</b> author<br/>
<b>Published Date =</b> 2000-1-1<br/>
<b>Pages =</b> 500<br/>
</p>
"""
class DetailsWidget(qtw.QWidget):
_title_font = qtg.QFont()
_title_font.setBold(True)
_title_font.setPixelSize(24)
_subtitle_font = qtg.QFont()
_subtitle_font.setBold(True)
_subtitle_font.setPixelSize(19)
_id_font = qtg.QFont()
_id_font.setBold(True)
_id_font.setPixelSize(15)
_redacted_details_font = qtg.QFont()
_redacted_details_font.setPixelSize(12)
def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
self.setFixedSize(1000, 500)
self.setWindowFlag(qtc.Qt.WindowType.Dialog, True)
self.setLayout(qtw.QGridLayout())
self.layout().setContentsMargins(0, 0, 0, 0)
self._details: Details = Details()
self._thumbnail_image = qtg.QImage(self._details.location)
self._thumbnail_image = self._thumbnail_image.scaled(
500,
500,
qtc.Qt.AspectRatioMode.KeepAspectRatio,
qtc.Qt.TransformationMode.SmoothTransformation,
)
self._details_rect = qtc.QRect(
self._get_actual_geometry().left() + self._thumbnail_image.width() + 10,
self._get_actual_geometry().top(),
self._get_actual_geometry().width() - self._thumbnail_image.width() - 20,
self._get_actual_geometry().height(),
)
height = 0
self._title_rects = []
font_metrics_rect = qtg.QFontMetrics(self._title_font).boundingRect(
self._details_rect, qtc.Qt.TextFlag.TextWordWrap, self._details.title, 0
)
drawing_rect = qtc.QRect(self._details_rect)
self._title_rects.append(drawing_rect)
height += font_metrics_rect.height() + 10
drawing_rect = qtc.QRect(self._details_rect)
drawing_rect.moveTop(height)
self._title_rects.append(drawing_rect)
font_metrics_rect = qtg.QFontMetrics(self._title_font).boundingRect(
self._details_rect, qtc.Qt.TextFlag.TextWordWrap, self._details.subtitle, 0
)
drawing_rect = qtc.QRect(self._details_rect)
height += font_metrics_rect.height() - 3
drawing_rect.moveTop(height)
self._title_rects.append(drawing_rect)
font_metrics_rect = qtg.QFontMetrics(self._title_font).boundingRect(
self._details_rect,
qtc.Qt.TextFlag.TextWordWrap,
str(self._details.id),
0,
)
self._title_rects.append(drawing_rect)
height += font_metrics_rect.height() + 10
self._details_rect.moveTop(height)
self._redacted_details_text_document = qtg.QTextDocument()
self._redacted_details_text_document.setHtml(self._details.to_html())
# First set the width,
self._redacted_details_text_document.setTextWidth(self._details_rect.width())
# then get the height of the QTextDocument based on the given width and set
# that + the titles heights + bottom padding as the total height.
if (total_height:=height + self._redacted_details_text_document.size().height() + 10) > self.height():
self.setFixedHeight(total_height)
def _get_actual_geometry(self) -> qtc.QRect:
# Probably not needed for normal desktop environments with window
# managers but I'm an epik i3 user so self.geometry() does not work as
# intended when full screening the window with $mod + F. Or I'm just
# retarded and this is not even a problem.
geometry = self.geometry()
geometry.setTopLeft(qtc.QPoint(0, 0))
return geometry
def paintEvent(self, event: qtg.QPaintEvent) -> None:
total_height = 0
painter = qtg.QPainter(self)
painter.setRenderHint(qtg.QPainter.RenderHint.TextAntialiasing)
painter.drawImage(0, 0, self._thumbnail_image)
painter.save()
painter.setFont(self._title_font)
painter.drawText(
self._title_rects[0], qtc.Qt.TextFlag.TextWordWrap, self._details.title
)
painter.setFont(self._subtitle_font)
painter.drawText(
self._title_rects[1], qtc.Qt.TextFlag.TextWordWrap, self._details.subtitle
)
painter.setFont(self._id_font)
painter.drawText(
self._title_rects[2],
qtc.Qt.TextFlag.TextWordWrap,
str(self._details.id),
)
painter.translate(self._details_rect.topLeft())
painter.setFont(self._redacted_details_font)
self._redacted_details_text_document.drawContents(painter)
painter.restore()
app = qtw.QApplication()
widget = DetailsWidget()
widget.show()
app.exec()
I can display the text and the image next to each other just fine, but the text is not selectable. Looking around for a way to do so, I stumbled upon QGraphicsTextItem. Should I re-do the whole thing in a QGraphicsView instead of using the paintEvent on a QWidget? The reason I'm hesitant to do so is because I don't know of the cons of using a QGraphicsView, maybe it's a lot more resource heavy and not the best for this use case?
You're complicating things unnecessarily.
Just use a basic QHBoxLayout and two QLabels, with the one on the left for the image, and the one on the right for the details.
If you want to allow text selection, use QLabel.setTextInteractionFlags(Qt.TextSelectableByMouse).
An even better solution would be to use a QGraphicsView with a QGraphicsPixmapItem for the image (using fitInView() in the resizeEvent to always show it as large as possible) and a QTextEdit for the details, set in read only mode.
Note that your usage of _get_actual_geometry is wrong in principle (besides the fact that you're calling 4 times in a row, while you could just use a local variable instead), because when a widget has not been shown yet it always has a default size (100x30 for widgets created with a parent, otherwise 640x480), so not only you'll be getting a wrong geometry, but you're also changing it, since setTopLeft() will only move the corner, not translate the rectangle: if you want the basic rectangle of the widget, just use rect(). Obviously, if you properly use layouts as suggested above, this won't be necessary in the first place.
In widget my implementation, model the source model (subclassed from a QAbstractItemModel), proxy_model (subclassed from QSortFilterProxyModel) the proxy, and tree (QTreeView) the tree representing the model.
I want to hide the first column.
I tried to use tree.hideColumn(0), the the tree is shown flat.
If I subclass filterAcceptsColumn in the proxy to return True only for the second column,, then no rows are shown.
I believe this is because the parent/child relationships are anchored on the first column in the indexes, and when the proxy ask for the number of rows for a given index of column 1, the model returns 0 (which is the expected behavior in the model implementation if I understood well).
If I set rowCount to return non 0 values in the model for columns index > 0, I can see the tree and the rows, but then the model is not passing the QAbstractItemModelTester test with the folloing error:
qt.modeltest: FAIL! childIndex != childIndex1 () returned FALSE
I understand well that in the tree model, child index must be attached to a single parent index (the first column).
But how am I supposed to be hiding the first column in a proxy model if the parent child relationship of the source model are not retained by the proxy if the first column is filtered? I feel it is a "bug" from the proxy, or I missed something !
Do anyone know the proper way of filtering/hiding the first column in the tree view without losing the parent/child information, and still validating a qmodel implementation?
Thanks !
A proper and correct implementation would at least require the proxy to create indexes for the parent of the second column, requiring correct implementation of index(), parent(), mapToSource() and mapFromSource(). For tree models that can be really tricky.
If the source model is not too complex and all its functions are correctly implemented, a possible workaround could be to just override the data() (and headerData) of the proxy and always return the sibling of the next column.
The following test is done with a simple QStandardItemModel, but I don't think using a QAbstractItemModel should be any different, as long as it's correctly implemented.
from PyQt5 import QtCore, QtGui, QtWidgets
class ColumnSwapProxy(QtCore.QSortFilterProxyModel):
def data(self, index, role=QtCore.Qt.DisplayRole):
return super().data(index.sibling(index.row(), index.column() + 1), role)
def headerData(self, section, orientation, role=QtCore.Qt.DisplayRole):
if orientation == QtCore.Qt.Horizontal:
section += 1
return super().headerData(section, orientation, role)
class Test(QtWidgets.QWidget):
def __init__(self):
super().__init__()
layout = QtWidgets.QVBoxLayout(self)
self.combo = QtWidgets.QComboBox()
layout.addWidget(self.combo)
self.tree = QtWidgets.QTreeView()
layout.addWidget(self.tree)
self.model = QtGui.QStandardItemModel()
self.createTree(self.model.invisibleRootItem())
self.tree.setModel(self.model)
self.model.setHorizontalHeaderLabels(
['Root', 'Fake root'] + ['Col {}'.format(c) for c in range(2, 6)])
self.tree.header().setSectionResizeMode(QtWidgets.QHeaderView.Stretch)
self.proxy = ColumnSwapProxy()
self.proxy.setSourceModel(self.model)
self.combo.addItem('Base model', self.model)
self.combo.addItem('First column hidden', self.proxy)
self.combo.currentIndexChanged.connect(self.setModel)
def setModel(self):
model = self.combo.currentData()
self.tree.setModel(model)
lastColumn = self.model.columnCount() - 1
self.tree.header().setSectionHidden(lastColumn, model == self.proxy)
def createTree(self, parent, level=0):
for r in range(10):
first = QtGui.QStandardItem('Root {} (level {})'.format(level + 1, r + 1))
if level < 2 and not r & 3:
self.createTree(first, level + 1)
row = [first]
for c in range(5):
row.append(QtGui.QStandardItem(
'Column {} (level {})'.format(c + 2, level + 1)))
parent.appendRow(row)
import sys
app = QtWidgets.QApplication(sys.argv)
w = Test()
w.show()
sys.exit(app.exec_())
This bit of Code here creates a scrollview of buttons to allow me select from a list of different "tickets"
SearchResultButton(Button(text=str(Datarray2[i]),id=str(Datarray2[i]),on_press=self.Pressbtn ))
self.ids.SearchResult.add_widget(SearchResultButton)
From there it opens this function, which should set a variable (In this case "UpdateTicketNum") which will be used in another function to set the label text in another screen.
def Pressbtn(self, SearchResultButton):
global UpdateTicket
UpdateTicket = OPENTicket
woo = SearchResultButton.text
print(SearchResultButton.text)
wow = [blank.strip() for blank in woo.split(',')]
print("\n\n\n\n")
global UpdateTicketNum
UpdateTicketNum = (wow[0])
self.manager.get_screen('UpdateTicket').UpdateOpen()
At this point it opens up the sqlite DB and double checks that the TicketNumber is valid. The issue comes in when trying to access the label inside the kv build
def UpdateOpen(self):
print("TESTSETST")
conn = sqlite3.connect('TicketData.db', timeout=10)
UD = conn.cursor()
UD.execute('SELECT TicketNumber FROM TicketData WHERE TicketNumber = ?',(UpdateTicketNum,))
tips = UD.fetchone()
print(tips[0])
tipsy = tips[0]
UpdatedLabelTexT = tipsy
sm.current=('UpdateTicket')
UpdateTicket.ids.UpdateLabels['text']=(UpdatedLabelTexT)
The UpdateTicket.ids.UpdateLabels['text']=UpdatedLabelText] field always claims to be a property of the scrollview buttons even though I am initializing it inside another class, and with different parameters. apologies if this question is poorly formatted. but 3 days of dying trying to figure this out and I snapped.
Here is the bit in KV
<UpdateTicket>
name: 'UpdateTicket'
on_enter:
root.UpdateOpen()
orientation: "vertical"
FloatLayout:
canvas.before:
Color:
rgba: .0, .6, 1, 1
Rectangle:
pos: self.pos
size: self.size
source: "lights.jpg"
Label:
id: UpdateLabels
text: "filler"
multiline: False
size_hint: (None, None)
size: (root.width/5,root.height/20)
pos_hint:{'center_x': .5,'center_y': .5 }
and how I initialize the screen
sm.add_widget(UpdateTicket(name='UpdateTicket'))
I found the solution that worked for me. when first initializing the app
class SampleApp(App)
return sm(to build the application)
I needed to replace that return SM with
global root
return sm
root = ScreenManager()
return root
And that appeared to fix my issue. thanks for reading
I'm writing a Solitaire GUI using wxPython, and I'm on Windows 7. I've only written one GUI before (in Java Swing), so I'm not as familiar as I could be with all the different types of widgets and controls. I'm faced with the challenge of having resizable, cascading piles of cards in the Tableaux of the Solitaire board. To me, using BitmapButtons for each card (or at least for face-up cards) and having a panel contain a pile of cards seemed natural, since it is legal to move sub-piles of cards in the Tableau from pile to pile in Solitaire. I'm sure there is a better way to do this, but for now I've been fiddling with a smaller GUI (not my main GUI) to try and achieve this. I've attached the code for the test GUI below.
Note: My main GUI uses a GridBagSizer with 14 cells. I haven't tried using the following panel/buttons in the GridBagSizer, or even know if a GridBagSizer is the best way to go about this.
import wx
class MyFrame(wx.Frame):
def __init__(self, parent, id_, title):
wx.Frame.__init__(self, parent, id_, title, size=(810, 580))
self.panel = wx.Panel(self, size=(72, 320), pos=(20,155))
self.buttons = []
self.init_buttons()
def init_buttons(self):
for i in range(6):
face_down = wx.Image('img/cardback.png', wx.BITMAP_TYPE_PNG).ConvertToBitmap()
wid = face_down.GetWidth()
hgt = face_down.GetHeight()
bmpbtn = wx.BitmapButton(self.panel, -1, bitmap=face_down, pos=(20,155+7*i), size=(wid, hgt))
bmpbtn.Bind(wx.EVT_ENTER_WINDOW, self.onMouseOver)
self.buttons.append(bmpbtn)
for i in range(1,14):
rank = 14 - i
if i % 2 == 0:
filename = 'img/%sC.png' % rank
else:
filename = 'img/%sH.png' % rank
img = wx.Image(filename, wx.BITMAP_TYPE_PNG).ConvertToBitmap()
wid = img.GetWidth()
hgt = img.GetHeight()
bmpbtn = wx.BitmapButton(self.panel, -1, bitmap=img, pos=(20, 177+20*i), size=(wid, hgt))
bmpbtn.Bind(wx.EVT_ENTER_WINDOW, self.onMouseOver)
self.buttons.append(bmpbtn)
def onMouseOver(self, event):
#event.Skip()
pass
class MyApp(wx.App):
def OnInit(self):
wx.InitAllImageHandlers()
self.frame = MyFrame(None, -1, "Solitaire")
self.frame.Show(True)
self.SetTopWindow(self.frame)
return True
app = MyApp(0)
app.MainLoop()
This is what results from running:
http://oi44.tinypic.com/1zv4swj.jpg
Which I was satisfied with, until I moved my mouse over some of the buttons:
http://oi44.tinypic.com/2rdupmq.jpg
This must have to do with the EVT_ENTER_WINDOW event. I attempted to write an event handler, but realized I didn't really know how to achieve what I need. According to the docs, a BitmapButton has different bitmaps for each of its states - hover, focus, selected, inactive, etc. However, I do not want to change the Bitmap on a mouseover event. I simply want the button to stay put, and to not display itself on top of other buttons.
Any help would be greatly appreciated. Incidentally, if anybody has advice for a better way (than GridBagSizer and these panels of buttons) to implement this GUI, I would love that!
I would recommend against using actual window controls for each of the cards. I would instead have a single canvas upon which you render the card bitmaps in their appropriate locations. You'll have to do a little extra math to determine what cards are being clicked on, but this is definitely the way to go.
Use a wx.Panel with a EVT_PAINT handler to do your drawing.
Here's a starting point that is written to use double-buffering to avoid flickering.
P.S. You can use bitmap = wx.Bitmap(path) to load an image, instead of bothering with wx.Image and converting it to a bitmap object.
import wx
class Panel(wx.Panel):
def __init__(self, parent):
super(Panel, self).__init__(parent)
self.SetBackgroundStyle(wx.BG_STYLE_CUSTOM)
self.Bind(wx.EVT_PAINT, self.on_paint)
self.Bind(wx.EVT_LEFT_DOWN, self.on_left_down)
self.Bind(wx.EVT_LEFT_UP, self.on_left_up)
def on_left_down(self, event):
print 'on_left_down', event.GetPosition()
def on_left_up(self, event):
print 'on_left_up', event.GetPosition()
def on_paint(self, event):
dc = wx.AutoBufferedPaintDC(self)
# Use dc.DrawBitmap(bitmap, x, y) to draw the cards here
class Frame(wx.Frame):
def __init__(self):
super(Frame, self).__init__(None)
self.SetTitle('My Title')
Panel(self)
def main():
app = wx.App()
frame = Frame()
frame.Center()
frame.Show()
app.MainLoop()
if __name__ == '__main__':
main()
I need to create the QWidget(QtoolButton) in QgridLayout without specifying the indices for row and column. It should automatically get created to next empty cell in the layout according to row and column mentioned.
I was not able to find any method in QgridLayout help.
I tried .addWidget (self, QWidget w), but it add all the QWidget to the index of (0,0) and all the buttons lie over each other.
Thanks in advance.
Let's suppose that you have a QGridLayout with 4 rows and 3 columns and you want to add buttons to it automatically from top to bottom and from left to right. That can easily be achieved if you are able to predict the position of the next button to be added. In our case:
row = number of added buttons / number of columns
column = number of added buttons % number of columns
(other type of filling work similarly). Let's put it in code:
from PyQt4.QtGui import *
class MyMainWindow(QMainWindow):
def __init__(self, parent=None):
super(MyMainWindow, self).__init__(parent)
self.central = QWidget(self)
self.grid = QGridLayout(self.central)
self.rows = 4
self.cols = 3
self.items = self.grid.count()
while(self.items < (self.rows * self.cols)):
self.addButton()
self.setCentralWidget(self.central)
def addButton(self):
# the next free position depends on the number of added items
row = self.items/self.cols
col = self.items % self.cols
# add the button to the next free position
button = QPushButton("%s, %s" % (row, col))
self.grid.addWidget(button, row, col)
# update the number of items
self.items = self.grid.count()
if __name__ == "__main__":
import sys
app = QApplication(sys.argv)
ui = MyMainWindow()
ui.show()
sys.exit(app.exec_())
You can handle the "next empty cell" by calculating rows and columns yourself. For example, you can subclass QGridLayout to implement any "next empty cell" algorithm according to your needs:
class AutoGridLayout(QGridLayout):
def __init__(self):
QGridLayout.__init__(self)
self.column = 0
self.row = 0
def addNextWidget(self, widget):
self.addWidget(widget, self.row, self.column)
self.column = self.column + 1 # Automatically advance to next column
# Setup main widget
app = QApplication(sys.argv)
mainWindow = QMainWindow()
centralWidget = QWidget()
mainWindow.setCentralWidget(centralWidget)
# Add widgets using the AutoGridLayout
layout = AutoGridLayout()
centralWidget.setLayout(layout)
layout.addNextWidget(QPushButton("1", centralWidget))
layout.addNextWidget(QPushButton("2", centralWidget))
layout.addNextWidget(QPushButton("3", centralWidget))
# Show and run the application
mainWindow.show()
app.exec_()
This source shall only show the general idea - you can manage the row and column indices according to your needs. Just implement the necessary logic in the addNextWidget() method by calculating the next desired row/column (in this example, the next column in row 0 is used).
Addition to other answers: If you need just rows with variable number of items, and not an actual grid, then you should use multiple QHBoxLayouts (one for each row) nested in one QVBoxLayout. That will also get you the behaviour you want, new items created on demand, without nasty gaps.