Python+Qt, QScrollArea problem: what's wrong with this code? - python

I have the following code:
#!/usr/bin/env python
import sys
from PyQt4 import QtGui, QtCore
class SimfilePanel(QtGui.QWidget):
'''This class provides the simfile panel shown on the right side of the main window.'''
def __init__(self, parent=None):
'''Load song info here.'''
QtGui.QWidget.__init__(self, parent)
## Make widgets.
# Pane with simfile information.
simfileInfoPane = QtGui.QWidget()
simfileInfoPane.setSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Expanding)
simfileInfoGrid = QtGui.QGridLayout()
simfileInfoPane.setLayout(simfileInfoGrid)
simfileInfoScrollArea = QtGui.QScrollArea()
simfileInfoScrollArea.setWidget(simfileInfoPane)
#if DEBUG: simfileInfoScrollArea.setBackgroundRole(QtGui.QPalette.Dark);
# This will change
labels = []
textfields = []
for i in range(0,20):
labels.append( QtGui.QLabel("Label "+str(i)) )
textfields.append( QtGui.QLineEdit() )
labels[i].setBuddy(textfields[i])
simfileInfoGrid.addWidget(labels[i], i, 0)
simfileInfoGrid.addWidget(textfields[i], i, 1)
## Put widgets in a grid layout.
mainvbox = QtGui.QVBoxLayout()
mainvbox.addWidget(simfileInfoScrollArea)
self.setLayout(mainvbox)
# Standalone testing
if __name__ == "__main__":
app = QtGui.QApplication(sys.argv)
panel = SimfilePanel()
panel.show()
sys.exit(app.exec_())
I can't get anything that I'm putting into the simfileInfoGrid to display! They'll display if I leave out the scroll area, but I need the scroll area as I will have a lot of fields to edit in the final version and I don't want to stretch the entire window over the screen.
As you see I've tried to add a size policy to simfileInfoPane, but it doesn't seem to affect anything. The area that's supposed to contain my pane stays empty!

Add the pane to the scroll area after you've added all the grid's contents. In particular you need to call QScrollArea.setWidget after you have finished creating the widget you add.
I don't know exactly why this is the problem, but I do know that I tend to initialize widgets "bottom-up": I finish adding all the contents of a sub-layout before I ever add it to a parent layout. I believe this is Qt optimizing order of rendering but I could be wrong about that.
The code below is a patch, mostly so you can see where the one-line change is.
diff -u 1848547.py tmp2.py
--- 1848547.py 2009-12-04 11:19:09.000000000 -0800
+++ tmp2.py 2009-12-04 11:34:58.000000000 -0800
## -19,7 +19,6 ##
simfileInfoPane.setLayout(simfileInfoGrid)
simfileInfoScrollArea = QtGui.QScrollArea()
- simfileInfoScrollArea.setWidget(simfileInfoPane)
#if DEBUG:
simfileInfoScrollArea.setBackgroundRole(QtGui.QPalette.Dark)
## -33,6 +32,8 ##
simfileInfoGrid.addWidget(labels[i], i, 0)
simfileInfoGrid.addWidget(textfields[i], i, 1)
+ simfileInfoScrollArea.setWidget(simfileInfoPane)
+
## Put widgets in a grid layout.
mainvbox = QtGui.QVBoxLayout()
mainvbox.addWidget(simfileInfoScrollArea)

I have recently been struggling with the same thing, and I believe I have found the solution you are looking for.
The problem seems to be that when you add an empty widget to the scroll area, it has dimensions of zero by zero (since there is nothing inside of it).
The reason it doesn't get bigger is because there is a flag called widgetResizable that is False by default.
By simply calling setWidgetResizable(True) on the scroll area, the widget gets bigger as new items are added.
I hope this helps.

Related

How to make QTableWidget fill all available window space and show vertical scroll bar if it's larger, but avoid blank white space if it's smaller?

