Russian Qt Forum
Май 05, 2024, 09:08 *
Добро пожаловать, Гость. Пожалуйста, войдите или зарегистрируйтесь.
Вам не пришло письмо с кодом активации?

Войти
 
  Начало   Форум  WIKI (Вики)FAQ Помощь Поиск Войти Регистрация  

Страниц: [1] 2   Вниз
  Печать  
Автор Тема: HowTo: Модель для неупорядоченных данных  (Прочитано 29651 раз)
Eugene Efremov
Гость
« : Май 01, 2009, 19:13 »

Модель для неупорядоченных данных

Статью пришлось разбить на две части из-за технического ограничения в 20000 знаков для одного сообщения. Аттач с примером прикреплен к первой части, но его требуемое правилами описание, в силу нахождения его в конце статьи, оказалось во второй. Будьте внимательны.


Disclaimer

Эта статья создана по результатам моей переписки с Константином. Целью написания послужило желание разобраться в использовании Qt'шной реализации шаблона MVC — особенно в части добавления новых данных. Сразу оговорюсь — я пока не считаю свои знания в этой области достаточно удовлетворительными. Статья содержит ошибки — как технические, так и концептуальные. И скажу прямо — я сильно рассчитываю на помощь читателей в их поиске и исправлении.


Вступление

В реализованном в Qt варианте MVC предполагается, что хранящиеся в модели данные упорядочены: представление взаимодействует с ней посредством индексов, содержащих номера строки и столбца. Между тем, в природе существует множество источников данных, порядок элементов в которых, в лучшем случае, не имеет значения, в худшем — вообще не подлежит определению. И если при просмотре модели мы, как правило, можем выдать эти данные в неком произвольном (точнее — обусловленном текущий выборкой из источника данных) порядке, то при добавлении/изменении таких данных этот порядок может меняться совершенно непредсказуемым образом. Это может создать некоторые сложности.

Мы рассмотрим простейший вариант такого источника — хэш (если конкретно — то QHash<QString, QString>) и попробуем построить для него модель, поддерживающую вставку и редактирование как значений, так и ключей. Отметим для этого примера дополнительное ограничение, также встречающиеся достаточно часто — ключи должны быть уникальны.


1. Внутренняя структура модели.

Итак, мы строим модель для хэша строк. Очевидно, ему будет соответствовать таблица в две колонки (для ключа и значения), каждая строка которой соответствует одному элементу хэша. Значит, мы унаследуем модель от QAbstractTableModel. Поскольку хэш — достаточно простая структура, мы можем хранить его непосредственно в самой модели.

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

Код
C++ (Qt)
class HashModel : public QAbstractTableModel
{
Q_OBJECT
 
private:
int m_empty;
QHash<QString, QString> m_hash;
 
public:
HashModel(QObject *p=0) : QAbstractTableModel(p), m_empty(0) {}
 
// ..... implementation .....
 
};
 

Кроме того, нам потребуется как-то преобразовывать строки и индексы, которые будет нам отдавать модель, в ключи нашего хэша. Введем для этой цели закрытые служебные ф-ции:

Код
C++ (Qt)
QString HashModel::id2key(const QModelIndex& id) const
{
if(!id.isValid()) return QString();
 
return row2key(id.row());
}
 
QString HashModel::row2key(int row) const
{
if(row >= m_hash.count()) return QString();
 
return m_hash.keys().at(row);
}
 

Теперь, определившись с доступом к хэшу, мы можем перейти к реализации интерфейса.

Лирическое отступление
Для ускорения доступа мы могли бы сохранять ключи в отдельном массиве и обращаться к его индексам. И в реальности, скорее всего, так бы и поступили. Но это лишило бы ценности наш пример, поскольку данные перестали бы быть неупорядоченными.



2. Доступ к данным.

Как известно, для реализации readonly доступа нам достаточно реализовать flags, data, headerData и *Count. Их реализации — кроме data — очевидны. На последней остановимся подробнее:

Код
C++ (Qt)
QVariant HashModel::data(const QModelIndex &id, int role) const
{
if (!id.isValid()) return QVariant();
 
switch(role)
{
case Qt::EditRole:
case Qt::DisplayRole:
 
QString key = id2key(id);
if(key.isNull()) return key;
 
switch (id.column())
{
case 0: return key;
case 1: return m_hash[key];
}
return QVariant();
 
// ....
}
return QVariant();
}
 

Мы получаем ключ с помощью определенной ранее id2key и проверяем его наличие. Отсутствие ключа означает, что мы имеем дело со свежевставленным пустым элементом, которого в хэше нет. Во всех остальных случаях это обычный элемент хэша.

Лирическое отступление
Может возникнуть вопрос — почему мы не храним ключи в индексах? Причин тому две. Во-первых, по каким-то неведомым мне причинам, индексы не предполагают хранения в них произвольного QVariant. Они разрешают хранить или указатели, или целые числа. Так что — если бы мы захотели хранить в нем строки, нам бы пришлось, на деле, хранить их где-то еще. Т.е, фактически, создавать тот же массив ключей, который мы уже не захотели создавать в предыдущей части. А во-вторых — это мало что дает. Просто поиск соответствия стоки и ключа переместится в ф-цию index, которую еще и перегружать придется...



3. Добавление элементов. Стандартный интерфейс.

Для добавления элементов в модель предусмотрен метод insertRows (для удаления, соответственно, removeRows — но его реализация очевидна). И здесь следует сделать важное замечания. Этот метод не предоставляет средств для добавления в модель новых данных. Т.е., если мы хотим использовать для добавления новых элементов стандартный механизм — мы должны с помощью insertRows создать новую строку, и только затем уже — добавлять туда данные, вызывая setData для каждой ячейки. Это, кончено, очень неудобно, и мы еще вернемся к этому вопросу. Сейчас для нас важно другое: строки, созданные insertRows, должны быть пустыми:
Код
C++ (Qt)
bool HashModel::insertRows(int row, int count, const QModelIndex &parent)
{
if(parent.isValid()) return false;
 
beginInsertRows(parent, row, row+count-1);
m_empty+=count;
endInsertRows();
 
return true;
}
 

