QT и Базы данных

Перейти к: навигация, поиск
Icon-expand.png Эта статья является незавершенной!
Эта статья нуждается в доработке. Если Вы можете помочь, сделайте это в соответствии с руководством по оформлению.

Работа с базами данных в Qt4

В библиотеке Qt4 имеются драйверы для работы со следующими СУБД:

  • QDB2 -- IBM DB2 версии не ниже 7.1;
  • QIBASE -- Borland InterBase;
  • QMYSQL -- MySQL;
  • QOCI -- Oracle;
  • QODBC -- ODBC (в том числе Microsoft SQL Server);
  • QPSQL -- PostgreSQL;
  • QSQLITE -- SQLite версии не ниже 3;
  • QSQLITE2 -- SQLite версии 2;
  • QTDS -- Sybase Adaptive Server.

В Qt4 Open Source Edition отсутствует поддержка коммерческих СУБД Oracle, Sybase и DB2, т.к. драйверы для них распространяются под лицензией, не совместимой с GPL.

Компиляция SQL-драйверов

При компиляции Qt4 по умолчанию устанавливается только драйвер QSQLITE. Чтобы подключить поддержку остальных СУБД, надо при запуске configure указать параметры -qt-sql-mysql, -qt-sql-psql, -qt-sql-odbc, -qt-sql-ibase для компиляции соответствующих статических библиотек или -plugin-sql-mysql, -plugin-sql-psql, -plugin-sql-odbc, -plugin-sql-ibase для компиляции динамических библиотек. Компилятору и компоновщику потребуются заголовочные файлы и библиотеки, поставляемые вместе с соответствующими СУБД. Поэтому при запуске configure обычно требуется указать параметр -Iпуть_к_h-файлам и -Lпуть_к_lib. В результате компиляции в каталоге lib появится нужная динамическая библиотека. Чтобы узнать, драйверы каких СУБД уже установлены, можно открыть демонстрационную программу demos/sqlbrowser.

Если библиотека Qt уже скомпилирована, то можно просто зайти в каталог qt/src/plugins/sqldrivers/НужныйДрайвер и выполнить компиляцию находящегося там проекта *.pro.

Например, для сборки драйвера ODBC в Windows:

cd %QTDIR%\src\plugins\sqldrivers\odbc
qmake -o Makefile odbc.pro
mingw32-make

При использовании Visual C++ последняя команда, разумеется, nmake. В Linux:

cd $QTDIR/src/plugins/sqldrivers/odbc
qmake "INCLUDEPATH+=/usr/local/unixODBC/include"
      "LIBS+=-L/usr/local/unixODBC/lib -lodbc"
make

Для сборки драйвера PostgreSQL в Windows:

cd %QTDIR%\src\plugins\sqldrivers\psql
qmake -o Makefile "INCLUDEPATH+=C:\psql\include"
        "LIBS+=C:\psql\lib\libpq.a" psql.pro
mingw32-make

(все пути не должны содержать пробелов). Для компилятора Microsoft Visual C++ указывается библиотека C:\psql\lib\ms\libpq.lib. В Linux:

cd $QTDIR/src/plugins/sqldrivers/psql
qmake -o Makefile "INCLUDEPATH+=/usr/include/pgsql"
        "LIBS+=-L/usr/lib -lpq" psql.pro
make

Заметим, что драйвер QPSQL может работать только с кодировкой utf-8. Для сборки драйвера MySQL в Windows:

cd %QTDIR%\src\plugins\sqldrivers\mysql
qmake -o Makefile "INCLUDEPATH+=C:\MySQL\include"
        "LIBS+=C:\MySQL\lib\opt\libmysql.a" mysql.pro
mingw32-make

(все пути не должны содержать пробелов). Для компиляции с помощью Microsoft Visual C++ вместо libmysql.a надо указать libmysql.lib. В Linux:

cd $QTDIR/src/plugins/sqldrivers/mysql
qmake -o Makefile "INCLUDEPATH+=/usr/local/include"
        "LIBS+=-L/usr/local/lib -lmysqlclient_r" mysql.pro
make

Результат компиляции (динамические библиотеки) помещается в каталог plugins/sqldrivers.

Обычно для Windows поставляются библиотеки *.lib, рассчитанные на использование компилятора Microsoft. Поскольку свободная версия Qt4 для Windows поддерживает только MinGW, то перед сборкой SQL-драйвера придётся сначала сгенерировать def-файл, например:

cd c:\mysql\lib\opt
reimp -d libmysql.lib

(утилита reimp поставляется вместе с MSYS), а затем сформировать библиотеку *.a:

dlltool -d libmysql.def -l libmysql.a libmysql.dll -k

Перед выполнением приложений Qt, если используются SQL-драйверы, скомпилированные в виде динамических библиотек, требуется позаботиться о том, чтобы сами эти библиотеки, а также, в свою очередь, используемые ими библиотеки, поставляемые с соответствующими СУБД, были доступны по стандартным путям поиска.

Если пути к библиотекам были добавлены в системную переменную PATH уже после того, как любое приложение Qt попыталось загрузить некоторый SQL-драйвер, то в Windows придётся очистить ветку реестра HKEY_CURRENT_USER\Software\Trolltech\OrganizationDefaults.

В окончательных релизах, поставляемых пользователю, Qt-драйверы qsql* лучше всего размещать в подкаталоге ./sqldrivers, а используемые ими библиотеки -- либо в каталоге самой программы, либо в стандартном каталоге Windows/system32 (для системы Windows) или usr/local/lib (для Linux).


Подключение в базе данных и выполнение SQL-запросов

Для подключения к базе данных надо указать название SQL-драйвера, например:

QSqlDatabase db = QSqlDatabase::addDatabase("QMYSQL", "MyDB1");

Второй необязательный параметр позволяет задать имя соединения. Затем указывается имя сервера, название базы данных, имя пользователя и пароль:

db.setHostName("localhost"); // или, например, "my1.server.ru"
db.setDatabaseName("mydb1");
db.setUserName("root");
db.setPassword("mypassword");

Если сервер использует нестандартный порт, то придётся задать и его:

db.setPort(НомерПорта);

В случае использования QODBC имя сервера не требуется, а вместо названия базы данных указывается ODBC-псевдоним (алиас). SQLite не поддерживает авторизацию пользователей, поэтому ему требуется указать только имя файла данных. Предопределённое имя ":memory:" позволяет размещать временную базу данных в оперативной памяти.

После того, как все параметры подключения заданы, можно открыть соединение:

bool connected = db.open();

Если подключение установить не удалось, то не плохо бы узнать описание ошибки и сообщить его пользователю:

if (!connected) {
    QMessageBox::critical( // Диалог с сообщением об ошибке.
        parent,                      // Родительский виджет.
        QObject::tr("Database Error"),   // Заголовок.
        db.lastError().text());          // Текст сообщения.
    return false; // Вернуть признак неудачного подключения.
}

Если подключение установлено, то можно выполнить любой SQL-запрос, например:

QSqlQuery query;
query.exec("SELECT id, name, salary FROM empl WHERE salary>=1000");

или

QSqlQuery query("SELECT id, name, salary FROM empl WHERE salary>=1000");

Здесь запрашиваются номера id, имена name и оклады salary всех работников из таблицы empl, у которых оклад не ниже 1 000. Обратите внимание, что если при создании объекта QSqlQuery указан текст запроса на языке SQL, то этот запрос сразу выполняется. В обоих случаях в конструкторе запроса можно указать базу данных QSqlDatabase, с которой он будет работать. Но как правило, приложение открывает только одно соединение с единственной базой данных, поэтому этот параметр принимает значение по умолчанию.

