QWidgetAction in QMenu are un-checkable if it has menu within - python

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)

Related

How to implement buttons in a delegate PyQt

I am making a program for translating text (see screenshot)
I have three classes
class for displaying a window that edits item :
class StyleDelegate(QStyledItemDelegate):
def __init__(self, parent=None):
super(StyleDelegate, self).__init__()
def createEditor(self, widget, style, index):
self.mainWidget = QWidget(widget)
self.line = QLineEdit() # line for input text
self.delButton= QPushButton('❌') # button for delete current item
self.trnButton = QPushButton('➕') # button for make translation text in another QListView
self.qhbLayout = QHBoxLayout()
self.qhbLayout.addWidget(self.line)
self.qhbLayout.addWidget(self.delButton)
self.qhbLayout.addWidget(self.trnButton)
self.mainWidget.setLayout(self.qhbLayout)
return self.mainWidget
# there is still a lot of code in this place
class for storing, adding, deleting and editing data:
class TranslateListModel(QAbstractListModel):
def __init__(self, parent=None):
super(TranslateListModel, self).__init__()
self.words = ['1', '2', '3', '4']
def removeItem(self, index):
self.beginRemoveRows(index, index.row(), index.row())
del self.words[index.row()]
self.endRemoveRows()
return True
# there is still a lot of code in this place
main class of the program:
class QTranslate(QtWidgets.QDialog, log.Ui_Dialog):
def __init__(self):
super().__init__()
self.originalModel = TranslateListModel()
self.translateModel = TranslateListModel()
self.styleDelegate = StyleDelegate()
self.originalLV.setModel(self.originalModel)
#QListView from Ui_Dialog
self.translateLV.setModel(self.translateModel)
#QListView from Ui_Dialog
self.originalLV.setItemDelegate(self.styleDelegate)
self.translateLV.setItemDelegate(self.styleDelegate)
# there is still a lot of code in this place
How to implement buttons to delete the current item and change the translation in another QListView using QStyledItemDelegate? I cannot access these buttons outside the StyleDelegate class to associate them with the methods of the TranslateListModel class.
A possible solution is to create signals for the delegate and connect them to the functions that will delete or add items, then emit those signals when the buttons are clicked:
class StyleDelegate(QStyledItemDelegate):
deleteRequested = QtCore.pyqtSignal(int)
translateRequested = QtCore.pyqtSignal(int)
def __init__(self, parent=None):
super(StyleDelegate, self).__init__()
def createEditor(self, widget, style, index):
# note: I removed the "self" references as they're unnecessary
mainWidget = QWidget(widget)
line = QLineEdit()
delButton= QPushButton('❌')
trnButton = QPushButton('➕')
qhbLayout = QHBoxLayout()
qhbLayout.addWidget(line)
qhbLayout.addWidget(delButton)
qhbLayout.addWidget(trnButton)
mainWidget.setLayout(qhbLayout)
delButton.clicked.connect(
lambda _, row=index.row(): self.deleteRequested.emit(row))
trnButton.clicked.connect(
lambda _, row=index.row(): self.translateRequested.emit(row))
return mainWidget
class QTranslate(QtWidgets.QDialog, log.Ui_Dialog):
def __init__(self):
# ...
self.originalLV.setItemDelegate(self.styleDelegate)
self.styleDelegate.deleteRequested.connect(self.deleteRow)
self.styleDelegate.translateRequested.connect(self.translateRow)
def deleteRow(self, row):
# ...
def translateRow(self, row):
# ...
Note that you should always use an unique delegate instance for each view, as explained in the documentation:
Warning: You should not share the same instance of a delegate between views. Doing so can cause incorrect or unintuitive editing behavior since each view connected to a given delegate may receive the closeEditor() signal, and attempt to access, modify or close an editor that has already been closed.

Can a QAction be used for multiple tasks?

