I revised the whole question because the behavior I want is hard to implement and actually use.
I'm trying to imitate the behavior in the File Explorer where when I press Shift while dragging, the file will be moved instead of copied.
This is the behavior I'm trying to imitate:
The behavior: is I'm using my LeftClick for selecting, and dragging.
About The behavior itself:
I overridden the mousePressEvent and mouseMoveEvent to start the drag. When the drag is created, it uses QTimer to detect if I pressed the Control and Shift modifier. Once a modifier is detected it sets the default drop action using setDefaultDropAction. (I think I should use setDropAction but It's only available in the dragMoveEvent and I'm doing it inside the QDrag Class)
The Issues:
Part of the behavior is working now but there is still some issues.
Even I press Shift, the DropIndicator is not changing from + to ->
Related to the issue above, The dropAction is only copyAction instead of moveAction even I'm pressing the Shift key.
My Question: What causes these issues? My gut tells me that I should've used setDropAction instead of setDefaultDropAction but again it's only available in the dragMoveEvent
My Testing Code:
import sys
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *
class ModifiedQDrag(QDrag):
def __init__(self, source):
super().__init__(source)
self.timer = QTimer(self)
self.timer.timeout.connect(self.process_event)
self.timer.setInterval(100)
self.timer.start()
def process_event(self):
if qApp.keyboardModifiers() & Qt.ControlModifier:
self.source().setDefaultDropAction(Qt.CopyAction)
elif qApp.keyboardModifiers() & Qt.ShiftModifier:
print("shift pressed")
self.source().setDefaultDropAction(Qt.MoveAction)
class Tree(QTreeView):
def __init__(self):
super().__init__()
self.setDragDropMode(QAbstractItemView.DragDrop)
self.setDropIndicatorShown(True)
self.viewport().setAcceptDrops(True)
self.setSelectionMode(QAbstractItemView.ExtendedSelection)
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
# -- mouse dragging -- #
def mousePressEvent(self, event):
if event.button() == Qt.RightButton:
self.dragStartPosition = event.pos()
return super().mousePressEvent(event)
def mouseMoveEvent(self, event):
if event.buttons() != Qt.RightButton:
return
if ((event.pos() - self.dragStartPosition).manhattanLength() < QApplication.startDragDistance()):
return
drag = ModifiedQDrag(self)
mimeData = QMimeData()
mimeData = self.model().mimeData([self.indexAt(event.pos())])
drag.setMimeData(mimeData)
dragAction = drag.exec(Qt.MoveAction | Qt.CopyAction, Qt.CopyAction)
return super().mouseMoveEvent(event)
def dragMoveEvent(self, event):
m = event.mimeData()
if m.hasUrls():
event.accept()
return
event.ignore()
def dropEvent(self, event):
print("[drop event] - dropped")
class FileSystemView(QWidget):
def __init__(self):
super().__init__()
# -- left side -- #
left_side_dir = r"<Dir>"
self.model = QFileSystemModel()
self.model.setRootPath(left_side_dir)
self.tree = Tree()
self.tree.setModel(self.model)
self.tree.setRootIndex(self.model.index(left_side_dir))
# -- right side -- #
right_side_dir = r"<Dir>"
self.model2 = QFileSystemModel()
self.model2.setRootPath(right_side_dir)
self.tree2 = Tree()
self.tree2.setModel(self.model2)
self.tree2.setRootIndex(self.model2.index(right_side_dir))
# -- layout -- #
self.tree_layout = QHBoxLayout()
self.tree_layout.addWidget(self.tree)
self.tree_layout.addWidget(self.tree2)
self.setLayout(self.tree_layout)
app = QApplication(sys.argv)
demo = FileSystemView()
demo.show()
sys.exit(app.exec_())
Qt can only react to mouse movements in order to trigger changes in the drop action: as the name suggests, dragMoveEvent() can only be called by a mouse move.
Considering that, a possible solution is to manually force the mouse movement whenever the keyboard modifiers change. In this way you don't even need to create a QDrag subclass and you can keep the default behavior.
Be aware that to properly get modifiers, you should not use keyboardModifiers(), but queryKeyboardModifiers(), as the first is only reliable when keyboard events are directly handled and might not be updated with the actual current state of the keyboard.
class Tree(QTreeView):
# ...
def checkDrag(self):
modifiers = qApp.queryKeyboardModifiers()
if self.modifiers != modifiers:
self.modifiers = modifiers
pos = QCursor.pos()
# slightly move the mouse to trigger dragMoveEvent
QCursor.setPos(pos + QPoint(1, 1))
# restore the previous position
QCursor.setPos(pos)
def mouseMoveEvent(self, event):
if event.buttons() != Qt.RightButton:
return
if ((event.pos() - self.dragStartPosition).manhattanLength() < QApplication.startDragDistance()):
return
self.modifiers = qApp.queryKeyboardModifiers()
# a local timer, it will be deleted when the function returns
dragTimer = QTimer(interval=100, timeout=self.checkDrag)
dragTimer.start()
self.startDrag(Qt.MoveAction|Qt.CopyAction)
def dragMoveEvent(self, event):
if not event.mimeData().hasUrls():
event.ignore()
return
if qApp.queryKeyboardModifiers() & Qt.ShiftModifier:
event.setDropAction(Qt.MoveAction)
else:
event.setDropAction(Qt.CopyAction)
event.accept()
Related
I have a custom QWidget that I have embedded into a QTableWidget.
When I toggle the QCheckBoxes and modify the text in the QLineEdit widgets, the program is not able to distinguish the widgets in rows 2 and 1 from the widgets in row 0. How can I change the program so that it prints the correct row and column of the QLineEdit widget that is being edited or the Checkbox that is being toggled?
Figure 1 shows a screenshot of the program with the output after selecting the third checkbox many times in Visual Studio Code. The output is expected to read “2 0” repeatedly but instead it reads “0 0”.
Figure 2 Similarly, when I modify the text in the QLineEdit in cell 2,0 from “My Custom Text” to “Text” the program prints “Handle Cell Edited 0,0”, although it is expected to print “Handle Cell Edited 2,0 Cell 2,0 was changed to Text”.
Code:
# Much of this code is copy pasted form user: three_pineapples post on stackoverflow:
# https://stackoverflow.com/a/26311179/18914416
import sys
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QWidget, QHBoxLayout, QTableWidget, \
QApplication, QTableWidgetItem, QLineEdit, QCheckBox
from PyQt5 import QtGui
class SimpleTable(QTableWidget):
def __init__(self,window):
# Call the parent constructor
QTableWidget.__init__(self)
self.window = window
class myWidget(QWidget):
#This code is adapted paritally form a post by user sebastian at:
#https://stackoverflow.com/a/29764770/18914416
def __init__(self,parent=None):
super(myWidget,self).__init__()
self.Layout1 = QHBoxLayout()
self.item = QLineEdit("My custom text")
#https://stackabuse.com/working-with-pythons-pyqt-framework/
self.Checkbox = QCheckBox()
self.Checkbox.setCheckState(Qt.CheckState.Unchecked)
self.Layout1.addWidget(self.Checkbox)
self.Layout1.addWidget(self.item)
#https://stackoverflow.com/questions/29764395/adding-multiple-widgets-to-qtablewidget-cell-in-pyqt
self.item.home(True)
#https://www.qtcentre.org/threads/58387-Left-text-alignment-for-long-text-on-QLineEdit
self.setLayout(self.Layout1)
class Window(QWidget):
def __init__(self):
super(Window, self).__init__()
layout = QHBoxLayout()
self.setLayout(layout)
self.table_widget = SimpleTable(window=self)
layout.addWidget(self.table_widget)
self.table_widget.setColumnCount(3)
self.table_widget.setHorizontalHeaderLabels(['Colour', 'Model'])
items = [('Red', 'Toyota'), ('Blue', 'RV'), ('Green', 'Beetle')]
for i in range(len(items)):
c = QTableWidgetItem(items[i][0])
m = QTableWidgetItem(items[i][1])
self.table_widget.insertRow(self.table_widget.rowCount())
self.table_widget.setItem(i, 1, c)
self.table_widget.setItem(i, 2, m)
myWidget1 = myWidget()
myWidget1.Checkbox.stateChanged.connect(self.handleButtonClicked)
myWidget1.item.editingFinished.connect(self.handle_cell_edited)
self.table_widget.setCellWidget(i,0,myWidget1)
myWidget1.Layout1.setContentsMargins(50*i+10,0,0,0)
self.show()
self.table_widget.itemChanged.connect(self.handle_cell_edited)
def handleButtonClicked(self):
#Adapted from a post by user: Andy at:
# https://stackoverflow.com/a/24149478/18914416
button = QApplication.focusWidget()
# or button = self.sender()
index = self.table_widget.indexAt(button.pos())
if index.isValid():
print(index.row(), index.column())
# I added this fuction:
def handle_cell_edited(self):
if QApplication.focusWidget() != None:
index = self.table_widget.indexAt(QApplication.focusWidget().pos())
x,y = index.column(),index.row()
if index.isValid():
print("Handle Cell Edited",index.row(), index.column())
if self.table_widget.item(y,x)!= None:
print(f"Cell {x},{y} was changed to {self.table_widget.item(y,x).text()}.")
def main():
app = QApplication(sys.argv)
window = Window()
sys.exit(app.exec_())
main()
What I've Tried So Far:
I learned that QT has two types of widgets that can be embedded in a table; a QTableWigetItem which can be inserted into a table using setItem()(3) and Qwidgets, which can be placed into a table using setCellWidget().(4) Generally, I know that using a QTableWigetItem one can set the item.setFlags(Qt.ItemFlag.ItemIsUserCheckable)
flag to create a checkbox in the cell. (3) However, when using the QTableWigetItem, I wasn’t able to find a way to indent the checkboxes. Because giving each checkbox its own indentation level is important in the context of my program, I’ve decided to use Qwidgets instead of QTableWigetItems in the few select cells where indenting is important.
I’ve read that by creating a QItemDelegate(5)(6), you can do a lot more with setting QWidgets in boxes. However, creating a delegate seems complicated, so I’d prefer to avoid this if possible. If there is no other way to make the program register the correct cell number of the cell being edited, creating a delegate will be the next thing I look into.
For anyone who might want to experiment with QTableWigetItems in this application, here is an equivalent program that uses QTableWigetItems instead of QWidgets but doesn't permit separate indentation or editing of the text field in column 0. For either and both of these two reasons, a QTableWigetItem seems not to be usable for the checkboxes in column 0.
Less Successful Attempt using QTableWidgetItem:
#Much of this code is copy pasted form user: three_pineapples post on stackoverflow:
# https://stackoverflow.com/a/26311179/18914416
import sys
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QWidget, QHBoxLayout, QTableWidget, \
QApplication, QTableWidgetItem, QLineEdit, QCheckBox
from PyQt5 import QtGui
class SimpleTable(QTableWidget):
def __init__(self,window):
QTableWidget.__init__(self)
self.window = window
class Window(QWidget):
def __init__(self):
super(Window, self).__init__()
layout = QHBoxLayout()
self.setLayout(layout)
self.table_widget = SimpleTable(window=self)
layout.addWidget(self.table_widget)
self.table_widget.setColumnCount(3)
self.table_widget.setHorizontalHeaderLabels(['Colour', 'Model'])
items = [('Red', 'Toyota'), ('Blue', 'RV'), ('Green', 'Beetle')]
for i in range(len(items)):
c = QTableWidgetItem(items[i][0])
m = QTableWidgetItem(items[i][1])
self.table_widget.insertRow(self.table_widget.rowCount())
self.table_widget.setItem(i, 1, c)
self.table_widget.setItem(i, 2, m)
item = QTableWidgetItem("My Custom Text")
item.setFlags(Qt.ItemFlag.ItemIsUserCheckable| Qt.ItemFlag.ItemIsEnabled)
item.setCheckState(Qt.CheckState.Unchecked)
self.table_widget.setItem(i,0,item)
#https://youtu.be/DM8Ryoot7MI?t=251
self.show()
#I added this line:
self.table_widget.itemChanged.connect(self.handle_cell_edited)
def handleButtonClicked(self):
#Adapted from a post by user: Andy at:
# https://stackoverflow.com/a/24149478/18914416
button = QApplication.focusWidget()
# or button = self.sender()
index = self.table_widget.indexAt(button.pos())
if index.isValid():
print(index.row(), index.column())
# I added this fuction:
def handle_cell_edited(self):
if QApplication.focusWidget() != None:
index = self.table_widget.indexAt(QApplication.focusWidget().pos())
x,y = index.column(),index.row()
if index.isValid():
print("Handle Cell Edited",index.row(), index.column())
if self.table_widget.item(y,x)!= None:
print(f"Cell {x},{y} was changed to {self.table_widget.item(y,x).text()}.")
def main():
app = QApplication(sys.argv)
window = Window()
sys.exit(app.exec_())
main()
Bibliography:
1.https://i.stack.imgur.com/FudE3.png
2.https://i.stack.imgur.com/C2ypp.png
3.https://youtu.be/DM8Ryoot7MI?t=251
4.https://stackoverflow.com/questions/24148968/how-to-add-multiple-qpushbuttons-to-a-qtableview/24149478#24149478
5.Creating a QItemDelegate for QWidgets, https://stackoverflow.com/a/35418141/18914416
6.Need to create a QItemDelegate to add a stylesheet to QTableWidgetItems: https://forum.qt.io/topic/13124/solved-qtablewidgetitem-set-stylesheet
The geometry of a widget is always relative to its parent.
In your first example, the problem is that the pos() returned for the widget is relative to the myWidget container, and since the vertical position is always a few pixels below the top of the parent (the layout margin), you always get the same value.
The second example has another conceptual problem: the checkbox of a checkable item is not an actual widget, so the widget you get is the table itself.
def handle_cell_edited(self):
# this will print True
print(isinstance(QApplication.focusWidget(), QTableWidget))
As explained above, the geometry is always relative to the parent, so you will actually get the position of the table relative to the window.
The solution to the first case is quite simple, as soon as you understand the relativity of coordinate systems. Note that you shall not rely on the focusWidget() (the widget might not accept focus), but actually get the sender(), which is the object that emitted the signal:
def handleButtonClicked(self):
sender = self.sender()
if not self.table_widget.isAncestorOf(sender):
return
# the widget coordinates must *always* be mapped to the viewport
# of the table, as the headers add margins
pos = sender.mapTo(self.table_widget.viewport(), QPoint())
index = self.table_widget.indexAt(pos)
if index.isValid():
print(index.row(), index.column())
In reality, this might not be that necessary, as an item delegate will suffice if the indentation is the only requirement: the solution is to properly set the option.rect() within initStyleOption() and use a custom role for the indentation:
IndentRole = Qt.UserRole + 1
class IndentDelegate(QStyledItemDelegate):
def initStyleOption(self, opt, index):
super().initStyleOption(opt, index)
indent = index.data(IndentRole)
if indent is not None:
left = min(opt.rect.right(),
opt.rect.x() + indent)
opt.rect.setLeft(left)
class SimpleTable(QTableWidget):
def __init__(self,window):
QTableWidget.__init__(self)
self.window = window
self.setItemDelegateForColumn(0, IndentDelegate(self))
class Window(QWidget):
def __init__(self):
# ...
for i in range(len(items)):
# ...
item.setData(IndentRole, 20 * i)
I want to display a button in each cell of a QTableWidget's column. Each button, when clicked, must remove its corresponding row in the table.
To do so, I created a RemoveRowDelegate class with the button as editor and used the QAbstractItemView::openPersistentEditor method in a CustomTable class to display the button permanently.
class RemoveRowDelegate(QStyledItemDelegate):
def __init__(self, parent, cross_icon_path):
super().__init__(parent)
self.cross_icon_path = cross_icon_path
self.table = None
def createEditor(self, parent, option, index):
editor = QToolButton(parent)
editor.setStyleSheet("background-color: rgba(255, 255, 255, 0);") # Delete borders but maintain the click animation (as opposed to "border: none;")
pixmap = QPixmap(self.cross_icon_path)
button_icon = QIcon(pixmap)
editor.setIcon(button_icon)
editor.clicked.connect(self.remove_row)
return editor
# Delete the corresponding row
def remove_row(self):
sending_button = self.sender()
for i in range(self.table.rowCount()):
if self.table.cellWidget(i, 0) == sending_button:
self.table.removeRow(i)
break
class CustomTable(QTableWidget):
def __init__(self, parent=None, df=None):
super().__init__(parent)
self.columns = []
self.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeToContents)
self.verticalHeader().setSectionResizeMode(QHeaderView.ResizeToContents)
if df is not None:
self.fill(df)
# Build the table from a pandas df
def fill(self, df):
self.columns = [''] + list(df.columns)
nb_rows, _ = df.shape
nb_columns = len(self.columns)
self.setRowCount(nb_rows)
self.setColumnCount(nb_columns)
self.setHorizontalHeaderLabels(self.columns)
for i in range(nb_rows):
self.openPersistentEditor(self.model().index(i, 0))
for j in range(1, nb_columns):
item = df.iloc[i, j-1]
table_item = QTableWidgetItem(item)
self.setItem(i, j, table_item)
def add_row(self):
nb_rows = self.rowCount()
self.insertRow(nb_rows)
self.openPersistentEditor(self.model().index(nb_rows, 0))
def setItemDelegateForColumn(self, column_index, delegate):
super().setItemDelegateForColumn(column_index, delegate)
delegate.table = self
I set the delegate for the first column of the table and build the latter from a pandas dataframe:
self.table = CustomTable() # Here, self is my user interface
remove_row_delegate = RemoveRowDelegate(self, self.cross_icon_path)
self.table.setItemDelegateForColumn(0, remove_row_delegate)
self.table.fill(df)
For now, this solution does the job but I think of several other possibilities:
Using the QTableWidget::setCellWidget method
Overriding the paint method and catching the left click event
But:
I believe the first alternative is not very clean as I must create the buttons in a for loop and each time a row is added (but after all, I also call openPersistentEditor the same way here).
I am wondering if the second alternative is worth the effort. And if it does, how to do it?
Also:
I believe my remove_row method can be optimized as I iterate over all rows (that is one of the reasons why I thought about the second alternative). Would you have a better suggestion ?
I had to override the setItemDelegateForColumn method so that I can access the table from the RemoveRowDelegate class. Can it be avoided ?
Any other remark that you think might be of interest would be greatly appreciated!
As suggested by #ekhumoro, I finally used a context menu:
class CustomTable(QTableWidget):
def __init__(self, parent=None, df=None, add_icon_path=None, remove_icon_path=None):
super().__init__(parent)
self.add_icon_path = add_icon_path
self.remove_icon_path = remove_icon_path
# Activation of customContextMenuRequested signal and connecting it to a method that displays a context menu
self.setContextMenuPolicy(Qt.CustomContextMenu)
self.customContextMenuRequested.connect(lambda pos: self.show_context_menu(pos))
def show_context_menu(self, pos):
idx = self.indexAt(pos)
if idx.isValid():
row_idx = idx.row()
# Creating context menu and personalized actions
context_menu = QMenu(parent=self)
if self.add_icon_path:
pixmap = QPixmap(self.add_icon_path)
add_icon = QIcon(pixmap)
add_row_action = QAction('Insert a line', icon=add_icon)
else:
add_row_action = QAction('Insert a line')
add_row_action.triggered.connect(lambda: self.insertRow(row_idx))
if self.remove_icon_path:
pixmap = QPixmap(self.remove_icon_path)
remove_icon = QIcon(pixmap)
remove_row_action = QAction('Delete the line', icon=remove_icon)
else:
remove_row_action = QAction('Delete the line')
remove_row_action.triggered.connect(lambda: self.removeRow(row_idx))
context_menu.addAction(add_row_action)
context_menu.addAction(remove_row_action)
# Displaying context menu
context_menu.exec_(self.mapToGlobal(pos))
Moreover, note that using QTableWidget::removeRow method is more optimized than my previous method. One just need to get the row index properly from the click position thanks to QTableWidget::indexAt method.
I'm facing an issue with QGraphicsItem and QGraphicsItemGroup. I can add to group and move the group around but when i try to remove the item from group it get's deleted. I can't find the object in the scene at all.
Same issue if i use a standard class (QGraphicsRectItem)
What is the correct way to remove an item from group while keeping it on the scene.
Code is below:
from PyQt5.QtCore import Qt,QRect
from PyQt5.QtWidgets import QGraphicsView, QGraphicsScene, QGraphicsItem,QApplication, QGraphicsItemGroup, QGraphicsEllipseItem,QPushButton , QVBoxLayout
from PyQt5 import QtCore, QtGui, QtWidgets
class Rectangle(QtWidgets.QGraphicsItem):
def __init__(self,rect):
super(Rectangle, self).__init__(parent=None)
self.rect = QtCore.QRectF(rect[0], rect[1], rect[2], rect[3])
self.setFlag(QtWidgets.QGraphicsItem.ItemIsMovable)
self.setFlag(QtWidgets.QGraphicsItem.ItemIsSelectable)
self.setFlag(QtWidgets.QGraphicsItem.ItemSendsGeometryChanges)
def boundingRect(self):
return self.rect
def paint(self, painter, option, widget=None):
painter.save()
painter.setRenderHints(
QtGui.QPainter.Antialiasing
| QtGui.QPainter.TextAntialiasing
| QtGui.QPainter.SmoothPixmapTransform
| QtGui.QPainter.HighQualityAntialiasing,
True,
)
painter.drawEllipse(self.rect)
painter.restore()
class Group(QGraphicsItemGroup):
def __init__(self):
super(Group, self).__init__()
self.setFlag(QGraphicsItemGroup.ItemIsMovable)
def boundingRect(self):
#if self.childItems():
# return self.childrenBoundingRect()
return QtCore.QRectF(200,200,20,20)
def paint(self,
painter: QtGui.QPainter,
option: QtWidgets.QStyleOptionGraphicsItem,
widget: QtWidgets.QWidget = None):
painter.setPen(QtGui.QPen(QtCore.Qt.NoPen))
painter.setBrush(QtGui.QBrush(QColor(0, 0, 255, 127)))
painter.drawRect(self.boundingRect())
class MyView(QGraphicsView):
def __init__(self):
super().__init__()
self.scene = QGraphicsScene()
self.setScene(self.scene)
self.setWindowTitle('Test')
self.setSceneRect(0, 0, 450, 450)
#add items
nodeItem = Rectangle([20,200,20,20])
self.scene.addItem(nodeItem)
nodeItem2 = Rectangle([40,300,20,20])
self.scene.addItem(nodeItem2)
#rect_item = QtWidgets.QGraphicsRectItem(QtCore.QRectF(0, 0, 100, 100))
#rect_item.setFlag(QtWidgets.QGraphicsItem.ItemIsMovable, True)
#self.scene.addItem(rect_item)
#add buttons
self.b1 = QPushButton("ungroup")
self.b1.clicked.connect(self.ungroup)
self.b1.setGeometry(QRect(10,10,100,100))
self.b2 = QPushButton("group")
self.b2.clicked.connect(self.cgroup)
self.b2.setGeometry(QRect(150,10,100,100))
self.b3 = QPushButton("print obj")
self.b3.clicked.connect(self.printobj)
self.b3.setGeometry(QRect(250,10,100,100))
self.scene.addWidget(self.b1)
self.scene.addWidget(self.b2)
self.scene.addWidget(self.b3)
def printobj(self):
for item in self.scene.items():
print(item)
def ungroup(self):
for item in self.group.childItems():
self.group.removeFromGroup(item)
print(f'item {item} removed from group {self.group}')
self.scene.destroyItemGroup(self.group)
#self.scene.update()
def cgroup(self):
self.group = Group()
self.scene.addItem(self.group)
for n in self.scene.items():
if isinstance(n,Rectangle):
print('n')
n.setParentItem(self.group)
#self.scene.update()
if __name__ == '__main__':
app = QApplication([])
f = MyView()
f.show()
sys.exit(app.exec_())
The problem is that cycling through the items creates a python wrapper for them, and being it a local reference, it gets automatically garbage collected when the function returns.
The python wrapper tries to check the ownership of the graphics item, but removing an item from the group doesn't automatically retransfer the ownership to the scene as it would happen by doing addItem(). The result is that the local reference to the items is removed, the garbage collection calls the destructors for the items, and they get deleted from the scene.
Keeping a persistent reference to the removed items could solve the problem:
def ungroup(self):
self.items = []
for item in self.group.childItems():
self.group.removeFromGroup(item)
self.items.append(item)
But consider that if the list is deleted or cleared, those items will be deleted as well, unless they are reparented.
Alternatively, you can explicitly remove the items from the scene and add them again (you should not add an item to the same scene):
def ungroup(self):
for item in self.group.childItems():
self.group.removeFromGroup(item)
self.scene.removeItem(item)
self.scene.addItem(item)
But, since you're going to destroy the group anyway, the solution is to only remove (and properly delete) the other items, and destroy the group, so that the remaining item will be transferred back to the scene:
def ungroup(self):
for item in self.group.childItems():
if not isinstance(item, Rectangle):
self.group.removeFromGroup(item)
del item
self.scene.destroyItemGroup(self.group)
Notes: 1. if you want to draw an ellipse, just use QGraphicsEllipseItem, so you don't need to override paint; 2. the ItemIsSelectable flag can cause recursive movements when the item is part of a group;
Using the code from Here and There, I made a GUI presenting my project on a smaller scale.
I have a qTableView,containing a large array of rows, and on each rows I have a delete and an edit button. On click, it should either edit or delete the current row. When using only the first source, it works exactly as intended, but as soon as I handle the click outside of the buttons class, it stops working.
Everytime I try to edit or delete, the button that either self.sender() or QtWidgets.qApp.focusWidget() sees as the sender has the coordinates [0,0], even if it's absolutely not it's coordinates.
I have searched on various websites and can't find this precise question.
What am I doing wrong, and what could I do to solve this problem?
My code :
from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5.QtWidgets import QFileDialog, QMessageBox
class EditButtonsWidget(QtWidgets.QWidget):
# Credit to : https://stackoverflow.com/a/29764914/13812144
def __init__(self, parent=None):
super(EditButtonsWidget,self).__init__(parent)
# add your buttons
layout = QtWidgets.QHBoxLayout()
# adjust spacings to your needs
layout.setContentsMargins(0,0,0,0)
layout.setSpacing(0)
self.editButton = QtWidgets.QPushButton('edit')
self.deleteButton = QtWidgets.QPushButton('del')
self.buttonRow = 0
# add your buttons
layout.addWidget(self.editButton)
layout.addWidget(self.deleteButton)
self.setLayout(layout)
class MainWindow(QtWidgets.QMainWindow):
def __init__(self, parent=None):
QtWidgets.QMainWindow.__init__(self,parent)
self.table = QtWidgets.QTableWidget()
self.table.setColumnCount(3)
self.setCentralWidget(self.table)
data1 = ['row1','row2','row3','row4']
data2 = ['1','2.0','3.00000001','3.9999999']
self.table.setRowCount(4)
for index in range(4):
item1 = QtWidgets.QTableWidgetItem(data1[index])
self.table.setItem(index,0,item1)
item2 = QtWidgets.QTableWidgetItem(data2[index])
self.table.setItem(index,1,item2)
self.btn_sell = EditButtonsWidget()
self.btn_sell.editButton.clicked.connect(self.handleButtonClicked)
self.table.setCellWidget(index,2,self.btn_sell)
def handleButtonClicked(self):
#button = QtWidgets.qApp.focusWidget()
button = self.sender()
index = self.table.indexAt(button.pos())
if index.isValid():
print(index.row(), index.column())
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
MainWindow = MainWindow()
MainWindow.show()
sys.exit(app.exec_())
The position must be of the widget that is set in the QTableWidget, not of one of its children.
In this case it is better to consider the EditButtonsWidget as a black box and expose the clicked signals of the buttons as new signals so that the sender is EditButtonsWidget and no longer the buttons:
class EditButtonsWidget(QtWidgets.QWidget):
edit_clicked = QtCore.pyqtSignal()
delete_clicked = QtCore.pyqtSignal()
def __init__(self, parent=None):
super(EditButtonsWidget,self).__init__(parent)
# add your buttons
layout = QtWidgets.QHBoxLayout(self)
# adjust spacings to your needs
layout.setContentsMargins(0,0,0,0)
layout.setSpacing(0)
self.editButton = QtWidgets.QPushButton('edit')
self.deleteButton = QtWidgets.QPushButton('del')
# add your buttons
layout.addWidget(self.editButton)
layout.addWidget(self.deleteButton)
self.editButton.clicked.connect(self.edit_clicked)
self.deleteButton.clicked.connect(self.delete_clicked)
for index in range(4):
item1 = QtWidgets.QTableWidgetItem(data1[index])
self.table.setItem(index,0,item1)
item2 = QtWidgets.QTableWidgetItem(data2[index])
self.table.setItem(index,1,item2)
self.btn_sell = EditButtonsWidget()
self.btn_sell.edit_clicked.connect(self.handleButtonClicked) # <---
self.table.setCellWidget(index,2,self.btn_sell)
Widget positions always use the parent's coordinate system as a reference.
In your case, the button is a child of EditButtonsWidget, and since it's also the first widget and the layout has no margins, the button is placed at 0, 0 in that coordinate reference system.
A theoretical solution to your problem would be to map the widget position to the actual widget you need a reference for, which is the viewport of the scroll area (the table):
def handleButtonClicked(self):
button = self.sender()
viewportPosition = button.mapTo(self.table.viewport(), QtCore.QPoint())
index = self.table.indexAt(viewportPosition)
if index.isValid():
print(index.row(), index.column())
The mapping is done using an empty QPoint, since the top-left corner of a widget is always 0, 0 in local coordinates.
While this works, it's not the most logic nor elegant or safest way to do so, as you should reference the actual index instaed.
A better solution would be to map the table index, use that as argument of the widget constructor, and send that index for a custom signal.
class EditButtonsWidget(QtWidgets.QWidget):
editClicked = QtCore.pyqtSignal(object)
def __init__(self, index):
super(EditButtonsWidget,self).__init__()
self.index = index
# ...
self.editButton.clicked.connect(lambda: self.editClicked.emit(index))
class MainWindow(QtWidgets.QMainWindow):
def __init__(self, parent=None):
# ...
for index in range(4):
# ...
persistenIndex = QtCore.QPersistentModelIndex(
self.table.indexFromItem(item2))
self.btn_sell = EditButtonsWidget(persistenIndex)
self.btn_sell.editClicked.connect(self.handleButtonClicked)
self.table.setCellWidget(index,2,self.btn_sell)
def handleButtonClicked(self, index):
if index.isValid():
print(index.row(), index.column())
Note that I used a QPersistentModelIndex, which ensures that the model index coordinates are always consistent even if the model changes (by deleting/inserting items or moving them).
Also note that you cannot directly use a QPersistentModelIndex for most functions that take a normal QModelIndex as parameter; in case you need that, you can recreate a QModelIndex like this:
modelIndex = self.table.model().index(
persistentIndex.row(), persistentIndex.column())
I have a gtk.Entry with an icon after the text, intending to be a text search field:
What I'm trying to do is to display a dropdown (i.e. a gtk.ComboBox) when the user clicks on the icon, to choose the type of search. A mock of that feature would be:
I have tried several things without any success. For example, trying to pack an empty gtk.ComboBox only showing an arrow right after the Entry, and stuffing it only on icon-press, which creates the illusion, but it has two drawbacks: a) when I stuff the ComboBox, the toolbar grows, and b) when I clear() the ListStore, the ComboBox retains its width and leaves an ugly grey box.
At this point I guess that I need to create a CellRenderer on icon-press that pops down the icon of the Entry, and I tried without a lot of success to understand the code of gtk.ComboBoxEntry (in gtkcomboboxentry.c), but as far as I understood it uses a vertical Container on the whole piece together with a CellRenderer.
Also GTK+3 doesn't have any ideas on this respect.
Any ideas, or some guidance in how to create this in PyGTK?
I was looking for something similar, so I came up with the code below. I haven't really worried about the aesthetics. I did pass a list of tuples to the MyPopup class, with the idea of passing handlers for each of the menu items in the dropdown. Note that the item.show() is necessary, even though there is a show_all():
from gi.repository import Gtk
class MyPopup(Gtk.MenuButton):
def __init__(self, btndefs):
super(MyPopup, self).__init__()
self.menu = Gtk.Menu()
self.set_popup(self.menu)
#self.set_label(">")
self.set_direction(Gtk.ArrowType.RIGHT)
for btndef in btndefs:
item = Gtk.MenuItem()
item.set_label(btndef[0])
item.show()
self.menu.append(item)
class MainWindow(Gtk.Window):
def __init__(self):
super(MainWindow, self).__init__()
self.set_size_request(100, -1)
self.connect("destroy", lambda x: Gtk.main_quit())
self.hbox = Gtk.Box(orientation = Gtk.Orientation.HORIZONTAL)
self.entry = Gtk.Entry()
self.popup = MyPopup( (("String",),
("String no case",),
("Hexadecimal",),
("Regexp",)) )
self.hbox.pack_start(self.entry, True, True, 0)
self.hbox.pack_start(self.popup, False, True, 0)
self.add(self.hbox)
self.show_all()
def run(self):
Gtk.main()
def main():
mw = MainWindow()
mw.run()
return 0
if __name__ == '__main__':
main()
yup its year late, but lets not make next person stumbled here to be sad like me.
this is the example using Gtk.Menu() popup, you can also similar feat. with Gtk.Popover()
#!/usr/bin/env python3
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, Gdk
opts = {
'hex' : "system-run-symbolic",
'regex' : "font-select-symbolic",
'string' : "font-x-generic-symbolic",
'no-case' : "tools-check-spelling-symbolic",
}
def make_menu(entry, opts):
menu = Gtk.Menu()
for label, icon in opts.items():
item = Gtk.MenuItem()
item.set_label(label)
item.connect(
"activate",
lambda w: entry.set_icon_from_icon_name(0, opts[w.get_label()])
)
menu.append(item)
# NOTE you can use Gtk.ImageMenuItem to add image but its
# Deprecated since version 3.10
menu.show_all()
return menu
def on_icon_release(widget, pos, event):
menu = make_menu(widget, opts)
menu.popup(
parent_menu_shell = None,
parent_menu_item = None,
func = None,
data = None,
button = Gdk.BUTTON_PRIMARY,
activate_time = event.get_time()
)
def make_entry():
entry = Gtk.Entry()
entry.set_icon_from_icon_name(0, 'action-unavailable-symbolic')
entry.set_icon_from_icon_name(1, 'fonts')
entry.set_icon_sensitive(1, True)
entry.set_icon_activatable(1, True)
entry.connect("icon-release", on_icon_release)
return entry
root = Gtk.Window()
root.add(make_entry())
root.show_all()
Gtk.main()