Если при выполнении запроса возникла ошибка, то метод lastError() позволяет вывести на экран её описание:

if ( ! query.isActive() )
    QMessageBox::warning(
        this, tr("Database Error"),
        query.lastError().text() );

Иначе можно получить данные, которые сервер вернул в качестве результата:

while ( query.next() ) {
    qint64 id = query.value(0).toLongLong();
    QString name = query.value(1).toString();
    double salary = query.value(2).toDouble();
    // .......
}

Метод QSqlQuery::next() переводит курсор на очередную запись результирующего набора данных или возвращает false, если достигнут его конец. Метод value(номер_столбца) возвращает значение типа QVariant, которое надо преобразовать к нужному типу с помощью методов QVariant::toInt, QVariant::toLongLong, QVariant::toString, QVariant::toDouble, QVariant::toDate, QVariant::toDateTime и т.д. Кроме next(), для навигации по набору данных можно использовать методы first(), last(), previous(), seek(int index, bool relative=false). Для увеличения быстродействия набор данных лучше сделать однонаправленным, вызвав метод QSqlQuery::setForwardOnly(true) до выполнения запроса, после этого можно использовать только next().

Метод QSqlQuery::size() возвращает число записей, полученных в результате выполнения запроса SELECT (-1, если была ошибка или если драйвер данной СУБД не поддерживает эту функцию). При выполнении SQL-запросов INSERT, UPDATE или DELETE вместо size() надо использовать метод QSqlQuery::numRowsAffected(). Чтобы узнать, возникла ли ошибка при последнем выполнении запроса, используется метод QSqlQuery::lastError(). Аналогичный метод имеет и класс QSqlDatabase. В обоих случаях возвращается экземпляр класса QSqlError. Тип ошибки можно выяснить, вызвав метод QSqlError::type(). Возможные типы ошибок: QSqlError::NoError (ошибок не было), QSqlError::ConnectionError (ошибка соединения), QSqlError::StatementError (синтаксическая ошибка в SQL-запросе), QSqlError::TransactionError (ошибка транзакции) и QSqlError::UnknownError (неизвестная ошибка).

Если требуется выполнить большое количество однотипных SQL-операторов, то эффективнее использовать запрос с параметрами:

QSqlQuery query;
query.prepare("INSERT INTO empl (id, name, salary) "
            "VALUES (:id, :name, :salary)");
for (int i=0; i<N; i++) {
    query.bindValue(":id", arr_id[i]);
    query.bindValue(":name", arr_name[i]);
    query.bindValue(":salary", arr_salary[i]);
    query.exec();
}

или, что же самое (только параметры безымянные):

QSqlQuery query;
query.prepare("INSERT INTO empl (id, name, salary) "
            "VALUES (?, ?, ?)");
for (int i=0; i<N; i++) {
    query.addBindValue(arr_id[i]);
    query.addBindValue(arr_name[i]);
    query.addBindValue(arr_salary[i]);
    query.exec();
}

Здесь выполняется вставка N записей, данные берутся из массивов arr_id, arr_name и arr_salary. Если СУБД поддерживает механизм транзакций, то для начала новой транзакции используется метод

bool QSqlDatabase::transaction()

для её подтверждения надо вызвать

bool QSqlDatabase::commit()

а для отмены:

bool QSqlDatabase::rollback()

Если СУБД не поддерживает транзакций, то вызовы transaction, commit и rollback ничего не делают. С помощью метода QSqlDriver::hasFeature() можно узнать, поддерживается ли данным драйвером и СУБД та или иная функция, в том числе и транзакции:

QSqlDriver *driver = QSqlDatabase::database().driver();
if (driver->hasFeature(QSqlDriver::Transactions))
    .......

Каждое соединение с базой данных может иметь только одну активную транзакцию. Если этого недостаточно, всегда можно открыть ещё несколько соединений с той же базой данных. Ссылку на соединение с базой данных можно получить, вызвав функцию QSqlDatabase::database(connectionName). Необязательный параметр connectionName -- это имя соединения, которое было задано при его создании с помощью QSqlDatabase::addDatabase().

По окончании работы с базой данных соединение надо закрыть: QSqlDatabase::close(). Затем можно либо открыть его снова с помощью метода open(), либо удалить из списка соединений, вызвав статический метод QSqlDatabase::removeDatabase(connectionName).

Qtdb00 1.png

Выполнение SQL-запросов (система Windows, драйвер QODBC)

Qtdb00.png

Выполнение SQL-запросов (система Linux, драйвер QMYSQL)

В листингах приведён пример программы, работающей с базой данных, а на рис. показан результат её работы в Windows и Linux.

