1

I want a widget in Qt that will act like a spreadsheet cell does. It can display text, then when the user double-clicks on it, it becomes editable. Once the user is done with editing and presses Enter, the text gets saved and the control is not editable anymore. If the user hits Escape while editing, then the control returns to its previous value.

One possible solution is sub-classing QWidget, QLabel and QLineEdit. Are there any other solutions available in Qt?

santahopar
  • 2,933
  • 2
  • 29
  • 50
  • 1
    Possible duplicate of [How to make a QLineEdit not editable in Windows](https://stackoverflow.com/questions/23915700/how-to-make-a-qlineedit-not-editable-in-windows) – Mikhail Jul 06 '18 at 20:50
  • @Mikhail, this will still require sub-classing and playing with style sheets, which does not make the code look that pretty. I was wondering if I can resort to a solution with delegates. – santahopar Jul 06 '18 at 20:58
  • 1
    If the task is not done by Qt you will have to subclass, listen to events, etc. subclassifying is the most common task, what's wrong with that ?, create a nice .h file and there place your code. I do not know what a beautiful and beautiful answer just for you. :) – eyllanesc Jul 06 '18 at 21:05
  • @eyllanesc, I like how the Qt example http://doc.qt.io/qt-5/qtwidgets-itemviews-spreadsheet-example.html is done for instance. It's using `QTableView`, which is too much for what I need. I was wondering if there is a similar solution for QLineEdit. – santahopar Jul 06 '18 at 21:09
  • @armanali That is done by a delegate, and the delegate creates the editor according to the user's wishes, but the delegate only works in the classes that belong to QAbstractItemView such as QTableView, QListView, QTableWidget, QListWidget. Do you have any of those classes? – eyllanesc Jul 06 '18 at 21:16
  • @eyllanesc, I don't. I have started the implementation with sub-classing. I will post the solution here, since I think it's a useful widget to have, at least in my opinion. – santahopar Jul 06 '18 at 21:18
  • @armanali That is done by a delegate, and the delegate creates the editor according to the user's wishes, but the delegate only works in the classes that belong to QAbstractItemView such as QTableView, QListView, QTableWidget, QListWidget. Do you have any of those classes? – eyllanesc Jul 06 '18 at 21:20
  • @armanali If the solution is the same as it is in the answers that are in the other question, better not, if it is different, better do it in the other question, since I see that your question is a duplicate. – eyllanesc Jul 06 '18 at 21:22
  • @eyllanesc, it's not. What the answers are suggesting is playing with the style sheets and making a QLineEdit look like a label, and vice versa. I want to create a widget call it EditableLabel which can provide all of the functionality out of the box. – santahopar Jul 06 '18 at 21:26
  • @eyllanesc, also my solution is something that switches the views back and forth. The answers are just showing how to make it read-only. – santahopar Jul 06 '18 at 21:27
  • @armanali see https://stackoverflow.com/a/23918591/6622587 – eyllanesc Jul 06 '18 at 21:30
  • @eyllanesc, yes I have seen it. It's playing with color palettes to make it look like something that it's not. I don't like that solution. – santahopar Jul 06 '18 at 21:32
  • 1
    @armanali What I indicate is that you must publish your answer in the other question since it is a solution for that question since your question is the same. – eyllanesc Jul 06 '18 at 21:34
  • @eyllanesc, I think it's better if I edit my question first, then I post the answer. – santahopar Jul 06 '18 at 21:51

3 Answers3

3

The following version also implements the same functionalities of your answer but instead of subclassing the QLineEdit and the QLabel only use eventFilter() and instead of managing the visibility manually let QStackedWidget do it.

#include <QApplication>
#include <QFormLayout>
#include <QKeyEvent>
#include <QLabel>
#include <QLineEdit>
#include <QStackedWidget>
#include <QVBoxLayout>

class MyEditableLabel: public QWidget{
    Q_OBJECT
    Q_PROPERTY(QString text READ text WRITE setText NOTIFY textChanged)
public:
    MyEditableLabel(QWidget *parent=nullptr):
        QWidget(parent),
        mLabel(new QLabel),
        mLineEdit(new QLineEdit)
    {
        setLayout(new QVBoxLayout);
        layout()->setMargin(0);
        layout()->setSpacing(0);
        layout()->addWidget(&stacked);

        stacked.addWidget(mLabel);
        stacked.addWidget(mLineEdit);
        mLabel->installEventFilter(this);
        mLineEdit->installEventFilter(this);
        setSizePolicy(mLineEdit->sizePolicy());
        connect(mLineEdit, &QLineEdit::textChanged, this, &MyEditableLabel::setText);
    }

    bool eventFilter(QObject *watched, QEvent *event){
        if (watched == mLineEdit) {
            if(event->type() == QEvent::KeyPress){
                QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event);
                if(keyEvent->key() == Qt::Key_Return ||
                        keyEvent->key() == Qt::Key_Escape ||
                        keyEvent->key() == Qt::Key_Enter)
                {
                    mLabel->setText(mLineEdit->text());
                    stacked.setCurrentIndex(0);
                }
            }
            else if (event->type() == QEvent::FocusOut) {
                mLabel->setText(mLineEdit->text());
                stacked.setCurrentIndex(0);
            }
        }
        else if (watched == mLabel) {
            if(event->type() == QEvent::MouseButtonDblClick){
                stacked.setCurrentIndex(1);
                mLineEdit->setText(mLabel->text());
                mLineEdit->setFocus();
            }
        }
        return QWidget::eventFilter(watched, event);
    }
    QString text() const{
        return mText;
    }
    void setText(const QString &text){
        if(text == mText)
            return;
        mText == text;
        emit textChanged(mText);
    }
signals:
    void textChanged(const QString & text);
private:
    QLabel *mLabel;
    QLineEdit *mLineEdit;
    QStackedWidget stacked;
    QString mText;
};

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    QWidget w;
    QFormLayout *lay = new QFormLayout(&w);

    MyEditableLabel el;
    lay->addRow("MyEditableLabel: ", &el);
    lay->addRow("QLineEdit: ", new QLineEdit);
    w.show();

    return a.exec();
}

