Laying Widgets in QGridLayout - qt

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.

Related

How to show only part of model in QTreeView?

I've been playing around with this code and I don't understand, how to adjust model columns visibility.
for i in range(3):
parent1 = QStandardItem('Family {}. Some long status text for sp'.format(i))
for j in range(3):
child1 = QStandardItem('Child {}'.format(i*3+j))
child2 = QStandardItem('row: {}, col: {}'.format(i, j+1))
child3 = QStandardItem('row: {}, col: {}'.format(i, j+2))
parent1.appendRow([child1, child2, child3])
model.appendRow(parent1)
# span container columns
view.setFirstColumnSpanned(i, view.rootIndex(), True)
I want to have one model with many fields and several views, each displaying only specific columns.
I don't want to hide columns with setColumnHidden, because when model gets extended, I'll have to hide new columns.
I'm familiar with Gtk and it's quite simple there: you create a TreeView, manually add columns, fill them with renderers and tell each renderer, from which column in model it should take data. Is it possible with Qt?
You can use a QSortFilterProxyModel:
class FilterColumnModel(QSortFilterProxyModel):
def __init__(self, parent=None):
super(FilterColumnModel, self).__init__(parent)
self._columns = None
#property
def columns(self):
return self._columns
#columns.setter
def columns(self, columns):
self._columns = columns[:]
self.invalidateFilter()
def filterAcceptsColumn(self, source_column, source_parent):
if self.columns is None:
return True
return source_column in self.columns
filter_model = FilterColumnModel()
filter_model.setSourceModel(model)
filter_model.columns = [0, 2]
view.setModel(filter_model)
view.setUniformRowHeights(True)

Implementing a delegate for wordwrap in a QTreeView (Qt/PySide/PyQt)?

