Automatic Dialogs

Материал из Wiki.crossplatform.ru

Перейти к: навигация, поиск
Image:qt-logo_new.png Image:qq-title-article.png
Qt Quarterly | Выпуск 13 | Документация


by Mark Summerfield

This article shows how to maintain sets of"attributes" (QVariant values), and how to allow users to view andedit them using dialogs that are created dynamically based on theattributes and their types.

[Download Source Code]

The Attributes class described in this article holds a set ofQVariants, and can create a dialog to present the QVariants tothe user in an appropriate way. For example, if a value is aninteger, the dialog will represent that integer using a QSpinBox.For colors, fonts and file names, the current color, font orfile name is shown, and an ellipsis button (...) is provided thatinvokes a suitable dialog through which the user can choose a value.

The dialog is not specified or laid out in code; instead we simplycall an Attributes object's setAttribute() function for eachattribute we want to hold, giving each one a name and an initialvalue. When we want the user to be able to edit the attributes, wejust call dialog(), and if it returns true we know that theuser clicked OK and that the attributes have been updated.

If you're an object-oriented purist, or performance obsessed, youmay want to bail out now. We use explicit type checking and createdynamic arbitrary data structures using QMap and QVariant;this gives us great flexibility, but at the cost of some efficiencyand a certain amount of type safety.

Here's how to create an Attributes object on the stack with somedefault values, and present a dialog based on the object to the user:

Attributes atts;
atts.setAttribute("Text", "Qt Conference");
atts.setAttribute("Type",
                  QStringList() << "One-Off" << "TODO");
atts.setAttribute("Start", QDate(2004, 5, 10));
 
if (atts.dialog(this, "Event")) {
    QString text = atts.value("Text").toString();
    QString type = atts.value("Type").toString();
    QDate start = atts.value("Start").toDate();
    ...
}

Each attribute has a name, a value, and some optional "extra" data.The name is used as the label in the dialog (withaccelerators automatically added where possible insidethe dialog() function). The value's type determines what sort ofwidget is used for presenting the value; we'll see what the extra datais in a moment. If the user clicks OK, the attribute data isupdated from the dialog's values; if the user clicks Cancel thedata is not changed.

Now lets look at a more sophisticated example. This time we'll supposethat we have a data structure populated with pointers to a number ofAttributes objects. We'll also exercise more control over thedialog that's generated.

Attributes *atts = new Attributes;
 
atts->setAttribute("Alias", "Administrator");
 
QStringList list;
list << "Active" << "Completed" << "Suspended";
QMap<QString, QVariant> extra;
extra["selected"] = 1;
atts->setAttribute("Status", list, extra);
 
extra.clear();
extra["minimum"] = 0;
extra["maximum"] = 100;
extra["suffix"] = "%";
atts->setAttribute("Rating", 50, extra);
 
extra.clear();
extra["hidden"] = true;
atts->setAttribute("Internal ID", "WX257C", extra);
 
atts->setAttribute("Background", cyan);
atts->setAttribute("Font", font());
 
extra.clear();
extra["fileFilter"] = QString("Executables (*;*.exe)");
const QString empty;
atts->setAttribute("Email Client", empty, extra);
 
atts->setAttribute("Web Browser", empty, extra);

The screenshots show the dialogs after some user interaction.

center center

The extra parameter is a map of names to values.We've reused the same extra variable for the sake of convenience,which is why we clear() it before giving it fresh values. For aQStringList attribute, like "Status", there is one extradefined: "selected". The "selected" extra's integer value is theindex of the selected item in the QComboBox that presents theQStringList's strings to the user. For integers like "Rating",we can set "minimum", "maximum", "prefix", and "suffix"extras if we wish.

Sometimes it is convenient to have attributes that we use within ourcode, but which we don't want to make available to users. This isachieved by setting "hidden" in the extra parameter to true.If we don't want a default value, we must provide an empty valueof the right type so that Attributes knows what type of value itcan accept. If we set "fileFilter" in the extra parameter, thevalue is taken to be a file name of type QString, and an ellipsisbutton that invokes a file dialog is provided. Similarly, a colorvalue has an ellipsis button that invokes a color dialog, and a fontvalue has one that invokes a font dialog.

[править] Implementing Attributes

The implementation of Attributes is easy except for thedialog() function. Here, we'll confine ourselves to reviewing someof the key features of the code.

Each attribute is stored in a PrivateAttribute object; this objectis designed to be accessed from Attributes objects, and shouldnever be directly used in your own code.

typedef QMap<QString, QVariant> StringVariantMap;
 
class PrivateAttribute
{
public:
    PrivateAttribute()
        : name(""), index(-1) {}
    PrivateAttribute(const QString &amp;name, int index,
            QVariant value,
            StringVariantMap extra = StringVariantMap())
        : name(name), index(index), value(value),
          extra(extra) {}
 
