I am making a subclass of the QTreeWidget.
While trying to capture renaming which involves its default signals - itemChanged and itemDoubleClicked, the methods does works but I came to notice that the itemChanged are called twice instead of once.
Could not really figure out where or what is triggered the cause of the second 'extra' signal.
Appreciate if anyone could shed some light.
IsNewItemRole = QtCore.Qt.UserRole + 1000
class CustomTreeDelegate(QtGui.QStyledItemDelegate):
#property
def text_color(self):
if not hasattr(self, "_text_color"):
self._text_color = QtGui.QColor()
return self._text_color
#text_color.setter
def text_color(self, color):
"""Sets QColor towards object.
Args:
color (QtGui.QColor): RGB color values.
"""
self._text_color = color
def initStyleOption(self, option, index):
"""Change the font color only if item is a new added item.
Args:
option ():
index (QModelIndex?)
"""
super(CustomTreeDelegate, self).initStyleOption(option, index)
if self.text_color.isValid() and index.data(IsNewItemRole):
option.palette.setBrush(QtGui.QPalette.Text, self.text_color)
class CustomTreeWidgetItem(QtGui.QTreeWidgetItem):
"""Initialization class for QTreeWidgetItem creation.
Args:
widget (QtGui.QTreeWidget): To append items into.
text (str): Input name for QTreeWidgetItem.
is_tristate (bool): Should it be a tri-state checkbox. False by default.
"""
def __init__(self, parent=None, text=None, is_tristate=False, is_new_item=False):
super(CustomTreeWidgetItem, self).__init__(parent)
self.setText(0, text)
# flags = QtCore.Qt.ItemIsEditable | QtCore.Qt.ItemIsUserCheckable
if is_tristate:
# flags |= QtCore.Qt.ItemIsTristate
# Solely for the Parent item
self.setFlags(
self.flags()
| QtCore.Qt.ItemIsTristate
| QtCore.Qt.ItemIsEditable
| QtCore.Qt.ItemIsUserCheckable
)
else:
self.setFlags(
self.flags()
| QtCore.Qt.ItemIsEditable
| QtCore.Qt.ItemIsUserCheckable
)
self.setCheckState(0, QtCore.Qt.Unchecked)
self.setData(0, IsNewItemRole, is_new_item)
def setData(self, column, role, value):
"""Override QTreeWidgetItem setData function.
QTreeWidget does not have a signal that defines when an item has been
checked/ unchecked. And so, this method will emits the signal as a
means to handle this.
Args:
column (int): Column value of item.
role (int): Value of Qt.ItemDataRole. It will be Qt.DisplayRole or
Qt.CheckStateRole
value (int or unicode):
"""
state = self.checkState(column)
QtGui.QTreeWidgetItem.setData(self, column, role, value)
if (role == QtCore.Qt.CheckStateRole and
state != self.checkState(column)):
tree_widget = self.treeWidget()
if isinstance(tree_widget, CustomTreeWidget):
tree_widget.itemToggled.emit(self, column)
class CustomTreeWidget(QtGui.QTreeWidget):
"""Initialization class for QTreeWidget creation.
Args:
widget ():
"""
itemToggled = QtCore.pyqtSignal(QtGui.QTreeWidgetItem, bool)
selectionItemChanged = QtCore.pyqtSignal(bool)
contentUpdates = QtCore.pyqtSignal()
def __init__(self, widget=None):
super(CustomTreeWidget, self).__init__(widget)
self.rename_counter = False
self.currentItemChanged.connect(self.selection_item_changed)
self.itemChanged.connect(self.tree_item_changed)
self.itemDoubleClicked.connect(self.tree_item_double_clicked)
def selection_item_changed(self, current, previous):
"""Overrides widget's default signal.
Emiited when current item selection is changed. This will also toggles
the state of `self.add_child_btn`.
If a child item is selected, the "Add Child" button will be disabled.
Args:
current (CustomTreeWidgetItem): Currently selected item.
previous (CustomTreeWidgetItem or None): Previous selected item.
"""
state = True
if not current:
# print '>>> nothing is selected'
state = False
return
if current.parent():
state = False
self.selectionItemChanged.emit(state)
def tree_item_changed(self, item, column):
"""Overrides widget's default signal.
Emitted when the contents of the selected item in the column changes.
Args:
item (CustomTreeWidgetItem): Selected item.
column (int): Column value of the selected item.
"""
print '>>> selection item is changed!'
if self.rename_counter and self.prev_name != item.text(column):
item.setData(0, IsNewItemRole, True)
self.rename_counter = False
self.contentUpdates.emit()
elif item.data(column, IsNewItemRole):
print 'item is already an newitemrole'
return
def tree_item_double_clicked(self, item, column):
"""Overrides widget's default signal.
Emitted when User performs double clicks inside the widget.
Args:
item (CustomTreeWidgetItem): Selected item.
column (int): Column value of the selected item.
"""
self.prev_name = item.text(column)
self.rename_counter = True
class MainApp(QtGui.QWidget):
def __init__(self, parent=None):
super(MainApp, self).__init__(parent)
test_dict = {
"menuA": ["a101", "a102"],
"menuC": ["c101", "c102", "c103"],
"menuB": ["b101"],
}
self._tree = CustomTreeWidget()
self._tree.header().hide()
self._tree_delegate = CustomTreeDelegate(self._tree)
self._tree.setItemDelegate(self._tree_delegate)
for pk, pv in sorted(test_dict.items()):
parent = CustomTreeWidgetItem(self._tree, pk, is_tristate=True)
for c in pv:
child = CustomTreeWidgetItem(parent, c)
self._tree.expandAll()
main_layout = QtGui.QHBoxLayout()
main_layout.addWidget(self._tree)
self.setLayout(main_layout)
if __name__ == "__main__":
app = QtGui.QApplication(sys.argv)
w = MainApp()
w.show()
sys.exit(app.exec_())
Related
I am trying to implement tri-state checkboxes into a QMenu.
My menu hierarchy will be something like:
menuA
|-- a101
|-- a102
menuB
|-- b101
Where the first tier (menuA, menuB) are of tri-state checkboxes while its sub items are normal checkboxes, implemented using QAction.
And so, with the use of QWidgetAction and QCheckBox, seemingly I am able to get the tristate working on the first tier level.
However as soon as I tried to use setMenu that contains the sub items into the first tier items, the options are no longer checkable even though it is able to display the sub items accordingly.
Initially I am using only QAction widgets but as I am iterating the sub items, the first tier item is always shown as a full check in which I would like to rectify it if possible and hence I am trying to make use of the tri-state.
Eg. If a101 is checked, menuA will be set with a partial state. If both a101 and a102 are checked, menuA will then be set with (full) check state.
class CustomCheckBox(QtGui.QCheckBox):
def __init__(self, text="", parent=None):
super(CustomCheckBox, self).__init__(text, parent=parent)
self.setText(text)
self.setTristate(True)
class QSubAction(QtGui.QAction):
def __init__(self, text="", parent=None):
super(QSubAction, self).__init__(text, parent)
self.setCheckable(True)
self.toggled.connect(self.checkbox_toggle)
def checkbox_toggle(self, value):
print value
class QCustomMenu(QtGui.QMenu):
"""Customized QMenu."""
def __init__(self, title, parent=None):
super(QCustomMenu, self).__init__(title=str(title), parent=parent)
self.setup_menu()
def mousePressEvent(self,event):
action = self.activeAction()
if not isinstance(action,QSubAction) and action is not None:
action.trigger()
return
elif isinstance(action,QSubAction):
action.toggle()
return
return QtGui.QMenu.mousePressEvent(self,event)
def setup_menu(self):
self.setContextMenuPolicy(QtCore.Qt.DefaultContextMenu)
def contextMenuEvent(self, event):
no_right_click = [QAddAction]
if any([isinstance(self.actionAt(event.pos()), instance) for instance in no_right_click]):
return
pos = event.pos()
def addAction(self, action):
super(QCustomMenu, self).addAction(action)
class MainApp(QtGui.QWidget):
def __init__(self, parent=None):
super(MainApp, self).__init__(parent)
self.test_dict = {
"testA" :{
"menuA": ["a101", "a102"],
},
"testBC": {
"menuC": ["c101", "c102", "c103"],
"menuB": ["b101"]
},
}
v_layout = QtGui.QVBoxLayout()
self.btn1 = QtGui.QPushButton("TEST BTN1")
v_layout.addWidget(self.btn1)
self.setLayout(v_layout)
self.setup_connections()
def setup_connections(self):
self.btn1.clicked.connect(self.button1_test)
def button1_test(self):
self.qmenu = QCustomMenu(title='', parent=self)
for pk, pv in self.test_dict.items():
base_qmenu = QCustomMenu(title=pk, parent=self)
base_checkbox = CustomCheckBox(pk, base_qmenu)
base_action = QtGui.QWidgetAction(base_checkbox)
base_action.setMenu(base_qmenu) # This is causing the option un-checkable
base_action.setDefaultWidget(base_checkbox)
self.qmenu.addAction(base_action)
for v in pv:
action = QSubAction(v, self)
base_qmenu.addAction(action)
self.qmenu.exec_(QtGui.QCursor.pos())
if __name__ == "__main__":
app = QtGui.QApplication(sys.argv)
w = MainApp()
w.show()
sys.exit(app.exec_())
The reason for which you can't set the state of a sub menu is that QMenu automatically uses the click on a sub menu to open it, "consuming" the click event.
To get that you'll have to ensure where the user is clicking and, if it's one of your QWidgetActions trigger it, ensuring that the event is not being propagated furthermore.
Also, the tri state logic is added to the children state, using the toggled signal that checks all menu actions to decide the actual state.
Note that contextMenuEvent (along with the menu policy setting) has been removed.
Finally, consider that using a checkbox that does not trigger an action in a menu item is not suggested, as it's counterintuitive since it goes against the expected behavior of a menu item.
class CustomCheckBox(QtGui.QCheckBox):
def __init__(self, text="", parent=None):
super(CustomCheckBox, self).__init__(text, parent=parent)
self.setText(text)
self.setTristate(True)
def mousePressEvent(self, event):
# only react to left click buttons and toggle, do not cycle
# through the three states (which wouldn't make much sense)
if event.button() == QtCore.Qt.LeftButton:
self.toggle()
def toggle(self):
super(CustomCheckBox, self).toggle()
newState = self.isChecked()
for action in self.actions():
# block the signal to avoid recursion
oldState = action.isChecked()
action.blockSignals(True)
action.setChecked(newState)
action.blockSignals(False)
if oldState != newState:
# if you *really* need to trigger the action, do it
# only if the action wasn't already checked
action.triggered.emit(newState)
class QSubAction(QtGui.QAction):
def __init__(self, text="", parent=None):
super(QSubAction, self).__init__(text, parent)
self.setCheckable(True)
class QCustomMenu(QtGui.QMenu):
"""Customized QMenu."""
def __init__(self, title, parent=None):
super(QCustomMenu, self).__init__(title=str(title), parent=parent)
def mousePressEvent(self,event):
actionAt = self.actionAt(event.pos())
if isinstance(actionAt, QtGui.QWidgetAction):
# the first mousePressEvent is sent from the parent menu, so the
# QWidgetAction found is one of the sub menu actions
actionAt.defaultWidget().toggle()
return
action = self.activeAction()
if not isinstance(action,QSubAction) and action is not None:
action.trigger()
return
elif isinstance(action,QSubAction):
action.toggle()
return
QtGui.QMenu.mousePressEvent(self,event)
def addAction(self, action):
super(QCustomMenu, self).addAction(action)
if isinstance(self.menuAction(), QtGui.QWidgetAction):
# since this is a QWidgetAction menu, add the action
# to the widget and connect the action toggled signal
action.toggled.connect(self.checkChildrenState)
self.menuAction().defaultWidget().addAction(action)
def checkChildrenState(self):
actionStates = [a.isChecked() for a in self.actions()]
if all(actionStates):
state = QtCore.Qt.Checked
elif any(actionStates):
state = QtCore.Qt.PartiallyChecked
else:
state = QtCore.Qt.Unchecked
self.menuAction().defaultWidget().setCheckState(state)
I have created subclass of QTreeWidget and QTreeWidgetItem where I am trying to highlight the new added items (where the text will be colored red).
The tree hierarchy that I have adopted is as follows:
|-- parent
|--|-- child
|--|-- child
The tree widget is initially populated from a dictionary.
To get the difference, I did it by converting the current hierarchy in the tree widget into a dictionary and have it compared against the initial dictionary that it was populated with.
However if I add in a new child to an existing parent in which the name of the new child already existed in another parent, the same method as mentioned above does not works, as it will colored the first result that it find.
To replicate:
select menuB
right mouse click > add new sub item
input name: a101
hit "Highlight Diff." button
a101 child item under menuA is highlighted instead of the one in menuB
What would be the best way to go in getting the index of newly added child(ren)?
Thank you for any replies.
P.S: If anyone has better suggestion for the parent highlighting, please feel free to chip in.
class CustomTreeWidgetItem(QtGui.QTreeWidgetItem):
def __init__(self, widget=None, text=None, is_tristate=False):
super(CustomTreeWidgetItem, self).__init__(widget)
self.setText(0, text)
if is_tristate:
# Solely for the Parent item
self.setFlags(
self.flags()
| QtCore.Qt.ItemIsTristate
| QtCore.Qt.ItemIsEditable
| QtCore.Qt.ItemIsUserCheckable
)
else:
self.setFlags(
self.flags()
| QtCore.Qt.ItemIsEditable
| QtCore.Qt.ItemIsUserCheckable
)
self.setCheckState(0, QtCore.Qt.Unchecked)
def setData(self, column, role, value):
state = self.checkState(column)
QtGui.QTreeWidgetItem.setData(self, column, role, value)
if (role == QtCore.Qt.CheckStateRole and
state != self.checkState(column)):
tree_widget = CustomTreeWidget()
if tree_widget is not None:
tree_widget.itemToggled.emit(self, column)
class CustomTreeWidget(QtGui.QTreeWidget):
def __init__(self, widget=None):
super(CustomTreeWidget, self).__init__(widget)
self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
self.customContextMenuRequested.connect(self.show_custom_menu)
def show_custom_menu(self):
base_node = self.selectedItems()[0]
qmenu = QtGui.QMenu(self)
remove_action = QtGui.QAction("Remove item", self)
remove_action.triggered.connect(self.remove_selected_item)
qmenu.addAction(remove_action)
# The following options are only effected for top-level items
# top-level items do not have `parent()`
if not base_node.parent():
add_new_child_action = QtGui.QAction("Add new sub item", self)
add_new_child_action.triggered.connect(
partial(self.adds_new_child_item, base_node)
)
# qmenu.addAction(add_new_child_action)
qmenu.insertAction(remove_action, add_new_child_action)
qmenu.exec_(QtGui.QCursor.pos())
def add_item_dialog(self, title):
text, ok = QtGui.QInputDialog.getText(
self,
"Add {0} Item".format(title),
"Enter name for {0}-Item:".format(title)
)
if ok and text != "":
return text
def add_new_parent_item(self):
input_text = self.add_item_dialog("Parent")
if input_text:
CustomTreeWidgetItem(self, input_text, is_tristate=True)
def adds_new_child_item(self, base_node):
input_text = self.add_item_dialog("Sub")
if input_text:
CustomTreeWidgetItem(base_node, input_text)
self.setItemExpanded(base_node, True)
def remove_selected_item(self):
root = self.invisibleRootItem()
for item in self.selectedItems():
(item.parent() or root).removeChild(item)
def derive_tree_items(self, mode="all"):
all_items = defaultdict(list)
root_item = self.invisibleRootItem()
top_level_count = root_item.childCount()
for i in range(top_level_count):
top_level_item = root_item.child(i)
top_level_item_name = str(top_level_item.text(0))
child_num = top_level_item.childCount()
all_items[top_level_item_name] = []
for n in range(child_num):
child_item = top_level_item.child(n)
child_item_name = str(child_item.text(0)) or ""
if mode == "all":
all_items[top_level_item_name].append(child_item_name)
elif mode == "checked":
if child_item.checkState(0) == QtCore.Qt.Checked:
all_items[top_level_item_name].append(child_item_name)
elif mode == "unchecked":
if child_item.checkState(0) == QtCore.Qt.Unchecked:
all_items[top_level_item_name].append(child_item_name)
return all_items
class MainApp(QtGui.QWidget):
def __init__(self):
super(MainApp, self).__init__()
# initial dictionary that is populated into the tree widget
test_dict = {
"menuA": ["a101", "a102"],
"menuC": ["c101", "c102", "c103"],
"menuB": ["b101"],
}
self._tree = CustomTreeWidget()
self._tree.header().hide()
for pk, pv in sorted(test_dict.items()):
parent = CustomTreeWidgetItem(self._tree, pk, is_tristate=True)
for c in pv:
child = CustomTreeWidgetItem(parent, c)
self.orig_dict = self._tree.derive_tree_items()
# Expand the hierarchy by default
self._tree.expandAll()
tree_layout = QtGui.QVBoxLayout()
self.btn1 = QtGui.QPushButton("Add new item")
self.btn2 = QtGui.QPushButton("Highlight Diff.")
tree_layout.addWidget(self._tree)
tree_layout.addWidget(self.btn1)
tree_layout.addWidget(self.btn2)
main_layout = QtGui.QHBoxLayout()
main_layout.addLayout(tree_layout)
self.setLayout(main_layout)
self.setup_connections()
def setup_connections(self):
self.btn1.clicked.connect(self.add_parent_item)
self.btn2.clicked.connect(self.highlight_diff)
def add_parent_item(self):
# Get current selected in list widget
# CustomTreeWidgetItem(self._tree, "test", is_tristate=True)
self._tree.add_new_parent_item()
def highlight_diff(self):
self.current_dict = self._tree.derive_tree_items()
if self.orig_dict != self.current_dict:
# check for key difference
diff = [k for k in self.current_dict if k not in self.orig_dict]
if diff:
# get the difference of top-level items
for d in diff:
top_item = self._tree.findItems(d, QtCore.Qt.MatchExactly|QtCore.Qt.MatchRecursive)
#print aaa[0].childCount()
top_item[0].setTextColor(0, QtGui.QColor(255, 0, 0))
if top_item[0].childCount():
for n in range(top_item[0].childCount()):
top_item[0].child(n).setTextColor(0, QtGui.QColor(255, 0, 0))
# to highlight the child diff. of existing top-items
# issue with this portion if the new added item name already existed
for k, v in self.current_dict.items():
if k in self.orig_dict:
diff = set(v).difference(self.orig_dict.get(k), [])
for d in diff:
child_item = self._tree.findItems(d, QtCore.Qt.MatchExactly|QtCore.Qt.MatchRecursive)
child_item[0].setTextColor(0, QtGui.QColor(255, 0, 0))
if __name__ == "__main__":
app = QtGui.QApplication(sys.argv)
w = MainApp()
w.show()
sys.exit(app.exec_())
You can save in a role a flag indicating if it is a new item or not and change the color using a delegate:
import sys
from functools import partial
from PyQt4 import QtCore, QtGui
from collections import defaultdict
IsNewItemRole = QtCore.Qt.UserRole + 1000
class CustomTreeDelegate(QtGui.QStyledItemDelegate):
#property
def text_color(self):
if not hasattr(self, "_text_color"):
self._text_color = QtGui.QColor()
return self._text_color
#text_color.setter
def text_color(self, color):
self._text_color = color
def initStyleOption(self, option, index):
super(CustomTreeDelegate, self).initStyleOption(option, index)
if self.text_color.isValid() and index.data(IsNewItemRole):
option.palette.setBrush(QtGui.QPalette.Text, self.text_color)
class CustomTreeWidgetItem(QtGui.QTreeWidgetItem):
def __init__(self, parent=None, text="", is_tristate=False, is_new_item=False):
super(CustomTreeWidgetItem, self).__init__(parent)
self.setText(0, text)
flags = QtCore.Qt.ItemIsEditable | QtCore.Qt.ItemIsUserCheckable
if is_tristate:
flags |= QtCore.Qt.ItemIsTristate
else:
self.setCheckState(0, QtCore.Qt.Unchecked)
self.setFlags(self.flags() | flags)
self.setData(0, IsNewItemRole, is_new_item)
def setData(self, column, role, value):
state = self.checkState(column)
QtGui.QTreeWidgetItem.setData(self, column, role, value)
if role == QtCore.Qt.CheckStateRole and state != self.checkState(column):
tree_widget = self.treeWidget()
if isinstance(tree_widget, CustomTreeWidget):
tree_widget.itemToggled.emit(self, column)
class CustomTreeWidget(QtGui.QTreeWidget):
itemToggled = QtCore.pyqtSignal(QtGui.QTreeWidgetItem, int)
def __init__(self, widget=None):
super(CustomTreeWidget, self).__init__(widget)
self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
self.customContextMenuRequested.connect(self.show_custom_menu)
def show_custom_menu(self, pos):
base_node = self.itemAt(pos)
if base_node is None:
return
qmenu = QtGui.QMenu(self)
remove_action = QtGui.QAction("Remove item", self)
remove_action.triggered.connect(self.remove_selected_item)
qmenu.addAction(remove_action)
# The following options are only effected for top-level items
# top-level items do not have `parent()`
if base_node.parent() is None:
add_new_child_action = QtGui.QAction("Add new sub item", self)
add_new_child_action.triggered.connect(
partial(self.adds_new_child_item, base_node)
)
# qmenu.addAction(add_new_child_action)
qmenu.insertAction(remove_action, add_new_child_action)
qmenu.exec_(self.mapToGlobal(pos))
def add_item_dialog(self, title):
text, ok = QtGui.QInputDialog.getText(
self, "Add {0} Item".format(title), "Enter name for {0}-Item:".format(title)
)
if ok:
return text
def add_new_parent_item(self):
input_text = self.add_item_dialog("Parent")
if input_text:
it = CustomTreeWidgetItem(
self, input_text, is_tristate=True, is_new_item=True
)
def adds_new_child_item(self, base_node):
input_text = self.add_item_dialog("Sub")
if input_text:
it = CustomTreeWidgetItem(base_node, input_text, is_new_item=True)
self.setItemExpanded(base_node, True)
it.setData(0, IsNewItemRole, True)
def remove_selected_item(self):
root = self.invisibleRootItem()
for item in self.selectedItems():
(item.parent() or root).removeChild(item)
class MainApp(QtGui.QWidget):
def __init__(self, parent=None):
super(MainApp, self).__init__(parent)
# initial dictionary that is populated into the tree widget
test_dict = {
"menuA": ["a101", "a102"],
"menuC": ["c101", "c102", "c103"],
"menuB": ["b101"],
}
self._tree = CustomTreeWidget()
self._tree.header().hide()
self._tree_delegate = CustomTreeDelegate(self._tree)
self._tree.setItemDelegate(self._tree_delegate)
for pk, pv in sorted(test_dict.items()):
parent = CustomTreeWidgetItem(self._tree, pk, is_tristate=True)
for c in pv:
child = CustomTreeWidgetItem(parent, c)
# Expand the hierarchy by default
self._tree.expandAll()
tree_layout = QtGui.QVBoxLayout()
self.btn1 = QtGui.QPushButton("Add new item")
self.btn2 = QtGui.QPushButton("Highlight Diff.")
tree_layout.addWidget(self._tree)
tree_layout.addWidget(self.btn1)
tree_layout.addWidget(self.btn2)
main_layout = QtGui.QHBoxLayout(self)
main_layout.addLayout(tree_layout)
self.setup_connections()
def setup_connections(self):
self.btn1.clicked.connect(self.add_parent_item)
self.btn2.clicked.connect(self.highlight_diff)
def add_parent_item(self):
# Get current selected in list widget
# CustomTreeWidgetItem(self._tree, "test", is_tristate=True)
self._tree.add_new_parent_item()
def highlight_diff(self):
self._tree_delegate.text_color = QtGui.QColor(255, 0, 0)
self._tree.viewport().update()
if __name__ == "__main__":
app = QtGui.QApplication(sys.argv)
w = MainApp()
w.show()
sys.exit(app.exec_())
I have a simple example here. What I want to happen is when a user changes the toggle state of the 'Checkbox' i want it to set the property value for the CustomTreeWidgetItem's attr.
In this case each CustomTreeWidgetItem has an attr called dataObject which is where I store my class object Family. Family has an attribute enabled which I want this property to reflect the toggle state of the TreeWidgetItems' checkbox.
I have looked around online and have not found a solution yet for this. Most answers were in c++ or they were causing errors. You can test the results by clicking the print button. It should reflect the changes.
Here are the highlighted bits.
Updated:
The linked question does not answer my question. It does not return the value for the checkbox clicked.
What is this code doing in simple terms? And what do i need to modify to make it set the data object to True or False? Currently it returns 0 or 2. I'm not sure if that is tristate or what. Hope someone can explain what is going on.
if column == 0 and role == 10:
My custom tree widget items have the property dataObject. This is where I store my class object Family which has the property enabled in it.
self.dataObject = None
class Family:
def __init__(self, name, members=None, data=None):
self.name = name
self.enabled = True
Hope that helps.
Thanks
all the code
# Imports
# ------------------------------------------------------------------------------
import sys
from PySide import QtGui, QtCore
# Class
# ------------------------------------------------------------------------------
class Family:
def __init__(self, name, members=None, data=None):
self.name = name
self.enabled = True
# Custom Tree Widget
# ------------------------------------------------------------------------------
class TreeNodeItem( QtGui.QTreeWidgetItem ):
def __init__( self, parent, name ):
## Init super class ( QtGui.QTreeWidgetItem )
super( TreeNodeItem, self ).__init__( parent )
# Column 0 - Text:
self.setText( 0, name )
# Tree Node Data
self.dataObject = None
def setData(self, column, role, value):
if column == 0 and role == 10:
self.dataObject.enabled = value
super(TreeNodeItem, self).setData(column, role, value)
# Variables
# ------------------------------------------------------------------------------
Families = [
Family("Smith"),
Family("Michaels"),
Family("Wislon"),
Family("Yoder")
]
# Main
# ------------------------------------------------------------------------------
class Example(QtGui.QWidget):
def __init__(self,):
super(Example, self).__init__()
self.initUI()
def initUI(self):
# formatting
self.resize(400, 400)
self.setWindowTitle("Example")
# widgets
self.printData = QtGui.QPushButton("Print Families Data")
self.itemList = QtGui.QTreeWidget()
self.itemList.setItemsExpandable(True)
self.itemList.setAnimated(True)
self.itemList.setItemsExpandable(True)
self.itemList.setColumnCount(1)
self.itemList.setHeaderLabels(['Families'])
# signals
self.printData.clicked.connect(self.PrintAllData)
# layout - row/column/verticalpan/horizontalspan
self.mainLayout = QtGui.QGridLayout(self)
self.mainLayout.addWidget(self.itemList,0,0)
self.mainLayout.addWidget(self.printData,1,0)
self.center()
self.show()
self.UpdateFamilies()
def center(self):
qr = self.frameGeometry()
cp = QtGui.QDesktopWidget().availableGeometry().center()
qr.moveCenter(cp)
self.move(qr.topLeft())
def PrintAllData(self):
for f in Families:
print f.name, f.enabled
def UpdateFamilies(self):
treeWidget = self.itemList
treeWidget.clear()
for f in Families:
# Add Families
treeNode = TreeNodeItem(treeWidget, f.name)
# assign family class object to dataObject attr of treenode
treeNode.dataObject = f
treeNode.setCheckState(0, QtCore.Qt.Checked)
# Main
# ------------------------------------------------------------------------------
if __name__ == "__main__":
app = QtGui.QApplication(sys.argv)
ex = Example()
sys.exit(app.exec_())
The answer was in the linked question (although in C++) Is it possible to create a signal for when a QTreeWidgetItem checkbox is toggled?
Short answer - no, there doesn't appear to be a signal for that checkbox. There are 2 ways to fix this:
1) As in the linked question, you could implement the SetData method and update your data at the same time:
# Custom Tree Widget
# ------------------------------------------------------------------------------
class TreeNodeItem( QtGui.QTreeWidgetItem ):
def __init__( self, parent, name ):
## Init super class ( QtGui.QTreeWidgetItem )
super( TreeNodeItem, self ).__init__( parent )
# Column 0 - Text:
self.setText( 0, name )
# Tree Node Data
self.dataObject = None
def setData(self, column, role, value):
if column == 0 and role == 10:
self.dataObject.enabled = value
super(TreeNodeItem, self).setData(column, role, value)
2) You could also try manually creating a checkbox, connect up it's signal and add it to the TreeNodeItem using QTreeWidget.setItemWidgem
I can add a single tooltip to all headers using
tableview = QTableView()
tableview.horizontalHeader().setToolTip("headers")
but can I add different tooltips to each header, i.e. I need to access the QWidgets that contains the headers, e.g. (not working):
tableview.horizontalHeader().Item[0].setToolTip("header 0")
I'm pretty new to this stuff too, but I think you'll need to subclass QTableView and reimplement the headerData function. Here is a working example. Hopefully you can extract what you need from it:
from PyQt4 import QtGui, QtCore
import sys
class PaletteListModel(QtCore.QAbstractListModel):
def __init__(self, colors = [], parent = None):
QtCore.QAbstractListModel.__init__(self,parent)
self.__colors = colors
# required method for Model class
def rowCount(self, parent):
return len(self.__colors)
# optional method for Model class
def headerData(self, section, orientation, role):
if role == QtCore.Qt.DisplayRole:
if orientation == QtCore.Qt.Horizontal:
return QtCore.QString("Palette")
else:
return QtCore.QString("Color %1").arg(section)
if role == QtCore.Qt.ToolTipRole:
if orientation == QtCore.Qt.Horizontal:
return QtCore.QString("Horizontal Header %s Tooltip" % str(section))
else:
return QtCore.QString("Vertical Header %s Tooltip" % str(section))
# required method for Model class
def data(self, index, role):
# index contains a QIndexClass object. The object has the following
# methods: row(), column(), parent()
row = index.row()
value = self.__colors[row]
# keep the existing value in the edit box
if role == QtCore.Qt.EditRole:
return self.__colors[row].name()
# add a tooltip
if role == QtCore.Qt.ToolTipRole:
return "Hex code: " + value.name()
if role == QtCore.Qt.DecorationRole:
pixmap = QtGui.QPixmap(26,26)
pixmap.fill(value)
icon = QtGui.QIcon(pixmap)
return icon
if role == QtCore.Qt.DisplayRole:
return value.name()
def setData(self, index, value, role = QtCore.Qt.EditRole):
row = index.row()
if role == QtCore.Qt.EditRole:
color = QtGui.QColor(value)
if color.isValid():
self.__colors[row] = color
self.dataChanged.emit(index, index)
return True
return False
# implment flags() method
def flags(self, index):
return QtCore.Qt.ItemIsEditable | QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable
if __name__ == '__main__':
app = QtGui.QApplication(sys.argv)
app.setStyle("plastique")
data = QtCore.QStringList()
data << "one" << "two" << "three" << "four" << "five"
tableView = QtGui.QTableView()
tableView.show()
red = QtGui.QColor(255,0,0)
green = QtGui.QColor(0,255,0)
blue = QtGui.QColor(0,0,255)
model = PaletteListModel([red, green, blue])
tableView.setModel(model)
sys.exit(app.exec_())
Here's what worked for me:
headerView = self._table.horizontalHeader()
for i in range(headerView.count()):
key = headerView.model().headerData(i, QtCore.Qt.Horizontal)
toolTip = myDictOfToolTips.get(key, None)
self._table.horizontalHeaderItem(i).setToolTip(toolTip)
QTableWidget (which inherits QTableView) has a method horizontalHeaderItem(int) which can be used to get the header items, so you maybe could swich to use that instead of QTableView?
If you use QTableView, you can set tooltip by QStandardItemModel:
QStandardItemModel myModel;
myModel.horizontalHeaderItem(1)->setToolTip("");
The sample code below (heavily influenced from here) has a right-click context menu that will appear as the user clicks the cells in the table. Is it possible to have a different right-click context menu for right-clicks in the header of the table? If so, how can I change the code to incorporate this?
import re
import operator
import os
import sys
from PyQt4.QtCore import *
from PyQt4.QtGui import *
def main():
app = QApplication(sys.argv)
w = MyWindow()
w.show()
sys.exit(app.exec_())
class MyWindow(QWidget):
def __init__(self, *args):
QWidget.__init__(self, *args)
self.tabledata = [('apple', 'red', 'small'),
('apple', 'red', 'medium'),
('apple', 'green', 'small'),
('banana', 'yellow', 'large')]
self.header = ['fruit', 'color', 'size']
# create table
self.createTable()
# layout
layout = QVBoxLayout()
layout.addWidget(self.tv)
self.setLayout(layout)
def popup(self, pos):
for i in self.tv.selectionModel().selection().indexes():
print i.row(), i.column()
menu = QMenu()
quitAction = menu.addAction("Quit")
action = menu.exec_(self.mapToGlobal(pos))
if action == quitAction:
qApp.quit()
def createTable(self):
# create the view
self.tv = QTableView()
self.tv.setStyleSheet("gridline-color: rgb(191, 191, 191)")
self.tv.setContextMenuPolicy(Qt.CustomContextMenu)
self.tv.customContextMenuRequested.connect(self.popup)
# set the table model
tm = MyTableModel(self.tabledata, self.header, self)
self.tv.setModel(tm)
# set the minimum size
self.tv.setMinimumSize(400, 300)
# hide grid
self.tv.setShowGrid(True)
# set the font
font = QFont("Calibri (Body)", 12)
self.tv.setFont(font)
# hide vertical header
vh = self.tv.verticalHeader()
vh.setVisible(False)
# set horizontal header properties
hh = self.tv.horizontalHeader()
hh.setStretchLastSection(True)
# set column width to fit contents
self.tv.resizeColumnsToContents()
# set row height
nrows = len(self.tabledata)
for row in xrange(nrows):
self.tv.setRowHeight(row, 18)
# enable sorting
self.tv.setSortingEnabled(True)
return self.tv
class MyTableModel(QAbstractTableModel):
def __init__(self, datain, headerdata, parent=None, *args):
""" datain: a list of lists
headerdata: a list of strings
"""
QAbstractTableModel.__init__(self, parent, *args)
self.arraydata = datain
self.headerdata = headerdata
def rowCount(self, parent):
return len(self.arraydata)
def columnCount(self, parent):
return len(self.arraydata[0])
def data(self, index, role):
if not index.isValid():
return QVariant()
elif role != Qt.DisplayRole:
return QVariant()
return QVariant(self.arraydata[index.row()][index.column()])
def headerData(self, col, orientation, role):
if orientation == Qt.Horizontal and role == Qt.DisplayRole:
return QVariant(self.headerdata[col])
return QVariant()
def sort(self, Ncol, order):
"""Sort table by given column number.
"""
self.emit(SIGNAL("layoutAboutToBeChanged()"))
self.arraydata = sorted(self.arraydata, key=operator.itemgetter(Ncol))
if order == Qt.DescendingOrder:
self.arraydata.reverse()
self.emit(SIGNAL("layoutChanged()"))
if __name__ == "__main__":
main()
Turned out to be simpler than I thought. In the same manner as I add the popup menu for the QTableView widget itself, I can just get the header from table object and then attach a context menu in the same way as I did with the regular context menu.
headers = self.tv.horizontalHeader()
headers.setContextMenuPolicy(Qt.CustomContextMenu)
headers.customContextMenuRequested.connect(self.header_popup)
There's another potentially more powerful way to do this if you take the step and inherit the view instead of simply composing it. Does custom context menu work here? Yes, but why does anything other than the view need to know about it? It also will help better shape your code to deal with other issues properly. Currently the implementation doesn't provide any encapsulation, cohesion or support separation of responsibility. In the end you will have one big blob which is the antithesis of good design.
I mention this because you seem to be placing all of the GUI Logic in this ever growing main function, and its the reason you ended up putting the sort implementation inside your model, which makes no sense to me. (What if you have two views of the model, you are forcing them to be sorted in the same way)
Is it more code? Yes, but it gives you more power which I think is worth mentioning. Below I'm demonstrating how to handle the headers and also any given cell you want. Also note that in my implementation if some OTHER widget exists which also defines a context menu event handler it will potentially get a chance to have crack at handling the event after mine; so that if someone else adds a handler for only certain cases they can do so without complicating my code. Part of doing this is marking if you handled the event or not.
Enough of my rambling and thoughts here's the code:
#Alteration : instead of self.tv = QTableView...
self.tv = MyTableView()
....
# somewhere in your TableView object's __init__ method
# yeah IMHO you should be inheriting and thus extending TableView
class MyTableView(QTableView):
def __init__(self, parent = None):
super(MyTableView, self).__init__()
self.setContextMenuPolicy(Qt.DefaultContextMenu)
## uniform one for the horizontal headers.
self.horizontalHeader().setContextMenuPolicy(Qt.ActionsContextMenu)
''' Build a header action list once instead of every time they click it'''
doSomething = QAction("&DoSomething", self.verticalHeader(),
statusTip = "Do something uniformly for headerss",
triggered = SOME_FUNCTION
self.verticalHeader().addAction(doSomething)
...
return
def contextMenuEvent(self, event)
''' The super function that can handle each cell as you want it'''
handled = False
index = self.indexAt(event.pos())
menu = QMenu()
#an action for everyone
every = QAction("I'm for everyone", menu, triggered = FOO)
if index.column() == N: #treat the Nth column special row...
action_1 = QAction("Something Awesome", menu,
triggered = SOME_FUNCTION_TO_CALL )
action_2 = QAction("Something Else Awesome", menu,
triggered = SOME_OTHER_FUNCTION )
menu.addActions([action_1, action_2])
handled = True
pass
elif index.column() == SOME_OTHER_SPECIAL_COLUMN:
action_1 = QAction("Uh Oh", menu, triggered = YET_ANOTHER_FUNCTION)
menu.addActions([action_1])
handled = True
pass
if handled:
menu.addAction(every)
menu.exec_(event.globalPos())
event.accept() #TELL QT IVE HANDLED THIS THING
pass
else:
event.ignore() #GIVE SOMEONE ELSE A CHANCE TO HANDLE IT
pass
return
pass #end of class