Листинг. Выполнение SQL-запросов (файл examples-qt/db00/db00.h)

    1 #include <QtGui>
    2 
    3 class MyDialog : public QDialog {
    4     Q_OBJECT
    5 public:
    6     MyDialog(QWidget *parent=0);
    7 protected:
    8     virtual void closeEvent(QCloseEvent *event);
    9 private slots:
   10     bool start();
   11 private:
   12     QComboBox *mode;   // Режим (драйвер).
   13     QLineEdit *host;   // Хост.
   14     QLineEdit *dbname; // Имя БД.
   15     QLineEdit *user;   // Пользователь.
   16     QLineEdit *password; // Пароль.
   17     QTextEdit *scr;      // Для вывода сообщений.
   18     QPushButton *btnStart; // Кнопка 'Старт'.
   19 ;


Листинг. Выполнение SQL-запросов (файл examples-qt/db00/db00.cpp)

    1 #include <QtGui>
    2 #include <QtSql>
    3 #include <QtTest/QtTest> // Для qWait().
    4 
    5 #include "db00.h"
    6 
    7 MyDialog::MyDialog(QWidget *parent)
    8         : QDialog(parent) {
    9     QTextCodec *codec = QTextCodec::codecForName("CP1251");
   10     QTextCodec::setCodecForTr(codec);
   11     QTextCodec::setCodecForCStrings(codec);
   12     QTextCodec::setCodecForLocale(codec);
   13 
   14     setWindowFlags(Qt::Window);
   15 
   16     mode = new QComboBox(this);
   17     QStringList drivers = QSqlDatabase::drivers();
   18     drivers.removeAll("QMYSQL3");
   19     drivers.removeAll("QOCI8");
   20     drivers.removeAll("QODBC3");
   21     drivers.removeAll("QPSQL7");
   22     drivers.removeAll("QTDS7");
   23     mode->addItems(drivers);
   24 
   25     host = new QLineEdit(tr("localhost"), this);
   26     dbname = new QLineEdit(this);
   27     user = new QLineEdit(this);
   28     password = new QLineEdit(this);
   29     password->setEchoMode(QLineEdit::Password);
   30 
   31     btnStart = new QPushButton(tr("Старт"), this);
   32 
   33     scr = new QTextEdit(this);
   34     scr->setReadOnly(true);
   35 
   36     QGridLayout *layout = new QGridLayout(this);
   37     layout->addWidget(new QLabel(tr("Режим:"), this),
   38                       0, 0, Qt::AlignRight);
   39     layout->addWidget(mode, 0, 1, 1, 3);
   40 
   41     layout->addWidget(new QLabel(tr("Хост:"), this),
   42                       1, 0, Qt::AlignRight);
   43     layout->addWidget(host, 1, 1);
   44 
   45     layout->addWidget(new QLabel(tr("База данных:"), this),
   46                       1, 2, Qt::AlignRight);
   47     layout->addWidget(dbname, 1, 3);
   48 
   49     layout->addWidget(new QLabel(tr("Пользователь:"), this),
   50                       2, 0, Qt::AlignRight);
   51     layout->addWidget(user, 2, 1);
   52 
   53     layout->addWidget(new QLabel(tr("Пароль:"), this),
   54                       2, 2, Qt::AlignRight);
   55     layout->addWidget(password, 2, 3);
   56 
   57     layout->addWidget(btnStart, 3, 1, 1, 2);
   58     layout->addWidget(scr, 4, 0, 1, 4);
   59 
   60     layout->setMargin(6);
   61     layout->setSpacing(5);
   62     layout->setColumnStretch(1, 1);
   63     layout->setColumnStretch(3, 1);
   64     layout->setRowStretch(4, 1);
   65     setLayout(layout);
   66 
   67     connect(btnStart, SIGNAL(clicked()), this, SLOT(start()));
   68 }
   69 
   70 bool MyDialog::start() {
   71     scr->append(tr("Соединяюсь с базой данных..."));
   72     QSqlDatabase db = QSqlDatabase::addDatabase(
   73         mode->currentText() );
   74     db.setHostName(host->text());
   75     db.setDatabaseName(dbname->text());
   76     db.setUserName(user->text());
   77     db.setPassword(password->text());
   78     if (db.open()) {
   79         mode->setEnabled(false);
   80         host->setEnabled(false);
   81         dbname->setEnabled(false);
   82         user->setEnabled(false);
   83         password->setEnabled(false);
   84         btnStart->setEnabled(false);
   85         scr->append(tr("Соединение установлено!"));
   86     }else{
   87         scr->append(tr("Не могу соединиться: "));
   88         scr->append(db.lastError().text());
   89         return false;
   90     }
   91 
   92     QSqlQuery sql = QSqlQuery();
   93     //sql.exec(tr("SET NAMES 'cp1251'"));
   94     QStringList dbtables = db.tables(QSql::Tables);
   95     if (dbtables.contains( tr("employee"),
   96                            Qt::CaseInsensitive)) {
   97         scr->append( tr(
   98                 "Таблица \"employee\" уже существует."));
   99         sql.exec(tr("DROP TABLE employee"));
  100         if ( sql.lastError().type() == QSqlError::NoError ) {
  101             scr->append( tr(
  102                 "Удалили таблицу \"employee\" "));
  103         }else{
  104             scr->append( tr(
  105                 "Не могу удалить таблицу \"employee\":"));
  106             scr->append(sql.lastError().text());
  107             return false;
  108         }
  109     }
  110 
  111     sql.exec( tr(
  112         "create table employee ( "
  113         "  id integer PRIMARY KEY, "
  114         "  name char(30) not null, "
  115         "  born date null, "
  116         "  salary numeric(12,2), "
  117         "  married boolean NULL ) " ) );
  118     if ( sql.lastError().type() == QSqlError::NoError ) {
  119         scr->append( tr(
  120             "Создали таблицу \"employee\"."));
  121     }else{
  122         scr->append( tr(
  123                 "Не могу создать таблицу \"employee\":"));
  124         scr->append(sql.lastError().text());
  125         return false;
  126     }
  127 
  128     if (sql.prepare( tr(
  129             "INSERT INTO employee "
  130             "  VALUES (?, ?, ?, ?, ?)") ) ) {
  131         int arr_id[] = {123, 345, 501};
  132         QString arr_name[] = {tr("Винни-Пух"),
  133                               tr("Ослик Иа"),
  134                               tr("Поросёнок")};
  135         QDate arr_born[] = {QDate(1971, 12, 31),
  136                             QDate(1965, 2, 23),
  137                             QDate(1982, 6, 14)};
  138         float arr_salary[] = {1234.56f, 2345.67f, 871};
  139         int arr_married[] = {1, 0, 0};
  140 
  141         for (unsigned int i=0; i < 3; i++) {
  142             sql.bindValue(0, arr_id[i]);
  143             sql.bindValue(1, arr_name[i]);
  144             sql.bindValue(2, arr_born[i]);
  145             sql.bindValue(3, arr_salary[i]);
  146             sql.bindValue(4, arr_married[i]);
  147             sql.exec();
  148             if ( sql.lastError().type() == QSqlError::NoError ) {
  149                 scr->append( tr(
  150                     "Вставили новую запись."));
  151             }else{
  152                 scr->append( tr(
  153                     "Не могу вставить новую запись:"));
  154                 scr->append(sql.lastError().text());
  155                 return false;
  156             }
  157         }
  158     }else{
  159         scr->append( tr(
  160                 "Не могу подготовить запрос:"));
  161         scr->append(sql.lastError().text());
  162         return false;
  163     }
  164 
  165     sql.exec( tr("SELECT * FROM employee ") );
  166     if ( sql.isActive() ) {
  167         QSqlRecord rec = sql.record();
  168         scr->append( tr(
  169             "В таблице \"employee\" %1 столбцов: ")
  170             .arg(rec.count() ) );
  171 
  172         QString fields;
  173         for(int j=0; j<rec.count(); j++)
  174             fields += rec.fieldName(j) + ", ";
  175 
  176         scr->append(fields);
  177 
  178         scr->append( tr(
  179             "В таблице \"employee\" %1 записей: ")
  180             .arg(sql.size() ) );
  181 
  182         while ( sql.next() ) {
  183             int id = sql.value(0).toInt();
  184             QString name = sql.value(1).toString();
  185             QDate born = sql.value(2).toDate();
  186             double salary = sql.value(3).toDouble();
  187             bool married = sql.value(4).toBool();
  188             scr->append( tr(
  189                 "%1\t %2\t %3\t %4\t %5")
  190                 .arg(id)
  191                 .arg(name)
  192                 .arg(born.toString(tr("dd/MM/yyyy")))
  193                 .arg(salary)
  194                 .arg(married) );
  195         }
  196     }else{
  197         scr->append( tr(
  198                 "Не могу получить данные:"));
  199         scr->append(sql.lastError().text());
  200         return false;
  201     }
  202 
  203     scr->append( tr(
  204         "При закрытии окна соединение с БД будет завершено."));
  205     return true;
  206 }
  207 
  208 void MyDialog::closeEvent(QCloseEvent *event) {
  209     QSqlDatabase db = QSqlDatabase::database();
  210     if (db.isOpen()) {
  211         db.close();
  212         scr->append("--------------------------");
  213         scr->append(tr("Соединение с базой данных закрыто!"));
  214         QTest::qWait(1000);    // Ждать 1 сек.
  215     }
  216 }
  217 
  218 int main(int argc, char *argv[]) {
  219     QApplication app(argc, argv);
  220 
  221     MyDialog *mainWin = new MyDialog();
  222     mainWin->show();
  223     return app.exec();
  224 }
  • (14) Установили для диалога флаг Qt::Windows, чтобы в заголовке окна появились кнопки сворачивания и восстановления.
  • (16-23) Создали поле со списком и заполнили его названиями всех доступных драйверов SQL, кроме устаревших (17-21).
  • (25-29) Создали однострочные поля для ввода параметров соединения.
  • (31) Кнопка Старт.
  • (33-34) Многострочное поле для вывода сообщений.
  • (36-65) Расположили все элементы с помощью сеточного менеджера размещения.
  • (67) Связали кнопку Старт с функцией start().
  • (69) Функция, выполняемая при нажатии на кнопку.
  • (71) Вывели сообщение.
  • (72-78) Создали соединение с базой данных.
  • (79-84) Если соединение установлено, то менять параметры уже нельзя.
  • (93) При необходимости сообщили серверу кодовую таблицу клиента.
  • (94-95) Выяснили, существует ли в базе данных таблица employee (работники).
  • (99) Если да, то удалили её.
  • (111) Создали новую таблицу.
  • (128-162) Выполнили серию SQL-запросов с параметрами для вставки новых записей в созданную таблицу.
  • (165) Получили все записи таблицы.
  • (166-174) Получили информацию о полях (столбцах).
  • (178-194) Вывели на экран все записи (строки) таблицы.
  • (208) Виртуальная функция closeEvent выполняется при закрытии окна.
  • (209-211) Если соединение с базой данных открыто, то закрываем его.
  • (212-214) Выводим сообщение и ждём секунду перед закрытием окна. Для компиляции проектов, использующих модуль QtTest, в файл проекта *.pro надо добавить строку CONFIG += qtestlib.

Перед компиляцией проекта, в котором используется модуль QtSql, надо добавить в pro-файл строку QT += sql.

Перед выполнением этой программы требуется создать какую-нибудь базу данных:

create database db1 character set utf8;

(вместо utf8 можно использовать кодировки cp1251 или koi8r); а перед использованием QODBC -- настроить ODBC-псевдоним.

Работа с таблицами баз данных

Самый простой способ отображения информации базы данных в виде таблицы заключается в использовании классов QSqlQueryModel и QTableView:

QSqlQueryModel model;
model.setQuery("select * from employee");

QTableView view;
view.setModel(&model);
view.show();

Но вместо QsqlQueryModel можно использовать класс QSqlTableModel, позволяющий работать с таблицами баз данных на более высоком уровне, чем выполнение SQL-запросов. Тогда приведённый выше фрагмент кода запишется следующим образом:

QSqlTableModel model;
model.setTable("employee");
model.select();

QTableView view;
view.setModel(&model);
view.show();

Более сложный пример: вместо выполнения SQL-запроса

SELECT * FROM employee WHERE salary >= 1000 ORDER BY id DESC

для модели QSqlTableModel надо задать фильтр и условие сортировки:

QSqlTableModel model = QSqlTableModel(parent, db);
model.setTable("employee");          // Имя таблицы базы данных.
model.setFilter("salary >= 1000");   // Условие WHERE.
model.setSort(0, Qt::DescendingOrder); // Сортировка по убыванию id.
model.select();                      // Получить данные.

После определения модели можно узнать значение любого поля любой записи, например:

QString name = model.record(i).value("name").toString();

или

int salary = model.data(model.index(i, 3)).toInt();

Для перебора всех записей набора данных:

for (int i = 0; i < model.rowCount(); ++i) {
    QSqlRecord record = model.record(i);
    QString name = record.value("name").toString();
    double salary = record.value("salary").toDouble();
    .......
}

Qtdb01.png

В листинге приведён пример работы с таблицей работников, созданной в результате выполнения предыдущей программы, а на рис. показан внешний вид таблицы в системе Windows.

Листинг. Таблица базы данных (файл examples-qt/db01/db01.cpp)

    1 // Таблица базы данных
    2 
    3 #include <QtGui>
    4 #include <QtSql>
    5 
    6 int main(int argc, char *argv[]) {
    7 
    8     QApplication app(argc, argv);
    9 
   10     QTextCodec *codec = QTextCodec::codecForName("CP1251");
   11     QTextCodec::setCodecForTr(codec);
   12     QTextCodec::setCodecForCStrings(codec);
   13     QTextCodec::setCodecForLocale(codec);
   14 
   15     QSqlDatabase db = QSqlDatabase::addDatabase("QODBC");
   16     db.setDatabaseName("mysql_db1");
   17     db.setUserName("");
   18     db.setPassword("");
   19     db.open();
   20 
   21     // QSqlQuery q;
   22     // Для корректного отображения символов кириллицы,
   23     // возможно, придётся установить кодировку:
   24     // q.exec(QObject::tr("SET NAMES 'cp1251'"));
   25 
   26     QSqlTableModel *model = new QSqlTableModel();
   27     model->setTable("employee");
   28 
   29     model->insertRows(0, 1);
   30     model->setData(model->index(0, 0), 159);
   31     model->setData(model->index(0, 1), QObject::tr("Сова"));
   32     model->setData(model->index(0, 2), QDate(1985, 12, 31));
   33     model->setData(model->index(0, 3), 12.34);
   34     model->setData(model->index(0, 4), 1);
   35     model->submitAll();
   36 
   37     model->setEditStrategy(QSqlTableModel::OnFieldChange);
   38 
   39     model->select();
   40     model->setHeaderData(0, Qt::Horizontal,
   41                          QObject::tr("Табельн.\nномер"));
   42     model->setHeaderData(1, Qt::Horizontal,
   43                          QObject::tr("Имя"));
   44     model->setHeaderData(2, Qt::Horizontal,
   45                          QObject::tr("День рождения"));
   46     model->setHeaderData(3, Qt::Horizontal,
   47                          QObject::tr("Зарплата"));
   48     model->setHeaderData(4, Qt::Horizontal,
   49                          QObject::tr("Женат/\nзамужем"));
   50 
   51     QTableView *view = new QTableView();
   52     view->setModel(model);
   53 
   54     view->setAlternatingRowColors(true);
   55     view->resizeRowsToContents();
   56     view->resizeColumnsToContents();
   57     view->show();
   58 
   59     return app.exec();
   60 }
  • (15-19) Установили параметры соединения и подключились к базе данных.
  • (21-24) При необходимости сообщили серверу кодировку строковых данных, которую использует наша программа.
  • (26-27) Создали модель QSqlTableModel и задали имя таблицы БД.
  • (29-35) Добавили к таблице новую строку, задали значения всех пяти ячеек этой строки и подтвердили запись изменений в базу данных (диагностика возможных ошибок для простоты опущена).
  • (37) Задали режим обновления данных в БД при редактировании таблицы: параметр QSqlTableModel::OnFieldChange означает, что запись изменений в базу данных будет выполняться автоматически после окончания редактирования очередной ячейки. Другие возможные режимы: QSqlTableModel::OnRowChange (при переходе к другой строке) и QSqlTableModel::OnManualSubmit (при выполнении метода submitAll, подтверждающего все изменения, или revertAll, отменяющего их).
  • (39) Выбрали данные из таблицы БД.
  • (40-49) Определили заголовки столбцов, которые будут отображаться в окне.
  • (51-52) Создали представление и задали для него модель.
  • (54) Установили чередующийся цвет фона для строк таблицы.
  • (55-56) Выполнили автоподстройку высоты строк и ширины столбцов.
  • (57) Вывели представление таблицы на экран.

Проверьте, как работает эта програма: можно изменять данные в ячейках, при редактировании чисел и дат автоматически отображаются кнопки инкремента/декремента. Но поскольку для элемента QDoubleSpinBox по умолчанию задано максимальное значение 99.99, то при попытке изменить, например, величину зарплаты работника, любое значение, большее этого максимального, автоматически усекается. Разумеется, попытка ввести число больше допустимого, оканчивается неудачей. Кроме того, после редактирования первой же ячейки таблицы, когда происходит автоматическое обновление данных, ширина столбцов и высота строк изменяется, т.к. размеры ячеек по умолчанию отличаются от тех, что установились в результате однократного выполнения методов resizeRowsToContents и resizeColumnsToContents.

Если мы хотим получать большую зарплату, выводить разные столбцы различным цветом, отображать галочки для полей логического типа, использовать календарик для ввода дат, в общем, как-то изменять заданные по умолчанию параметры отображения и редактирования ячеек, то у нас имеются две возможности: разрабатывать свой класс модели и/или представления таблицы или использовать специальные объекты-делегаты для её ячеек. Рассмотрим оба варианта по очереди.

Разработка модели и представления таблицы БД

Решим сначала простую задачу: в ячейках последнего столбца таблицы, где хранится только два возможных значения, будем отображать элемент QCheckBox и текст "Да" или "Нет" (рис.). Кроме того, запретим редактирование первого столбца, изменим цвет фона ячеек первого и последнего столбцов, а также параметры шрифта во втором столбце.

Qtdb02.png

Для этого определим свою модель таблицы, использовав в качестве базового класс QSqlQueryModel. А чтобы управлять размерами ячеек таблицы, определим свой класс представления на основе стандартного QTableView. В листингах . и . приведён текст программы.

Листинг. Модель и представление таблицы БД (файл examples-qt/db02/db02.h)

    1 #include <QSqlQueryModel>
    2 #include <QTableView>
    3 
    4 class MyModel : public QSqlQueryModel {
    5     Q_OBJECT
    6 public:
    7     MyModel(QObject *parent = 0);
    8     Qt::ItemFlags flags(const QModelIndex &index) const;
    9     QVariant data(const QModelIndex &index,
   10                   int role = Qt::DisplayRole) const;
   11     bool setData(const QModelIndex &index,
   12                  const QVariant &value, int role);
   13 private:
   14     void refresh();
   15 };
   16 
   17 class MyView : public QTableView {
   18     Q_OBJECT
   19 public:
   20     MyView(QWidget *parent = 0);
   21 private:
   22     virtual void resizeEvent(QResizeEvent *event);
   23 };

Листинг. Модель и представление таблицы БД (файл examples-qt/db02/db02.cpp)

    1 // Таблица базы данных: пользовательская модель и представление
    2 
    3 #include <QtGui>
    4 #include <QtSql>
    5 
    6 #include "db02.h"
    7 
    8 MyModel::MyModel(QObject *parent)
    9                 : QSqlQueryModel(parent) {
   10     refresh();
   11 }
   12 
   13 Qt::ItemFlags MyModel::flags(
   14         const QModelIndex &index) const {
   15 
   16     Qt::ItemFlags flags = QSqlQueryModel::flags(index);
   17     if (index.column() >= 1 && index.column() < 4)
   18         flags |= Qt::ItemIsEditable;
   19     if (index.column() == 4)
   20         flags |= Qt::ItemIsUserCheckable;
   21     return flags;
   22 }
   23 
   24 QVariant MyModel::data(
   25             const QModelIndex &index,
   26             int role) const {
   27 
   28     QVariant value = QSqlQueryModel::data(index, role);
   29 
   30     switch (role) {
   31 
   32     case Qt::DisplayRole: // Данные для отображения
   33     case Qt::EditRole:    // Данные для редактирования
   34         if (index.column() == 0)
   35             return value.toString().prepend(tr("№"));
   36         else if (index.column() == 2 && role == Qt::DisplayRole)
   37             return value.toDate().toString("dd.MM.yyyy");
   38         else if (index.column() == 3 && role == Qt::DisplayRole)
   39             return tr("%1")
   40                    .arg(value.toDouble(), 0, 'f', 2);
   41         else if (index.column() == 4)
   42             return value.toInt() != 0 ? tr("Да") : tr("Нет");
   43         else
   44             return value;
   45 
   46     case Qt::TextColorRole: // Цвет текста
   47         if(index.column() == 1)
   48             return qVariantFromValue(QColor(Qt::blue));
   49         else
   50             return value;
   51 
   52     case Qt::TextAlignmentRole: // Выравнивание
   53         if(index.column() == 3)
   54             return int(Qt::AlignRight | Qt::AlignVCenter);
   55         else if(index.column() == 2 || index.column() == 4)
   56             return int(Qt::AlignHCenter | Qt::AlignVCenter);
   57         else
   58             return int(Qt::AlignLeft | Qt::AlignVCenter);
   59 
   60     case Qt::FontRole: // Шрифт
   61         if(index.column() == 1) {
   62             QFont font = QFont("Helvetica", 10, QFont::Bold);
   63             return qVariantFromValue(font);
   64         }else
   65             return value;
   66 
   67     case Qt::BackgroundColorRole: {  // Цвет фона
   68         int a = (index.row() % 2) ? 14 : 0;
   69         if(index.column() == 0)
   70             return qVariantFromValue(QColor(220,240-a,230-a));
   71         else if(index.column() == 4)
   72             return qVariantFromValue(QColor(200,220-a,255-a));
   73         else
   74             return value;
   75     }
   76     case Qt::CheckStateRole:  // Галочка
   77         if (index.column() == 4)
   78             return (QSqlQueryModel::data(index).toInt() != 0) ?
   79                     Qt::Checked : Qt::Unchecked;
   80         else
   81             return value;
   82 
   83     case Qt::SizeHintRole:  // Размер ячейки
   84         if (index.column() == 0)
   85             return QSize(70, 10);
   86         if (index.column() == 4)
   87             return QSize(60, 10);
   88         else
   89             return QSize(110, 10);
   90     }
   91     return value;
   92 }
   93 
   94 bool MyModel::setData(
   95             const QModelIndex &index,
   96             const QVariant &value,
   97             int /* role */) {
   98     if (index.column() < 1 || index.column() > 4)
   99         return false;
  100 
  101     QModelIndex primaryKeyIndex = QSqlQueryModel::index(
  102                 index.row(), 0);
  103     int id = QSqlQueryModel::data(primaryKeyIndex).toInt();
  104 
  105     //clear(); // Если надо полностью перерисовать таблицу.
  106 
  107     bool ok;
  108     QSqlQuery query;
  109     if (index.column() == 1) {
  110         query.prepare("update employee set name = ? where id = ?");
  111         query.addBindValue(value.toString());
  112         query.addBindValue(id);
  113     }else if(index.column() == 2) {
  114         query.prepare("update employee set born = ? where id = ?");
  115         query.addBindValue(value.toDate());
  116         query.addBindValue(id);
  117     }else if(index.column() == 3) {
  118         query.prepare("update employee set salary = ? where id = ?");
  119         query.addBindValue(value.toDouble());
  120         query.addBindValue(id);
  121     }else if(index.column() == 4) {
  122         query.prepare("update employee set married = ? where id = ?");
  123         query.addBindValue(value.toBool());
  124         query.addBindValue(id);
  125     }
  126     ok = query.exec();
  127     refresh();
  128     return ok;
  129 }
  130 
  131 void MyModel::refresh() {
  132     setQuery("select * from employee ORDER BY id");
  133 
  134     setHeaderData(0, Qt::Horizontal,
  135                   tr("Табельн.\nномер"));
  136     setHeaderData(1, Qt::Horizontal,
  137                   tr("Имя"));
  138     setHeaderData(2, Qt::Horizontal,
  139                   tr("День рождения"));
  140     setHeaderData(3, Qt::Horizontal,
  141                   tr("Зарплата"));
  142     setHeaderData(4, Qt::Horizontal,
  143                   tr("Женат/\nзамужем"));
  144 }
  145 
  146 //------------------------------------
  147 MyView::MyView(QWidget *parent)
  148       : QTableView(parent) {
  149 
  150 }
  151 
  152 void MyView::resizeEvent(QResizeEvent *event) {
  153     resizeRowsToContents();
  154     resizeColumnsToContents();
  155     QTableView::resizeEvent(event);
  156 }
  157 
  158 //------------------------------------
  159 int main(int argc, char *argv[]) {
  160 
  161     QApplication app(argc, argv);
  162 
  163     QTextCodec *codec = QTextCodec::codecForName("CP1251");
  164     QTextCodec::setCodecForTr(codec);
  165     QTextCodec::setCodecForCStrings(codec);
  166 
  167     QSqlDatabase db = QSqlDatabase::addDatabase("QMYSQL");
  168     db.setDatabaseName("db1");
  169     db.setUserName("root");
  170     db.setPassword("password");
  171     db.open();
  172 
  173     // QSqlQuery q;
  174     // Для корректного отображения кириллицы, возможно,
  175     // придётся установить кодировку:
  176     //q.exec(QObject::tr("SET NAMES 'cp1251'"));
  177 
  178     MyModel *model = new MyModel();
  179 
  180     MyView *view = new MyView();
  181     view->setModel(model);
  182 
  183     view->setAlternatingRowColors(true);
  184     view->resizeRowsToContents();
  185     view->resizeColumnsToContents();
  186     view->show();
  187 
  188     return app.exec();
  189 }
  • (8-10) В конструкторе модели таблицы вызвали конструктор базового класса и определённый ниже метод refresh (131), в котором указаны заголовки столбцов и текст SQL-запроса, выполняемый для получения данных из БД.
  • (13-22) В методе flags переопределили свойства ячеек таблицы.
  • (16) Получили значения флагов-свойств, которые определены для данного столбца в базовом классе.
  • (17-18) Для всех столбцов, кроме первого (вернее, нулевого) и последнего, выставили флаг разрешения редактирования Qt::ItemIsEditable.
  • (19-20) Для последнего столбца установили признак Qt::ItemIsUserCheckable, в результате во всех его ячейках перед текстовой меткой будет отображаться элемент QCheckBox.
  • (24-92) В методе data переопределяются данные, "хранящиеся" в ячейках таблицы. В зависимости от параметра role (роль), это либо сами данные, читаемые из БД, либо параметры их отображения.
  • (28) Получили значение, определённое в базовом классе.
  • (30-90) Определяем параметры ячейки таблицы, в зависимости от значения параметра role: Qt::DisplayRole (данные для отображения ячейки таблицы), Qt::EditRole (данные для режима редактирования), Qt::TextColorRole (цвет текста), Qt::TextAlignmentRole (выравнивание текста), Qt::FontRole (параметры шрифта), Qt::BackgroundColorRole (цвет фона ячейки), Qt::CheckStateRole (надо ли отображать элемент QCheckBox), Qt::SizeHintRole (предпочитаемые размеры ячейки).
  • (34-35) Для самого первого столбца перед числом добавили символ "№".
  • (36-37) Для столбца с датами рождения в режиме отображения задали формат "число.месяц.год" (по умолчанию действует формат "год-месяц-число").
  • (38-40) Для столбца зарплат в режиме отображения установили точность два знака после запятой.
  • (41-42) В последнем столбце отображаем текст "Да" или "Нет".
  • (43-44) Для всех остальных стрлбцов (остался только столбец с именами работников) ничего не изменяем.
  • (46-50) Для столбца с именами установили голубой цвет символов, для всех остальных -- цвет по умолчанию.
  • (52-58) Числа выравниваем по правому краю, даты и надписи "Да/Нет" -- по центру, остальные ячейки -- по левому краю. Во всех случаях центрируем по вертикали.
  • (60-65) Для столбца имён задали жирный шрифт "Helvetica" размером 10 пунктов, для всех остальных ячеек -- шрифт по умолчанию.
  • (67-74) Установили цвет фона для ячеек самого первого и последнего столбцов (для чётных строк -- немного светлее, чем для нечётных). Для всех остальных столбцов -- цвет фона по умолчанию.
  • (76-81) Для ячеек последнего столбца устанавливаем галочку или сбрасываем её, в зависимости от значения, хранящегося в БД.
  • (83-90) Установили предпочтительные размеры ячеек.
  • (94-129) В методе setData переопределяются данные, которые будут записываться в БД.
  • (98-99) Могут изменяться только ячейки из столбцов с 1 по 4.
  • (101-103) Выяснили значение поля id (целочисленный идентификатор) изменяемой записи.
  • (107-128) Определили текст SQL-запроса для обновления данных каждого столбца таблицы.
  • (131-144) Задали текст SQL-запроса для чтения данных из БД и отображаемые заголовки столбцов таблицы. Заметим, что если не указать критерий сортировки, то для некоторых драйверов (например, QPSQL) после редактирования строки таблицы будут "прыгать" (только что изменённая строка будет перемещаться на последнее место).
  • (147-150) В конструкторе представления таблицы не забыли вызвать конструктор базового класса.
  • (152-156) Определяем действия, выполняемые при изменении размеров таблицы (в данном случае делается автоматическая подгонка размеров по содержимому ячеек).
  • (159-189) Процедура main повторяет текст предыдущей программы, только в (178-181) используется наш класс модели и класс для представления таблицы.

Для своей модели мы использовали базовый класс QSqlQueryModel, работающий с произвольным набором SQL-запросов для чтения и записи данных в БД. Но за всё приходится платить: нам пришлось подробно расписывать реализацию методов data и setData. В данном случае мы имели дело с единственной таблицей базы данных, поэтому можно было в качестве базового класса взять QSqlTableModel.

Проверьте, как работает эта программа. Намного лучше, чем предыдущая, не правда ли? Размеры ячеек теперь не "прыгают" при редактировании, а при щелчке левой кнопкой мыши по элементу QCheckBox в ячейках последнего столбца автоматически изменяется текстовая метка "Да/Нет". Но ввести трёхзначную зарплату всё ещё не получается. Мы исправим данный недостаток в следующем разделе.


Делегаты для ячеек таблицы

Делегаты -- это специальные классы, которые могут использоваться для управления режимами отображения или редактирования ячеек таблицы QTableView, равно как и элементов других представлений (QListView, QTreeView). В данном разделе мы определим делегатов для редактирования дат и чисел в ячейках таблицы.

Qtdb03.png

В листингах . и . приведён текст программы, а на рис. показан внешний вид окна в момент ввода даты рождения.

Листинг. Делегаты (файл examples-qt/db03/db03.h)

    1 #include <QtGui>
    2 #include <QtSql>
    3 
    4 class MyModel : public QSqlQueryModel {
    5     Q_OBJECT
    6 public:
    7     MyModel(QObject *parent = 0);
    8     Qt::ItemFlags flags(const QModelIndex &index) const;
    9     QVariant data(const QModelIndex &index,
   10                   int role = Qt::DisplayRole) const;
   11     bool setData(const QModelIndex &index,
   12                  const QVariant &value, int role);
   13 private:
   14     void refresh();
   15 };
   16 
   17 //-----------------------------------------------
   18 class MyView : public QTableView {
   19     Q_OBJECT
   20 public:
   21     MyView(QWidget *parent = 0);
   22 private:
   23     virtual void resizeEvent(QResizeEvent *event);
   24 };
   25 
   26 //-----------------------------------------------
   27 class MyDSBDelegate : public QItemDelegate {
   28     Q_OBJECT
   29 public:
   30     MyDSBDelegate(double min=0.00,
   31                   double max=999999999.99,
   32                   double step=0.1,
   33                   int precision=2,
   34                   QObject *parent = 0);
   35     QWidget *createEditor(
   36                 QWidget *parent,
   37                 const QStyleOptionViewItem &option,
   38                 const QModelIndex &index) const;
   39     void setEditorData(QWidget *editor,
   40                        const QModelIndex &index) const;
   41     void setModelData(QWidget *editor,
   42                       QAbstractItemModel *model,
   43                       const QModelIndex &index) const;
   44     void updateEditorGeometry(
   45             QWidget *editor,
   46             const QStyleOptionViewItem &option,
   47             const QModelIndex &index) const;
   48 private:
   49     double m_min;
   50     double m_max;
   51     double m_step;
   52     int m_precision;
   53 };
   54 
   55 //---------------------------------------------
   56 class MyDEDelegate : public QItemDelegate {
   57     Q_OBJECT
   58 public:
   59     MyDEDelegate(bool calpopup = true,
   60                  QObject *parent = 0);
   61     QWidget *createEditor(
   62                 QWidget *parent,
   63                 const QStyleOptionViewItem &option,
   64                 const QModelIndex &index) const;
   65     void setEditorData(QWidget *editor,
   66                        const QModelIndex &index) const;
   67     void setModelData(QWidget *editor,
   68                       QAbstractItemModel *model,
   69                       const QModelIndex &index) const;
   70     void updateEditorGeometry(
   71             QWidget *editor,
   72             const QStyleOptionViewItem &option,
   73             const QModelIndex &index) const;
   74 private:
   75     bool m_calpopup;
   76 };
  • (4-15) Модель таблицы.
  • (18-24) Представление таблицы.
  • (27-53) Класс-делегат для вещественных чисел (на основе элемента QDoubleSpinBox).
  • (56-76) Класс-делегат для ввода даты (на основе элемента QDateEdit).

Листинг. Делегаты (файл examples-qt/db03/db03.cpp)

    1 // Таблица базы данных: делегаты
    2 
    3 #include <QtGui>
    4 #include <QtSql>
    5 
    6 #include "db03.h"
    7 
    8 MyModel::MyModel(QObject *parent)
    9        : QSqlQueryModel(parent) {
   10     refresh();
   11 }
   12 
   13 Qt::ItemFlags MyModel::flags(
   14         const QModelIndex &index) const {
   15 
   16     Qt::ItemFlags flags = QSqlQueryModel::flags(index);
   17     if (index.column() >= 1 && index.column() < 4)
   18         flags |= Qt::ItemIsEditable;
   19     if (index.column() == 4)
   20         flags |= Qt::ItemIsUserCheckable;
   21     return flags;
   22 }
   23 
   24 QVariant MyModel::data(
   25             const QModelIndex &index,
   26             int role) const {
   27 
   28     QVariant value = QSqlQueryModel::data(index, role);
   29 
   30     switch (role) {
   31 
   32     case Qt::DisplayRole:
   33     case Qt::EditRole:
   34         if (index.column() == 0)
   35             return value.toString().prepend(tr("№"));
   36         else if (index.column() == 2 && role == Qt::DisplayRole)
   37             return value.toDate().toString("dd.MM.yyyy");
   38         else if (index.column() == 3 && role == Qt::DisplayRole)
   39             return tr("%1")
   40                    .arg(value.toDouble(), 0, 'f', 2);
   41         else if (index.column() == 4 && role == Qt::DisplayRole)
   42             return value.toInt() != 0 ? tr("Да") : tr("Нет");
   43         else
   44             return value;
   45 
   46     case Qt::TextColorRole:
   47         if(index.column() == 1)
   48             return qVariantFromValue(QColor(Qt::blue));
   49         else
   50             return value;
   51 
   52     case Qt::TextAlignmentRole:
   53         if(index.column() == 3)
   54             return int(Qt::AlignRight | Qt::AlignVCenter);
   55         else if(index.column() == 2 || index.column() == 4)
   56             return int(Qt::AlignHCenter | Qt::AlignVCenter);
   57         else
   58             return int(Qt::AlignLeft | Qt::AlignVCenter);
   59 
   60     case Qt::FontRole:
   61         if(index.column() == 1) {
   62             QFont font = QFont("Helvetica", 10, QFont::Bold);
   63             return qVariantFromValue(font);
   64         }else
   65             return value;
   66 
   67     case Qt::BackgroundColorRole: {
   68         int a = (index.row() % 2) ? 14 : 0;
   69         if(index.column() == 0)
   70             return qVariantFromValue(QColor(220,240-a,230-a));
   71         else if(index.column() == 4)
   72             return qVariantFromValue(QColor(200,220-a,255-a));
   73         else
   74             return value;
   75     }
   76     case Qt::CheckStateRole:
   77         if (index.column() == 4)
   78             return (QSqlQueryModel::data(index).toInt() != 0) ?
   79                     Qt::Checked : Qt::Unchecked;
   80         else
   81             return value;
   82 
   83     case Qt::SizeHintRole:
   84         if (index.column() == 0)
   85             return QSize(70, 10);
   86         if (index.column() == 4)
   87             return QSize(60, 10);
   88         else
   89             return QSize(110, 10);
   90     }
   91     return value;
   92 }
   93 
   94 bool MyModel::setData(
   95             const QModelIndex &index,
   96             const QVariant &value,
   97             int /* role */) {
   98     if (index.column() < 1 || index.column() > 4)
   99         return false;
  100 
  101     QModelIndex primaryKeyIndex = QSqlQueryModel::index(
  102         index.row(), 0);
  103     int id = QSqlQueryModel::data(primaryKeyIndex).toInt();
  104 
  105     //clear(); // Если надо полностью перерисовать таблицу.
  106 
  107     bool ok;
  108     QSqlQuery query;
  109     if (index.column() == 1) {
  110         query.prepare("update employee set name = ? where id = ?");
  111         query.addBindValue(value.toString());
  112         query.addBindValue(id);
  113     }else if(index.column() == 2) {
  114         query.prepare("update employee set born = ? where id = ?");
  115         query.addBindValue(value.toDate());
  116         query.addBindValue(id);
  117     }else if(index.column() == 3) {
  118         query.prepare("update employee set salary = ? where id = ?");
  119         query.addBindValue(value.toDouble());
  120         query.addBindValue(id);
  121     }else if(index.column() == 4) {
  122         query.prepare("update employee set married = ? where id = ?");
  123         query.addBindValue(value.toBool());
  124         query.addBindValue(id);
  125     }
  126     ok = query.exec();
  127     refresh();
  128     return ok;
  129 }
  130 
  131 void MyModel::refresh() {
  132     setQuery("select * from employee ORDER BY id");
  133 
  134     setHeaderData(0, Qt::Horizontal,
  135                   tr("Табельн.\nномер"));
  136     setHeaderData(1, Qt::Horizontal,
  137                   tr("Имя"));
  138     setHeaderData(2, Qt::Horizontal,
  139                   tr("День рождения"));
  140     setHeaderData(3, Qt::Horizontal,
  141                   tr("Зарплата"));
  142     setHeaderData(4, Qt::Horizontal,
  143                   tr("Женат/\nзамужем"));
  144 }
  145 
  146 //------------------------------------
  147 MyView::MyView(QWidget *parent)
  148       : QTableView(parent) {
  149 
  150     MyDSBDelegate *dsbd = new MyDSBDelegate(
  151         0.0, 999999.99, 0.05, 2, this);
  152     setItemDelegateForColumn(3, dsbd);
  153 
  154     MyDEDelegate *ded = new MyDEDelegate(
  155             true, this);
  156     setItemDelegateForColumn(2, ded);
  157 }
  158 
  159 void MyView::resizeEvent(QResizeEvent *event) {
  160     resizeRowsToContents();
  161     // resizeColumnsToContents();
  162     QTableView::resizeEvent(event);
  163 }
  164 
  165 //------------------------------------
  166 MyDSBDelegate::MyDSBDelegate(
  167                   double min,
  168                   double max,
  169                   double step,
  170                   int precision,
  171                   QObject *parent)
  172              : QItemDelegate(parent),
  173                m_min(min),
  174                m_max(max),
  175                m_step(step),
  176                m_precision(precision) {
  177 }
  178 
  179 QWidget *MyDSBDelegate::createEditor(
  180             QWidget *parent,
  181             const QStyleOptionViewItem& /* option */,
  182             const QModelIndex& /* index */) const {
  183     QDoubleSpinBox *editor = new QDoubleSpinBox(parent);
  184     editor->setMinimum(m_min);
  185     editor->setMaximum(m_max);
  186     editor->setDecimals(m_precision);
  187     editor->setSingleStep(m_step);
  188     editor->installEventFilter(const_cast<MyDSBDelegate*>(this));
  189     return editor;
  190 }
  191 
  192 void MyDSBDelegate::setEditorData(
  193                 QWidget *editor,
  194                 const QModelIndex &index) const {
  195     double value = index.model()->data(
  196             index, Qt::EditRole).toDouble();
  197     QDoubleSpinBox *dsb = static_cast<QDoubleSpinBox*>(editor);
  198     dsb->setValue(value);
  199 }
  200 
  201 void MyDSBDelegate::setModelData(
  202             QWidget *editor,
  203             QAbstractItemModel *model,
  204             const QModelIndex& index) const {
  205     QDoubleSpinBox *dsb = static_cast<QDoubleSpinBox*>(editor);
  206     dsb->interpretText();
  207     double value = dsb->value();
  208     model->setData(index, value);
  209 }
  210 
  211 void MyDSBDelegate::updateEditorGeometry(
  212             QWidget *editor,
  213             const QStyleOptionViewItem &option,
  214             const QModelIndex& /* index */) const {
  215     editor->setGeometry(option.rect);
  216 }
  217 
  218 //------------------------------------
  219 MyDEDelegate::MyDEDelegate(
  220                     bool calpopup,
  221                     QObject *parent)
  222             : QItemDelegate(parent),
  223               m_calpopup(calpopup) {
  224 }
  225 
  226 QWidget *MyDEDelegate::createEditor(
  227             QWidget *parent,
  228             const QStyleOptionViewItem& /* option */,
  229             const QModelIndex& /* index */) const {
  230     QDateEdit *editor = new QDateEdit(parent);
  231     editor->setCalendarPopup(m_calpopup);
  232     editor->installEventFilter(const_cast<MyDEDelegate*>(this));
  233     return editor;
  234 }
  235 
  236 void MyDEDelegate::setEditorData(
  237                 QWidget *editor,
  238                 const QModelIndex &index) const {
  239     QDate value = index.model()->data(
  240             index, Qt::EditRole).toDate();
  241     QDateEdit *de = static_cast<QDateEdit*>(editor);
  242     de->setDate(value);
  243 }
  244 
  245 void MyDEDelegate::setModelData(
  246             QWidget *editor,
  247             QAbstractItemModel *model,
  248             const QModelIndex& index) const {
  249     QDateEdit *de = static_cast<QDateEdit*>(editor);
  250     de->interpretText();
  251     QDate value = de->date();
  252     model->setData(index, value);
  253 }
  254 
  255 void MyDEDelegate::updateEditorGeometry(
  256             QWidget *editor,
  257             const QStyleOptionViewItem &option,
  258             const QModelIndex& /* index */) const {
  259     editor->setGeometry(option.rect);
  260 }
  261 
  262 //------------------------------------
  263 int main(int argc, char *argv[]) {
  264 
  265     QApplication app(argc, argv);
  266 
  267     QTextCodec *codec = QTextCodec::codecForName("CP1251");
  268     QTextCodec::setCodecForTr(codec);
  269     QTextCodec::setCodecForCStrings(codec);
  270 
  271     QSqlDatabase db = QSqlDatabase::addDatabase("QODBC");
  272     db.setDatabaseName("mysql_db1");
  273     db.setUserName("");
  274     db.setPassword("");
  275     db.open();
  276 
  277     // QSqlQuery q;
  278     // q.exec(QObject::tr("SET NAMES 'cp1251'"));
  279 
  280     MyModel *model = new MyModel();
  281 
  282     MyView *view = new MyView();
  283     view->setModel(model);
  284 
  285     view->setAlternatingRowColors(true);
  286     view->resizeRowsToContents();
  287     view->resizeColumnsToContents();
  288     view->show();
  289 
  290     return app.exec();
  291 }
  • (1-144) Начало листинга повторяет текст предыдущей программы.
  • (150-151) Создаём экземпляр делегата для редактирования вещественных чисел; первые два параметра конструктора -- разрешённый диапазон, третий параметр -- шаг изменения, четвёртый -- количество знаков после запятой, пятый -- родительский виджет.
  • (152) Связали только что созданный экземпляр делегата с третим (считая с нуля) столбцом таблицы.
  • (154-156) Создали экземпляр делегата для редактирования даты (первый параметр конструктора определяет, будет ли при вводе даты использоваться всплывающее окно календаря) и связали его со вторым столбцом таблицы.
  • (159-163) Реализация метода resizeEvent взята из предыдущей программы.
  • (166-177) Конструктор делегата для редактирования вещественных чисел. Параметры: диапазон, шаг, точность и указатель на родительский виджет.
  • (179-190) Переопределяем виртуальный метод createEditor, который выполняется при входе в режим редактирования ячейки таблицы. В нашем случае создаётся элемент QDoubleSpinBox и задаются его параметры.
  • (185) Наконец-то наша зарплата стала больше, чем 99.99.
  • (188) Установили фильтр для событий: теперь наш экземпляр MyDSBDelegate (параметр this) будет получать все сообщения, посылаемые элементу редактирования edit.
  • (192-199) Переопределили виртуальный метод setEditorData, в котором задаются данные, отображаемые элементом редактирования при его создании.
  • (201-209) Переопределили виртуальный метод setModelData, в котором определяются данные, передаваемые элементом редактирования в модель.
  • (211-216) Переопределили виртуальный метод updateEditorGeometry, в котором осуществляется прорисовка элемента редактирования.
  • (219-260) Реализовали класс MyDEDelegate для редактирования даты. Свойство m_calpopup определяет режим редактирования: будет ли использоваться всплывающий календарик или кнопки инкремента/декремента.
  • (263-291) Функция main (как и в предыдущем примере).

Таким образом, создание пользовательского делегата для редактирования данных включает следующие шаги:

  • Описание класса-делегата на основе QItemDelegate.
  • Реализация конструктора и виртуальных методов createEditor, setEditorData, setModelData и updateEditorGeometry.
  • Создание экземпляра делегата (например, в конструкторе объекта-вида) и связывание всех или некоторых элементов вида (в случае таблицы -- всех ячеек или определённых строк и/или колонок) с этим экземпляром путём вызова метода setItemDelegate, setItemDelegateForRow или setItemDelegateForColumn.

Если требуется изменить вид элемента не только в режиме редактирования, но и в обычном состоянии, то в классе делегата надо переопределить виртуальный метод paint. Например, для MyDSBDelegate::paint:

void MyDSBDelegate::paint (
        QPainter *painter, 
        const QStyleOptionViewItem& option, 
        const QModelIndex& index ) const {

    QString text;
    QRect rect;
    QVariant value;
    QStyleOptionViewItemV2 opt = setOptions(index, option);
    value = index.data(Qt::DisplayRole);
    text = QLocale().toString(value.toDouble(), 'f', 2);
    opt.displayAlignment = Qt::AlignRight | Qt::AlignVCenter;
    drawDisplay(painter, opt, opt.rect, text);
}

Здесь задано отображение чисел с точностью до двух знаков после десятичной точки, с выравниванием по правому краю и центрированием по вертикали. Получается, что всю информацию, связанную с внешним видом ячеек таблицы (шрифт, цвет, выравнивание), можно перенести из модели в классы делегатов. Хотя идеологически этот вариант кажется более предпочтительным (модель -- для данных, вид -- для их отображения), но на самом деле вопрос сводится к тому, что считать данными: перейдя на более высокий уровень абстракции, мы понимаем, что шрифт, цвет, параметры выравнивания и точность представления чисел -- это тоже особый вид данных (вернее, метаданных), которые могут храниться в БД и изменяться пользователем.