Search code examples
pyqt5qtableviewqlineeditqpushbuttonqstyleditemdelegate

How to let QLineEdit and QPushButton show in a column and style like this in a tableview in PyQt5?


I have 3 three pictures shown in below:

picture1

enter image description here

enter image description here

How to let QLineEdit and QPushButton show in a column and style like this in a tableview in PyQt5?

I have the following three pictures shown in below,

I want to write a GUI which fulfill these feature by PyQt5:

  1. when click mouse one time, it will select this line, and also highlight this line1. just like digital 1 point to
  2. after some seconds, at 'Click here to add a file' click mouse again one time, it will enter edit mode. just like digital 2 point to, a QLineEdit and a QPushButton '...' will display in the 2nd column. if I click '...', and pop up a File Selection Dialog, when I select a file, it will replace 'Click here to add a file' by file absolute path.

    be careful: not double-click mouse enter into edit mode, it should be click mouse one time, some seconds later, click mouse again, will enter into edit mode. when I select a file which absolute path is very very long. I can see some char show behind QPushButton '...', it looks like QPushButton overlap on the right of QLineEdit.

  3. when step 2 is done, if continue to click mouse in other line, QLineEdit and QPushButton '...' in step 2 will disapper, like line 'VAR("myModelConer")

I have research 3 features many days, but cannot get my desire style. I will give my code in here, as a example, it is 2 rows and 2 columns. anybody could help me to modify and fullfil above 3 function.

Thanks in advance

from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
class Delegate(QStyledItemDelegate):
    def __init__(self, parent=None):
        super(Delegate, self).__init__(parent)

    def createEditor(self, parent, option, index):
        if index.column() == 0:
            lineedit    = QLineEdit("$woroot/1.scs",parent)
            pushbutton  = QPushButton("...", parent)
            #lineedit   = QLineEdit("..",self.parent())
            #pushbutton = QPushButton("...", self.parent())
            lineedit.index       = [index.row(), index.column()]
            pushbutton.index    = [index.row(), index.column()]
            h_box_layout = QHBoxLayout()
            h_box_layout.addWidget(lineedit)
            h_box_layout.addWidget(pushbutton)
            h_box_layout.setContentsMargins(0, 0, 0, 0)
            h_box_layout.setAlignment(Qt.AlignCenter)
            widget = QWidget()
            widget.setLayout(h_box_layout)
            self.parent().setIndexWidget(
                index,
                widget
            )
        elif index.column() == 1:
            combobox = QComboBox(parent)
            combobox.addItems(section_list)
            combobox.setEditable(True)
            #combobox.editTextChanged.connect(self.commitAndCloseEditor)        
            return combobox

    def setEditorData(self, editor, index):
        text = index.model().data(index, Qt.DisplayRole)
        print "setEditorData, text=", text
        text = str(text)
        i = editor.findText(text)
        print "i=", i
        if i == -1:     
            i = 0
        editor.setCurrentIndex(i)  

    def setModelData(self, editor, model, index):

        text = editor.currentText()
        if len(text) >= 1:
            model.setData(index, text)

    def updateEditorGeometry(self, editor, option, index):
        editor.setGeometry(option.rect)

    def commitAndCloseEditor(self):
        editor = self.sender()
        if isinstance(editor, (QTextEdit, QLineEdit,QSpinBox,QComboBox)):
            self.commitData[QWidget].emit(editor)
            self.closeEditor[QWidget].emit(editor)
if __name__ == '__main__':
    import sys
    app = QApplication(sys.argv)
    model = QStandardItemModel(4, 2)
    tableView = QTableView()
    tableView.setModel(model)
    delegate = Delegate(tableView)
    tableView.setItemDelegate(delegate)
    section_list = ['w','c','h']
    for row in range(4):
        for column in range(2):
            index = model.index(row, column, QModelIndex())
            model.setData(index, (row + 1) * (column + 1))
    tableView.setWindowTitle("Spin Box Delegate")
    tableView.show()
    sys.exit(app.exec_())

