2

I have a QTableView as below. I'd like to press the Test button and insert an "a" at the cursor - for example in the middle of "11" at (row,column) = (2,2). That is, the user double-clicks cell (2,2) and places the cursor in the middle of "11", and presses Test. Desired result: "1a1".

screen capture

Is this doable? If yes, how? Thanks very much.

# coding: utf-8

import sys
from PyQt4 import QtGui, QtCore
from PyQt4.QtCore import *
from PyQt4.QtGui import *

MY_ARRAY = [['00', '01', '02'],
            ['10', '11', '12'],
            ['20', '21', '22']]


class MyWindow(QTableView):
    def __init__(self, *args):
        super(MyWindow, self).__init__()

        self.tablemodel = MyTableModel(MY_ARRAY)

        self.tableview = QTableView()

        self.tableview.setModel(self.tablemodel)

        self.tableview.setItemDelegate(MyDelegate(self))

        self.layout = QVBoxLayout(self)
        self.layout.addWidget(self.tableview)

        self.button1 = QPushButton("Test")

        self.button1.released.connect(self.test)

        self.layout.addWidget(self.button1)
        self.setLayout(self.layout)

    def test(self):

        # MY_ARRAY.append([30,31,32])

        index = self.tableview.currentIndex()
        item = self.tablemodel.data(index, Qt.DisplayRole)

        print("item %s " % item)

        item_edit = self.tableview.edit(index)

        qDebug("qDebug: item_edit %s " % item_edit)

        MY_ARRAY.insert(index.row(), ['30', '31', '32'])

        self.tablemodel.layoutChanged.emit()

        qDebug(" {} " .format(MY_ARRAY))

        qcursor = QCursor.pos()
        qDebug(" {} ".format(qcursor))

        qcursor1 = self.mapFromGlobal(qcursor)
        qDebug(" {} ".format(qcursor1))

        # qDebug(" self.tableview.indexAt(qcursor) {} ".format(self.tableview(qcursor)))
        # qDebug(" self.tableview.indexAt(qcursor1) {} ".format(self.tableview(qcursor1)))

        # print(' index.row(): ', index.row())

        qDebug(
            " tableview.rowViewportPosition %s " %
            self.tableview.rowViewportPosition(index.row()))
        qDebug(
            " tableview.columnViewportPosition %s " %
            self.tableview.columnViewportPosition(index.column()))

        # qDebug(" tableview.viewport() %s " % self.tableview.viewport(qcursor))

        item = self.tableview.setCurrentIndex(index)
        qDebug(" tableview.item() %s " % self.tableview)


class MyTableModel(QAbstractTableModel):
    def __init__(self, datain, parent=None, *args):
        super(MyTableModel, self).__init__(parent, *args)

        self.arraydata = datain

    def rowCount(self, parent):
        return len(self.arraydata)

    def columnCount(self, parent):
        return len(self.arraydata[0])

    def data(self, index, role):
        if not index.isValid():
            return None

        elif not (role == Qt.DisplayRole or role == Qt.EditRole):
            return None
        return (self.arraydata[index.row()][index.column()])

    def setData(self, index, value, role=Qt.EditRole):
        self.arraydata[index.row()][index.column()] = value
        return True

    def flags(self, index):
        return Qt.ItemIsEditable | Qt.ItemIsEnabled | Qt.ItemIsSelectable


class MyDelegate(QStyledItemDelegate):

    def __init__(self, parent=None):
        super(MyDelegate, self).__init__(parent)

    def createEditor(self, parent, option, index):
        editor = QLineEdit(parent)
        self.connect(editor, SIGNAL("returnPressed()"),
                     self.commitAndCloseEditor)
        return editor

    def commitAndCloseEditor(self):
        editor = self.sender()
        if isinstance(editor, (QTextEdit, QLineEdit)):
            self.emit(SIGNAL("commitData(QWidget*)"), editor)
            self.emit(SIGNAL("closeEditor(QWidget*)"), editor)

    def setEditorData(self, editor, index):
        text = index.model().data(index, Qt.DisplayRole)

        editor.setText(text)

    def setModelData(self, editor, model, index):
        model.setData(index, editor.text())