I want my QTableWidget to:
Fill all available space inside the window
Show vertical scroll bar if it's longer than the provided space
Adapt to changing window geometry
Do not show blank white space underneath, if it's smaller than the provided space (without stretching bottom row).
So basically what I want to see:
Short table without white space
Long table fills no less no more than available and shows scroll bar
But I can't achieve these two states simultaneously.
What I get instead:
Ugly white space beneath the table
Table stretches the window itself
The difference in the two behaviors is in calculating table height manually. If I do so, a long table stretches the window, but otherwise it tries to fill all unused space with blank white.
Here is my minimal code:
from PyQt6.QtWidgets import *
from PyQt6.QtCore import *
from PyQt6.QtGui import *
import os
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setGeometry(100, 100, 200, 100)
# self.tableLabel = QLabel() # I thought it might help somehow
self.table = QTableWidget(4, 1)
self.setTable(4)
self.table.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
self.fewRowsBtn = QPushButton('Few rows')
self.manyRowsBtn = QPushButton('Many rows')
self.button = QPushButton('Button')
self.button.setFixedWidth(120)
self.fewRowsBtn.clicked.connect(lambda: self.setTable(4))
self.manyRowsBtn.clicked.connect(lambda: self.setTable(20))
self.map = QLabel()
canvas = QPixmap(320, 320)
canvas.fill()
self.map.setPixmap(canvas)
leftLayout = QVBoxLayout()
leftLayout.addWidget(self.fewRowsBtn)
leftLayout.addWidget(self.manyRowsBtn)
leftLayout.addWidget(self.map)
rightLayout = QVBoxLayout()
rightLayout.addWidget(self.table)
rightLayout.addWidget(self.button) #, alignment=Qt.AlignmentFlag.AlignBottom)
generalLayout = QHBoxLayout()
generalLayout.addLayout(leftLayout)
generalLayout.addLayout(rightLayout)
container = QWidget()
container.setLayout(generalLayout)
self.setCentralWidget(container)
def setTable(self, rows):
self.table.setRowCount(rows)
# self.table.setFixedHeight(self.tableHeight())
def tableWidth(self):
tableWidth = self.table.verticalHeader().width() + \
self.table.horizontalHeader().length() + \
self.table.frameWidth() * 2
app.processEvents() # Otherwise verticalScrollBar().isVisible()
app.processEvents() # lags one event
if self.table.verticalScrollBar().isVisible():
tableWidth += self.table.verticalScrollBar().width()
return tableWidth
def tableHeight(self):
tableHeight = self.table.verticalHeader().length() + \
self.table.horizontalHeader().height() + \
self.table.frameWidth() * 2
return tableHeight
app = QApplication(os.sys.argv)
mywindow = MainWindow()
mywindow.show()
app.exec()
I tried playing with different size policies, but all of them give the behavior mentioned above.
I had a vague idea about placing the table in a label, but I don't know how it could help.
Additionally, one other behavior I don't understand is why adding top or bottom alignment for the button under the table makes available space half the height of the window. As if because of that the QVBoxLayout starts distributing space evenly between it's two items.
I think getting the value of the available space in a given window geometry migth help me adjust table size manually. So at least an advise how to do that could help me.
I use Python 3.9.12 and PyQt 6.3.0

Widgets not displaying with macOS vibrancy (NSVisualEffectView)

