pythonpyqt5file-manager

Why are all the buttons on a QWidget not showing?


I am a python Intermediate but I have missed out a few basic parts of python. I am currently learning PyQt5 but don't want to create a window using subclasses yet. I have tried PyQt5 before but never really understood it and was copying and pasting everything without understanding the fundamentals. I am currently working on a very basic file manager in python, but I am having some problems displaying folder buttons.

I am automatically creating buttons depending on how many files and folders there are (I am making all of them folder icons for now) and adding them to a QWidget. Here is the section of my code which is not working expectedly:

for x in files:
    folderbutton = QPushButton(w)
    folderbutton.setIcon(foldericon)
    folderbutton.setIconSize(QSize(100, 100))
    folderbutton.setGeometry(folposx, folposy, 100, 100)
    folposx += 10
    if folposx >= 1919:
        folposx = 0
        folposy += 120

the for loop loops through all the files. folposx and folposy are defined earlier in the code. They are both set to zero. folderbutton is what temporarily defines each QPushButton. foldericon is defined earlier in the code as a QIcon. After defining the position and size of the button, the code adds 10 to folposx, so the next button is beside it. If the button is over the screen's width, the Y value of the button increases and the X value of the button resets, making a new line for the folder buttons.

The above is what I expected to happen, although the actual result was this

I would greatly appreciate it if anyone could help me, and if anyone needs my full code, I am happy to edit this question with it.

Thanks, Zayan.

Edit: the below is my full code, all of the code is needed to reproduce my problem.

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

files = os.listdir()

def window():
    global files
    app = QApplication(sys.argv)
    w = QWidget()
    w.setStyleSheet('background: black; color: white;')
    folposx = 0
    folposy = 0
    w.setGeometry(0, 0, 1919, 1079)
    foldericon = QIcon('C:/Users/HP/OneDrive/Python_Projects/aura_explorer/folder.png') 
    pixmap = foldericon.pixmap(100, 100)
    for x in files:
        folderbutton = QPushButton(w)
        folderbutton.setIcon(foldericon)
        folderbutton.setIconSize(QSize(100, 100))
        folderbutton.setGeometry(folposx, folposy, 100, 100)
        folposx += 10
        if folposx >= 1919:
            folposx = 0
            folposy += 120
            

    w.show()
    sys.exit(app.exec_())

if __name__ == '__main__':
   window()