И здесь мы сталкиваемся с еще одной странностью всей этой системы. Но в данном случае эта странность идет нам на  пользу.

Внимательный читатель мог бы заметить, что переменная row, фактически, нами игнорируется: мы увеличиваем m_empty, что эквивалентно вставки новой строки в конец модели — независимо от значения row. Действительно, если бы мы везде в этой ф-ции заменили row на 0 — ничего бы не изменилось. Иными словами для beginInsertRows/endInsertRows совершенно безразлично, куда на самом деле вставляются новые столбцы — лишь бы их было нужное количество.

Лирическое отступление
Возникает вопрос — почему insertRows не поддерживает добавление данных? Причина проста: QAbstractItemModel не знает сколько и каких данных мы будем вставлять, и куда. Она не знает, сколько у нас столбцов в модели, соответствует элемент модели строке или же ячейке и т.д. Соответственно, не может и предоставить прототип метода, однозначно отвечающего нашим запросам.




4. Добавление элементов. Расширение интерфейса.

Как было сказано выше, добавлять новые элементы, пользуясь стандартным интерфейсом, крайне неудобно (а также — крайне неэффективно). Устранить этот недостаток можно, добавив в интерфейс ф-цию, принимающую пару ключ/значение и добавляющую ее в хэш. Простейшая реализация этой ф-ции будет, очевидно, примерно такой:

Код
C++ (Qt)
bool HashModel::insertItem(const QString& key, const QString& val)
{
if(key.isEmpty() || m_hash.contains(key)) return false;
 
beginInsertRows(QModelIndex(), m_hash.count(), m_hash.count());
m_hash[key] = val;
endInsertRows();
 
return true;
}
 

Здесь мы снова используем свойство beginInsertRows/endInsertRows, позволяющее вставлять данные куда угодно. Но с учетом этого «снова», нельзя ли вместо этого повторно использовать уже готовый код — insertRows? Можно, но результат оставляет желать лучшего:

Код
C++ (Qt)
bool HashModel::insertItem(const QString& key, const QString& val)
{
if(key.isEmpty() || m_hash.contains(key)) return false;
 
insertRow(m_hash.count());
Q_ASSERT(m_empty>0);
 
m_empty--;
m_hash[key] = val;
emit dataChanged(createIndex(0,0), createIndex(m_hash.count()-1,1));
 
return true;
}
 

Работа, которую мы здесь проделали, достаточно бессмысленна: сперва мы вызвали insertRows и увеличили m_empty, затем мы уменьшили m_empty обратно и лишь после этого вставили значение. (Кроме того, раз мы не знаем, куда именно мы ее вставили, мы должны сигнализировать об изменении всех непустых данных модели. Но это, как раз, вполне ожидаемо).

Но это решение было бы вполне разумным, если insertRows вызвали до нас и m_empty уже имеет отличное от нуля значение. В этом случае мы просто избавляемся от существующей пустой строки в модели. Таким образом, оптимальный вариант будет таким: мы реализуем — как приватные методы — оба варианта и вызываем из insertItem тот либо другой в зависимости от значения m_empty:

Код
C++ (Qt)
bool HashModel::insertItem(const QString& key, const QString& val)
{
if(key.isEmpty() || hasItem(key)) return false;
 
if(m_empty > 0)
{
insertToEmpty(key, val);
}
else
{
insertToHash(key, val);
}
return true;
}
 
void HashModel::insertToEmpty(const QString& key, const QString& val)
{
Q_ASSERT(m_empty > 0);
 
m_empty--;
m_hash[key] = val;
emit dataChanged(createIndex(0,0), createIndex(m_hash.count()-1,1));
}
 
void HashModel::insertToHash(const QString& key, const QString& val)
{
Q_ASSERT(!hasItem(key));
 
beginInsertRows(QModelIndex(), m_hash.count(), m_hash.count());
m_hash[key] = val;
endInsertRows();
}
 

В этом коде, кроме всего вышесказанного, часто повторяющейся вызов m_hash.contains(key) вынесен в открытый метод hasItem: пользователям модели тоже будет полезно знать, присутствует ли в ней тот или иной элемент.

Лирическое отступление
Почему с использованием стандартных интерфейсов возникли такие сложности, что нам пришлось вводить собственный? Причин тому две. Одну из них мы уже упоминали выше: система не знает, что у нас за модель и как в нее вставлять данные. Ее стандартный интерфейс ориентирован лишь на отдельную обработку каждой ячейки данных. И, соответственно, довольно криво работает в ситуациях, когда элемент занимает больше одной ячейки (а он, как правило, занимает целую строку). Вторая причина в том, что в реализации Model/View/Controller, с которой нам приходится работать, одна из этих трех базовых компонент попросту отсутствует. У нас нет контроллера. На замену ему предлагаются делегаты, которые, кроме того, берут на себя также и львиную долю работы представления (все в одну кучу, ага...). Но они предлагают интерфейсы лишь для изменения существующих элементов. Если мы хотим добавить новый — получаем дырку, которую и приходится затыкать взятыми откуда попало подручными средствами...



5. Изменение существующих данных.

Наконец, мы приходим к последнему из необходимых элементов стандартного интерфейса — ф-ции setData. Если бы мы разрешали редактирование только значений (что, в реальности, скорее всего, было бы оптимальным выбором) — мы могли бы реализовать ее гораздо раньше. Но мы хотим также разрешить изменение с ее помощью ключей. А единственный способ сделать это для хэша — удалить существующий ключ, заменив его новым. А значит нам придется использовать интерфейс, который мы определили в предыдущем разделе.

