I'm looking for some input on how to go about creating a horizontal Kanban-style board using PySide2. My app is a file browser where you select an item from the folder QTreeView
on the left and the right view will populate with cards. The right side card view is where I'm stumped.
Here is my design goal:
My current wip implementation uses a QTreeView
to display the cards - it's close but not exactly what I'm looking for. Currently, delegate draws the parents as pseudo headers and the children as cards. As you can see below, one of the problems with using QTreeView
is that the children are listed vertically, rather than my preferred horizontal listing.
My current wip implementation:
I have a few of ideas on how to go about this:
- Use
setIndexWidget()
to add aQListView
under each pseudo header parent item. I'm not sure if this is the intended use of this method or how to get model data to populate the list. - Replace or cover the
QListView
with aQWidget
that is dynamically populated with model data from the folder view's selection. This widget createsQLabels
andQlistViews
for each header item from the model. I feel like it's over complicating things and a solution modifying an existing view would probably be better in the long run. - Use another view that I'm unaware of!
Any thoughts on how to go about building this? Does a vertical Kanban widget exist? Thanks!
Also, here are some other views that I've tried:
list
column view
Example Code:
import os
import sys
import collections
from PySide2 import QtWidgets, QtGui, QtCore
class MainWidget(QtWidgets.QWidget):
def __init__(self, model_data):
super(MainWidget, self).__init__()
self.setMinimumSize(600, 500)
# Model
self.model_data = model_data
self.folder_model = QtGui.QStandardItemModel()
self._fill_model(model_data)
# Folder view
self.folders_view = QtWidgets.QTreeView()
self.folders_view.setModel(self.folder_model)
self.folders_view.expandAll()
self.folders_view.setItemsExpandable(False)
self.folders_view.setVerticalScrollMode(QtWidgets.QAbstractItemView.ScrollPerPixel)
# Files Delegate
self.files_delegate = FilesItemDelegate()
self.folders_view.setItemDelegate(self.files_delegate)
# Layout
self.main_layout = QtWidgets.QHBoxLayout()
self.main_layout.addWidget(self.folders_view)
self.main_layout.setContentsMargins(0, 0, 0, 0)
self.setLayout(self.main_layout)
def _fill_model(self, value, parent=None):
if isinstance(value, collections.abc.Mapping):
for key, val in sorted(value.items()):
if key == 'meta_data':
pass
else:
item = QtGui.QStandardItem(key)
item.setData(val['meta_data']['name'], QtCore.Qt.DisplayRole)
item.setData(val['meta_data']['item_type'], QtCore.Qt.UserRole + 1)
try: # special data for major items
item.setData(val['meta_data']['major_number'], QtCore.Qt.UserRole)
except KeyError:
pass
try: # special data for minor items
item.setData(val['meta_data']['comment'], QtCore.Qt.UserRole)
except KeyError:
pass
try: # Add row under a parent item
parent.appendRow(item)
self._fill_model(value=val, parent=item)
except AttributeError: # Add first item to model
self.folder_model.appendRow(item)
self._fill_model(value=val, parent=item)
class FilesItemDelegate(QtWidgets.QStyledItemDelegate):
def __init__(self, parent=None):
super(FilesItemDelegate, self).__init__(parent)
def sizeHint(self, option, index):
if index.data(role=QtCore.Qt.UserRole + 1) == 'major':
size = super(FilesItemDelegate, self).sizeHint(option, index)
size.setWidth(option.rect.width())
size.setHeight(50)
return size
elif index.data(role=QtCore.Qt.UserRole + 1) == 'minor':
size = super(FilesItemDelegate, self).sizeHint(option, index)
if option.state & QtWidgets.QStyle.State_Selected:
width = 250
elif option.state & QtWidgets.QStyle.State_MouseOver:
width = 250
else:
width = 200
size.setWidth(width)
size.setHeight(200)
return size
else:
return super(FilesItemDelegate, self).sizeHint(option, index)
def paint(self, painter, option, index):
# Rect
rect_item = option.rect
# Background
painter.setPen(QtCore.Qt.NoPen)
# File component
if index.data(role=QtCore.Qt.UserRole + 1) == 'major':
# Rects:
rect_header_icon = QtCore.QRect(
rect_item.left() + 7,
rect_item.top(),
50,
rect_item.height() - 3)
rect_header_name = QtCore.QRect(
rect_header_icon.right() + 8,
rect_item.top(),
rect_item.width() - rect_header_icon.width(),
rect_item.height() * .666)
rect_header_major = QtCore.QRect(
rect_header_icon.right() + 8,
rect_header_name.bottom(),
rect_item.width() - rect_header_icon.width(),
rect_item.height() * .333)
if option.state & QtWidgets.QStyle.State_Selected:
painter.setBrush(QtGui.QColor('#00000000'))
elif option.state & QtWidgets.QStyle.State_MouseOver:
painter.setBrush(QtGui.QColor('#00000000'))
else:
painter.setBrush(QtGui.QColor('#00000000'))
painter.drawRect(option.rect)
# Icon
painter.save()
painter.setBrush(QtGui.QColor('#262626'))
painter.drawRect(rect_header_icon)
painter.restore()
# Primary Title
painter.setPen(QtGui.QColor(100, 100, 100))
# Name
name_index = index.data(role=QtCore.Qt.DisplayRole)
name_font = QtGui.QFont("Segoe UI", 13, QtGui.QFont.DemiBold | QtGui.QFont.NoAntialias)
painter.setFont(name_font)
QtWidgets.QApplication.style().drawItemText(painter, rect_header_name, QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter,
QtWidgets.QApplication.palette(), True,
name_index)
# Major number
name_index = index.data(role=QtCore.Qt.UserRole)
name_font = QtGui.QFont("Segoe UI", 13, QtGui.QFont.DemiBold | QtGui.QFont.NoAntialias)
painter.setFont(name_font)
QtWidgets.QApplication.style().drawItemText(painter, rect_header_major,
QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter,
QtWidgets.QApplication.palette(), True,
name_index)
# Cards
elif index.data(role=QtCore.Qt.UserRole + 1) == 'minor':
# Rects
rect_icon_target = QtCore.QRect(
rect_item.left(),
rect_item.top(),
rect_item.width(),
rect_item.height() - int(rect_item.height()/2)
)
rect_data_target = QtCore.QRect(
rect_item.left(),
rect_icon_target.bottom(),
rect_item.width(),
rect_item.height() - int(rect_item.height()/2)
)
pad_data_target = 15
rect_name_target = QtCore.QRect(
rect_item.left() + pad_data_target,
rect_icon_target.bottom() + pad_data_target,
rect_item.width() - pad_data_target,
rect_data_target.height()/2 - pad_data_target
)
rect_comment_target = QtCore.QRect(
rect_item.left() + pad_data_target,
rect_name_target.bottom() + pad_data_target,
rect_item.width() - pad_data_target,
rect_data_target.height() - pad_data_target
)
# Image half
painter.save()
path = QtGui.QPainterPath()
path.addRect(rect_icon_target)
painter.setBrush(QtGui.QColor(90, 90, 90))
painter.drawPath(path)
painter.restore()
# Data half
painter.save()
path = QtGui.QPainterPath()
path.setFillRule(QtCore.Qt.WindingFill)
path.addRect(rect_data_target)
if option.state & QtWidgets.QStyle.State_Selected:
painter.setBrush(QtGui.QColor(0, 149, 119))
elif option.state & QtWidgets.QStyle.State_MouseOver:
painter.setBrush(QtGui.QColor(100, 100, 100))
else:
painter.setBrush(QtGui.QColor(67, 67, 67))
painter.drawPath(path.simplified())
painter.restore()
# Primary Title
painter.setPen(QtGui.QColor(255, 255, 255))
name_index = index.data(role=QtCore.Qt.DisplayRole)
name_font = QtGui.QFont("Segoe UI", 13, QtGui.QFont.DemiBold | QtGui.QFont.NoAntialias)
painter.setFont(name_font)
QtWidgets.QApplication.style().drawItemText(painter, rect_name_target, QtCore.Qt.AlignLeft,
QtWidgets.QApplication.palette(), True,
name_index)
# Comment
painter.setPen(QtGui.QColor(255, 255, 255))
comment_index = index.data(role=QtCore.Qt.UserRole)
comment_font = QtGui.QFont("Segoe UI", 13, QtGui.QFont.DemiBold | QtGui.QFont.NoAntialias)
painter.setFont(comment_font)
QtWidgets.QApplication.style().drawItemText(painter, rect_comment_target, QtCore.Qt.AlignLeft,
QtWidgets.QApplication.palette(), True,
comment_index)
else:
return super(FilesItemDelegate, self).paint(painter, option, index)
def launch():
model_data = {
'low_poly': {
'001': {'meta_data': {'name': '001', 'item_type': 'minor', 'comment': 'hey'}},
'002': {'meta_data': {'name': '002', 'item_type': 'minor', 'comment': 'you'}},
'003': {'meta_data': {'name': '003', 'item_type': 'minor', 'comment': 'guyyyyys'}},
'004': {'meta_data': {'name': '004', 'item_type': 'minor', 'comment': "i'll"}},
'005': {'meta_data': {'name': '005', 'item_type': 'minor', 'comment': 'be'}},
'006': {'meta_data': {'name': '006', 'item_type': 'minor', 'comment': 'back'}},
'meta_data': {'item_type': 'major', 'name': 'low_poly', 'major_number': '001'}},
'high_poly': {
'001': {'meta_data': {'name': '001', 'item_type': 'minor', 'comment': 'as'}},
'002': {'meta_data': {'name': '002', 'item_type': 'minor', 'comment': 'you'}},
'003': {'meta_data': {'name': '003', 'item_type': 'minor', 'comment': 'wish'}},
'004': {'meta_data': {'name': '004', 'item_type': 'minor', 'comment': "lok"}},
'005': {'meta_data': {'name': '005', 'item_type': 'minor', 'comment': 'tar'}},
'006': {'meta_data': {'name': '006', 'item_type': 'minor', 'comment': 'ogar'}},
'meta_data': {'item_type': 'major', 'name': 'high_poly', 'major_number': '002'}},
'no_poly': {
'001': {'meta_data': {'name': '001', 'item_type': 'minor', 'comment': 'this'}},
'002': {'meta_data': {'name': '002', 'item_type': 'minor', 'comment': 'is'}},
'003': {'meta_data': {'name': '003', 'item_type': 'minor', 'comment': 'my'}},
'004': {'meta_data': {'name': '004', 'item_type': 'minor', 'comment': "boomstick"}},
'005': {'meta_data': {'name': '005', 'item_type': 'minor', 'comment': 'good'}},
'006': {'meta_data': {'name': '006', 'item_type': 'minor', 'comment': 'bye'}},
'meta_data': {'item_type': 'major', 'name': 'high_poly', 'major_number': '003'}},
'meta_data': {'item_type': 'asset', 'name': 'asset1'}
}
try:
os.environ["QTWEBENGINE_CHROMIUM_FLAGS"] = "--enable-logging --log-level=3"
os.environ["QT_AUTO_SCREEN_SCALE_FACTOR"] = "1" # High dpi setting
app = QtWidgets.QApplication(sys.argv)
except:
pass
window = MainWidget(model_data)
window.show()
try:
sys.exit(app.exec_())
except:
pass
return window
if __name__ == "__main__":
launch()