I'm creating a file browser with PyQt6. This is what I'm thinking of doing right now:
from PyQt6 import QtWidgets as qtw
from PyQt6 import QtGui as qtg
class FileBrowserWidget(qtw.QScrollArea):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.current_items = []
self.main_widget = qtw.QWidget()
self.main_widget.setLayout(qtw.QVBoxLayout())
self.setWidget(self.main_widget)
def add_file(self, thumbnail: qtg.QPixmap, description: str):
item = qtw.QWidget()
item.setLayout(qtw.QHBoxLayout())
file_thumbnail_label = qtw.QLabel()
file_thumbnail_label.setPixmap(thumbnail)
file_description_label = qtw.QLabel(description)
item.layout().addWidget(file_thumbnail_label)
item.layout().addWidget(file_description_label)
self.current_items.append(item)
Note that this is just a rough sketch of the widget. All the code does is display a (thumbnail, description) pair for files inside a directory in a scrollable window. I also plan to implement pagination for it, with at least 25 rows (files) per page.
My questions are:
Is this the way to do it or is there some other better way to go about creating a file browser?
How would I go about implementing pagination to the file browser?
EDIT:
My apologies, it's not just any file browser, it's an image file browser.
Example image of what I'm thinking of creating:
A basic possibility is to use a QListWidget, with some customized settings and precautions when adding items:
the iconSize() must be big enough to show the thumbnails;
a bigger font for the view
the sizeHint() of each item must be specified in order to always respect the same row height and provide text elision;
the image must be scaled and "enlarged" to the icon size in order to keep vertical alignment of the text, otherwise images that have different widths will show the text starting at different positions;
class ImageListView(QtWidgets.QWidget):
def __init__(self):
super().__init__()
self.view = QtWidgets.QListWidget()
self.view.setIconSize(QtCore.QSize(64, 64))
bigFont = self.font()
bigFont.setPointSize(24)
self.view.setFont(bigFont)
self.addButton = QtWidgets.QPushButton('Add image(s)')
layout = QtWidgets.QVBoxLayout(self)
layout.addWidget(self.view)
layout.addWidget(self.addButton)
self.addButton.clicked.connect(self.addImage)
def addImage(self):
paths, _ = QtWidgets.QFileDialog.getOpenFileNames(self,
'Select image(s)', '', 'Images (*.png *.jpg *.jpeg)')
size = self.view.iconSize()
for path in paths:
source = QtGui.QPixmap(path)
if source.isNull():
continue
if source.width() > size.width() or source.height() > size.height():
source = source.scaled(size, QtCore.Qt.KeepAspectRatio,
QtCore.Qt.SmoothTransformation)
# create an empty squared image to keep vertical alignment
square = QtGui.QPixmap(size)
square.fill(QtCore.Qt.transparent)
qp = QtGui.QPainter(square)
rect = source.rect()
rect.moveCenter(square.rect().center())
qp.drawPixmap(rect, source)
qp.end()
name = QtCore.QFileInfo(path).baseName()
item = QtWidgets.QListWidgetItem(name)
item.setIcon(QtGui.QIcon(square))
item.setToolTip(path)
item.setSizeHint(size)
self.view.addItem(item)
For more advanced customization, you can still use a QListWidget, but you also need to set a custom item delegate and override its paint() method.
Related
I'm trying to make an app that shows all the connected files of a certain main file. When you click on a main file, it will show you the list of all files that needs it. As I've started making the app, I'm stuck here one whole day realizing how to create a QFileSystemModel() that only shows the files in a certain list because all the connected files from the mail file are stored in a list.
Here is an example, I want to just show the files in the list which are:
main_file1 = ["[053ALO] - test file.txt", "[053ALO] - test file.txt", "[053ALO] - test file.txt"]
As I've seen on the other related questions, They mentioned the use of QAbstractItemView but I have no Idea how. I'm thinking of iterating the list and making QAbstractItemView in each item and appending it to the treeview but it doesn't show the icon and details of every file which is shown in the picture.
My Question: Is it possible to create a QFileSystemModel() that only shows the files in a certain list?
My Testing Code: (My plan is to use the left side for the main files, and the right one for the connected files)
import sys
from PyQt5.QtWidgets import QApplication, QWidget, QTreeView, QFileSystemModel, \
QHBoxLayout
class FileSystemView(QWidget):
def __init__(self):
super().__init__()
appWidth = 800
appHeight = 300
self.setWindowTitle('File System Viewer')
self.setGeometry(300, 300, appWidth, appHeight)
dir_path = r'<Your directory>'
self.model = QFileSystemModel()
self.model.setRootPath(dir_path)
self.tree = QTreeView()
self.tree.setModel(self.model)
self.tree.setRootIndex(self.model.index(dir_path))
self.tree.setColumnWidth(0, 250)
self.tree.setAlternatingRowColors(True)
self.model2 = QFileSystemModel()
self.model2.setRootPath(dir_path)
self.tree2 = QTreeView()
self.tree2.setModel(self.model2)
self.tree2.setRootIndex(self.model2.index(dir_path))
self.tree2.setColumnWidth(0, 250)
self.tree2.setAlternatingRowColors(True)
layout = QHBoxLayout()
layout.addWidget(self.tree)
layout.addWidget(self.tree2)
self.setLayout(layout)
if __name__ == '__main__':
app = QApplication(sys.argv)
demo = FileSystemView()
demo.show()
sys.exit(app.exec_())
Update:
With the help of #musicamante I'ved realized that I needed to subclass QSortFilterProxyModel in order to customize the filtering that I want to happen. I'm pretty sure that my approach in the code below is close now but I'm still stuck with this problem where when I clicked on a file in the left side, the similar file in the right side disappears. (seen on this video link)
This is the complete oposite of what I want to happen. What I want to happen is when I click a file on the left side, ONLY the file with the same name will aslo appear on the right side.
I tried changing the condition in the if else statement inside the filterAcceptsRow but it just leaves the right side completely empty.
I've provided the Testing code below:
import sys
from PyQt5.QtWidgets import QApplication, QWidget, QTreeView, QFileSystemModel, QHBoxLayout
from PyQt5.QtCore import QSortFilterProxyModel, Qt
class modifiedQSortFilterProxyModel(QSortFilterProxyModel):
def __init__(self):
super().__init__()
self.file = ''
def filterAcceptsRow(self, source_row, source_parent):
filename = self.sourceModel().index(source_row, 0, source_parent).data()
if filename == self.file:
return False
else:
return True
class FileSystemView(QWidget):
def __init__(self):
super().__init__()
appWidth = 800
appHeight = 300
self.setWindowTitle('File System Viewer')
self.setGeometry(300, 300, appWidth, appHeight)
dir_path = r''
# -- left -- #
self.model = QFileSystemModel()
self.model.setRootPath(dir_path)
self.tree = QTreeView()
self.tree.setModel(self.model)
self.tree.setRootIndex(self.model.index(dir_path))
self.tree.setColumnWidth(0, 250)
self.tree.setAlternatingRowColors(True)
self.tree.clicked.connect(self.onClicked)
# -- right -- #
self.model2 = QFileSystemModel()
self.model2.setRootPath(dir_path)
self.filter_proxy_model = modifiedQSortFilterProxyModel()
self.filter_proxy_model.setSourceModel(self.model2)
self.filter_proxy_model.setFilterCaseSensitivity(Qt.CaseInsensitive)
self.filter_proxy_model.setDynamicSortFilter(True)
self.filter_proxy_model.setFilterKeyColumn(0)
root_index = self.model2.index(dir_path)
proxy_index = self.filter_proxy_model.mapFromSource(root_index)
self.tree2 = QTreeView()
self.tree2.setModel(self.filter_proxy_model)
self.tree2.setRootIndex(proxy_index)
self.tree2.setColumnWidth(0, 250)
self.tree2.setAlternatingRowColors(True)
# -- layout -- #
layout = QHBoxLayout()
layout.addWidget(self.tree)
layout.addWidget(self.tree2)
self.setLayout(layout)
def onClicked(self, index):
path = self.sender().model().fileName(index)
self.filter_proxy_model.file = path
self.filter_proxy_model.invalidateFilter()
if __name__ == '__main__':
app = QApplication(sys.argv)
demo = FileSystemView()
demo.show()
sys.exit(app.exec_())
There are two problems.
First of all, if you want to show only the index that matches the selected file, you should return True, not False (the function says if the row is accepted, meaning that it's shown).
Then, you cannot just base the filter on the file name: models are hierarchical, and the filter must also match parent indexes (the parent directory); the reason for which you see a blank view is that returning False from a non matching file name, you're filtering out the parent directory.
Considering this, the filter must always accept an index if the parent is different, and eventually return the result of the comparison. To achieve so, you can use the QFileInfo that refers to the source index, which is returned by the models' fileInfo():
class ModifiedQSortFilterProxyModel(QSortFilterProxyModel):
fileInfo = None
def filterAcceptsRow(self, source_row, source_parent):
if not self.fileInfo:
return True
source_index = self.sourceModel().index(source_row, 0, source_parent)
info = self.sourceModel().fileInfo(source_index)
if self.fileInfo.absolutePath() != info.absolutePath():
return True
return self.fileInfo.fileName() == info.fileName()
def setFilter(self, info):
self.fileInfo = info
self.invalidateFilter()
class FileSystemView(QWidget):
# ...
def onClicked(self, index):
self.filter_proxy_model.setFilter(self.model.fileInfo(index))
Note: names of classes and constants should always start with an uppercase letter.
So I have a table with 1 row and multiple columns and I use the custom delegate function for the image thumbnail. I also included the set background colour function when I click it. However, it doesn't change the colour of the item background. I have to use the custom delegate function so that my icon image is sticking with each other from different cell without any additional spaces between.
my code is as below
import random
from PyQt5 import QtCore, QtGui, QtWidgets
imagePath2 = "arrowREDHead.png"
imagePath = "arrowREDBody.png"
class IconDelegate(QtWidgets.QStyledItemDelegate):
def paint(self, painter, option, index):
icon = index.data(QtCore.Qt.DecorationRole)
mode = QtGui.QIcon.Normal
if not (option.state & QtWidgets.QStyle.State_Enabled):
mode = QtGui.QIcon.Disabled
elif option.state & QtWidgets.QStyle.State_Selected:
mode = QtGui.QIcon.Selected
state = (
QtGui.QIcon.On
if option.state & QtWidgets.QStyle.State_Open
else QtGui.QIcon.Off
)
pixmap = icon.pixmap(option.rect.size(), mode, state)
painter.drawPixmap(option.rect, pixmap)
def sizeHint(self, option, index):
return QtCore.QSize(20, 20)
class MainWindow(QtWidgets.QMainWindow):
def __init__(self, parent=None):
super().__init__(parent)
table = QtWidgets.QTableWidget(1, 10)
delegate = IconDelegate(table)
table.setItemDelegate(delegate)
self.setCentralWidget(table)
for j in range(table.columnCount()):
if(j % 2 == 0):
pixmap = QtGui.QPixmap(imagePath)
icon = QtGui.QIcon(pixmap)
it = QtWidgets.QTableWidgetItem()
it.setIcon(icon)
table.setItem(0, j, it)
else:
pixmap = QtGui.QPixmap(imagePath2)
icon = QtGui.QIcon(pixmap)
it = QtWidgets.QTableWidgetItem()
it.setIcon(icon)
table.setItem(0, j, it)
table.item(0, 1).setBackground(QtGui.QColor(100,100,150))
table.resizeRowsToContents()
table.resizeColumnsToContents()
table.setShowGrid(False)
table.itemClicked.connect(self.sequenceClicked)
def sequenceClicked(self, item):
self.itemClicked = item
print("item", item)
item.setBackground(QtGui.QColor(255, 215, 0))
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
w = MainWindow()
w.show()
sys.exit(app.exec_())
This is the image with the gap between icons and it form not a perfect looking arrow thats why I used the custom delegate function to stick the icon together and let it look like a perfect arrow
This is the image that has the perfect look of the arrow as it doesnt has any gap between the cells by using the delegate function. However, it doesnt allow me to change the background colour for the cell.
This is the image of the arrowHead
This is the image of the arrowBody
As with any overridden function, if the base implementation is not called, the default behavior is ignored.
paint() is the function responsible of drawing any aspect of the item, including its background. Since in your override you are just drawing the pixmap, everything else is missing, so the solution would be to call the base implementation and then draw the pixmap.
In this specific case, though, this wouldn't be the right choice, as the base implementation already draws the item's decoration, and in certain conditions (assuming that the pixmap has an alpha channel), it will probably result in having the images overlapping.
There are three possible solutions.
Use QStyle functions ignoring the icon
In this case, we do what the base implementation would, which is calling the style drawControl function with the CE_ItemViewItem, but we modify the option before that, removing anything related to the icon:
def paint(self, painter, option, index):
# create a new option (as the existing one should not be modified)
# and initialize it for the current index
option = QtWidgets.QStyleOptionViewItem(option)
self.initStyleOption(option, index)
# remove anything related to the icon
option.features &= ~option.HasDecoration
option.decorationSize = QtCore.QSize()
option.icon = QtGui.QIcon()
if option.widget:
style = option.widget.style()
else:
style = QtWidgets.QApplication.style()
# call the drawing function for view items
style.drawControl(style.CE_ItemViewItem, option, painter, option.widget)
# proceed with the custom drawing of the pixmap
icon = index.data(QtCore.Qt.DecorationRole)
# ...
Update the icon information in the delegate
A QStyledItemDelegate always calls initStyleOption at the beginning of most its functions, including paint and sizeHint. By overriding that function, we can change some aspects of the drawing, including the icon alignment and positioning.
In this specific case, knowing that we have only two types of icons, and they must be right or left aligned depending on the image, it's enough to update the proper decoration size (based on the optimal size of the icon), and then the alignment based on the column.
class IconDelegate(QtWidgets.QStyledItemDelegate):
def initStyleOption(self, option, index):
super().initStyleOption(option, index)
option.decorationSize = option.icon.actualSize(option.rect.size())
if index.column() & 1:
option.decorationPosition = option.Left
option.decorationAlignment = QtCore.Qt.AlignLeft|QtCore.Qt.AlignVCenter
else:
option.decorationPosition = option.Right
option.decorationAlignment = QtCore.Qt.AlignRight|QtCore.Qt.AlignVCenter
Note: this approach only works as long as the images all have the same ratio, and the row height is not manually changed.
Use a custom role for the image
Qt uses roles to store and retrieve various aspects of each index (font, background, etc.), and user roles can be defined to store custom data.
setIcon() actually sets a QIcon in the item using the DecorationRole, and the delegate uses it to draw it. If the data returned for that role is empty/invalid, no icon is drawn.
If we don't use setIcon() but store the icon in the item with a custom role instead, the default drawing functions will obviously not draw any decoration, but we can still access it from the delegate.
# we usually define roles right after the imports, as they're constants
ImageRole = QtCore.Qt.UserRole + 1
class IconDelegate(QtWidgets.QStyledItemDelegate):
def paint(self, painter, option, index):
super().paint(painter, option, index)
icon = index.data(ImageRole)
# ...
class MainWindow(QtWidgets.QMainWindow):
def __init__(self, parent=None):
# ...
it = QtWidgets.QTableWidgetItem()
it.setData(ImageRole, pixmap)
I'm trying to display an animated GIF on the left side of a QListWidgetItem in a QListWidget with the label text following. I've been reading that QLabels hold QMovies which can run GIF animations and that I'd need to create a custom widget and use that instead of the default QListWidgetItem, but I've had no luck. Does anyone how to do this? Am I over-complicating things?
I've written up a basic test case below:
#! /usr/bin/env python
from PySide2 import QtGui, QtWidgets, QtCore
class List_Widget_Gif(QtWidgets.QWidget):
def __init__(self, label_text, gif, parent=None):
super(List_Widget_Gif, self).__init__(parent)
# Layout
horizontal_box_layout = QtWidgets.QHBoxLayout()
# Create text label
self.text_label = QtWidgets.QLabel()
self.text_label.setText(label_text)
# Create label to apply GIF to (Apparently this is the best thing to use for GIF in this case?)
self.icon_label = QtWidgets.QLabel()
movie = QtGui.QMovie(gif, QtCore.QByteArray(), self)
self.icon_label.setMovie(movie)
movie.start()
# Add widgets to layout
horizontal_box_layout.addWidget(self.text_label)
horizontal_box_layout.addWidget(self.icon_label)
#Set the layout
self.setLayout(horizontal_box_layout)
class TestUI(QtWidgets.QMainWindow):
def __init__(self, parent=None):
super(TestUI, self).__init__(parent)
self.setObjectName("TestUI")
#Vars to pass
self.my_gif = "my_cool_animation.gif"
self.my_text = "This is awesome text"
def setup_UI(self):
#Create Default List Widget
list_widget = QtWidgets.QListWidget()
# Create Default List Widget Item
default_list_item = QtWidgets.QListWidgetItem()
# Create Custom List Widget with label and GIF motion
custom_list_widget_item = List_Widget_Gif(self.my_text, self.my_gif)
# Add default item to list widget
list_widget.insertItem(list_widget.count(), default_list_item)
# Set the default item to the custom one with the gif motion.
self.ui.layerList.setItemWidget(default_list_item, custom_list_widget_item)
#Set into UI
self.setCentralWidget(list_widget)
self.show()
if __name__ == "__main__":
app = QtWidgets.QApplication([])
test = TestUI()
test.setup_UI()
app.exec_()
First you have a typo (it should throw an exception) so you have to change self.ui.layerList to list_widget.
Correcting the above there are several possible causes of error:
The margins must be removed from the layout of the custom widget:
horizontal_box_layout.setContentsMargins(0, 0, 0, 0)
Do not use relative paths since they are the cause of silent errors, it is better to build the absolute path based on the location of another element. If I assume that the .gif is in the same folder as the .py then you can use that information to do so it changes:
import os
CURRENT_DIR = os.path.dirname(os.path.realpath(__file__))
self.my_gif = os.path.join(CURRENT_DIR, "my_cool_animation.gif")
It works, you just need to resize the default item to the size of your custom item.
def setup_UI(self):
#Create Default List Widget
list_widget = QtWidgets.QListWidget()
# Create Default List Widget Item
default_list_item = QtWidgets.QListWidgetItem()
# Create Custom List Widget with label and GIF motion
custom_list_widget_item = List_Widget_Gif(self.my_text, self.my_gif)
# Add default item to list widget
list_widget.insertItem(list_widget.count(), default_list_item)
# Set the default item to the custom one with the gif motion.
list_widget.setItemWidget(default_list_item, custom_list_widget_item)
default_list_item.setSizeHint(custom_list_widget_item.size())
#Set into UI
self.setCentralWidget(list_widget)
self.show()
Output
If I have Table Like as mention Below,
And I want to do as below to squeeze all columns bear to minimum scrollbar size or without scrollbar,
In PyQt5 in QTableview how can I do align any content to center in cell and want to minimum scrollbar and if possible without scrollbar then also it is well good.
as like below image text are not align and I wish to do squeeze all columns as per image 1 and align text to center in PyQt5 in Python.
The trick is to use the Stretch resize mode of the horizontal header, which ensures that all columns fit the available size of the view. The only problem comes from the minimumSectionSize(), which by default is a value dependent on the font and the margin between the sort indicator and the text of each header section, so, even using Stretch, the columns wouldn't resize below that width.
By setting the minimum size to 0 we can prevent that behavior. Keep in mind, though, that even with not-so-narrow columns (under 16-18 pixels wide) you will not be able to see the header text at all, no matter if there could be enough space for the text to be shown: some space is always reserved to the header section separators and their margin.
About the text alignment, the standard approach is to use setTextAlignment on each item. If you need to do that constantly, just use a subclass of QStandardItem that automatically sets its alignment after initialization.
from PyQt5 import QtCore, QtGui, QtWidgets
class FitTable(QtWidgets.QTableView):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.horizontalHeader().setSectionResizeMode(QtWidgets.QHeaderView.Stretch)
self.horizontalHeader().setMinimumSectionSize(0)
def resizeEvent(self, event):
super().resizeEvent(event)
if not self.model() or not self.model().columnCount():
return
# the text can be completely hidden on very narrow columns if the
# elide mode is enabled; let's disable it for widths lower than
# the average width of 3 characters
colSize = self.viewport().width() // self.model().columnCount()
if colSize < self.fontMetrics().averageCharWidth() * 3:
self.setTextElideMode(QtCore.Qt.ElideNone)
else:
self.setTextElideMode(QtCore.Qt.ElideRight)
class CenteredItem(QtGui.QStandardItem):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.setTextAlignment(QtCore.Qt.AlignCenter)
class Window(QtWidgets.QWidget):
def __init__(self):
QtWidgets.QWidget.__init__(self)
layout = QtWidgets.QGridLayout(self)
self.table = FitTable()
layout.addWidget(self.table)
model = QtGui.QStandardItemModel()
self.table.setModel(model)
for row in range(5):
rowItems = []
for column in range(30):
# usually the text alignment is manually applied like this:
# item = QtGui.QStandardItem(str(column + 1))
#
# item.setTextAlignment(QtCore.Qt.AlignCenter)
#
# for convenience, I use a subclass that automatically does that
item = CenteredItem(str(column + 1))
rowItems.append(item)
model.appendRow(rowItems)
if __name__ == '__main__':
import sys
app = QtWidgets.QApplication(sys.argv)
window = Window()
window.show()
sys.exit(app.exec_())
I am creating a form with some QTextEdit widgets.
The default height of the QTextEdit exceeds a single line of text and as the contents' height exceeds the QTextEdit's height, it creates a scroll-bar to scroll the content.
I would like to override this behaviour to create a QTextEdit that would rather wrap its height to its contents. This means that the default height would be one line and that on wrapping or entering a new line, the QTextEdit would increase its height automatically. Whenever the contents height exceeds the QTextEdit's height, the latter should not create a scroll bar but simply increase in height.
How can I go about doing this? Thanks.
This is almost exactly like a question I answer the other day about making a QTextEdit adjust its height in reponse to content changes: PySide Qt: Auto vertical growth for TextEdit Widget
I am answering instead of marking a duplicate as I suspect its possible you want a variation on this. Let me know if you want me to expand this answer:
The other question had multiple parts. Here is the excerpt of the growing height widget:
class Window(QtGui.QDialog):
def __init__(self):
super(Window, self).__init__()
self.resize(600,400)
self.mainLayout = QtGui.QVBoxLayout(self)
self.mainLayout.setMargin(10)
self.scroll = QtGui.QScrollArea()
self.scroll.setWidgetResizable(True)
self.scroll.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
self.mainLayout.addWidget(self.scroll)
scrollContents = QtGui.QWidget()
self.scroll.setWidget(scrollContents)
self.textLayout = QtGui.QVBoxLayout(scrollContents)
self.textLayout.setMargin(10)
for _ in xrange(5):
text = GrowingTextEdit()
text.setMinimumHeight(50)
self.textLayout.addWidget(text)
class GrowingTextEdit(QtGui.QTextEdit):
def __init__(self, *args, **kwargs):
super(GrowingTextEdit, self).__init__(*args, **kwargs)
self.document().contentsChanged.connect(self.sizeChange)
self.heightMin = 0
self.heightMax = 65000
def sizeChange(self):
docHeight = self.document().size().height()
if self.heightMin <= docHeight <= self.heightMax:
self.setMinimumHeight(docHeight)
the following code sets a QTextEdit widget to the height of the content:
# using QVBoxLayout in this example
grid = QVBoxLayout()
text_edit = QTextEdit('Some content. I make this a little bit longer as I want to see the effect on a widget with more than one line.')
# read-only
text_edit.setReadOnly(True)
# no scroll bars in this example
text_edit.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
text_edit.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
text_edit.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
# you can set the width to a specific value
# text_edit.setFixedWidth(400)
# this is the trick, we nee to show the widget without making it visible.
# only then the document is created and the size calculated.
# Qt.WA_DontShowOnScreen = 103, PyQt does not have this mapping?!
text_edit.setAttribute(103)
text_edit.show()
# now that we have a document we can use it's size to set the QTextEdit's size
# also we add the margins
text_edit.setFixedHeight(text_edit.document().size().height() + text_edit.contentsMargins().top()*2)
# finally we add the QTextEdit to our layout
grid.addWidget(text_edit)
I hope this helps.