При редактировании ключа мы проверяем, находится ли редактируемая строка в хэше. Если да — сперва удаляем ее, затем вставляем заново с новым ключём и старым значением. Если она пустая — сразу вставляем с пустым значением. И еще тонкий момент — прежде, чем это делать мы должны убедиться, что новый ключ не совпадает ни с одним из существующих, иначе при вставке в хэш он его затрет:

Код
C++ (Qt)
bool HashModel::setData(const QModelIndex &id, const QVariant &val, int role)
{
if(!id.isValid()) return false;
if(role != Qt::EditRole) return false;
 
QString key = id2key(id);
QString sval = val.value<QString>();
 
switch(id.column())
{
case 1:
if(key.isNull()) return false;
 
m_hash[key] = sval;
emit dataChanged(id, id);
return true;
 
case 0:
if(hasItem(sval)) return false;
 
QString rval = "";
if(!key.isNull())
{
rval = m_hash[key];
m_hash.remove(key);
m_empty++;
}
insertToEmpty(sval, rval);
return true;
 
}
return false;
}
 

Помимо этого, мы, по аналогии с вставкой, можем (точнее — должны) определить интерфейс для быстрого изменения значений по ключу. Как и в случае с добавлением нового ключа, мы не знаем, какая именно строка здесь изменилась. Поэтому, мы сигнализируем о возможном изменении всех значений:

Код
C++ (Qt)
bool HashModel::setItem(const QString& key, const QString& val)
{
if(key.isEmpty() || !hasItem(key)) return false;
 
m_hash[key] = val;
emit dataChanged(createIndex(0,1), createIndex(m_hash.count()-1,1));
 
return true;
}
 

Еще одним дополнением будет возможность задать весь хэш (метод setDataSource — реализация очевидна). Наиболее очевидная область использования этого функционала — начальная инициализация модели.


6. Поиск ключей.

В принципе, на этом нашу модель уже можно было бы считать готовой. Однако, для полноты не мешало бы иметь механизм, позволяющий получить индекс по заданному значению ключа. И такой механизм у нас есть, хотя выглядит он не слишком красиво. Это метод QAbstractItemModel::match. Однако, стандартная реализация этого метода будет, очевидно, работать с нашей моделью весьма неэффективно. Поэтому мы переопределим этот метод, реализовав для ключей более эффективный поиск:

Код
C++ (Qt)
QModelIndexList HashModel::match(const QModelIndex &id, int role, const QVariant &val, int hints, Qt::MatchFlags fl) const
{
if(id.column() == 0 && role == Qt::DisplayRole)
{
int rf = fl&0x0F;
if(rf == Qt::MatchExactly || rf == Qt::MatchFixedString && fl&Qt::MatchCaseSensitive)
{
QString key = val.value<QString>();
QModelIndexList res;
if(hasItem(key))
{
int start = fl&Qt::MatchWrap ? 0 : id.row();
int pos = m_hash.keys().indexOf(key, start);
 
if(pos != -1) res.append(createIndex(pos, id.column()));
}
return res;
}
}
 
return QAbstractTableModel::match(id, role, val, hints, fl);
}
 

Нам здесь все равно приходится искать позицию ключа в списке. Но даже это будет быстрее, чем многократный вызов data, имеющий место в исходной реализации. Кроме того, отсутствие ключа мы обнаружим сразу.

Лирическое отступление
Приведенный код не слишком красив — приходится проверять множество условий, чтобы выбрать из всех вариантов единственный нужный. Причина этого ясна: функции передается слишком много параметров. Это плохо, но мы с этим мало что можем сделать — здесь опять-таки нужно править QAbstractItemModel.


Так или иначе, на этом реализацию самой модели можно считать законченной. Мы не рассмотрели вопросы, связанные с выделением и поддержкой drag`n`drop, но это — даже если оставить в стороне вопрос, какое отношение drag`n`drop вообще имеет к модели — слишком большая тема, чтобы ее можно было полноценно описать в рамках данной статьи. Кроме того, поскольку мы уже определили необходимые интерфейсы, преодолевающие сложности, связанные с неупорядоченностью данных модели, а значит — для реализации этого дополнительного функционала мы можем использовать уже их. С учетом этого, реализация будет подобна таковой для любой другой сложной модели — что не имеет прямого отношения к тематике данного HowTo.

Посему, будем считать модель готовой к употреблению, и рассмотрим теперь некоторые аспекты ее использования.

продолжение см. ниже...
« Последнее редактирование: Май 03, 2009, 18:39 от Eugene Efremov » Записан
Eugene Efremov
Гость
« Ответ #1 : Май 01, 2009, 19:14 »

Модель для неупорядоченных данных

Статью пришлось разбить на две части из-за технического ограничения в 20000 знаков для одного сообщения. Аттач с примером прикреплен к первой части, но его требуемое правилами описание, в силу нахождения его в конце статьи, оказалось во второй. Будьте внимательны.


7. Контроллер.

Как уже отмечалось выше — нормальных контроллеров у нас нет. Поэтому возникает соблазн реализовать соответствующий функционал где попало — например прямо в методах главного окна. Однако эти деструктивные поползновения смешать данные с представлением мы отбросим — в конце концов, если Qt не озаботилось предоставить нам контроллер, нам никто не мешает сделать его самим.

Поскольку интерфейс для редактирования элементов уже реализован связкой представление/делегат, нам нужен только интерфейс для их добавления и удаления:

Код
C++ (Qt)
class HashController : public QObject
{
Q_OBJECT
 
private:
QAbstractItemView *m_view;
HashModel *m_model;
 
public:
HashController(QAbstractItemView *view);
 
public slots:
void insertItem(const QString& key, const QString& val);
void removeItem();
 
};
 

Отметим, что нам даже не нужно передавать модель в конструктор — мы можем получить ее из view. И то же самое view послужит нам в качестве родительского объекта.

Реализация insertItem с использованием расширенного интерфейса нашей модели тривиальна:

Код
C++ (Qt)
void HashController::insertItem(const QString& key, const QString& val)
{
if(m_model->hasItem(key))
{
m_model->setItem(key, val);
}
else
{
m_model->insertItem(key, val);
}
}
 

