Wiki

Редактор списка строк

Mark Summerfield (перевод Racheengel)

Часто необходимо представить пользователю список строк с возможностью добавления новых, редактирования и удаления существующих. В этой статье описан общий класс StringListEdit и подкласс FileListEdit для упрощения этих задач.

[Загрузить исходный код]

Давайте посмотрим, как такой класс может быть использован:

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();

Создадим список шрифтов, доступных приложению, и новый объект StringListEdit, в который передается данный списое при инициализации. Установим заголовок и текст, который будет использован во всплывающих диалогах редактирования, а затем откроем диалог. Если пользователь нажмет OK, заменим текущий список измененным. Диалог, который отобразится по вызову exec(), выглядит примерно так:

Stringlistedit

Если пользователь нажметEdit, появится диалог редактирования:

Stringlisteditq

Давайте объявим наш класс.

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;
};

Мы реализовали несколько простых функций доступа и сделали addString(), editString() и removeString() виртуальными для облегчения наследования.

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);
}

Кнопки Remove, Up и Down создаются так же, как и Edit; единственная разница в том, что мы используем свои защищенные указатели, каждый из которых соединен с соответствующим слотом.

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();
}

Для удобства, попытаемся сделать так, чтобы ширина списка была достаточна для отображения строк без использования горизонтальной прокрутки, но не более 80% ширины экрана.

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;
}

Функции list() и setTexts() простые.

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);
}

Кнопки Add, OK и Cancel должны быть доступны всегда, другие же должны быть доступны, только если их нажатие будет иметь смысл. За это отвечает функция updateButtons().

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();
    }
}

Если пользователь нажмет Add, используем диалог ввода для ввода строки. Если он нажмет OK (и введенная строка не повторяется в списке, либо duplicatesOk имеет значение true), мы добавим строку в список, сделаем ее текущей и обновим кнокпи.

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();
    }
}

Код editString() несколько отличен от addString(). Если нет строки для редактирования, мы просто вызываемl addString(); иначе мы получаем строку (отображая по умолчанию оригинальный текст), и, если новая строка не пустая (либо повторяется при duplicatesOk равному false), мы изменяем оригинальный элемент в списке и обновляем кнопки.

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();
}

Если есть, что удалять, (и пользователь ответил "да" на вопрос об удалении при ask равному true), мы удаляем строку из списка.

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();
    }
}

Для изменения порядка строк в списке используются функции moveUp() и moveDown(). Мы меняем местами текущую строку с предыдущей, если текущая строка не первая в списке. Код для moveDown() очень похож на код moveUp().

Редактор списка файлов

Для списков имен файлов нам нужно несколько иное поведение. Мы можем захотеть указать фильтр имен, и функции addString() и editString() должны использовать файловые диалоги вместо строковых. Вот объявления класса FileListEdit, который расширяет StringListEdit для обработки имен файлов:

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;
};

Конструктор пустой. Также мы создали функции чтения и установки фильтра имен.

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();
    }
}

Эта функция очень похожа на addString() базового класса. Мы запоминаем имя файла в переменной path, чтобы файловый диалог всегда показывал последний использованный каталог.

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();
    }
}

Мы не вызываем addString(), если пользователь не выбрал имя файла, т.к. это не имеет смысла.


Copyright © 2004 Trolltech Trademarks