I have a question about using QtGui.QGraphicsWidget. In a given example I will try to describe my problem. There are two QGraphicsWidget instances inside the QtGui.QtGraphicsScene, of which one is a parent (app_widget.main_widget - blue one when running), and other one is its child widget (app_widget.subwidget - red one when running). If you run the program, with a Plus key on your keyboard you can transform widgets circularly through the states.
# --coding: utf-8 --
# window.py
import sys
from PySide import QtGui, QtCore
class WidgetBase(QtGui.QGraphicsWidget):
def __init__(self, parent = None):
super(WidgetBase, self).__init__(parent)
def set_background(self, color_hex):
palette = QtGui.QPalette()
color = QtGui.QColor()
color.setRgba(color_hex)
palette.setColor(QtGui.QPalette.Background, color)
self.setPalette(palette)
self.setAutoFillBackground(True)
class AppWidget(WidgetBase):
def __init__(self):
super(AppWidget, self).__init__()
self.main_widget = WidgetBase(self)
self.subwidget = WidgetBase(self.main_widget)
class Window(QtGui.QMainWindow):
change_app_state = QtCore.Signal()
def __init__(self, parent=None):
QtGui.QMainWindow.__init__(self, parent)
SCREEN_DIM = QtGui.QDesktopWidget().screenGeometry()
self.app_scene = QtGui.QGraphicsScene(0, 0, SCREEN_DIM.width(), SCREEN_DIM.height())
app_view = QtGui.QGraphicsView(self.app_scene)
app_view.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
app_view.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
self.setCentralWidget(app_view)
app_widget = AppWidget()
app_widget.main_widget.set_background(0x50449558)
app_widget.main_widget.resize(500, 500)
app_widget.subwidget.set_background(0x50ff3300)
app_widget.subwidget.resize(500 * 0.5, 500)
self.app_scene.addItem(app_widget)
self.machine = QtCore.QStateMachine()
state1 = QtCore.QState(self.machine)
state2 = QtCore.QState(self.machine)
state3 = QtCore.QState(self.machine)
state4 = QtCore.QState(self.machine)
state1.assignProperty(app_widget.main_widget, 'geometry', QtCore.QRectF(0, 0, 500, 500))
state2.assignProperty(app_widget.main_widget, 'scale', 0.5)
state3.assignProperty(app_widget.main_widget, 'scale', 1)
state4.assignProperty(app_widget.main_widget, 'geometry', QtCore.QRectF(0, 0, 1000, 500))
trans1 = state1.addTransition(self.change_app_state, state2)
trans2 = state2.addTransition(self.change_app_state, state3)
trans3 = state3.addTransition(self.change_app_state, state4)
trans4 = state4.addTransition(self.change_app_state, state1)
trans1.addAnimation(QtCore.QPropertyAnimation(app_widget.main_widget, 'scale', state1))
trans2.addAnimation(QtCore.QPropertyAnimation(app_widget.main_widget, 'scale', state2))
trans3.addAnimation(QtCore.QPropertyAnimation(app_widget.main_widget, 'geometry', state3))
trans4.addAnimation(QtCore.QPropertyAnimation(app_widget.main_widget, 'geometry', state4))
self.machine.setInitialState(state1)
self.machine.start()
def keyPressEvent(self, e):
if e.key() == QtCore.Qt.Key_Plus:
self.change_app_state.emit()
if __name__ == '__main__':
app = QtGui.QApplication(sys.argv)
window = Window()
window.showFullScreen()
sys.exit(app.exec_())
When scaling parent widget (changing 'scale' property - QtGui.QGraphicsWidget property inherited from QtGui.QGraphicsObject), a child widget also get scaled. But, when I change geometry of parent widget (changing 'geometry' property - QtGui.QGraphicsWidget property), child widget geometry remains unchanged.
I am running Python 2.7.6, PySide version 1.2.1 and QtCore version 4.8.6.
Why isn't a child widget always following parents transformations? Is there any way to scale only one axis of parent widget and get all children widgets to get scaled proportionally?
Answered on qt forum.
"It will only follow it's parent widget if you put it in a layout that you set on the said parent widget. Otherwise it's up to you to handle the positioning and size of the child widget."
Link on qt forum post:
https://forum.qt.io/topic/59958/pyside-qtgui-qgraphicswidget-child-parent-transformations
Related
i want to display two QTreeViews inside one window and i cant figure out why my testing code doesn't show the red widget (future 2nd TreeView). Any ideas why it doesn't appear?
I am new to PyQt5 and ive followed a tutorial on youtube and a written here. Before i started this question ive searched on stackoverflow, but i didn't find a topic which had this issue.
StandardItem is a subclass of QStandardItem and Color is a subclass of QWidget. Im not defining any layouts inside both classes (just setting default settings for QStandardItems and adding color to see my layout).
class MainWindow(QMainWindow):
def __init__(self):
super(MainWindow, self).__init__()
self.setWindowTitle("JSON View")
self.resize(700, 700)
treeView = QTreeView(self)
treeView.setHeaderHidden(True) # To hide first column
treeModel = QStandardItemModel()
rootNode = treeModel.invisibleRootItem()
# Data
america = StandardItem('America', 16, set_bold=True)
california = StandardItem('California', 14)
america.appendRow(california)
oakland = StandardItem('Oakland', 12)
california.appendRow(oakland)
rootNode.appendRow(america)
treeView.setModel(treeModel)
treeView.expandAll()
treeView.doubleClicked.connect(self.getValue)
# Layout
layout = QHBoxLayout()
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(20)
layout.addWidget(Color('red'))
layout.addWidget(Color('yellow'))
layout.addWidget(treeView)
treeView.setVisible(True)
widget = QWidget()
widget.setLayout(layout)
self.setCentralWidget(widget)
So after reading more documentation (QTreeView, QGroupBox, QVBoxLayout) on this topic ive found that i had to use an additional layout inside where i can combine two more layouts for each tree view. Afterwards i would use a dummy-widget as central widget. I posted the code below if anyone has the same problem:
class MainWindow(QMainWindow):
def __init__(self):
super(MainWindow, self).__init__()
self.setWindowTitle("JSON View")
# Retrieve geometry of window
windowGeometry = self.frameGeometry()
# Get center of the screen
centerScreen = QDesktopWidget().availableGeometry().center()
# Move window to the center
windowGeometry.moveCenter(centerScreen)
self.move(windowGeometry.topLeft())
treeViewLeft = QTreeView(self)
treeViewLeft.setHeaderHidden(True) # To hide first column
treeViewRight = QTreeView(self)
treeViewRight.setHeaderHidden(True)
groupBoxLeft = QGroupBox('1. Json tree view')
groupBoxRight = QGroupBox('2. Json tree view')
# TODO: Add Column headers for each tree view
treeModelLeft = QStandardItemModel(0, 7)
rootNodeLeft = treeModelLeft.invisibleRootItem() # Provides access to the model’s top-level items through the QStandardItem API
treeModelRight = QStandardItemModel()
rootNodeRight = treeModelRight.invisibleRootItem() # Provides access to the model’s top-level items through the QStandardItem API
# Data (left)
america = StandardItem('America', 16, set_bold=True)
california = StandardItem('California', 14)
america.appendRow(california)
oakland = StandardItem('Oakland', 12)
california.appendRow(oakland)
rootNodeLeft.appendRow(america)
treeViewLeft.setModel(treeModelLeft)
treeViewLeft.expandAll()
treeViewLeft.doubleClicked.connect(self.getValue)
treeViewLeft.setVisible(True)
# Data (right)
america = StandardItem('America', 16, set_bold=True)
california = StandardItem('California', 14)
america.appendRow(california)
oakland = StandardItem('Oakland', 12)
california.appendRow(oakland)
rootNodeRight.appendRow(america)
treeViewRight.setModel(treeModelRight)
treeViewRight.expandAll()
treeViewRight.doubleClicked.connect(self.getValue)
treeViewRight.setVisible(True)
# Layout
hbox = QVBoxLayout()
hbox.addWidget(treeViewLeft)
groupBoxLeft.setLayout(hbox)
hbox2 = QVBoxLayout()
hbox2.addWidget(treeViewRight)
groupBoxRight.setLayout(hbox2)
mainLayout = QHBoxLayout()
mainLayout.addWidget(groupBoxLeft)
mainLayout.addWidget(groupBoxRight)
widget = QWidget() # Dummy
widget.setLayout(mainLayout)
self.setCentralWidget(widget) # Must-have
def getValue(self, val):
print(val.data())
print(val.row())
print(val.column())
The following code-examples helped aswell:
pythonguis and pythonspot
Cheers!
I'm creating a GUI using PyQt, to display 4 images in a window, positioned in this way:
Top left
Top right
Bottom left
Bottom right
I'd like to be able to undock them, but also to redock them back to any of the 4 available space.
My final goal is to set it up so that moving an undocked image where another one is already placed, would move that second image out of the way (docking it in another free quadrant or undocking it), or that placing it in the center would make it occupy all the 4 quadrants (undocking all the others).
I've tried achieving this with QDockWidget, but so far I'm not achieving good results.
My current code:
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("Window")
grid_layout = QGridLayout()
dock_window_1 = QMainWindow()
docked = QDockWidget("Dockable", self)
dock_window_1.addDockWidget(Qt.DockWidgetArea.TopDockWidgetArea, docked)
dockedWidget = QWidget(self)
docked.setWidget(dockedWidget)
dockedWidget.setLayout(QVBoxLayout())
dockedWidget.layout().addWidget(QPushButton("1"))
dock_window_2 = QMainWindow()
docked_2 = QDockWidget("Dockable_2", self)
dock_window_2.addDockWidget(Qt.DockWidgetArea.RightDockWidgetArea, docked_2)
dockedWidget_2 = QWidget(self)
docked.setWidget(dockedWidget_2)
dockedWidget_2.setLayout(QVBoxLayout())
dockedWidget_2.layout().addWidget(QPushButton("2"))
dock_window_3 = QMainWindow()
docked_3 = QDockWidget("Dockable_3", self)
dock_window_3.addDockWidget(Qt.DockWidgetArea.LeftDockWidgetArea, docked_3)
dockedWidget_3 = QWidget(self)
docked.setWidget(dockedWidget_3)
dockedWidget_3.setLayout(QVBoxLayout())
dockedWidget_3.layout().addWidget(QPushButton("3"))
dock_window_4 = QMainWindow()
docked_4 = QDockWidget("Dockable_4", self)
dock_window_4.addDockWidget(Qt.DockWidgetArea.BottomDockWidgetArea, docked_4)
dockedWidget_4 = QWidget(self)
docked.setWidget(dockedWidget_4)
dockedWidget_4.setLayout(QVBoxLayout())
dockedWidget_4.layout().addWidget(QPushButton("4"))
grid_layout.addWidget(dock_window_1, 0, 0)
grid_layout.addWidget(dock_window_2, 1, 0)
grid_layout.addWidget(dock_window_3, 0, 1)
grid_layout.addWidget(dock_window_4, 1, 1)
widget = QWidget()
widget.setLayout(grid_layout)
self.setCentralWidget(widget)
This kind of works, but I'm only able to redock a widget in its original place.
Can anyone help me getting on the right road here?
Thanks in advance!
Most of the behavior you are describing is actually already implemented by Qt using the QMainWindow and the QDockWidget.
The issue with your code is you are creating a unique QMainWindow for each of the QDockWidgets when the program only needs one QMainWindow. Additionally you are adding each of the QDockWidgets to the layout after you have already added them using the standard dockAreas surrounding the central widget.
If you eliminate the unneeded QMainWindows and instead assign each of the QDockWidgets to the your MainWindow, your desired functionality should work automatically.
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("Window")
grid_layout = QGridLayout()
docked = QDockWidget("Dockable", self)
docked.setAllowedAreas(Qt.LeftDockWidgetArea | Qt.RightDockWidgetArea)
dockedWidget = QWidget(self)
docked.setWidget(dockedWidget)
dockedWidget.setLayout(QVBoxLayout())
dockedWidget.layout().addWidget(QPushButton("1"))
docked_2 = QDockWidget("Dockable_2", self)
docked_2.setAllowedAreas(Qt.LeftDockWidgetArea | Qt.RightDockWidgetArea)
dockedWidget_2 = QWidget(self)
docked_2.setWidget(dockedWidget_2)
dockedWidget_2.setLayout(QVBoxLayout())
dockedWidget_2.layout().addWidget(QPushButton("2"))
docked_3 = QDockWidget("Dockable_3", self)
docked_3.setAllowedAreas(Qt.LeftDockWidgetArea | Qt.RightDockWidgetArea)
dockedWidget_3 = QWidget(self)
docked_3.setWidget(dockedWidget_3)
dockedWidget_3.setLayout(QVBoxLayout())
dockedWidget_3.layout().addWidget(QPushButton("3"))
docked_4 = QDockWidget("Dockable_4", self)
docked_4.setAllowedAreas(Qt.LeftDockWidgetArea | Qt.RightDockWidgetArea)
dockedWidget_4 = QWidget(self)
docked_4.setWidget(dockedWidget_4)
dockedWidget_4.setLayout(QVBoxLayout())
dockedWidget_4.layout().addWidget(QPushButton("4"))
self.addDockWidget(Qt.DockWidgetArea.LeftDockWidgetArea, docked)
self.addDockWidget(Qt.DockWidgetArea.RightDockWidgetArea, docked_2)
self.addDockWidget(Qt.DockWidgetArea.LeftDockWidgetArea, docked_3)
self.addDockWidget(Qt.DockWidgetArea.RightDockWidgetArea, docked_4)
widget = QWidget()
widget.setLayout(grid_layout)
self.setCentralWidget(widget)
I'm interested in how to save a selected value from my combobox as variable, so when I press e.g. B then I want it to be saved as SelectedValueCBox = selected value, which would be B in this case.
Thank you for your help
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
import sys
class App(QMainWindow):
def __init__(self):
super().__init__()
self.title = "PyQt5 - StockWindow"
self.left = 0
self.top = 0
self.width = 200
self.height = 300
self.setWindowTitle(self.title)
self.setGeometry(self.left, self.top, self.width, self.height)
self.tab_widget = MyTabWidget(self)
self.setCentralWidget(self.tab_widget)
self.show()
class MyTabWidget(QWidget):
def __init__(self, parent):
super(QWidget, self).__init__(parent)
self.layout = QVBoxLayout(self)
#self.layout = QGridLayout(self)
self.tabs = QTabWidget()
self.tab1 = QWidget()
self.tabs.resize(300, 200)
self.tabs.addTab(self.tab1, "Stock-Picker")
self.tab1.layout = QGridLayout(self)
button = QToolButton()
self.tab1.layout.addWidget(button, 1,1,1,1)
d = {'AEX':['A','B','C'], 'ATX':['D','E','F'], 'BEL20':['G','H','I'], 'BIST100':['J','K','L']}
def callback_factory(k, v):
return lambda: button.setText('{0}_{1}'.format(k, v))
menu = QMenu()
self.tab1.layout.addWidget(menu, 1,1,1,1)
for k, vals in d.items():
sub_menu = menu.addMenu(k)
for v in vals:
action = sub_menu.addAction(str(v))
action.triggered.connect(callback_factory(k, v))
button.setMenu(menu)
self.tab1.setLayout(self.tab1.layout)
self.layout.addWidget(self.tabs)
self.setLayout(self.layout)
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = App()
sys.exit(app.exec_())
Since you're already returning a lambda for the connection, the solution is to use a function instead.
class MyTabWidget(QWidget):
def __init__(self, parent):
# ...
def callback_factory(k, v):
def func():
self.selectedValueCBox = v
button.setText('{0}_{1}'.format(k, v))
return func
# ...
self.selectedValueCBox = None
Note that your code also has many issues.
First of all, you should not add the menu to the layout: not only it doesn't make any sense (the menu should pop up, while adding it to a layout makes it "embed" into the widget, and that's not good), but it also creates graphical issues especially because you added the menu to the same grid "slot" (1, 1, 1, 1) which is already occupied by the button.
Creating a layout with a widget as argument automatically sets the layout to that widget. While in your case it doesn't create a big issue (since you've already set the layout) you should not create self.tab1.layout with self. Also, since you've already set the QVBoxLayout (due to the parent argument), there's no need to call setLayout() again.
A widget container makes sense if you're actually going to add more than one widget. You're only adding a QTabWidget to its layout, so it's almost useless, and you should just subclass from QTabWidget instead.
Calling resize on a widget that is going to be added on a layout is useless, as the layout will take care of the resizing and the previous resize call will be completely ignored. resize() makes only sense for top level widgets (windows) or the rare case of widgets not managed by a layout.
self.layout() is an existing property of all QWidgets, you should not overwrite it. The same with self.width() and self.height() you used in the App class.
App should refer to an application class, but you're using it for a QMainWindow. They are radically different types of classes.
Finally, you have no combobox in your code. A combobox is widget that is completely different from a drop down menu like the one you're using. I suggest you to be more careful with the terminology in the future, otherwise your question would result very confusing, preventing people to actually being able to help you.
I want to up my game in UI design using PyQt5. I feel like the resources for UI customization in PyQt5 are not easy to find. It is possible to try and make personalized widget, but the overall method seems non-standardized.
I need to build an arrow widget that is hoverable, overlappable with other widgets and highly customized. As I read in this tutorial and some other posts, it possible to do exactly what you need using paintEvent. Thus that is what I tried, but overall, I feel like the method is quite messy, and I'd like some guidelines on building complex Customized, general widget. Here's what I have:
Customized Shape: I built my code based on this
Hoverable property: I read everywhere that modifying the projects styleSheet is usually the way to go, especially if you want to make your Widget general and adapt to colors, the problem is that I wasn't able to find how to use properly self.palette to fetch the current colors of the QApplication styleSheet. I feel like i's have to maybe use enterEvent and leaveEvent, but I tried to redraw the whole widget with a painter in those functions and it said
QPainter::begin: Painter already active
QWidget::paintEngine: Should no longer be called
QPainter::begin: Paint device returned engine == 0, type: 1
QPainter::setRenderHint: Painter must be active to set rendering hints
Overlappable Property: I found a previous post which seemed to have found a solution: create a second widget that is children of the main widget, in order to be able to move the children around. I tried that but it seems that it doesn't want to move, no matter the position I give the widget.
Here is my code:
import sys
from PyQt5.QtWidgets import QWidget, QHBoxLayout, QGraphicsDropShadowEffect, QApplication, QFrame, QPushButton
from PyQt5.QtCore import Qt, QPoint, QLine
from PyQt5.QtGui import QPainter, QPen, QColor, QPalette
class MainWidget(QWidget):
def __init__(self):
super(MainWidget, self).__init__()
self.resize(500, 500)
self.layout = QHBoxLayout()
self.setLayout(self.layout)
self.myPush = QPushButton()
self.layout.addWidget(self.myPush)
self.arrow = ArrowWidget(self)
position = QPoint(-40, 0)
self.layout.addWidget(self.arrow)
self.arrow.move(position)
class ArrowWidget(QWidget):
def __init__(self, parent=None):
super(ArrowWidget, self).__init__(parent)
self.setWindowFlag(Qt.FramelessWindowHint)
self.setAttribute(Qt.WA_TranslucentBackground)
self.w = 200
self.h = 200
self.blurRadius = 20
self.xO = 0
self.yO = 20
self.resize(self.w, self.h)
self.layout = QHBoxLayout()
# myFrame = QFrame()
# self.layout.addWidget(myFrame)
self.setLayout(self.layout)
self.setStyleSheet("QWidget:hover{border-color: rgb(255,0,0);background-color: rgb(255,50,0);}")
shadow = QGraphicsDropShadowEffect(blurRadius=self.blurRadius, xOffset=self.xO, yOffset=self.yO)
self.setGraphicsEffect(shadow)
def paintEvent(self, event):
painter = QPainter(self)
painter.setRenderHint(QPainter.Antialiasing)
painter.begin(self)
# painter.setBrush(self.palette().window())
# painter.setPen(QPen(QPalette, 5))
ok = self.frameGeometry().width()/2-self.blurRadius/2-self.xO/2
oky = self.frameGeometry().height()/2-self.blurRadius/2-self.yO/2
painter.drawEllipse(QPoint(self.frameGeometry().width()/2-self.blurRadius/2-self.xO/2, self.frameGeometry().height()/2-self.blurRadius/2-self.yO/2), self.w/2-self.blurRadius/2-self.yO/2-self.xO/2, self.h/2-self.blurRadius/2-self.yO/2-self.xO/2)
painter.drawLines(QLine(ok-25, oky-50, ok+25, oky), QLine(ok+25, oky, ok-25, oky+50))
painter.end()
if __name__ == '__main__':
app = QApplication(sys.argv)
testWidget = MainWidget()
testWidget.show()
sys.exit(app.exec_())
If someone could help me make this work and explain along the way to help us better understand the structure of customized widgets and explain a better method that isn't messy like this one, I believe it would be a plus to the beginners like me using PyQt5 as a main Framework for UI making.
There is no "standard" method for custom widgets, but usually paintEvent overriding is required.
There are different issues in your example, I'll try and address to them.
Overlapping
If you want a widget to be "overlappable", it must not be added to a layout. Adding a widget to a layout will mean that it will have its "slot" within the layout, which in turn will try to compute its sizes (based on the widgets it contains); also, normally a layout has only one widget per "layout slot", making it almost impossible to make widget overlap; the QGridLayout is a special case which allows (by code only, not using Designer) to add more widget to the same slot(s), or make some overlap others. Finally, once a widget is part of a layout, it cannot be freely moved nor resized (unless you set a fixedSize).
The only real solution to this is to create the widget with a parent. This will make it possible to use move() and resize(), but only within the boundaries of the parent.
Hovering
While it's true that most widgets can use the :hover selector in the stylesheet, it only works for standard widgets, which do most of their painting by themself (through QStyle functions). About this, while it's possible to do some custom painting with stylesheets, it's generally used for very specific cases, and even in this case there is no easy way to access to the stylesheet properties.
In your case, there's no need to use stylesheets, but just override enterEvent and leaveEvent, set there any color you need for painting and then call self.update() at the end.
Painting
The reason you're getting those warnings is because you are calling begin after declaring the QPainter with the paint device as an argument: once it's created it automatically calls begin with the device argument. Also, it usually is not required to call end(), as it is automatically called when the QPainter is destroyed, which happens when the paintEvent returns since it's a local variable.
Example
I created a small example based on your question. It creates a window with a button and a label within a QGridLayout, and also uses a QFrame set under them (since it's been added first), showing the "overlapping" layout I wrote about before. Then there's your arrow widget, created with the main window as parent, and that can be moved around by clicking on it and dragging it.
import sys
from PyQt5 import QtCore, QtGui, QtWidgets
class ArrowWidget(QtWidgets.QWidget):
def __init__(self, parent=None):
super().__init__(parent)
# since the widget will not be added to a layout, ensure
# that it has a fixed size (otherwise it'll use QWidget default size)
self.setFixedSize(200, 200)
self.blurRadius = 20
self.xO = 0
self.yO = 20
shadow = QtWidgets.QGraphicsDropShadowEffect(blurRadius=self.blurRadius, xOffset=self.xO, yOffset=self.yO)
self.setGraphicsEffect(shadow)
# create pen and brush colors for painting
self.currentPen = self.normalPen = QtGui.QPen(QtCore.Qt.black)
self.hoverPen = QtGui.QPen(QtCore.Qt.darkGray)
self.currentBrush = self.normalBrush = QtGui.QColor(QtCore.Qt.transparent)
self.hoverBrush = QtGui.QColor(128, 192, 192, 128)
def mousePressEvent(self, event):
if event.buttons() == QtCore.Qt.LeftButton:
self.mousePos = event.pos()
def mouseMoveEvent(self, event):
# move the widget based on its position and "delta" of the coordinates
# where it was clicked. Be careful to use button*s* and not button
# within mouseMoveEvent
if event.buttons() == QtCore.Qt.LeftButton:
self.move(self.pos() + event.pos() - self.mousePos)
def enterEvent(self, event):
self.currentPen = self.hoverPen
self.currentBrush = self.hoverBrush
self.update()
def leaveEvent(self, event):
self.currentPen = self.normalPen
self.currentBrush = self.normalBrush
self.update()
def paintEvent(self, event):
qp = QtGui.QPainter(self)
qp.setRenderHints(qp.Antialiasing)
# painting is not based on "pixels", to get accurate results
# translation of .5 is required, expecially when using 1 pixel lines
qp.translate(.5, .5)
# painting rectangle is always 1px smaller than the actual size
rect = self.rect().adjusted(0, 0, -1, -1)
qp.setPen(self.currentPen)
qp.setBrush(self.currentBrush)
# draw an ellipse smaller than the widget
qp.drawEllipse(rect.adjusted(25, 25, -25, -25))
# draw arrow lines based on the center; since a QRect center is a QPoint
# we can add or subtract another QPoint to get the new positions for
# top-left, right and bottom left corners
qp.drawLine(rect.center() + QtCore.QPoint(-25, -50), rect.center() + QtCore.QPoint(25, 0))
qp.drawLine(rect.center() + QtCore.QPoint(25, 0), rect.center() + QtCore.QPoint(-25, 50))
class MainWidget(QtWidgets.QWidget):
def __init__(self):
super().__init__()
layout = QtWidgets.QGridLayout()
self.setLayout(layout)
self.button = QtWidgets.QPushButton('button')
layout.addWidget(self.button, 0, 0)
self.label = QtWidgets.QLabel('label')
self.label.setAlignment(QtCore.Qt.AlignCenter)
layout.addWidget(self.label, 0, 1)
# create a frame that uses as much space as possible
self.frame = QtWidgets.QFrame()
self.frame.setFrameShape(self.frame.StyledPanel|self.frame.Raised)
self.frame.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding)
# add it to the layout, ensuring it spans all rows and columns
layout.addWidget(self.frame, 0, 0, layout.rowCount(), layout.columnCount())
# "lower" the frame to the bottom of the widget's stack, otherwise
# it will be "over" the other widgets, preventing them to receive
# mouse events
self.frame.lower()
self.resize(640, 480)
# finally, create your widget with a parent, *without* adding to a layout
self.arrowWidget = ArrowWidget(self)
# now you can place it wherever you want
self.arrowWidget.move(220, 140)
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
testWidget = MainWidget()
testWidget.show()
sys.exit(app.exec_())
I am using QHBoxLayout for layout. The breakdown is: pic.1 is what happens, pic.2 is what I want - widgets don't overlap and there is a gap between them. For pic.2, I created a Gap widget to stick between the existing widgets. But this is a cumbersome solution which implies extra maintenance (especially when I have more than two widgets to care for). Moreover, since B overlaps A, I think the newly added gap widget overlaps A as well, maybe according the fraction of its size. I am not so sure about this.
I tried using self.layout.addSpacing(10), but that doesn't work. The red widget shifts by 10 pixels from where it was before, not from the border of the widget on the left.
Note, too, that the contained widgets will all have some minimum width specification.
So, how can I add the space between two widgets like in pic.2?
import sys
from PyQt5.QtWidgets import *
from PyQt5.QtGui import *
from PyQt5.QtCore import *
class Gap(QWidget):
def __init__(self, parent=None):
super().__init__(parent=parent)
self.setMinimumWidth(10)
self.setMinimumHeight(1)
class Line(QWidget):
def __init__(self, parent=None):
super().__init__(parent=parent)
# Set layout
self.layout = QHBoxLayout()
self.setLayout(self.layout)
# Set palette
self.palette1 = QPalette()
self.palette1.setColor(QPalette.Window, Qt.red)
self.palette2 = QPalette()
self.palette2.setColor(QPalette.Window, Qt.blue)
self.palettebg = QPalette()
self.palettebg.setColor(QPalette.Window, Qt.green)
self.setAutoFillBackground(True)
self.setPalette(self.palettebg)
# Set labels
self.label1 = QLabel(self)
self.label1.setText("A")
self.label1.setStyleSheet('font-size: 36pt;')
self.label1.adjustSize()
self.label1.setAutoFillBackground(True)
self.label1.setPalette(self.palette1)
self.label1.setMinimumSize(36, 36)
self.label2 = QLabel(self)
self.label2.setText("B")
self.label2.move(30, 0)
self.label2.setStyleSheet('font-size: 36pt;')
self.label2.adjustSize()
self.label2.setAutoFillBackground(True)
self.label2.setPalette(self.palette2)
self.label2.setMinimumSize(36, 36)
self.gap = Gap()
self.layout.addWidget(self.label1)
# self.layout.addWidget(self.gap)
# self.layout.addSpacing(10)
self.layout.addWidget(self.label2)
class App(QMainWindow):
def __init__(self):
super().__init__()
self.title = 'PyQt5'
self.left = 10
self.top = 10
self.width = 200
self.height = 54
self.initUI()
def initUI(self):
self.setWindowTitle(self.title)
self.setGeometry(self.left, self.top, self.width, self.height)
self.line = Line(self)
self.line.resize(74, 54)
self.line.move(50, 50)
self.show()
if __name__ == '__main__':
app = QApplication(sys.argv)
# CUSTOM
app.setFont(QFontDatabase().font("Monospace", "Regular", 14))
ex = App()
sys.exit(app.exec_())
EDIT clarification, as requested: (1) suppose the size of the parent widget is too small (and cannot be resized), (2) the parent widget has QHBoxLayout with widgets A and B added to it, (3) the parent widget being too small, QHBoxLayout aranges the child widgets A and B such that they overlap each other like in the first picture. (4) Such overlap is undesirable, instead the widgets just need to be placed one after another, with no overlap, and with gap between them, like in picture 2. I don't know how to do this with QHBoxLayout.
EDIT 2 Here is a visual explanation.
The green here is the parent widget - not resizable by assumption. Widget A is added:
Add widget B:
Now, widget B is on top of A. I don't want it. I want this instead:
The minimumSize of the child widgets does not influence the minimumSize of the parent widget, and the use of the layout does not influence the minimumSize of the widget either. The layouts set the minimumSizeHint and sizeHint using as information the minimumSize of the widgets that handle plus other features such as the size and sizing policy. So in the first instance you must set the minimumSize of the parent widget to be the minimumSizeHint of it.
On the other hand the layout has a spacing by default so it is advisable to set it to 0.
class Line(QWidget):
def __init__(self, parent=None):
super().__init__(parent=parent, autoFillBackground=True)
# Set palette
palette1 = QPalette()
palette1.setColor(QPalette.Window, Qt.red)
palette2 = QPalette()
palette2.setColor(QPalette.Window, Qt.blue)
palettebg = QPalette()
palettebg.setColor(QPalette.Window, Qt.green)
self.setPalette(palettebg)
# Set labels
self.label1 = QLabel(text="A", autoFillBackground=True)
self.label1.setStyleSheet('font-size: 36pt;')
self.label1.setPalette(palette1)
self.label1.setMinimumSize(36, 36)
self.label1.adjustSize()
self.label2 = QLabel(text="B", autoFillBackground=True)
self.label2.setStyleSheet('font-size: 36pt;')
self.label2.setPalette(palette2)
self.label2.setMinimumSize(36, 36)
self.label2.adjustSize()
# Set layout
layout = QHBoxLayout(self, spacing=0)
layout.addWidget(self.label1)
layout.addSpacing(10)
layout.addWidget(self.label2)
self.setMinimumSize(self.minimumSizeHint())
# or
# layout = QHBoxLayout(self, spacing=10)
# layout.addWidget(self.label1)
# layout.addWidget(self.label2)
# self.setMinimumSize(self.minimumSizeHint())
Update:
The maximum size of the layout that can be handled is the size of the parent widget, so in its case it will compress not respecting the spaces, a solution is to set a widget that is the content, and in that to establish the layout, so the layout will stretch to the content widget with freedom.
class Line(QWidget):
def __init__(self, parent=None):
super().__init__(parent=parent, autoFillBackground=True)
# Set palette
palette1 = QPalette()
palette1.setColor(QPalette.Window, Qt.red)
palette2 = QPalette()
palette2.setColor(QPalette.Window, Qt.blue)
palettebg = QPalette()
palettebg.setColor(QPalette.Window, Qt.green)
self.setPalette(palettebg)
# Set labels
self.label1 = QLabel(text="A", autoFillBackground=True)
self.label1.setStyleSheet('font-size: 36pt;')
self.label1.setPalette(palette1)
self.label1.setMinimumSize(36, 36)
self.label1.adjustSize()
self.label2 = QLabel(text="B", autoFillBackground=True)
self.label2.setStyleSheet('font-size: 36pt;')
self.label2.setPalette(palette2)
self.label2.setMinimumSize(36, 36)
self.label2.adjustSize()
content_widget = QWidget(self)
layout = QHBoxLayout(content_widget, spacing=10)
layout.addWidget(self.label1)
layout.addWidget(self.label2)
content_widget.setFixedSize(content_widget.minimumSizeHint())