Trolltech | Documentation | Qt Quarterly | Another Look at Events »

A String List Editor
by Mark Summerfield
We often need to present users with a list of strings that they can add to, edit, or remove from. This article presents a generic StringListEdit class and a FileListEdit subclass to simplify these common tasks.

[Download Source Code]

Let's start by looking at how such a class might be used:

    QStringList fontList = QFontDatabase().families();
    StringListEdit listEdit = new StringListEdit(fontList);
    listEdit.setCaption(tr("Font List"));
    listEdit.setTexts(tr("Add Font Name"), tr("&Name:"),
                      tr("Edit Font Name"));
    if (listEdit.exec())
        fontList = listEdit.list();
    

Here we create a list of the fonts available to the application, and create a new StringListEdit, passing it an initial list. We set the caption and the texts to be used in the popup line edits and then execute the dialog. If the user clicks OK, we replace the original list with the edited one. The dialog that appears as a result of the exec() call looks like this:

Stringlistedit

If the user clicks Edit, the following dialog will pop up:

Stringlisteditq

Let's start with the definition of the class.

    class StringListEdit : public QDialog
    {
        Q_OBJECT
    public:
        StringListEdit(const QStringList &list,
                       QWidget *parent = 0);
    
        void setTexts(const QString &addCaption,
                      const QString &addLabel,
                      const QString &editCaption,
                      const QString &editLabel = "");
        bool askBeforeRemoving() const { return ask; }
        void setAskBeforeRemoving(bool a) { ask = a; }
        bool allowDuplicates() const { return duplicatesOk; }
        void setAllowDuplicates(bool allow)
            { duplicatesOk = allow; }
        QStringList list() const;
        void setList(const QStringList &list);
    
    protected slots:
        virtual void addString();
        virtual void editString();
        virtual void removeString();
        void moveUp();
        void moveDown();
        void updateButtons();
    
    protected:
        QString addCaption;
        QString addLabel;
        QString editCaption;
        QString editLabel;
        bool ask;
        bool duplicatesOk;
        QListBox *listBox;
        QPushButton *editButton;
        QPushButton *removeButton;
        QPushButton *upButton;
        QPushButton *downButton;
    };
    

We've implemented some simple accessor functions and made addString(), editString(), and removeString() virtual to make subclassing easier.

    StringListEdit::StringListEdit(const QStringList &list,
                                   QWidget *parent)
        : QDialog(parent),
          ask(false), duplicatesOk(false)
    {
        addCaption = tr("Add String");
        editCaption = tr("Edit String");
        addLabel = editLabel = tr("String:");
    
        QHBoxLayout *hbox = new QHBoxLayout(this, 5, 5);
        QVBoxLayout *vbox = new QVBoxLayout;
        listBox = new QListBox(this);
        connect(listBox,
                SIGNAL(currentChanged(QListBoxItem*)),
                this, SLOT(updateButtons()));
        hbox->addWidget(listBox, 1);
    
        QPushButton *button = new QPushButton(tr("&Add..."),
                                              this);
        connect(button, SIGNAL(clicked()),
                this, SLOT(addString()));
        vbox->addWidget(button);
        editButton = new QPushButton(tr("&Edit..."), this);
        connect(editButton, SIGNAL(clicked()),
                this, SLOT(editString()));
        vbox->addWidget(editButton);
        ...
        vbox->addStretch(1);
        button = new QPushButton(tr("OK"), this);
        connect(button, SIGNAL(clicked()),
                this, SLOT(accept()));
        vbox->addWidget(button);
        button = new QPushButton(tr("Cancel"), this);
        connect(button, SIGNAL(clicked()),
                this, SLOT(reject()));
        vbox->addWidget(button);
        hbox->addLayout(vbox);
    
        setList(list);
    }
    

The Remove, Up, and Down buttons are created in the same way as the Edit button; the only difference is that we use their own protected pointers, and each is connected to a different slot.

    void StringListEdit::setList(const QStringList &list)
    {
        listBox->clear();
        listBox->insertStringList(list);
        QFontMetrics fm(listBox->font());
        int width = 0;
        for (int i = 0; i < (int)list.count(); ++i) {
            int w = fm.width(list[i]);
            if (w > width)
                width = w;
        }
        if (listBox->verticalScrollBar())
            width += listBox->verticalScrollBar()->width();
        listBox->setMinimumWidth(
            QMIN(width, qApp->desktop()->screenGeometry()
                            .width() * 0.8));
        updateButtons();
    }
    

For the user's convenience, we try to ensure that the list box is as wide as necessary to show the strings without using a horizontal scroll bar, but no wider than 80% of the screen width.

    QStringList StringListEdit::list() const
    {
        QStringList lst;
        for (int i = 0; i < (int)listBox->count(); ++i)
            lst.append(listBox->text(i));
        return lst;
    }
    
    void StringListEdit::setTexts(
        const QString &addCaption, const QString &addLabel,
        const QString &editCaption, const QString &editLabel)
    {
        this->addCaption = addCaption;
        this->addLabel = addLabel;
        this->editCaption = editCaption;
        this->editLabel = editLabel.isEmpty() ? addLabel
                                              : editLabel;
    }
    

