Главная
Новости, анонсы ...


 Опыты
Статьи, исходники
и прочее






Чернопятов Е.А. Использование CListCtrl в режиме "виртуального списка" (вер. 2.0)

  • Вступление
  • Создание и работа с виртуальным списком на основе CListCtrl  
    • обработка LVN_GETDISPINFO
    • редактирование элемента (LVN_ENDLABELEDIT)
    • изменение состояния элемента (NM_CLICK, LVN_KEYDOWN)
    • поиск по списку (LVN_ODFINDITEM)
  • Заключение
  • mini-FAQ
  • Ссылки 

Вступление

Любой программист в своей работе сталкивается с необходимостью хранить данные в виде списков или массивов. Работа с данными также предполагает необходимость отображения их пользователю. Для хранения язык С++ предоставляет множество возможностей - от созданных вручную статических или динамических массивов типа int nArray[1024] до мощных классов библиотеки STL или MFC. Для отображения как правило используются разнообразные списки, типа ListBox, ListCtrl.

Типичный способ работы обычно выглядит так:
- создать массив данных (считать его откуда-нибудь, или сгенерировать - неважно);
- создать элемент для отображения;
- заполнить этот элемент данными;
- при изменении данных в массиве изменить данные в элементе отображения.

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

Хотелось бы, чтобы при любом изменении наших данных (в массиве или списке) они автоматически изменялись в элементе отображения. И это возможно. Некоторые элементы управления в Windows, такие, как ListCtrl , предоставляют нам возможность использования "виртуальных данных", то есть когда данные (или их копии) физически хранятся не в самом элементе (и даже ссылок на них нет), а в пользовательской структуре (массиве, списке, в чем угодно). В самом же элементе происходит только их отображение. Причём сама система берет на себя управление отображением только тех строк данных, которые реально видны пользователю в данный момент, что сильно увеличивает скорость работы. То есть не надо мучительно ждать, пока ползунок на скролл-баре листа наконец-то превратится в тоненькую полоску и список таки заполнится этими 65535 фамилиями, именами, отчествами и т.п. :)

CListCtrl

Рассмотрим использование "виртуальных данных" на примере элемента управления CListCtrl . В терминологии MSDN такой элемент управления называется "virtual list control". В дальнейшем я буду называть его для простоты "виртуальным списком ".
Чтобы сделать из обычного CListCtrl виртуальный список достаточно задать ему стиль LVS_OWNERDATA и перехватывать несколько нотификационных сообщений (notification messages). Кстати, использование виртуального списка позволяет увеличить количество элементов в CListCtrl с int до DWORD. В качестве примера программы, которая использует виртуальный списки, можно привести FileRearranger

Итак, пусть у пользователя есть внутреннее хранилище данных - БД, большой массив или что-нибудь ещё. Введём понятие элемента данных , скажем, класс CListDataItem будет одним элементом массива. 

// единица пользовательских данных
class CListDataItem : public CObject
{	
public:
	// data
	CString m_sField1;	// поле данных 
	CString m_sField2;	// поле данных 
	CString m_sField3;	// поле данных 
	BOOL	m_bOn;	// состояние элемента (вкл/выкл) 
	int	m_nIcon;	// иконка элемента в списке 

	// ctor/dtors
	CListDataItem(){
		m_bOn = FALSE;
		m_nIcon = 0;
	};

	CListDataItem(const CListDataItem& a) {
		CopyFrom(a);
	};

	CListDataItem& operator =(const CListDataItem& a) {
		CopyFrom(a);
		return *this;
	};

	// copy helper
	void CopyFrom(const CListDataItem& a) {
		m_sField1 = a.m_sField1;
		m_sField2 = a.m_sField2;
		m_sField3 = a.m_sField3;
		m_bOn = a.m_bOn;		
		m_nIcon = a.m_nIcon;
	};

	~CListDataItem() {};
};

сам массив определим как:

typedef CArray < CListDataItem,CListDataItem& > CListData;

С CListData пользователь производит всевозможные манипуляции - добавление, исправление, удаление записей, и содержимое массива данных автоматически отображается в виртуальном списке.

В качестве интерфейса программы выберем обычное dialog-based приложение, с одной главной формой, элементом управления типа CListCtrl и кнопками для выполнения типичных задач. 

Пример формы

Для установки стиля LVS_OWNERDATA у CListCtrl используем окно свойств Resource View.

Изменение стиля на LVS_OWNERDATA

 

Обработка LVN_GETDISPINFO

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