Для removeItem же вообще достаточно стандартного интерфейса. Удаляем мы текущий столбец, получая его из представления:

Код
C++ (Qt)
void HashController::removeItem()
{
QModelIndex id = m_view->currentIndex();
if(!id.isValid()) return;
 
m_model->removeRow(id.row());
}
 

Единственным недостатком такой схемы будет то, что представление об этом контролере ничего не знает, а значит — средств для его вызова нам не предоставляет. Мы, конечно, можем реализовать собственное представление, но значительно проще будет обернуть существующее представление в виджет, добавив туда же пару кнопок для управления контроллером. Фактически, такой виджет тоже будет играть роль представления — только более высокого уровня.

Лирическое отступление
Обращает на себя внимание, что контроллер у нас теперь занимает болше одного объекта: помимо нашего HashController, часть функций контроллера взял на себя QAbstractItemDelegate скрытый в недрах представления. Более того, нам ничего не мешает, например, разделить HashController на две части — одну для удаления элементов, другую — для добавления. В принципе, это хорошо. Для контроллеров, фактически, реализуется компонентный подход. Собственно, именно желание разбить контроллер на компоненты и привело, скорее всего, авторов Qt к концепции делегатов. Но это уже другая история...



8. Использование в контроллере стандартного интерфейса модели.

Одним из требованием к модели была возможность полного доступа к ней с использованием только стандартного интерфейса QAbstractItemModel. Посмотрим, сможем ли мы реализовать функционал нашего контроллера, используя только стандартный интерфейс.

Фактически, нам нужно переписать только insertItem, поскольку во всех других случаях мы уже его используем. Мы можем реализовать этот функционал, ничего не зная о ключах, но нам придется задействовать match со всем ее недостатками:

Код
C++ (Qt)
void HashController::insertItem(const QString& key, const QString& val)
{
QModelIndexList ml = m_model->match(m_model->index(0,0), Qt::DisplayRole, key, -1, Qt::MatchExactly);
 
if(ml.empty())
{
int fin = m_model->rowCount();
m_model->insertRow(fin);
 
QModelIndex ki = m_model->index(fin, 0);
m_model->setData(ki, key);
 
QModelIndexList ml2 = m_model->match(m_model->index(0,0), Qt::DisplayRole, key, -1, Qt::MatchExactly);
QModelIndex ki2 = ml2.first();
QModelIndex kv2 = m_model->index(ki2.row(), 1);
m_model->setData(kv2, val);
}
else
{
QModelIndex ki = ml.first();
QModelIndex kv = m_model->index(ki.row(), 1);
m_model->setData(kv, val);
}
}
 

Мы видим, что код получился крайне громоздким и неудобочитаемым. Обращает на себя внимание, что для вставки новой пары ключ значение, нам пришлось не только два раз вызвать setData, но и дважды провести поиск, поскольку после вставки ключа индексы стали недействительны!

Единственная ценность проведенного изменения в том, что теперь HashController может работать с любой моделью, а не только с нашей. Учитывая, что писался он именно под HashModel, эта ценность представляется весьма сомнительной... Так что, для реальной работы, конечно, стоит использовать расширенный интерфейс модели, раз уж мы его определили.

Истинный же смысл этого написания этого варианта контроллера был в том, что мы убедились: наша модель работает и полностью поддерживает стандартный интерфейс. А значит, на этом работу по ее написанию можно считать законченной.


Дополнение. Описание вложения.

Прилагаемый код содержит классы HashModel, HashController а также простейший вариант виджета для обертки представления (по совместительству выступает в качестве главного окна примера). Также прилагается простенький диалог для ввода пары ключ/значение.

Класс HashController может быть откомпилирован в обоих вышеописанных вариантах. По умолчанию он использует расширенный интерфейс HashModel, для использования стандартного интерфейса необходимо установить макрос HC_USE_STANDART.

Требования к версии Qt: не ниже 4.4 (в диалоге используется QFormLayout).


Проверено под Windows 2003 в Qt 4.4.1, в компиляторе mingw, gcc version 3.4.5
Исходный код прикреплен к первой части статьи (см. выше).
Записан
ритт
Гость
« Ответ #2 : Май 01, 2009, 23:47 »

бросил читать на четвёртом пункте. возмутительно! статью следовало назвать "я так ничерта и не понял", а не "howto".

важные моменты: QHash не сохраняет последовательность ключей; QModelIndex не хранит данные; что не так с ролями и с каких пор подсказки вьюхе считаются нонсенсом?; стандартный интерфейс предназначен для _самодостаточных_ моделей, а не для кастратов каких-то с зависимостями от прихотей юзверя, а для кастратов "расширенный" интерфейс подойдёт...
наверняка, далее такая же чушь, но читать дальше я увольняюсь.
Записан
pastor
Administrator
Джедай : наставник для всех
*****
Offline Offline

Сообщений: 2901



Просмотр профиля WWW
« Ответ #3 : Май 02, 2009, 10:13 »

Константин, я бы небыл настолько категоричен в отношении к статье. Eugene Efremov попытался написать её, за что ему отдельное спасибо. Человек только учится. Все неточности в статье можно обсудить и исправить - будет польза и форуму и автору. имхо
Записан

Integrated Computer Solutions, Inc. (ICS)
http://www.ics.com/
Eugene Efremov
Гость
« Ответ #4 : Май 02, 2009, 17:36 »

статью следовало назвать "я так ничерта и не понял"

Вообще-то, я достаточно ясно сказал про это в самом начале стати. И писать ее взялся именно с целью понять. Более того, ранее, в нашем обсуждении этого вопроса, я достаточно ясно высказался, что именно я собираюсь сделать:

меня этот тред начинает утомлять...

Жаль.
Хотя, в принципе, понимаю, что таким дотошным выяснением деталей можно достать кого угодно.  Улыбающийся

