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:
Related
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_())
What I'm trying to do, is to create a subclass of QTableView that will accept a standardized model (which is the result of a mysql query), and populate it. With it, I'd like to define a custom context menu which allows me to add or remove a row. This approach allows me to define the model and subclass once, and then call them both to populate them with a number of different queries without duplicating code.
The add_row and remove_row QActions would essentially call the model methods insertRows and removeRows.
Here's a functioning example (apart from the mariadb module which is separate):
from PySide2 import QtWidgets, QtCore, Qt, QtGui
import maria_db
import numpy
app = QtWidgets.QApplication([])
#Define model for tablePersonnel
class TableModel(QtCore.QAbstractTableModel):
def __init__(self, db,query,tableName,id):
super(TableModel, self).__init__()
self.db=db
self.query=query
self.tableName=tableName
self.id=id
self.pullData()
def pullData(self):
self.datasheet=self.db.RunQuery(self.query)
self.rows = numpy.array(self.datasheet.fetchall())
self.headers = numpy.array(self.datasheet.column_names)
self.idIndex=int(numpy.where(self.headers==self.id)[0])
def data(self, index, role):
if role == QtCore.Qt.DisplayRole:
return str(self.rows[index.row()][index.column()])
def rowCount(self, parent):
return len(self.rows)
def columnCount(self, parent):
return len(self.headers)
def headerData(self, section, orientation, role=QtCore.Qt.DisplayRole):
if orientation == QtCore.Qt.Horizontal and role == QtCore.Qt.DisplayRole:
return self.headers[section]
def flags(self,index):
if index.column()!=self.idIndex:
return QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsEditable | QtCore.Qt.ItemIsSelectable
else:
return QtCore.Qt.ItemIsSelectable
def setData(self, index,value,role=QtCore.Qt.EditRole):
if index.isValid():
sel_id=self.rows[index.row()][0]
sel_column=self.headers[index.column()]
if self.db.UpdateCell(self.tableName,sel_id,self.id,sel_column,value)==True:
self.rows[self.idFinder(self.rows,sel_id)][1]=value
self.dataChanged.emit(index,index)
return True
def insertRows(self):
print("inserting row!!!")
def removeRows(self):
pass
#find index of selected ID
def idFinder(self,data, search):
for i in range(len(data)):
for j in range(len(data[i])):
if data[i][j] == search:
return i
else:
return False
class EditTable(QtWidgets.QTableView):
def __init__(self):
super(EditTable, self).__init__()
self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
self.customContextMenuRequested.connect(self.context_menu)
def context_menu(self):
context=QtWidgets.QMenu()
add_row=context.addAction("Add row")
add_row.triggered.connect(lambda: self.personnelTableModel.insertRows())
rem_row=context.addAction("Remove row")
rem_row.triggered.connect(lambda: self.personnelTableModel.removeRows())
cursor=QtGui.QCursor()
context.exec_(cursor.pos())
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
super(MainWindow, self).__init__()
widget = QtWidgets.QWidget()
layout = QtWidgets.QVBoxLayout()
widget.setLayout(layout)
button = QtWidgets.QPushButton("Push me")
self.db = maria_db.ConnectDb("localhost", "root", "1234", "test")
self.personnelTableModel = TableModel(self.db,"SELECT * FROM personnel","personnel","personnel_id")
table = EditTable()
table.setModel(self.personnelTableModel)
layout.addWidget(table)
layout.addWidget(button)
self.setCentralWidget(widget)
window = MainWindow()
window.show()
app.exec_()
So my problem is that when I call add_row.triggered.connect(lambda: self.personnelTableModel.insertRows()), I get a 'EditTable' object has no attribute 'personnelTableModel' which essentially means that there is no method insertRows in self. How can I refer to the model assigned to the object that is constructed using the subclass of the QTableView EditTable, through the subclass itself? Am I going about this the wrong way?
What is personnelTableModel for EditTable? the model, and how can I access the model from the view? Using the model() method. On the other hand, a lambda method is not necessary.
def context_menu(self):
context=QtWidgets.QMenu()
add_row=context.addAction("Add row")
add_row.triggered.connect(self.model().insertRows)
rem_row=context.addAction("Remove row")
rem_row.triggered.connect(self.model().removeRows)
cursor=QtGui.QCursor()
context.exec_(cursor.pos())
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'm trying to implement the QAbstractListModel class in order to display several similar widgets.
The following code shows my problem:
import sys
from PyQt4 import QtCore
from PyQt4 import QtGui
class Model(QtCore.QAbstractListModel):
def __init__(self, parent=None):
super(QtCore.QAbstractListModel, self).__init__(parent)
self._widgets = []
def headerData(self, section, orientation, role):
""" Returns header for columns """
return "bla"
def rowCount(self, parentIndex=QtCore.QModelIndex()):
""" Returns number of interfaces """
return len(self._widgets)
def data(self, index, role):
""" Returns the data to be displayed """
if role == QtCore.Qt.DisplayRole:
row = index.row()
return self._widgets[row]
def insertRow(self, widget, parentIndex=QtCore.QModelIndex()):
""" Inserts a row into the model """
self.beginInsertRows(parentIndex, 0, 1)
self._widgets.append(widget)
self.endInsertRows()
class Widget(QtGui.QWidget):
def __init__(self, parent=None, name="None"):
super(QtGui.QWidget, self).__init__(parent)
self.layout = QtGui.QHBoxLayout()
self.setLayout(self.layout)
self.checkbox = QtGui.QCheckBox()
self.button = QtGui.QPushButton(self)
self.label = QtGui.QLabel(self)
self.label.setText(name)
self.layout.addWidget(self.checkbox)
self.layout.addWidget(self.button)
self.layout.addWidget(self.label)
class Window(QtGui.QMainWindow):
def __init__(self, parent=None):
super(QtGui.QMainWindow, self).__init__(parent)
self.view = QtGui.QListView(self)
self.model = Model()
self.view.setModel(self.model)
self.setCentralWidget(self.view)
self.model.insertRow(
widget=Widget(self)
)
self.model.insertRow(
widget=Widget(self)
)
self.model.insertRow(
widget=Widget(self)
)
self.model.insertRow(
widget=Widget(self)
)
self.show()
app = QtGui.QApplication(sys.argv)
window = Window()
sys.exit(app.exec_())
It works, there are four clickable entries in the list, but in only one of them the widget is actually displayed. Why is that?
I guess this behaviour is either caused by data() or how I'm using beginInsertRows(), but I can't figure out where the error is.
ìnsertRow() function now looks like that
def insertRow(self, widget, parentIndex=QtCore.QModelIndex()):
""" Inserts a row into the model """
self.beginInsertRows(parentIndex, len(self._widgets), len(self._widgets))
self._widgets.append(widget)
self.endInsertRows()
but it still does not work.
Actually, the four widgets are displayed. The issue is, they're all displayed in the top left corner. You can see they overlap if you put names, here "1", "2", "3", "4":
Fixing the row numbers doesn't solve the issue. The widgets will be in the correct rows but will still be displayed on the top left. It is because data is supposed to return text for the display role.
To display simple widgets, I suggest using QListWidget and the setItemWidget method. Otherwise you'll have to use delegates.
self.beginInsertRows(parentIndex, 0, 1)
0 - start
1 - end
You refresh only first row during insert
self.beginInsertRows(parentIndex, len(self._widgets), len(self._widgets))
must be work. Don't remember this method
self.model.reset()
this refresh all list, without specific rows
I really like a clean result of self-adjusting to the QTableView's width of the columns using:
self.view.horizontalHeader().setResizeMode(QHeaderView.Stretch)
But unfortunately with this flag used the columns width doesn't stay adjustable any longer (the user is not able to resize the columns width).
I wonder if there is an alternative way to set the columns width to the width of the QTableView and yet keeping the columns width "user-adjustable"?
from PyQt4.QtCore import *
from PyQt4.QtGui import *
import sys
class Model(QAbstractTableModel):
def __init__(self, parent=None, *args):
QAbstractTableModel.__init__(self, parent, *args)
self.items = ['Item_A_001','Item_A_002','Item_B_001','Item_B_002']
self.totalColumn=10
def rowCount(self, parent=QModelIndex()):
return len(self.items)
def columnCount(self, parent=QModelIndex()):
return self.totalColumn
def setColumnCount(self, number):
self.totalColumn=number
self.reset()
def data(self, index, role):
if not index.isValid(): return QVariant()
elif role != Qt.DisplayRole:
return QVariant()
row=index.row()
if row<len(self.items):
return QVariant(self.items[row])
else:
return QVariant()
class MyWindow(QWidget):
def __init__(self, *args):
QWidget.__init__(self, *args)
tableModel=Model(self)
self.view=QTableView(self)
self.view.setModel(tableModel)
self.view.horizontalHeader().setResizeMode(QHeaderView.Stretch)
hideButton=QPushButton('Hide Column')
hideButton.clicked.connect(self.hideColumn)
unhideButton=QPushButton('Unhide Column')
unhideButton.clicked.connect(self.unhideColumn)
layout = QVBoxLayout(self)
layout.addWidget(self.view)
layout.addWidget(hideButton)
layout.addWidget(unhideButton)
self.setLayout(layout)
def hideColumn(self):
self.view.model().setColumnCount(1)
def unhideColumn(self):
self.view.model().setColumnCount(10)
if __name__ == "__main__":
app = QApplication(sys.argv)
w = MyWindow()
w.show()
sys.exit(app.exec_())
so here is a little trick I came up with. I modified the resize event of MyWindow to resize your QTableWidget.
class MyWindow(QWidget):
def __init__(self, *args):
QWidget.__init__(self, *args)
self.tableModel=Model(self) #Set the model as part of your class to access it in the event handler
self.view=QTableView(self)
self.view.setModel(self.tableModel) #Modified here too
self.view.horizontalHeader().setResizeMode(QHeaderView.Interactive) #Mode set to Interactive to allow resizing
hideButton=QPushButton('Hide Column')
hideButton.clicked.connect(self.hideColumn)
unhideButton=QPushButton('Unhide Column')
unhideButton.clicked.connect(self.unhideColumn)
layout = QVBoxLayout(self)
layout.addWidget(self.view)
layout.addWidget(hideButton)
layout.addWidget(unhideButton)
self.setLayout(layout)
def hideColumn(self):
self.view.model().setColumnCount(1)
def unhideColumn(self):
self.view.model().setColumnCount(10)
#Added a reimplementation of the resize event
def resizeEvent(self, event):
tableSize = self.view.width() #Retrieves your QTableView width
sideHeaderWidth = self.view.verticalHeader().width() #Retrieves the left header width
tableSize -= sideHeaderWidth #Perform a substraction to only keep all the columns width
numberOfColumns = self.tableModel.columnCount() #Retrieves the number of columns
for columnNum in range( self.tableModel.columnCount()): #For each column
self.view.setColumnWidth(columnNum, int(tableSize/numberOfColumns) ) #Set the width = tableSize / nbColumns
super(MyWindow, self).resizeEvent(event) #Restores the original behaviour of the resize event
The only downfall is that the scrollbar is flicking. I think this can be solved with some adjustment.
Update:
Get a cleaner look when resizing, no more flicking, disabled the scrollbar.
Modified initialization of the QTableView:
self.view=QTableView(self)
self.view.setModel(self.tableModel)
self.view.horizontalHeader().setResizeMode(QHeaderView.Interactive)
#Added these two lines
self.view.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.view.horizontalHeader().setStretchLastSection(True)
Modified resizeEvent:
def resizeEvent(self, event):
super(MyWindow, self).resizeEvent(event)
tableSize = self.view.width()
sideHeaderWidth = self.view.verticalHeader().width()
tableSize -= sideHeaderWidth
numberOfColumns = self.tableModel.columnCount()
remainingWidth = tableSize % numberOfColumns
for columnNum in range( self.tableModel.columnCount()):
if remainingWidth > 0:
self.view.setColumnWidth(columnNum, int(tableSize/numberOfColumns) + 1 )
remainingWidth -= 1
else:
self.view.setColumnWidth(columnNum, int(tableSize/numberOfColumns) )