Move pyqt button out of list of buttons - python

I have a list of pyqt4 push button and want to move the position. Since it is troublesome it make lots of buttons variable I create them through a list. The code below
import sys
from PyQt5.QtWidgets import QApplication, QPushButton, QWidget, QVBoxLayout
class Window(QWidget):
def __init__(self):
QWidget.__init__(self)
layout = QVBoxLayout(self)
self.buttons = []
for i in range(3):
self.buttons.append(QPushButton('',self))
self.buttons[-1].setFixedWidth(50)
self.buttons[-1].setFixedHeight(50)
self.buttons[-1].move(70*i+50,300)
layout.addWidget(self.buttons[-1])
if __name__ == '__main__':
import sys
app = QApplication(sys.argv)
window = Window()
window.resize(500,500)
window.show()
sys.exit(app.exec_())
don't work for specify the position
but
class Window(QWidget):
def __init__(self):
QWidget.__init__(self)
layout = QVBoxLayout(self)
self.button1 = QPushButton('',self)
self.button1.setFixedWidth(50)
self.button1.setFixedHeight(50)
self.button1.move(50,300)
self.button2 = QPushButton('',self)
self.button2.setFixedWidth(50)
self.button2.setFixedHeight(50)
self.button2.move(120,300)
self.button3 = QPushButton('',self)
self.button3.setFixedWidth(50)
self.button3.setFixedHeight(50)
self.button3.move(190,300)
layout = QVBoxLayout(self)
layout.addWidget(self.button1)
layout.addWidget(self.button2)
layout.addWidget(self.button3)
works fine.
What's the reason behind?

If you want to manually specify the geometry (position and size) of widgets, you should not add them to a layout.
Your second example "works" just because you already created and set a layout to the top-level widget (the Window class), and since a layout already exists the second one is not "installed". Running it from the console will shows this error:
StdErr: QLayout: Attempting to add QLayout "" to Window "", which already has a layout
When a widget is added to a layout, the ownership of the "layout item" (an abstract item used by layouts to manage its widgets) is transferred to the layout, and the widget is reparented to the widget that uses that layout.
Since the second layout cannot be set, it will not manage the geometries of the items you tried to add, and the result is that they will keep the geometries you set before.
The same result can be obtained if you remove all the last lines referencing the other layout, which is exactly what you need.
Also, note that in order to add a widget that is not managed by a layout, a parent is required. Your last example also works because you specified the window as the parent while instantiating them; if you don't do that the buttons will not be shown, but if you do show them (with show() or setVisible(True)) they will each appear in separate windows, as they will become their own top level windows.
If you don't have other widgets that should be managed by a layout, you can also avoid creating the first layout at all (but, still, the parent is required).
Finally, let me tell you that using manual geometries is generally discouraged, and there are very few and specific cases for which it's a good idea to go with.
The main reason behind this is that widgets tend to show very differently from device to device, and that depends on various aspects:
different OS and OS versions draw widgets differently (sometimes dramatically), and this involves varying the widget size and behavior; while this might not be a major issue for simple widgets, it can be a problem for things like item views, spinboxes, etc;
different OS and systems use different styles, which also includes things like internal widget content margins and minimum size;
specific system settings (most importantly, the default font) could make widgets mostly unreadable, specifically with text being clipped within the widget margins if the font size is too big;
depending on the OS, you might face issues if the system uses High DPI displays, and you could end up with very tiny widgets that are almost impossible to interact with;
fixed geometries force you (and the user) to have a fixed widget/window size, and (along with the DPI issue above) this can be a problem: the same widget could look too big or too small;

Related

QScrollArea, how do I make my central widget scrollable? [duplicate]