I have currently got a PyQt5 application which is quite simple (just one button). I'd like for it to have vibrancy, so I've ported ObjC vibrancy code to Python. My vibrancy code is as follows:
frame = NSMakeRect(0, 0, * self.get_screen_size()) # get_screen_size returns the resolution of the monitor
view = objc.objc_object(c_void_p=self.winId().__int__()) # returns NSView of current window
visualEffectView = NSVisualEffectView.new()
visualEffectView.setAutoresizingMask_(NSViewWidthSizable|NSViewHeightSizable) # equivalent to: visualEffectView.autoresizingMask = NSViewW...
visualEffectView.setFrame_(frame)
visualEffectView.setState_(NSVisualEffectStateActive)
visualEffectView.setMaterial_(NSVisualEffectMaterialDark)
visualEffectView.setBlendingMode_(NSVisualEffectBlendingModeBehindWindow)
window = view.window()
window.contentView().addSubview_positioned_relativeTo_(visualEffectView, NSWindowBelow, None)
# equal to: [window.contentView addSubview:visualEffectView positioned:NSWindowBelow relativeTo:nul]
window.setTitlebarAppearsTransparent_(True)
window.setStyleMask_(window.styleMask() | NSFullSizeContentViewWindowMask) # so the title bar is also vibrant
self.repaint()
All I'm doing to draw a button is: btn = QPushButton("test", self)
self is a class inherited from QMainWindow and everything else should be fairly unimportant.
Behaviour with the window.contentView().addSubview... line commented out (no vibrancy)
Behaviour without it commented out (with vibrancy)
Thanks!
The UI elements are actually drawn, but the NSVisualEffectView is covering them.
I fixed that issue by adding another view QMacCocoaViewContainer to the window.
You'll also need to set the Window transparent, otherwise the widgets will have a slight background border.
If you use the dark vibrant window also make sure to set appearance correctly, so buttons, labels etc. are rendered correctly.
frame = NSMakeRect(0, 0, self.width(), self.height())
view = objc.objc_object(c_void_p=self.winId().__int__())
visualEffectView = NSVisualEffectView.new()
visualEffectView.setAutoresizingMask_(NSViewWidthSizable|NSViewHeightSizable)
visualEffectView.setWantsLayer_(True)
visualEffectView.setFrame_(frame)
visualEffectView.setState_(NSVisualEffectStateActive)
visualEffectView.setMaterial_(NSVisualEffectMaterialUltraDark)
visualEffectView.setBlendingMode_(NSVisualEffectBlendingModeBehindWindow)
visualEffectView.setWantsLayer_(True)
self.setAttribute(Qt.WA_TranslucentBackground, True)
window = view.window()
content = window.contentView()
container = QMacCocoaViewContainer(0, self)
content.addSubview_positioned_relativeTo_(visualEffectView, NSWindowBelow, container)
window.setTitlebarAppearsTransparent_(True)
window.setStyleMask_(window.styleMask() | NSFullSizeContentViewWindowMask)
appearance = NSAppearance.appearanceNamed_('NSAppearanceNameVibrantDark')
window.setAppearance_(appearance)
Result:

Resizing table to contents in PyQt5 [duplicate]

I've googled around but I'm not able to find a solution to my problem.
I have a QTableWidget with 2 columns and what I'm trying to do is to make them visible to the whole widget without the horizontal scrollbar to appear.
With a picture it should be all clear:
I have used Qt Designer to create the UI and some code to fill all the widgets and other stuff.
So, first I resized th2 2 columns to the content with:
self.statTable.resizeColumnToContents(0)
self.statTable.resizeColumnToContents(1)
and it works, but then the Widget is not resizing to the 2 columns width.
This has a very easy solution in PyQt5. All you need to do is set the size adjust policy on the table when initialising the UI, and it will automatically resize to fit the contents. This can either be done via Qt Designer (in the QAbstractScrollArea section of the Property Editor), or programmatically, like this:
self.statTable.setSizeAdjustPolicy(
QtWidgets.QAbstractScrollArea.AdjustToContents)
You then just need to do:
self.statTable.resizeColumnsToContents()
whenever the table is re-populated.
For PyQt4, everything has to be calculated manually, and a few hacks are also required to get completely consistent results. The demo script below works okay for me, but YMMV:
import random
from PyQt4 import QtCore, QtGui
class Window(QtGui.QWidget):
def __init__(self):
super(Window, self).__init__()
self.table = QtGui.QTableWidget(5, 2, self)
self.button = QtGui.QPushButton('Populate', self)
self.button.clicked.connect(self.populate)
layout = QtGui.QGridLayout(self)
layout.addWidget(self.table, 0, 0)
layout.addWidget(self.button, 1, 0)
layout.setColumnStretch(1, 1)
def populate(self):
words = 'Red Green Blue Yellow Black White Purple'.split()
length = random.randint(2, len(words))
self.table.setRowCount(random.randint(3, 30))
for column in range(self.table.columnCount()):
for row in range(self.table.rowCount()):
item = QtGui.QTableWidgetItem(' '.join(
random.sample(words, random.randint(1, length))))
self.table.setItem(row, column, item)
self.table.setVisible(False)
self.table.verticalScrollBar().setValue(0)
self.table.resizeColumnsToContents()
self.table.setVisible(True)
self.setTableWidth()
def setTableWidth(self):
width = self.table.verticalHeader().width()
width += self.table.horizontalHeader().length()
if self.table.verticalScrollBar().isVisible():
width += self.table.verticalScrollBar().width()
width += self.table.frameWidth() * 2
self.table.setFixedWidth(width)
def resizeEvent(self, event):
self.setTableWidth()
super(Window, self).resizeEvent(event)
if __name__ == '__main__':
import sys
app = QtGui.QApplication(sys.argv)
window = Window()
window.setGeometry(700, 150, 800, 400)
window.show()
sys.exit(app.exec_())
For the autoadjust settings on the table widget in Qt Designer, you canlook in the object inspector for the table widget you can drill down to it as shown below.
(PyQt5)
The issue for me is that my cells in the right-most column are multi-line (QPlainTextEdit), and I wanted word-wrapping... but I also wanted this right-most column to extend to fill the parent container.
It seems you can do everything you need in PyQt5 designer, in the Property editor for your QTableView:
in the "QTableView" section check "wordWrap"
in the "QAbstractScroll" section check "AdjustToContents" (as mentioned by Crap Phone)
in the "Header" section check "horizontalHeaderStretchLastSection"
This then generates the following sort of code:
self.history_table_view.setSizeAdjustPolicy(QtWidgets.QAbstractScrollArea.AdjustToContentsOnFirstShow)
self.history_table_view.horizontalHeader().setStretchLastSection(True )
"word wrap = True" appears to be the default setting, so nothing is shown, but it would be this:
self.history_table_view.setWordWrap(True)
Use your_tablewidget.resizeColumnsToContents() every single time after you call your_tablewidget.setItem(). You don't need any other setting.
you know just try, tableWidget.resize(1200, 600) *you can change resolution but it is your answer...