I created a tool using Qt Designer, where it has 3 QLineEdits that is catered for translateX, translateY and translateZ.
For each QLineEdit, I have created a context menu that allows me to set a keyframe for one of the above attribute depending on User's choice.
So instead of writing 3 separate functions that catered to each attribute, I thought of 'recycling' them by using 1 method, but I am having issues with it as I am not very sure if it will be possible since I am using a single QAction.
class MyTool(QtGui.QWidget):
def __init__(self, parent=None):
super(MyTool, self).__init__(parent = parent)
# Read off from convert uic file.
self.ui = Ui_MyWidget()
self.ui.setupUi(self)
# translateX
self.ui.xLineEdit.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
self.ui.xLineEdit.customContextMenuRequested.connect(self.custom_menu)
# translateY
self.ui.yLineEdit.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
self.ui.yLineEdit.customContextMenuRequested.connect(self.custom_menu)
# translateZ
self.ui.zLineEdit.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
self.ui.zLineEdit.customContextMenuRequested.connect(self.custom_menu)
self.popMenu = QtGui.QMenu(self)
set_key_action = QtGui.QAction("Set Key at Current Frame", self)
# I am having issues here..
set_key_action.triggered.connect(self.set_key)
self.popMenu.addAction(set_key_action)
...
...
def set_key(self, attr):
# assuming I am trying to effect this locator1 that already exists in the scene
current_item = "|locator1"
cmds.setKeyframe("{0}.{1}".format(current_item, attr))
def custom_menu(self, point):
self.popMenu.exec_(QtGui.QCursor.pos())
Again, because it is only a single QAction and hence I was stumped... Or will it be better for me to stick in using 3 separate functions instead?
The main problem is that when you connect the triggered signal you do not know that QLineEdit is going to be pressed. Where can we know that QLineEdit was pressed? Well, in the method custom_menu since there the method sender() returns the widget that opens its contextual menu, and to transfer it, a property or data is used, so the fine is to compare the property and the QLineEdit:
class MyTool(QtGui.QWidget):
def __init__(self, parent=None):
super(MyTool, self).__init__(parent=parent)
# Read off from convert uic file.
self.ui = Ui_MyWidget()
self.ui.setupUi(self)
for le in (self.ui.xLineEdit, self.ui.yLineEdit, self.ui.zLineEdit):
le.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
le.customContextMenuRequested.connect(self.custom_menu)
self.popMenu = QtGui.QMenu(self)
self.set_key_action = QtGui.QAction("Set Key at Current Frame", self)
self.set_key_action.triggered.connect(self.set_key)
self.popMenu.addAction(self.set_key_action)
def set_key(self):
le = self.set_key_action.property("lineedit")
# or
# le = self.set_key_action.data()
if le is self.ui.xLineEdit:
print("xLineEdit")
elif le is self.ui.yLineEdit:
print("yLineEdit")
elif le is self.ui.zLineEdit:
print("zLineEdit")
def custom_menu(self, p):
if self.sender() is not None:
self.set_key_action.setProperty("lineedit", self.sender())
# or
# self.set_key_action.setData(self.sender())
self.popMenu.exec_(QtGui.QCursor.pos())
Without debug or source code , i can't figure out what is happen here , because in theory all works , so or i can't understand correctly or have some error in other part of code.
class MyTool(QtGui.QWidget):
def __init__(self, parent=None):
super(MyTool, self).__init__(parent = parent)
# Read off from convert uic file.
self.ui = Ui_MyWidget()
self.ui.setupUi(self)
# translateX
self.ui.xLineEdit.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
self.ui.xLineEdit.customContextMenuRequested.connect(self.custom_menu)
# translateY
self.ui.yLineEdit.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
self.ui.yLineEdit.customContextMenuRequested.connect(self.custom_menu)
# translateZ
self.ui.zLineEdit.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
self.ui.zLineEdit.customContextMenuRequested.connect(self.custom_menu)
self.popMenu = QtGui.QMenu(self)
set_key_action = QtGui.QAction("Set Key at Current Frame", self)
**# Assuming that this phase pass !**
set_key_action.triggered.connect(self.set_key)
self.popMenu.addAction(set_key_action)
...
...
def set_key(self, attr):
**# What happen when you debug this block ?**
current_item = "|locator1"
cmds.setKeyframe("{0}.{1}".format(current_item, attr))
def custom_menu(self, point):
self.popMenu.exec_(QtGui.QCursor.pos())

Accessing to QListWidgetItem from widget inside