This question already has an answer here:
How to use QScrollArea to make scrollbars appear
(1 answer)
Closed 2 years ago.
I'm trying to turn a widget into a scrollable window. However, all I've accomplished is opening a QScrollarea that's completely independent from my central widget.
class Ui_MainWindow(object):
def mainscreen(self, MainWindow):
self.centralwidget = QtWidgets.QWidget(MainWindow)
self.scrollArea = QtWidgets.QScrollArea()
self.scrollArea.setWidget(self.centralwidget)
MainWindow.setCentralWidget(self.centralwidget)
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
MainWindow = QtWidgets.QMainWindow()
ui = Ui_MainWindow()
ui.mainscreen(MainWindow)
MainWindow.show()
app.exec_()
This just opens up my MainWindow on its own, only way I get an scrollarea open is if I do self.scrollarea.show . I cant figure out how QScrollArea works, how am I supposed to do it?
Simple explanation: the scroll area has no relation with the main window, and then it's not shown when the main window is.
Never, ever modify (or imitate) files generated by pyuic
You are making some confusion around the object structure, Qt parent/ownership and how a scroll area is set up.
This is also caused by the fact that you're clearly trying to mimic the structure and behavior of a file generated by pyuic, which should never be done. Those files are intended to be directly used as they are, without any modification, and imported in the main script of your program.
Their peculiar structure is intended only for that purpose, and the way they're built is not to be emulated, as creation of UI by code should only be done by subclassing a QWidget subclass (QMainWindow, QDialog, etc); trying to imitate what they do is not only unnecessary, but most times leads to confusion, bugs and unexpected behavior, exactly like in your case.
The only valid reason to open (and just read) a file generated by pyuic is to learn how widgets are created in the setupUi, keeping in mind that the setupUi argument (usually, MainWindow for QMainWindows, Form for QWidgets, Dialog for QDialogs, unless the name of the top level widget is changed in the object inspector of designer) is the main widget on which the ui is built upon.
Manually editing those files should only be done if one really knows what she/he is doing. There are few, very rare cases for which this is considered necessary, and it's usually to workaround very specific known bugs in the uic module, that normally rise on very specific conditions and situations.
What is happening?
Then, when a QWidget instance is created with an existing widget as argument, the result is that the new widget becomes a child of the existing one.
Widget can be then reparented in various ways: by setting a widget for a scroll area using setWidget(), or by setting the widget as a central widget to a QMainWindow.
What happens with your code is that you created a new QWidget that becomes a child of the QMainWindow with the following line:
self.centralwidget = QtWidgets.QWidget(MainWindow)
Then you create a scroll area (without any parent):
self.scrollArea = QtWidgets.QScrollArea()
Widgets that are not created with a parent are normally considered top level windows, unless they are added to a layout (or manually reparented using setParent(), but that's another story), which causes the parent (the widget on which the layout is set) to take ownership of the new child widget. Otherwise, if you try to show the "parent-less" widget, it will be shown as a top level widget, meaning it will have its own window, and as any top level widget, they can only be shown by manually calling show() or setVisible(True) (and that's your case).
Then you try to set the centralwidget of the scroll area, which will result in reparenting it:
self.scrollArea.setWidget(self.centralwidget)
Finally, you're setting that widget as the central widget, which will reparent it back again to the main window, with the result that the scroll area will not have a widget anymore (QObjects, from which QWidget inherits, can only have a single parent).
MainWindow.setCentralWidget(self.centralwidget)
The correct approach
The solution is to correctly create a QWidget (or QMainWindow in your case) subclass, set the scroll area as the central widget (if you want it as that) and create a new widget for its contents:
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self.scrollArea = QtWidgets.QScrollArea()
self.setCentralWidget(self.scrollArea)
self.scrollArea.setWidgetResizable(True)
self.contents = QtWidgets.QWidget()
self.scrollArea.setWidget(self.contents)
# create a layout for the scroll area contents; using the target widget
# as argument of the layout constructor automatically sets the layout on
# the specified widget
layout = QtWidgets.QVBoxLayout(self.contents)
# the same as:
# layout = QtWidgets.QVBoxLayout()
# self.contents.setLayout(layout)
for row in range(20):
button = QtWidgets.QPushButton('button {}'.format(row + 1))
layout.addWidget(button)
if __name__ == '__main__':
import sys
app = QtWidgets.QApplication(sys.argv)
mainWindow = MainWindow()
mainWindow.show()
sys.exit(app.exec_())

Stacked Layout causing alignment to top problem

