With Python and PyQt5 I programmed follow GUI.
Now I want customize the program so that I'm able to change the left table width by click on the right table border (light blue line) and moving the mouse (see arrows in picture). while changing the table width, the right side should also change.
I hope my description is clear.
GUI output:
Part of the code for GUI layout:
# table for writeOutput
self.tableWidget = QTableWidget(0, 2)
self.tableWidget.verticalHeader().setVisible(False);
self.tableWidget.setMinimumHeight(170)
self.tableWidget.setMinimumWidth(600)
# generate chart
self.main_widget = QWidget(self)
self.chart = QtBarChart(self.main_widget)
# Layout for input elements
inputLayout = QHBoxLayout()
inputLayout.addWidget(self.typeCBox)
inputLayout.addWidget(self.yearSpinBox)
inputLayout.addWidget(self.button)
inputLayout.addStretch(1)
# right inner layout for output elements e.g chart
innerOutLayout = QVBoxLayout()
innerOutLayout.addWidget(self.chart)
innerOutLayout.addWidget(self.outEdit)
# outer layout for output elements e.g. table
outLayout = QHBoxLayout()
outLayout.addWidget(self.tableWidget)
outLayout.addLayout(innerOutLayout)
# top layout who groups all elements together
topLevelLayout = QVBoxLayout()
topLevelLayout.addLayout(inputLayout)
topLevelLayout.addLayout(outLayout)
What kind of function or code changes do I need to achieve my goal?
--> QSplitter is a good hint
When I try to implement a QSplitter and change my program as below I get an error.
# Layout for input elements
inputLayout = QHBoxLayout()
inputLayout.addWidget(self.typeCBox)
inputLayout.addWidget(self.yearSpinBox)
inputLayout.addWidget(self.button)
inputLayout.addStretch(1)
# right inner layout for output elements e.g chart
innerOutLayout = QVBoxLayout()
innerOutLayout.addWidget(self.chart)
innerOutLayout.addWidget(self.outEdit)
# create Splitter between Table and innerOutLayout() --> (Chart and outEdit)
tableSplitter = QSplitter(Qt.Horizontal)
tableSplitter.addWidget(self.tableWidget)
tableSplitter.addLayout(innerOutWidget)
# outer layout for output elements e.g. table
outLayout = QHBoxLayout()
outLayout.addWidget(self.tableWidget)
outLayout.addWidget(tableSplitter)
# top layout who groups all elements together
topLevelLayout = QVBoxLayout()
topLevelLayout.addLayout(inputLayout)
topLevelLayout.addLayout(outLayout)
When I ran the program I get following error:
tableSplitter.addLayout(innerOutWidget)
AttributeError: 'QSplitter' object has no attribute 'addLayout'
How can I solve this error and how to change to code to work.
The solution is to use QSplitter, these are similar to QLayout, the goal is to control the size of the widgets.
In the following example example shows its use.
from PyQt5 import QtCore, QtGui, QtWidgets
class Widget(QtWidgets.QWidget):
def __init__(self, parent=None):
super(Widget, self).__init__(parent=parent)
self.verticalLayout = QtWidgets.QVBoxLayout(self)
self.splitter = QtWidgets.QSplitter(self)
self.splitter.setOrientation(QtCore.Qt.Horizontal)
self.createTable(self.splitter, 2, 2)
self.splitter1 = QtWidgets.QSplitter(self.splitter)
self.splitter1.setOrientation(QtCore.Qt.Vertical)
self.createTable(self.splitter1, 3, 3)
self.createTable(self.splitter1, 4, 4)
self.verticalLayout.addWidget(self.splitter)
def createTable(self, parent, nrows, ncols):
tableWidget = QtWidgets.QTableWidget(parent)
tableWidget.setRowCount(nrows)
tableWidget.setColumnCount(ncols)
for i in range(tableWidget.rowCount()):
for j in range(tableWidget.columnCount()):
item = QtWidgets.QTableWidgetItem(f"{i}{j}")
tableWidget.setItem(i, j, item)
return tableWidget
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
w = Widget()
w.show()
sys.exit(app.exec_())
Related
In the following code:
import sys
from PyQt5 import QtWidgets
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QApplication, QWidget, QHBoxLayout, QTableWidget, QPushButton
class Window(QtWidgets.QMainWindow):
def __init__(self, method):
super(Window, self).__init__()
mainWidget = QWidget()
mainLayout = QHBoxLayout(mainWidget)
table = QTableWidget(10, 3)
button1 = QPushButton("Play")
button2 = QPushButton("Cancel")
mainLayout.addWidget(table)
mainLayout.addWidget(button1)
mainLayout.addWidget(button2)
if (method == 1):
rtnValue = mainLayout.setAlignment(button1, Qt.AlignTop)
print("Method 1:", rtnValue)
elif (method == 2):
rtnValue = mainLayout.setAlignment(mainLayout, Qt.AlignTop)
print("Method 2:", rtnValue)
else:
rtnValue = mainLayout.setAlignment(Qt.AlignTop)
print("Method X:", rtnValue)
self.setCentralWidget(mainWidget)
self.show()
if __name__ == "__main__":
print("python QLayoutAlignment.py[ <MethodToUse=1>")
app = QApplication(sys.argv)
method = 1 if (len(sys.argv) < 2) else int(sys.argv[1])
GUI = Window(method)
sys.exit(app.exec_())
when I call the program like below with mainLayout.setAlignment(button1, Qt.AlignTop) being called, it works as expected with the "Play" button aligned at the top and "Cancel" button aligned at the center vertically. I also found the documentation for bool QLayout::setAlignment(QWidget *w, Qt::Alignment alignment) although in Qt.
python QLayoutAlignment.py 1
However when I call the the program like below with mainLayout.setAlignment(mainLayout, Qt.AlignTop) being called, it does not seem to work. All the buttons are vertically center aligned. I interpreted the Qt documentation of bool QLayout::setAlignment(QLayout *l, Qt::Alignment alignment)) as "it align all the added widget of the layout to the set alignment". So what does this function actually do (when is it used)?
python QLayoutAlignment.py 2
Lastly, I also saw another example from Center and top align a QVBoxLayout. When I call the program like below with mainLayout.setAlignment(Qt.AlignTop) being called, it also does not work with all the buttons vertically center aligned. For this one I could not find its documentation. So what does this function actually do (when is it used) and where can I find its documentation?
python QLayoutAlignment.py 3
The .setAlignment method which accepts a layout is used for aligning sub-layouts, that is child layouts you've added to the parent layout using .addLayout.
Below is a little demo based on your code.
import sys
from PyQt5 import QtWidgets
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QApplication, QWidget, QHBoxLayout, QTableWidget, QPushButton
class Window(QtWidgets.QMainWindow):
def __init__(self, method=0):
super(Window, self).__init__()
mainWidget = QWidget()
self.mainLayout = QHBoxLayout(mainWidget)
table = QTableWidget(10, 3)
self.button1 = QPushButton("Play")
self.button2 = QPushButton("Cancel")
self.subLayout = QHBoxLayout()
buttona1 = QPushButton("1")
buttona1.clicked.connect(self.clicked1)
buttona2 = QPushButton("2")
buttona2.clicked.connect(self.clicked2)
buttona3 = QPushButton("3")
buttona3.clicked.connect(self.clicked3)
buttona4 = QPushButton("4")
buttona4.clicked.connect(self.clicked4)
self.subLayout.addWidget(buttona1)
self.subLayout.addWidget(buttona2)
self.subLayout.addWidget(buttona3)
self.subLayout.addWidget(buttona4)
self.mainLayout.addWidget(table)
self.mainLayout.addWidget(self.button1)
self.mainLayout.addWidget(self.button2)
self.mainLayout.addLayout(self.subLayout)
self.setCentralWidget(mainWidget)
self.show()
def clicked1(self):
rtnValue = self.mainLayout.setAlignment(self.button1, Qt.AlignTop)
print("Method 1:", rtnValue)
def clicked2(self):
rtnValue = self.mainLayout.setAlignment(self.mainLayout, Qt.AlignTop)
print("Method 2:", rtnValue)
def clicked3(self):
rtnValue = self.mainLayout.setAlignment(Qt.AlignTop)
print("Method 3:", rtnValue)
def clicked4(self):
rtnValue = self.mainLayout.setAlignment(self.subLayout, Qt.AlignTop)
print("Method 4:", rtnValue)
if __name__ == "__main__":
print("python QLayoutAlignment.py[ <MethodToUse=1>")
app = QApplication(sys.argv)
method = 1 if (len(sys.argv) < 2) else int(sys.argv[1])
GUI = Window(method)
sys.exit(app.exec_())
You'll notice if you trigger this self.mainLayout.setAlignment(self.mainLayout, Qt.AlignTop) the return value is False. This is telling you that the layout you're aligning could not be found in the current layout. Since you're calling .setAlignment on mainLayout the layout you're affecting must be in that layout.
In the 4th method, I've added a sub-layout, and as you can see this ( rtnValue = self.mainLayout.setAlignment(self.subLayout, Qt.AlignTop)) works as expected and returns True.
First of all, it's important to understand that the Qt layout system works by using layout items (see QLayoutItem), which are abstract items that are used as virtual containers for objects: widgets, spacers and layouts (when nested layouts are used). Every QLayout is, in fact, a subclass of QLayoutItem.
Using setAlignment means setting the alignment of the specified layout item:
layout.setAlignment(item, alignment) sets the alignment of item, which has to be directly contained in layout;
layout.setAlignment(alignment) sets the alignment of layout related to its parent layout; note that this does not mean that items inside layout will use the specified alignment;
Your second case, mainLayout.setAlignment(mainLayout, Qt.AlignTop), does not work and returns False because mainLayout is, obviously, not "contained" in mainLayout. In fact, if you carefully read the documentation, it says:
returns true if w/l is found in this layout (not including child layouts);
In your third case, you don't see any result because mainLayout is the top layout for the widget, and since there's no parent layout the alignment seems to be ignored. As specified above, using layout.setAlignment(alignment) does not set the alignment of child items, but only of the layout item of layout. If you add that mainLayout to another layout, you will see that the alignment is respected for the layout.
Setting the layout alignment is rarely used, also because it's often counterintuitive: one might led to believe that setting the alignment of a layout will align its contents, but that's not the case.
To clarify, consider that setting the layout alignment is almost the same as creating a widget with that layout, and adding that widget with the specified alignment. With that in mind, it makes more sense: you're not aligning the contents of the widget, but the widget itself.
Consider the following example: besides the table on the left (used for comparison), I'm adding a layout on the left by specifying its alignment, then I'm adding a widget on the right by specifying the alignment of the widget for the main layout. As you can see, they appear exactly the same.
from PyQt5 import QtCore, QtWidgets
import sys
app = QtWidgets.QApplication(sys.argv)
test = QtWidgets.QWidget()
mainLayout = QtWidgets.QHBoxLayout(test)
# a very tall table to show the difference in alignment
mainLayout.addWidget(QtWidgets.QTableView(minimumHeight=300))
leftLayout = QtWidgets.QHBoxLayout()
# setting the alignment of leftLayout relative to mainLayout
leftLayout.setAlignment(QtCore.Qt.AlignTop)
leftLayout.addWidget(QtWidgets.QTableView(maximumHeight=100))
leftLayout.addWidget(QtWidgets.QPushButton())
mainLayout.addLayout(leftLayout)
rightWidget = QtWidgets.QWidget()
# adding the widget to mainLayout by aligning it on top as with leftLayout
mainLayout.addWidget(rightWidget, alignment=QtCore.Qt.AlignTop)
rightLayout = QtWidgets.QHBoxLayout(rightWidget)
rightLayout.addWidget(QtWidgets.QTableView(maximumHeight=100))
rightLayout.addWidget(QtWidgets.QPushButton())
test.show()
sys.exit(app.exec_())
Finally, if you want to align widgets, you either specify the alignment for each widget, or you add nested layout.
When many widgets are going to be added with the same alignment, the nested layout is usually the best solution: in your case, add a vertical layout to the main layout, then add horizontal layout to the vertical for the buttons, and add a stretch to the vertical to "push" the horizontal layout on top.
Alternatively, you can use a grid layout and eventually use spacers to ensure that the widgets are "aligned" as required.
from PyQt5 import QtCore, QtWidgets
import sys
app = QtWidgets.QApplication(sys.argv)
test = QtWidgets.QWidget()
mainLayout = QtWidgets.QHBoxLayout(test)
class Button(QtWidgets.QPushButton):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.setSizePolicy(
QtWidgets.QSizePolicy.Preferred,
QtWidgets.QSizePolicy.Preferred
)
noAlignGroup = QtWidgets.QGroupBox('no alignment')
mainLayout.addWidget(noAlignGroup)
noAlignLayout = QtWidgets.QHBoxLayout(noAlignGroup)
noAlignLayout.addWidget(QtWidgets.QTableView())
noAlignLayout.addWidget(Button())
noAlignLayout.addWidget(Button())
widgetAlignGroup = QtWidgets.QGroupBox('addWidget(widget, alignment)')
mainLayout.addWidget(widgetAlignGroup)
widgetAlignLayout = QtWidgets.QHBoxLayout(widgetAlignGroup)
widgetAlignLayout.addWidget(QtWidgets.QTableView())
widgetAlignLayout.addWidget(Button(), alignment=QtCore.Qt.AlignTop)
widgetAlignLayout.addWidget(Button(), alignment=QtCore.Qt.AlignTop)
layoutAlignGroup = QtWidgets.QGroupBox('nestedLayout.setAlignment()')
mainLayout.addWidget(layoutAlignGroup)
layoutAlignLayout = QtWidgets.QHBoxLayout(layoutAlignGroup)
layoutAlignLayout.addWidget(QtWidgets.QTableView())
buttonLayout = QtWidgets.QHBoxLayout()
layoutAlignLayout.addLayout(buttonLayout)
buttonLayout.setAlignment(QtCore.Qt.AlignTop)
buttonLayout.addWidget(Button())
buttonLayout.addWidget(Button())
stretchGroup = QtWidgets.QGroupBox('nestedLayout + stretch')
mainLayout.addWidget(stretchGroup)
stretchLayout = QtWidgets.QHBoxLayout(stretchGroup)
stretchLayout.addWidget(QtWidgets.QTableView())
rightLayout = QtWidgets.QVBoxLayout()
stretchLayout.addLayout(rightLayout)
buttonLayout = QtWidgets.QHBoxLayout()
rightLayout.addLayout(buttonLayout)
buttonLayout.addWidget(Button())
buttonLayout.addWidget(Button())
rightLayout.addStretch()
gridAlignGroup = QtWidgets.QGroupBox('grid + spacer')
mainLayout.addWidget(gridAlignGroup)
gridLayout = QtWidgets.QGridLayout(gridAlignGroup)
gridLayout.addWidget(QtWidgets.QTableView(), 0, 0, 2, 1)
gridLayout.addWidget(Button(), 0, 1)
gridLayout.addWidget(Button(), 0, 2)
spacer = QtWidgets.QSpacerItem(1, 50, vPolicy=QtWidgets.QSizePolicy.Expanding)
gridLayout.addItem(spacer, 1, 1)
test.show()
sys.exit(app.exec_())
I am trying to create a dynamic GUI with multiple Groupbox objects in a QVBoxLayout. As there are a lot of them, I will be needing a scroll area to make them available to the end user.
So I tried to change to top widget of this tab from a QWidget to a QScrollArea.
Before the change:
This is the kind of result I want but with a scroll bar because the window is too high.
After the change to QScrollArea:
My GroupBoxs are now "collapsed" and there is not scrollbar. I tried setting their size but it is not adequate because they are not fixed. I searched the documentation and tried to use WidgetResizable or I tried to set a fixed height or the sizehint but nothing worked as I wanted.
After creating the the Groupbox, the sizeHint for my QScrollArea is already very low (around 150px of height) so I think I'm missing a parameter.
It would be complicated to provide code as it is intricate. If necessary I could recreate the problem in a simpler way.
How to reproduce:
from PyQt5 import QtWidgets, QtGui, QtCore
from PyQt5.QtWidgets import *
import sys
class Example(QMainWindow):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
v_layout = QVBoxLayout()
scroll_area = QScrollArea()
self.layout().addWidget(scroll_area)
scroll_area.setLayout(v_layout)
# v_layout.setSizeConstraint(QLayout.SetMinimumSize)
for i in range(50):
box = QGroupBox()
grid = QGridLayout()
box.setLayout(grid)
grid.addWidget(QLabel("totototo"), 0, 0)
grid.addWidget(QLineEdit(), 1, 0)
grid.addWidget(QPushButton(), 2, 0)
v_layout.addWidget(box)
self.show()
app = QApplication(sys.argv)
ex = Example()
sys.exit(app.exec_())
Uncommenting # v_layout.setSizeConstraint(QLayout.SetMinimumSize) allows the content of the group boxes to deploy and fixes the first part of the issue. But there is still not scroll bar.
You have 2 errors:
A widget should not be added to the layout of a QMainWindow, but the setCentralWidget method should be used.
You should not add the layout to the QScrollArea but use a widget as a container for the other widgets, also if you use layouts then you have to activate the widgetResizable property.
Considering the above, the solution is:
def initUI(self):
scroll_area = QScrollArea(widgetResizable=True)
self.setCentralWidget(scroll_area)
container = QWidget()
scroll_area.setWidget(container)
v_layout = QVBoxLayout(container)
for i in range(50):
box = QGroupBox()
grid = QGridLayout()
box.setLayout(grid)
grid.addWidget(QLabel("totototo"), 0, 0)
grid.addWidget(QLineEdit(), 1, 0)
grid.addWidget(QPushButton(), 2, 0)
v_layout.addWidget(box)
self.show()
I'm working on a GUI application with pyqt5. At a certain dialog I need many components, being one of those a QWebEngineView as a canvas for plotting data, which should take most of the space available even if the chart is not ready when creating the dialog.
I expect it to look something like this:
I investigated and found about the stretch factor. I saw that QSizePolicy is directly applicable only to widgets and not to layouts, as shown in this SO answer. But then I saw that the methods addWidget and addLayout allow me to set the stretch factor in the direction of the QBoxLayout, and that seemed ideal for my intentions.
So I tried with:
from PyQt5.QtWidgets import QWidget, QLabel, QVBoxLayout, QHBoxLayout
from strategy_table import StrategyTable
layout = QVBoxLayout()
layout.addWidget(QLabel("Strategy components"))
# Upper layout for a table. Using a 30% of vertical space
upper_layout = QHBoxLayout() # I'm using a HBox because I also want a small button at the right side of the table
self.table = StrategyTable(self) # Own class derived from QTableWidget
upper_layout.addWidget(self.table)
add_button = QPushButton('Add')
add_button.clicked.connect(self._show_add_dialog)
upper_layout.addWidget(add_button)
layout.addLayout(upper_layout, 3) # Setting 20% of size with the stretch factor
# Then the plot area, using 60% of vertical space
layout.addWidget(QLabel("Plot area"))
canvas = QWebEngineView()
layout.addWidget(self.canvas, 6)
# Finally, a small are of 10% of vertical size to show numerical results
layout.addWidget(QLabel("Results"))
params_layout = self._create_results_labels() # A column with several QLabel-QTextArea pairs, returned as a QHBoxLayout
layout.addLayout(params_layout, 1)
self.setLayout(layout)
But it looked exactly the same as before:
It looked quite ok before adding the results Layout at the bottom, I guess because the upper table is empty at the beginning, and therefore took very little space and left the rest to the canvas.
Anyway, it seems that the stretch factor is being ignored, so I don't know if I am missing something here, or that I didn't fully understand the stretch factor.
BTW, I know I would use the QtEditor for designing the GUI, but I kind of prefer doing these things manually.
The problem is simple, the layouts handle the position and size of the widgets, but it has limits, among them the minimum size of the widgets, and in your case the last element has a height higher than 10%, so physically it is impossible. We can see that by removing the content or using a QScrollArea:
import sys
from PyQt5 import QtCore, QtGui, QtWidgets, QtWebEngineWidgets
class StrategyTable(QtWidgets.QTableWidget):
pass
class Widget(QtWidgets.QWidget):
def __init__(self, parent=None):
super(Widget, self).__init__(parent)
lay = QtWidgets.QVBoxLayout(self)
table = StrategyTable()
button = QtWidgets.QPushButton("Add")
hlay = QtWidgets.QHBoxLayout()
hlay.addWidget(table)
hlay.addWidget(button)
canvas = QtWebEngineWidgets.QWebEngineView()
canvas.setUrl(QtCore.QUrl("http://www.google.com/"))
scroll = QtWidgets.QScrollArea()
content_widget = QtWidgets.QWidget()
scroll.setWidgetResizable(True)
scroll.setWidget(content_widget)
vlay = QtWidgets.QVBoxLayout()
vlay.addWidget(QtWidgets.QLabel("Results:"))
params_layout = self._create_results_labels()
content_widget.setLayout(params_layout)
vlay.addWidget(scroll)
lay.addLayout(hlay, 3)
lay.addWidget(canvas, 6)
lay.addLayout(vlay, 1)
def _create_results_labels(self):
flay = QtWidgets.QFormLayout()
for text in ("Delta", "Gamma", "Rho", "Theta", "Vega"):
flay.addRow(text, QtWidgets.QTextEdit())
return flay
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
w = Widget()
w.show()
sys.exit(app.exec_())
Hope this will be useful to understand Layouts along with stretch factor
Sample Layout based on percentages (CPP)
QVBoxLayout *topMostVerticalLayout = new QVBoxLayout(this);
QHBoxLayout *upperHorznLayout = new QHBoxLayout();
QHBoxLayout *bottomHorznLayout = new QHBoxLayout();
QVBoxLayout *InnerVerticalLayout1 = new QVBoxLayout(this);
QVBoxLayout *InnerVerticalLayout2 = new QVBoxLayout(this);
QVBoxLayout *InnerVerticalLayout3 = new QVBoxLayout(this);
QVBoxLayout *InnerVerticalLayout4 = new QVBoxLayout(this);
QVBoxLayout *InnerVerticalLayout5 = new QVBoxLayout(this);
bottomHorznLayout->addLayout(InnerVerticalLayout1,15); //(15% stretch)
bottomHorznLayout->addLayout(InnerVerticalLayout2,15); //(15% stretch)
bottomHorznLayout->addLayout(InnerVerticalLayout3,15); //(15% stretch)
bottomHorznLayout->addLayout(InnerVerticalLayout4,15); //(15% stretch)
bottomHorznLayout->addLayout(InnerVerticalLayout5,40); //(40% stretch)
topMostVerticalLayout->addLayout(upperHorznLayout,3); //(30% stretch)
topMostVerticalLayout->addLayout(bottomHorznLayout,7); //(70% stretch)
this->setLayout(topMostVerticalLayout);
I'm trying to create a rich-text editor with a layout similar to Microsoft Word's 'Page View' or 'Print Layout.' I'd like to have a QTextEdit horizontally centered in the main window, with the scroll-bar aligned against the far right edge of the main window.
I couldn't find a way to move a QTextEdit's default scroll-bar independent of the QTextEdit itself. Instead, I tried creating a separate scroll-bar, and making the QTextEdit grow vertically using the solution found here: A QWidget like QTextEdit that wraps its height automatically to its contents?
Here is my attempt:
import sys
from PySide import QtGui, QtCore
class MainWindow(QtGui.QMainWindow):
def __init__(self):
super(MainWindow, self).__init__()
self.initUI()
def initUI(self):
cw = CentralWidget()
self.setCentralWidget(cw)
self.setGeometry(200, 200, 1000, 600)
self.show()
def resizeEvent(self, event):
self.centralWidget().setFixedHeight(event.size().height())
class CentralWidget(QtGui.QWidget):
def __init__(self):
super(CentralWidget, self).__init__()
self.initUI()
def initUI(self):
text = MainTextEdit()
text.setMinimumWidth(850)
text.setStyleSheet('border: 0;')
pageWidget = QtGui.QWidget()
scroll = QtGui.QScrollArea()
scroll.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
scroll.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
scroll.setMaximumWidth(18)
# If you change setWidgetResizeable to False,
# the textedit will center, but scrolling will not work.
scroll.setWidgetResizable(True)
scroll.setWidget(pageWidget)
hbox = QtGui.QHBoxLayout()
hbox.setContentsMargins(0,0,0,0)
hbox.addStretch(0.5)
hbox.addWidget(text)
hbox.addStretch(0.5)
pageWidget.setLayout(hbox)
hbox2 = QtGui.QHBoxLayout()
hbox2.setContentsMargins(0,0,0,0)
hbox2.addWidget(pageWidget)
hbox2.addWidget(scroll)
self.setLayout(hbox2)
class MainTextEdit(QtGui.QTextEdit):
def __init__(self, *args, **kwargs):
super(MainTextEdit, self).__init__(*args, **kwargs)
self.document().contentsChanged.connect(self.sizeChange)
self.setFontPointSize(80)
self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
def sizeChange(self):
docHeight = self.document().size().height()
self.setMinimumHeight(docHeight)
def main():
app = QtGui.QApplication(sys.argv)
mw = MainWindow()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
There are at least two problems with this:
Problem #1
As is, the code above does not horizontally center the QTextEdit in the main window, but the scroll bar at the far-right does work. If you change scroll.setWidgetResizable(True) to scroll.setWidgetResizable(False) on line 41, the QTextEdit will center horizontally, but the scroll-bar does not work. It seems you can get one feature or the other, but not both.
Problem #2
In order to keep the MainWindow from auto-expanding when the QTextEdit grows, the MainWindow assigns a fixed height to the CentralWidget whenever the MainWindow is resized (see line 19 of the code above). This works well until the user tries to vertically shrink the main window. The window can be vertically expanded by clicking and dragging the bottom border, but it can't be vertically shrunk.
Conclusion
Maybe this is the wrong approach all-together. Any suggestions?
Set a symmetrical margin via setViewportMargins on the QTextEdit which inherits from QAbstractScrollArea.
Example:
from PySide import QtGui, QtCore
app = QtGui.QApplication([])
window = QtGui.QWidget()
layout = QtGui.QVBoxLayout(window)
edit = QtGui.QTextEdit('jfdh afdhgfkjg fdnvfh vklkfjvkflj lddkl ljklfjkl jvkldjfkvljfgvjldf ll dl dljvklj ljljlbl llkb jbgl')
edit.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
edit.setViewportMargins(30, 0, 30, 30)
layout.addWidget(edit)
window.show()
app.exec_()
Gives:
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_())