Тогда предлагаю сделать так. Я иду в тему «Уроки и статьи», пишу там — в меру своего текущего понимания — заготовку для обещанного howto, после чего общими усилиями доводим его до ума. На готовом примере с описанием все ошибки и неточности сразу будут видны. А в конечном счете получится (надеюсь) что-то, полезное для всего форума.

Никаких возражений против этого я не встретил. И писал — зная, что моих знаний и опыта для полноценного освещения вопроса явно недостаточно — именно в расчете на дальнейшее сотрудничество и помощь в исправлении ошибок. А не на вопли о том, что это «возмутительно».


QHash не сохраняет последовательность ключей;

Я где-то утверждал обратное?
Наоборот, я написал что выбрал его — в качестве иллюстрации проблемы — именно по причине неупорядоченности содержащихся в нем данных.

Единственное, на что я закладываюсь — это на то, что пока хэш не меняется QHash::keys возвращает всякий раз одинаковый список. Естественно, что как только мы модифицируем хэш, этот список станет другим.

Вообще, способ, которым я установил связь между этим хэшем и индексами мне самому не нравится, как многое другое в этой модели. Так что, если кто-нибудь предложит другой вариант — я ему буду только благодарен.

QModelIndex не хранит данные;

Для чего тогда нужны QAbstractItemModel::createIndex принимающий целые числа или указатели, а также QModelIndex::internalPointer и QModelIndex::internalId их возвращающие?

что не так с ролями

Они реализованы через enum и тянут за собой повторяющийся switch/case во все места, где используются.

и с каких пор подсказки вьюхе считаются нонсенсом?;

Смешение данных и представления.

стандартный интерфейс предназначен для _самодостаточных_ моделей, а не для кастратов каких-то с зависимостями от прихотей юзверя, а для кастратов "расширенный" интерфейс подойдёт...

Мда. На это даже и не знаю что сказать. Осмысленного утверждения, с которым можно согласиться или оспорить я здесь не увидел. Одни эмоции.  Грустный

читать дальше я увольняюсь.

Очень жаль. Я рассчитывал на конструктивный диалог.


Просьба ко всем читателям: пожалуйста, не надо писать сюда только для того, чтобы сообщить, что автор дурак, а в статье написана полная чушь. Мне это и без вас известно.

Если вы видите конкретную ошибку — будь то в коде, тексте статьи или общих положениях, на которые это все опирается — сообщите о ней. Если при этом еще укажите, как ее исправить — будет совсем хорошо. Если вы видите спорное или неточное утверждение и хотите его обсудить, или предлагайте внести в статью те или иные изменения — welcome.

Но если вы пришли только за тем, чтобы сказать, что все плохо, и никаких конкретных предложений, как это все исправить, у вас нет — пожалуйста, идите с этим в какое-нибудь другое место...
Записан
Eugene Efremov
Гость
« Ответ #5 : Май 02, 2009, 18:35 »

Все неточности в статье можно обсудить и исправить - будет польза и форуму и автору.
Спасибо за поддержку.
Выражаю надежду, что на форуме найдутся люди, готовые этим заняться...
Записан
ритт
Гость
« Ответ #6 : Май 02, 2009, 19:59 »

гг...я был послан )

pastor, Eugene Efremov попытался написать своё негативное отношение к реализации model-view в Qt, а это не больно-то связано с названием статьи. тут ситуация примерно как с давешним вопросом про QString ("что это за поддержка unicode такая, если она даже стандартный wchar_t не понимает?!") - сначала эмоции, а потом думалка.


Цитировать
вопли о том, что это «возмутительно»
ну, правильно. я возмущён - вот и вопию. к тому же, в начале статьи ссылка на меня и на наш диалог, хотя, в статье используются лишь собственные домыслы, а полуается, что и я причастен к этому...

Цитировать
Для чего тогда нужны QAbstractItemModel::createIndex принимающий целые числа или указатели, а также QModelIndex::internalPointer и QModelIndex::internalId их возвращающие?
мб стоило сначала прояснить это для себя? подсказка: где используется createIndex(...)?

Цитировать
Цитировать
что не так с ролями

Они реализованы через enum и тянут за собой повторяющийся switch/case во все места, где используются.
фобия switch/case? хотел бы я знать что ты предложишь взамен ролей.

Цитировать
Цитировать
и с каких пор подсказки вьюхе считаются нонсенсом?;

Смешение данных и представления.
хорошо, простой пример: пишем модель - аналог QFileSystemModel. сжатые папки и файлы хотим выделять текстом синего цвета, а для файлов изображений, содержащих превьюшки, стандартную иконку файла хотим заменить превьюшкой. итак, имеем только минимальный необходимый набор ролей - EditRole + DisplayRole. и что, в каждой вьюхе будешь дублировать часть работы модели только для того, чтобы выяснить "а сжатый ли это файл/папка? а имеет ли этот файл встроенную превьюшку?"? будешь дублировать? или всё-таки переоценишь правильностью своих высказываний?

далее: ты сетуешь на то, что в модель нельзя вставить поизвольный объём данных "одним махом". давай прикинем - абстрактная модель знать не знает сколько ты (разработчик) собираешься сделать столбцов в строке, будет ли это количество столбцов одинаковым для каждой строки и, тем-более, _сколько_столбцов_ будет в строках, которых ещё не существует, но пользователь _может_ их создать. и ты на месте модели хотел бы иметь метод insertRow(int pos, void* data0, void* data1, ..., void* data40)? а потом какой-то умник создаёт строку с 50-ю столбцами и возмущается, что его задача не предусматривается абстрактным интерфейсом.
с другой стороны, если пользователю потребуется создать строку с произвольным содержимым, чего мне беспокоиться о том, что для её вставки потребуется 40-50 вызовов setData(...)? да пусть хоть 150, если я всё-равно не могу предусмотреть эффективного интерфейса для данной задачи. но если могу, то почему просто его не добавить, как это сделано в QSqlTableModel (bool QSqlTableModel::insertRecord ( int row, const QSqlRecord & record ))?
ах, вьюха не знает никакого insertRecord() модели. а разве вьюха добавляет/удаляет строки в/из модель/и?!