    QString name;
    int index;
    QVariant value;
    StringVariantMap extra;
    QWidget *widget;
};

The index member stores the position of the attribute. Attributesare displayed in the dialog() in index order, and this orderis determined by the order in which attributes are created by calls toAttributes::setAttribute(). The widget pointer is used by thedialog() function.

The Attributes class definition follows:

class Attributes : public QObject
{
    Q_OBJECT
 
public:
    Attributes();
    Attributes(const Attributes &amp;other);
 
    Attributes &amp;operator=(const Attributes &amp;other);
 
    int count() const
        { return attributes.count(); }
    QStringList names(bool indexOrder = false) const;
    bool dialog(QWidget *parent, const QString &amp;caption,
                bool autoMnemonics = true);
 
    QVariant value(const QString &amp;name) const;
    int index(const QString &amp;name) const;
    StringVariantMap extra(const QString &amp;name) const;
 
public slots:
    void setAttribute(const QString &amp;name, QVariant value,
            StringVariantMap extra = StringVariantMap());
    void removeAttribute(const QString &amp;name)
        { attributes.remove(name); }
    void clear()
        { attributes.clear(); }
 
private slots:
    void mapped(const QString &amp;name);
 
private:
    QString insertMnemonic(const QString &amp;text,
                           QString *allowed);
 
    int nextIndex;
    QString pixmapPath;
    QString filePath;
    QMap<QString, QFont> fonts;
    QDialog *dlg;
    QSignalMapper *mapper;
    QMap<QString, PrivateAttribute> attributes;
};

We will omit the code for the copy constructor and the assignmentfunction since they do little more than memberwise copying. Thenames() function could be implemented with the single statement

return attributes.keys();

but our implementation (not shown)provides the ability to order the names.

We'll also skip theindex() and extra() functions, since the coverage ofvalue() is sufficient to understand them.

Attributes::Attributes()
    : nextIndex(0), pixmapPath("."), filePath("."),
      dlg(0), mapper(0)
{
}

We use the pixmapPath and filePath strings, and the fontsmap to record temporary transient data. This is useful ifdialog() is invoked repeatedly for the same Attributes object;e.g., maintaining the last path used for a file dialog. nextIndexis an internal counter that ensures that each time an attribute isadded (using setAttribute()), it is ordered after those that wereadded previously.

QVariant Attributes::value(const QString &amp;name) const
{
    if (!attributes.contains(name))
        return QVariant();
 
    const PrivateAttribute &amp;attr = attributes[name];
    if (attr.value.type() == QVariant::StringList)
        return attr.value.toStringList()[
                    attr.extra["selected"].toInt()];
    return attr.value;
}

We return an invalid QVariant if the attribute doesn't exist. Ifthe attribute's value is a string list, we return the selected string.

void Attributes::setAttribute(const QString &amp;name,
                   QVariant value, StringVariantMap extra)
{
    if (value.type() == QVariant::CString)
        value.cast(QVariant::String);
 
    if (!attributes.contains(name)) {
        if (value.type() == QVariant::StringList) {
            if (!extra.contains("selected"))
                extra["selected"] = 0;
        } else if (value.type() == QVariant::UInt) {
            if (!extra.contains("minimum"))
                extra["minimum"] = uint(0);
        }
 
        attributes[name] = PrivateAttribute(name,
                               nextIndex++, value, extra);
    } else {
        PrivateAttribute &amp;attr = attributes[name];
        attr.value = value;
        if (extra.count()) {
            StringVariantMap::const_iterator i =
                extra.constBegin();
            for (; i != extra.constEnd(); ++i)
                attr.extra[i.key()] = i.data();
        }
    }
}

The setAttribute() function has two modes of operation. If theattribute name doesn't exist, we create a new attribute with the givenvalue and extra data, providing defaults for the extradata where necessary; otherwise we set the attribute's value tovalue, and update its extra data with the new extradata.

The dialog() function is quite long; so we'll just quote andexplain some extracts from the code. There are blocks of similar codefor each type we handle, so we only need to show snippets from asample type to convey the key ideas.