#include "main.moc"
eyllanesc
  • 235,170
  • 19
  • 170
  • 241
  • Doesn’t the usage of event filter affect the performance? I’ve heard it’s faster when you sub-class, rather than install an event filter to a widget. – santahopar Jul 07 '18 at 01:09
  • No, where you saw that, the event filter serves to listen to events without subclassing. and Qt uses it intensely. – eyllanesc Jul 07 '18 at 01:10
  • Well you have to register an event filter with the application. So doesn’t that add overhead on top of the existing event filter? – santahopar Jul 07 '18 at 01:15
  • @armanali I just add an element to a list that has the widget, how much overhead will that be? – eyllanesc Jul 07 '18 at 01:18
  • I don’t know. I have read it somewhere. Maybe the information was not accurate. – santahopar Jul 07 '18 at 01:19
  • 1
    @armanali Well, as I say I have used many times to avoid inheriting a class just to hear an event because if subclassing for it would have to create more memory to exchange data, if necessary for example the model – eyllanesc Jul 07 '18 at 01:25
1

You could use a QInputdialog to change QLabel and override the mouseDoubleClickEvent to trigger the input dialog.

I, as some here have learned that there is no means to pull edited text from a QLabel. Not without changing QLabels internal code.

Here's an example using a QInputDialog, as means:

//intrlbl.h

#ifndef INTRLBL_H
#define INTRLBL_H

#include <QWidget>
#include <QLabel>
#include <QMouseEvent>



class intrLbl: public QLabel
{
    Q_OBJECT
public:
    intrLbl(QWidget *parent);

    void mouseDoubleClickEvent(QMouseEvent *event) override;
    QString text;

};

#endif // INTRLBL_H



//intrlbl.cpp file

#include "intrlbl.h"
#include <QDebug>
#include <QInputDialog>

intrLbl::intrLbl(QWidget *parent)
{
    this->setText("Text Changeable Via Double Click QInput Dialog");
    this->setFocusPolicy(Qt::ClickFocus);
    this->setWordWrap(false);

}