I have a vertical layout with alignment set to "top" that contains what I want to be a fixed height "header" bar contained in a QHBoxLayout, with a QTextEdit window underneath it. One of the sections of this header bar contains a QStackedLayout. If you run the sample code below, when resizing the window the QTextEdit window begins pulling away from the "header" bar instead of staying anchored just underneath the "header" bar. The intended behavior would be for the QTextEdit window to remain anchored just underneath the header bar and expand and contract along its bottom margin. How do I fix the size of the QStackedLayout element to achieve this behavior?
from PySide2 import QtWidgets, QtCore, QtGui
from PySide2.QtWidgets import QApplication
import sys
class Demo(QtWidgets.QDialog):
def __init__(self, parent=None):
super(Demo, self).__init__(parent)
self.main_layout = QtWidgets.QVBoxLayout(self)
self.main_layout.setAlignment(QtCore.Qt.AlignTop)
# Header with combobox anchored to the top of the layout
self.data_results_layout = QtWidgets.QHBoxLayout()
self.results_stacked_layout = QtWidgets.QStackedLayout()
self.data_results_layout.addLayout(self.results_stacked_layout)
self.combobox_results_widget = QtWidgets.QWidget()
self.combobox_results_widget.setFixedHeight(25)
self.combobox_results_layout = QtWidgets.QVBoxLayout(self.combobox_results_widget)
self.combobox_results_layout.setContentsMargins(0,0,0,0)
self.directory_combobox = QtWidgets.QComboBox()
self.directory_combobox.setFixedHeight(25)
self.combobox_results_layout.addWidget(self.directory_combobox)
self.results_stacked_layout.addWidget(self.combobox_results_widget)
self.main_layout.addLayout(self.data_results_layout)
# QTextEdit Window
self.asset_data_layout = QtWidgets.QVBoxLayout()
self.asset_data_window = QtWidgets.QTextEdit('Ready.')
self.asset_data_layout.addWidget(self.asset_data_window)
self.main_layout.addLayout(self.asset_data_layout)
if __name__ == '__main__':
app = QApplication(sys.argv)
demo = Demo()
demo.show()
sys.exit(app.exec_())
Setting the alignment on a layout does not set the alignment of the widgets it manages, but only the alignment that that layout will have when added to another layout manager (you can read a more deeper explanation in this answer).
Basically, the following:
layout.setAlignment(QtCore.Qt.AlignTop)
is almost the same as this:
otherLayout.addLayout(layout, alignment=QtCore.Qt.AlignTop)
So, not only that alignment won't affect the children alignment, but since you're using that layout as main layout for the widget, that alignment is completely ignored.
In order to achieve what you want you can set a stretch for the lower alignment (as suggested in the comment by S.Nick):
self.main_layout.addLayout(self.asset_data_layout, stretch=1)
On the other hand, consider that QStackedLayout should only used for custom widgets or dedicated behavior, and in most cases it's better to use its convenience class QStackedWidget, on which you can set an appropriate size policy:
self.results_stacked_layout = QtWidgets.QStackedWidget()
self.data_results_layout.addWidget(self.results_stacked_layout)
self.results_stacked_layout.setSizePolicy(
QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Maximum)
With the above, the stacked widget will resize itself horizontally according to its contents and will only use the minimum vertical space required by it: since the combobox expands horizontally as much as possible, it will try to occupy all the horizontal available space, and since the vertical policy of the combobox is fixed, it will only use the vertical space required, leaving all the remaining vertical space to the other widgets in the parent layout.
Note that the size policy definitions might seem a bit counterintuitive; the rule is that it's always referred to the sizeHint of the widget: Maximum means that the size hint of the contents will be used as maximum size, and it cannot be larger than that.

PyQt5 resize tabs to its content

