I am trying to make a QTreeWidget that lets the user rearrange its elements, and if the user drags and drops a tree item to another widget I don't want the item to be deleted. To get this behavior, I'm trying to use setDropAction in dropEvent.
The code below successfully rejects drops from other widgets, and allows drops to other widgets without deleting the original, but it seems to break drag-and-drop within the tree - it causes the item to disappear when dropped.
https://www.screencast.com/t/driIjyg8ekzt
import sys
from PyQt5 import QtWidgets, QtGui
from PyQt5.QtCore import Qt
class MyTree(QtWidgets.QTreeWidget):
def __init__(self):
super().__init__()
self.setDragDropMode(self.DragDrop)
self.setSelectionMode(self.ExtendedSelection)
self.setSelectionBehavior(self.SelectRows)
self.setDefaultDropAction(Qt.CopyAction)
self.setAcceptDrops(True)
def dropEvent(self, e: QtGui.QDropEvent):
if e.source() is self:
print("move")
e.setDropAction(Qt.MoveAction)
e.accept()
super().dropEvent(e)
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
my_list = QtWidgets.QListWidget()
my_list.addItems(list('1234'))
my_list.show()
my_list.setDragEnabled(True)
my_list.setAcceptDrops(True)
my_tree = MyTree()
for item in list('abcd'):
QtWidgets.QTreeWidgetItem(my_tree, [item])
my_tree.show()
sys.exit(app.exec_())
If you just want to prevent drop from external sources, just use:
self.setDragDropMode(self.InternalMove)
And don't reimplement dropEvent().
Your code doesn't work properly mostly because you've set the event as accepted, and an item view ignores a drop event if it has already been accepted.
In your case, it would be better to do this:
def dzropEvent(self, e: QtGui.QDropEvent):
if e.source() != self:
# ignore the drop from other sources
return
e.setDropAction(Qt.MoveAction)
super().dropEvent(e)
But if you really want to ignore external drops, you should ignore the event right from the dragEnterEvent(), so that it's made also clear to the user that dropping is not allowed.
Related
I have a small GUI app made using pyqt5.
I found a strange problem while using eventFilter...
def eventFilter(self, source, event):
if event.type() == QtCore.QEvent.KeyPress:
# RETURN
if event.key() == QtCore.Qt.Key_Return:
if source is self.userLineEdit:
self.pswLineEdit.setFocus()
elif source is self.pswLineEdit:
self.LoginButton.click()
# TAB
elif event.key() == QtCore.Qt.Key_Tab:
if source is self.userLineEdit:
self.pswLineEdit.setFocus()
return super().eventFilter(source, event)
While pressing enter key just behave normally, tab key does not.
I don't know where the problem could be. I'm going to link a video to show the exact problem as I'm not able to describe how this is not working
Link to video
I know it's pixelated (sorry) but the important thing is the behavior of the cursor
SMALL EXAMPLE
import sys
from PyQt5.QtWidgets import QApplication, QWidget, QVBoxLayout, QLineEdit
from PyQt5 import QtCore
class App(QWidget):
def __init__(self):
super().__init__()
self.title = 'Hello, world!'
self.left = 10
self.top = 10
self.width = 640
self.height = 480
self.initUI()
def initUI(self):
self.setWindowTitle(self.title)
self.setGeometry(self.left, self.top, self.width, self.height)
self.userEdit = QLineEdit(self)
self.pswEdit = QLineEdit(self)
self.userEdit.setPlaceholderText("Username")
self.pswEdit.setPlaceholderText("Password")
self.userEdit.installEventFilter(self)
self.pswEdit.installEventFilter(self)
mainLayout = QVBoxLayout()
mainLayout.addWidget(self.userEdit)
mainLayout.addWidget(self.pswEdit)
self.setLayout(mainLayout)
self.show()
def eventFilter(self, source, event):
if event.type() == QtCore.QEvent.KeyPress:
# RETURN
if event.key() == QtCore.Qt.Key_Return:
if source is self.userEdit:
self.pswEdit.setFocus()
# TAB
elif event.key() == QtCore.Qt.Key_Tab:
if source is self.userEdit:
self.pswEdit.setFocus()
return super().eventFilter(source, event)
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = App()
sys.exit(app.exec_())
If you don't filter events, they are processed by the object and eventually propagated back to the parent.
By default, QWidget will try to focus the next (or previous, for Shift+Tab) child widget in the focus chain, by calling its focusNextPrevChild(). If it can do it, it will actually set the focus on that widget, otherwise the event is ignored and propagated to the parent.
Since most widgets (including QLineEdit) don't handle the tab keys for focus changes on their own, as they don't have children, their parent will receive it, which will call focusNextPrevChild() looking for another child widget, and so on, up to the object tree, until a widget finally can handle the key, eventually changing the focus.
In your case, this is what's happening:
you check events and find that the tab key event is received by the first line edit;
you set the focus on the other line edit, the password field;
you let the event be handled anyway by the widget, since you're not ignoring or filtering it out;
the first line edit calls focusNextPrevChild() but is not able to do anything with it;
the event is propagated to the parent, which then calls its own focusNextPrevChild();
the function checks the current focused child widget, which is the password field you just focused, and finds the next, which coincidentally is the first line edit, which gets focused again;
The simple solution is to just add return True after changing the focus, so that the event doesn't get propagated to the parent causing a further focus change:
if event.key() == QtCore.Qt.Key_Tab:
if source is self.userEdit:
self.pswEdit.setFocus()
return True
Note that overriding the focus behavior is quite complex, and you have to be very careful about how focus and events are handled, especially for specific widgets that might deal with events in particular ways (studying the Qt sources is quite useful for this), otherwise you'll get unexpected behavior or even fatal recursion.
For instance, there's normally no need for an event filter for the return key, as QLineEdit already provides the returnPressed signal:
self.userEdit.returnPressed.connect(self.pswEdit.setFocus)
Qt already has a quite thorough focus management system, if you just want more control over the way the tab chain works use existing functions like setTabOrder() on the parent or top level window, and if you want to have more control over how (or if) they get it, use setFocusPolicy().
I am having an issue that I haven't been able to figure out. When my table loads, selecting any first item and attempting to return it's index via selectedIndexes(), the index is empty. If I then click anywhere else, the method works as intended. Why? Here's some MRC:
from PyQt6.QtCore import pyqtSignal, Qt
from PyQt6.QtGui import QStandardItem, QStandardItemModel
from PyQt6 import QtWidgets as qt
import sys
class Main(qt.QMainWindow):
def __init__(self):
super().__init__()
frame = qt.QFrame()
self.setCentralWidget(frame)
laymb = qt.QVBoxLayout(frame)
model = QStandardItemModel()
tabmb = TableViewClick()
tabmb.setModel(model)
tabmb.setGridStyle(Qt.PenStyle(0))
tabmb.setSelectionMode(qt.QTableView.SelectionMode(1))
tabmb.setSelectionBehavior(tabmb.SelectionBehavior(1))
laymb.addWidget(tabmb)
subj = []
for e in range(10):
subj.append(QStandardItem('Testing...'))
model.appendColumn(subj)
tabmb.signal.connect(self._click_table)
def _click_table(self):
table = self.sender()
indexes = table.selectedIndexes()
print(indexes)
class TableViewClick(qt.QTableView):
signal = pyqtSignal()
def mousePressEvent(self, event):
self.signal.emit()
qt.QTableView.mousePressEvent(self, event)
app = qt.QApplication(sys.argv)
main_gui = Main()
main_gui.show()
sys.exit(app.exec())
As you can see, I've tied it to a mousePressEvent because I need to call _click_table for later code. I've troubleshot some and can select a row via selectRow(), and the above does not happen, but I don't want to do that because the GUI loads with a row already selected. Any ideas how you can get the first time you click anywhere in the table to return the index? With the index I'm going to get the row clicked for later code. If there's a better way, I'm open to it. But, I want to click an item and get the row.
I have a context menu with a checkbox and a spinbox. Both work as expected, except that when I am typing a number in the spinbox, I tend to hit enter to close the menu, but that also toggles the checkbox. Does anyone know how to prevent that?
minimal example:
import sys
from PyQt5.QtWidgets import QApplication, QWidget, QFrame, QMenu, QAction, QWidgetAction, QFormLayout, QSpinBox
from PyQt5.QtCore import Qt
t = True
v = 0
class SpinAction(QWidgetAction):
def __init__(self, parent):
super(SpinAction, self).__init__(parent)
w = QWidget()
layout = QFormLayout(w)
self.spin = QSpinBox()
layout.addRow('value', self.spin)
w.setLayout(layout)
self.setDefaultWidget(w)
def _menu(position):
menu = QMenu()
test_action = QAction(text="Test", parent=menu, checkable=True)
test_action.setChecked(t)
test_action.toggled.connect(toggle_test)
spin_action = SpinAction(menu)
spin_action.spin.setValue(v)
spin_action.spin.valueChanged.connect(spin_changed)
menu.addAction(test_action)
menu.addAction(spin_action)
action = menu.exec_(w.mapToGlobal(position))
def toggle_test(val):
global t
t = val
def spin_changed(val):
global v
v = val
app = QApplication(sys.argv)
w = QFrame()
w.setContextMenuPolicy(Qt.CustomContextMenu)
w.customContextMenuRequested.connect(_menu)
w.show()
sys.exit(app.exec_())
The widget must accept focus, so a proper focus policy (StrongFocus or WheelFocus) must be set, but since it's a container you should also set the focus proxy to the spin, so that it will handle the focus for it.
In this way the keyboard navigation of the menu will also work properly by using arrows or Tab to focus the spinbox.
The action also has to be set as the active action of the menu in order to prevent the menu to accept events that may trigger other actions, and a way to achieve that is to install an event filter on the spin and check for events that should make the widget action as the active one, like FocusIn and Enter event.
Note that this results in some unintuitive results from the UX perspective: by default, hovering a normal QAction makes it the active one, but the user might want to move the mouse away from the spinbox in order to edit it without having the mouse on it, and by doing it another action could become active; in the meantime the spin should still keep the focus for obvious reasons. The result is that the other action might seem visually selected while the spinbox has focus. There is no obvious solution for this, and that's why widgets used for QWidgetActions should always be chosen with care.
class SpinAction(QWidgetAction):
FocusEvents = QtCore.QEvent.FocusIn, QtCore.QEvent.Enter
ActivateEvents = QtCore.QEvent.KeyPress, QtCore.QEvent.Wheel)
WatchedEvents = FocusEvents + ActivateEvents
def __init__(self, parent):
# ...
w.setFocusPolicy(self.spin.focusPolicy())
w.setFocusProxy(self.spin)
self.spin.installEventFilter(self)
def eventFilter(self, obj, event):
if obj == self.spin and event.type() in self.WatchedEvents:
if isinstance(self.parent(), QtWidgets.QMenu):
self.parent().setActiveAction(self)
if event.type() in self.FocusEvents:
self.spin.setFocus()
return super().eventFilter(obj, event)
Note: the obj == self.spin condition is required since QWidgetAction already installs an event filter. A more elegant solution is to create a QObject subclass intended for that purpose only, and only overriding eventFilter().
The UI must be in our native language which is Hebrew. Hebrew is a "Right to Left" (RTL) language, so everything must be displayed from right to left.
I figured out that it's actually simple to display Hebrew text in PySide2 since python accepts non English strings, and there is function: app.setLayoutDirection(Qt.RightToLeft) which sets the entire layout to RTL which solves my issue.
However, I faced a weird problem which probably happens because of the rtl ui (I didn't notice the problem in default left to right layout).
I am using a QListWidget to display the students names in a list (in the future, the users will be able to add/remove students. Now the list displays dummy data). All items are editable, so the user can change the student's names.
The weird thing is that when I edit a short name (2 or 3 letters) the input is cut off and only shown again when editing is done. The screenshots should make it more clear:
As you can see, when I double click the name "דן" to change it, the name gets cut off. When I start typing to change the name, the beginning of the new input "אריאל" is still cut off. Only when I press enter to finish editing, The name is displayed correctly again.
I know that this problem happens because of the RTL display because when I comment that out, everything works normally both for Hebrew and English input.
Do you have any idea why this happens and how I can fix it?
Here's the full code:
school_system_manager.py
import sys
from PySide2.QtCore import Qt
from PySide2.QtGui import QKeySequence
from PySide2.QtWidgets import (
QAction,
QApplication,
QDockWidget,
QMainWindow,
QStatusBar,
QWidget,
)
from panels import StudentsPanel
class SchoolSystemManager(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("מנהל מערכת שעות")
# region Actions
exit_action = QAction("&יציאה", self)
exit_action.setShortcut(QKeySequence.Quit)
exit_action.setStatusTip("כיבוי מנהל מערכת השעות")
exit_action.triggered.connect(QApplication.instance().quit)
# endregion
# region Docking
dock = QDockWidget("רשימת התלמידים", self)
student_list = StudentsPanel(dock)
student_list.addItems(("דניאל", "שי", "שירה", "דן", "שרון", "עדן"))
dock.setWidget(student_list)
self.addDockWidget(Qt.RightDockWidgetArea, dock)
# endregion
# region Menus and status bar
menu = self.menuBar()
file_menu = menu.addMenu("&קובץ")
file_menu.addAction(exit_action)
view_menu = menu.addMenu("&תצוגה")
view_menu.addAction(dock.toggleViewAction())
self.setStatusBar(QStatusBar(self))
# endregion
self.setCentralWidget(QWidget())
if __name__ == "__main__":
app = QApplication(sys.argv)
app.setLayoutDirection(Qt.RightToLeft) # Comment out for left to right display
school_system_manager = SchoolSystemManager()
school_system_manager.show()
sys.exit(app.exec_())
panels.py
from PySide2.QtCore import Qt
from PySide2.QtWidgets import QAbstractItemView, QListWidget, QListWidgetItem
class StudentsPanel(QListWidget):
def __init__(self, parent=None, min_width=200):
super().__init__(parent)
self.setMinimumWidth(min_width)
self.setEditTriggers(QAbstractItemView.DoubleClicked)
def addItem(self, item):
item = QListWidgetItem(item, self)
item.setFlags(item.flags() | Qt.ItemIsEditable)
def addItems(self, items):
for item in items:
item = QListWidgetItem(item, self)
item.setFlags(item.flags() | Qt.ItemIsEditable)
Try using setting the role of Qt.TextAlignmentRole to Qt.AlignRight
This may help you solve this
I am trying to handle a drop event on a TreeWidget from itself by overriding the dropEvent method. Ultimately, I need to get a handle to the TreeWidgetItem being dropped. So far, the only useful information I get from the event regarding the dropped item is a QByteArray that seems to contain text from the item being dropped, except that it's poorly formatted with lots of spaces and a bunch of non-printable characters.
Any help would be greatly appreciated.
edit:
Here is the code, as asked, but I'm really not doing anything special, I'm literally just reading the only type of data contained in the mimeData of the drop event. It sounds as though I'm going to have to override the Drag event?? And add some type of identifier to allow me to get a handle back to the original QTreeWidget??
def dropEvent( self, event ):
data = event.mimeData().data( 'application/x-qabstractitemmodeldatalist' )
print data
not quite sure I'm understanding the question correctly, but your mime data comes from the startDrag method, where you've created a QMimeData object, set it's type and supplied data accordingly. In your dropEvent method check the type of the incoming data and process it accordingly or ignore if you don't recognize the type.
Also take a look at the documentation here: Drag and Drop it should give you an idea on how drag and drop works in qt
I also made a small example here, see if it would work for you:
import sys
from PyQt4 import QtGui, QtCore
class TestTreeWidget(QtGui.QTreeWidget):
def __init__(self, parent = None):
super(TestTreeWidget, self).__init__(parent)
self.setDragEnabled(True)
self.setAcceptDrops(True)
def startDrag(self, dropAction):
# create mime data object
mime = QtCore.QMimeData()
mime.setData('application/x-item', '???')
# start drag
drag = QtGui.QDrag(self)
drag.setMimeData(mime)
drag.start(QtCore.Qt.CopyAction | QtCore.Qt.CopyAction)
def dragMoveEvent(self, event):
if event.mimeData().hasFormat("application/x-item"):
event.setDropAction(QtCore.Qt.CopyAction)
event.accept()
else:
event.ignore()
def dragEnterEvent(self, event):
if (event.mimeData().hasFormat('application/x-item')):
event.accept()
else:
event.ignore()
def dropEvent(self, event):
if (event.mimeData().hasFormat('application/x-item')):
event.acceptProposedAction()
data = QtCore.QString(event.mimeData().data("application/x-item"))
item = QtGui.QTreeWidgetItem(self)
item.setText(0, data)
self.addTopLevelItem(item)
else:
event.ignore()
class MainForm(QtGui.QMainWindow):
def __init__(self, parent=None):
super(MainForm, self).__init__(parent)
self.view = TestTreeWidget(self)
self.view.setColumnCount(1)
item0 = QtGui.QTreeWidgetItem(self.view)
item0.setText(0, 'item0')
item1 = QtGui.QTreeWidgetItem(self.view)
item1.setText(0, 'item1')
self.view.addTopLevelItems([item0, item1])
self.setCentralWidget(self.view)
def main():
app = QtGui.QApplication(sys.argv)
form = MainForm()
form.show()
app.exec_()
if __name__ == '__main__':
main()
also you may want to take a look at the similar post here: QTreeView with drag and drop support in PyQt
hope this helps, regards