I'm trying to figure out how I can get the QWidget that I insert into a QListWidget as a QListWidgetItem to be able to access the list it is a part of so that it can do the following:
Increase/decrease it's position in the list
Remove itself from the list
Pass information from it's own class to a function in the main class
My script layout is a main.py which is where the MainWindow class is. The MainWindow uses the class generated from the main ui file. I also have the custom widget which is it's own class.
Example of GUI:
Relevant code snippets:
main.py
from PyQt4.QtGui import QMainWindow, QApplication
from dungeonjournal import Ui_MainWindow
from creature_initiative_object import InitCreatureObject
from os import walk
class MainWindow(QMainWindow, Ui_MainWindow):
def __init__(self, parent=None):
super(QMainWindow, self).__init__(parent)
self.setupUi(self)
etc......
def AddToInitiative(self):
creature = self.comboBoxSelectCharacter.currentText()
if(creature):
creatureInfo = ''
with open("creatures/"+str(creature)+".creature", "r") as f:
creatureInfo = f.read()
creatureInfo = creatureInfo.split("|")
customWidget = InitCreatureObject()
customWidgetItem = QtGui.QListWidgetItem(self.initiativeList)
customWidgetItem.setSizeHint(QtCore.QSize(400,50))
self.initiativeList.addItem(customWidgetItem)
self.initiativeList.setItemWidget(customWidgetItem, customWidget)
customWidget.setName(creatureInfo[0])
return
creature_initiative_object.py
class Ui_InitCreatureObject(object):
def setupUi(self, InitCreatureObject):
etc...
class InitCreatureObject(QtGui.QWidget, Ui_InitCreatureObject):
def __init__(self, parent=None, f=QtCore.Qt.WindowFlags()):
QtGui.QWidget.__init__(self, parent, f)
self.setupUi(self)
Edit 1:
To clarify again, I need to be able to use the buttons in the widget to modify the position of itself in the list. The list is part of the main ui. The buttons for up arrow, down arrow, Select, and Remove are the one's I'm trying to get to interact with things outside of their class.
The function they call needs to be able to determine which listItem is being called, be able to modify the list.
For example, if I click remove, it then needs to know which item in the list to remove. So it needs to first know what the list is, then it needs to know what item it is. I'm not sure how to access the instance of the widget that is occupying that listitem. I also am not sure how to get that listitem based on a button press from inside that listitem's class.
Edit 2:
Per the first answer I tried to work that into my code.
main.py had the following function added
def RemoveItem(self):
cwidget = self.sender().parent()
item = self.initiativeList.itemAt(cwidget.pos())
row = self.initiativeList.row(item)
self.initiativeList.takeItem(row)
print(row)
creature_initiative_object.py had the following added to the InitCreatureObject class
class InitCreatureObject(QtGui.QWidget, Ui_InitCreatureObject):
def __init__(self, parent=None, f=QtCore.Qt.WindowFlags()):
QtGui.QWidget.__init__(self, parent, f)
self.setupUi(self)
self.mainwidget = main.MainWindow()
self.btnRemove.clicked.connect(self.mainwidget.RemoveItem)
Item is still not being passed. The parent object seems to be right but when I get the row it always says -1.
The strategy to get the QTableWidgetItem is to use the itemAt() method but for this you must know the position of some point within the QTableWidgetItem.
Since the main objective is to get the item when a signal is sent, then the connected slot is used, so I recommend connecting all the signals to that slot. Given the above the following steps are taken:
Get the object that emits the signal through sender().
Get the sender parent() since this will be the custom widget that was added to the QListWidget() along with the item.
Get the position of the custom widget through pos(), this is the position that should be used in the itemAt() method.
Then you get the text of the button or some parameter that tells me the task to know what action you want to do.
The above can be implemented as follows:
def someSlot(self):
p = self.sender().parent()
it = self.lw.itemAt(p.pos())
text = self.sender().text()
if text == "task1":
do task1
elif text == "task2":
do task2
From the above, the following example is proposed:
class CustomWidget(QWidget):
def __init__(self, text, parent=None):
QWidget.__init__(self, parent)
self.setLayout(QHBoxLayout())
self.buttons = []
vb = QVBoxLayout()
self.layout().addLayout(vb)
self.btnTask1 = QPushButton("task1")
self.btnTask2 = QPushButton("task2")
vb.addWidget(self.btnTask1)
vb.addWidget(self.btnTask2)
self.buttons.append(self.btnTask1)
self.buttons.append(self.btnTask2)
self.btnTask3 = QPushButton("task3")
self.btnTask4 = QPushButton("task4")
self.btnTask5 = QPushButton("task5")
self.btnTask6 = QPushButton("task6")
self.layout().addWidget(self.btnTask3)
self.layout().addWidget(self.btnTask4)
self.layout().addWidget(self.btnTask5)
self.layout().addWidget(self.btnTask6)
self.buttons.append(self.btnTask3)
self.buttons.append(self.btnTask4)
self.buttons.append(self.btnTask5)
self.buttons.append(self.btnTask6)
class MainWindow(QMainWindow):
def __init__(self, parent=None):
QMainWindow.__init__(self, parent)
self.lw = QListWidget(self)
self.setCentralWidget(self.lw)
for i in range(5):
cw = CustomWidget("{}".format(i))
for btn in cw.buttons:
btn.clicked.connect(self.onClicked)
item = QListWidgetItem(self.lw)
item.setSizeHint(QSize(400, 80))
self.lw.addItem(item)
self.lw.setItemWidget(item, cw)
def onClicked(self):
p = self.sender().parent()
it = self.lw.itemAt(p.pos())
row = self.lw.row(it)
text = self.sender().text()
print("item {}, row {}, btn: {}".format(it, row, text))
#if text == "task1":
# do task1
#elif text == "task2":
# do task2
if __name__ == '__main__':
app = QApplication(sys.argv)
w = MainWindow()
w.show()
sys.exit(app.exec_())
In your Case:
class MainWindow(QMainWindow, Ui_MainWindow):
[...]
def AddToInitiative(self):
[...]
customWidget = InitCreatureObject()
customWidget.btnRemove.clicked.connect(self.RemoveItem)
# ^^^^^
[...]
def RemoveItem(self):
cwidget = self.sender().parent()
item = self.initiativeList.itemAt(cwidget.pos())
row = self.initiativeList.row(item)
self.initiativeList.takeItem(row)
print(row)