Solution

  • The cause(s) of the problem

    What you see in the image is caused by the following factors:

    The buttons are there, but every new button is added only slightly to the right than the position of the previous one; since the forced geometry doesn't allow borders, you only see the background of each button, and only the 10 pixels on the left of each one: every newly stacked button completely hides the rest of the underlying button, resulting in a seamless black strip. Only the final button is completely visible, since it's on top of the stack.

    Try the following change:

        ...
        for x in files:
            ...
            folderbutton.setStyleSheet('border: 1px solid red;')
            ...
    

    And you'll probably see something like this:

    enter image description here

    Note that in this case the border is visible because of the way Qt style sheets work.

    As you can see, buttons are stacked each one on top of the other. It's like having stickers with a black background and an image shown in them (but with some blank margin around it): if you put them one over the other, only slightly moving them on the right of the one below them, you'll end up with a lot of black on the left of the topmost sticker, which is the result of all the visible left edges of each underlying sticker.

    A possible solution

    Now, considering all the above, you should always adjust the horizontal and vertical shifts using the button size hint. Also, you must remember that you need to check if you're not going beyond the right edge before setting its geometry, not after, otherwise you'll probably end up with a partially visible row on the right.

    Here is a better implementation of your code:

        folposx = folposy = rowheight = 0
        for x in files[:40]:
            folderbutton = QPushButton(w)
            folderbutton.setIcon(foldericon)
            folderbutton.setIconSize(QSize(100, 100))
    
            # let the button adjust its size based on its needs
            folderbutton.adjustSize()
    
            width = folderbutton.width()
            height = folderbutton.height()
            if folposx + width > w.width():
                folposx = 0
                folposy += rowheight
            else:
                rowheight = max(rowheight, height)
    
            folderbutton.setGeometry(folposx, folposy, width, height)
    
            folposx += width
    

    Unfortunately, this won't solve an important issue: the widgets will always keep the geometries you explicitly set, and if the window is resized you will end up with a lot of empty space on the right (when making the window wider) or having lots of elements partially or completely invisible (if the window becomes narrower).

    The layout alternative

    In theory, you could use a layout manager (possibly, QGridLayout), but that will only partially solve the problem: if the window is made very wide, there will be a lot of unused space in each row, since the grid has a fixed column count.

    A possibility could be to use a "Flow layout", which Qt doesn't provide on its own, and has to be subclassed from QLayout (see the official PySide6 example or a similar implementation for PyQt5). In theory it allows to automatically "wrap" widgets as soon as there is no horizontal space left.

    This won't solve a major problem, no matter the layout type you use: if there are too many items, the window will eventually grow in height, possibly beyond the screen size.
    You could then use a QScrollArea, create a container QWidget (applied using QScrollArea.setWidget()), set the above layout on it, and add the buttons to that layout.

    Still, not enough

    Unfortunately, we still haven't solved all problems; for instance:

    With all that in mind, it's clear that none of the solutions above is effective.

    Luckily, Qt does provide an alternative: the Qt model/view approach, through QListView and QFileSystemModel. Item views are much better for these purposes, as they are faster and normally require less memory than using actual widgets.

    QListView is normally used to vertically display items, but it's actually able to show a mono dimensional model (like a list) in a 2D space, including arranging its items horizontally, eventually wrapping each "line" as soon as there is no space left.
    QFileSystemModel is a "Qt-model interface" to the underlying file system, providing consistent entries and even appropriate icons on its own. Its asynchronous and caching capabilities also prevent blocking when entering or exiting folders with thousands of entries.

    The benefit is that you don't have to care about item layout anymore, and the view will always provide scrolling capabilities whenever necessary.

    Entering directories via doubleclick is even simpler: you only need to set the root index of the view based on the directory you want to show, which can be easily done using the view's signals or even externally (to navigate upper levels).

    def window():
        app = QApplication(sys.argv)
        app.setStyle('windows')
        fileView = QListView()
        fileView.setStyleSheet('''
            QListView { 
                background: black; 
            }
            QListView:item {
                color: white; 
            }
        ''')
    
        fileView.setIconSize(QSize(100, 100))
        fileView.setViewMode(QListView.IconMode)
        fileView.setResizeMode(QListView.Adjust)
        fileView.setWordWrap(True)
        fileView.setSpacing(10)
        # eventually set a fixed item size (this may be a problem 
        # as it will elide the text for long names):
        # fileView.setUniformItemSizes(True)
    
        model = QFileSystemModel()
        root = QDir.currentPath()
        model.setRootPath(root)
        fileView.setModel(model)
        fileView.setRootIndex(model.index(root))
    
        def enterDir(index):
            if model.isDir(index):
                fileView.setRootIndex(index)
        fileView.doubleClicked.connect(enterDir)
    
        screen = QApplication.primaryScreen()
        geo = screen.availableGeometry()
        geo.setSize(geo.size() * .8)
        geo.moveCenter(screen.availableGeometry().center())
        fileView.setGeometry(geo)
    
        fileView.show()
        sys.exit(app.exec_())
    

    And here is the result: enter image description here

    As you can see, the width is not always consistent (word wrapping only happens when the name contains spaces), but it's still usable, and far better than doing everything on your own.

    A final, unrelated note about "[I] don't want to create a window using subclasses yet". While Python allows functional programming, it still is an Object Oriented language, and you also have to consider that aspect when using an extremely OOP oriented toolkit as Qt is. While using simple functions may work fine for simple cases, basing your whole code on it may become a huge obstacle in the long run, complicating things where they could be way simpler if done with proper awareness.
    I strongly suggest you to begin to use subclassing as soon as possible, instead of trying to avoid it, because your "precautious" intention may require implementing unnecessarily convoluted approaches that often result in unexpected behaviors (not to mention that some things can only be done with subclassing), especially if you're trying to use things such as global without really considering their implications.