LVN_GETDISPINFO

В коде получим следующие изменения:

 - в .h добавится строка
afx_msg void OnLvnGetdispinfoList(NMHDR *pNMHDR, LRESULT *pResult);
 - в .cpp в карте сообщений 
ON_NOTIFY(LVN_GETDISPINFO, IDC_LIST, OnLvnGetdispinfoList)
и собственно тело функции
void CVirtualListDlg::OnLvnGetdispinfoList(NMHDR *pNMHDR, LRESULT *pResult)
{
  NMLVDISPINFO *pDispInfo = reinterpret_cast<NMLVDISPINFO*>(pNMHDR);
  LV_ITEM* pItem = &(pDispInfo)->item; // сразу получим указатель на LV_ITEM
  int iItemIndex = pItem->iItem; // и индекс, для наглядности
  
  *pResult = 0;
}

Согласно MSDN , получив сообщение LVN_GETDISPINFO , необходимо проверить, какую именно информацию запрашивает от нас система. Информацию следует поместить в соответствующее поле структуры LV_ITEM. Возможными значениями являются:

  • LVIF_TEXT   Неоходимо заполнить pszText.
  • LVIF_IMAGE   Неоходимо заполнить iImage.
  • LVIF_INDENT   Неоходимо заполнить iIndent.
  • LVIF_PARAM   Неоходимо заполнить lParam.
  • LVIF_STATE   Неоходимо заполнить state.

В нашей программе для каждого элемента списка отображаются: состояние (вкл/выкл), иконка, и содержимое в несколько колонок.

Пример списка с данными

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

void CVirtualListDlg::OnLvnGetdispinfoList(NMHDR *pNMHDR, LRESULT *pResult)
{
	NMLVDISPINFO *pDispInfo = reinterpret_cast<NMLVDISPINFO*>(pNMHDR);
	LV_ITEM* pItem = &(pDispInfo)->item;  // сразу получим указатель на LV_ITEM 
	int iItemIndex = pItem->iItem;  // и индекс, для наглядности 
  
	
	CListDataItem DataItem = m_arrMyData[iItemIndex];

	// от нас хотят текст? 
	if(pItem->mask & LVIF_TEXT)
	{
		if(pItem->iSubItem == 0) //первая колонка
		{			
			lstrcpyn(pItem->pszText, DataItem.m_sField1, pItem->cchTextMax);
		}
		else	
			if(pItem->iSubItem == 1) //вторая колонка
			{
				lstrcpyn(pItem->pszText, DataItem.m_sField2, pItem->cchTextMax);
			}
			else
				if(pItem->iSubItem == 2) // третья колонка
				{
					lstrcpyn(pItem->pszText, DataItem.m_sField3, pItem->cchTextMax);
				}		
	}
	// от нас хотят изображение?
	if(pItem->mask & LVIF_IMAGE)
	{
		// Если у нас ListCtrl c иконками - то в pItem->iImage присваиваем номер иконки из 
		// CImageList для этого элемента массива данных
		pItem->iImage = DataItem.m_nIcon;

		// Чек-бокс у нас - тоже изображение, и  
		// чтобы использовать изображение чек-бокса, необходимо задействовать "state mask"
		pItem->mask |= LVIF_STATE;
		pItem->stateMask = LVIS_STATEIMAGEMASK;
		
		if(DataItem.m_bOn)
		{
			// если включен - отображать чек-бокс с галочкой
			pItem->state = INDEXTOSTATEIMAGEMASK(2);
		}
		else 
		{
			// иначе отображать пустой чек-бокс
			pItem->state = INDEXTOSTATEIMAGEMASK(1);
		}
	}

	*pResult = 0;
}

Это необходимый минимум для того, чтобы заставить наш CListCtrl вести себя как виртуальный список. Однако прямо сейчас он работать не будет. В самом деле, ну откуда CListCtrl узнает, сколько данных у нас в массиве, когда они меняются, когда нам надо принудительно отрисовать список при их изменении и т.д... Для информирования об этом элемента управления служит функция void CListCtrl::SetItemCount(int nItems). Её и следует вызывать каждый раз, когда количество элементов в массиве изменяется, или мы хотим принудительно перерисовать список при изменении содержимого одного или нескольких элементов массива (если этого не сделать, придется ждать, пока система сама не решит, что пора перерисовать список, например, при перекрытии его другим окном). В нашем случае вызов будет выглядеть, например, так:

