Forgive me if the question has already been asked, but I couldn't find the answer anywhere.
I am trying to test a small gui that contains a QListWidget and a QTreeWidget.
More specifically, I want to test the drag and drop behavior from one of the QListWidgetItem of the QListWidget to the QTreeWidget.
The ui works as intented but the problem comes when testing the drag and drop behavior as I am attempting to use QTest.mousePress() on the item, as this method only takes a QWidget as an input, and not a QListWidgetItem.
import sys
from PySide2 import QtWidgets, QtGui, QtCore
def startup():
app = QtWidgets.QApplication(sys.argv)
main_window = MainWindow()
main_window.show()
sys.exit(app.exec_())
class MainWindow(QtWidgets.QDialog):
def __init__(self, parent=None):
super(MainWindow, self).__init__(parent)
main_layout = QtWidgets.QHBoxLayout(self)
self.source = SourceList()
main_layout.addWidget(self.source)
self.destination = DestinationTree()
main_layout.addWidget(self.destination)
self.add_items(self.source)
def add_items(self, list_widget):
items = ['apple', 'banana']
for item in items:
list_widget_item = QtWidgets.QListWidgetItem(item)
list_widget.addItem(list_widget_item)
def item_count(self):
number_of_items = 0
iterator = QtWidgets.QTreeWidgetItemIterator(self)
while iterator.value():
number_of_items += 1
iterator += 1
return number_of_items
class SourceList(QtWidgets.QListWidget):
def __init__(self, parent=None):
super(SourceList, self).__init__(parent)
self.setViewMode(QtWidgets.QListWidget.IconMode)
self.setDragEnabled(True)
def startDrag(self, supportedActions):
items = self.selectedItems()
name = items[0].text()
drag = QtGui.QDrag(self)
ba = bytearray(name, 'utf-8')
drag_mime_data = QtCore.QMimeData()
drag_mime_data.setData('MoveQComponentItem', QtCore.QByteArray(ba))
drag.setMimeData(drag_mime_data)
drag.exec_(QtCore.Qt.MoveAction)
class DestinationTree(QtWidgets.QTreeWidget):
def __init__(self, parent=None):
super(DestinationTree, self).__init__(parent)
self.setAcceptDrops(True)
def dragEnterEvent(self, drag_event):
mime_data = drag_event.mimeData()
if mime_data.hasFormat('MoveQComponentItem'):
drag_event.acceptProposedAction()
def dragMoveEvent(self, drag_event):
return
def dragLeaveEvent(self, drag_event):
return
def dropEvent(self, drop_event):
print('Entering Drop event')
byte_array = drop_event.mimeData().data('MoveQComponentItem')
name = byte_array.data().decode('utf8')
print(name)
item = QtWidgets.QTreeWidgetItem()
item.setText(0, name)
self.addTopLevelItem(item)
def item_count(self):
number_of_items = 0
iterator = QtWidgets.QTreeWidgetItemIterator(self)
while iterator.value():
number_of_items += 1
iterator += 1
return number_of_items
if __name__ == '__main__':
startup()
I'd like to test this with something similar to this :
def test_shouldAddOneWidgetToTheTree_whenDragingFromListItemToTree(self):
my_q_list_widget_item = main_window.source.item(0)
tree_widget = main_window.destination
QtTest.QTest.mousePress(my_q_list_widget_item, QtCore.Qt.LeftButton)
QtTest.QTest.mouseMove(tree_widget)
QtTest.QTest.mouseRelease(tree_widget, QtCore.Qt.LeftButton)
count = tree_widget.item_count()
assert count == 1
Ideally I'd need a solution that works both on python 2 and 3, also if that helps, I'm using pytest.
Any idea would be greatly appreciated :)
Related
I was inspired by this repo to add custom tooltip text to items when they are added to the QListWidget. However, I only want the tooltip message to appear when the item is chosen. How would I implement this?
Here is a GUI example that I have to test this feature:
import serial, time, sys
import serial.tools.list_ports
from PyQt5 import QtGui, QtWidgets, QtCore
import json, time
class GUI(QtWidgets.QWidget):
def __init__(self):
super().__init__()
self.__init_ui()
def closeEvent(self, event):
super().closeEvent(event)
def __init_ui(self):
self.setWindowTitle('QListWidgetToolTipDemo')
self.history_log = HistoryList()
self.history_log.itemDoubleClicked.connect(self.history_item_selected)
self.history_log.returnPressed.connect(self.history_item_selected)
self.history_log.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
self.line_edit = QtWidgets.QLineEdit()
self.line_edit.returnPressed.connect(self.populate_history)
self.middle_layout = QtWidgets.QHBoxLayout()
self.middle_layout.addWidget(self.history_log)
self.middle_layout.addWidget(self.line_edit)
middle_layout_wrapper = QtWidgets.QWidget()
middle_layout_wrapper.setLayout(self.middle_layout)
middle_layout_wrapper.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed)
# Sets full GUI layout
gui_layout = QtWidgets.QVBoxLayout()
gui_layout.addWidget(middle_layout_wrapper)
gui_layout.setAlignment(QtCore.Qt.AlignmentFlag.AlignTop)
gui_layout.setContentsMargins(QtCore.QMargins(0, 0, 0, 0))
self.setLayout(gui_layout)
def populate_history(self):
self.history_log.addItem(self.line_edit.text())
self.line_edit.clear()
def history_item_selected(self):
self.line_edit.setText(self.history_log.currentItem().text())
class HistoryList(QtWidgets.QListWidget):
returnPressed = QtCore.pyqtSignal()
def __init__(self) -> None:
super().__init__()
self.setSelectionMode(QtWidgets.QAbstractItemView.SingleSelection)
self.setMouseTracking(True)
self.itemEntered.connect(self.__showToolTip)
def keyPressEvent(self, ev):
super().keyPressEvent(ev)
if ev.key() in (QtCore.Qt.Key_Enter, QtCore.Qt.Key_Return):
self.returnPressed.emit()
def addItem(self, aitem) -> None:
item = ''
text = ''
if isinstance(aitem, str):
item = QtWidgets.QListWidgetItem()
text = aitem
item.setText(text)
elif isinstance(aitem, QtWidgets.QListWidgetItem):
item = aitem
text = item.text()
self.setItemWidget(item, QtWidgets.QWidget())
super().addItem(item)
def __showToolTip(self, item: QtWidgets.QListWidgetItem):
text = item.text()
text_width = self.fontMetrics().boundingRect(text).width()
width = self.width()
info = {"time":str(time.time()), "entry":text}
info = json.dumps(info, indent=4)
item.setToolTip(info)
if __name__ == "__main__":
app = QtWidgets.QApplication([])
console = GUI()
screensize = app.desktop().availableGeometry().size()
console.show()
exit(app.exec_())
Currently, the following is how a tooltip works for any item:
Example where only display tooltip when item is selected:
Expanding on the suggestion from musicamante in the comments, I was able to get tooltip to display only for the selected item by overriding the event method and watch for a tooltip event (inspired from this SO post)
import serial, time, sys
import serial.tools.list_ports
from PyQt5 import QtGui, QtWidgets, QtCore
import json, time
class GUI(QtWidgets.QWidget):
def __init__(self):
super().__init__()
self.__init_ui()
def closeEvent(self, event):
super().closeEvent(event)
def __init_ui(self):
self.setWindowTitle('QListWidgetToolTipDemo')
self.history_log = HistoryList()
self.history_log.itemDoubleClicked.connect(self.history_item_selected)
self.history_log.returnPressed.connect(self.history_item_selected)
self.history_log.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
self.line_edit = QtWidgets.QLineEdit()
self.line_edit.returnPressed.connect(self.populate_history)
self.middle_layout = QtWidgets.QHBoxLayout()
self.middle_layout.addWidget(self.history_log)
self.middle_layout.addWidget(self.line_edit)
middle_layout_wrapper = QtWidgets.QWidget()
middle_layout_wrapper.setLayout(self.middle_layout)
middle_layout_wrapper.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed)
# Sets full GUI layout
gui_layout = QtWidgets.QVBoxLayout()
gui_layout.addWidget(middle_layout_wrapper)
gui_layout.setAlignment(QtCore.Qt.AlignmentFlag.AlignTop)
gui_layout.setContentsMargins(QtCore.QMargins(0, 0, 0, 0))
self.setLayout(gui_layout)
def populate_history(self):
self.history_log.addItem(self.line_edit.text())
self.line_edit.clear()
def history_item_selected(self):
self.line_edit.setText(self.history_log.currentItem().text())
class HistoryList(QtWidgets.QListWidget):
returnPressed = QtCore.pyqtSignal()
def __init__(self) -> None:
super().__init__()
self.setSelectionMode(QtWidgets.QAbstractItemView.SingleSelection)
self.setMouseTracking(True)
self.itemEntered.connect(self.__addToolTip)
self.items_tooltip_info_dict = dict()
def keyPressEvent(self, ev):
super().keyPressEvent(ev)
if ev.key() in (QtCore.Qt.Key_Enter, QtCore.Qt.Key_Return):
self.returnPressed.emit()
def event(self, e: QtCore.QEvent) -> bool:
if e.type() == QtCore.QEvent.ToolTip:
if not self.selectedItems():
QtWidgets.QToolTip.hideText()
return True
else:
index = self.indexFromItem(self.currentItem()).row()
info = json.dumps(self.items_tooltip_info_dict[index], indent=4)
QtWidgets.QToolTip.showText(e.globalPos(),info)
e.accept()
return super().event(e)
def addItem(self, aitem) -> None:
item = ''
text = ''
if isinstance(aitem, str):
item = QtWidgets.QListWidgetItem()
text = aitem
item.setText(text)
elif isinstance(aitem, QtWidgets.QListWidgetItem):
item = aitem
text = item.text()
self.setItemWidget(item, QtWidgets.QWidget())
super().addItem(item)
def __addToolTip(self, item: QtWidgets.QListWidgetItem):
text = item.text()
info = {"time":str(time.time()), "entry":text}
index = self.indexFromItem(item).row()
self.items_tooltip_info_dict[index] = info
if __name__ == "__main__":
app = QtWidgets.QApplication([])
console = GUI()
screensize = app.desktop().availableGeometry().size()
console.show()
exit(app.exec_())
With PyQt4, I am using a QtableView with more than 10 columns. The user must have the choice of showing/hiding a column.
This is generally done by adding a small button in the top-right of the table's header. The button shows a menu with checked/unchecked Checkboxes allowing to hide/show columns.
This is an example from Sqlite-Manager Table.
So, I wonder how can I do the same with PyQt's QtableView?
Thanks,
Thank you Kitsune Meyoko, it was a great Idea.. ;)
I found another solution pretty much like yours by using QMenu with Checkable QActions instead of a QPushButton: Let's Go:
import sys
import string
from PyQt4.QtCore import *
from PyQt4.QtGui import *
class Header(QHeaderView):
def __init__(self, parent=None):
super(Header, self).__init__(Qt.Horizontal, parent)
self.setContextMenuPolicy(Qt.CustomContextMenu)
self.customContextMenuRequested.connect(self.ctxMenu)
self.setup()
#pyqtSlot(bool)
def printID(self, i):
print("id")
if i == False:
self.hideSection(0)
else:
self.showSection(0)
#pyqtSlot(bool)
def printNAME(self, i):
print("name")
if i == False:
self.hideSection(1)
else:
self.showSection(1)
#pyqtSlot(bool)
def printUSERNAME(self, i):
print("username")
if i == False:
self.hideSection(2)
else:
self.showSection(2)
def setup(self):
self.id = QAction("id",self)
self.id.setCheckable(True)
self.id.setChecked(True)
self.connect(self.id, SIGNAL("triggered(bool)"), self, SLOT("printID(bool)"))
self.name = QAction("name",self)
self.name.setCheckable(True)
self.name.setChecked(True)
self.connect(self.name, SIGNAL("triggered(bool)"), self, SLOT("printNAME(bool)"))
self.username = QAction("username",self)
self.username.setCheckable(True)
self.username.setChecked(True)
self.connect(self.username, SIGNAL("triggered(bool)"), self, SLOT("printUSERNAME(bool)"))
def ctxMenu(self, point):
menu = QMenu(self)
self.currentSection = self.logicalIndexAt(point)
menu.addAction(self.id)
menu.addAction(self.name)
menu.addAction(self.username)
menu.exec_(self.mapToGlobal(point))
class Table(QTableWidget):
def __init__(self, parent=None):
super(Table, self).__init__(parent)
self.setHorizontalHeader(Header(self))
self.setColumnCount(3)
self.setHorizontalHeaderLabels(['id', 'name', 'username'])
self.populate()
def populate(self):
self.setRowCount(10)
for i in range(10):
for j,l in enumerate(string.ascii_letters[:3]):
self.setItem(i, j, QTableWidgetItem(l))
if __name__ == '__main__':
app = QApplication(sys.argv)
t = Table()
t.show()
app.exec_()
sys.exit()
In QTableView not have kind of button just like "Sqlite-Manager Table". But your can custom widget by using QtGui.QPushButton and work with QtGui.QMenu together to get column from user. And use QTableView.hideColumn (self, int column) & QTableView.showColumn (self, int column) to hide show your column;
Full example;
import sys
import random
from functools import partial
from PyQt4 import QtGui
class QCustomTableViewWidget (QtGui.QWidget):
def __init__ (self, myQStandardItemModel, *args, **kwargs):
super(QCustomTableViewWidget, self).__init__(*args, **kwargs)
# Layout setup
self.localQTableView = QtGui.QTableView()
self.rightCornerQPushButton = QtGui.QPushButton()
menuQHBoxLayout = QtGui.QHBoxLayout()
menuQHBoxLayout.addStretch(1)
menuQHBoxLayout.addWidget(self.rightCornerQPushButton)
allQVBoxLayout = QtGui.QVBoxLayout()
allQVBoxLayout.addLayout(menuQHBoxLayout)
allQVBoxLayout.addWidget(self.localQTableView)
self.setLayout(allQVBoxLayout)
# Object setup
self.localQTableView.setModel(myQStandardItemModel)
self.rightCornerQPushButton.setText('Show column')
currentQMenu = QtGui.QMenu()
for column in range(myQStandardItemModel.columnCount()):
currentQAction = QtGui.QAction('Column %d' % (column + 1), currentQMenu)
currentQAction.setCheckable(True)
currentQAction.setChecked(True)
currentQAction.toggled.connect(partial(self.setColumnVisible, column))
currentQMenu.addAction(currentQAction)
self.rightCornerQPushButton.setMenu(currentQMenu)
def setColumnVisible (self, column, isChecked):
if isChecked:
self.localQTableView.showColumn(column)
else:
self.localQTableView.hideColumn(column)
def tableView (self):
return self.localQTableView
# Simulate data
myQStandardItemModel = QtGui.QStandardItemModel()
for _ in range(10):
myQStandardItemModel.appendRow([QtGui.QStandardItem('%d' % random.randint(100, 999)), QtGui.QStandardItem('%d' % random.randint(100, 999)), QtGui.QStandardItem('%d' % random.randint(100, 999))])
# Main application
myQApplication = QtGui.QApplication(sys.argv)
myQCustomTableViewWidget = QCustomTableViewWidget(myQStandardItemModel)
myQCustomTableViewWidget.show()
sys.exit(myQApplication.exec_())
I need to create a custom signal on Qmdisubwindow close. In other word, when I closed any subwindow, a signal is emitted with the name of that window being closed. Below is my trail, but seems not right. Error occurs as:
a subwindow already created without calling
add subwindow option is not working
closable action is not working
Hope you can show me how to fix it.
import sys
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
class MyMdi(QMdiSubWindow):
sigClosed = pyqtSignal(str)
def __init__(self, parent=None):
super(MyMdi, self).__init__(parent)
def closeEvent(self, event):
"""Get the name of active window about to close
"""
name = name
self.sigClosed.emit('{} is close'.format(name))
QMdiSubWindow.closeEvent(self, event)
class MainWindow(QMainWindow):
count = 0
def __init__(self, parent = None):
super(MainWindow, self).__init__(parent)
self.mdi = MyMdi()
self.setCentralWidget(self.mdi)
bar = self.menuBar()
file = bar.addMenu("File")
file.addAction("New")
file.triggered[QAction].connect(self.windowaction)
self.setWindowTitle("MDI demo")
# my signal
self.mdi.sigClosed.connect(self.windowclosed)
#pyqtSlot(str)
def windowclosed(self, text):
print(text)
def windowaction(self, q):
if q.text() == "New":
MainWindow.count = MainWindow.count+1
sub = QMdiSubWindow()
sub.setWidget(QTextEdit())
sub.setWindowTitle("subwindow"+str(MainWindow.count))
self.mdi.addSubWindow(sub)
sub.show()
def main():
app = QApplication(sys.argv)
ex = MainWindow()
ex.show()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
You have an initial error: a QMdiSubWindow must be inside a QMdiArea but there is none in your code.
On the other hand, the idea of subclassing is good but you have several drawbacks:
You are not using it initially since there is no QMdiArea, if you execute the QAction then your application will be closed because a QMdiSubWindow does not have any method called addSubWindow.
The QMdiSubWindow does not have an attribute called name, you must use windowTitle.
class MdiSubWindow(QMdiSubWindow):
sigClosed = pyqtSignal(str)
def closeEvent(self, event):
"""Get the name of active window about to close
"""
self.sigClosed.emit(self.windowTitle())
QMdiSubWindow.closeEvent(self, event)
class MainWindow(QMainWindow):
count = 0
def __init__(self, parent=None):
super(MainWindow, self).__init__(parent)
self.mdi = QMdiArea()
self.setCentralWidget(self.mdi)
bar = self.menuBar()
file = bar.addMenu("File")
file.addAction("New")
file.triggered[QAction].connect(self.windowaction)
self.setWindowTitle("MDI demo")
#pyqtSlot(str)
def windowclosed(self, text):
print(text)
def windowaction(self, q):
if q.text() == "New":
MainWindow.count = MainWindow.count + 1
sub = MdiSubWindow()
sub.setWidget(QTextEdit())
sub.setAttribute(Qt.WA_DeleteOnClose)
sub.setWindowTitle("subwindow" + str(MainWindow.count))
sub.sigClosed.connect(self.windowclosed)
self.mdi.addSubWindow(sub)
sub.show()
I have a number of QTreeWidget. Here, there are two trees.
the left one has "a" , "b".
I want to drag this item into the right tree.
I have no error but the item become empty.
How should I do for dragging the left data to the right tree?
and why?
data is this.
b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\n\x00\x00\x00\x00\x02\x00a\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\n\x00\x00\x00\x00\x02\x00b'
from PySide import QtCore
from PySide import QtGui
import sys
class MainWindow(QtGui.QMainWindow):
def __init__(self, parent=None):
super(MainWindow, self).__init__(parent=None)
self.sequoia = Sequoia()
self.baobab = Baobab()
self.c_widget = QtGui.QWidget()
h_boxlayout = QtGui.QHBoxLayout()
h_boxlayout.addWidget(self.sequoia, 30)
h_boxlayout.addWidget(self.baobab, 70)
self.c_widget.setLayout(h_boxlayout)
self.setCentralWidget(self.c_widget)
class Sequoia(QtGui.QTreeWidget):
def __init__(self, parent=None):
super(Sequoia, self).__init__(parent=None)
self.setColumnCount(2)
self.setAcceptDrops(True)
self.setDragDropMode(QtGui.QAbstractItemView.InternalMove)
self.sampleitem = QtGui.QTreeWidgetItem()
self.sampleitem.setText(0, "a")
self.sampleitem.setText(1, "b")
self.addTopLevelItem(self.sampleitem)
class Baobab(QtGui.QTreeWidget):
def __init__(self, parent=None):
super(Baobab, self).__init__(parent=None)
self.setDragDropMode(QtGui.QAbstractItemView.InternalMove)
self.setColumnCount(2)
def dragEnterEvent(self, event):
if event.mimeData().hasFormat('application/x-qabstractitemmodeldatalist'):
event.accept()
return QtGui.QTreeWidget.dragEnterEvent(self, event)
def dragMoveEvent(self, event):
if event.mimeData().hasFormat('application/x-qabstractitemmodeldatalist') and not isinstance(event, QtGui.QDropEvent):
event.accept()
return QtGui.QTreeWidget.dragMoveEvent(self, event)
def dropEvent(self, event):
if event.mimeData().hasFormat('application/x-qabstractitemmodeldatalist'):
bytearray = event.mimeData().data('application/x-qabstractitemmodeldatalist')
datastream = QtCore.QDataStream(bytearray, QtCore.QIODevice.ReadOnly)
print(3306, bytearray.data())
item = QtGui.QTreeWidgetItem()
item.setFlags(QtCore.Qt.ItemFlag.ItemIsEditable|QtCore.Qt.ItemFlag.ItemIsEnabled|QtCore.Qt.ItemFlag.ItemIsSelectable|QtCore.Qt.ItemIsDragEnabled|QtCore.Qt.ItemIsDropEnabled)
item.read(datastream)
self.addTopLevelItem(item)
def main():
try:
QtGui.QApplication([])
except Exception as e:
print(e)
mw = MainWindow()
mw.show()
sys.exit(QtGui.QApplication.exec_())
if __name__ == "__main__":
main()
It is not necessary to implement your own drag-and-drop method between in QTreeWidget, you just have to configure it correctly:
from PySide import QtCore, QtGui
import sys
class MainWindow(QtGui.QMainWindow):
def __init__(self, parent=None):
super(MainWindow, self).__init__(parent=None)
self.sequoia = Sequoia()
self.baobab = Baobab()
self.c_widget = QtGui.QWidget()
h_boxlayout = QtGui.QHBoxLayout(self.c_widget)
self.setCentralWidget(self.c_widget)
h_boxlayout.addWidget(self.sequoia, 30)
h_boxlayout.addWidget(self.baobab, 70)
class Sequoia(QtGui.QTreeWidget):
def __init__(self, parent=None):
super(Sequoia, self).__init__(parent=None)
self.setColumnCount(2)
self.setDefaultDropAction(QtCore.Qt.CopyAction)
self.setDragDropMode(QtGui.QAbstractItemView.InternalMove)
self.setAcceptDrops(True)
self.sampleitem = QtGui.QTreeWidgetItem()
self.sampleitem.setText(0, "a")
self.sampleitem.setText(1, "b")
self.addTopLevelItem(self.sampleitem)
class Baobab(QtGui.QTreeWidget):
def __init__(self, parent=None):
super(Baobab, self).__init__(parent=None)
self.setColumnCount(2)
self.setAcceptDrops(True)
def main():
app = QtGui.QApplication(sys.argv)
mw = MainWindow()
mw.show()
sys.exit(app.exec_())
if __name__ == "__main__":
main()
If you still want to implement it manually then if we use your perspective a possible solution is:
def dropEvent(self, event):
if event.mimeData().hasFormat(
"application/x-qabstractitemmodeldatalist"
):
ba = event.mimeData().data(
"application/x-qabstractitemmodeldatalist"
)
ds = QtCore.QDataStream(
ba, QtCore.QIODevice.ReadOnly
)
i = 0
item = QtGui.QTreeWidgetItem()
while not ds.atEnd():
row = ds.readInt32()
column = ds.readInt32()
map_items = ds.readInt32()
self.addTopLevelItem(item)
for _ in range(map_items):
role = ds.readInt32()
value = ds.readQVariant()
item.setData(i, role, value)
i = (i + 1) % self.columnCount()
But the above is forced, a better solution is to use the dropMimeData method of the model:
def dropEvent(self, event):
if event.mimeData().hasFormat(
"application/x-qabstractitemmodeldatalist"
):
parent = self.indexAt(event.pos())
self.model().dropMimeData(
event.mimeData(), event.dropAction(), 0, 0, parent
)
I have a Qmenu that I am creating by loading a list with Qsettings and I am trying to be able to remove items from the menu by loading the list in a QListQWidget and deleting the selected items. Currently I am able to delete the menu items in the list widget and it removes them from qsettings as well but I can't figure out how to remove the items from the menu without restarting. I have tried various things such as removeAction etc but haven't been able to figure it out.
Here is my code:
import functools
import sys
from PyQt5 import QtCore
from PyQt5.QtWidgets import QWidget, QPushButton, QHBoxLayout, \
QApplication, QAction, QMenu, QListWidgetItem, \
QListWidget, QGridLayout
class MainWindow(QWidget):
settings = QtCore.QSettings('test_org', 'my_app')
def __init__(self, parent=None):
super(MainWindow, self).__init__(parent)
self.layout = QHBoxLayout()
self.menu_btn = QPushButton()
self.menu = QMenu()
self.add_menu = self.menu.addMenu("Menu")
self.menu_btn.setMenu(self.menu)
self.open_list_btn = QPushButton('open list')
self.load_items = self.settings.value('menu_items', [])
for item in self.load_items:
self.action = QAction(item[0], self)
self.action.setData(item)
self.add_menu.addAction(self.action)
self.action.triggered.connect(functools.partial(self.menu_clicked, self.action))
self.layout.addWidget(self.menu_btn)
self.layout.addWidget(self.open_list_btn)
self.setLayout(self.layout)
self.open_list_btn.clicked.connect(self.open_window)
def open_window(self):
self.create_menu_item = List()
self.create_menu_item.show()
def menu_clicked(self, item):
itmData = item.data()
print(itmData)
class List(QWidget):
settings = QtCore.QSettings('test_org', 'my_app')
def __init__(self, parent=None):
super(List, self).__init__(parent)
self.menu_items = self.settings.value('menu_items', [])
self.layout = QGridLayout()
self.list = QListWidget()
self.remove_btn = QPushButton('Remove')
self.layout.addWidget(self.list, 1, 1, 1, 1)
self.layout.addWidget(self.remove_btn, 2, 1, 1, 1)
self.setLayout(self.layout)
self.remove_btn.clicked.connect(self.remove_items)
for item in self.menu_items:
self.item = QListWidgetItem()
self.item.setText(str(item[0]))
self.list.addItem(self.item)
def remove_items(self):
self.menu_items = self.settings.value('menu_items', [])
del self.menu_items[self.list.currentRow()]
self.settings.setValue('menu_items', self.menu_items)
listItems = self.list.selectedItems()
if not listItems: return
for item in listItems:
self.list.takeItem(self.list.row(item))
if __name__ == '__main__':
app = QApplication(sys.argv)
w = MainWindow()
w.show()
app.exec_()
Does anyone have any ideas?
EDIT:
This is the structure of the list in QSettings. I load the menu with this and I load the QlistWidget with this. I am trying to get the menu to remove the items as well when I remove them for the QListWidget.
mylist = ['item_name',['itemdata1', 'itemdata2', 'itemdata3'],
'item_name2',['itemdata1', 'itemdata2', 'itemdata3'],
'item_name3',['itemdata1', 'itemdata2', 'itemdata3']]
I think the data structure that you are using is incorrect because when I execute your code it generates twice as many QActions, the structure I propose is a dictionary where the keys are the name of the QAction and the value of the list of data:
{
'item0': ['itemdata00', 'itemdata01', 'itemdata02'],
'item1': ['itemdata10', 'itemdata11', 'itemdata12'],
...
}
To build the initial configuration use the following script:
create_settings.py
from PyQt5 import QtCore
if __name__ == '__main__':
settings = QtCore.QSettings('test_org', 'my_app')
d = {}
for i in range(5):
key = "item{}".format(i)
value = ["itemdata{}{}".format(i, j) for j in range(3)]
d[key] = value
settings.setValue('menu_items', d)
print(d)
settings.sync()
On the other hand I think that the widget that you want to handle the destruction of QActions should take over the corresponding QMenu as I show below:
import sys
from PyQt5 import QtCore, QtWidgets
class MainWindow(QtWidgets.QWidget):
def __init__(self, parent=None):
super(MainWindow, self).__init__(parent)
layout = QtWidgets.QHBoxLayout(self)
menu_btn = QtWidgets.QPushButton()
open_list_btn = QtWidgets.QPushButton('open list')
layout.addWidget(menu_btn)
layout.addWidget(open_list_btn)
menu = QtWidgets.QMenu()
menu_btn.setMenu(menu)
self.menu_manager = MenuManager("menu_items", "Menu")
menu.addMenu(self.menu_manager.menu)
self.menu_manager.menu.triggered.connect(self.menu_clicked)
open_list_btn.clicked.connect(self.menu_manager.show)
def menu_clicked(self, action):
itmData = action.data()
print(itmData)
class MenuManager(QtWidgets.QWidget):
def __init__(self, key, menuname, parent=None):
super(MenuManager, self).__init__(parent)
self.settings = QtCore.QSettings('test_org', 'my_app')
self.key = key
self.layout = QtWidgets.QVBoxLayout(self)
self.listWidget = QtWidgets.QListWidget()
self.remove_btn = QtWidgets.QPushButton('Remove')
self.layout.addWidget(self.listWidget)
self.layout.addWidget(self.remove_btn)
self.remove_btn.clicked.connect(self.remove_items)
self.menu = QtWidgets.QMenu(menuname)
load_items = self.settings.value(self.key, [])
for name, itemdata in load_items.items():
action = QtWidgets.QAction(name, self.menu)
action.setData(itemdata)
self.menu.addAction(action)
item = QtWidgets.QListWidgetItem(name)
item.setData(QtCore.Qt.UserRole, action)
self.listWidget.addItem(item)
def remove_items(self):
for item in self.listWidget.selectedItems():
it = self.listWidget.takeItem(self.listWidget.row(item))
action = it.data(QtCore.Qt.UserRole)
self.menu.removeAction(action)
self.sync_data()
def sync_data(self):
save_items = {}
for i in range(self.listWidget.count()):
it = self.listWidget.item(i)
action = it.data(QtCore.Qt.UserRole)
save_items[it.text()] = action.data()
self.settings.setValue(self.key, save_items)
self.settings.sync()
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
w = MainWindow()
w.show()
sys.exit(app.exec_())
I got it figured out. I am not sure of a better way but I did it using object names.
In the MainWindow I set objectNames to self.action using the first item of each list in the list of lists inside the for loop like this:
self.action.setObjectName(item[0])
Then I created this function in the MainWindow class:
def remove_menu_item(self, value):
self.add_menu.removeAction(self.findChild(QAction, value))
Then I added this:
w.remove_menu_item(item.text())
To the remove function in the List class to get the same first item in the list of lists which is now the objectName for the QActions.