def main():
    app = QApplication(sys.argv)
    w = MyWindow()
    w.show()
    sys.exit(app.exec_())

if __name__ == "__main__":
    main()
mike
  • 113
  • 2
  • 10
  • Welcome to Stack Overflow! It looks like you are asking for homework help. While we have no issues with that per se, please observe these [dos and don'ts](http://meta.stackoverflow.com/questions/334822/how-do-i-ask-and-answer-homework-questions/338845#338845), and edit your question accordingly. – Joe C Dec 03 '16 at 17:29
  • @JoeC. This really looks nothing like a homework question. – ekhumoro Dec 03 '16 at 18:07
  • Well, it's not homework. It's a problem from my amateur project about dual language alignment. An alogorithm would automatically aligns two texts. Very often there would be misalignment and manual alignment is required. I am relatively new to PyQt. I have spent a few days reading books and googling but cant find any solutions. So I'd really appreciate if anyone can give a pointer or two. – mike Dec 04 '16 at 01:24
  • What I really need is a way to grab the table element (cell) where the cursor is placed, something like **cell = mytableview.cellAt(i,j)** (Anyway to do this?). As I understand it, the cell is a QLineEdit instance. I can then do something like qtextcursor = cell.textCursor() and handle qtextcursor like an text editor (plenty of tutorials on the net about editors). – mike Dec 04 '16 at 01:44

2 Answers2

2

A table cells do not have a cursor and are not directly editable. The editing capabilties are provided by the item-delegate. By default, the editor widget for text data is a QLineEdit, but other data types may use different editor widgets, such as a QSpinBox for numerical data, or a QComboBox for boolean data. The specific widget used can be controlled by setting a custom item-delegate.

The big problem with using something like a button to insert text in the editing widget, is that the editor will be automatically closed (and destroyed) as soon as the button is clicked. It will therefore be much simpler to use a context-menu to add custom actions:

class MyWindow(QTableView):
    def __init__(self, *args):
        ...
        self.delegate = MyDelegate(self)
        self.delegate.contextMenuRequested.connect(self.showContextMenu)
        self.tableview.setItemDelegate(self.delegate)

    def showContextMenu(self, editor, pos):
        pos = editor.mapToGlobal(pos)
        menu = editor.createStandardContextMenu()
        menu.addSeparator()
        action = menu.addAction('Insert Text')
        if menu.exec_(pos) is action:
            editor.insert(' foo ')

class MyDelegate(QStyledItemDelegate):
    contextMenuRequested = pyqtSignal(object, QPoint)

    def createEditor(self, parent, option, index):
        editor = QLineEdit(parent)
        editor.setContextMenuPolicy(Qt.CustomContextMenu)
        editor.customContextMenuRequested.connect(
            lambda pos: self.contextMenuRequested.emit(editor, pos))
        return editor
ekhumoro
  • 115,249
  • 20
  • 229
  • 336
  • Thanks very much. That "1a1" is just an example. MyDelegate(QStyledItemDelegate)'s createEditor() creates a QLineEdit editor, I just don't know how to make use of it. My use case looks like this: Each cell contains a string (a paragraph of English text or text in other languages). A user visually examines the content of a cell and decides on a place to insert some extra text, he then places the cursor in that place and presses a pushbutton. I found indexWidget for QTableView in the meantime, but am not too sure whether it can be made use of. – mike Dec 05 '16 at 15:00
  • @mike. Using a separate button to insert text will be quite difficult to implement. A context-menu will be much simpler. See my updated answer. – ekhumoro Dec 05 '16 at 18:47
  • Thanks very much again. I'll give it a try using your suggestion. Great help, I appreciate it. – mike Dec 06 '16 at 00:33
2

After a lot of struggle and uses of qDebug I finally managed to find a solution. I am sure it can be further improved. But I don't know much about PyQt. The idea is to cache the cursor position in MyDelegate(QStyledItemDelegate) before the editor is closed. I hope it can be useful to someone who encounters the same problem.

class MyDelegate(QStyledItemDelegate):
    ...
    def createEditor(self, parent, option, index):
        self.cursorpos = -1  # unset flag
        editor = QLineEdit(parent)
        self.connect(editor, SIGNAL("editingFinished()"),
                         self.commitAndCloseEditor)
        return editor

    def commitAndCloseEditor(self):
        editor = self.sender()
        self.cursorpos = editor.cursorPosition()
        if isinstance(editor, (QTextEdit, QLineEdit)):
            self.emit(SIGNAL("commitData(QWidget*)"), editor)
            self.emit(SIGNAL("closeEditor(QWidget*)"), editor)

The whole thing is given below.

# coding: utf-8

import sys
from PyQt4 import QtGui, QtCore
from PyQt4.QtCore import *
from PyQt4.QtGui import *

MY_ARRAY = [['00', '01', '02'],
            ['10', '11', '12'],
            ['20', '21', '22']]


class MyWindow(QTableView):
    def __init__(self, *args):
        super(MyWindow, self).__init__()

        self.tablemodel = MyTableModel(MY_ARRAY)

        self.tableview = QTableView()

        self.tableview.setModel(self.tablemodel)

        # self.tableview.setItemDelegate(MyDelegate(self))

        self.delegate = MyDelegate(self)
        self.tableview.setItemDelegate(self.delegate)

        self.layout = QVBoxLayout(self)
        self.layout.addWidget(self.tableview)

        self.button1 = QPushButton("Test")

        self.button1.released.connect(self.test)

        self.layout.addWidget(self.button1)
        self.setLayout(self.layout)

    def test(self):

        index = self.tableview.currentIndex()
        item = self.tablemodel.data(index, Qt.DisplayRole)

        qDebug("item %s " % item)

        qDebug(" <test><MyDelegateMyDelegate> self.delegate.cursorpos: %s " % self.delegate.cursorpos)

        cursorpos = self.delegate.cursorpos
        qDebug(" <test> cursor pos %s " % cursorpos)
        if cursorpos > -1:
            index.model().setData(index, item[:cursorpos] + ' foo ' + item[cursorpos:])
            self.tablemodel.layoutChanged.emit()
            self.delegate.cursorpos = -1


class MyTableModel(QAbstractTableModel):
    def __init__(self, datain, parent=None, *args):
        super(MyTableModel, self).__init__(parent, *args)

        self.arraydata = datain

    def rowCount(self, parent):
        return len(self.arraydata)

    def columnCount(self, parent):
        return len(self.arraydata[0])

    def data(self, index, role):
        if not index.isValid():
            return None

        elif not (role == Qt.DisplayRole or role == Qt.EditRole):
            return None
        return (self.arraydata[index.row()][index.column()])

    def setData(self, index, value, role=Qt.EditRole):
        self.arraydata[index.row()][index.column()] = value
        return True

    def flags(self, index):
        return Qt.ItemIsEditable | Qt.ItemIsEnabled | Qt.ItemIsSelectable


class MyDelegate(QStyledItemDelegate):

    def __init__(self, parent=None):
        super(MyDelegate, self).__init__(parent)
        self.cursorpos = -1  # unset flag

    def createEditor(self, parent, option, index):
        self.cursorpos = -1  # unset flag
        editor = QLineEdit(parent)
        self.connect(editor, SIGNAL("editingFinished()"),
                         self.commitAndCloseEditor)
        return editor

    def commitAndCloseEditor(self):
        editor = self.sender()
        self.cursorpos = editor.cursorPosition()
        if isinstance(editor, (QTextEdit, QLineEdit)):
            self.emit(SIGNAL("commitData(QWidget*)"), editor)
            self.emit(SIGNAL("closeEditor(QWidget*)"), editor)

    def setEditorData(self, editor, index):
        text = index.model().data(index, Qt.DisplayRole)

        editor.setText(text)


    def setModelData(self, editor, model, index):
        model.setData(index, editor.text())

def main():
    app = QApplication(sys.argv)
    w = MyWindow()
    w.show()
    sys.exit(app.exec_())

if __name__ == "__main__":
    main()
mike
  • 113
  • 2
  • 10