Inserted tabs not showing in QTabWidget despite show() called

I have a QTabWidget in PyQt which provides the possibility to tear off tabs and the re-attach them when they are closed. It works well except that newly attached tabs aren't immediately showing. A blank widget is showing and the widget is shown only after the current tab has been changed and then changed back.
I've searched StackOverflow and the answers to similar problems have pointed out that the added widget's show()-method needs to be called after the new tab is added. I tried that but the newly added tab is still not showing.
Slimmed down example code:
from PyQt4 import QtGui, QtCore
class DetachableTabWidget(QtGui.QTabWidget):
""" Subclass of QTabWidget which provides the ability to detach
tabs and making floating windows from them which can be reattached.
"""
def __init__(self, *args, **kwargs):
super(DetachableTabWidget, self).__init__(*args, **kwargs)
self.setTabBar(_DetachableTabBar())
def detach_tab(self, i):
""" Make floating window of tab.
:param i: index of tab to detach
"""
teared_widget = self.widget(i)
widget_name = self.tabText(i)
# Shift index to the left and remove tab.
self.setCurrentIndex(self.currentIndex() - 1 if self.currentIndex() > 0 else 0)
self.removeTab(i)
# Store widgets window-flags and close event.
teared_widget._flags = teared_widget.windowFlags()
teared_widget._close = teared_widget.closeEvent
# Make stand-alone window.
teared_widget.setWindowFlags(QtCore.Qt.Window)
teared_widget.show()
# Redirect windows close-event into reattachment.
teared_widget.closeEvent = lambda event: self.attach_tab(teared_widget, widget_name)
def attach_tab(self, widget, name):
""" Attach widget when receiving signal from child-window.
:param widget: :class:`QtGui.QWidget`
:param name: name of attached widget
"""
widget.setWindowFlags(widget._flags)
widget.closeEvent = widget._close
self.addTab(widget, name)
self.setCurrentWidget(widget)
self.currentWidget().show()
class _DetachableTabBar(QtGui.QTabBar):
def __init__(self, *args, **kwargs):
super(_DetachableTabBar, self).__init__(*args, **kwargs)
self._start_drag_pos = None
self._has_dragged = False
self.setMovable(True)
def mousePressEvent(self, event):
# Keep track of where drag-starts.
self._start_drag_pos = event.globalPos()
super(_DetachableTabBar, self).mousePressEvent(event)
def mouseMoveEvent(self, event):
# If tab is already detached, do nothing.
if self._has_dragged:
return
# Detach-tab if drag in y-direction is large enough.
if abs((self._start_drag_pos - event.globalPos()).y()) >= QtGui.QApplication.startDragDistance()*8:
self._has_dragged = True
self.parent().detach_tab(self.currentIndex())
def mouseReleaseEvent(self, event):
self._has_dragged = False
if __name__ == '__main__':
import sys
app = QtGui.QApplication(sys.argv)
window = QtGui.QMainWindow()
widget = DetachableTabWidget()
widget.addTab(QtGui.QLabel('Tab 1'), 'tab 1')
widget.addTab(QtGui.QLabel('Tab 2'), 'tab 2')
window.setCentralWidget(widget)
window.show()
sys.exit(app.exec_())
It seems than when you accept the QCloseEvent (default behavior) the widget is hidden at the end of your closeEvent handler. But your call to show() occurs before the end of the handler. The solution is to ignore the QCloseEvent.
...
teared_widget.closeEvent = lambda event: self.attach_tab(teared_widget, widget_name, event)
def attach_tab(self, widget, name, event):
""" Attach widget when receiving signal from child-window.
:param widget: :class:`QtGui.QWidget`
:param name: name of attached widget
:param event: close Event
"""
widget.setWindowFlags(widget._flags)
widget.closeEvent = widget._close
self.addTab(widget, name)
self.setCurrentWidget(widget)
event.ignore()

How can I get right-click context menus for clicks in QTableView header?

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

Categories

Resources