I am working on a custom drag-drop implementation in a QTableView. When I drag a cell and drop it on another cell I want to manually change some data in the model based on what was dragged and where it was dropped. How can I do this? I've been reading through all Qt documentation but I am utterly lost, and in particular with drag-drop it seems that the C++ to PyQt conversion is a little less intuitive.
Basically what I need is when I drop I want to know what cells were initially dragged, and where they were dropped. Where my confusion lies seems to be with QMimeData. From what I can tell when the drag starts, the drag event receives the right MIME data but I don't know how to get at it in PyQt (been able to do this sort of thing with text and urls in the past but I'm lost when it comes to an item view). I also need to know where I'm dropping to. I guess I could do an "item at cursor pos" but I assume this data already exists in the drop event and I just need to figure out how to query it.
Here's a simple example:
import sys
from PyQt4 import QtGui, QtCore
class TableView(QtGui.QTableView):
def __init__(self, parent=None):
QtGui.QTreeWidget.__init__(self, parent)
self.setDragEnabled(True)
self.setDropIndicatorShow(True)
self.setDragDropMode(QtGui.QAbstractItemView.DragDrop)
self.setDragDropOverwriteMode(False)
self.setSelectionBehavior(QtGui.QAbstractItemView.SelectRows)
def dragEnterEvent(self, event):
event.accept()
def dropEvent(self, event):
# I want to do cool things with the dragged cells, and I need to know where they dropped!
print(event.mimeData().formats()) # this tells me that I shuld get some sort of "qabstractitemmodeldatalist". Sounds promising...
print(event.mimeData().data("application/x-qabstractitemmodeldatalist")) # this gives me an interesting looking QByteArray but I have no idea what to do with it...
event.accept()
class Dialog(QtGui.QDialog):
def __init__(self):
super(Dialog, self).__init__()
model = QtGui.QStandardItemModel(self)
model.insertRow(0, QtGui.QStandardItem("C"))
model.insertRow(0, QtGui.QStandardItem("B"))
model.insertRow(0, QtGui.QStandardItem("A"))
table = TableView(self)
table.setModel(model)
app = QtGui.QApplication(sys.argv)
ex = Dialog()
ex.show()
sys.exit(app.exec_())
You can not know where it was dragged since the mimeData does not have that information but you can get the data dragged, for that we created a temporary model where we will establish the mimeData emulating the same behavior of the drag. To obtain where it was dropped, the position that comes as part of the event must be used together with indexAt(), thus obtaining the QModelIndex:
import sys
from PyQt4 import QtGui, QtCore
class TableView(QtGui.QTableView):
def __init__(self, parent=None):
QtGui.QTreeWidget.__init__(self, parent)
self.setDragEnabled(True)
self.setDropIndicatorShown(True)
self.setDragDropMode(QtGui.QAbstractItemView.DragDrop)
self.setDragDropOverwriteMode(False)
self.setSelectionBehavior(QtGui.QAbstractItemView.SelectRows)
def dragEnterEvent(self, event):
event.accept()
def dropEvent(self, event):
if self.viewport().rect().contains(event.pos()):
fake_model = QtGui.QStandardItemModel()
fake_model.dropMimeData(
event.mimeData(), event.dropAction(), 0, 0, QtCore.QModelIndex()
)
print("from:")
for r in range(fake_model.rowCount()):
for c in range(fake_model.columnCount()):
ix = fake_model.index(r, c)
print(ix.data())
to_index = self.indexAt(event.pos())
if to_index.isValid():
print("to:", to_index.data())
super(TableView, self).dropEvent(event)
class Dialog(QtGui.QDialog):
def __init__(self):
super(Dialog, self).__init__()
model = QtGui.QStandardItemModel(self)
for letter in "ABC":
model.appendRow(QtGui.QStandardItem(letter))
table = TableView()
table.setModel(model)
lay = QtGui.QVBoxLayout(self)
lay.addWidget(table)
if __name__ == "__main__":
app = QtGui.QApplication(sys.argv)
ex = Dialog()
ex.show()
sys.exit(app.exec_())
Related
At the moment I'm writing a calendar program with QT. My main window holds a QCalendarWidget and now I want to listen to double click events of the cells. My problem is that I do not know how I can get a cell (which ia a child of the QCalendarWidget) so I can add an event listener to it. With:
calendarWidget.findChildren(QtCore.QObject)
I can get all children of the Widget but I do not know how to identify a cell. Do you have any ideas how I can do this?
The calendar widget contains a QTableView, so you can get a reference to that and query its contents.
The demo below installs an event-filter on the table to get double-clicks, because the table's doubleClicked signal is disabled by the calendar (presumably to prevent editing of the cells).
from PyQt4 import QtCore, QtGui
class Window(QtGui.QWidget):
def __init__(self):
super(Window, self).__init__()
self.calendar = QtGui.QCalendarWidget(self)
self.table = self.calendar.findChild(QtGui.QTableView)
self.table.viewport().installEventFilter(self)
layout = QtGui.QVBoxLayout(self)
layout.addWidget(self.calendar)
def eventFilter(self, source, event):
if (event.type() == QtCore.QEvent.MouseButtonDblClick and
source is self.table.viewport()):
index = self.table.indexAt(event.pos())
print('row: %s, column: %s, text: %s' % (
index.row(), index.column(), index.data()))
return super(Window, self).eventFilter(source, event)
if __name__ == '__main__':
import sys
app = QtGui.QApplication(sys.argv)
window = Window()
window.setGeometry(750, 250, 300, 300)
window.show()
sys.exit(app.exec_())
I am trying to build a hover Dialog but i am stuck at the interaction between QComboBox selection and QDialog's leaveEvent. it looks like when i try to click on combobox to select something, it triggers a leaveEvent which then hides my QDialog. Why is this happening? What can i try to ensure that the Dialog is only hidden when I move my mouse outside of the Dialog?
from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *
import sys
class hoverDialog(QDialog):
def __init__(self, parent=None):
super().__init__()
self.setAttribute(Qt.WA_DeleteOnClose)
self.setWindowFlags(Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint)
self.v = QVBoxLayout()
self.combobox = QComboBox()
self.combobox.addItems(['Work-around','Permanent'])
self.textedit = QPlainTextEdit()
self.v.addWidget(self.combobox)
self.v.addWidget(self.textedit)
self.setLayout(self.v)
#self.setMouseTracking(True)
def leaveEvent(self, event):
self.hide()
return super().leaveEvent(event)
class Table(QWidget):
def __init__(self, parent=None):
super().__init__()
self.label4 = QLabel()
self.label4.setText("hover popup")
self.label4.installEventFilter(self)
self.checkbox1 = QCheckBox()
self.pushButton3 = QPushButton()
self.pushButton3.setText('Generate')
self.pushButton3.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Preferred)
self.pushButton3.clicked.connect(self.buttonPressed)
self.hbox5 = QHBoxLayout()
self.hbox5.addWidget(self.checkbox1)
self.hbox5.addWidget(self.label4)
self.hbox5.addWidget(self.pushButton3)
self.vbox1 = QVBoxLayout()
self.vbox1.addLayout(self.hbox5)
self.setLayout(self.vbox1)
self.autoResolve = hoverDialog(self)
def eventFilter(self, obj, event):
if obj == self.label4 and event.type() == QtCore.QEvent.Enter and self.autoResolve.isHidden():
self.onHovered()
return super().eventFilter(obj, event)
def onHovered(self):
pos = QtGui.QCursor.pos()
self.autoResolve.move(pos)
self.autoResolve.show()
def buttonPressed(self):
if self.checkbox.isChecked():
print('do something..')
if __name__ == '__main__':
app = QApplication(sys.argv)
form = Table()
form.show()
app.exec_()
Looking at the sources, I believe that the origin of the problem is in Qt's behavior whenever a new popup widget is shown.
I cannot guarantee this at 100%, but in any case the simplest solution is to hide the widget only if the combo popup is not shown:
def leaveEvent(self, event):
if not self.combobox.view().isVisible():
self.hide()
Note that your approach is not really perfect: since the dialog could be shown with a geometry that is outside the current mouse position, it will not be able to hide itself until the mouse actually enters it. You should probably filter FocusOut and WindowDeactivate events also.
I'm trying to detect mouse clicks for anywhere inside an area with several widgets. For this I'm using the following code:
custom_widget = CustomWidget()
custom_widget.mouse_pressed_signal.connect(self.on_custom_label_mouse_pressed)
main_layout_vbox.addWidget(custom_widget)
hbox = QtWidgets.QHBoxLayout()
custom_widget.setLayout(hbox)
# Adding several widgets to hbox_l6
class CustomWidget(QtWidgets.QWidget):
mouse_pressed_signal = QtCore.pyqtSignal(QtGui.QMouseEvent)
def __init__(self):
super().__init__()
def mousePressEvent(self, i_qmouseevent):
super(CustomWidget, self).mousePressEvent(i_qmouseevent)
logging.debug("======== CustomWidget - mousePressEvent ========")
self.mouse_pressed_signal.emit(i_qmouseevent)
Problem
This works when clicking in any of the child widgets, but there's a problem: If I click between widgets (so in the area of the hbox layout that is not covered by a widget) the mousePressEvent is not captured
Question
How can I solve this problem? (Or is there another approach that you can recommend?) The important thing is that I am able to capture mouse clicks anywhere inside of custom_widget / hbox (see code above)
If you want to listen to other widget's mousePressEvent you can use an eventFilter as I show below:
from PyQt5 import QtCore, QtGui, QtWidgets
import random
class Widget(QtWidgets.QWidget):
mouse_clicked_signal = QtCore.pyqtSignal(QtGui.QMouseEvent, QtWidgets.QWidget)
def __init__(self, parent=None):
super(Widget, self).__init__(parent)
hlay = QtWidgets.QHBoxLayout(self)
for cls in (QtWidgets.QLabel, QtWidgets.QPushButton, QtWidgets.QFrame, QtWidgets.QWidget):
widget = cls()
color = QtGui.QColor(*random.sample(range(255), 3))
widget.setStyleSheet("background-color: {}".format(color.name()))
hlay.addWidget(widget)
for w in self.findChildren(QtWidgets.QWidget) + [self]:
w.installEventFilter(self)
self.resize(640, 480)
def eventFilter(self, watched, event):
if event.type() == QtCore.QEvent.MouseButtonPress:
self.mouse_clicked_signal.emit(event, watched)
return super(Widget, self).eventFilter(watched, event)
if __name__ == '__main__':
import sys
app = QtWidgets.QApplication(sys.argv)
w = Widget()
w.mouse_clicked_signal.connect(print)
w.show()
sys.exit(app.exec_())
I have the code below for a interactive label in PyQt4 that can be clicked, right clicked and scrolled. I am converting the code for PyQt5 and lots of things in my code are right now based on this element.
class ExtendedQLabel(QLabel):
def __init(self, parent):
super().__init__(parent)
def mousePressEvent(self, ev):
if ev.button() == QtCore.Qt.RightButton:
self.emit(SIGNAL('rightClicked()'))
else:
self.emit(SIGNAL('clicked()'))
def wheelEvent(self, ev):
self.emit(SIGNAL('scroll(int)'), ev.delta())
How do I make this PyQt5 compatible?
Ok, after a lot of things I understood what I was doing wrong:
from PyQt5 import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
class ExtendedQLabel(QLabel):
def __init(self, parent):
super().__init__(parent)
clicked = pyqtSignal()
rightClicked = pyqtSignal()
def mousePressEvent(self, ev):
if ev.button() == Qt.RightButton:
self.rightClicked.emit()
else:
self.clicked.emit()
if __name__ == '__main__':
app = QApplication([])
eql = ExtendedQLabel()
eql.clicked.connect(lambda: print('clicked'))
eql.rightClicked.connect(lambda: print('rightClicked'))
eql.show()
app.exec_()
In the line with the text clicked = pyqtSignal() and rightClicked = pyqtSignal()what ties those signals to that class that makes the code above work? Well the answer is the correct indentation, correct indentation will nest the variable to the class instead of randomly creating a variable that has no use. It took me a lot of time to perceive this, so thought lefting this here could be useful if someone google this.
from PyQt5 import QtGui,QtCore,QtWidgets
class Clickable_Label(QtWidgets.QLabel):
def __init__(self):
super().__init__()
clicked = QtCore.pyqtSignal() # signal when the text entry is left clicked
def mousePressEvent(self, event):
self.clicked.emit()
QtWidgets.QLabel.mousePressEvent(self, event)
myLabel=Clickable_Label()
myLabel.setText("Clickable_Label")
myLabel.board_rule_download.clicked.connect(lambda: print('clicked'))`enter code here`
I think the title is fairly self explanatory. I'm working to create a small standalone app that requires the user to drag and drop audio files onto buttons to in turn associate the file with a corresponding button on a piece of hardware by using the filepath, etc...
I've followed a ton of drag and drop tutorials for widgets, and my friend has for lists, however I'm beginning to believe that it can't be done for a button? I'm aware that you can drag and drop text onto a button. I am not fully up to speed with Qt yet so there may just be a glaring error that I'm missing.
Here is the code, many thanks!
import sys
from PyQt4 import QtGui, QtCore
class Button(QtGui.QPushButton):
def __init__(self, parent):
super(Button, self).__init__(parent)
self.setAcceptDrops(True)
self.setDragDropMode(QAbstractItemView.InternalMove)
def dragEnterEvent(self, event):
if event.mimeData().hasUrls():
event.acceptProposedAction()
else:
super(Button, self).dragEnterEvent(event)
def dragMoveEvent(self, event):
super(Button, self).dragMoveEvent(event)
def dropEvent(self, event):
if event.mimeData().hasUrls():
for url in event.mimeData().urls():
path = self.addItem(url.path())
print path
event.acceptProposedAction()
else:
super(Button,self).dropEvent(event)
class MyWindow(QtGui.QWidget):
def __init__(self):
super(MyWindow,self).__init__()
self.setGeometry(100,100,300,400)
self.setWindowTitle("Filenames")
self.btn = QtGui.QPushButton()
self.btn.setGeometry(QtCore.QRect(90, 90, 61, 51))
self.btn.setText("Change Me!")
layout = QtGui.QVBoxLayout(self)
layout.addWidget(self.btn)
self.setLayout(layout)
if __name__ == '__main__':
app = QtGui.QApplication(sys.argv)
window = MyWindow()
window.show()
sys.exit(app.exec_())
There are three problems with your posted code, the main being that you aren't even using the custom Button class that you made. You are adding just a regular button to your window with:
self.btn = QtGui.QPushButton()
instead of:
self.btn = Button(self)
Also, QPushButtons don't have a setDragDropMode() method, so you'll need to comment that line out. I'm not sure what it does anyway.
Also, QPushButton doesn't have an addItem() method so I'm not sure what that's about unless you were planning on implementing it. I replaced it below with just printing the file path.
Here is a working version of your code, that just prints the file path of any file dragged into the button:
import sys
from PyQt4 import QtGui, QtCore
class Button(QtGui.QPushButton):
def __init__(self, parent):
super(Button, self).__init__(parent)
self.setAcceptDrops(True)
#self.setDragDropMode(QAbstractItemView.InternalMove)
def dragEnterEvent(self, event):
if event.mimeData().hasUrls():
event.acceptProposedAction()
else:
super(Button, self).dragEnterEvent(event)
def dragMoveEvent(self, event):
super(Button, self).dragMoveEvent(event)
def dropEvent(self, event):
if event.mimeData().hasUrls():
for url in event.mimeData().urls():
print str(url.toLocalFile())
event.acceptProposedAction()
else:
super(Button,self).dropEvent(event)
class MyWindow(QtGui.QWidget):
def __init__(self):
super(MyWindow,self).__init__()
self.setGeometry(100,100,300,400)
self.setWindowTitle("Filenames")
self.btn = Button(self)
self.btn.setGeometry(QtCore.QRect(90, 90, 61, 51))
self.btn.setText("Change Me!")
layout = QtGui.QVBoxLayout(self)
layout.addWidget(self.btn)
self.setLayout(layout)
if __name__ == '__main__':
app = QtGui.QApplication(sys.argv)
window = MyWindow()
window.show()
sys.exit(app.exec_())