В этой статье я расскажу о том как реализовать перемещение нескольких элементов контейнера за раз. Такая необходимость, например, возникает, когда пользователь выделяет несколько, в общем случае несмежных, элементов в QTableView и перетаскивает их в пределах таблицы. С единичным выделением, все довольно просто: удалил элемент в старой позиции и вставил в новую; кто-то позаботится о случае с перемещением от меньших индексов к большим, когда после удаления элемента в исходной позиции, нужно будет скорректировать индекс назначения, а кто-то сочтет, что сойдет и так.
Например eсть коллекция: 0 1 2 3 4 5
Нужно переместить элемент из позиции 1 в позицию 3 (индексы считаем с нуля)
Удаляем элемент из позиции 1: 0 2 3 4 5
Вставляем его в позицию 3: 0 2 3 1 4 5
Я бы все-таки ожидал, что элемент из позиции 1 будет вставлен перед элементом в изначальной позиции 3. В таком случае результат выглядел бы как: 0 2 1 3 4 5
Возможно это выглядит всего лишь как вопрос личных предпочтений (я как раз недавно спорил с одним человеком на эту тему), но на самом деле это не так.
Например на первом скриншоте cxerimolio будет вставлено перед ribo и это правильно
В то время как на втором скриншоте, к удивлению пользователя cxerizo будет вставлено не перед piro, как того следовало ожидать судя по обозначеной позиции для вставки, а после него.
Поэтому в случае forward move нужно корректировать индекс назначения.
Прежде чем продолжить оговорюсь, что я не буду фокусироваться на реализации Qt модели - той, что наследуется от QAbstractItemModel. Меня интересует только как правильно перемещать элементы в контейнере на уровне бизнес логики, в то время как QAbstractItemModel это всего лишь навсего адаптер между model и view/delegate. Тем не менее бизнес модель должна как-то уведомлять представление о том, что в ней что-то изменилось, поэтому снабдим ее соответствующим сигналом для случая move.
Назовем нашу business model "SomeContainer" - это обьект, содержащий в себе коллекцию неких обьектов (в данном примере используются int'ы) и предоставляющий простейший интерфейс для манипуляций над ними.
Вот так будет выглядеть SomeContainer в нашем простейшем случае:
{
public: //types
typedef std::vector< int > ValueList; //collection of values to be moved typedef size_t IndexType; //type of indices in ValueList typedef std::vector< IndexType > IndexList; //collection of indices in ValueList
public: //methods
explicit SomeContainer( QObject *parent = 0);
//! move an item at srcIndex to the position before the item at destIndex
void moveItem( IndexType srcIndex, IndexType destIndex );
//! move multiple items to the position before the item at destIndex
void moveItems( const IndexList& srcIndices, IndexType destIndex );
//auxiliary methods needed for unit testing purpose only
void push_back( int value ) { m_items.push_back( value ); }
int at( IndexType index ) const { return m_items.at( index ); }
signals:
//!notifies that element at index oldIndex has been moved to the position defined by newIndex
void itemMoved( IndexType oldIndex, IndexType newIndex );
private: //fields
ValueList m_items;
};
Теперь можно перейти к написанию функции move для случая с перемещением одного элемента. Как я уже говорил все довольно-таки просто - удаляем элемент, корректируем индекс назначения, вставляем елемент. Что мне не нравится в этом, так это то, что после удаления элемента все элементы массива следующие за удаленным элементом будут перемещаться влево, а перед его вставкой они снова будут перемещаться, но теперь уже вправо. На самом деле достаточно будет только перемещать элементы между исходным индексом и индексом назначения, причем делать это можно единожды. Нужно только правильно выбрать направление сдвига в зависимости от направления перемещения интересующего нас элемента. Это несколько улучшит производительность, поэтому мы сразу же реализуем данную оптимизацию.
Вот так будет выглядеть наша функция moveItem
Q_ASSERT( destIndex > 0 && destIndex <= m_items.size() ); //validity check for the destination index
//NOTE: m_item.size() is a valid position for the destination index - the item will be moved to the tail
if ( destIndex > srcIndex ) //in case if destIndex < srcIndex --destIndex is not needed as all the items will be shifted to the right
ValueList::iterator srcIt = m_items.begin();
std::advance( srcIt, srcIndex );
ValueList::iterator destIt = m_items.begin(); std::advance( destIt, destIndex );
if ( srcIndex > destIndex )
emit itemMoved( srcIndex, destIndex );
А вот так вот будет выглядеть реализация методов forwardMove и backwardMove. Предположим, что они обьявлены в приватном неймспейсе.
void forwardMove( ValueList::iterator srcIt, ValueList::iterator destIt )
{
//shifting all the items in the range [ sourcIndex + 1, destinationIndex ] to the left
ValueList::iterator afterSrcIt = srcIt;
++afterSrcIt;
ValueList::iterator afterDestIt = destIt;
++afterDestIt;
std::copy( afterSrcIt, afterDestIt, srcIt );
//write the moved item into the destination position
*destIt = movedItem;
//! moves backward the item pointed by srcIt to the position pointed by destIt
void backwardMove( ValueList::iterator srcIt, ValueList::iterator destIt )
{
//shifting all the items in the range [ destinationIndex, sourcIndex - 1 ] to the right
ValueList::iterator afterSrcIt = srcIt;
++afterSrcIt;
//dest < src
std::copy_backward( destIt, srcIt, afterSrcIt );
//write the moved item into the destination position
*destIt = movedItem;
Теперь перейдем к перемещению множества элементов за один раз. Просто вызывать moveItem в цикле нельзя. Во-первых, нужно корректировать позицию вставки после перемещения некоторых элементов. Во-вторых нужно также корректировать исходные индексы.
Например, имеется коллекция: 0 1 2 3 4 5 6 7
Нужно переставить элементы из позиций 0 и 1 в позицию перед элементом 6.
Передвигаем 0 в позицию перед 6, получаем: 1 2 3 4 5 0 6 7
Теперь под индексом 1 находится не 1, а 2 и следовательно нам нужно скорректировать исходный индекс 1 соответствующим образом, чтобы следующая перестановка произошла правильно.
Аналогичные сложности возникают и при перемещении назад, не говоря уже о таких случаях как, например перемещение элементов по индексами 0, 5, 3 с сохранением порядка в позицию перед 4. Ситуация еще больше осложняется тем, что нужно еще корректно уведомлять подписчиков на сигнал itemMoved. В случае неправильных уведомлений программа может даже завершиться аварийно из-за того что PersistentModelIndex'ы в QModelView оказались в неправильном состоянии.
Ниже приведен код функции moveItems, а также код двух вспомогательных функторов, необходимых для ее работы. Предполагается что код функторов расположен в приватном неймспейсе.
template< class T >
struct DecIfInRange
{
: m_minValue( minValue ), m_maxValue( maxValue )
{
}
T operator() ( const T& value ) const
{
T m_minValue;
T m_maxValue;
template< class T >
struct IncIfInRange
{
: m_minValue( minValue ), m_maxValue( maxValue )
{
}
T operator() ( const T& value ) const
{
T m_minValue;
T m_maxValue;
...
void SomeContainer::moveItems( const IndexList& srcIndices, IndexType destIndex )
{
for ( IndexList::iterator it=src.begin(); it != src.end(); ++it )
{
IndexList::iterator it2 = it;
++it2;
if ( *it < destIndex )
{
else if ( *it > destIndex ) //if we move from the right of the destination point the next element should be moved on one index to the right.
{
++destIndex;
Вот собственно и вся реализация.
Исходный код примера, а также юнит тесты доступны для загрузки здесь.