How can I implement a QComboBox that allows you to choose from a tree structure, akin to QTreeView?
I came up with the following class (TreeComboBox) using a two-part recipe at developer.nokia.com (Part 1, Part 2):
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
from PyQt5.QtGui import *
class TreeComboBox(QComboBox):
def __init__(self, *args):
super().__init__(*args)
self.__skip_next_hide = False
tree_view = QTreeView(self)
tree_view.setFrameShape(QFrame.NoFrame)
tree_view.setEditTriggers(tree_view.NoEditTriggers)
tree_view.setAlternatingRowColors(True)
tree_view.setSelectionBehavior(tree_view.SelectRows)
tree_view.setWordWrap(True)
tree_view.setAllColumnsShowFocus(True)
self.setView(tree_view)
self.view().viewport().installEventFilter(self)
def showPopup(self):
self.setRootModelIndex(QModelIndex())
super().showPopup()
def hidePopup(self):
self.setRootModelIndex(self.view().currentIndex().parent())
self.setCurrentIndex(self.view().currentIndex().row())
if self.__skip_next_hide:
self.__skip_next_hide = False
else:
super().hidePopup()
def selectIndex(self, index):
self.setRootModelIndex(index.parent())
self.setCurrentIndex(index.row())
def eventFilter(self, object, event):
if event.type() == QEvent.MouseButtonPress and object is self.view().viewport():
index = self.view().indexAt(event.pos())
self.__skip_next_hide = not self.view().visualRect(index).contains(event.pos())
return False
app = QApplication([])
combo = TreeComboBox()
combo.resize(200, 30)
parent_item = QStandardItem('Item 1')
parent_item.appendRow([QStandardItem('Child'), QStandardItem('Yesterday')])
model = QStandardItemModel()
model.appendRow([parent_item, QStandardItem('Today')])
model.appendRow([QStandardItem('Item 2'), QStandardItem('Today')])
model.setHeaderData(0, Qt.Horizontal, 'Name', Qt.DisplayRole)
model.setHeaderData(1, Qt.Horizontal, 'Date', Qt.DisplayRole)
combo.setModel(model)
combo.show()
app.exec_()
Related
Closed. This question needs to be more focused. It is not currently accepting answers.
Want to improve this question? Update the question so it focuses on one problem only by editing this post.
Closed 1 year ago.
Improve this question
I am trying to make small program in Python,using PyQt5.The program will as have many button has arranged neatly.When dragging one of the buttons to another button, you can swap their positions while the buttons are still arranged neatly.I tried many ways, but in the end the buttons couldn’t be arranged neatly.What should I do to achieve this effect
Sounds like a job for QEventFilter.
Simple version:
from PyQt5.QtCore import QEvent, QObject
from PyQt5.QtGui import QPainter
from PyQt5.QtWidgets import QWidget, QApplication, QHBoxLayout, QPushButton
class Filter(QObject):
def __init__(self, parent = None):
super().__init__(parent)
self._pressed = None
self._drag = False
def setDragMode(self, active):
""" When drag mode activated buttons are not clickable (mouse events are filtered) """
self._drag = active
def setLayout(self, layout):
self._layout = layout
for i in range(layout.count()):
item = layout.itemAt(i)
if item.widget():
item.widget().installEventFilter(self)
def eventFilter(self, obj, event):
if not self._drag:
return False
if event.type() == QEvent.MouseButtonPress:
if isinstance(obj, QPushButton):
self._pressed = obj
return True
elif event.type() == QEvent.MouseMove:
button = self._pressed
layout = self._layout
if button is None:
return True
pos = obj.mapToParent(event.pos())
index = layout.indexOf(button)
geometry = button.geometry()
if pos.x() < geometry.topLeft().x():
if index - 1 >= 0:
prev = layout.itemAt(index - 1).widget().geometry()
if prev.topRight().x() > pos.x():
layout.insertWidget(index - 1, button)
elif pos.x() > geometry.topRight().x():
if index + 1 < layout.count():
next_ = layout.itemAt(index + 1).widget().geometry()
if next_.topLeft().x() < pos.x():
layout.insertWidget(index + 1, button)
return True
elif event.type() == QEvent.MouseButtonRelease:
self._pressed = None
return True
return False
if __name__ == "__main__":
app = QApplication([])
widget = QWidget()
layout = QHBoxLayout()
buttons = [QPushButton("button {}".format(i)) for i in range(5)]
for button in buttons:
layout.addWidget(button)
widget.setLayout(layout)
widget.show()
filter_ = Filter()
filter_.setLayout(layout)
filter_.setDragMode(True)
app.exec_()
More intuitive and interactive version:
from PyQt5.QtCore import QEvent, QObject
from PyQt5.QtGui import QPainter
from PyQt5.QtWidgets import QWidget, QApplication, QHBoxLayout, QPushButton
class Overlay(QWidget):
def __init__(self, parent = None):
super().__init__(parent)
self._widget = None
self._pressPos = None
self._topLeftPos = None
self._movePos = None
self._pixmap = None
def setDragWidget(self, widget, pressPos, topLeftPos):
self._widget = widget
self._pressPos = pressPos
self._movePos = pressPos
self._topLeftPos = topLeftPos
self._pixmap = widget.grab()
def mouseMoved(self, pos):
self._movePos = pos
self.update()
def mouseReleased(self):
self._pixmap = None
self.update()
def paintEvent(self, event):
if self._pixmap is None:
return
painter = QPainter(self)
painter.drawPixmap(self._topLeftPos + self._movePos - self._pressPos, self._pixmap)
def swap_widgets(layout, index1, index2):
if index1 == index2:
return
widget1 = layout.itemAt(index1).widget()
widget2 = layout.itemAt(index2).widget()
layout.insertWidget(index1, widget2)
layout.insertWidget(index2, widget1)
class Filter(QObject):
def __init__(self, parent = None):
super().__init__(parent)
self._pressed = None
self._drag = False
self._overlay = None
def setDragMode(self, active):
""" When drag mode activated buttons are not clickable (mouse events are filtered) """
self._drag = active
def setLayout(self, layout):
self._layout = layout
for i in range(layout.count()):
item = layout.itemAt(i)
if item.widget():
item.widget().installEventFilter(self)
def eventFilter(self, obj, event):
if not self._drag:
return False
if event.type() == QEvent.MouseButtonPress:
if isinstance(obj, QPushButton):
self._pressed = obj
parent = obj.parent()
overlay = Overlay(parent)
overlay.setGeometry(parent.rect())
pressPos = obj.mapToParent(event.pos())
overlay.setDragWidget(obj, pressPos, obj.geometry().topLeft())
overlay.show()
self._overlay = overlay
return True
elif event.type() == QEvent.MouseMove:
button = self._pressed
layout = self._layout
if button is None:
return True
pos = obj.mapToParent(event.pos())
overlay = self._overlay
if overlay is not None:
overlay.mouseMoved(pos)
return True
elif event.type() == QEvent.MouseButtonRelease:
overlay = self._overlay
button = self._pressed
layout = self._layout
if overlay is not None:
overlay.mouseReleased()
overlay.hide()
pos = obj.mapToParent(event.pos())
index1 = layout.indexOf(button)
for index2 in range(layout.count()):
widget = layout.itemAt(index2).widget()
if widget is None:
continue
if widget.geometry().contains(pos):
swap_widgets(layout, index1, index2)
break
self._pressed = None
return True
return False
if __name__ == "__main__":
app = QApplication([])
widget = QWidget()
layout = QHBoxLayout()
buttons = [QPushButton("button {}".format(i)) for i in range(5)]
for button in buttons:
layout.addWidget(button)
widget.setLayout(layout)
widget.show()
filter_ = Filter()
filter_.setLayout(layout)
filter_.setDragMode(True)
app.exec_()
I haveve implemented a table model (inherited QAbstractTableModel) and added it on a QTableView.
I wanted to add delegate to use custom edit widgets for my data items (for example, use a QSlider for some double values). Alas, after I've added a delegate, the table view does not shows values in this column anymore.
Can anyone tell me, how to fix it?
Here's the code:
# -*- coding: utf-8 -*-
import os, sys
from enum import IntEnum, unique
from collections import OrderedDict
from PyQt5 import QtCore, QtGui, QtWidgets
#unique
class BGModelCols(IntEnum):
alpha = 0
alpha_for_nans = 1
color_map = 2
is_boolean = 3
x_size_px = 4
y_size_px = 5
is_visible = 6
class ScanRadarSimulator(QtCore.QObject):
backgrounds_supported = ["height", "elevation", "visibility", "closing_angles", "land_rcs"]
#property
def background_names(self):
return [self.tr("Land height"), self.tr("Elevation"), self.tr("Visibility"), self.tr("Closing angles"),\
self.tr("Land RCS")]
def __init__(self, parent=None):
super().__init__(parent)
class BackgroundTableModel(QtCore.QAbstractTableModel):
def __init__(self, radar_simulator, parent=None):
super().__init__(parent)
self._radar_simulator = radar_simulator
self._background_names = self._radar_simulator.background_names
assert isinstance(self._radar_simulator, ScanRadarSimulator)
self.column_names = {BGModelCols.alpha: self.tr("α-channel"),
BGModelCols.alpha_for_nans: self.tr("α-channel for NANs"),
BGModelCols.color_map: self.tr("Color map"),
BGModelCols.is_boolean: self.tr("Is boolean mask"),
BGModelCols.x_size_px: self.tr("X pixels"),
BGModelCols.y_size_px: self.tr("Y pixels"),
BGModelCols.is_visible: self.tr("Is visible")}
self._background_items = OrderedDict()
for bg_id in radar_simulator.backgrounds_supported:
bg_dict = {BGModelCols.alpha: 0.7,
BGModelCols.alpha_for_nans: 0.0,
BGModelCols.color_map: "jet",
BGModelCols.is_boolean: False,
BGModelCols.x_size_px: 4000,
BGModelCols.y_size_px: 4000,
BGModelCols.is_visible: False}
self._background_items[bg_id] = bg_dict
def rowCount(self, parent=QtCore.QModelIndex()):
return len(self._radar_simulator.backgrounds_supported)
def columnCount(self, parent=QtCore.QModelIndex()):
return len(BGModelCols)
def flags(self, index=QtCore.QModelIndex()):
if index.isValid():
return QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsEditable
def data(self, index=QtCore.QModelIndex(), role=QtCore.Qt.DisplayRole):
if not index.isValid():
return QtCore.QVariant()
row, col = index.row(), index.column()
print("DATA", row, col)
col_item = BGModelCols(col)
if role == QtCore.Qt.DisplayRole:
return str(self._background_items[radar_simulator.backgrounds_supported[row]][col_item])
elif role == QtCore.Qt.EditRole:
return self._background_items[radar_simulator.backgrounds_supported[row]][col_item]
else:
return QtCore.QVariant()
def setData(self, index, value, role=QtCore.Qt.EditRole):
if not index.isValid():
return False
if role == QtCore.Qt.EditRole:
row, col = index.row(), index.column()
col_item = BGModelCols(col)
self._background_items[radar_simulator.backgrounds_supported[row]][col_item] = value
self.dataChanged.emit(index, index)
return True
def headerData(self, section, orientation, role=QtCore.Qt.DisplayRole):
if role == QtCore.Qt.DisplayRole:
if orientation == QtCore.Qt.Horizontal:
return self.column_names[BGModelCols(section)]
elif orientation == QtCore.Qt.Vertical:
return self._background_names[section]
else:
return QtCore.QVariant()
class AlphaChannelDelegate(QtWidgets.QItemDelegate):
def __init__(self, parent=None):
super().__init__(parent)
def createEditor(self, parent, option, index):
print("CREATING EDITOR")
slider = QtWidgets.QSlider(parent)
slider.setMinimum(0)
slider.setMaximum(100)
slider.setOrientation(QtCore.Qt.Horizontal)
return slider
def setModelData(self, editor, model, index):
# row, col = index.row(), index.column()
# col_item = BGModelCols(col)
# model._background_items[model._radar_simulator.backgrounds_supported[row]][col_item] = editor.value() / 100.
print("setModelData")
model.setData(index, editor.value() / 100., QtCore.Qt.EditRole)
def setEditorData(self, editor, index):
print("setEditorData")
row, col = index.row(), index.column()
col_item = BGModelCols(col)
val = int(index.model()._background_items[index.model()._radar_simulator.backgrounds_supported[row]][col_item] * 100)
editor.setValue(val)
def updateEditorGeometry(self, editor, option, index):
editor.setGeometry(option.rect)
def paint(self, painter, option, index):
QtWidgets.QApplication.style().drawControl(QtWidgets.QStyle.CE_ItemViewItem, option, painter)
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
app.setStyle("fusion")
radar_simulator = ScanRadarSimulator()
table_view = QtWidgets.QTableView()
alpha_delegate = AlphaChannelDelegate(table_view)
table_view.setItemDelegateForColumn(int(BGModelCols.alpha), alpha_delegate)
table_view.setEditTriggers(QtWidgets.QAbstractItemView.AllEditTriggers)
table_view.setModel(BackgroundTableModel(radar_simulator))
table_view.show()
sys.exit(app.exec_())
Well, it will work if comment out the paint method.
^_^
Is there any way to load more than one column at start in QColumnView?
I tried simulating the click on the desired index in the tree view. Though the click event is received it doesn't load the second column. Tried calling the createColumn as well with the index. But both approaches didn't work.
from PyQt4 import QtCore, QtGui
import os
try:
_fromUtf8 = QtCore.QString.fromUtf8
except AttributeError:
def _fromUtf8(s):
return s
try:
_encoding = QtGui.QApplication.UnicodeUTF8
def _translate(context, text, disambig):
return QtGui.QApplication.translate(context, text, disambig, _encoding)
except AttributeError:
def _translate(context, text, disambig):
return QtGui.QApplication.translate(context, text, disambig)
class MyModel(QtGui.QFileSystemModel):
def __init__(self):
super().__init__()
self.checkedIndexes = {}
self.parentChecked=False
def flags(self,index):
flags=super().flags(index)|QtCore.Qt.ItemIsUserCheckable
return flags
def checkState(self, index):
if index in self.checkedIndexes:
return self.checkedIndexes[index]
else:
return QtCore.Qt.Checked
def data(self, index, role=QtCore.Qt.DisplayRole):
if role == QtCore.Qt.CheckStateRole:
if index.column() == 0:
return self.checkState(index)
else:
return super().data(index, role)
def setData(self, index, value, role):
if (role == QtCore.Qt.CheckStateRole and index.column() == 0):
self.checkedIndexes[index] = value
self.dataChanged.emit(index,index)
return True
return super().setData(index, value, role)
def hasChildren(self,index):
hasChildren=super().hasChildren(index)
path=super().filePath(index)
dirIter=QtCore.QDirIterator(path,QtCore.QDir.AllDirs|QtCore.QDir.NoDotAndDotDot|QtCore.QDir.NoSymLinks)
if dirIter.hasNext():
return True
else:
return False
return hasChildren
class columnView(QtGui.QDialog):
def __init__(self,parent=None):
super().__init__(parent)
self.ui = Ui_Dialog()
self.ui.setupUi(self)
self.model=MyModel()
self.model.setFilter(QtCore.QDir.AllDirs|QtCore.QDir.NoDotAndDotDot|QtCore.QDir.NoSymLinks)
path=os.path.expanduser("~")
self.model.setRootPath(path)
self.ui.columnView.setModel(self.model)
#print("path=",path)
self.ui.columnView.setRootIndex(self.model.index(path))
self.ui.columnView.updatePreviewWidget.connect(self.closePreview)
self.show()
openIndex=self.model.index(os.path.join(path,"Documents"))
self.ui.columnView.createColumn(openIndex)
#QtCore.QMetaObject.invokeMethod(self.ui.columnView, "clicked", QtCore.Qt.QueuedConnection, QtCore.Q_ARG(QtCore.QModelIndex, openIndex))
self.ui.columnView.clicked.connect(self.rowClicked)
self.ui.closePushButton.clicked.connect(self.close)
def rowClicked(self,index):
print("row clicked=",self.model.filePath(index))
def closePreview(self,index):
self.ui.columnView.setPreviewWidget(None)
class Ui_Dialog(object):
def setupUi(self, Dialog):
Dialog.setObjectName(_fromUtf8("Dialog"))
Dialog.resize(596, 389)
self.verticalLayout = QtGui.QVBoxLayout(Dialog)
self.verticalLayout.setObjectName(_fromUtf8("verticalLayout"))
self.columnView = QtGui.QColumnView(Dialog)
self.columnView.setObjectName(_fromUtf8("columnView"))
self.verticalLayout.addWidget(self.columnView)
self.horizontalLayout = QtGui.QHBoxLayout()
self.horizontalLayout.setObjectName(_fromUtf8("horizontalLayout"))
spacerItem = QtGui.QSpacerItem(40, 20, QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Minimum)
self.horizontalLayout.addItem(spacerItem)
self.closePushButton = QtGui.QPushButton(Dialog)
self.closePushButton.setObjectName(_fromUtf8("closePushButton"))
self.horizontalLayout.addWidget(self.closePushButton)
self.verticalLayout.addLayout(self.horizontalLayout)
self.retranslateUi(Dialog)
QtCore.QMetaObject.connectSlotsByName(Dialog)
def retranslateUi(self, Dialog):
Dialog.setWindowTitle(_translate("Dialog", "Dialog", None))
self.closePushButton.setText(_translate("Dialog", "Close", None))
if __name__ == "__main__":
import sys
app = QtGui.QApplication(sys.argv)
view = columnView()
sys.exit(app.exec_())
Though both TreeView and ColumnView is designed to display hierarchical data, I feel that when compared with TreeView, the ColumnView implementation was given less significance and highly frustrating. In TreeView you can do the above easily with QTreeView.expand(index).
The only way to do is to select the row with the index using the selection model
self.ui.columnView.selectionModel().setCurrentIndex(index,QtGui.QItemSelectionModel.Current|QtGui.QItemSelectionModel.Select)
This will highlight the row and will load the corresponding next column.
Ref: https://forum.qt.io/topic/76588/loading-two-columns-at-start-in-qcolumnview
When the button is clicked I need the tree-item to turn highlighted (editing mode) (see the image below). Currently I have to double-click the item to set it in to highlighted mode. How to achieve this?
from PyQt4 import QtCore, QtGui
app = QtGui.QApplication([])
class Dialog(QtGui.QDialog):
def __init__(self, parent=None):
super(Dialog, self).__init__(parent)
self.setLayout(QtGui.QVBoxLayout())
tree = QtGui.QTreeWidget()
self.layout().addWidget(tree)
button = QtGui.QPushButton()
button.setText('Add Item')
self.layout().addWidget(button)
button.clicked.connect(self.onClick)
item = QtGui.QTreeWidgetItem()
item.setFlags(item.flags() | QtCore.Qt.ItemIsEditable)
item.setText(0, 'Item number 1')
tree.addTopLevelItem(item)
def onClick(self):
print 'onClick'
dialog=Dialog()
dialog.show()
app.exec_()
from PyQt4 import QtCore, QtGui
app = QtGui.QApplication([])
class ItemDelegate(QtGui.QItemDelegate):
def __init__(self, parent):
QtGui.QItemDelegate.__init__(self, parent)
def createEditor(self, parent, option, index):
if not index: return
line = QtGui.QLineEdit(parent)
line.setText('Please enter the ')
# line.selectAll()
line.setSelection(0, len(line.text()))
line.setFocus()
return line
class Dialog(QtGui.QDialog):
def __init__(self, parent=None):
super(Dialog, self).__init__(parent)
self.setLayout(QtGui.QVBoxLayout())
self.tree = QtGui.QTreeWidget()
self.layout().addWidget(self.tree)
button = QtGui.QPushButton()
button.setText('Add Item')
self.layout().addWidget(button)
button.clicked.connect(self.onClick)
self.tree.setColumnCount(3)
delegate=ItemDelegate(self)
self.tree.setItemDelegate(delegate)
for row in range(5):
rootItem = QtGui.QTreeWidgetItem()
self.tree.addTopLevelItem(rootItem)
for column in range(3):
rootItem.setText(column, 'Root %s row %s'%(row, column))
for child_number in range(2):
childItem = QtGui.QTreeWidgetItem(rootItem)
for col in range(3):
childItem.setText(col, 'Child %s row %s'%(child_number, col))
def onClick(self):
rootItem = QtGui.QTreeWidgetItem()
rootItem.setText(0, 'New Item')
rootItem.setFlags(rootItem.flags() | QtCore.Qt.ItemIsEditable)
self.tree.addTopLevelItem(rootItem)
self.tree.openPersistentEditor(rootItem, 0)
rootItem.setSelected(True)
dialog=Dialog()
dialog.show()
app.exec_()
I would like to display "folders" and "files" in a QTreeView. Folders are meant to be able to contain files, and due to this relationship I wish for folder items to be displayed above file items in the tree view. The view should be sortable. How do I make sure that folder items are displayed above file items at all times in the tree view?
The below code provides an example of a tree view with folder and file items:
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
from PyQt5.QtGui import *
def _create_item(text, is_folder):
item = QStandardItem(text)
item.setData(is_folder, Qt.UserRole)
return item
def _folder_row(name, date):
return [_create_item(text, True) for text in (name, date)]
def _file_row(name, date):
return [_create_item(text, False) for text in (name, date)]
class _Window(QMainWindow):
def __init__(self):
super().__init__()
widget = QWidget()
self.__view = QTreeView()
layout = QVBoxLayout(widget)
layout.addWidget(self.__view)
self.setCentralWidget(widget)
model = QStandardItemModel()
model.appendRow(_file_row('File #1', '01.09.2014'))
model.appendRow(_folder_row('Folder #1', '01.09.2014'))
model.appendRow(_folder_row('Folder #2', '02.09.2014'))
model.appendRow(_file_row('File #2', '03.09.2014'))
model.setHorizontalHeaderLabels(['Name', 'Date'])
self.__view.setModel(model)
self.__view.setSortingEnabled(True)
app = QApplication([])
w = _Window()
w.show()
app.exec_()
A solution is to wrap the model in a QSortFilterProxyModel, and reimplement the proxy's lessThan method to make it so that folder items are always placed before file items:
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
from PyQt5.QtGui import *
def _create_item(text, is_folder):
item = QStandardItem(text)
item.setData(is_folder, Qt.UserRole)
return item
def _folder_row(name, date):
return [_create_item(text, True) for text in (name, date)]
def _file_row(name, date):
return [_create_item(text, False) for text in (name, date)]
class _SortProxyModel(QSortFilterProxyModel):
"""Sorting proxy model that always places folders on top."""
def __init__(self, model):
super().__init__()
self.setSourceModel(model)
def lessThan(self, left, right):
"""Perform sorting comparison.
Since we know the sort order, we can ensure that folders always come first.
"""
left_is_folder = left.data(Qt.UserRole)
left_data = left.data(Qt.DisplayRole)
right_is_folder = right.data(Qt.UserRole)
right_data = right.data(Qt.DisplayRole)
sort_order = self.sortOrder()
if left_is_folder and not right_is_folder:
result = sort_order == Qt.AscendingOrder
elif not left_is_folder and right_is_folder:
result = sort_order != Qt.AscendingOrder
else:
result = left_data < right_data
return result
class _Window(QMainWindow):
def __init__(self):
super().__init__()
widget = QWidget()
self.__view = QTreeView()
layout = QVBoxLayout(widget)
layout.addWidget(self.__view)
self.setCentralWidget(widget)
model = QStandardItemModel()
model.appendRow(_file_row('File #1', '01.09.2014'))
model.appendRow(_folder_row('Folder #1', '01.09.2014'))
model.appendRow(_folder_row('Folder #2', '02.09.2014'))
model.appendRow(_file_row('File #2', '03.09.2014'))
model.setHorizontalHeaderLabels(['Name', 'Date'])
proxy_model = _SortProxyModel(model)
self.__view.setModel(proxy_model)
self.__view.setSortingEnabled(True)
app = QApplication([])
w = _Window()
w.show()
app.exec_()