короче, каждое лирическое отступление в статье - это иллюстрация непонимания того или иного момента. а лажать троллей - это зря, с этим на лор.

зы. быть может, я не прав, возможно, в мире существуют настолько удобные и универсальные реализации MV(C), что кутэшная рядом с ними - настоящее дерьмо, но я таких пока не встречал. сделают iv-ng, учтут недочёты предыдущей реализации - хорошо. а пока что и текущей реализации достаточно - только нужно документацию внимательно читать, если чего-то не понимаешь.
Записан
pastor
Administrator
Джедай : наставник для всех
*****
Offline Offline

Сообщений: 2901



Просмотр профиля WWW
« Ответ #7 : Май 02, 2009, 20:41 »

pastor, Eugene Efremov попытался написать своё негативное отношение к реализации model-view в Qt

Кстате не все довольны MV в Qt и в самом Trolltech ))). Так что для меня это не новость ))
Записан

Integrated Computer Solutions, Inc. (ICS)
http://www.ics.com/
ритт
Гость
« Ответ #8 : Май 02, 2009, 20:51 »

и я не говорил, что прямо-таки доволен, но в Qt других реализаций модель-вью нет - работаем с тем, что имеется...
Записан
johnny
Гость
« Ответ #9 : Май 02, 2009, 22:15 »

to Eugene Efremov

Я тоже пытаюсь разобраться в qt-моделях, с интересом почитал ваш "крик души" Улыбающийся. Если можно, поясните несколько моментов:

1. Вы пытаетесь обеспечить вставку произвольного числа строк. Я так и не понял - зачем? Ну разрешайте вставку тока одной, если идет попытка вставить больше - возвращайте false.

2. "Иными словами для beginInsertRows/endInsertRows совершенно безразлично, куда на самом деле вставляются новые столбцы — лишь бы их было нужное количество." - мне представляется, что подписчики этих сигналов используют информацию о месте вставки чтобы избежать ненужных запросов данных - им потребуется перерисовать тока вставленные столбцы. Если передавать неверную инфу - будут грабли с отрисовкой.

3. "Одним из требованием к модели была возможность полного доступа к ней с использованием только стандартного интерфейса QAbstractItemModel" - я не понял, почему мы должны использовать интерфейс QAbstractItemModel в реализации метода нашего же класса? Какие выгоды мы от этого получаем - код же просто кошмарный...

PS Заинтересовался Qt тока позавчера, так что сильно не пинайте Улыбающийся


Записан
johnny
Гость
« Ответ #10 : Май 02, 2009, 22:35 »

Цитировать
Несомненно, читатель уже обратил внимание на уродливую связку swicth/case.

Ну тут еще вопрос - что лучше. Если избавиться от role, то получится куча функций вида:
data(), icon(), background(), tooltip(), whatWhis(), ...

так по крайней мере вся логика получения данных в одном месте.

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

Я ради интереса почитал доки к QSQLTableModel. Так там есть editStrategy (OnFieldChange, OnRowChange, OnManualSubmit), которая определяет, когда сабмитить изменения в базу. OnRowChanged отлавливается при попытке установить данные не текущей редактируемой строки или вставить новую строку. Кстати для OnFieldChange и OnRowChange вставка более одной строки запрещена.


Имхо, для вашего случая надо что-то подобное. Возможно, было бы полезно сделать некий наследник QAbstractTableModel, который бы поддерживал понятие "текущая редактируемая строка".
Записан
Eugene Efremov
Гость
« Ответ #11 : Май 03, 2009, 17:44 »

Eugene Efremov попытался написать своё негативное отношение к реализации model-view в Qt, а это не больно-то связано с названием статьи.

Ммм... Я правильно понял, что основную негативную реакцию вызвали мои «лирические отступления», в которых я сетую на несовершенство реализации MVC в Qt?


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

Ну вообще-то, без этого диалога статьи бы не было. Я получил из него довольно много информации, которую здесь и использовал, за что тебе отдельное спасибо. К сожалению, информации все-таки не хватило и кое-что пришлось домысливать. А дальше — уж что получилось, то получилось.

Если бы мы продолжали тот диалог в прежнем режиме до полного прояснения всех деталей — возможно, результат был бы лучше. Но тебе это надоело. И это понятно, потому что способ это медленный и неэффективный. Я предложил, как альтернативу диалогу, вариант с написанием статьи, отражающим — со всеми его ошибками — мой текущий уровень, с последующим исправлением этих ошибок. Ты не возражал. Я написал. Естественно, многое описал неправильно, но ведь так и предполагалось с самого начала.

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


Цитировать
Для чего тогда нужны QAbstractItemModel::createIndex принимающий целые числа или указатели, а также QModelIndex::internalPointer и QModelIndex::internalId их возвращающие?
мб стоило сначала прояснить это для себя?

До сих про я полагал, что они используются для сохранения в индексах — в той или иной форме — ссылок на элементы данных модели. Что-то не так?

где используется createIndex(...)?

Главным образом — в реализации метода index, насколько я понимаю.


Цитировать
Цитировать
что не так с ролями
Они реализованы через enum и тянут за собой повторяющийся switch/case во все места, где используются.
фобия switch/case? хотел бы я знать что ты предложишь взамен ролей.

Первое что приходит в голову — Factory(в той или иной форме) + State. Модель отвечает за предоставление фабрики, фабрика генерит иерархию состояний (читай тех же ролей, но объектно-ориентированных), через их методы View узнает о тех или иных аспектах модели.

Кстати, если этот вариант обработать напильником, он и в текущую реализацию влезет, но придется все делать внутри модели, что весьма криво. Но сколько-нибудь оправдано это будет, лишь если эта модель сама лежит в корне большой иерархии других моделей...