// добавить 5 строк
void CVirtualListDlg::OnBnClickedButtonAdd()
{
	srand( (unsigned)time( NULL ) );

	// добавляем по 5 строк данных во внутрений массив
	for (INT_PTR i= 0;i<5 ;i++)
	{
		CListDataItem item;
		item.m_sField1.Format(_T("%d"), rand() ); 
		item.m_sField2.Format(_T("%d"), rand() ); 
		item.m_sField3.Format(_T("%d"), rand() ); 
		item.m_bOn = FALSE;
		item.m_nIcon = (int)i;

		m_arrMyData.Add(item);
	}
	// скажем  списку, сколько у нас теперь данных
	m_lstVirtualList.SetItemCount((int)m_arrMyData.GetCount());
}

Если произошло изменение в уже существующих данных, и заранее известно, какие именно элементы требуют перерисовки - можно использовать функцию  BOOL CListCtrl::RedrawItems(int nFirst, int nLast). Но если произошло добавление/удаление данных - следует использовать void CListCtrl::SetItemCount(int nItems).

Вот теперь всё будет работать.

 

Редактирование элемента (LVN_ENDLABELEDIT)

Можно ли в виртуальном списке редактировать элементы? В обычном списке это делается путем добавления стиля LVS_EDITLABELS, и перехватом сообщений LVN_BEGINLABELEDIT и LVN_ENDLABELEDIT . В виртуальном списке это тоже работает. Надо только помнить о том, что мы должны изменить содержимое нашего внутреннего массива согласно введенным пользователем данныv, в отличие от обычного списка, где мы меняем строки непосредственно в списке.
Позволю себе напомнить, что в  CListCtrl есть возможность редактировать только первую колонку. Если надо редактировать другие колонки - придется писать свой код или поискать что-нибудь подходящее в   сети
Для нашего примера нам вполне хватит перехвата сообщения LVN_ENDLABELEDIT. По его получении будем изменять данные в массиве (не забудьте добавить стиль LVS_EDITLABELS нашему списку: ).

// обработка изменения данных пользователем
    
void CVirtualListDlg::OnLvnEndlabeleditList(NMHDR *pNMHDR, LRESULT *pResult)
{
	NMLVDISPINFO *pDispInfo = reinterpret_cast(pNMHDR);
	LVITEM item = pDispInfo->item;

	// пользователь отменил ввод
	if (item.pszText == NULL)
	{
		*pResult = 0; // "0" информирует систему о неуспешном вводе
		return;
	}

	// пользователь произвёл ввод, надо обновить наши данные
	m_arrMyData[item.iItem].m_sField1 = item.pszText;
	
	*pResult = 1; // "1" информирует систему об успешном вводе
}

В принципе, внутри обработчика OnLvnEndlabeleditList можно производить проверку на корректность введенных данных, и в случае их несоответствия, например, нужному диапазону, искусственно отменять их ввод.

 

Изменение состояния элемента (NM_CLICK, LVN_KEYDOWN)

Очень часто в в списках требуется как-то отмечать элементы, "включать/выключать" их. В MFC есть готовый класс с такой функциональностью CCheckListBox. Однако CListCtrl оной лишен, как в обычном, так и в виртуальном режиме. 
Но это лишь на первый взгляд. "Список с чек-боксами" можно реализовать, используя возможность списка отображать т.н. "состояние" (state) элемента. Состояние и будет означать - "включен/выключен" конкретный элемент, или нет (тавтология какая-то...). Интерактивность, то есть "переключение" состояния элемента пользователем можно реализовать, отлавливая события от мыши или клавиатуры, и реагируя на них:

  • Для отображения состояния "состояния" элемента нарисуем 2 картинки - отмеченный чек-бокс и неотмеченный чекбокс. Для этого в редакторе ресурсов создаем битмап, например 32х16, вот такой:Изменение стиля на LVS_OWNERDATA
  • задаем ему какой-нибудь понятный идентификатор, например IDB_BITMAP_CB
  • в объявление класса диалога добавляем переменную типа "список изображений"
    CImageList m_ilStates;
  • в OnInitDialog прописываем создание списка изображений 
    m_ilStates.Create(IDB_BITMAP_CB, 16, 1, RGB(1,1,1));
  • и указываем, что эти изображения будут использоваться для отображения состояния (state) элементов списка
    m_lstVirtualList.SetImageList(&m_ilStates,LVSIL_STATE);