The list() and setTexts() functions are easy.

    void StringListEdit::updateButtons()
    {
        bool hasItems = (listBox->count() > 0);
        editButton->setEnabled(hasItems);
        removeButton->setEnabled(hasItems);
        int i = listBox->currentItem();
        upButton->setEnabled(hasItems && i > 0);
        downButton->setEnabled(hasItems &&
                               i < (int)listBox->count() - 1);
    }
    

Although the Add, OK, and Cancel buttons should always be available, the other buttons should only be enabled when clicking them makes sense. This is what updateButtons() achieves.

    void StringListEdit::addString()
    {
        bool ok;
        QString str = QInputDialog::getText(addCaption,
                           addLabel, QLineEdit::Normal,
                           listBox->currentText(), &ok, this);
        if (ok && !str.isEmpty()) {
            if (!duplicatesOk && listBox->findItem(str,
                                  CaseSensitive | ExactMatch))
                return;
            listBox->insertItem(str);
            listBox->setCurrentItem(listBox->count() - 1);
            listBox->ensureCurrentVisible();
            updateButtons();
        }
    }
    

If the user clicks Add, we use an input dialog to get their string. If they click OK (and providing the string isn't a duplicate or duplicatesOk is true), we add the string to the list box, make it current, and update the buttons.

    void StringListEdit::editString()
    {
        bool ok;
        QString original = listBox->currentText();
        if (!original.isEmpty()) {
            QString str = QInputDialog::getText(
                                  editCaption, editLabel,
                                  QLineEdit::Normal, original,
                                  &ok, this);
            if (ok && !str.isEmpty()) {
                if (!duplicatesOk && listBox->findItem(
                             str, CaseSensitive | ExactMatch))
                    return;
                listBox->changeItem(str,
                                    listBox->currentItem());
                updateButtons();
            }
        } else {
            addString();
        }
    }
    

The code for editString() is subtly different from addString(). If there's no string to edit, we simply call addString(); otherwise we get the string (giving the original string as the default text), and providing the new string isn't empty (or a duplicate if duplicatesOk is false), we change the original list box item, and update the buttons.

    void StringListEdit::removeString()
    {
        QString original = listBox->currentText();
        if (original.isEmpty() ||
            (ask && QMessageBox::question(this, tr("Remove"),
                      tr("Remove '%1'?").arg(original),
                      QMessageBox::Yes | QMessageBox::Default,
                      QMessageBox::No | QMessageBox::Escape)
                   == QMessageBox::No))
            return;
        listBox->removeItem(listBox->currentItem());
        updateButtons();
    }
    

If there's a string to remove (and if the user says yes to removing it if ask is true), we remove the string from the list.

    void StringListEdit::moveUp()
    {
        int i = listBox->currentItem();
        if (i > 0) {
            QString temp = listBox->text(i - 1);
            listBox->changeItem(listBox->text(i), i - 1);
            listBox->changeItem(temp, i);
            listBox->setCurrentItem(i - 1);
            updateButtons();
        }
    }
    

For some lists the order matters, so we've provided the moveUp() and moveDown() functions. We swap the current string with the previous string, providing the current string isn't the first string. We don't show moveDown() since it is very similar to moveUp().

A File List Editor

For lists of file names we need slightly different behavior. We might want to specify a file name filter, and the addString() and editString() functions should use file dialogs rather than input dialogs. Here's the definition of a FileListEdit class that specializes StringListEdit to handle file names:

    class FileListEdit : public StringListEdit
    {
        Q_OBJECT
    public:
        FileListEdit(const QStringList &list,
                     QWidget *parent = 0)
            : StringListEdit(list, parent),
              filter(tr("Any (*;*.*)")), path(".") {}
    
        QString fileFilter() const { return filter; }
        void setFileFilter(const QString &newFilter)
            { filter = newFilter; }
    
    private slots:
        void addString();
        void editString();
    
    private:
        QString filter;
        QString path;
    };
    

The constructor is empty. We've also provided a getter and a setter for the file name filter.

    void FileListEdit::addString()
    {
        QString fileName = QFileDialog::getOpenFileName(path,
                                filter, this, "", addCaption);
        if (!fileName.isEmpty()) {
            if (!duplicatesOk && listBox->findItem(
                        fileName, CaseSensitive | ExactMatch))
                return;
            listBox->insertItem(fileName);
            listBox->setCurrentItem(listBox->count() - 1);
            listBox->ensureCurrentVisible();
            path = fileName;
            updateButtons();
        }
    }
    

This is very similar to the base class's addString(). We remember the file name in path so that on subsequent invocations the file dialog starts in the last directory that was used.

    void FileListEdit::editString()
    {
        QString fileName = QFileDialog::getOpenFileName(
                                listBox->currentText(),
                                filter, this, "",
                                editCaption);
        if (!fileName.isEmpty()) {
            if (!duplicatesOk && listBox->findItem(
                        fileName, CaseSensitive | ExactMatch))
                return;
            listBox->changeItem(fileName,
                                listBox->currentItem());
            path = fileName;
            updateButtons();
        }
    }
    

We don't call addString() if the user doesn't choose a file name since it doesn't make sense for file names.


This document is licensed under the Creative Commons Attribution-Share Alike 2.5 license.

Copyright © 2004 Trolltech Trademarks Another Look at Events »