Другой вариант. Замечаем, что роли — это, по-существу, свойства элементов модели, и пляшем уже от этого. Это, пожалуй, лучше, чем вариант с фабрикой состояний, но потребует куда более существенной переработки всей системы.


Цитировать
Цитировать
и с каких пор подсказки вьюхе считаются нонсенсом?;
Смешение данных и представления.
хорошо, простой пример: пишем модель - аналог QFileSystemModel. сжатые папки и файлы хотим выделять текстом синего цвета, а для файлов изображений, содержащих превьюшки, стандартную иконку файла хотим заменить превьюшкой. итак, имеем только минимальный необходимый набор ролей - EditRole + DisplayRole. и что, в каждой вьюхе будешь дублировать часть работы модели только для того, чтобы выяснить "а сжатый ли это файл/папка? а имеет ли этот файл встроенную превьюшку?"? будешь дублировать? или всё-таки переоценишь правильностью своих высказываний?

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

А теперь — ответ на все сразу.
Модель не должна заморачиваться на тему «каким цветом выделить файл». Вьюха не должна заморачиваться на тему «а не архивный ли файл соответствует этому элементу». Более того, ей вообще совсем не обязательно знать, что такое файл. Единственное, что должна знать вьюшка — это что у данного элемента модели выставлен некий атрибут, и что элементы с таким атрибутом надо отображать неким особым способом. А будет она его выделять синим, красным или серо-буро-малиновым — это уже ее, вьюшки, личное дело. Дело же модели — этот атрибут выставить. И только. А не пытаться указывать вьюшке, как ей делать ее работу.

С рисунками ситуация немного сложнее, поскольку, в зависимости от контекста, рисунок можно считать как частью данных, так и элементом представления. В данном случае иконки и превьюшки являются частью файла (или связанной с файлом метаинформацией), так что будет правильным считать, что мы имеем дело с первым вариантом. Соответственно, модель должна их обработать и предоставить вьюшке, попутно известив ее, если один из элементов отсутствует. Остальное — опять-таки не ее дело.


далее: ты сетуешь на то, что в модель нельзя вставить поизвольный объём данных "одним махом".

Хде?!
(Посмотрев текст) Ты, что ли, имеешь в виду это:
Цитировать
Метод insertRows <...> не предоставляет средств для добавления в модель новых данных. Т.е., если мы хотим использовать для добавления новых элементов стандартный механизм — мы должны с помощью insertRows создать новую строку, и только затем уже — добавлять туда данные, вызывая setData для каждой ячейки. Это, кончено, очень неудобно, и мы еще вернемся к этому вопросу.

Мда. Действительно, можно, наверное, и так понять. Надо будет как-то это переформулировать или дополнить.


давай прикинем - абстрактная модель знать не знает сколько ты (разработчик) собираешься сделать столбцов в строке,
<...>
я всё-равно не могу предусмотреть эффективного интерфейса для данной задачи. но если могу, то почему просто его не добавить, как это сделано в QSqlTableModel (bool QSqlTableModel::insertRecord ( int row, const QSqlRecord & record ))?

О, кстати. Это надо будет вставить в статью в качестве объяснения необходимости введения расширенного интерфейса. Пожалуй, если оно там будет, то предыдущий отрывок менять и не нужно...

ах, вьюха не знает никакого insertRecord() модели. а разве вьюха добавляет/удаляет строки в/из модель/и?!

Это делает контроллер. Которого у нас нет и который приходится писать ручками. Именно с использованием расширенного интерфейса. Если бы ты дочитал статью до конца, то увидел бы, что там про это написано.


короче, каждое лирическое отступление в статье - это иллюстрация непонимания того или иного момента.

Если и так, то что же?
Я, кончено, могу их все удалить. И с теми, где пишется про case/switch и роли, наверное, так сделать лучше всего: тема слишком необъятна, вызывает очень много вопросов и выходит далеко за рамки статьи.

Но в основной целью этих лирических отступлений является разъяснение, почему я использовал тот или иной способ решения задачи. То, что в результате получилась критика — возможно, необоснованная — в определенной мере закономерно: задачу всегда хочется решить наиболее красивым способом, а если инструмент этого не позволяет, то в первую очередь напрашиваться вывод, что инструмент плох. Хотя ограничения, конечно, могут быть и вполне объективными.

Думаю, лучшим решением будет не удалять их, а исправить: если критика действительно вызвана непониманием, и в основе тех или иных ограничений имеются объективные причины — нужно указать эти причины и/или разъяснить ошибочность подобной «наивной» критики.


нужно документацию внимательно читать, если чего-то не понимаешь.

К сожалению, этого недостаточно. Документация хороша, когда ты уже понимаешь, что именно ты хочешь там найти. Того общего описания, которое там имеется, для этого понимания мало. Бланшета и Шлее — тоже. Если эту статью удаться довести до ума — может быть она хоть частично заткнет этот пробел...


P.S. Между тем, прошло уже двое суток, а я так и не вижу ни одного указания на какую-либо конкретную ошибку. Все ограничивается критикой моей критики Подмигивающий У меня уже появляется крамольная мысль, что может все и не так плохо, может там вовсе даже и нет никаких по-настоящему серьезных ошибок, раз никто до сих пор меня в них пальцем ни ткнул?  Подмигивающий

P.P.S. Хотя на самом деле ошибки там, кончено же, есть. Вот только что просматривал текст, наткнулся на такой перл:
Код
C++ (Qt)
for(int i=row; i<row+count; i++)
{
m_empty++;
}
 
Улыбающийся
Прямая иллюстрация пагубности копипасты, кстати.
Надо будет не забыть поправить. Неужели никто этого не заметил?
Записан
Eugene Efremov
Гость
« Ответ #12 : Май 03, 2009, 18:16 »

1. Вы пытаетесь обеспечить вставку произвольного числа строк. Я так и не понял - зачем? Ну разрешайте вставку тока одной, если идет попытка вставить больше - возвращайте false.

