Source code for labscript_utils.qtwidgets.headerview_with_widgets
from qtutils.qt import QtCore, QtGui, QtWidgets
[docs]class HorizontalHeaderViewWithWidgets(QtWidgets.QHeaderView):
"""A QHeaderView that supports inserting arbitrary
widgets into sections. Use setWidget(logical_index, widget)
to set and setWidget(logical_index, None) to unset.
Decorations, checkboxes or anything other than text in the
headers containing widgets is unsupported, and may result
in garbled output"""
thinspace = u'\u2009' # For indenting text
stylesheet = """
QHeaderView::section {
/* Will be set dynamically: */
padding-top: %dpx;
padding-bottom: %dpx;
/* Required, otherwise set to zero upon setting any stylesheet at all: */
padding-left: 4px;
/* Required for some reason, otherwise other settings ignored: */
color: black;
}
/* Any other style goes here: */
%s
"""
[docs] def __init__(self, model, parent=None):
self.widgets = {}
self.indents = {}
self.model = model
QtWidgets.QHeaderView.__init__(self, QtCore.Qt.Horizontal, parent)
self.setDefaultAlignment(QtCore.Qt.AlignLeft)
self.sectionMoved.connect(self.update_widget_positions)
self.sectionResized.connect(self.update_widget_positions)
self.geometriesChanged.connect(self.update_widget_positions)
self.sectionCountChanged.connect(self.update_widget_positions)
self.model.columnsInserted.connect(self.on_columnsInserted)
self.model.columnsRemoved.connect(self.on_columnsRemoved)
self.setSectionsMovable(True)
self.vertical_padding = 0
self.position_update_required = False
self.custom_style = ''
self.update_indents()
[docs] def setStyleSheet(self, custom_style):
self.custom_style = custom_style
self.update_indents()
[docs] def showSection(self, *args, **kwargs):
result = QtWidgets.QHeaderView.showSection(self, *args, **kwargs)
self.update_indents()
self.update_widget_positions()
return result
[docs] def hideSection(self, *args, **kwargs):
result = QtWidgets.QHeaderView.hideSection(self, *args, **kwargs)
self.update_indents()
self.update_widget_positions()
return result
[docs] def setSectionHidden(self, *args, **kwargs):
result = QtWidgets.QHeaderView.setSectionHidden(self, *args, **kwargs)
self.update_indents()
self.update_widget_positions()
return result
[docs] def viewportEvent(self, event):
if event.type() == QtCore.QEvent.Paint:
self.update_widget_positions()
return QtWidgets.QHeaderView.viewportEvent(self, event)
[docs] def setWidget(self, logical_index, widget=None):
header_item = self.model.horizontalHeaderItem(logical_index)
if header_item is None:
self.model.setHorizontalHeaderItem(logical_index, QtGui.QStandardItem())
if widget is None:
if logical_index in self.widgets:
widget = self.widgets[logical_index]
widget.setParent(None)
del self.widgets[logical_index]
widget.removeEventFilter(self)
del self.indents[widget]
label_text = self.model.headerData(logical_index, QtCore.Qt.Horizontal, QtCore.Qt.DisplayRole)
# Compatibility with both API types:
if hasattr(QtCore, 'QVariant') and isinstance(label_text, QtCore.QVariant):
if label_text.isNull():
return
else:
label_text = label_text.toString()
if label_text is None:
return
else:
raw_label_text = label_text.replace(self.thinspace, '')
self.model.setHeaderData(logical_index, QtCore.Qt.Horizontal, raw_label_text, QtCore.Qt.DisplayRole)
else:
self.widgets[logical_index] = widget
widget.setParent(self)
widget.installEventFilter(self)
if not self.isSectionHidden(logical_index) and not widget.isVisible():
widget.show()
self.update_indents()
self.update_widget_positions()
[docs] def showEvent(self, event):
QtWidgets.QHeaderView.showEvent(self, event)
self.update_indents()
self.update_widget_positions()
[docs] def update_indents(self):
max_widget_height = 0
for visual_index in range(self.count()):
logical_index = self.logicalIndex(visual_index)
if logical_index in self.widgets:
widget = self.widgets[logical_index]
if not self.isSectionHidden(logical_index):
max_widget_height = max(max_widget_height, widget.size().height())
desired_indent = widget.size().width()
item = self.model.horizontalHeaderItem(logical_index)
font = item.font()
fontmetrics = QtGui.QFontMetrics(font, self)
indent = ''
while fontmetrics.width(indent) < desired_indent:
indent += self.thinspace
self.indents[widget] = indent
font = self.font()
fontmetrics = QtGui.QFontMetrics(font, self)
height = fontmetrics.height()
required_padding = (max_widget_height + 2 - height) // 2
required_padding = max(required_padding, 3)
QtWidgets.QHeaderView.setStyleSheet(self, self.stylesheet % (required_padding, required_padding, self.custom_style))
[docs] def sectionSizeFromContents(self, logical_index):
base_size = QtWidgets.QHeaderView.sectionSizeFromContents(self, logical_index)
width, height = base_size.width(), base_size.height()
if logical_index in self.widgets:
widget_size = self.widgets[logical_index].size()
widget_width, widget_height = widget_size.width(), widget_size.height()
height = max(height, widget_height + 2)
width = max(width, widget_width + 7)
return QtCore.QSize(width, height)
[docs] def update_widget_positions(self):
# Do later and compress events, so as not to call
# self.do_update_widget_positions multiple times:
if not self.position_update_required:
timer = QtCore.QTimer.singleShot(0, self.do_update_widget_positions)
self.position_update_required = True
[docs] def do_update_widget_positions(self):
self.position_update_required = False
if not self.count():
return
max_height = max(self.sectionSizeFromContents(i).height()
for i in range(self.count())
if not self.isSectionHidden(i))
for visual_index in range(self.count()):
logical_index = self.logicalIndex(visual_index)
if logical_index in self.widgets:
widget = self.widgets[logical_index]
if not self.isSectionHidden(logical_index) and not widget.isVisible():
widget.show()
elif self.isSectionHidden(logical_index) and widget.isVisible():
widget.hide()
section_position = self.sectionViewportPosition(logical_index)
widget_size = widget.size()
widget_width, widget_height = widget_size.width(), widget_size.height()
widget_target_x = section_position + 3
widget_target_y = (max_height - widget_height) // 2 - 1
widget_current_pos = widget.pos()
widget_current_x, widget_current_y = widget_current_pos.x(), widget_current_pos.y()
if (widget_target_x, widget_target_y) != (widget_current_x, widget_current_y):
widget.move(widget_target_x, widget_target_y)
try:
indent = self.indents[widget]
except KeyError:
return
label_text = self.model.headerData(logical_index, QtCore.Qt.Horizontal, QtCore.Qt.DisplayRole)
# Compatibility with both API types:
if hasattr(QtCore, 'QVariant') and isinstance(label_text, QtCore.QVariant):
if not label_text.isNull():
label_text = label_text.toString()
else:
label_text = ''
if label_text is None:
label_text = ''
raw_label_text = label_text.replace(self.thinspace, '')
if label_text != indent + raw_label_text:
self.model.setHeaderData(
logical_index, QtCore.Qt.Horizontal, indent + raw_label_text, QtCore.Qt.DisplayRole)
[docs] def eventFilter(self, target, event):
"""Ensure we don't leave the curor set as a resize
handle when the mouse moves onto a child widget:"""
if event.type() == QtCore.QEvent.Enter:
self.unsetCursor()
return False
[docs] def on_columnsInserted(self, parent, logical_first, logical_last):
n_inserted = logical_last - logical_first + 1
widgets_with_offset = {}
for logical_index, widget in self.widgets.items():
if logical_index < logical_first:
widgets_with_offset[logical_index] = widget
else:
widgets_with_offset[logical_index + n_inserted] = widget
self.widgets = widgets_with_offset
self.update_widget_positions()
[docs] def on_columnsRemoved(self, parent, logical_first, logical_last):
n_removed = logical_last - logical_first + 1
widgets_with_offset = {}
for logical_index, widget in self.widgets.items():
if logical_index < logical_first:
widgets_with_offset[logical_index] = widget
elif logical_index <= logical_last:
self.setWidget(logical_index, None)
else:
widgets_with_offset[logical_index - n_removed] = widget
self.widgets = widgets_with_offset
self.update_widget_positions()
if __name__ == '__main__':
import sys
import qtutils.icons
class TestApp(object):
def __init__(self):
self.window = QtGui.QWidget()
self.window.resize(640, 480)
layout = QtGui.QVBoxLayout(self.window)
self.model = QtGui.QStandardItemModel()
self.treeview = QtGui.QTreeView(self.window)
self.header = HorizontalHeaderViewWithWidgets(self.model)
self.treeview.setSortingEnabled(True)
self.model.setHorizontalHeaderLabels(['Delete', 'Name', 'Value', 'Units', 'Expansion'])
self.button = QtGui.QPushButton('hello, world!')
self.button.setIcon(QtGui.QIcon(':qtutils/fugue/smiley-lol'))
self.button2 = QtGui.QToolButton()
self.button2.setIcon(QtGui.QIcon(':qtutils/fugue/plus'))
self.button3 = QtGui.QToolButton()
self.button3.setMinimumHeight(50)
self.button3.setIcon(QtGui.QIcon(':qtutils/fugue/minus'))
self.button4 = QtGui.QCheckBox()
self.header.setWidget(0, self.button)
self.header.setWidget(1, self.button2)
self.header.setWidget(2, self.button3)
self.header.setWidget(4, self.button4)
self.treeview.setHeader(self.header)
self.treeview.setModel(self.model)
layout.addWidget(self.treeview)
self.model.insertColumn(2, [QtGui.QStandardItem('test')])
self.window.show()
for col in range(self.model.columnCount()):
self.treeview.resizeColumnToContents(col)
QtCore.QTimer.singleShot(2000, lambda: self.header.hideSection(3))
QtCore.QTimer.singleShot(4000, lambda: self.header.showSection(3))
QtCore.QTimer.singleShot(6000, lambda: self.header.setWidget(0, None))
QtCore.QTimer.singleShot(8000, lambda: self.header.setWidget(0, self.button))
qapplication = QtGui.QApplication(sys.argv)
qapplication.setAttribute(QtCore.Qt.AA_DontShowIconsInMenus, False)
app = TestApp()
qapplication.exec_()