Consider this example. I want to make a program where the main window is divided into three parts which can be resized. In the middle I want to have two widgets placed vertially, the bottom one is QTabWidget, where users can change certain properties. Currently I have only one tab and one property there can be more.
I saw similar questions (here and here) but I can't seem to fathom how all the different parts related to size and layout even work together in the first place + they were C++ questions.
Please help me resize QTabWidget to its minimum necessary size to show the contents of the current tab.
As side note you can point me to some understandable docs for a beginner in GUI and PyQt5.
import sys
from PyQt5 import QtWidgets
from PyQt5.QtWidgets import QLineEdit, QLabel, QSplitter, QWidget, QListWidget, QApplication, QTabWidget, QGroupBox, \
QFormLayout, QSizePolicy, QLayout
from PyQt5.QtCore import Qt
class Example(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self.init_tabs()
self.main_splitter = QSplitter(Qt.Horizontal)
some_left_widget = QWidget()
some_right_widget = QWidget()
mid = QSplitter(Qt.Vertical)
mid.addWidget(QListWidget())
mid.addWidget(self.tabs)
self.main_splitter.addWidget(some_left_widget)
self.main_splitter.addWidget(mid)
self.main_splitter.addWidget(some_right_widget)
self.setCentralWidget(self.main_splitter)
self.showMaximized()
def init_tabs(self):
self.properties_dict = {}
self.properties_dict['Text'] = QLineEdit()
self.tabs = QTabWidget()
self.properties_groupbox = QGroupBox("Overview")
layout = QFormLayout()
for k, v in self.properties_dict.items():
layout.addRow(QLabel(k + ':'), v)
self.properties_groupbox.setLayout(layout)
self.tabs.addTab(self.properties_groupbox, 'Properties')
# I have no idea how these work
self.properties_groupbox.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum)
self.properties_groupbox.resize(self.properties_groupbox.minimumSizeHint())
self.properties_groupbox.adjustSize()
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = Example()
sys.exit(app.exec_())
Left one is now, right one is desired
A QSplitter uses complex computation to evaluate the sizes it assigns to each of its child widgets, especially when resizing (which is something that also happens as soon as it's first shown, like any other widget).
The most important aspects it takes into account are the widgets size hints (what the widget suggests it would be the preferable size) and size policy (how the widget can be resized and how it will behave if there's more or less available space).
To achieve what you want, you'll need to set the size policy stretch (which is the proportion of available space in the layout the widget will try to use).
Just add the following lines after adding the widgets to the splitter:
mid.setStretchFactor(0, 1)
mid.setStretchFactor(1, 0)
The first line indicates that the first widget (the list) will use a stretch factor of 1, while the second (the tab widget) will be 0. The stretch factor is computed based on the sum of all the stretch factors of the widgets.
In this way the list will try to uccupy the maximum available space (since 1 is the maximum of 1 + 0), while the tab the least.
Remember that stretch factor also consider the size hints of the widget, so if you set 2 to the list and 1 to the tab, you will not get a list with a height twice than that of the tab.
Also, as soon as the splitter is resized, the new proportions will be used when the splitter is resized, ignoring the previously set stretch factors.

QFileDialog opens in new window while adding it to QHBoxLayout

My issue is that when I am adding QFileDialog to QVBoxLayout it opens in new window. Below is the code which produces my problem.
from PyQt5.QtWidgets import QVBoxLayout, QFileDialog, QPushButton, QWidget
class MainWindow(QtWidgets.QWidget):
def __init__(self):
super(MainWindow, self).__init__()
self.setWindowTitle("My own MainWindow")
self.fileDialog = QFileDialog()
self.confirmAction = QPushButton("Press me", self)
mainLayout = QVBoxLayout()
mainLayout.addWidget(self.fileDialog)
mainLayout.addWidget(self.confirmAction)
self.setLayout(mainLayout)
According to the docs:
Window flags are a combination of a type (e.g. Qt::Dialog) and zero or
more hints to the window system (e.g. Qt::FramelessWindowHint).
If the widget had type Qt::Widget or Qt::SubWindow and becomes a
window (Qt::Window, Qt::Dialog, etc.), it is put at position (0, 0) on
the desktop. If the widget is a window and becomes a Qt::Widget or
Qt::SubWindow, it is put at position (0, 0) relative to its parent
widget.
So these flags are used to vary the behavior of the widget, for example to convert it into a window, a dialog, a tooltip, and so on.
In the docs gives the following list:
Qt::Widget: This is the default
type for QWidget. Widgets of this type are child widgets if they have
a parent, and independent windows if they have no parent. See also
Qt::Window and Qt::SubWindow.
Qt::Window: Indicates that the
widget is a window, usually with a window system frame and a title
bar, irrespective of whether the widget has a parent or not. Note that
it is not possible to unset this flag if the widget does not have a
parent.
Qt::Dialog :Window Indicates that the widget is a
window that should be decorated as a dialog (i.e., typically no
maximize or minimize buttons in the title bar). This is the default
type for QDialog. If you want to use it as a modal dialog, it should
be launched from another window, or have a parent and used with the
QWidget::windowModality property. If you make it modal, the dialog
will prevent other top-level windows in the application from getting
any input. We refer to a top-level window that has a parent as a
secondary window.
Qt::Sheet: Window Indicates that the
window is a Macintosh sheet. Since using a sheet implies window
modality, the recommended way is to use QWidget::setWindowModality(),
or QDialog::open(), instead.
Qt::Drawer: Window Indicates
that the widget is a Macintosh drawer.
Qt::Popup : Window Indicates that the widget is a pop-up top-level window, i.e.
that it is modal, but has a window system frame appropriate for pop-up
menus.
Qt::Tool: Window Indicates that the widget is a
tool window. A tool window is often a small window with a smaller than
usual title bar and decoration, typically used for collections of tool
buttons. If there is a parent, the tool window will always be kept on
top of it. If there isn't a parent, you may consider using
Qt::WindowStaysOnTopHint as well. If the window system supports it, a
tool window can be decorated with a somewhat lighter frame. It can
also be combined with Qt::FramelessWindowHint.
On Mac OS X, tool windows correspond to the Floating class of windows.
This means that the window lives on a level above normal windows; it
impossible to put a normal window on top of it. By default, tool
windows will disappear when the application is inactive. This can be
controlled by the Qt::WA_MacAlwaysShowToolWindow attribute.
Qt::ToolTip:Window Indicates that the widget is a
tooltip. This is used internally to implement tooltips.
Qt::SplashScreen: Window Indicates that the window is a
splash screen. This is the default type for QSplashScreen.
Qt::Desktop:Window Indicates that this widget is the
desktop. This is the type for QDesktopWidget.
Qt::SubWindow: Indicates that this widget is a sub-window,
such as a QMdiSubWindow widget.
In your case we must change the behavior of Qt::Dialog to Qt::Widget, in the following code I show the code that does it:
class MainWindow(QWidget):
def __init__(self):
super(MainWindow, self).__init__()
self.setWindowTitle("My own MainWindow")
self.fileDialog = QFileDialog(self)
self.fileDialog.setOption(QFileDialog.DontUseNativeDialog)
self.fileDialog.setWindowFlags(Qt.Widget)
self.confirmAction = QPushButton("Press me", self)
mainLayout = QVBoxLayout()
mainLayout.addWidget(self.fileDialog)
mainLayout.addWidget(self.confirmAction)
self.setLayout(mainLayout)
Screenshot:
I've been looking into this myself, dissatisfied with the "just use non-native dialogs" bit. I've been hacking around in the KDE platform file dialog implementation and have gotten (stuck) pretty close to what I'd want.
The only point of attach I've found is just before the dialog will actually be shown; before that there seems to be no way to know the actual parent widget. But there we can:
find the parent QWidget (from the parent QWindow)
from that, obtain the (first) (user-side) QFileDialog instance
if the parent QWidget has a layout, replace the found QFileDialog instance with our own dialog
save the original user-side QFileDialog instance
in the dtor, either restore the original QFD in the layout, or call deleteLater() on it (and set it to NULL in case the action causes recursive calling of the dtor).
Glitches:
- dialogs may end up with 2 sets of OK/Cancel/etc. buttons
- if not, these buttons may actually close only the embedded QFD and not the enclosing dialog (seen with the python example linked above)
- resizing works but the saveSize/restoreSize mechanism doesn't
- AFAICT all signals aren't connected properly (the preview in the Scribus open-file dialog doesn't react to selecting a file as it should). File opening does work though.
Full patch here on this BKO ticket:
https://bugs.kde.org/show_bug.cgi?id=404833#c15
Evidently this is only useful for hackers and software that can ship its own platform theme plugin (which is where the KDE platform file dialog comes from). Fortunately those plugins tend to be relatively small.

QBoxLayouts with Python

Because of their high customizability I've been relying on using multiple GroupBoxes while building app GUIs. But it appears QGroupBoxes make a certain impact on how fast an interface builds.
Now with layout's .insertLayout() method I can build an entire graphics interface placing the widgets any where I want. The dialogs feel very lightweight and extremely fast to re-draw. Unfortunately I can't find a way to control their appearance. I would appreciate if you would give me some clue on how to control the layout visual properties. I am particularly interested in knowing:
How to draw layout border, how to control a border line width,
How to place a layout title (similar to what QGroupBox's .setTitle() does)
How to control the layout outside and inside margins.
How to make layout minimizable/size-restorable (So the user could click some minus/arrow icon to fold/unfold layout when they need or don't need certain widgets belonging to the same layout.
The example with three nested layouts is posted below. As it is seen on dialog screenshot there is no way to visually differentiate one layout from another since there are no border, no titles, no dividers and etc.
from PyQt4 import QtGui, QtCore
class Dialog_01(QtGui.QMainWindow):
def __init__(self):
super(QtGui.QMainWindow,self).__init__()
tabWidget = QtGui.QTabWidget()
tabGroupBox = QtGui.QGroupBox()
tabLayout = QtGui.QVBoxLayout()
tabLayout.setContentsMargins(0, 0, 0, 0)
tabLayout.setSpacing(0)
subLayoutA=QtGui.QVBoxLayout()
tabLayout.insertLayout(0, subLayoutA)
tabGroupBox.setLayout(tabLayout)
tabWidget.addTab(tabGroupBox,' Tab A ')
listWidgetA = QtGui.QListWidget()
for i in range(3):
QtGui.QListWidgetItem( 'Item '+str(i), listWidgetA )
subLayoutA.addWidget(listWidgetA)
subLayoutB=QtGui.QHBoxLayout()
tabLayout.insertLayout(1, subLayoutB)
subLayoutB.addWidget(QtGui.QLineEdit('LineEdit 1'))
subLayoutB.addWidget(QtGui.QLineEdit('LineEdit 2'))
subLayoutC=QtGui.QVBoxLayout()
tabLayout.insertLayout(2, subLayoutC)
subLayoutC.addWidget(QtGui.QPushButton('PushButton 1'))
subLayoutC.addWidget(QtGui.QPushButton('PushButton 2'))
self.setCentralWidget(tabWidget)
if __name__ == '__main__':
app = QtGui.QApplication(sys.argv)
dialog_1 = Dialog_01()
dialog_1.show()
dialog_1.resize(480,320)
sys.exit(app.exec_())
EDITED LATER
I inserted two lines into an example code to implement one of the suggestions made by sebastian. A Spacing-Margins method combos can be effectively used to get some additional tweaks done. Here is a screenshot (still could not get rid of the spacing around pushButtons):
QLayout sublcasses don't have a visual representation, which becomes clear by the fact that QLayout classes do not inherit QWidget. They only calculate the positions of the widgets they are responsible for in the context of their "parent" widget.
So the answer to questions 1,2 and 4 is basically: You can't.
You'll always have to have a QWidget in combination with a QLayout.
E.g. to group your two buttons into a frame with a box use a QFrame:
subLayoutC=QtGui.QVBoxLayout()
buttonFrame = QtGui.QFrame()
buttonFrame.setFrameStyle(QtGui.QFrame.Plain |QtGui.QFrame.Box)
buttonFrame.setLayout(subLayoutC)
subLayoutC.addWidget(QtGui.QPushButton('PushButton 1'))
subLayoutC.addWidget(QtGui.QPushButton('PushButton 2'))
# now we add the QFrame widget - not subLayoutC to the tabLayout
tabLayout.addWidget(buttonFrame) # I think your suggested edit was correct here
self.setCentralWidget(tabWidget)
Concerning question 3, check the docs:
http://qt-project.org/doc/qt-4.8/qlayout.html#setContentsMargins
http://qt-project.org/doc/qt-4.8/qboxlayout.html#setSpacing

Categories