I have a tree view with a custom delegate to which I am trying to add word wrap functionality. The word wrapping is working fine, but the sizeHint() seems to not work, so when the text wraps, the relevant row does not expand to include it.
I thought I was taking care of it in sizeHint() by returning document.size().height().
def sizeHint(self, option, index):
text = index.model().data(index)
document = QtGui.QTextDocument()
document.setHtml(text)
document.setTextWidth(option.rect.width())
return QtCore.QSize(document.idealWidth(), document.size().height())
However, when I print out document.size().height() it is the same for every item.
Also, even if I manually set the height (say, to 75) just to check that things will look reasonable, the tree looks like a goldfish got shot by a bazooka (that is, it's a mess):
As you can see, the text in each row is not aligned properly in the tree.
Similar posts
Similar issues have come up before, but no solutions to my problem (people usually say to reimplement sizeHint(), and that's what I am trying):
QTreeWidget set height of each row depending on content
QTreeView custom row height of individual rows
http://www.qtcentre.org/threads/1289-QT4-QTreeView-and-rows-with-multiple-lines
SSCCE
import sys
from PySide import QtGui, QtCore
class SimpleTree(QtGui.QTreeView):
def __init__(self, parent = None):
QtGui.QTreeView.__init__(self, parent)
self.setAttribute(QtCore.Qt.WA_DeleteOnClose)
self.setGeometry(500,200, 400, 300)
self.setUniformRowHeights(False) #optimize: but for word wrap, we don't want this!
print "uniform heights in tree?", self.uniformRowHeights()
self.model = QtGui.QStandardItemModel()
self.model.setHorizontalHeaderLabels(['Task', 'Description'])
self.setModel(self.model)
self.rootItem = self.model.invisibleRootItem()
item0 = [QtGui.QStandardItem('Sneeze'), QtGui.QStandardItem('You have been blocked up')]
item00 = [QtGui.QStandardItem('Tickle nose, this is a very long entry. Row should resize.'), QtGui.QStandardItem('Key first step')]
item1 = [QtGui.QStandardItem('<b>Get a job</b>'), QtGui.QStandardItem('Do not blow it')]
self.rootItem.appendRow(item0)
item0[0].appendRow(item00)
self.rootItem.appendRow(item1)
self.setColumnWidth(0,150)
self.expandAll()
self.setWordWrap(True)
self.setItemDelegate(ItemWordWrap(self))
class ItemWordWrap(QtGui.QStyledItemDelegate):
def __init__(self, parent=None):
QtGui.QStyledItemDelegate.__init__(self, parent)
self.parent = parent
def paint(self, painter, option, index):
text = index.model().data(index)
document = QtGui.QTextDocument() # #print "dir(document)", dir(document)
document.setHtml(text)
document.setTextWidth(option.rect.width()) #keeps text from spilling over into adjacent rect
painter.save()
painter.translate(option.rect.x(), option.rect.y())
document.drawContents(painter) #draw the document with the painter
painter.restore()
def sizeHint(self, option, index):
#Size should depend on number of lines wrapped
text = index.model().data(index)
document = QtGui.QTextDocument()
document.setHtml(text)
document.setTextWidth(option.rect.width())
return QtCore.QSize(document.idealWidth() + 10, document.size().height())
def main():
app = QtGui.QApplication(sys.argv)
myTree = SimpleTree()
myTree.show()
sys.exit(app.exec_())
if __name__ == "__main__":
main()
The issue seems to stem from the fact that the value for option.rect.width() passed into QStyledItemDelegate.sizeHint() is -1. This is obviously bogus!
I've solved this by storing the width in the model from within the paint() method and accessing this from sizeHint().
So in your paint() method add the line:
index.model().setData(index, option.rect.width(), QtCore.Qt.UserRole+1)
and in your sizeHint() method, replace document.setTextWidth(option.rect.width()) with:
width = index.model().data(index, QtCore.Qt.UserRole+1)
if not width:
width = 20
document.setTextWidth(width)

Sudden additional scroll bar for wx.grid.Grid

I use wx.grid.Grid for display table.
The number of grid rows is changed and it may be more than 2000.
For number of rows less than 1723 GUI displays normally.
But if I have greater than or equal to 1723 rows, GUI displays incorrectly:
Sudden additional vertical scroll bar appears.
If aim a mouse cursor on this additional scrollbar then my laptop display blinks
wx.grid.Grid don't expands into ScrolledPanel.
How can I use wx.grid.Grid with greate number of rows (more than 2000)?
Thanks in advance.
Code Sample:
import wx
import wx.grid
from wx.lib.scrolledpanel import ScrolledPanel
class TestPanel(ScrolledPanel):
def __init__(self, parent):
ScrolledPanel.__init__(self, parent, wx.ID_ANY, size=(640, 480))
self.sizer = wx.BoxSizer(wx.VERTICAL)
self.sizer.Add(self._create_table(), 1, wx.EXPAND | wx.ALL, 5)
self.SetSizer(self.sizer)
self.SetupScrolling()
self.SetAutoLayout(1)
def _create_table(self):
_table = wx.grid.Grid(self, -1)
_table.CreateGrid(0, 1)
for i in xrange(1723): # Work normally If I use 1722 rows
_table.AppendRows()
_table.SetCellValue(i, 0, str(i))
return _table
class TestFrame(wx.Frame):
def __init__(self):
wx.Frame.__init__(self, None, wx.ID_ANY,
title="Scroll table", size=(640, 480))
self.fSizer = wx.BoxSizer(wx.VERTICAL)
self.fSizer.Add(TestPanel(self), 1, wx.EXPAND)
self.SetSizer(self.fSizer)
self.Show()
if __name__ == "__main__":
app = wx.App(False)
frame = TestFrame()
app.MainLoop()
In this case you should use a wx.Panel instead of the ScrolledPanel. The grid is able to manage its scrolling on its own, it doesn't need to have any help from its parent.
If you have other widgets to display that wont fit in the panel, and you want to be able to scroll them and the grid in and out of view, then using the ScrolledPanel would be appropriate, but then you should do something to constrain the size of the grid so it doesn't try to expand to show all cells.

pyqt qt4 QTableView how to disable sorting for certain columns?

So I have a QTableView and I only want to let column sorting on column 1 but not column2.
Naturally I tried to installEventFilter on QHeaderView or QTableView, but MouseButtonPress event is not being passed unless you installEventFilter on QApplication
Now if when eventFilter is called, the target object is always the top level widget although event.pos() is actually relative to the header or tablecell depending on where you click.
So we cannot use QHeaderView.rect().contains(event.pos()) to find out if the user clicks on the header because you get false positive when you click on the top edge of the very first table cell.
You can still however calculate this using globalPos but then your eventFilter's logic has to change when you change layout or add more widgets above the tableview.
I believe it is a bug that event.pos() returns the relative pos even the object argument always refer to the same top level widget.
A more logical API would be that there is a event.target() method to return the target where it calculates the relative pos.
But I don't see a target() method or a way to find the target in this event filter.
Maybe I'm missing something?
# -*- coding: utf-8 -*-
# pyqt windows 4.10.3
# python 2.7.5 32 bits
from PyQt4.QtCore import *
from PyQt4.QtGui import *
app = None
tableHeader = None
class MyModel(QAbstractTableModel):
def rowCount(self, QModelIndex_parent=None, *args, **kwargs):
return 2
def columnCount(self, QModelIndex_parent=None, *args, **kwargs):
return 2
def data(self, modelIndex, role=None):
if modelIndex.isValid():
row = modelIndex.row()
col = modelIndex.column()
if role == Qt.DisplayRole:
return "%02d,%02d" % (row, col)
def flags(self, index):
if index.isValid():
return Qt.ItemIsEnabled
def headerData(self, section, Qt_Orientation, role=None):
if role == Qt.DisplayRole and Qt_Orientation == Qt.Horizontal:
return "Column " + str(section+1)
class MyEventFilter(QObject):
def eventFilter(self, object, event):
if event.type() == QEvent.MouseButtonPress:
# object is always app/top level widget
print 'MouseButtonPress target :' + repr(object)
# even though event.pos() gives pos relative to the header when you click on header,
# and pos relative to table cells when you click on table cell
print repr(event.pos())
# however we can get the mouse's global position
print repr(event.globalPos())
# given the top level widget's geometry
print repr(app.activeWindow().geometry())
# and the table header's left, top and height
print repr(tableHeader.rect())
# we can find out whether mouse click is targeted at the header
print repr(event.globalPos().y() - app.activeWindow().geometry().y())
# BUT WHAT IF THE LAYOUT CHANGE OR WE ADD MORE WIDGETS ABOVE THE TABLEVIEW?
# WE HAVE TO ADJUST THE CALCULATION ABOVE!
return False
if __name__ == "__main__":
import sys
app = QApplication(sys.argv)
w = QMainWindow()
t = QTableView()
tableHeader = t.horizontalHeader()
t.setModel(MyModel())
w.setCentralWidget(t)
ef = MyEventFilter()
# installing in QMainWindow or QTableView won't catch MouseButtonPress
# https://qt-project.org/forums/viewthread/9347
#w.installEventFilter(ef)
#t.installEventFilter(ef)
app.installEventFilter(ef)
w.show()
sys.exit(app.exec_())
There's a much easier solution: reimplement the sort method of the model, and only permit sorting for the appropriate column.
Also, as an added refinement, use the sortIndicatorChanged signal of the header to restore the current sort indicator when appropriate.
Here's a demo script:
from PyQt4 import QtGui, QtCore
class TableModel(QtGui.QStandardItemModel):
_sort_order = QtCore.Qt.AscendingOrder
def sortOrder(self):
return self._sort_order
def sort(self, column, order):
if column == 0:
self._sort_order = order
QtGui.QStandardItemModel.sort(self, column, order)
class Window(QtGui.QWidget):
def __init__(self, rows, columns):
QtGui.QWidget.__init__(self)
self.table = QtGui.QTableView(self)
model = TableModel(rows, columns, self.table)
for row in range(rows):
for column in range(columns):
item = QtGui.QStandardItem('(%d, %d)' % (row, column))
item.setTextAlignment(QtCore.Qt.AlignCenter)
model.setItem(row, column, item)
self.table.setModel(model)
self.table.setSortingEnabled(True)
self.table.horizontalHeader().sortIndicatorChanged.connect(
self.handleSortIndicatorChanged)
layout = QtGui.QVBoxLayout(self)
layout.addWidget(self.table)
def handleSortIndicatorChanged(self, index, order):
if index != 0:
self.table.horizontalHeader().setSortIndicator(
0, self.table.model().sortOrder())
if __name__ == '__main__':
import sys
app = QtGui.QApplication(sys.argv)
window = Window(5, 5)
window.show()
window.setGeometry(600, 300, 600, 250)
sys.exit(app.exec_())

Clickable elements or child widgets inside custom-painted delegate

I have a QListView, where I display items using a custom delegate with custom painting. Within each item (i.e. each list row) I want to be able to show a couple of "hyperlinks" which the user could click on and which would then call on some functions.
I have already tried to check the official documentation (e.g. Model/View Programming) as well as quite a lot of googling, but haven't been able to figure out how to accomplish this.
I have two ideas, each with their own problems:
I could draw them using child widgets, like a flat QPushButton. How do I then position and display these widgets?
I could also draw them as text strings. How do I then make them clickable? Or can I capture click events on the parent QListView and somehow determine coordinates from those? I could then match coordinates to these clickable elements and act accordingly.
My initial approach was to use QListWidget with .setItemWidget(), where I had a proper widget with a layout and child widgets. Unfortunately this was too slow when my list grew to hundreds or thousands of items. That's why I changed to QListView with a delegate.
I seem to be closing in on a solution.
I can receive clicks on the elements by overriding the delegate's .editorEvent(event, model, option, index). I can then find out the event.type(), the clicked row from index.row() and the actual coordinates from event.x() and event.y() (since, if the event type is MouseButtonRelease, the event is a QMouseEvent).
From these, I think I can correlate the coordinates to my elements on screen and act accordingly.
I will update this answer once I have working code.
EDIT
A simple working example, using PySide:
class MyModel(QtGui.QStandardItemModel):
def __init__(self):
super(MyModel, self).__init__()
for i in range(10): self.appendRow(QtGui.QStandardItem("Row %d" % i))
class MyDelegate(QtGui.QStyledItemDelegate):
def __init__(self, parent=None):
super(MyDelegate, self).__init__(parent)
self.links = {}
def makeLinkFunc(self, row, text):
def linkFunc(): print("Clicked on %s in row %d" % (text, row))
return linkFunc
def paint(self, painter, option, index):
painter.save()
textHeight = QtGui.QFontMetrics(painter.font()).height()
painter.drawText(option.rect.x()+2, option.rect.y()+2+textHeight, index.data())
rowLinks = {}
for i in range(3):
text = "Link %d" % (3-i)
linkWidth = QtGui.QFontMetrics(font).width(text)
x = option.rect.right() - (i+1) * (linkWidth + 10)
painter.drawText(x, y, text)
rect = QtCore.QRect(x, y - textHeight, linkWidth, textHeight)
rowLinks[rect] = self.makeLinkFunc(index.row(), text)
self.links[index.row()] = rowLinks
painter.restore()
def sizeHint(self, option, index):
hint = super().sizeHint(option, index)
hint.setHeight(30)
return hint
def editorEvent(self, event, model, option, index):
if event.type() == QtCore.QEvent.MouseButtonRelease:
for rect, link in self.links[index.row()].items():
if rect.contains(event.pos()):
link()
return True
return False
listmodel = MyModel()
listview = QtGui.QListView()
listview.setModel(listmodel)
listview.setItemDelegate(MyDelegate(parent=listview))
listview.setSelectionMode(QtGui.QAbstractItemView.NoSelection)

Resources