Где? Никакой вставки произвольного числа строк за раз у меня нет. Кроме, конечно, требуемого интерфейсом insertRows. Так с ним — это не ко мне, это к троллям...

2. "Иными словами для beginInsertRows/endInsertRows совершенно безразлично, куда на самом деле вставляются новые столбцы — лишь бы их было нужное количество." - мне представляется, что подписчики этих сигналов используют информацию о месте вставки чтобы избежать ненужных запросов данных - им потребуется перерисовать тока вставленные столбцы. Если передавать неверную инфу - будут грабли с отрисовкой.

Мне тоже это кажется странным. И хорошо бы, чтобы этот момент пояснил кто-нибудь из более знающих людей.
Но факт остается фактом: это работает. Кстати, это один из моментов моей переписки с Константином, когда он выразился абсолютно четко и ясно и даже привел фрагмент кода. Но добиться у него объяснения, почему это работает, мне так и не удалось.


3. "Одним из требованием к модели была возможность полного доступа к ней с использованием только стандартного интерфейса QAbstractItemModel" - я не понял, почему мы должны использовать интерфейс QAbstractItemModel в реализации метода нашего же класса? Какие выгоды мы от этого получаем - код же просто кошмарный...

Улыбающийся
Только чтобы убедиться, что этот интерфейс работает. А значит сущности, которые про нашу модель ничего не знают, могут им пользоваться. Сами мы, конечно, не обязаны это все использовать, если точно знаем, что перед нами именно наша модель и мы можем пользоваться ее расширенным интерфейсом.

Можно считать это таким своеобразным юнит-тестом: сделать реализации через оба интерфейса и убедиться, что они работают одинаково.


Цитировать
Несомненно, читатель уже обратил внимание на уродливую связку swicth/case.

Ну тут еще вопрос - что лучше. Если избавиться от role, то получится куча функций вида:
data(), icon(), background(), tooltip(), whatWhis(), ...

Про то, что можно было бы (если бы да кабы...) с этим сделать я уже отписал в письме Константину.
А вообще — вижу что тема swicth/case/role вызывает много вопросов общего характера, которые к статье это имеют мало отношения. Так что, пожалуй ее упоминание из статьи лучше действительно убрать. А дискуссию на эту тему перенести в отдельную ветку...
 

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

Я ради интереса почитал доки к QSQLTableModel. Так там есть editStrategy (OnFieldChange, OnRowChange, OnManualSubmit), которая определяет, когда сабмитить изменения в базу. OnRowChanged отлавливается при попытке установить данные не текущей редактируемой строки или вставить новую строку. Кстати для OnFieldChange и OnRowChange вставка более одной строки запрещена.

Имхо, для вашего случая надо что-то подобное.
Возможно. Спасибо за идею, подумаю над этим.

Возможно, было бы полезно сделать некий наследник QAbstractTableModel, который бы поддерживал понятие "текущая редактируемая строка".

Угу, вот только у нас, помимо QAbstractTableModel, есть уже готовый ворох ее наследников. И если мы хотим дополнить ее интерфейс для всех случаев — встает проблема, что делать с ними со всеми...
Записан
Eugene Efremov
Гость
« Ответ #13 : Май 03, 2009, 18:44 »

Удалил из статьи вот это вот:

Цитировать
Лирическое отступление №2
Несомненно, читатель уже обратил внимание на уродливую связку swicth/case. Более того, заглянув в исходный код примера, он мог бы обнаружить ту же самую связку в ф-циях headerData и setData. И у него должен был возникнуть закономерный вопрос — почему мы от нее не избавимся? К сожалению, это сделать не так-то легко. Полноценное решение включало бы в себя достаточно глубокую переработку всей имеющейся реализации MVC с избавлением от использования перечислений там, где следует использовать полиморфизм. По понятным причинам, мы этого сделать не можем. Обходные пути существуют, но они достаточно сложны. И их обсуждение выходит далеко за рамки данной статьи.

Лирическое отступление №3
Я уж молчу о том, что многим из этих «ролей» вообще нечего делать в модели, поскольку с их помощью выясняются вопросы, находящиеся сугубо в компетенции представления. Ну какое, спрашивается, имеет отношение к модели цвет фона, размер шрифта и другие подобные вещи?!

Дальнейшее обсуждение этой тематики (если есть желание) предлагаю перенести в отдельную тему (в разделе MVC). Здесь же, думаю, это нужно объявить оффтопиком.


P.S. Также я подредактировал «лирическое отступление» к четвертой и добавил новое — к третьей. Теперь там объясняется, почему insertRows не добавляет в модель данные.
Записан
johnny
Гость
« Ответ #14 : Май 03, 2009, 19:44 »

1. Вы пытаетесь обеспечить вставку произвольного числа строк...

Где? Никакой вставки произвольного числа строк за раз у меня нет. Кроме, конечно, требуемого интерфейсом insertRows.

Дык кто ж мешает, если count > 1 просто возвращать false? Зачем "натягивать" вставку многих записей, если это совершенно не нужно для нашей модели? Именно такое поведение у QSQLTableModel при некоторых настройках...

Цитировать
Возможно, было бы полезно сделать некий наследник QAbstractTableModel, который бы поддерживал понятие "текущая редактируемая строка".

Угу, вот только у нас, помимо QAbstractTableModel, есть уже готовый ворох ее наследников. И если мы хотим дополнить ее интерфейс для всех случаев — встает проблема, что делать с ними со всеми...

Ну на самом деле ни такой уж и ворох - QAbstractListModel (для которой это малоактуально), QAbstractTableModel и QStandartItemModel. Если неохота писать двух наследников - можно извернуться с шаблонным классом, предок которого определяется аргументом шаблона Улыбающийся
Записан
Страниц: [1] 2   Вверх
  Печать  
 
Перейти в:  


Страница сгенерирована за 0.759 секунд. Запросов: 22.