void intrLbl::mouseDoubleClickEvent(QMouseEvent *event)
{
       QString title
               = QInputDialog::getText(this,
                 tr("Enter your Idea Title:"),
                  tr("Title:"), QLineEdit::Normal,
                  tr("enter your title here"));

      if(!title.isEmpty())
      {
            qDebug() << "Title set to:" << title;
            this->setText(title);
       }
      else
      {
          title = "Title";
          this->setText(title);
      }
}    
  • Another Idea I've not successfully completed is to inherit QLineEdit. QLine edit has methods to hide the frame adjust background color and set read only mode. This would entail overriding the double click event and using a key press event on enter which adds more code and would not be as performant. One issue I've encountered is that when I implement keypress event I can no longer edit the text in QLineEdit. Any info on this would be greatly appreciated. – Joseph Farrish Feb 27 '20 at 16:02
0

One of the solutions is to have a QLineEdit and set it to read-only and style it in a way that it will look like a label. I personally do not like this solution, because it's more of a hacking approach. I have come up with something that in my opinion is pretty cool, which includes sub-classing QWidget, QLabel and QLineEdit:

Let's first introduce a model, which will be created in the sub-classed version of our QWidget and this model will be passed to its child widgets, the sub-classed versions of QLabel and QLineEdit:

Model header - mymodel.h:

#ifndef MYMODEL_H
#define MYMODEL_H

#include <QObject>

class MyModel : public QObject {
    Q_OBJECT
    Q_PROPERTY(Mode mode READ getMode WRITE setMode NOTIFY modeChanged)
    Q_PROPERTY(QString text READ getText WRITE setText NOTIFY textChanged)
public:
    enum class Mode {
        ReadOnly = 0,
        Edit = 1,
    };

    explicit MyModel(QObject* parent = nullptr);

    Mode getMode() const {
        return _mode;
    }

    const QString& getText() const {
        return _text;
    }

signals:
    void modeChanged(Mode mode);
    void textChanged(const QString& text);

public slots:
    void setMode(Mode mode);
    void setText(const QString& text);

private:
    Mode _mode;
    QString _text;
};

#endif // MYMODEL_H

Model implementation - mymodel.cpp

#include "mymodel.h"

MyModel::MyModel(QObject *parent)
    : QObject(parent)
    , _mode(MyModel::Mode::ReadOnly)
    , _text(QString()) {
}

void MyModel::setMode(MyModel::Mode mode) {
    if (_mode != mode) {
        _mode = mode;
        emit modeChanged(_mode);
    }
}

void MyModel::setText(const QString &text) {
    if (_text != text) {
        _text = text;
        emit textChanged(text);
    }
}

As we see the model has the text, which is common for both the QLabel and the QLineEdit, and it has a mode, which can be either read only or edit mode.

The label implementation is a sub-class of Label. Header - mylabel.h:

#ifndef MYLABEL_H
#define MYLABEL_H

#include <QLabel>
#include <QSharedPointer>

#include "mymodel.h"

class MyLabel : public QLabel {
    Q_OBJECT
public:
    explicit MyLabel(QWidget *parent = 0);
    void setModel(QSharedPointer<MyModel> model);

protected:
    void mouseDoubleClickEvent(QMouseEvent *) override;

private:
    QSharedPointer<MyModel> _model;
};

#endif // MYLABEL_H

Implementation - mylabel.cpp:

#include "mylabel.h"

#include <QMouseEvent>

MyLabel::MyLabel(QWidget *parent)
    : QLabel(parent) {
}

void MyLabel::setModel(QSharedPointer<MyModel> model) {
    _model = model;
}

void MyLabel::mouseDoubleClickEvent(QMouseEvent *) {
    _model->setText(text());
    _model->setMode(MyModel::Mode::Edit);
}

As we our class MyLabel has a setModel() method, which will take the model from its parent. We are overriding the mouseDoubleClickEvent(), though which we are setting the text of the model to whatever text there is in the label, and setting the mode to edit, because when double-clicking we want to edit the text.

Now let's take a look at the QLineEdit. Our version of QLineEdit, called MyLineEdit, is listening to keyboard events and when Enter and Esc keys are pressed it either saves the text to the model, or discards it. Then it changes the mode to read-only.

MyLineEdit.h:

#ifndef MYLINEEDIT_H
#define MYLINEEDIT_H

#include <QLineEdit>
#include <QSharedPointer>

#include "mymodel.h"