Код для отображения состояния у нас уже есть (в обработчике OnLvnGetdispinfoList). Теперь надо прописать реакцию на клавиатуру и мышь. Мы должны отследить нажатие левой кнопки мыши, и если оно попадает в "чек-бокс", то нам необходимо изменить состояние соответствующего элемента во внутреннем массиве, и переотобразить строку этого элемента. Для клавиатуры мы пропишем реакцию на нажатие клавиши "Пробел" на строке.

  • Создаем обработчик на NM_CLICK
    Пример списка с данными
  • В обработчике проверяем, на какую часть строки пришелся клик
  • Если это "чек-бокс", то изменяем состояние элемента в массиве и перерисовываем список
  • Код:
// обработка щелчка мыши для переключения состояния
void CVirtualListDlg::OnNMClickList(NMHDR *pNMHDR, LRESULT *pResult)
{
	NMLISTVIEW* pNMListView = (NM_LISTVIEW*)pNMHDR;

	LVHITTESTINFO hi;	
	hi.pt = pNMListView->ptAction;
	
	//проверить, куда попал клик
	int nItem = m_lstVirtualList.HitTest(&hi); 

	if(nItem != -1)
	{
		// Попали в строку, проверить, попали ли мы в иконку состояния?
		// (этот прием работает только для списка в режиме list или report)
		// Кроме того, если для списка установлен стиль LVS_EX_FULLROWSELECT,
		// то щелчок на других колонках тоже будет засчитан как переключение состояния!

		if( (hi.flags & LVHT_ONITEMSTATEICON) != 0)
		{
			ToggleState(nItem);// переключить состояние элемента в массиве
		}
	}


	*pResult = 0;
}											

Для переключения по пробелу (тут попроще):

// обработка нажатия клавиши для переключения состояния
void CVirtualListDlg::OnLvnKeydownList(NMHDR *pNMHDR, LRESULT *pResult)
{
	LPNMLVKEYDOWN pLVKeyDown = reinterpret_cast(pNMHDR);
	
	// будем переключаться по пробелу
	if( pLVKeyDown->wVKey == VK_SPACE )
	{		
		{
			// переключаем первый из выбранных (если они есть)
			if(m_lstVirtualList.GetSelectionMark() != -1)
				ToggleState(m_lstVirtualList.GetSelectionMark()); 
		}
	}

	*pResult = 0;
}

 

Поиск (перевод из MSDN + "Using virtual lists" (c) by PEK)

Если виртуальному списку требуется найти конкретный элемент, он посылает нотификационное сообщение LVN_ODFINDITEM. Это происходит, например, если пользователь пытается найти элемент в CListCtrl, набирая его название по первым буквам, или список получает сообщения LVM_FINDITEM. Информация для поиска посылается в виде структуры LVFINDINFO, которая, в свою очередь, является членом структуры NMLVFINDITEM. Программисту необходимо обрабатывать это сообщения путем переопределения метода  OnChildNotify, принадлежащего пользовательскому списку. Метод должен проверять, пришло ли ему сообщение LVN_ODFINDITEM. Если да, то необходимо предпринять адекватные действия.
При использовании виртуальных списков всегда надо быть готовым к тому, что пользователь захочет осуществить поиск в списке. Следует возвращать индекс искомого элемента при нахождении его, или -1, если такой элемент не найден.
 

Это был MSDN. А теперь тоже самое, но по-человечески. Когда пользователь начинает искать строку в списке, набирая её начало по буквам, списку начинают приходить сообщения LVN_ODFINDITEM (обрабочик будет выглядеть так - void CVirtualListDlg::OnLvnOdfinditemList(NMHDR *pNMHDR, LRESULT *pResult)). Причем на каждое нажатие клавиши приходит сообщение, содержащее набираемую строку буква за буквой. То есть если мы ищем строку, содержащую слово "Anna", нам придут 4 сообщения, содержащих в параметрах структуры NMLVFINDITEM сначала A, потом An, потом Ann, и наконец Anna по мере набора слова. Если подходящая строка найдена - информируем об этом систему, возвращая в параметре обработчика LRESULT *pResult индекс найденной строки. Если не найдена - возвращаем селектор к элементу, с которого начали поиск, а в pResult возвращаем "-1".
Пример:
Пусть у нас есть список (взято из статьи Using virtual lists by PEK)

  • Anders 
  • Anna
  • Annika
  • Bob
  • Emma
  • Emmanuel

Логика нашей работы по выбору (подсветке) строки при этом будет следующей:

  • Если набрано "A" - надо выбрать Annika
  • Если набрано "AND" - надо выбрать Anders
  • Если набрано "ANNK" - подсветка должна остаться на Anna
  • Если набрано "E" - надо выбрать Emma

