def paintVerticalCell(self, painter: QPainter, hv: QHeaderView, cellIndex: QModelIndex, leafIndex: QModelIndex, logicalLeafIndex: int, styleOptions: QStyleOptionHeader, sectionRect: QRect, left: int): uniopt = QStyleOptionHeader(styleOptions) self.setForegroundBrush(uniopt, cellIndex) self.setBackgroundBrush(uniopt, cellIndex) width = self.cellSize(cellIndex, hv, uniopt).width() if cellIndex == leafIndex: width = sectionRect.width() - left top = self.currentCellLeft(cellIndex, leafIndex, logicalLeafIndex, sectionRect.top(), hv) height = self.currentCellWidth(cellIndex, leafIndex, logicalLeafIndex, hv) r = QRect(left, top, width, height) uniopt.text = cellIndex.data(Qt.DisplayRole) painter.save() uniopt.rect = r if cellIndex.data(Qt.UserRole): hv.style().drawControl(QStyle.CE_HeaderSection, uniopt, painter, hv) m = QMatrix() m.rotate(-90) painter.setWorldMatrix(m, True) new_r = QRect(0, 0, r.height(), r.width()) new_r.moveCenter(QPoint(-r.center().y(), r.center().x())) uniopt.rect = new_r hv.style().drawControl(QStyle.CE_HeaderLabel, uniopt, painter, hv) else: hv.style().drawControl(QStyle.CE_Header, uniopt, painter, hv) painter.restore() return left + width
def paintVerticalCell(self, painter: QPainter, hv: QHeaderView, cellIndex: QModelIndex, leafIndex: QModelIndex, logicalLeafIndex: int, styleOptions: QStyleOptionHeader, sectionRect: QRect, left: int): uniopt = QStyleOptionHeader(styleOptions) self.setForegroundBrush(uniopt, cellIndex) self.setBackgroundBrush(uniopt, cellIndex) width=self.cellSize(cellIndex, hv, uniopt).width() if cellIndex==leafIndex: width=sectionRect.width()-left top=self.currentCellLeft(cellIndex, leafIndex, logicalLeafIndex, sectionRect.top(), hv) height=self.currentCellWidth(cellIndex, leafIndex, logicalLeafIndex, hv) r = QRect(left, top, width, height) uniopt.text = cellIndex.data(Qt.DisplayRole) painter.save() uniopt.rect = r if cellIndex.data(Qt.UserRole): hv.style().drawControl(QStyle.CE_HeaderSection, uniopt, painter, hv) m = QMatrix() m.rotate(-90) painter.setWorldMatrix(m, True) new_r = QRect(0, 0, r.height(), r.width()) new_r.moveCenter(QPoint(-r.center().y(), r.center().x())) uniopt.rect = new_r hv.style().drawControl(QStyle.CE_HeaderLabel, uniopt, painter, hv) else: hv.style().drawControl(QStyle.CE_Header, uniopt, painter, hv) painter.restore() return left+width def paintVerticalSection(self, painter: QPainter, sectionRect: QRect, logicalLeafIndex: int, hv: QHeaderView, styleOptions: QStyleOptionHeader, leafIndex: QModelIndex): oldBO = painter.brushOrigin() left = sectionRect.x() indexes = QModelIndexList(self.parentIndexes(leafIndex)) for i in range(indexes.size()): realStyleOptions = QStyleOptionHeader(styleOptions) if i<indexes.size()-1 and (realStyleOptions.state&QStyle.State_Sunken or realStyleOptions.state&QStyle.State_On): t = QStyle.State(QStyle.State_Sunken | QStyle.State_On) realStyleOptions.state = realStyleOptions.state&~t #FIXME: parent items are not highlighted left=self.paintVerticalCell(painter, hv, indexes[i], leafIndex, logicalLeafIndex, realStyleOptions, sectionRect, left) painter.setBrushOrigin(oldBO) def __init__(self, orientation: Qt.Orientation, parent: QWidget): super().__init__(orientation, parent) self._pd = self.private_data() self.sectionResized.connect(self.on_sectionResized) self.setHighlightSections(self.options.get("highlightSections")) self.setClickable(self.options.get("clickable")) self.show() #force to be visible getattr(parent, "set%sHeader"%("Horizontal", "Vertical")[orientation!=Qt.Horizontal])(self) self.sectionMoved.connect(self.on_sectionMoved) def on_sectionMoved(self, logicalIndex, oldVisualIndex, newVisualIndex): view, model = self.parent(), self.parent().model() if not hasattr(model, "reorder"): return #reorder underlying data of models with /reorder/ def only if getattr(self, "manual_move", False): self.manual_move=False return self.manual_move=True self.moveSection(newVisualIndex, oldVisualIndex) #cancel move if model.reorder(oldVisualIndex, newVisualIndex, self.orientation()): #Reorder column widths / row heights horizontal = self.orientation()==Qt.Horizontal itemSize = (view.rowHeight, view.columnWidth)[horizontal] setItemSize = (view.setRowHeight, view.setColumnWidth)[horizontal] rng = sorted((oldVisualIndex, newVisualIndex)) options = [(itemSize(i), i) for i in range(rng[0], rng[1]+1)] options.insert(newVisualIndex-rng[0], options.pop(oldVisualIndex-rng[0])) for i, col in enumerate(range(rng[0], rng[1]+1)): setItemSize(col, options[i][0]) getattr(view, "select"+("Row", "Column")[horizontal])(newVisualIndex) #FIXME: don't select if sorting is enable? if self.isSortIndicatorShown(): sortIndIndex = next((i for i, o in enumerate(options) if o[1]==self.sortIndicatorSection()), None) if sortIndIndex is not None: #sort indicator is among sections being reordered self.setSortIndicator(sortIndIndex+rng[0], self.sortIndicatorOrder()) #FIXME: does unnecessary sorting model.layoutChanged.emit() #update view def styleOptionForCell(self, logicalInd: int)->QStyleOptionHeader: opt = QStyleOptionHeader() self.initStyleOption(opt) if self.isSortIndicatorShown() and self.sortIndicatorSection()==logicalInd: opt.sortIndicator = (QStyleOptionHeader.SortUp, QStyleOptionHeader.SortDown)[self.sortIndicatorOrder()==Qt.AscendingOrder] if self.window().isActiveWindow(): opt.state = opt.state|QStyle.State_Active opt.textAlignment = Qt.AlignCenter opt.iconAlignment = Qt.AlignVCenter opt.section = logicalInd visual = self.visualIndex(logicalInd) if self.count() == 1: opt.position = QStyleOptionHeader.OnlyOneSection else: if visual == 0: opt.position = QStyleOptionHeader.Beginning else: opt.position = QStyleOptionHeader.End if visual==self.count()-1 else QStyleOptionHeader.Middle if self.isClickable(): # if logicalIndex == d.hover: # ... if self.highlightSections() and self.selectionModel(): if self.orientation()==Qt.Horizontal: if self.selectionModel().columnIntersectsSelection(logicalInd, self.rootIndex()): opt.state = opt.state|QStyle.State_On if self.selectionModel().isColumnSelected(logicalInd, self.rootIndex()): opt.state = opt.state|QStyle.State_Sunken else: if self.selectionModel().rowIntersectsSelection(logicalInd, self.rootIndex()): opt.state = opt.state|QStyle.State_On if self.selectionModel().isRowSelected(logicalInd, self.rootIndex()): opt.state = opt.state|QStyle.State_Sunken if self.selectionModel(): previousSelected=False if self.orientation()==Qt.Horizontal: previousSelected = self.selectionModel().isColumnSelected(self.logicalIndex(visual - 1), self.rootIndex()) else: previousSelected = self.selectionModel().isRowSelected(self.logicalIndex(visual - 1), self.rootIndex()) nextSelected=False if self.orientation()==Qt.Horizontal: nextSelected = self.selectionModel().isColumnSelected(self.logicalIndex(visual + 1), self.rootIndex()) else: nextSelected = self.selectionModel().isRowSelected(self.logicalIndex(visual + 1), self.rootIndex()) if previousSelected and nextSelected: opt.selectedPosition = QStyleOptionHeader.NextAndPreviousAreSelected else: if previousSelected: opt.selectedPosition = QStyleOptionHeader.PreviousIsSelected else: if nextSelected: opt.selectedPosition = QStyleOptionHeader.NextIsSelected else: opt.selectedPosition = QStyleOptionHeader.NotAdjacent return opt def sectionSizeFromContents(self, logicalIndex: int)->QSize: if self._pd.headerModel: curLeafIndex = QModelIndex(self._pd.leafIndex(logicalIndex)) if curLeafIndex.isValid(): styleOption = QStyleOptionHeader(self.styleOptionForCell(logicalIndex)) s = QSize(self._pd.cellSize(curLeafIndex, self, styleOption)) curLeafIndex=curLeafIndex.parent() while curLeafIndex.isValid(): if self.orientation() == Qt.Horizontal: s.setHeight(s.height()+self._pd.cellSize(curLeafIndex, self, styleOption).height()) else: s.setWidth(s.width()+self._pd.cellSize(curLeafIndex, self, styleOption).width()) curLeafIndex=curLeafIndex.parent() return s return super().sectionSizeFromContents(logicalIndex) def paintSection(self, painter: QPainter, rect: QRect, logicalIndex: int): if rect.isValid(): leafIndex = QModelIndex(self._pd.leafIndex(logicalIndex)) if leafIndex.isValid(): if self.orientation() == Qt.Horizontal: self._pd.paintHorizontalSection(painter, rect, logicalIndex, self, self.styleOptionForCell(logicalIndex), leafIndex) else: self._pd.paintVerticalSection(painter, rect, logicalIndex, self, self.styleOptionForCell(logicalIndex), leafIndex) return super().paintSection(painter, rect, logicalIndex) def on_sectionResized(self, logicalIndex: int): if self.isSectionHidden(logicalIndex): return leafIndex = QModelIndex(self._pd.leafIndex(logicalIndex)) if leafIndex.isValid(): leafsList = QModelIndexList(self._pd.leafs(self._pd.findRootIndex(leafIndex))) for n in range(leafsList.indexOf(leafIndex), 0, -1): logicalIndex-=1 w = self.viewport().width() h = self.viewport().height() pos = self.sectionViewportPosition(logicalIndex) r = QRect(pos, 0, w - pos, h) if self.orientation() == Qt.Horizontal: if self.isRightToLeft(): r.setRect(0, 0, pos + self.sectionSize(logicalIndex), h) else: r.setRect(0, pos, w, h - pos) self.viewport().update(r.normalized()) def setModel(self, model): super().setModel(model) model.layoutChanged.connect(self.layoutChanged) self.layoutChanged() def layoutChanged(self): if self.model(): self._pd.initFromNewModel(self.orientation(), self.model()) axis = ("column", "row")[self.orientation()!=Qt.Horizontal] cnt = getattr(self.model(), axis+"Count")(QModelIndex()) if cnt: self.initializeSections(0, cnt-1) MultiIndexHeaderView=HierarchicalHeaderView class DataFrameModel(QtCore.QAbstractTableModel): #na_values:least|greatest - for sorting options = {"striped": True, "stripesColor": "#fafafa", "na_values": "least", "tooltip_min_len": 21} def __init__(self, dataframe=None): super().__init__() self.setDataFrame(dataframe if dataframe is not None else pd.DataFrame()) def setDataFrame(self, dataframe): self.df = dataframe.copy() # self.df_full = self.df self.layoutChanged.emit() def rowCount(self, parent): return len(self.df) def columnCount(self, parent): return len(self.df.columns) def readLevel(self, y=0, xs=0, xe=None, orient=None): c = getattr(self.df, ("columns", "index")[orient!=HorizontalHeaderDataRole]) if not hasattr(c, "levels"): #not MultiIndex return [QtGui.QStandardItem(str(i)) for i in c] sibl = [] section_start, v, xe = xs, None, xe or len(c) for i in range(xs, xe): label = c.labels[y][i] if label!=v: if y+1<len(c.levels) and i>xs: children = self.readLevel(y+1, section_start, i, orient=orient) sibl[-1].appendRow(children) item = QtGui.QStandardItem(str(c.levels[y][label])) sibl.append(item) section_start = i v=label if y+1<len(c.levels): children = self.readLevel(y+1, section_start, orient=orient) sibl[-1].appendRow(children) return sibl def data(self, index, role): row, col = index.row(), index.column() if role in (Qt.DisplayRole, Qt.ToolTipRole): ret = self.df.iat[row, col] if ret is not None and ret==ret: #convert to str except for None, NaN, NaT if isinstance(ret, float): ret = "{:n}".format(ret) elif isinstance(ret, datetime.date): #FIXME: show microseconds optionally ret = ret.strftime(("%x", "%c")[isinstance(ret, datetime.datetime)]) else: ret = str(ret) if role == Qt.ToolTipRole: if len(ret)<self.options["tooltip_min_len"]: ret = "" return ret elif role == Qt.BackgroundRole: if self.options["striped"] and row%2: return QBrush(QColor(self.options["stripesColor"])) elif role in (HorizontalHeaderDataRole, VerticalHeaderDataRole): hm = QtGui.QStandardItemModel() hm.appendRow(self.readLevel(orient=role)) return hm def reorder(self, oldIndex, newIndex, orientation): "Reorder columns / rows" horizontal = orientation==Qt.Horizontal cols = list(self.df.columns if horizontal else self.df.index) cols.insert(newIndex, cols.pop(oldIndex)) self.df = self.df[cols] if horizontal else self.df.T[cols].T return True # def filter(self, filt=None): # self.df = self.df_full if filt is None else self.df[filt] # self.layoutChanged.emit() def headerData(self, section, orientation, role): if role != Qt.DisplayRole: return label = getattr(self.df, ("columns", "index")[orientation!=Qt.Horizontal])[section] # return label if type(label) is tuple else label return ("\n", " | ")[orientation!=Qt.Horizontal].join(str(i) for i in label) if type(label) is tuple else str(label) def dataFrame(self): return self.df def sort(self, column, order): # print("sort", column, order) #FIXME: double sort after setSortingEnabled(True) if len(self.df): asc = order==Qt.AscendingOrder na_pos = 'first' if (self.options["na_values"]=="least")==asc else 'last' self.df.sort_values(self.df.columns[column], ascending=asc, inplace=True, na_position=na_pos) self.layoutChanged.emit() if __name__=="__main__": import sys, locale locale.setlocale(locale.LC_ALL, '') #system locale settings app = QtGui.QApplication(sys.argv) form = QtGui.QWidget() form.setAttribute(Qt.WA_DeleteOnClose) #http://stackoverflow.com/a/27178019/1119602 form.setMinimumSize(700, 260) view = QtGui.QTableView() QtGui.QVBoxLayout(form).addWidget(view) form.show() #Prepare data tuples=[('bar', 'one', 'q'), ('bar', 'two', 'q'), ('baz', 'one', 'q'), ('baz', 'two', 'q'), ('foo', 'one', 'q'), ('foo', 'two', 'q'), ('qux', 'one', 'q'), ('qux', 'two', 'q')] index = pd.MultiIndex.from_tuples(tuples, names=['first', 'second', 'third']) df=pd.DataFrame(pd.np.random.randn(6, 6), index=index[:6], columns=index[:6]) print("DataFrame:\n%s"%df) #Prepare view # oldh, oldv = view.horizontalHeader(), view.verticalHeader() # oldh.setParent(form), oldv.setParent(form) #Save old headers for some reason MultiIndexHeaderView(Qt.Horizontal, view) MultiIndexHeaderView(Qt.Vertical, view) view.horizontalHeader().setMovable(True) #reorder DataFrame columns manually #Set data view.setModel(DataFrameModel(df)) view.resizeColumnsToContents() view.resizeRowsToContents() #Set sorting enabled (after setting model) view.setSortingEnabled(True) sys.exit(app.exec())