class MyLineEdit : public QLineEdit {
    Q_OBJECT
public:
    MyLineEdit(QWidget* parent = nullptr);
    void setModel(QSharedPointer<MyModel> model);

protected:
    void keyPressEvent(QKeyEvent* event) override;
    void focusOutEvent(QFocusEvent*);

private:
    QSharedPointer<MyModel> _model;
};

#endif // MYLINEEDIT_H

And here's the implementation - MyLineEdit.cpp:

#include "mylineedit.h"

#include <QKeyEvent>

MyLineEdit::MyLineEdit(QWidget *parent)
    : QLineEdit(parent) {
}

void MyLineEdit::setModel(QSharedPointer<MyModel> model) {
    _model = model;
}

void MyLineEdit::keyPressEvent(QKeyEvent *event) {
    if (event->key() == Qt::Key_Enter) {
        _model->setText(text());
        _model->setMode(MyModel::Mode::ReadOnly);
    } else if (event->key() == Qt::Key_Escape) {
        _model->setMode(MyModel::Mode::ReadOnly);
    } else {
        QLineEdit::keyPressEvent(event);
    }
}

void MyLineEdit::focusOutEvent(QFocusEvent *) {
    _model->setText(text());
    _model->setMode(MyModel::Mode::ReadOnly);
}

So now we have the model, we have our version of QLabel and our version of QLineEdit. What we want now is a parent widget that will contain both of them, listen to signals from the model and change its appearance based on the signals. That class is derived from QWidget and is called MyEditableLabel:

MyEditableLabel.h:

#ifndef MYEDITABLELABEL_H
#define MYEDITABLELABEL_H

#include <QSharedPointer>
#include <QWidget>

#include "mylabel.h"
#include "mylineedit.h"

class MyEditableLabel : public QWidget {
    Q_OBJECT
public:
    explicit MyEditableLabel(QWidget *parent = nullptr);
    QString getText() const {return _text;}
private:
    MyLabel *_label;
    MyLineEdit *_lineEdit;
    QSharedPointer<MyModel> _model;

private slots:
    void onModeChanged(MyModel::Mode mode);
    void onTextChanged(const QString &text);

private:
    QString _text;
};

#endif // MYEDITABLELABEL_H

MyEditableLabel.cpp: #include "myeditablelabel.h"

#include <QHBoxLayout>

MyEditableLabel::MyEditableLabel(QWidget *parent)
    : QWidget(parent) {
    _model = QSharedPointer<MyModel>(new MyModel());
    _model->setText("Click me!");
    _label = new MyLabel(this);
    _label->setModel(_model);
    _lineEdit = new MyLineEdit(this);
    _lineEdit->setModel(_model);
    _lineEdit->setReadOnly(false);

    QHBoxLayout *mainLayout = new QHBoxLayout();
    mainLayout->setMargin(0);
    mainLayout->setSpacing(0);
    mainLayout->addWidget(_label);
    mainLayout->addWidget(_lineEdit);
    setLayout(mainLayout);

    connect(_model.data(), &MyModel::modeChanged, this, &MyEditableLabel::onModeChanged);
    onModeChanged(_model->getMode());
    connect(_model.data(), &MyModel::textChanged, this, &MyEditableLabel::onTextChanged);
    onTextChanged(_model->getText());
}

void MyEditableLabel::onModeChanged(MyModel::Mode mode) {
    _lineEdit->setVisible(mode == MyModel::Mode::Edit);
    _lineEdit->selectAll();
    _label->setVisible(mode == MyModel::Mode::ReadOnly);
}

void MyEditableLabel::onTextChanged(const QString &text) {
    _lineEdit->setText(text);
    _label->setText(text);
    _text = text;
}

Usage: Using this is pretty straightforward. If you're using the Qt Creator designer, then you want to draw a QWidget and the right click on it and promote it to MyEditableLabel and you're done. If you're not using the Qt Creator designer then you just have to create and instance of MyEditableLabel and you're in business.

Improvements: It probably is a better idea to not create the model in the constructor of MyEditableLabel, but outside of it and have a setModel method in MyEditableLabel.

eyllanesc
  • 235,170
  • 19
  • 170
  • 241
santahopar
  • 2,933
  • 2
  • 29
  • 50