Solution

  • If you want to use a complex widget for an editor, you certainly should not use setIndexWidget() within createEditor, because you will loose direct access to and control over it. Return the complex widget instead, and ensure that both setModelData and setEditorData act properly.

    To check for the "delayed" click, you also need to override editorEvent() to ensure that the event is actually a left button click.
    This only won't be enough, though: item view selections are always delayed by a cycle of the event loop, so getting the current selection just after a click is not reliable, as it will updated afterwards; you need to use a single shot QTimer in order to correctly check the selection and current index of the table.

    Finally, there's no need to check for the column in the delegate, just use setItemDelegateForColumn() instead.

    class ClickDelegate(QtWidgets.QStyledItemDelegate):
        blankText = '<Click here to add path>'
    
        def openFileDialog(self, lineEdit):
            if not self.blankText.startswith(lineEdit.text()):
                currentPath = lineEdit.text()
            else:
                currentPath = ''
            path, _ = QtWidgets.QFileDialog.getOpenFileName(lineEdit.window(), 
                'Select file', currentPath)
            if path:
                lineEdit.setText(path)
    
        def createEditor(self, parent, option, index):
            editor = QtWidgets.QWidget(parent)
    
            layout = QtWidgets.QHBoxLayout(editor)
            layout.setContentsMargins(0, 0, 0, 0)
            layout.setSpacing(0)
    
            editor.lineEdit = QtWidgets.QLineEdit(self.blankText)
            layout.addWidget(editor.lineEdit)
            # set the line edit as focus proxy so that it correctly handles focus
            editor.setFocusProxy(editor.lineEdit)
            # install an event filter on the line edit, because we'll need to filter
            # mouse and keyboard events
            editor.lineEdit.installEventFilter(self)
    
            button = QtWidgets.QToolButton(text='...')
            layout.addWidget(button)
            button.setFocusPolicy(QtCore.Qt.NoFocus)
            button.clicked.connect(lambda: self.openFileDialog(editor.lineEdit))
            return editor
    
        def setEditorData(self, editor, index):
            if index.data():
                editor.lineEdit.setText(index.data())
            editor.lineEdit.selectAll()
    
        def setModelData(self, editor, model, index):
            # if there is no text, the data is cleared
            if not editor.lineEdit.text():
                model.setData(index, None)
            # if there is text and is not the "blank" default, set the data accordingly
            elif not self.blankText.startswith(editor.lineEdit.text()):
                model.setData(index, editor.lineEdit.text())
    
        def initStyleOption(self, option, index):
            super().initStyleOption(option, index)
            if not option.text:
                option.text = self.blankText
    
        def eventFilter(self, source, event):
            if isinstance(source, QtWidgets.QLineEdit):
                if (event.type() == QtCore.QEvent.MouseButtonPress and 
                    source.hasSelectedText() and 
                    self.blankText.startswith(source.text())):
                        res = super().eventFilter(source, event)
                        # clear the text if it's the "Click here..."
                        source.clear()
                        return res
                elif event.type() == QtCore.QEvent.KeyPress and event.key() in (
                    QtCore.Qt.Key_Escape, QtCore.Qt.Key_Tab, QtCore.Qt.Key_Backtab):
                        # ignore some key events so that they're correctly filtered as
                        # they are emitted by actual editor (the QWidget) 
                        return False
            return super().eventFilter(source, event)
    
        def checkIndex(self, table, index):
            if index in table.selectedIndexes() and index == table.currentIndex():
                table.edit(index)
    
        def editorEvent(self, event, model, option, index):
            if (event.type() == QtCore.QEvent.MouseButtonPress and 
                event.button() == QtCore.Qt.LeftButton and
                index in option.widget.selectedIndexes()):
                    # the index is already selected, we'll delay the (possible)
                    # editing but we MUST store the direct reference to the table for
                    # the lambda function, since the option object is going to be
                    # destroyed; this is very important: if you use "option.widget"
                    # in the lambda the program will probably hang or crash
                    table = option.widget
                    QtCore.QTimer.singleShot(0, lambda: self.checkIndex(table, index))
            return super().editorEvent(event, model, option, index)