Растягивающаяся фоновая картинка в PyQt5 с сохранением пропорций

Есть простое приложение, главное окно QMainWindow и в нем несколько QDockWidget. Цель поставить фоновую картинку на это окно, да так, чтобы она занимало всё место (растягивалась вместе с окном) и при этом сохраняла пропорции. Желательно через self.setStyleSheet.

Я пробовал background-image: url("background.png"); и border-image: url("background.png") 0 0 0 0;.

В первом случае картинка сохраняла свой размер, если картинка, скажем, размером 1280x720 - и при окне 200x200 она такой будет, обрезая себе края, и при окне 1920x1080, либо повторяясь, либо оставляя пустое место, если я добавлял background-repeat: no-repeat;.

Во втором случае картинка растягивалась во всё окно, однако пропорции не сохранялись.

Насколько я понял никаких background-size: 100%; в QSS нет.

Важно то что на пустом QMainWidget размещается несколько QDockWidget. Находил способ через создания QLabel, установки в нем QPixMap и размещения этого виджета как центрального в QMainWidget, однако в этом случае картинки не видно за QDockWidget, даже если к ним применено background-color: transparent;.

Есть варианты как решить это?

Пример кода:

import sys
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *

class DockDemo(QMainWindow):
    def __init__(self,parent=None):
        super(DockDemo, self).__init__(parent)
        layout=QHBoxLayout()


        self.setStyleSheet("""
            QMainWindow {
                background-image: url("background.png");
            }
            """)

        self.items=QDockWidget('Dockable',self)
        self.items2=QDockWidget('Dockable2',self)

        self.listWidget=QListWidget()
        self.listWidget.addItem('Item1')
        self.listWidget.addItem('Item2')
        self.listWidget.addItem('Item3')
        self.listWidget.addItem('Item4')

        self.listWidget2=QListWidget()
        self.listWidget2.addItem('Item1')
        self.listWidget2.addItem('Item2')
        self.listWidget2.addItem('Item3')
        self.listWidget2.addItem('Item4')


        self.items.setWidget(self.listWidget)
        self.items2.setWidget(self.listWidget)
        self.items.setFloating(False)
        self.items2.setFloating(False)
        self.addDockWidget(Qt.RightDockWidgetArea,self.items)
        self.addDockWidget(Qt.RightDockWidgetArea,self.items2)

        self.items.setStyleSheet("background-color: transparent;")
        self.items2.setStyleSheet("background-color: transparent;")


        self.setLayout(layout)
        self.setWindowTitle('Dock')

if __name__ == '__main__':
    app=QApplication(sys.argv)
    demo=DockDemo()
    demo.show()
    sys.exit(app.exec_())

Пример того, что я имею ввиду (gif). Белая рамка - само окно, картинка в нем - фон, и эта картинка занимает всё место, и поверх неё уже крепятся QDockWidget.


Ответы (2 шт):

Автор решения: S. Nick

Обратите внимание [Qt Main Window Framework]1

Главное окно обеспечивает основу для создания пользовательского интерфейса приложения. Qt имеет QMainWindow и связанные с ним классы для управления главным окном.
QMainWindow имеет свой собственный layout, в который вы можете добавить QToolBars, QDockWidgets, QMenuBar и QStatusBar.
В макете есть центральная область, которую можно занять любым виджетом. Вы можете увидеть изображение макета ниже.

введите сюда описание изображения

Попробуйте так:

import sys
from PyQt5.Qt import *
from PyQt5 import QtCore, QtGui, QtWidgets


class ImageLabel(QtWidgets.QGraphicsView):
    def __init__(self, *args, **kwargs):
        super(ImageLabel, self).__init__(*args, **kwargs)
        self.setScene(QtWidgets.QGraphicsScene())
        self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) 
        self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)

    def setImage(self, filename):
        self.setPixmap(QtGui.QPixmap(filename))

    def setPixmap(self, pixmap):
        item = QtWidgets.QGraphicsPixmapItem(pixmap)
        item.setTransformationMode(QtCore.Qt.SmoothTransformation)
        self.scene().addItem(item)

    def resizeEvent(self, event):
        r = self.scene().itemsBoundingRect()
        self.fitInView(r, QtCore.Qt.KeepAspectRatio)
        super(ImageLabel, self).resizeEvent(event)


class DockDemo(QMainWindow):
    def __init__(self,parent=None):
        super(DockDemo, self).__init__(parent)
        
        self.centralWidget = QWidget()                                         # +++
        self.setCentralWidget(self.centralWidget)                              # +++
        layout = QHBoxLayout(self.centralWidget)                               # + centralWidget

        self.image = ImageLabel()                                              # +++
        layout.addWidget(self.image)                                           # +++        
        self.image.setImage("boy.jpg")                           # установите свое изображение !!!

        self.items = QDockWidget('Dockable 1', self)
        self.items2 = QDockWidget('Dockable 2', self)

        self.listWidget = QListWidget()
        self.listWidget.addItems(['Item 1', 'Item 2', 'Item 3', 'Item 4', ])
        self.listWidget2 = QListWidget()
        self.listWidget2.addItems(['Item-1', 'Item-2', 'Item-3', 'Item-4', ])

        self.items.setWidget(self.listWidget)
        self.items2.setWidget(self.listWidget2)                                # listWidget2 !!!
        self.items.setFloating(False)
        self.items2.setFloating(False)
        self.addDockWidget(Qt.RightDockWidgetArea,self.items)
        self.addDockWidget(Qt.RightDockWidgetArea,self.items2)
    
  
if __name__ == '__main__':
    app = QApplication(sys.argv)
    demo = DockDemo()
    demo.setWindowTitle('Dock')
    demo.show()
    sys.exit(app.exec_())

введите сюда описание изображения

введите сюда описание изображения

введите сюда описание изображения

→ Ссылка
Автор решения: declider

Что ж, я смог. Перед тем как писать решение, надо упомянуть что у меня в проекте есть вот такая функция

def resource_path(relative_path):
    try:
        base_path = sys._MEIPASS
    except Exception:
        base_path = os.path.abspath(".")

    return os.path.join(base_path, relative_path)

Она мне нужна чтобы загружать картинку (в моем случае background.png) в Exe файл для pyinstaller с флагом --onefile.

Теперь само решение:

background_image = resource_path("background.png").replace('\\','/')

pix = QPixmap(background_image)  #Фон
def main_resize(event): #Ресайз основного окна
    pal = self.palette()
    if pix.width()>=self.width() and pix.height()>=self.height(): #Если размер окна слишком мал
        pal.setBrush(self.backgroundRole(), QBrush(pix.scaled(pix.size(),Qt.KeepAspectRatioByExpanding,Qt.SmoothTransformation)))
    else:
        pal.setBrush(self.backgroundRole(), QBrush(pix.scaled(self.size(),Qt.KeepAspectRatioByExpanding,Qt.SmoothTransformation)))
    self.setPalette(pal)

self.resizeEvent = main_resize

Вообще можно было обойтись только кодом в блоке else, однако я не хочу чтобы картинка сужалась и была слишком мала, поэтому если размер окна меньше размера загруженной картинки и по ширине, и по высоте - она не трансформируется, а если нет - растягивается с сохранение пропорций.

→ Ссылка