So, I have this simple PyQt5 code which is essentially a file explorer. I need to be able to select arbitrary files or groups of files (directories and all of the children). I would like to:
Add a Checkbox next to each item
If an item is checked/uncheck and has sub-items, the state of the sub-items should be set to the state of the item. So if you check a directory, everything underneath it should also get checked.
When an items check state is changed, directly or indirectly, I need to invoke a callback with the full path (relative to the root) of the item.
I am essentially building a list of selected files to process.
import sys
from PyQt5.QtWidgets import QApplication, QFileSystemModel, QTreeView, QWidget, QVBoxLayout
from PyQt5.QtGui import QIcon
class App(QWidget):
def __init__(self):
super().__init__()
self.title = 'PyQt5 file system view - pythonspot.com'
self.left = 10
self.top = 10
self.width = 640
self.height = 480
self.initUI()
def initUI(self):
self.setWindowTitle(self.title)
self.setGeometry(self.left, self.top, self.width, self.height)
self.model = QFileSystemModel()
self.model.setRootPath('')
self.tree = QTreeView()
self.tree.setModel(self.model)
self.tree.setAnimated(False)
self.tree.setIndentation(20)
self.tree.setSortingEnabled(True)
self.tree.setWindowTitle("Dir View")
self.tree.resize(640, 480)
windowLayout = QVBoxLayout()
windowLayout.addWidget(self.tree)
self.setLayout(windowLayout)
self.show()
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = App()
sys.exit(app.exec_())
The QFileSystemModel doesn't load the contents of a directory until explicitly requested (in case of a tree view, it onyl happens when the directory is expanded the first time).
This requires to carefully verify and set the check state of each path recursively not only whenever a new file or directory is added (or renamed/removed), but also when the directory contents are actually loaded.
In order to correctly implement this, the check states should also be stored using file paths, because when the contents of a directory change some indexes might be invalidated.
The following implementation should take care of all written above, and emit a signal only when an item state is actively changed and the parent state is changed, but not for the children items of a checked directory.
While this choice might seem partially incoherent, it's a performance requirement, as you cannot get the individual signals for each subdirectory (nor you might want to): if you check the top level directory, you might receive thousands of unwanted notifications; on the other hand, it might be important to receive a notification if the parent directory state has changed, whenever all items become checked or unchecked.
from PyQt5 import QtCore, QtWidgets
class CheckableFileSystemModel(QtWidgets.QFileSystemModel):
checkStateChanged = QtCore.pyqtSignal(str, bool)
def __init__(self):
super().__init__()
self.checkStates = {}
self.rowsInserted.connect(self.checkAdded)
self.rowsRemoved.connect(self.checkParent)
self.rowsAboutToBeRemoved.connect(self.checkRemoved)
def checkState(self, index):
return self.checkStates.get(self.filePath(index), QtCore.Qt.Unchecked)
def setCheckState(self, index, state, emitStateChange=True):
path = self.filePath(index)
if self.checkStates.get(path) == state:
return
self.checkStates[path] = state
if emitStateChange:
self.checkStateChanged.emit(path, bool(state))
def checkAdded(self, parent, first, last):
# if a file/directory is added, ensure it follows the parent state as long
# as the parent is already tracked; note that this happens also when
# expanding a directory that has not been previously loaded
if not parent.isValid():
return
if self.filePath(parent) in self.checkStates:
state = self.checkState(parent)
for row in range(first, last + 1):
index = self.index(row, 0, parent)
path = self.filePath(index)
if path not in self.checkStates:
self.checkStates[path] = state
self.checkParent(parent)
def checkRemoved(self, parent, first, last):
# remove items from the internal dictionary when a file is deleted;
# note that this *has* to happen *before* the model actually updates,
# that's the reason this function is connected to rowsAboutToBeRemoved
for row in range(first, last + 1):
path = self.filePath(self.index(row, 0, parent))
if path in self.checkStates:
self.checkStates.pop(path)
def checkParent(self, parent):
# verify the state of the parent according to the children states
if not parent.isValid():
return
childStates = [self.checkState(self.index(r, 0, parent)) for r in range(self.rowCount(parent))]
newState = QtCore.Qt.Checked if all(childStates) else QtCore.Qt.Unchecked
oldState = self.checkState(parent)
if newState != oldState:
self.setCheckState(parent, newState)
self.dataChanged.emit(parent, parent)
self.checkParent(parent.parent())
def flags(self, index):
return super().flags(index) | QtCore.Qt.ItemIsUserCheckable
def data(self, index, role=QtCore.Qt.DisplayRole):
if role == QtCore.Qt.CheckStateRole and index.column() == 0:
return self.checkState(index)
return super().data(index, role)
def setData(self, index, value, role, checkParent=True, emitStateChange=True):
if role == QtCore.Qt.CheckStateRole and index.column() == 0:
self.setCheckState(index, value, emitStateChange)
for row in range(self.rowCount(index)):
# set the data for the children, but do not emit the state change,
# and don't check the parent state (to avoid recursion)
self.setData(index.child(row, 0), value, QtCore.Qt.CheckStateRole,
checkParent=False, emitStateChange=False)
self.dataChanged.emit(index, index)
if checkParent:
self.checkParent(index.parent())
return True
return super().setData(index, value, role)
class Test(QtWidgets.QWidget):
def __init__(self):
super().__init__()
layout = QtWidgets.QVBoxLayout(self)
self.tree = QtWidgets.QTreeView()
layout.addWidget(self.tree, stretch=2)
model = CheckableFileSystemModel()
model.setRootPath('')
self.tree.setModel(model)
self.tree.setSortingEnabled(True)
self.tree.header().setSectionResizeMode(QtWidgets.QHeaderView.Stretch)
self.logger = QtWidgets.QPlainTextEdit()
layout.addWidget(self.logger, stretch=1)
self.logger.setReadOnly(True)
model.checkStateChanged.connect(self.updateLog)
self.resize(640, 480)
QtCore.QTimer.singleShot(0, lambda: self.tree.expand(model.index(0, 0)))
def updateLog(self, path, checked):
if checked:
text = 'Path "{}" has been checked'
else:
text = 'Path "{}" has been unchecked'
self.logger.appendPlainText(text.format(path))
self.logger.verticalScrollBar().setValue(
self.logger.verticalScrollBar().maximum())
if __name__ == '__main__':
import sys
app = QtWidgets.QApplication(sys.argv)
test = Test()
test.show()
sys.exit(app.exec_())
Related
Some context:
I made a QTreeView and set QFileSystemModel as its model. There are 2 TreeViews, the one on the right corresponds in the list of the project files in a directory, and the left one serves for the view of the connected files for that project file. With help, I've implemented this behavior where when I click on a project file on the right side, its connected files will appear on the left side. (I removed the mechanism of storing and the connected files as it will add a ton of code)
The problem:
After adding the 2 TreeViews above, I then added a context menu with only one option, which is to delete the selected files. I think my first attempt which is on the left side is successful because I can see the path printed in the console. Now the problem lies when I tried to do it on the right side. Knowing that the model in this side is subclassed from the QSortFilterProxyModel, I then added the sourceModel() so that it will return the QFileSystemModel. But when I tried to run the file, It just gives me an error. You can see my implementation below.
def deleteFile(self, event):
index_list = self.tree_sender.selectedIndexes()
for index in index_list:
if index.column() == 0:
if isinstance(self.tree_sender.model(), ModifiedQSortFilterProxyModel)
fileIn = self.tree_sender.model().sourceModel().fileInfo(index)
else:
fileIn = self.tree_sender.model().fileInfo(index)
# method above is also not working even I add sourceModel()
# It just ends the app imediately
path_to_delete = fileIn.absoluteFilePath()
print(path_to_delete)
My Testing Code:
import sys
from PyQt5.QtWidgets import QApplication, QWidget, QTreeView, QFileSystemModel, QHBoxLayout, \
QVBoxLayout, QPushButton, QListWidget, QListWidgetItem, QMenu, QAction, QAbstractItemView
from PyQt5.QtCore import QSortFilterProxyModel, Qt
from PyQt5.QtGui import QCursor
class ModifiedQSortFilterProxyModel(QSortFilterProxyModel):
fileInfo = None
con_files = 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 info.fileName() in self.con_files
def setFilter(self, info, connected_files):
self.fileInfo = info
self.con_files = connected_files
self.invalidateFilter()
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>'
# -- 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.setSelectionMode(QAbstractItemView.ExtendedSelection)
self.tree.clicked.connect(self.onClicked)
self.tree.setContextMenuPolicy(Qt.CustomContextMenu)
self.tree.customContextMenuRequested.connect(self.context_menu)
# -- right -- #
self.model2 = QFileSystemModel()
self.model2.setRootPath(dir_path)
self.model2.setReadOnly(False)
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)
self.tree2.setSelectionMode(QAbstractItemView.ExtendedSelection)
self.tree2.setContextMenuPolicy(Qt.CustomContextMenu)
self.tree2.customContextMenuRequested.connect(self.context_menu)
# -- layout -- #
self.tree_layout = QHBoxLayout()
self.tree_layout.addWidget(self.tree)
self.tree_layout.addWidget(self.tree2)
self.setLayout(self.tree_layout)
def context_menu(self, event):
self.tree_sender = self.sender()
# ^^^ I dont know how to access the treeview where context menu
# ^^^ is triggered so I just tried to pass it as a variable
self.menu = QMenu(self)
deleteAction = QAction('Delete', self)
deleteAction.triggered.connect(lambda event: self.deleteFile(event))
self.menu.addAction(deleteAction)
self.menu.popup(QCursor.pos())
def deleteFile(self, event):
index_list = self.tree_sender.selectedIndexes()
for index in index_list:
if index.column() == 0:
if isinstance(self.tree_sender.model(), ModifiedQSortFilterProxyModel):
fileIn = self.tree_sender.model().sourceModel().fileInfo(index)
else:
fileIn = self.tree_sender.model().fileInfo(index)
# method above is also not working even I add sourceModel()
# It just ends the app imediately
path_to_delete = fileIn.absoluteFilePath()
print(path_to_delete)
def onClicked(self, index):
print("onclick")
connected_files = [] # list of connected files
self.filter_proxy_model.setFilter(self.model.fileInfo(index), connected_files)
if __name__ == '__main__':
app = QApplication(sys.argv)
demo = FileSystemView()
demo.show()
sys.exit(app.exec_())
My Question: What causes the error? is it sourceModel() implementation? or I did something very wrong?
I hope someone can point me out to the right direction.
I totally forgot to use mapToSource, but thankfully #musicamante guided me through it. My question is now solved, and the final implementation of the deleteFile method is given below.
def deleteFile(self, event):
index_list = self.tree_sender.selectedIndexes()
for index in index_list:
if index.column() == 0:
model = self.tree_sender.model()
if isinstance(model, ModifiedQSortFilterProxyModel):
proxy_index = model.mapToSource(index)
fileIn = proxy_index.model().fileInfo(proxy_index)
else:
fileIn = model.fileInfo(index)
path_to_delete = fileIn.absoluteFilePath()
print(path_to_delete)
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.
Having trouble trying to figure out how to catch a row in a QTableView that's being edited has been canceled. For example, if I am editing a newly inserted row in a QTableView and the ESC, up/down arrows keys have been pressed, I need to remove the row because (in my mind) has been cancelled. Also holds true if the user clicks away from the row. I can't really post any code as I have no idea how to implement something like this. Any ideas?
I have a an example of, what I believe, is what you want (at least for key pressed issue). From there you can do something similar for the clicking issue.
My solution uses a custom QItemDelegate that overrides the eventFilter method. Also it uses a naive model (because you want to use QTableView), the use of the layoutChanged signal on the model is due to functionality of the example, read the docs for more suitable add/delete data features according to your needs.
Hope it helps.
The sample ui:
# -*- coding: utf-8 -*-
# Form implementation generated from reading ui file 'test.ui',
# licensing of 'test.ui' applies.
#
# Created: Wed Nov 7 16:10:12 2018
# by: pyside2-uic running on PySide2 5.11.0
#
# WARNING! All changes made in this file will be lost!
from PySide2 import QtCore, QtGui, QtWidgets
class Ui_Test(object):
def setupUi(self, Test):
Test.setObjectName("Test")
Test.resize(538, 234)
self.horizontalLayout = QtWidgets.QHBoxLayout(Test)
self.horizontalLayout.setObjectName("horizontalLayout")
self.gridLayout = QtWidgets.QGridLayout()
self.gridLayout.setObjectName("gridLayout")
self.tableView = QtWidgets.QTableView(Test)
self.tableView.setObjectName("tableView")
self.gridLayout.addWidget(self.tableView, 0, 0, 1, 1)
self.addRow = QtWidgets.QPushButton(Test)
self.addRow.setObjectName("addRow")
self.gridLayout.addWidget(self.addRow, 0, 1, 1, 1)
self.horizontalLayout.addLayout(self.gridLayout)
self.retranslateUi(Test)
QtCore.QMetaObject.connectSlotsByName(Test)
def retranslateUi(self, Test):
Test.setWindowTitle(QtWidgets.QApplication.translate("Test", "Dialog", None, -1))
self.addRow.setText(QtWidgets.QApplication.translate("Test", "add row", None, -1))
The actual classes involved (I use PySide2):
from PySide2 import QtWidgets, QtCore, QtGui
from _test import Ui_Test
class MyDialog(QtWidgets.QDialog):
def __init__(self, parent = None):
super(MyDialog, self).__init__(parent = parent)
self.ui = Ui_Test()
self.ui.setupUi(self)
self._model = MyModel([["first row 1 col", "first row 2"],["second row 1", "second row 2"]])
self.ui.tableView.setModel(self._model)
self.ui.addRow.clicked.connect(self._model.addRow)
self.ui.tableView.setItemDelegate(MyDelegate(self.ui.tableView))
# this is crucial: we need to be sure that the selection is single on the view
self.ui.tableView.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectItems)
self.ui.tableView.setSelectionMode(QtWidgets.QAbstractItemView.SingleSelection)
class MyModel(QtCore.QAbstractTableModel):
def __init__(self, table_data = None, parent = None):
super(MyModel, self).__init__(parent = parent)
if not table_data: self._data = []
self._data = table_data
def rowCount(self, parent = None):
return len(self._data)
def columnCount(self, parent = None):
return 2
def addRow(self):
self._data.append(["new item", "new item"])
self.layoutChanged.emit()
def removeRow(self, row):
if 0 <= row < self.rowCount():
del self._data[row]
self.layoutChanged.emit()
def data(self, index, role = QtCore.Qt.DisplayRole):
if index.isValid():
if role == QtCore.Qt.DisplayRole:
row = index.row()
col = index.column()
return self._data[row][col]
def setData(self, index, value, role = QtCore.Qt.EditRole):
if index.isValid():
if role == QtCore.Qt.EditRole:
row = index.row()
col = index.column()
self._data[row][col] = str(value)
return True
else:
return False
else:
return False
def flags(self, index):
return QtCore.Qt.ItemIsSelectable|QtCore.Qt.ItemIsEditable|QtCore.Qt.ItemIsEnabled
class MyDelegate(QtWidgets.QItemDelegate):
def __init__(self, parent = None):
super(MyDelegate, self).__init__(parent)
self.view = parent
def eventFilter(self, editor, event):
# there is a lot of checking in order to identify the desired situation
# and avoid errors
if isinstance(event, QtGui.QKeyEvent):
if event.type() == QtCore.QEvent.KeyPress:
if event.key() == QtCore.Qt.Key_Escape:
# we should have a list here of length one (due to selection restrictions on the view)
index = self.view.selectedIndexes()
if index:
if index[0].isValid():
row = index[0].row()
self.view.model().removeRow(row)
return super(MyDelegate, self).eventFilter(editor, event)
if __name__ == '__main__':
app = QtWidgets.QApplication()
diag = MyDialog()
diag.show()
app.exec_()
I am fairly new to PyQt, I'm working on a project that contains a QTableView, with one of its columns displaying system paths. I would like to add a QTreeView so users can click the + or > buttons to expand what is underneath the paths.
Here is my basic implementation:
from PyQt4 import QtGui
from PyQt4 import QtCore
class MainWindow(QtGui.QMainWindow):
def __init__(self, parent=None):
super(MainWindow, self).__init__(parent)
self.resize(600,400)
self.setWindowTitle("My Basic Treeview")
self.treeview = QtGui.QTreeView(self)
self.treeview.model = QtGui.QFileSystemModel()
self.treeview.model.setRootPath('/opt')
self.treeview.setModel(self.treeview.model)
self.treeview.setColumnWidth(0, 200)
self.setCentralWidget(self.treeview)
if __name__ == '__main__':
import sys
app = QtGui.QApplication(sys.argv)
w = MainWindow()
w.show()
sys.exit(app.exec_())
Although, in the above case, I get all folders but I just want the /opt path and its underneath folders.
import operator
from PyQt4.QtCore import *
from PyQt4.QtGui import *
class MyWindow(QWidget):
def __init__(self, data_list, header, *args):
QWidget.__init__(self, *args)
# setGeometry(x_pos, y_pos, width, height)
self.setGeometry(300, 200, 570, 450)
self.setWindowTitle("Click on column title to sort")
table_model = MyTableModel(self, data_list, header)
table_view = QTableView()
table_view.setModel(table_model)
# set font
font = QFont("Courier New", 14)
table_view.setFont(font)
# set column width to fit contents (set font first!)
table_view.resizeColumnsToContents()
# enable sorting
table_view.setSortingEnabled(True)
layout = QVBoxLayout(self)
layout.addWidget(table_view)
self.setLayout(layout)
class MyTableModel(QAbstractTableModel):
def __init__(self, parent, mylist, header, *args):
QAbstractTableModel.__init__(self, parent, *args)
self.mylist = mylist
self.header = header
def rowCount(self, parent):
return len(self.mylist)
def columnCount(self, parent):
return len(self.mylist[0])
def data(self, index, role):
if not index.isValid():
return None
elif role != Qt.DisplayRole:
return None
return self.mylist[index.row()][index.column()]
def headerData(self, col, orientation, role):
if orientation == Qt.Horizontal and role == Qt.DisplayRole:
return self.header[col]
return None
# the solvent data ...
header = ['Name', ' Email', ' Status', ' Path']
# use numbers for numeric data to sort properly
data_list = [
('option_A', 'zyro#email.com', 'Not Copied', '/Opt'),
('option_B', 'zyro#email.com', 'Not Copied', '/Users'),
]
app = QApplication([])
win = MyWindow(data_list, header)
win.show()
app.exec_()
Visual example :
I think your question can be divided in two parts:
how, in a QTreeView, the /opt path and its children can be shown, but without showing its siblings. In other words, how is it possible to show the root directory in a QTreeView ;
how can a QTreeView be added to a QTableView.
1. How to include the root directory in a QTreeView :
The root of a QTreeView is the directory for which the content is shown in the view. It is set when calling the method setRootIndex. According to a post by wysota on Qt Centre:
You can't display the invisibleRootItem because it is a fake item used only to have an equivalent of empty QModelIndex.
A workaround would be to set the root directory to the parent of /opt and filtering out the siblings of /opt with a subclass of a QSortFilterProxyModel. Note that I've also reimplemented the sizeHint method which will be necessary for the resizing of the rows of the QTableView:
from PyQt4 import QtGui, QtCore
import os
class MyQTreeView(QtGui.QTreeView):
def __init__(self, path, parent=None):
super(MyQTreeView, self).__init__(parent)
ppath = os.path.dirname(path) # parent of path
self.setFrameStyle(0)
#---- File System Model ----
sourceModel = QtGui.QFileSystemModel()
sourceModel.setRootPath(ppath)
#---- Filter Proxy Model ----
proxyModel = MyQSortFilterProxyModel(path)
proxyModel.setSourceModel(sourceModel)
#---- Filter Proxy Model ----
self.setModel(proxyModel)
self.setHeaderHidden(True)
self.setRootIndex(proxyModel.mapFromSource(sourceModel.index(ppath)))
#--- Hide All Header Sections Except First ----
header = self.header()
for sec in range(1, header.count()):
header.setSectionHidden(sec, True)
def sizeHint(self):
baseSize = super(MyQTreeView,self).sizeHint()
#---- get model index of "path" ----
qindx = self.rootIndex().child(0, 0)
if self.isExpanded(qindx): # default baseSize height will be used
pass
else: # shrink baseShize height to the height of the row
baseSize.setHeight(self.rowHeight(qindx))
return baseSize
class MyQSortFilterProxyModel(QtGui.QSortFilterProxyModel):
def __init__(self, path, parent=None):
super(MyQSortFilterProxyModel, self).__init__(parent)
self.path = path
def filterAcceptsRow(self, row, parent):
model = self.sourceModel()
path_dta = model.index(self.path).data()
ppath_dta = model.index(os.path.dirname(self.path)).data()
if parent.data() == ppath_dta:
if parent.child(row, 0).data() == path_dta:
return True
else:
return False
else:
return True
2. How to add a *QTreeView* to a *QTableView* :
It is possible to add a QTreeView to a QTableView by using a QItemDelegate. The post by Pavel Strakhov greatly helped me for this, since I had never used QTableView in combination with delegates before answering to this question. I always used QTableWidget instead with the setCellWidget method.
Note that I've setup a signal in the MyDelegate class which call the method resizeRowsToContents in the MyTableView class. This way, the height of the rows resize according the the reimplementation of the sizeHint method of the MyQTreeView class.
class MyTableModel(QtCore.QAbstractTableModel):
def __init__(self, parent, mylist, header, *args):
super(MyTableModel, self).__init__(parent, *args)
self.mylist = mylist
self.header = header
def rowCount(self, parent=QtCore.QModelIndex()):
return len(self.mylist)
def columnCount(self, parent=QtCore.QModelIndex()):
return len(self.mylist[0])
def data(self, index, role):
if not index.isValid():
return None
elif role != QtCore.Qt.DisplayRole:
return None
return self.mylist[index.row()][index.column()]
def headerData(self, col, orientation, role):
if orientation == QtCore.Qt.Horizontal and role == QtCore.Qt.DisplayRole:
return self.header[col]
return None
class MyDelegate(QtGui.QItemDelegate):
treeViewHeightChanged = QtCore.pyqtSignal(QtGui.QWidget)
def createEditor(self, parent, option, index):
editor = MyQTreeView(index.data(), parent)
editor.collapsed.connect(self.sizeChanged)
editor.expanded.connect(self.sizeChanged)
return editor
def sizeChanged(self):
self.treeViewHeightChanged.emit(self.sender())
class MyTableView(QtGui.QTableView):
def __init__(self, data_list, header, *args):
super(MyTableView, self).__init__(*args)
#---- set up model ----
model = MyTableModel(self, data_list, header)
self.setModel(model)
#---- set up delegate in last column ----
delegate = MyDelegate()
self.setItemDelegateForColumn(3, delegate)
for row in range(model.rowCount()):
self.openPersistentEditor(model.index(row, 3))
#---- set up font and resize calls ----
self.setFont(QtGui.QFont("Courier New", 14))
self.resizeColumnsToContents()
delegate.treeViewHeightChanged.connect(self.resizeRowsToContents)
3. Basic application :
Here is a basic application based on the code you provided in your OP:
if __name__ == '__main__':
header = ['Name', ' Email', ' Status', ' Path']
data_list = [('option_A', 'zyro#email.com', 'Not Copied', '/opt'),
('option_B', 'zyro#email.com', 'Not Copied', '/usr')]
app = QtGui.QApplication([])
win = MyTableView(data_list, header)
win.setGeometry(300, 200, 570, 450)
win.show()
app.exec_()
Which results in:
The code below creates QTreeWidget with five items.
The self.setDragDropMode(self.InternalMove) flag assures that when the item is dragged on top of
another there will be no copy of it made (so the number of items always stay the same).
If we replace this line with self.setDragDropMode(self.DragDrop) then every time an item is dragged/dropped a new copy of will be created.
Since I don't want a copy of item to be created on every dragAndDrop event I would be happy with InternalMove flag if it wouldn't be blocking QTreeWidget from accepting the drops from outside of its own view (if InternalMove flag is set QTreeWidget does not allow dragging-dropping from another QTreeWidget, QListView or File Browser).
Is there a way to set an override so QTreeWidget doesn't create a duplicate of dragged item and yet allows a dropping from outside of its own window.
from PyQt4 import QtCore, QtGui
app = QtGui.QApplication([])
class Tree(QtGui.QTreeWidget):
def __init__(self, *args, **kwargs):
super(Tree, self).__init__()
self.setDragEnabled(True)
self.setDropIndicatorShown(True)
self.setDragDropMode(self.InternalMove)
items=[QtGui.QTreeWidgetItem([name]) for name in ['Item_1','Item_2','Item_3','Item_4','Item_5']]
self.addTopLevelItems(items)
self.resize(360,240)
self.show()
tree=Tree()
sys.exit(app.exec_())
The key to solving this problem is that you have to implement when the object has moved to the next QListsWidget and check whether or not the data is duplicated or not. Take the data from source to destination by removing the source and adding this data to destination QListsWidget.
Use two methods, dragEnterEvent and dropEvent, to handle them all;
Implemented dragEnterEvent Check object to move is same QListsWidget.
Implemented dropEvent Check data is duplicated or not And take data from source to destination.
Example:
import sys
from PyQt4 import QtCore, QtGui
class QCustomTreeWidget (QtGui.QTreeWidget):
def __init__(self, parent = None):
super(QCustomTreeWidget, self).__init__(parent)
self.setDragEnabled(True)
self.setDragDropMode(QtGui.QAbstractItemView.DragDrop)
self.resize(360,240)
def dragEnterEvent (self, eventQDragEnterEvent):
sourceQCustomTreeWidget = eventQDragEnterEvent.source()
if isinstance(sourceQCustomTreeWidget, QCustomTreeWidget):
if self != sourceQCustomTreeWidget:
sourceQCustomTreeWidget.setDragDropMode(QtGui.QAbstractItemView.DragDrop)
eventQDragEnterEvent.accept()
else:
sourceQCustomTreeWidget.setDragDropMode(QtGui.QAbstractItemView.InternalMove)
QtGui.QTreeWidget.dragEnterEvent(self, eventQDragEnterEvent)
else:
QtGui.QTreeWidget.dragEnterEvent(self, eventQDragEnterEvent)
def dropEvent (self, eventQDropEvent):
sourceQCustomTreeWidget = eventQDropEvent.source()
if isinstance(sourceQCustomTreeWidget, QCustomTreeWidget):
if self != sourceQCustomTreeWidget:
sourceQCustomTreeWidget.setDragDropMode(QtGui.QAbstractItemView.DragDrop)
sourceQTreeWidgetItem = sourceQCustomTreeWidget.currentItem()
isFound = False
for column in range(0, self.columnCount()):
sourceQString = sourceQTreeWidgetItem.text(column)
listsFoundQTreeWidgetItem = self.findItems(sourceQString, QtCore.Qt.MatchExactly, column)
if listsFoundQTreeWidgetItem:
isFound = True
break
if not isFound:
(sourceQTreeWidgetItem.parent() or sourceQCustomTreeWidget.invisibleRootItem()).removeChild(sourceQTreeWidgetItem)
self.invisibleRootItem().addChild(sourceQTreeWidgetItem)
else:
sourceQCustomTreeWidget.setDragDropMode(QtGui.QAbstractItemView.InternalMove)
QtGui.QTreeWidget.dropEvent(self, eventQDropEvent)
else:
QtGui.QTreeWidget.dropEvent(self, eventQDropEvent)
class QCustomQWidget (QtGui.QWidget):
def __init__ (self, parent = None):
super(QCustomQWidget, self).__init__(parent)
self.my1QCustomTreeWidget = QCustomTreeWidget(self)
self.my2QCustomTreeWidget = QCustomTreeWidget(self)
items = [QtGui.QTreeWidgetItem([name]) for name in ['Item_1', 'Item_2', 'Item_3', 'Item_4', 'Item_5']]
self.my1QCustomTreeWidget.addTopLevelItems(items)
self.allQHBoxLayout = QtGui.QHBoxLayout()
self.allQHBoxLayout.addWidget(self.my1QCustomTreeWidget)
self.allQHBoxLayout.addWidget(self.my2QCustomTreeWidget)
self.setLayout(self.allQHBoxLayout)
app = QtGui.QApplication([])
myQCustomQWidget = QCustomQWidget()
myQCustomQWidget.show()
sys.exit(app.exec_())
Useful reference for event handle