I get this behavior only on linux. On windows this problem does not happen.
I have a table using the model/view framework. One of the columns is editable, and when I enter it to change the data the old data is still visible in the background while I edit the data in the foreground.
I'm not sure what is causing this, I've tried flipping various settings, but I've been unable to change the behavior.
Maybe a simpler question that will still help me: I'm correct to be looking in the view code for this issue correct? Would the model possibly have anything to do with this? Do I need to set any currently editing flags in the model?
Assuming that the view is where the issue is here is most of the view logic I'm using. There are some caveats to the following code: I'm injecting common code used between QTableViews and QTreeViews, so there are a few functions this class has that are no explicitly listed as methods:
from __future__ import absolute_import, division, print_function
from guitool.__PYQT__ import QtCore, QtGui
from guitool import api_item_view
from guitool.guitool_decorators import signal_, slot_
API_VIEW_BASE = QtGui.QTableView
class APITableView(API_VIEW_BASE):
rows_updated = signal_(str, int)
contextMenuClicked = signal_(QtCore.QModelIndex, QtCore.QPoint)
API_VIEW_BASE = API_VIEW_BASE
def __init__(view, parent=None):
API_VIEW_BASE.__init__(view, parent)
api_item_view.injectviewinstance(view)
view._init_table_behavior()
view._init_header_behavior()
view.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
view.customContextMenuRequested.connect(view.on_customMenuRequested)
#---------------
# Initialization
#---------------
def _init_table_behavior(view):
view.setCornerButtonEnabled(False)
view.setWordWrap(True)
view.setSortingEnabled(True)
view.setShowGrid(True)
# Selection behavior #view.setSelectionBehavior(QtGui.QAbstractItemView.SelectColumns)
view.setSelectionBehavior(QtGui.QAbstractItemView.SelectItems)
view._defaultEditTriggers = QtGui.QAbstractItemView.AllEditTriggers
view.setEditTriggers(view._defaultEditTriggers)
view.setIconSize(QtCore.QSize(64, 64))
def _init_header_behavior(view):
""" Header behavior """
# Row Headers
verticalHeader = view.verticalHeader()
verticalHeader.setVisible(True)
#verticalHeader.setSortIndicatorShown(True)
verticalHeader.setHighlightSections(True)
verticalHeader.setResizeMode(QtGui.QHeaderView.Interactive)
verticalHeader.setMovable(True)
# Column headers
horizontalHeader = view.horizontalHeader()
horizontalHeader.setVisible(True)
horizontalHeader.setStretchLastSection(True)
horizontalHeader.setSortIndicatorShown(True)
horizontalHeader.setHighlightSections(True)
# Column Sizes
# DO NOT USE ResizeToContents. IT MAKES THINGS VERY SLOW
#horizontalHeader.setResizeMode(QtGui.QHeaderView.ResizeToContents)
#horizontalHeader.setResizeMode(QtGui.QHeaderView.Stretch)
horizontalHeader.setResizeMode(QtGui.QHeaderView.Interactive)
#horizontalHeader.setCascadingSectionResizes(True)
# Columns moveable
horizontalHeader.setMovable(True)
#---------------
# Qt Overrides
#---------------
def setModel(view, model):
""" QtOverride: Returns item delegate for this index """
api_item_view.setModel(view, model)
def keyPressEvent(view, event):
assert isinstance(event, QtGui.QKeyEvent)
API_VIEW_BASE.keyPressEvent(view, event)
if event.matches(QtGui.QKeySequence.Copy):
#print('Received Ctrl+C in View')
view.copy_selection_to_clipboard()
#print ('[view] keyPressEvent: %s' % event.key())
def mouseMoveEvent(view, event):
assert isinstance(event, QtGui.QMouseEvent)
API_VIEW_BASE.mouseMoveEvent(view, event)
def mousePressEvent(view, event):
assert isinstance(event, QtGui.QMouseEvent)
API_VIEW_BASE.mousePressEvent(view, event)
#print('no editing')
view.setEditTriggers(QtGui.QAbstractItemView.NoEditTriggers)
def mouseReleaseEvent(view, event):
assert isinstance(event, QtGui.QMouseEvent)
#print('editing ok')
view.setEditTriggers(view._defaultEditTriggers)
API_VIEW_BASE.mouseReleaseEvent(view, event)
def clearSelection(view, *args, **kwargs):
print('[table_view] clear selection')
API_VIEW_BASE.clearSelection(view, *args, **kwargs)
#---------------
# Slots
#---------------
#slot_(str, int)
def on_rows_updated(view, tblname, num):
# re-emit the model signal
view.rows_updated.emit(tblname, num)
#slot_(QtCore.QPoint)
def on_customMenuRequested(view, pos):
index = view.indexAt(pos)
view.contextMenuClicked.emit(index, pos)
# ----
# Injected funcs from api_item_view
#register_view_method
def infer_delegates(view, **headers):
""" Infers which columns should be given item delegates """
get_thumb_size = headers.get('get_thumb_size', None)
col_type_list = headers.get('col_type_list', [])
num_cols = view.model().columnCount()
num_duplicates = int(num_cols / len(col_type_list))
col_type_list = col_type_list * num_duplicates
for colx, coltype in enumerate(col_type_list):
if coltype in qtype.QT_PIXMAP_TYPES:
if VERBOSE:
print('[view] colx=%r is a PIXMAP' % colx)
thumb_delegate = APIThumbDelegate(view, get_thumb_size)
view.setItemDelegateForColumn(colx, thumb_delegate)
elif coltype in qtype.QT_BUTTON_TYPES:
if VERBOSE:
print('[view] colx=%r is a BUTTON' % colx)
button_delegate = APIButtonDelegate(view)
view.setItemDelegateForColumn(colx, button_delegate)
else:
if VERBOSE:
print('[view] colx=%r does not have a delgate' % colx)
#register_view_method
def set_column_persistant_editor(view, column):
""" Set each row in a column as persistant """
num_rows = view.model.rowCount()
print('view.set_persistant: %r rows' % num_rows)
for row in range(num_rows):
index = view.model.index(row, column)
view.view.openPersistentEditor(index)
The item-delegate editor needs to paint its own background:
editor.setAutoFillBackground(True)
Obviously, this has to be fixed within the custom delegate classes (APIThumbDelegate and/or APIButtonDelegate) which create the editor widgets (i.e. via their createEditor functions).
Related
I'm trying to build an application with PyQT6 that allows users to browse through a list of images with thumbnails and display the selected image in an image viewer. The application can also add and delete images. Adding images seems to work fine, but when I delete an image from the model the row in the QListView suddenly displays the data from the next row in the list. After a random interval of anywhere between half a second and about five seconds the row will actually be removed, and the list will display the proper file ordering. The fact that this behavior occurs makes me think I'm not removing the item from the model properly, and ideally I'd like the deletion of a row to be instantaneous.
Here is my minimum reproducible example:
import PyQt6 as qt
import PyQt6.QtCore as QtCore
from PyQt6.QtCore import Qt, QAbstractListModel, QModelIndex
from PyQt6.QtGui import *
from PyQt6.QtWidgets import *
import os
import sys
import traceback
class ImageDataModel(QAbstractListModel):
def __init__(self, images=None):
super(ImageDataModel, self).__init__()
if images is None:
self.images = []
else:
self.images = images
self.thumbnails = []
for img_path in self.images:
icon = QPixmap(img_path).scaledToHeight(20)
self.thumbnails.append(icon)
def data(self, index, role):
if role == Qt.ItemDataRole.DisplayRole:
img_path = self.images[index.row()]
return img_path
if role == Qt.ItemDataRole.DecorationRole:
thumbnail = self.thumbnails[index.row()]
return thumbnail
def rowCount(self, index):
return len(self.images)
def removeRow(self, index):
self.images.pop(index)
self.thumbnails.pop(index)
class myListView(QListView):
def __init__(self, parent=None):
super().__init__()
self.parent = parent
self.setSelectionMode(QListView.SelectionMode.ExtendedSelection)
def currentChanged(self, current: QtCore.QModelIndex, previous: QtCore.QModelIndex) -> None:
if (current.row() >= 0):
self.parent.get_selection(current) # this method simply displays the selected image
return super().currentChanged(current, previous)
class MyMenu(QMainWindow):
def __init__(self):
super().__init__()
self.layout = QHBoxLayout()
self.list = myListView(self)
try:
image_file_list = [x for x in os.listdir('path/to/image/directory') if x.lower().endswith(".png")]
except:
image_file_list = []
image_file_list.sort()
self.model = ImageDataModel(image_file_list)
self.list.setModel(self.model)
self.list.clicked.connect(self.get_selection) # this method simply displays the selected image
self.list.setCurrentIndex(self.model.index(0,0))
self.layout.addWidget(self.list)
self.widget = QWidget()
self.widget.setLayout(self.layout)
self.setCentralWidget(self.widget)
# Deletes the currently displayed image and annotation from the dataset
def delete_image(self):
# Determine what to set the new index to after deletion
if self.list.currentIndex().row() != 0:
new_index = self.list.currentIndex().row() - 1
else:
new_index = 0
# Attempt to remove the row and delete the file
try:
self.list.model().removeRow(self.list.currentIndex().row())
os.remove(self.img_path)
# Set index row to the image immediately preceding the deleted image
index = self.model.createIndex(new_index, 0)
self.list.setCurrentIndex(index)
except:
traceback.print_exc()
# Replaced display code for brevity
def get_selection(self, item):
print(item.row())
# Handles keypresses
def keyPressEvent(self, e) -> None:
global model_enabled
if (e.key() == Qt.Key.Key_Escape):
app.quit()
if (e.key() == Qt.Key.Key_Delete):
self.delete_image()
def main():
app = QApplication(sys.argv)
window = MyMenu()
window.show()
app.exec()
main()
Any change in the size, order/layout and data of a model should always be done using the proper function calls so that the views linked to the model get proper notifications about those changes.
For size and layout changes, it's important to always call the begin* and end* functions, which allows the view to be notified about the upcoming change, so they can keep a persistent list of the current items (including selection) and restore it when the change is completed.
Row removal is achieved using beginRemoveRows() and endRemoveRows().
In your case:
def removeRow(self, index):
self.beginRemoveRows(QModelIndex(), index, index)
self.images.pop(index)
self.thumbnails.pop(index)
self.endRemoveRows()
return True # <- the function expects a bool in return
Note that the correct way to implement row removal is done by implementing removeRows(), not removeRow() (singular), which internally calls removeRows anyway. So, you can leave the existing removeRow() call, do not override removeRow() and implement removeRows() instead.
def removeRows(self, row, count, parent=QModelIndex()):
if row + count >= len(self.images) or count < 1:
return False
self.beginRemoveRows(parent, row, row + count - 1)
del self.images[row:row+count]
del self.thumbnails[row:row+count]
self.endRemoveRows()
return True
A similar concept should always be done when adding new items after a view is linked to the model; in that case, implement insertRows() and there you'll call beginInsertRows() insert the new data and finally call endInsertRows().
Note that your code will throw an exception if the images is None, as it doesn't create the thumbnails object.
I am trying to have a series of QTableView created at runtime and added to newly created pages of a multipage QTabWidget.
All seems to go fine, but the QTableView don't show up.
The QTabWidget gets zeroed (reset to no pages) and refurbished (...) flawlessly (at least it looks like so) depending on the selection of a combobox (and the dictionaries therein related).
I am also using a delegate callback to include a column of checkboxes to the QTableView (thanks to https://stackoverflow.com/a/50314085/7710452), which works fine stand alone.
Here is the code.
Main Window
EDIT
as recommended by eyllanesc, here is the standalone module (jump to the end of the post for details on the part I think is problematic):
"""
qt5 template
"""
import os
import sys
from PyQt5 import QtWidgets as qtw
from PyQt5 import QtCore as qtc
from PyQt5 import QtGui as qtg
from PyQt5 import uic
from configparser import ConfigParser, ExtendedInterpolation
from lib.SearchControllers import findGuis, get_controller_dict, show_critical, show_exception
import resources.resources
from lib.CheckBoxesDelegate import CheckBoxDelegate
myForm_2, baseClass = uic.loadUiType('./forms/setup.ui')
class MainWindow(baseClass):
def __init__(self, config_obj: ConfigParser,
config_name: str,
proj_name: str,
*args,
**kwargs):
super().__init__(*args, **kwargs)
self.ui = myForm_2()
self.ui.setupUi(self)
# your code begins here
self.setWindowTitle(proj_name + " Setup")
self.ui.logo_lbl.setPixmap(qtg.QPixmap(':/logo_Small.png'))
self.config_obj = config_obj
self.config_name = config_name
self.proj_filename = proj_name
self.proj_config = ConfigParser(interpolation=ExtendedInterpolation())
self.proj_config.read(proj_name)
self.guis_dict = {}
self.components = {}
self.cdp_signals = {}
self.root_path = self.config_obj['active']['controllers']
self.tableViews = []
self.tabs = []
self.iniControllersBox()
self.setActSignals()
self.load_bulk()
self.set_signals_table()
self.update_CurController_lbl()
self.update_ControllersTab() # here is where the action gets hot
# your code ends here
self.show() # here crashes if I passed the new tab to the instance of
# QTabView. otherwise it shows empty tabs
#########################################################
def load_bulk(self):
# get the list of running components into a dictionary
for i in self.list_controllers:
i_path = os.path.join(self.root_path, i)
print(i)
self.components[i] = get_controller_dict(i_path,
self.config_obj,
'Application.xml',
'Subcomponents/Subcomponent',
'Name',
'src')
for j in self.components[i]:
print(j)
signals_key = (i , j)
tgt = os.path.join(self.root_path, self.components[i][j])
self.cdp_signals[signals_key] = get_controller_dict(i_path,
self.config_obj,
self.components[i][j],
'Signals/Signal',
'Name',
'Type',
'Routing')
def set_signals_table(self):
self.ui.MonitoredDevicesTable.setHorizontalHeaderItem(0, qtw.QTableWidgetItem('GUI caption'))
self.ui.MonitoredDevicesTable.setHorizontalHeaderItem(1, qtw.QTableWidgetItem('Monitored Signal'))
def setActSignals(self):
self.ui.controllersBox.currentIndexChanged.connect(self.update_guis_list)
self.ui.controllersBox.currentIndexChanged.connect(self.update_CurController_lbl)
self.ui.controllersBox.currentIndexChanged.connect(self.update_ControllersTab)
def update_ControllersTab(self):
self.ui.componentsTab.clear() # this is the QTabWidget
self.tabs = []
self.tableViews = []
curr_controller = self.ui.controllersBox.currentText()
for i in self.components[curr_controller]:
if len(self.cdp_signals[curr_controller, i]) == 0:
continue
self.tabs.append(qtw.QWidget())
tabs_index = len(self.tabs) - 1
header_labels = ['', 'Signal', 'Type', 'Routing', 'Input']
model = qtg.QStandardItemModel(len(self.cdp_signals[curr_controller, i]), 5)
model.setHorizontalHeaderLabels(header_labels)
# in the next line I try to create a new QTableView passing
# the last tab as parameter, in the attempt to embed the QTableView
# into the QWidget Tab
self.tableViews.append(qtw.QTableView(self.tabs[tabs_index]))
tbw_Index = len(self.tableViews) - 1
self.tableViews[tbw_Index].setModel(model)
delegate = CheckBoxDelegate(None)
self.tableViews[tbw_Index].setItemDelegateForColumn(0, delegate)
rowCount = 0
for row in self.cdp_signals[curr_controller, i]:
for col in range(len(self.cdp_signals[curr_controller, i][row])):
index = model.index(rowCount, col, qtc.QModelIndex())
model.setData(index, self.cdp_signals[curr_controller, i][row][col])
try:
self.ui.componentsTab.addTab(self.tabs[tabs_index], i) # no problems, some controllers ask up to
except Exception as ex:
print(ex)
def update_CurController_lbl(self):
self.ui.active_controller_lbl.setText(self.ui.controllersBox.currentText())
def iniControllersBox(self):
self.list_controllers = [os.path.basename(f.path) for f in os.scandir(self.root_path) if f.is_dir() and str(
f.path).upper().endswith('NC')]
self.ui.controllersBox.addItems(self.list_controllers)
for i in range(self.ui.controllersBox.count()):
self.ui.controllersBox.setCurrentIndex(i)
newKey = self.ui.controllersBox.currentText()
cur_cntrlr = os.path.join(self.config_obj['active']['controllers'], self.ui.controllersBox.currentText())
self.guis_dict[newKey] = findGuis(cur_cntrlr, self.config_obj)
self.ui.controllersBox.setCurrentIndex(0)
self.update_guis_list()
def update_guis_list(self, index=0):
self.ui.GuisListBox.clear()
self.ui.GuisListBox.addItems(self.guis_dict[self.ui.controllersBox.currentText()])
if __name__ == '__main__':
config = ConfigParser()
config.read('./config.ini')
app = qtw.QApplication([sys.argv])
w = MainWindow(config, './config.ini',
'./test_setup_1.proj')
sys.exit(app.exec_())
and here the external to add the checkboxes column:
class CheckBoxDelegate(QtWidgets.QItemDelegate):
"""
A delegate that places a fully functioning QCheckBox cell of the column to which it's applied.
"""
def __init__(self, parent):
QtWidgets.QItemDelegate.__init__(self, parent)
def createEditor(self, parent, option, index):
"""
Important, otherwise an editor is created if the user clicks in this cell.
"""
return None
def paint(self, painter, option, index):
"""
Paint a checkbox without the label.
"""
self.drawCheck(painter, option, option.rect, QtCore.Qt.Unchecked if int(index.data()) == 0 else QtCore.Qt.Checked)
def editorEvent(self, event, model, option, index):
'''
Change the data in the model and the state of the checkbox
if the user presses the left mousebutton and this cell is editable. Otherwise do nothing.
'''
if not int(index.flags() & QtCore.Qt.ItemIsEditable) > 0:
return False
if event.type() == QtCore.QEvent.MouseButtonRelease and event.button() == QtCore.Qt.LeftButton:
# Change the checkbox-state
self.setModelData(None, model, index)
return True
if event.type() == QtCore.QEvent.MouseButtonPress or event.type() == QtCore.QEvent.MouseMove:
return False
return False
def setModelData (self, editor, model, index):
'''
The user wanted to change the old state in the opposite.
'''
model.setData(index, 1 if int(index.data()) == 0 else 0, QtCore.Qt.EditRole)
The 1st picture shows the layout in QTDesigner, the 2nd the result (emtpy tabs) when avoiding the crashing.
the QTabWidget has no problems in zeroing, or scale up, back to as many tab as I need, it's just that I have no clue on how to show the QTabview. My approach was to try to embed the QTabView in the tabpage passing it as parameter to the line creating the new QTabView.
Since I am using rather convoluted dictionaries, calling an XML parser to fill them up, not to mention the config files, I know even this version of my script is hardly reproduceable/runnable.
If someone had the patience of focusing on the update_ControllersTab method though, and tell me what I am doing wrong handling the QWidgets, it'd be great.
Again the basic idea is to clear the QTabWidget any time the user selects a different controller (combo box on the left):
self.ui.componentsTab.clear() # this is the QTabWidget
self.tabs = [] # list to hold QTabView QWidgets (pages) throughout the scope
self.tableViews = [] # list to hold QTabView(s) thorughout the scope
count how many tabs (pages) and hence embedded TabViews I need with the new controllers selected.
and then for each tab needed:
create a new tab (page)
self.tabs.append(qtw.QWidget())
tabs_index = len(self.tabs) - 1
create a new QTabView using a model:
header_labels = ['', 'Signal', 'Type', 'Routing', 'Input']
model = qtg.QStandardItemModel(len(self.cdp_signals[curr_controller, i]), 5)
model.setHorizontalHeaderLabels(header_labels)
self.tableViews.append(qtw.QTableView(self.tabs[tabs_index]))
tbw_Index = len(self.tableViews) - 1
self.tableViews[tbw_Index].setModel(model)
populate the TableView with data, and then finally add the tab widget (with the suppposedly embedded QTableView to the QTabWidget (the i argument is a string from my dbases Names:
self.ui.componentsTab.addTab(self.tabs[tabs_index], i)
This method is called also by the __init__ to initialize and apparently all goes error free, until the last 'init' statement:
`self.show()`
at which point the app crashes with:
Process finished with exit code 1073741845
on the other hand, if here instead of trying to embed the QTableView:
self.tableViews.append(qtw.QTableView(self.tabs[tabs_index]))
I omit the parameter, that is:
self.tableViews.append(qtw.QTableView())
the app doesn't crash anymore, but of course no QtableViews are shown, only empty tabpages:
As stupid as this may sound the problem is in... the delegate class that creates the checkboxes in the first column (see https://stackoverflow.com/a/50314085/7710452)
I commented out those two lines:
delegate = CheckBoxDelegate(None)
self.tableViews[tbw_Index].setItemDelegateForColumn(0, delegate)
and... bingo!
the CheckBoxDelegate works fine in the example shown in the post (a single QTableView form). I also tinkered around adding columns and rows, and moving the checkbox column back and forth with no problems. In that standalone. But as soon as I add the class and set the delegate, i am back at square zero, app crashing with:
Process finished with exit code 1073741845
so I am left with this problem now. Thnx to whomever read this.
Problem solved, see comment to post above.
I would like to render each item in a QTreeView differently based on a number of attributes stored in a database and based on whether the item is a folder or a file. However, I don't understand how the QTreeView or QFileSystemModel communicate with the delegate. Whenever an item must be drawn, including during initialization, I'd expect to provide the delegate with all the parameters it requires and then use a series of if statements within the delegate to set how the particular item is drawn. I've only found the .setItemDelegate method and don't know when or how the delegate is actually called or how it loops through all the items in the model. Below is an example based on material online. There are two problems:
I placed code in comments that I was unable to get working. Once I understand how the delegate can receive information from the QTreeView (or calling class), I believe I can do the rest.
I was unable to get this subclass of the QTreeView to display the folder and file icons.
Code:
import sys
from PySide.QtCore import *
from PySide.QtGui import *
class fileSystemDelegate(QItemDelegate):
def __init__(self, parent=None):
QItemDelegate.__init__(self, parent) #shouldn't this insure the icons are drawn?
def paint(self, painter, option, index):
painter.save()
# set background
painter.setPen(QPen(Qt.NoPen))
if option.state & QStyle.State_Selected: #DURING DRAW LOOP: idx = self.currentIndex(); if self.fileSystemModel.isDir(idx): PAINT RED
painter.setBrush(QBrush(Qt.red))
else:
painter.setBrush(QBrush(Qt.white)) #ELSE PAINT WHITE
painter.drawRect(option.rect)
# draw item
painter.setPen(QPen(Qt.black))
text = index.data(Qt.DisplayRole)
painter.drawText(option.rect, Qt.AlignLeft, text) #there is no painter.drawIcon?
painter.restore()
class fileSystemBrowser(QTreeView):
def __init__(self, parent=None):
super().__init__(parent)
delegate = fileSystemDelegate()
self.setItemDelegate(delegate) # how to provide delegate with additional info about the item to be drawn ?
self.fileSystemModel = QFileSystemModel()
self.fileSystemModel.setRootPath(QDir.currentPath())
self.setModel(self.fileSystemModel)
if __name__ == '__main__':
app = QApplication(sys.argv)
window = fileSystemBrowser()
window.show()
sys.exit(app.exec_())
EDIT 1:
I've added an example "database" in the form of a dictionary and changed the approach to rely on the data method rather than the delegate. I would expect this code to perform the dictionary lookup whenever information is displayed in the tree and therefore print to the terminal when the user enters C:\Program Files\Internet Explorer\ on a Microsoft Windows computer. However, it just displays the directory without printing anything to the terminal. I'd like to know:
How do I get if statements in the data method to trigger for every item in the display as they are being drawn?
How can I display an icon after the default icon is displayed, on the same row?
Code:
import sys
from PySide.QtCore import *
from PySide.QtGui import *
database = {'C:\Program Files\Internet Explorer\ExtExport.exe':(1,3), 'C:\Program Files\Internet Explorer\iexplore.exe':(0,0)}
class fileSystemBrowser(QTreeView):
def __init__(self, parent=None):
super().__init__(parent)
self.fileSystemModel = QFileSystemModel()
self.fileSystemModel.setRootPath(QDir.currentPath())
self.setModel(self.fileSystemModel)
def data(self, index, role=Qt.DisplayRole):
if index.isValid():
path = self.fileSystemModel.filePath(index)
if self.fileSystemModel.isDir(index):
if database.get(path) != None:
if database[path][0] > 0:
print("Acting on custom data 0.") # add another icon after the regular folder icon
if database[path][1] > 0:
print("Acting on custom data 1.") # add another (different) icon after the regular folder or previous icon
if __name__ == '__main__':
app = QApplication(sys.argv)
window = fileSystemBrowser()
window.show()
sys.exit(app.exec_())
EDIT 2:
Subclassing the model definitely did make a difference. Now the script appears to be calling my new data method on every item. Unfortunately, the data method doesn't work yet so the result is a treeview without icons or text. Sometimes I receive the error: "QFileSystemWatcher: failed to add paths: C:/PerfLogs". Based on examples online, I've commented where I think my errors may be, but I cannot yet get this to work. What am I doing wrong?
import sys
from PySide.QtCore import *
from PySide.QtGui import *
database = {'C:\Program Files\Internet Explorer\ExtExport.exe':(1,3), 'C:\Program Files\Internet Explorer\iexplore.exe':(0,0)}
class newFileModel(QFileSystemModel):
def __init__(self, parent=None):
QFileSystemModel.__init__(self, parent)
#self.elements = [[Do I need this? What should go here?]]
def data(self, index, role=Qt.DisplayRole):
if index.isValid():
path = self.filePath(index)
if self.isDir(index):
if database.get(path) != None:
if database[path][0] > 0:
print("Acting on custom data 0.") # I can add code here for different color text, etc.
if database[path][1] > 0:
print("Acting on custom data 1.") # I'll add code later
#return self.data(index, role) # Do I need this about here?
class fileSystemBrowser(QTreeView):
def __init__(self, parent=None):
super().__init__(parent)
self.fileSystemModel = newFileModel()
self.fileSystemModel.setRootPath(QDir.currentPath())
self.setModel(self.fileSystemModel)
if __name__ == '__main__':
app = QApplication(sys.argv)
window = fileSystemBrowser()
window.show()
sys.exit(app.exec_())
Here is a basic demo that shows how to add an extra column with icons and other formatting. Note that an attempt is made to normalise the file-paths so that comparisons and dictionary look-ups should be more reliable:
import sys
from PySide.QtCore import *
from PySide.QtGui import *
database = {
QFileInfo('C:\Program Files\Internet Explorer\ExtExport.exe').absoluteFilePath(): (1, 3),
QFileInfo('C:\Program Files\Internet Explorer\iexplore.exe').absoluteFilePath(): (0, 0),
}
class FileSystemModel(QFileSystemModel):
def __init__(self, parent=None):
super().__init__(parent)
style = qApp.style()
self.icons = [
style.standardIcon(QStyle.SP_MessageBoxInformation),
style.standardIcon(QStyle.SP_MessageBoxWarning),
]
def columnCount(self, parent=QModelIndex()):
return super().columnCount(parent) + 1
def data(self, index, role=Qt.DisplayRole):
extra = False
if index.isValid():
extra = index.column() == self.columnCount(index.parent()) - 1
info = self.fileInfo(index)
path = info.absoluteFilePath()
if path in database:
major, minor = database[path]
print('found:', (major, minor), path)
if extra:
if role == Qt.DecorationRole:
if major > 0:
return self.icons[0]
else:
return self.icons[1]
elif role == Qt.DisplayRole:
return '%s/%s' % (major, minor)
elif role == Qt.ForegroundRole:
if minor > 2:
return QColor('red')
if not extra:
return super().data(index, role)
def headerData(self, section, orientation, role=Qt.DisplayRole):
if (orientation == Qt.Horizontal and
role == Qt.DisplayRole and
section == self.columnCount() - 1):
return 'Extra'
return super().headerData(section, orientation, role)
class FileSystemBrowser(QTreeView):
def __init__(self, parent=None):
super().__init__(parent)
self.fileSystemModel = FileSystemModel()
self.fileSystemModel.setRootPath(QDir.currentPath())
self.setModel(self.fileSystemModel)
self.header().moveSection(self.fileSystemModel.columnCount() - 1, 1)
if __name__ == '__main__':
app = QApplication(sys.argv)
window = FileSystemBrowser()
window.show()
sys.exit(app.exec_())
EDIT:
The roles used in the data method are all documented under the ItemDataRole enum, and are introduced as follows:
Each item in the model has a set of data elements associated with it,
each with its own role. The roles are used by the view to indicate to
the model which type of data it needs. Custom models should return
data in these types.
For the extra column that has been added, it is necessary to supply everything, because it is a virtual column that is not part of the underlying model. But for the other columns, we can just call the base-class implementation to get the default values (of course, if desired, we could also return custom values for these columns to modify the existing behaviour).
I have a QTreeView, and I've figured out how to set its style by using setStyleSheet in my main class:
self.view.setStyleSheet("""
QTreeView::item {
margin: 2px;
}
""")
That will style the entire QTreeView. But I want certain items in the tree to be bolded. When I create the branches (using [the parent widget].appendRow("the name of the item")), is there a way to 'tag' or isolate specific items so it can be styled the same way? I think the answer has something to do with the 'AccessibleName' or 'ObjectName' properties, but I'm having trouble finding documentation on it.
Update: This is what I have so far:
#!/usr/bin/python
# -*- coding: utf-8 -*-
from __future__ import division
from __future__ import print_function
from future_builtins import *
import os, sys
from PySide.QtCore import *
from PySide.QtGui import *
path_to_media = '/Volumes/show/vfx/webisodes/%d1%/seq/%d2%/%d3%/renders/2d/comp/'
class FileTree(QTreeView):
"""Main file tree element"""
def __init__(self):
QTreeView.__init__(self)
def keyPressEvent(self, event):
if event.key() == Qt.Key_Space or event.key() == Qt.Key_Return:
index = self.selectedIndexes()[0]
crawler = index.model().itemFromIndex(index)
if crawler.uri:
print("launching", crawler.uri)
p = os.popen(('open -a "RV.app" "'+ crawler.uri) +'"', "r")
QTreeView.keyPressEvent(self, event)
class Branch(QStandardItem):
"""Branch element"""
def __init__(self, label, uri = None, tag = None):
QStandardItem.__init__(self, label)
self.uri = uri
class AppForm(QMainWindow):
def __init__(self, parent = None):
super(AppForm, self).__init__(parent)
self.model = QStandardItemModel()
self.view = FileTree()
self.view.setStyleSheet("""
QTreeView::item {
margin: 2px;
}
""")
self.view.setEditTriggers(QAbstractItemView.NoEditTriggers)
self.view.setModel(self.model)
self.setCentralWidget(self.view)
self.Grow()
# self.view.setSortingEnabled(True)
def Grow(self):
"""Populates FileTree (using BuildBranch())"""
global path_to_media
self.path = {}
self.path['source'] = path_to_media
self.path['parts'] = []
self.path['depth'] = 0
self.path['crawl'] = {}
for i in self.path['source'].split('%'):
if i[0] == "d" and i[1].isdigit():
self.path['depth'] += 1
else:
self.path['parts'].append(i)
self.BuildBranch(self.path['parts'], self.path['depth'], parentWidget = self.model.invisibleRootItem())
def BuildBranch(self, parts, depth, uri = '', count = 0, parent = '', parentWidget = ''):
"""Recursively crawls folder structure and adds appropriate branches"""
if not uri: uri = parts[0]
else: uri += parent + parts[count]
try:
if os.listdir(uri):
for i in os.listdir(uri):
if i[0] != '.':
if count != depth:
if os.path.isdir(uri):
thisWidget = Branch(i)
parentWidget.appendRow(thisWidget)
self.BuildBranch(parts, depth, uri, count + 1, i, parentWidget = thisWidget)
else:
thisWidget = Branch(i)
parentWidget.appendRow(thisWidget)
elif count == depth:
thisWidget = Branch(i, uri + i, 'media')
parentWidget.appendRow(thisWidget)
else:
print("nothing here; nuking " + parent)
# Need to add code to nuke unused branch
except OSError:
print("Folder structure error... nuking the branch")
# Need to add code to nuke unused branch
def main():
app = QApplication(sys.argv)
form = AppForm()
form.resize(800, 600)
form.setWindowTitle('Qt Dailies')
form.show()
app.exec_()
if __name__ == "__main__":
main()
Update 2: Okay, I modified my Branch class so that if 'bold' is passed to it, it makes the branch bold (in theory)...
class Branch(QStandardItem):
def __init__(self, label, uri = None, tag = None):
QStandardItem.__init__(self, label)
self.uri = uri
if tag == 'bold':
self.setData(self.createBoldFont(), Qt.FontRole)
def createBoldFont(self):
if self.font: return self.font
self.font = QFont()
self.font.setWeight(QFont.Bold)
return self.font
... but while the code runs, nothing seems to happen. What am I still not getting?
Qt's model-view architecture allows for data that describes the different roles being performed. For example, there is a role for editing data, displaying data, etc. You're interested in the font role (i.e. Qt::FontRole) as the font has a weight enum of which bold is one value.
When you build your branches you first need to identify which items should be bolded. I'll assume you have a method like such that can identify whether or not they should be bold:
def should_be_bolded(self, item):
return 1 # your condition checks
Now just set the weight on the font and set the font role of the item using its setData method:
def BuildBranch(...):
thisWidget = Branch(i)
if self.should_be_bolded(thisWidget):
thisWidget.setData(self.createBoldFont(), Qt.FontRole)
def createFont(self):
if self.font: return self.font
self.font = QFont()
self.font.setWeight(QFont.Bold)
return self.font
But wait... you already have a subclass of QtandardItem, so you can use that:
class Branch(QStandardItem):
"""Branch element"""
def __init__(self, label, uri = None, tag = None):
QStandardItem.__init__(self, label)
self.uri = uri
if self.should_be_bolded():
self.bold_myself()
You'll have to fix the should_be_bolded and bold_myself methods, cleaning up accordingly, but hopefully you get the point.
Stephen pointed out that you can also subclass one of the QAbstractItemModels, like the QStandardItemModel you're using, and return a specific Qt.FontRole. His way makes that knowledge implicit in the model. Decide where that knowledge best belongs and place it in the most appropriate place, whether it's in the item, the tree-creation algorithm, the model, or even a view model.
In your model's data() method you can add code to set the font depending on the content of the item. For example, if you wanted to bold everything in one particular row,
def data(self, index, role):
if role == QtCore.Qt.FontRole:
if index.row() == 1:
boldFont = QtGui.QFont()
boldFont.setBold(True)
return boldFont
You just need a way to retrieve the name of your branch when given an index for it. That depends on the implementation of your tree model.
The Qt Model/View tutorial has a good example, although it's in C++. Look at section 2.2 (Extending the Read Only Example with Roles).
I'm rewriting this post to clarify some things and provide a full class definition for the Virtual List I'm having trouble with. The class is defined like so:
from wx import ListCtrl, LC_REPORT, LC_VIRTUAL, LC_HRULES, LC_VRULES, \
EVT_LIST_COL_CLICK, EVT_LIST_CACHE_HINT, EVT_LIST_COL_RIGHT_CLICK, \
ImageList, IMAGE_LIST_SMALL, Menu, MenuItem, NewId, ITEM_CHECK, Frame, \
EVT_MENU
class VirtualList(ListCtrl):
def __init__(self, parent, datasource = None,
style = LC_REPORT | LC_VIRTUAL | LC_HRULES | LC_VRULES):
ListCtrl.__init__(self, parent, style = style)
self.columns = []
self.il = ImageList(16, 16)
self.Bind(EVT_LIST_CACHE_HINT, self.CheckCache)
self.Bind(EVT_LIST_COL_CLICK, self.OnSort)
if datasource is not None:
self.datasource = datasource
self.Bind(EVT_LIST_COL_RIGHT_CLICK, self.ShowAvailableColumns)
self.datasource.list = self
self.Populate()
def SetDatasource(self, datasource):
self.datasource = datasource
def CheckCache(self, event):
self.datasource.UpdateCache(event.GetCacheFrom(), event.GetCacheTo())
def OnGetItemText(self, item, col):
return self.datasource.GetItem(item, self.columns[col])
def OnGetItemImage(self, item):
return self.datasource.GetImg(item)
def OnSort(self, event):
self.datasource.SortByColumn(self.columns[event.Column])
self.Refresh()
def UpdateCount(self):
self.SetItemCount(self.datasource.GetCount())
def Populate(self):
self.UpdateCount()
self.datasource.MakeImgList(self.il)
self.SetImageList(self.il, IMAGE_LIST_SMALL)
self.ShowColumns()
def ShowColumns(self):
for col, (text, visible) in enumerate(self.datasource.GetColumnHeaders()):
if visible:
self.columns.append(text)
self.InsertColumn(col, text, width = -2)
def Filter(self, filter):
self.datasource.Filter(filter)
self.UpdateCount()
self.Refresh()
def ShowAvailableColumns(self, evt):
colMenu = Menu()
self.id2item = {}
for idx, (text, visible) in enumerate(self.datasource.columns):
id = NewId()
self.id2item[id] = (idx, visible, text)
item = MenuItem(colMenu, id, text, kind = ITEM_CHECK)
colMenu.AppendItem(item)
EVT_MENU(colMenu, id, self.ColumnToggle)
item.Check(visible)
Frame(self, -1).PopupMenu(colMenu)
colMenu.Destroy()
def ColumnToggle(self, evt):
toggled = self.id2item[evt.GetId()]
if toggled[1]:
idx = self.columns.index(toggled[2])
self.datasource.columns[toggled[0]] = (self.datasource.columns[toggled[0]][0], False)
self.DeleteColumn(idx)
self.columns.pop(idx)
else:
self.datasource.columns[toggled[0]] = (self.datasource.columns[toggled[0]][0], True)
idx = self.datasource.GetColumnHeaders().index((toggled[2], True))
self.columns.insert(idx, toggled[2])
self.InsertColumn(idx, toggled[2], width = -2)
self.datasource.SaveColumns()
I've added functions that allow for Column Toggling which facilitate my description of the issue I'm encountering. On the 3rd instance of this class in my application the Column at Index 1 will not display String values. Integer values are displayed properly. If I add print statements to my OnGetItemText method the values show up in my console properly. This behavior is not present in the first two instances of this class, and my class does not contain any type checking code with respect to value display.
It was suggested by someone on the wxPython users' group that I create a standalone sample that demonstrates this issue if I can. I'm working on that, but have not yet had time to create a sample that does not rely on database access. Any suggestions or advice would be most appreciated. I'm tearing my hair out on this one.
Are you building on the wxPython demo code for virtual list controls? There are a couple of bookkeeping things you need to do, like set the ItemCount property.
One comment about your OnGetItemText method: Since there's no other return statement, it will return None if data is None, so your test has no effect.
How about return data or "" instead?
There's a problem with the native object in Windows. If GetImg returns None instead of -1 the list has a problem with column 1 for some reason. That from Robin over on the Google Group post for this issue.