Widget Windows in UltimateListCtrl Won't Resize/Refresh

My goal is to get a ULC that has a column of TextCtrls that will dynamically resize the row as the user types, so if there is a much better way, say so.
Here is what I've tried:
The ExpandoTextCtrl is exactly what I want. I have a working example, so I know I'm able to implement it correctly, I'm after a column of these:
import wx
import sys
import wx.lib.expando as ex
class TestPanel(wx.Panel):
def __init__(self, parent):
wx.Panel.__init__(self, parent)
text = "\"I\'ll Be Missing You\" is a song recorded by American rapper Puff Daddy and American singer Faith Evans, featuring R&B group 112, in memory of fellow Bad Boy Records artist Christopher \"The Notorious B.I.G.\" Wallace, who was gunned down on March 9, 1997. --Wikipedia"
self.edit_text = ex.ExpandoTextCtrl(self, value = text, size = (200,50))
self.edit_text.SetMaxHeight(sys.maxint)
class TestFrame(wx.Frame):
def __init__(self):
wx.Frame.__init__(self, None, title="expando Demo")
panel = TestPanel(self)
self.Show()
#----------------------------------------------------------------------
if __name__ == "__main__":
app = wx.App(False)
frame = TestFrame()
app.MainLoop()
As you add or remove lines it adjusts the height on the fly, for any character (not just the number of newlines or something simple like that).
If I add it to a cell in an UltimateListCtrl, it has a static size which is approximately column width and 2 lines of text visible, so it won't even instantiate showing the entire text, but it also won't resize either.
I have the same problem with other types of Windows is the list. I wrote a button that changes its size when you click it. This code of mine (I can post if you want but it feels redundant) runs perfectly in a panel of its own or in a Sizer with other widgets, etc, but in the ULC it will only instantiate at the original size and never changes with calls to button.SetSize().
I have researched bug reports for the ULC but haven't seen anything relevant and not fixed. I have tried calling the ULC's Refresh(), Update(), and Show(False/True) methods, and all of the above on the parent Panel and the Frame with no success.
Here is the code, based around a common example of the ULC online:
import wx
import sys
from wx.lib.agw import ultimatelistctrl as ULC
import wx.lib.expando as ex
class TestPanel(wx.Panel):
def __init__(self, parent):
wx.Panel.__init__(self, parent)
self.sizes = self.size_gen()
font = wx.SystemSettings_GetFont(wx.SYS_DEFAULT_GUI_FONT)
boldfont = wx.SystemSettings_GetFont(wx.SYS_DEFAULT_GUI_FONT)
boldfont.SetWeight(wx.BOLD)
boldfont.SetPointSize(12)
self.ultimateList = ULC.UltimateListCtrl(self, agwStyle = wx.LC_REPORT
| wx.LC_VRULES
| wx.LC_HRULES
| ULC.ULC_HAS_VARIABLE_ROW_HEIGHT)
info = ULC.UltimateListItem()
info._mask = wx.LIST_MASK_TEXT | wx.LIST_MASK_IMAGE | wx.LIST_MASK_FORMAT | ULC.ULC_MASK_CHECK
info._image = []
info._format = 0
info._kind = 1
info._text = "Artist Name"
self.ultimateList.InsertColumnInfo(0, info)
info = ULC.UltimateListItem()
info._format = wx.LIST_FORMAT_RIGHT
info._mask = wx.LIST_MASK_TEXT | wx.LIST_MASK_IMAGE | wx.LIST_MASK_FORMAT | ULC.ULC_MASK_FONT
info._image = []
info._text = "Title"
info._font = boldfont
self.ultimateList.InsertColumnInfo(1, info)
info = ULC.UltimateListItem()
info._mask = wx.LIST_MASK_TEXT | wx.LIST_MASK_IMAGE | wx.LIST_MASK_FORMAT
info._format = 0
info._text = "Genre"
info._font = font
info._image = []
self.ultimateList.InsertColumnInfo(2, info)
self.ultimateList.InsertStringItem(0, "Newsboys")
self.ultimateList.SetStringItem(0, 1, "Go")
self.ultimateList.SetStringItem(0, 2, "Rock")
text = "\"I\'ll Be Missing You\" is a song recorded by American rapper Puff Daddy and American singer Faith Evans, featuring R&B group 112, in memory of fellow Bad Boy Records artist Christopher \"The Notorious B.I.G.\" Wallace, who was gunned down on March 9, 1997. --Wikipedia"
self.ultimateList.InsertStringItem(1, "Puffy")
edit_text = ex.ExpandoTextCtrl(self.ultimateList, value = text, size=(200,50))
edit_text.SetMaxHeight(sys.maxint)
self.ultimateList.SetItemWindow(1, col=1, wnd=edit_text, expand=True)
self.ultimateList.SetStringItem(1, 2, "Pop")
self.ultimateList.InsertStringItem(2, "Family Force 5")
self.button = wx.Button(self.ultimateList, label='button', size =(200,200))
self.button.Bind(wx.EVT_BUTTON, self.on_button)
self.ultimateList.SetItemWindow(2, 1, self.button, expand=True)
#self.ultimateList.SetStringItem(2, 1, "III")
self.ultimateList.SetStringItem(2, 2, "Crunk")
self.ultimateList.SetColumnWidth(0, 150)
self.ultimateList.SetColumnWidth(1, 200)
self.ultimateList.SetColumnWidth(2, 100)
sizer = wx.BoxSizer(wx.VERTICAL)
sizer.Add(self.ultimateList, 1, wx.EXPAND)
self.SetSizer(sizer)
def on_button(self, event):
self.button.SetSize(self.sizes.next())
def size_gen(self):
sizes = [(150,200),(200,200),(80,80)]
index = 0
while True:
yield sizes[index]
index = index + 1
if index > 2:
index = 0
########################################################################
class TestFrame(wx.Frame):
def __init__(self):
wx.Frame.__init__(self, None, title="MvP UltimateListCtrl Demo")
panel = TestPanel(self)
self.Show()
#----------------------------------------------------------------------
if __name__ == "__main__":
app = wx.App(False)
frame = TestFrame()
app.MainLoop()
EDIT
I've tried several more approaches now. I got rid of all the column formatting and replaced those 3 blocks with simplelist.InsertColumn(index, label) calls. The most useful thing I did was remove the expand=True from the ULC.SetItemWindow() call. This seems to have returned control of the button's width (but not height) to the button. Since it starts at 150Wx200H, when I call next and it changes to 200x200, the button overflows into the next cell to the right. When I call next again, commanding an 80x80 size, the button shrinks to 150x200, its unable to be set smaller than its initial size.
If I initialize the button smaller than I ever need, say 50x30, then I can set all the sizes (80,80;200,200) correctly in both dimensions, but the button then overflows into its neighbours to the right and below.
You can see that the list is not refreshing any of the rows.
You can also see that the widget maintains its original upper left corner position (maybe this is correct, but I don't think so)
If I add list.Refresh/DoLayout/Layout/Update to the on_button, it has no effect.
Dragging or repositioning the window (top level Frame) has no effect.
ULC.SendSizeEvent has no effect.
Next Idea
I also tried deleting the entire row and inserting a new button of new size, like so:
def on_button(self,event):
new_size = self.sizes.next()
print new_size
l = self.ultimateList
label = l.GetItemWindow(1, 1).GetLabel()
l.DeleteItem(1)
self.button = wx.Button(self.ultimateList, label=label, size=new_size)
self.button.Bind(wx.EVT_BUTTON, self.on_button)
l.InsertStringItem(1, 'Family Farce 5')
l.SetItemWindow(1,1,self.button)
l.SetStringItem(1,2,'Crunk')
I don't think this is an ok strategy, as destroying and rebuilding an edit_text on every keystroke sounds like it would have a whole lot of problems, but to be fair I haven't tried it yet.
Anyway, with the button I can call it and resizes correctly. The problem is that the ULC doesn't redraw subsequent rows based on the new height, or even the initial height, but the default height of a row. It does draw the recreated row with the button the correct height.
This time (deleting the line and adding a new line) I noticed resizing the window (top level Frame) forced the redraw, so I added SendSizeEvent to on_button and now the button works perfectly.
Apparently I've worked out how to replace a row with one of a new size.
So the question is still about Windows in a ULC, I can dynamically resize a widget, but how can I force the ULC to redraw itself after sizing a widget?
Another way to say it: Why does SendSizeEvent force a refresh after inserting a new item into the list, but not after modifying an existing item? I could subclass the ULC and extend a particular method or property if I knew which one to do. I've tried looking at the source but I can't make heads or tails of it.
First: ULC creates the line heights only once and if the height is already there when painting a line, it wont do it again.
Furthermore: When you add a new window, the size gets written into the line object. This will never be done again and cannot be triggered by any means manually.
However:
If you adjust UltimateListCtrl like this it will work.
Function GetWindowSize of class UltimateListItemData (line 2863):
def GetWindowSize(self):
""" Returns the associated window size. """
wnd = self._wnd
if wnd.GetSizer(): # the window is a complex one hold by a sizer
size = wnd.GetBestSize()
else: # simple window, without sizers
size = wnd.GetSize()
print(size)
return size
In your code you need to invalidate the old line heights, so it gets recalculated:
def on_button(self, event):
self.button.SetSize(self.sizes.next())
self.ultimateList._mainWin.ResetLineDimensions(True)
self.ultimateList._mainWin.RecalculatePositions()
Please note, that this modifies ULC and accesses private variables.
Lokla

How to add a fixed header to a QScrollArea?

I currently have a QScrollArea defined by:
self.results_grid_scrollarea = QScrollArea()
self.results_grid_widget = QWidget()
self.results_grid_layout = QGridLayout()
self.results_grid_layout.setSizeConstraint(QLayout.SetMinAndMaxSize)
self.results_grid_widget.setLayout(self.results_grid_layout)
self.results_grid_scrollarea.setWidgetResizable(True)
self.results_grid_scrollarea.setWidget(self.results_grid_widget)
self.results_grid_scrollarea.setViewportMargins(0,20,0,0)
which sits quite happily nested within other layouts/widgets, resizes as expected, etc.
To provide headings for the grid columns, I'm using another QGridLayout positioned directly above the scroll area - this works... but looks a little odd, even when styled appropriately, especially when the on-demand (vertical) scrollbar appears or disappears as needed and the headers no longer line up correctly with the grid columns. It's an aesthetic thing I know... but I'm kinda picky ;)
Other widgets are added/removed to the self.results_grid_layout programatically elsewhere. The last line above I've just recently added as I thought it would be easy to use the created margin area, the docs for setViewportMargins state:
Sets margins around the scrolling area. This is useful for applications such as spreadsheets with "locked" rows and columns. The marginal space is is left blank; put widgets in the unused area.
But I cannot for the life of me work out how to actually achieve this, and either my GoogleFu has deserted me today, or there's little information/examples out there on how to actually achieve this.
My head is telling me I can assign just one widget, controlled by a layout (containing any number of other widgets) to the scrollarea - as I have done. If I add say a QHeaderview for example to row 0 of the gridlayout, it will just appear below the viewport's margin and scroll with the rest of the layout? Or am I missing something and just can't see the wood for the trees?
I'm just learning Python/Qt, so any help, pointers and/or examples (preferably with Python but not essential) would be appreciated!
Edit: Having followed the advice given so far (I think), I came up with the following little test program to try things out:
import sys
from PySide.QtCore import *
from PySide.QtGui import *
class MainWindow(QMainWindow):
def __init__(self, parent=None):
super(MainWindow, self).__init__(parent)
self.setMinimumSize(640, 480)
self.container_widget = QWidget()
self.container_layout = QVBoxLayout()
self.container_widget.setLayout(self.container_layout)
self.setCentralWidget(self.container_widget)
self.info_label = QLabel(
"Here you can see the problem.... I hope!\n"
"Once the window is resized everything behaves itself.")
self.info_label.setWordWrap(True)
self.headings_widget = QWidget()
self.headings_layout = QGridLayout()
self.headings_widget.setLayout(self.headings_layout)
self.headings_layout.setContentsMargins(1,1,0,0)
self.heading_label1 = QLabel("Column 1")
self.heading_label1.setContentsMargins(16,0,0,0)
self.heading_label2 = QLabel("Col 2")
self.heading_label2.setAlignment(Qt.AlignCenter)
self.heading_label2.setMaximumWidth(65)
self.heading_label3 = QLabel("Column 3")
self.heading_label3.setContentsMargins(8,0,0,0)
self.headings_layout.addWidget(self.heading_label1,0,0)
self.headings_layout.addWidget(self.heading_label2,0,1)
self.headings_layout.addWidget(self.heading_label3,0,2)
self.headings_widget.setStyleSheet(
"background: green; border-bottom: 1px solid black;" )
self.grid_scrollarea = QScrollArea()
self.grid_widget = QWidget()
self.grid_layout = QGridLayout()
self.grid_layout.setSizeConstraint(QLayout.SetMinAndMaxSize)
self.grid_widget.setLayout(self.grid_layout)
self.grid_scrollarea.setWidgetResizable(True)
self.grid_scrollarea.setWidget(self.grid_widget)
self.grid_scrollarea.setViewportMargins(0,30,0,0)
self.headings_widget.setParent(self.grid_scrollarea)
### Add some linedits to the scrollarea just to test
rows_to_add = 10
## Setting the above to a value greater than will fit in the initial
## window will cause the lineedits added below to display correctly,
## however - using the 10 above, the lineedits do not expand to fill
## the scrollarea's width until you resize the window horizontally.
## What's the best way to fix this odd initial behaviour?
for i in range(rows_to_add):
col1 = QLineEdit()
col2 = QLineEdit()
col2.setMaximumWidth(65)
col3 = QLineEdit()
row = self.grid_layout.rowCount()
self.grid_layout.addWidget(col1,row,0)
self.grid_layout.addWidget(col2,row,1)
self.grid_layout.addWidget(col3,row,2)
### Define Results group to hold the above sections
self.test_group = QGroupBox("Results")
self.test_layout = QVBoxLayout()
self.test_group.setLayout(self.test_layout)
self.test_layout.addWidget(self.info_label)
self.test_layout.addWidget(self.grid_scrollarea)
### Add everything to the main layout
self.container_layout.addWidget(self.test_group)
def resizeEvent(self, event):
scrollarea_vpsize = self.grid_scrollarea.viewport().size()
scrollarea_visible_size = self.grid_scrollarea.rect()
desired_width = scrollarea_vpsize.width()
desired_height = scrollarea_visible_size.height()
desired_height = desired_height - scrollarea_vpsize.height()
new_geom = QRect(0,0,desired_width+1,desired_height-1)
self.headings_widget.setGeometry(new_geom)
def main():
app = QApplication(sys.argv)
form = MainWindow()
form.show()
app.exec_()
if __name__ == '__main__':
main()
Is something along these lines the method to which you were pointing? Everything works as expected as is exactly what I was after, except for some odd initial behaviour before the window is resized by the user, once it is resized everything lines up and is fine.
I'm probably over-thinking again or at least overlooking something... any thoughts?
I had a similar problem and solved it a little differently. Instead of using one QScrollArea I use two and forward a movement of the lower scroll area to the top one. What the code below does is
It creates two QScrollArea widgets in a QVBoxLayout.
It disables the visibility of the scroll bars of the top QScrollArea and assigns it a fixed height.
Using the valueChanged signal of the horizontal scroll bar of the lower QScrollArea it is possible to "forward" the horizontal scroll bar value from the lower QScrollArea to the top one resulting a fixed header at the top of the window.
class MainWindow(QMainWindow):
def __init__(self, parent=None):
super(MainWindow, self).__init__(parent)
widget = QWidget()
self.setCentralWidget(widget)
vLayout = QVBoxLayout()
widget.setLayout(vLayout)
# TOP
scrollAreaTop = QScrollArea()
scrollAreaTop.setWidgetResizable(True)
scrollAreaTop.setFixedHeight(30)
scrollAreaTop.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
scrollAreaTop.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
scrollAreaTop.setWidget(QLabel(" ".join([str(i) for i in range(100)])))
# BOTTOM
scrollAreaBottom = QScrollArea()
scrollAreaBottom.setWidgetResizable(True)
scrollAreaBottom.setWidget(QLabel("\n".join([" ".join([str(i) for i in range(100)]) for _ in range(10)])))
scrollAreaBottom.horizontalScrollBar().valueChanged.connect(lambda value: scrollAreaTop.horizontalScrollBar().setValue(value))
vLayout.addWidget(scrollAreaTop)
vLayout.addWidget(scrollAreaBottom)
You may be over-thinking things slightly.
All you need to do is use the geometry of the scrollarea's viewport and the current margins to calculate the geometry of any widgets you want to place in the margins.
The geometry of these widgets would also need to be updated in the resizeEvent of the scrollarea.
If you look at the source code for QTableView, I think you'll find it uses this method to manage its header-views (or something very similar).
EDIT
To deal with the minor resizing problems in your test case, I would advise you to read the Coordinates section in the docs for QRect (in particular, the third paragraph onwards).
I was able to get more accurate resizing by rewriting your test case like this:
import sys
from PySide.QtCore import *
from PySide.QtGui import *
class MainWindow(QMainWindow):
def __init__(self, parent=None):
super(MainWindow, self).__init__(parent)
self.setMinimumSize(640, 480)
self.container_widget = QWidget()
self.container_layout = QVBoxLayout()
self.container_widget.setLayout(self.container_layout)
self.setCentralWidget(self.container_widget)
self.grid_scrollarea = ScrollArea(self)
self.test_group = QGroupBox("Results")
self.test_layout = QVBoxLayout()
self.test_group.setLayout(self.test_layout)
self.test_layout.addWidget(self.grid_scrollarea)
self.container_layout.addWidget(self.test_group)
class ScrollArea(QScrollArea):
def __init__(self, parent=None):
QScrollArea.__init__(self, parent)
self.grid_widget = QWidget()
self.grid_layout = QGridLayout()
self.grid_widget.setLayout(self.grid_layout)
self.setWidgetResizable(True)
self.setWidget(self.grid_widget)
# save the margin values
self.margins = QMargins(0, 30, 0, 0)
self.setViewportMargins(self.margins)
self.headings_widget = QWidget(self)
self.headings_layout = QGridLayout()
self.headings_widget.setLayout(self.headings_layout)
self.headings_layout.setContentsMargins(1,1,0,0)
self.heading_label1 = QLabel("Column 1")
self.heading_label1.setContentsMargins(16,0,0,0)
self.heading_label2 = QLabel("Col 2")
self.heading_label2.setAlignment(Qt.AlignCenter)
self.heading_label2.setMaximumWidth(65)
self.heading_label3 = QLabel("Column 3")
self.heading_label3.setContentsMargins(8,0,0,0)
self.headings_layout.addWidget(self.heading_label1,0,0)
self.headings_layout.addWidget(self.heading_label2,0,1)
self.headings_layout.addWidget(self.heading_label3,0,2)
self.headings_widget.setStyleSheet(
"background: green; border-bottom: 1px solid black;" )
rows_to_add = 10
for i in range(rows_to_add):
col1 = QLineEdit()
col2 = QLineEdit()
col2.setMaximumWidth(65)
col3 = QLineEdit()
row = self.grid_layout.rowCount()
self.grid_layout.addWidget(col1,row,0)
self.grid_layout.addWidget(col2,row,1)
self.grid_layout.addWidget(col3,row,2)
def resizeEvent(self, event):
rect = self.viewport().geometry()
self.headings_widget.setGeometry(
rect.x(), rect.y() - self.margins.top(),
rect.width() - 1, self.margins.top())
QScrollArea.resizeEvent(self, event)
if __name__ == '__main__':
app = QApplication(sys.argv)
form = MainWindow()
form.show()
sys.exit(app.exec_())

Categories