Auto-growing of QTreeWidget(or QTableWidget) - qt

I placed QTreeWidget into QVBoxLayout.
self._tree = QTreeWidget()
layout = QVBoxLayout()
layout.addWidget(self._some_other_widget)
layout.addWidget(self._tree)
layout.addStretch()
self.setLayout(layout)
All nodes in this tree widget are always expanded.
I would like to create this tree widget with minimal height (for example, for one row/item). When new items are added this tree widget also grows to show all items. How can this be done? Thank you in advance.

There are at least two solutions:
Use self._tree.setSizeAdjustPolicy(QAbstractScrollArea.AdjustToC‌​ontents). Thank you for this, G.M.
I have also found another solution:
def sizeHint(self):
return self._sizeHint()
def minimumSizeHint(self):
return self._sizeHint()
def _sizeHint(self):
sh = super().sizeHint()
if self._item_count > 0:
return QSize(sh.width(), self.sizeHintForRow(0) * self._item_count)
else:
return QSize(sh.width(), 40)

Related

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)

DIV tag equivalent in Qt Graphics Framework

I am working on a simple desktop application where I have to show a tree structure of folders and files along with other diagrams. For this I chose Qt and python (PySide). I need a structure like below (Forgive me for the bad drawing. But you get the idea):
The folders can be double clicked to expand/shrink. When a folder expands, new child elements need to take more space, and the folders below the current folder must move down. Similarly when the folder is shrunk, the folders below the current folder must come up; just like a standard folder system.
Hence I am in search of a <div> equivalent element in Qt where I can place each directory and all of its children inside that div and the div can expand and shrink. This way I don't have to write code for a re-draw every time the folder is opened/closed. Currently I have to calculate each item's position and place the child items respective to that position. That is a lot of calculation and no of items are > 1000. With a div, I will just re-calculate positions of child items and resize the div. Other divs can then automatically re-draw themselves.
I am not using QTreeView because as I said earlier, I have to draw other diagrams and connect these folders with them. QTreeView will live in its own space (with scroll bar and stuff), and I won't be able to draw lines to connect items in QTreeView and QGraphicsScene.
You can view my current work here in github. Here is the file that has my work.
I'm not sure what you're thinking of "<div>". It's just the most simple HTML container, and it seems to have nothing to do with your goal.
You can use graphics layouts to align items in the scene automatically. Here's how it can be implemented:
from PySide import QtGui, QtCore
class Leaf(QtGui.QGraphicsProxyWidget):
def __init__(self, path, folder = None):
QtGui.QGraphicsProxyWidget.__init__(self)
self.folder = folder
label = QtGui.QLabel()
label.setText(QtCore.QFileInfo(path).fileName())
self.setWidget(label)
self.setToolTip(path)
self.setAcceptedMouseButtons(QtCore.Qt.LeftButton)
def mousePressEvent(self, event):
if self.folder:
self.folder.toggleChildren()
class Folder(QtGui.QGraphicsWidget):
def __init__(self, path, isTopLevel = False):
QtGui.QGraphicsWidget.__init__(self)
self.offset = 32
childrenLayout = QtGui.QGraphicsLinearLayout(QtCore.Qt.Vertical)
childrenLayout.setContentsMargins(self.offset, 0, 0, 0)
flags = QtCore.QDir.AllEntries | QtCore.QDir.NoDotAndDotDot
for info in QtCore.QDir(path).entryInfoList(flags):
if info.isDir():
childrenLayout.addItem(Folder(info.filePath()))
else:
childrenLayout.addItem(Leaf(info.filePath()))
self.childrenWidget = QtGui.QGraphicsWidget()
self.childrenWidget.setLayout(childrenLayout)
mainLayout = QtGui.QGraphicsLinearLayout(QtCore.Qt.Vertical)
mainLayout.setContentsMargins(0, 0, 0, 0)
self.leaf = Leaf(path, self)
mainLayout.addItem(self.leaf)
mainLayout.addItem(self.childrenWidget)
if isTopLevel:
mainLayout.addStretch()
self.setLayout(mainLayout)
def paint(self, painter, option, widget):
QtGui.QGraphicsWidget.paint(self, painter, option, widget)
if self.childrenWidget.isVisible() and self.childrenWidget.layout().count() > 0:
lastChild = self.childrenWidget.layout().itemAt(self.childrenWidget.layout().count() - 1)
lastChildY = self.childrenWidget.geometry().top() + \
lastChild.geometry().top() + self.leaf.geometry().height() / 2;
painter.drawLine(self.offset / 2, self.leaf.geometry().bottom(), self.offset / 2, lastChildY)
for i in range(0, self.childrenWidget.layout().count()):
child = self.childrenWidget.layout().itemAt(i)
childY = self.childrenWidget.geometry().top() + \
child.geometry().top() + self.leaf.geometry().height() / 2
painter.drawLine(self.offset / 2, childY, self.offset, childY)
def toggleChildren(self):
if self.childrenWidget.isVisible():
self.layout().removeItem(self.childrenWidget)
self.childrenWidget.hide()
self.leaf.widget().setStyleSheet("QLabel { color : blue; }")
print "hide"
else:
self.childrenWidget.show()
self.layout().insertItem(1, self.childrenWidget)
self.leaf.widget().setStyleSheet("")
self.update()
if __name__ == "__main__":
import sys
app = QtGui.QApplication(sys.argv)
scene = QtGui.QGraphicsScene()
view = QtGui.QGraphicsView(scene)
# put your root path here
scene.addItem(Folder("/usr/share/alsa", True))
view.show()
view.resize(400, 400)
sys.exit(app.exec_())

wxPython Solitaire GUI

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()

Laying Widgets in QGridLayout

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.

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