bool Attributes::dialog(QWidget *parent,
            const QString &amp;caption, bool autoMnemonics)
{
    dlg = new QDialog(parent);
    dlg->setCaption(caption);
    mapper = new QSignalMapper(dlg);
 
    QVBoxLayout *vbox = new QVBoxLayout(dlg, 5, 5);
    QHBoxLayout *hbox = 0;

The dialog uses a signal mapper to capture ellipsis button clicksand respond appropriately to them. The whole dialog is laid outvertically, with each attribute occupying a successive horizontallayout within the vertical layout.

After the initial declarations we iterate over each attribute. Thisserves two purposes: firstly we need to know the widest label so thatwe can make all the labels the same width, and secondly we want tocreate an integer-to-string map that maps eachattribute's index position to its name---this is so that we can layout each attribute in index order.

    QMap<int, QString> order;
    ...
    QMap<int, QString>::const_iterator j =
        order.constBegin();
    for (; j != order.constEnd(); ++j) {
        PrivateAttribute &amp;attr = attributes[j.data()];
        if (attr.extra.contains("hidden")
                &amp;&amp; attr.extra["hidden"].toBool())
            continue;
        value = attr.value;

The function's main loop iterates over the names of the attributes;any that are hidden, or of a type that we cannot handle, are ignored.

For the rest, we copy their name and try to insert an ampersand ('&')to create a mnemonic if this is possible. We use a simple andimperfect algorithm (not shown) that works as follows. We hold astring containing the letters A to Z. For each name, we see if its firstletter is in the string; if it is, we insert the ampersand before theletter in the name and delete that letter from the string. Otherwise,we do the same for any letter in the name that is preceded by a space;if that doesn't work, we do the same for any letter in the name.If no strategy works, we don't add a mnemonic.

        QLabel *label = new QLabel(text, dlg);
        label->setFixedWidth(labelWidth);
        hbox = new QHBoxLayout(vbox);
        hbox->addWidget(label);

We create a QLabel for each attribute, using its name (possiblywith an ampersand) for its text. We make it fixed width (based on thewidest label) and add it to a new horizontal layout.

        switch (type) {
        case QVariant::String:
            lineEdit = new QLineEdit(value.toString(),
                                     dlg);
            if (attr.extra.contains("maximum"))
                lineEdit->setMaxLength(
                    attr.extra["maximum"].toInt());
            attr.widget = lineEdit;
            label->setBuddy(lineEdit);
            hbox->addWidget(lineEdit, 2);
 
            if (attr.extra.contains("fileFilter")) {
                button = new QPushButton(tr("..."), dlg);
                button->setFixedWidth(ellipsisWidth);
                connect(button, SIGNAL(clicked()),
                        mapper, SLOT(map()));
                mapper->setMapping(button, attr.name);
                hbox->addWidget(button);
            }
            break;
        ...

What we do next depends on the attribute's type. In the case of astring, we create a line edit with the string's value and set themaximum length if that's been given in the extra data. We remember thewidget used (attr.widget = lineEdit), and add it tothe horizontal layout. If the string is holding a file name (indicatedby an extra "fileFilter" data item), we create an ellipsis button andconnect it to the signal mapper. We also add the button to thehorizontal layout.

Once all the widgets for the attributes have been added, we createanother horizontal layout and add a stretch followed by an OKand a Cancel button, suitably connected.

    bool result = false;
    if (dlg->exec()) {
        QMap<QString, PrivateAttribute>::iterator i =
            attributes.begin();
        for (; i != attributes.end(); ++i) {
            if (i.data().extra.contains("hidden")
                    &amp;&amp; i.data().extra["hidden"].toBool())
                continue;

Next we show the dialog to the user. If they click OK we iterateover the attributes, again skipping any that are hidden.

            switch (type) {
            case QVariant::String:
                lineEdit = (QLineEdit *)i.data().widget;
                i.data().value = lineEdit->text();
                break;
            ...
            }

We retrieve the data from the remembered widgets (which vary dependingon the attributes' types), updating the Attributes object with theupdated values. Finally, we delete the dialog.

If any of the attributes has an ellipsis button which the userclicked, the mapped() function is called. Here's an extract fromit, to show what happens in the case of a file name string.

void Attributes::mapped(const QString &amp;name)
{
    PrivateAttribute &amp;attr = attributes[name];
    ...
    QString fileName;
 
    switch (attr.value.type()) {
    case QVariant::String:
        if (attr.extra.contains("fileFilter")) {
            fileName = QFileDialog::getSaveFileName(
                filePath,
                attr.extra["fileFilter"].toString(),
                dlg, "", tr("Choose a file"));
            if (!fileName.isEmpty()) {
                ((QLineEdit *)attr.widget)->
                        setText(fileName);
                filePath = QDir(fileName).absPath();
            }
        }
        break;
    ...
}

If a file name ellipsis button is pressed, we present the user with afile dialog. If the user chooses a file, we set the file nameline edit in the dialog to show it. We also record the path so thatthe next time the dialog is invoked by this Attributes object, thepath will be the one the user last used. Notice that we do notupdate the attributes object here. That is done in dialog() if theuser closed the dialog by clicking OK.

[править] Conclusion

Attributes could be implemented in other ways, for example, usinga QGridLayout. If there are lots of attributes, using a QTable, or a QScrollBox might be necessary; or each attributecould have a "group" name (defaulting to, say, "General"), and wecould create a tabbed dialog, with groups corresponding to tabs.