Посмотрим, как это всё будет выглядеть в коде:

// поиск набираемой с клавиатуры строки по списку 
// (взято целиком из примера на http://www.codeproject.com/listctrl/virtuallist.asp
// "Using virtual lists" by (с)PEK)
void CVirtualListDlg::OnLvnOdfinditemList(NMHDR *pNMHDR, LRESULT *pResult)
{
	LPNMLVFINDITEM pFindInfo = reinterpret_cast(pNMHDR);
		
	// Значение по умолчанию "-1", это означает,
	// что элемент, содержащий искомую строку, не найден 
	*pResult = -1;
	

	// Поиск должен осуществляться по строке 
		if((pFindInfo->lvfi.flags & LVFI_STRING) == 0 ) {
		return;		
		}
	// Строка которую набирает пользователь, и которую мы ищем в нашем списке 
	CString searchstr = pFindInfo->lvfi.psz;

	
	/* В pFindInfo->iStart находится индекс элемента, с которогомы начинаем поиск.
	   Мы ищем до конца спика, а затем перестаруем с начала и снова доходим до 
	   pFindInfo->iStart, за исключением случая, когда мы нашли требеумый элемент 
	   (содержащий искомую строку)
	 */

	int startPos = pFindInfo->iStart;
	// Если индекс "startPos" находится вне списка (например, если выбран последний элемент), то сделать его первым
	if(startPos >= m_lstVirtualList.GetItemCount())
		startPos = 0;

	int currentPos=startPos;
	
	//начало поиска
	do
	{		
		// проверяем, содержит ли очередной элемент массива все символы из строки поиска?
		if( _tcsnicmp(m_arrMyData[currentPos].m_sField1, searchstr, searchstr.GetLength()) == 0) {
			// Если да, то выбрать этот элемент и прекратить поиск.
			*pResult = currentPos;
			break;
		}

		// взять следующий 
		currentPos++;

		// Надо ли перестартовать с начала списка?
		if(currentPos >= m_lstVirtualList.GetItemCount())
			currentPos = 0;

	// Остановиться, если достигли элемента, с которого начинали поиск 
	}
	while(currentPos != startPos);		
}
									
									

 

Заключение

Если идея использовать виртуальный список не впечатлила - подумайте вот о чём:

  • Что, если нам необходимо вручную сортировать список? Например, у нас есть кнопки "Передвинуть вверх" и "Передвинуть вниз".
    Что проще - один раз произвести перемещение в массиве данных, или два раза - и в массиве, и в списке?
  • Что, если список огромен, например, результат запроса к БД ?
  • Что, если мы читаем изображения с диска и выводим на экран эскизы? Да еще и манипулируем ими :)???
  • Что, если.....

Слишком много "если". Используйте виртуальные списки!

 

mini-FAQ

  • Как сделать "сетку" для списка?
  • m_lstList.SetExtendedStyle(m_lstList.GetExtendedStyle()|LVS_EX_GRIDLINES);

 

  • Как сделать выбор строки целиком в списке?
  • m_lstList.SetExtendedStyle(m_lstList.GetExtendedStyle()|LVS_EX_FULLROWSELECT);

 

  • Как кешировать элементы?
  • Кеширование может понадобиться, если подготовка очередного элемента занимает существенно время. Система посылает списку сообщение LVN_ODCACHEHINT, в параметрах которого указывается диапазон элементов, содержимое которых потребуется отобразить в ближайшее время. Таким образом, пользователь может заранее подготовить их (сгенерировать или подгрузить, например, в случае изображений).
    Обработчик может выглядеть примерно так (хотя MSDN советует переопределять OnChildNotify, и если в него пришло LVN_ODCACHEHINT, производить его обработку):
void CVirtualListDlg::OnOdcachehintList(NMHDR* pNMHDR, LRESULT* pResult) 
{
	NMLVCACHEHINT* pCacheHint = (NMLVCACHEHINT*)pNMHDR;

	TRACE(	_T("Надо кешировать элементы с %d по %d\n"),
			pCacheHint->iFrom,
			pCacheHint->iTo );
	

	*pResult = 0;
}

Более подробно см. разделы "Caching and Virtual List Controls" и "Using List-View Controls" в MSDN.

 

Ссылки

 

Оглавление

Последние изменения от 09.08.2013




 

e-mail:  Yegor A. Blackheel

Поиск по сайту с помощью Yandex

 www.guestbook.ru - лучший сервер гостевых книг