I recently updated from PyQt 5.7.0 to 5.7.1 and code that worked correctly prior to the update stopped working correctly. So, either my code was always wrong but PyQt5 allowed it to work, or a bug was introduced in PyQt 5.7.1.
I have a custom table view that inherits from QTableView using a custom model that inherits from QAbstractTableModel. When new rows are added to the model, they are not visible in the table view. In fact, no rows ever become visible. Through some debugging, I have validated that the number of rows is changing as expected in my derived model class.
import sys
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
import PyQt5.Qt
class JobTableModel(QAbstractTableModel):
def __init__(self, data, parent):
super(JobTableModel, self).__init__()
assert isinstance(parent, QTableView), "'parent' is not a QTableView object"
self._parent = parent
self._data = data
self._rows = 0
self._updateModel()
# end constructor
def updateRows(self, rows):
self.layoutAboutToBeChanged.emit()
self._rows = rows
self.layoutChanged.emit()
# end updateRows
def _updateModel(self):
# Only update rows that are visible to the user
# Note: self._parent is a QTableView
minRow = self._parent.rowAt(0)
if minRow >= 0:
maxRow = self._parent.rowAt(self._parent.height())
if maxRow < 0: maxRow = self._rows - 1
for row in range(minRow, maxRow + 1):
self.dataChanged.emit(self.index(row, 0), self.index(row, self.columnCount(None) - 1))
QTimer.singleShot(490, self._updateModel)
# end _updateModel
def headerData(self, section, orientation, role):
if role == Qt.DisplayRole and orientation == Qt.Horizontal:
return str(section)
#
# end headerData
def rowCount(self, modelIndex):
return self._rows
# end rowCount
def columnCount(self, modelIndex):
return 8 # always the same number of columns
# end columnCount
def data(self, index, role):
if not index.isValid(): return None
if role == Qt.DisplayRole: return '{0}, {1}'.format(index.row(), index.column())
return None
# end data
class JobTableView(QTableView):
def __init__(self, data, parent):
super(JobTableView, self).__init__(parent)
self.setModel(JobTableModel(data, self))
self.setAlternatingRowColors(True)
self.setWordWrap(False)
self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn)
self.verticalHeader().setVisible(False)
self.verticalHeader().setDefaultSectionSize(23)
self.setSelectionBehavior(QAbstractItemView.SelectRows)
self.setEditTriggers(QAbstractItemView.NoEditTriggers)
if __name__ == '__main__':
app = QApplication(sys.argv)
tv = JobTableView(None, None)
tv.show()
tv.model().updateRows(1)
app.exec_()
I ran the test case using PyQt-5.7, PyQt-5.7.1 and PyQt-5.7.2.dev1701131704 (built with SIP-4.19.1.dev1701101411). The problem is reproducible in PyQt-5.7.1, but not in the other two versions. So, as was suggested in the comments, there is a bug in PyQt-5.7.1 which has already been fixed in the latest snapshots.
Related
I want to make a qtableview widget correctly updating. I'm working on a calibration applet, where i wanna fill cell by cell of an (e. g.) 100 x 100 x 4 array.
If my hardware reaches position 1, 2, 3, and so on, I will trigger a voltage measurement and gather those values with an i2c-read out-function.
So issues a la "my qtableview is not updating" are omnipresent.
But so far, I'm not able to adapt examples I have read, to make my code behaving as I want.
So if you look at my screenshot:
the problem is:
when I'm clicking on row or col +/-, the yellow highlighting is not changing instantly
when I'm clicking on store i²c, which is meant to put a dummy 0.0 in/on selected cell, this is also not changing instantly
Several methods like telling the model that data has changed, I was not able to implement correctly so far.
Could some of you help me to add a few lines just to force applet to update correctly?
fillCSV_forum.py:
### libraries:
import sys # to use e. g. exit function
from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5.QtCore import Qt
import pandas as pd # to use pandas tables
import numpy as np # to use numpy arrays
### user-defined header files / modules:
from uLib_coloredWidget import Color # import user-defined functions..
from rndGen import i2c_read # .. see folder
### initial settings:
# general
np.random.seed(4) # if using np.random, then pseudo random values will occure
### globals:
nRow = 5; nCol = 5; nSht = 4 # table dimensions
rowIdx = colIdx = shtIdx = 0 # aux vars to index array
rndArray = np.random.rand(nSht, nRow, nCol) * 4.3 # auxilliary before integrating i2c
tabNames = ["A4", "A5","A6","A7"] # array (list) with tab names
rowIdx = 1; colIdx = 1 # aux vars to index selected cell
### declarations / definitions:
class TableModel(QtCore.QAbstractTableModel):
def __init__(self, data):
super(TableModel, self).__init__()
self._data = data
def data(self, index, role):
if role == Qt.BackgroundRole and index.column() == colIdx and index.row() == rowIdx:
# See below for the data structure.
return QtGui.QColor('yellow')
if role == Qt.DisplayRole:
value = self._data.iloc[index.row(), index.column()]
if isinstance(value, float): # to set fixed DISPLAYED precision of floats
return "%.4f" % value
return str(value)
def rowCount(self, index):
return self._data.shape[0]
def columnCount(self, index):
return self._data.shape[1]
def headerData(self, section, orientation, role):
# section is the index of the column/row.
if role == Qt.DisplayRole:
if orientation == Qt.Horizontal:
return str(self._data.columns[section])
if orientation == Qt.Vertical:
return str(self._data.index[section])
class App(QtWidgets.QMainWindow):
# local variable's declarations
# init
def __init__(self):
super().__init__() # default one
self.setup_main_window() # using helper function to increase readability (function call within self scope)
# setup main window
self.createLayout() # function call to create layouts with widgets
self.post_main_window() # pass edited layouts to main window
# declaration / methods / helper functions
def setup_main_window(self): # to set window's / applet's properties
self.centralwidget = QtWidgets.QWidget()
self.setCentralWidget(self.centralwidget)
self.resize( 800, 400 )
self.setWindowTitle( "# disposition calibration #" )
def post_main_window(self): # to publish edited layouts in app window
self.centralwidget.setLayout(self.lyoOut)
def createLayout(self): # to create layouts with widgets
self.lyoOut = QtWidgets.QVBoxLayout() # declare different layouts
self.lyoIn1 = QtWidgets.QHBoxLayout()
self.lyoIn2 = QtWidgets.QGridLayout()
self.createWidgets() # function call pass widgets to sub-layouts
self.lyoOut.addLayout(self.lyoIn1) # inner layouts got widgets by self.createWidgets()
self.lyoOut.addLayout(self.lyoIn2) # merge edited inner layout in/to outside layout here
def createWidgets(self): # create master-layout's widgets (function calls)
# fill 1st row of ouside layout
self.lyoIn1 = self.createNestedTabs(self.lyoIn1) # function call to create master-tabs
# fill 2nd row of outside layout
self.lyoIn2 = self.createButtons(self.lyoIn2) # function call to create buttons
def createNestedTabs(self, layout2modify): # create 1st tab layer
self.MstTabs = QtWidgets.QTabWidget() # create tabs-widget
self.MstTabs.setTabPosition(QtWidgets.QTabWidget.North) # set it's location
self.MstTabs.addTab(self.createChildTabs(), "data") # add several sub-tab layouts to that widget
self.MstTabs.addTab(Color("orange"), "plot") #
stylesheet = """
QTabBar::tab:selected {background: lightgreen;}
QTabBar::tab:!selected {background: lightyellow;}
"""
self.MstTabs.setStyleSheet(stylesheet)
layout2modify.addWidget(self.MstTabs) # add this tabs-widget to passed-in layout
return layout2modify # return edited layout
def createChildTabs(self): # create 2nd tab layer
self.ChdTabs = QtWidgets.QTabWidget() # create tabs-widget
self.ChdTabs.setTabPosition(QtWidgets.QTabWidget.West) # set it's location
self.ChdTabs.addTab(self.createPandasTables(0), "A4")
self.ChdTabs.addTab(self.createPandasTables(1), "A5")
self.ChdTabs.addTab(self.createPandasTables(2), "A6")
self.ChdTabs.addTab(self.createPandasTables(3), "A7")
return self.ChdTabs # return created widgets
def createPandasTables(self, shtIdx): # to creating and editing pandas tables-widgets
# use indexed (pandas)dataframe sheet values
Lbl = ["a","b","c","d","e"]
self.df = pd.DataFrame(rndArray[shtIdx], columns = Lbl, index = Lbl)
# .. to create a widget
self.table_widget = QtWidgets.QTableView() # create QTableView-Widget
self.model = TableModel(self.df) # make df to user defined table model to use in widgets
self.table_widget.setModel(self.model) # pass created model to created widget
# certain formatings
self.table_widget.resizeColumnsToContents() # set column width to content
self.table_widget.horizontalHeader().setStretchLastSection(True) # strech last column to frame width
self.table_widget.verticalHeader().setStretchLastSection(True) # strech last row to frame height
self.table_widget.setAlternatingRowColors(True) # switch on alternating row highlighting
return self.table_widget # return created widgets
def createButtons(self, layout2modify): # helper function - to create layout's buttons
bStoreI2C = QtWidgets.QPushButton("Store i²c")
bStoreI2C.clicked.connect(lambda:self.storeVal())
bStoreI2C.setStyleSheet("QPushButton::hover"
"{"
"background-color : yellow;"
"}")
layout2modify.addWidget(bStoreI2C, 1, 3, 2, 1)
self.lbl_1 = QtWidgets.QLabel()
self.lbl_1.setText(str(rowIdx))
self.lbl_1.setAlignment(QtCore.Qt.AlignCenter)
layout2modify.addWidget(self.lbl_1, 1, 5, 2, 1)
bRowAdd = QtWidgets.QPushButton("row +")
bRowAdd.clicked.connect(lambda:self.rowAdd())
layout2modify.addWidget(bRowAdd, 2, 6)
bRowSub = QtWidgets.QPushButton("row -")
bRowSub.clicked.connect(lambda:self.rowSub())
layout2modify.addWidget(bRowSub, 1, 6)
return layout2modify # return edited layout
def storeVal(self):
#i2c_vals = get_i2c_values(i2c_addrs)
for i in range (0,4):
#self.tbData[i, rowIdx, colIdx] = i2c_vals[i] # change cell entries with imported value
rndArray[i, rowIdx, colIdx] = 0
#self.tbData[sht, row, col] = 99 # change cell entry with imported value
# try 1
#self.table_widget.update()
#self.table_widget.repaint()
#self.model.select()
#self.table_widget.select()
# try 2
# self.refreshModel() # not working so far
#self.model = TableModel(self.df) # make df to user defined table model to use in widgets
#self.table_widget.setModel(self.model)
# print(rndArray)
print('i²c-value(s) stored')
def rowAdd(self):
global rowIdx
rowIdx = (rowIdx + 1) % nRow # increment and modulo to cycle
self.lbl_1.setText(str(rowIdx)) # update label's text
print('row is ', rowIdx)
def rowSub(self):
global rowIdx
rowIdx = (rowIdx - 1) % nRow # increment and modulo to cycle
self.lbl_1.setText(str(rowIdx)) # update label's text
print('row is ', rowIdx)
### main:
def main():
app = QtWidgets.QApplication(sys.argv) # instanciate app
window = App() # instanciate window
window.show() # show window
app.exec_() # stuck here 'til window is closed
print('# window will be terminated.. #')
time.sleep(2)
print('# ..app execution closed #')
# make file executable
if __name__ == '__main__':
main()
rndGen.py: (is called in fillCSV_forum.py)
import numpy as np
def i2c_read():
floats = np.random.rand(4,1,1) * 4.3
return floats
uLib_coloredWidget.py: (is called in fillCSV_forum.py)
from PyQt5.QtGui import QColor, QPalette
from PyQt5.QtWidgets import QWidget
class Color(QWidget):
def __init__(self, color):
super().__init__()
self.setAutoFillBackground(True)
palette = self.palette()
palette.setColor(QPalette.Window, QColor(color))
self.setPalette(palette)
pip freeze --local-output of virtual enviroment:
numpy==1.23.0
pandas==1.4.3
PyQt5==5.15.7
PyQt5-Qt5==5.15.2
PyQt5-sip==12.11.0
python-dateutil==2.8.2
pytz==2022.1
six==1.16.0
[... additionally many hours of trial and error....]
i think i finally got a dirty solution / work around..
the problem i could determining, was e. g. if i am clicking the col+/- or store button, the focus of recently selected tab is vanishing.
first when click again into any tab region or select another tabs those values are updating.
so i tried to look for programmatically tab swap and did this as a dirty work around because i could not find a method like "reactivate tab again"
i added ... :
def storeVal(self):
#i2c_vals = get_i2c_values(i2c_addrs)
for i in range (0,nSht):
self.df[i].iat[rowIdx, colIdx] = 99
print('i²c-value(s) stored')
self.show_data()
def show_data(self):
x = self.ChdTabs.currentIndex()
print(x) # debugging
self.ChdTabs.setCurrentIndex(1)
self.ChdTabs.setCurrentIndex(x)
... a show method and called it at the end of the store-method.
in this show method i programmatically swap the active tab back and forth. this is so fast, that i cannot see it
now my values are correctly shown
another tiny if else code is necessary to also swap if tab 1 is selected, but this is cosmetic thing
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 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()
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'd like to set my rows to a fixed height. I've found an example using QAbstractItemModel, but I'm using QStandardItemModel. When I run the app, the QTreeView is blank. Any thoughts on how I could get this working for a QStandardItemModel?
import sys
from PySide import QtCore, QtGui
class TreeItem(object):
def __init__(self, data, parent=None):
self.parentItem = parent
self.data = data
self.childItems = []
def appendChild(self, item):
self.childItems.append(item)
def row(self):
if self.parentItem:
return self.parentItem.childItems.index(self)
return 0
class TreeModel(QtCore.QAbstractItemModel):
def __init__(self, parent=None):
super(TreeModel, self).__init__(parent)
self.rootItem = TreeItem(None)
for i, c in enumerate("abcdefg"):
child = TreeItem([i, c], self.rootItem)
self.rootItem.appendChild(child)
parent = self.rootItem.childItems[1]
child = TreeItem(["down", "down"], parent)
parent.appendChild(child)
def columnCount(self, parent):
return 2
def data(self, index, role):
if not index.isValid():
return None
if role == QtCore.Qt.DisplayRole:
item = index.internalPointer()
return item.data[index.column()]
elif role == QtCore.Qt.SizeHintRole:
print "giving size hint"
return QtCore.QSize(10, 10)
return None
def flags(self, index):
if not index.isValid():
return QtCore.Qt.NoItemFlags
return QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable
def headerData(self, section, orientation, role):
if orientation == QtCore.Qt.Horizontal and role == QtCore.Qt.DisplayRole:
return ["A", "B"][section]
return None
def index(self, row, column, parent):
if not self.hasIndex(row, column, parent):
return QtCore.QModelIndex()
if not parent.isValid():
parentItem = self.rootItem
else:
parentItem = parent.internalPointer()
childItem = parentItem.childItems[row]
if childItem:
return self.createIndex(row, column, childItem)
else:
return QtCore.QModelIndex()
def parent(self, index):
if not index.isValid():
return QtCore.QModelIndex()
parentItem = index.internalPointer().parentItem
if parentItem == self.rootItem:
return QtCore.QModelIndex()
return self.createIndex(parentItem.row(), 0, parentItem)
def rowCount(self, parent):
if parent.column() > 0:
return 0
if not parent.isValid():
parentItem = self.rootItem
else:
parentItem = parent.internalPointer()
return len(parentItem.childItems)
if __name__ == '__main__':
app = QtGui.QApplication(sys.argv)
model = TreeModel()
view = QtGui.QTreeView()
view.setModel(model)
view.setWindowTitle("Simple Tree Model")
view.show()
sys.exit(app.exec_())
Also, I've been looking around for the meaning of the term delegate in Qt, but the concept still isn't clicking in my head yet. Any insight into that would be appreciated as well!
You generally do this using QItemDelegates (or QStyledItemDelegate if you want stylesheet styling to work) by overriding the sizeHint method and always returning a size of a fixed height.
class MyDelegate(QtGui.QStyledItemDelegate):
def sizeHint(self, option, index):
my_fixed_height = 30
size = super(MyDelegate, self).sizeHint(option, index)
size.setHeight(my_fixed_height)
return size
view = QtGui.QTreeView()
delegate = MyDelegate()
view.setItemDelegate(delegate)