× Aktuálně z oboru

SHIELD Experience Upgrade 7 – méně hledání a více zábavy [ clanek/2018052902-shield-experience-upgrade-7-mene-hledani-a-vice-zabavy/ ]
Celá zprávička [ clanek/2018052902-shield-experience-upgrade-7-mene-hledani-a-vice-zabavy/ ]

Aplikace ve Win32 API - 'průzkumník' (pokračování)

[ http://programujte.com/profil/17127-libor-benes/ ]Google [ ?rel=author ]       [ http://programujte.com/profil/14523-martin-simecek/ ]Google [ ?rel=author ]       29. 3. 2010       21 908×

Jednoduchou aplikaci vytvořenou v minulém díle poněkud rozšíříme, aby alespoň trochu lahodila oku. Přidáme si menu, tlačítkovou lištu, stavový řádek a trochu funkce.

V předchozí části jsme si vytvořili jednoduchou aplikaci, "průzkumníka", která je opravdu velmi strohá. V tomto díle si přidáme menu, ze kterého budeme ovládat zobrazení tlačítkové lišty a stavového řádku. Na tlačítkovou lištu umístíme jedno tlačítko pro ukončení aplikace a druhé pro pohyb "o úroveň výše". Do stavového řádku budeme vypisovat aktuální úplnou cestu adresáře, jehož obsah se právě v ListView zobrazuje. A ještě si přidáme možnost procházení adresářové struktury přímo z ListView, tj. otevření podadresáře a možnost zobrazit obsah nadřazeného adresáře.

Ze všeho nejdříve si položky v ListView setřídíme. Ve výsledku by měl být první "adresář", který umožní zobrazit obsah nadřazeného adresáře (".."), dále by měly být adresáře seřazeny podle abecedy a následovat by je měly soubory (také setříděné podle abecedy). Obsah ListView lze nechat setřídit automaticky nastavením vlastnosti LVS_SORTASCENDING nebo LVS_SORTDESCENDING pro setřídění vzestupně, resp. sestupně podle jména při vytváření instance ListView. To bohužel není náš případ, my potřebujeme položky ještě rozlišovat podle typu (adresář/soubor). Proto si musíme třídicí funkci napsat sami (a také ji sami ve vhodnou dobu volat).

Třídění bude tedy probíhat nejprve podle typu položek a následně podle jména položek. Na uživatelské třídění položek slouží volání funkce:

ListView_SortItems(HWND hwnd, PFNLVCOMPARE pfnCompare, LPARAM lParamSort)

Neboli v našem případě funkce z třídy CMyListView:

BOOL CMyListView::SortItems(PFNLVCOMPARE pfnCompare, LPARAM lParamSort)

Parametr pfnCompare je adresa třídicí funkce a lParamSort je parametr, který je této funkci předán při každém volání této funkce (často se předává this).

Třídicí funkce musí mít následující tvar:

int CALLBACK CompareFunc(LPARAM lParam1, LPARAM lParam2, LPARAM lParamSort)

Parametr lParamSort je ten, který jsme předali při volání SortItems. Parametry lParam1 a lParam2 jsou data přiřazená každé položce v ListVIew. Tedy nikoliv indexy nebo jména, ale data přiřazená jednotlivým položkám. Způsobů, jak dosáhnout potřebného výsledku, je mnoho. V tomto případě (z ukázkových účelů nevyužijeme C++ list) použijeme ukládání informací o jednotlivých položkách do spojitého jednosměrného seznamu a každé položce v ListView předáme pointer na buňku s informacemi o ní.

Každá buňka seznamu bude tedy obsahovat jméno položky (řetězec) a typ položky (číslo - stačí byte). Definice struktury bude tedy následující:

typedef struct _LIST_VIEW_STRUCT {
	// jmeno polozky
	wchar_t				wzName[MAX_PATH];
	// typ polozky (0x00 - prazdna, 0x01 - soubor, 0xFF - adresar)
	BYTE				nType;
	// ukazatel na dalsi polozku seznamu (nebo NULL)
	_LIST_VIEW_STRUCT*	pNext;

} LIST_VIEW_STRUCT;

Vzhledem k tomu, že obsah ListView bude vždy jen jeden, tak nám stačí před každým naplněním ListView (a tedy i buněk tohoto seznamu) obsah seznamu vyčistit a nechat znovu naplnit. Omezíme tím neustálé alokace paměti. Seznam definujeme jako členskou proměnnou třídy panelu ListView (ListViewPanel):

LIST_VIEW_STRUCT		m_sItemList;

Tím se dostáváme i k funkcím nad tímto seznamem. Budeme potřebovat vložení, vymazání a nakonec celkové uvolnění paměti:

LIST_VIEW_STRUCT* CListViewPanel::InsertListItem(LPCWSTR pwzName, BYTE nType)
{
	LIST_VIEW_STRUCT*	pNext	= &m_sItemList;
	LIST_VIEW_STRUCT*	pLast;
	// projdi polozky seznamu
	while(pNext != NULL) {
		pLast = pNext;
		// zjisti, ktera je prazdna, a do ni to uloz
		if (pNext->nType == 0) {
			wcscpy_s(pNext->wzName, MAX_PATH, pwzName);
			pNext->nType = nType;
			return(pNext);
		}

		pNext = pNext->pNext;
	}
	// pokud nebyla zadna prazdna, tak v pLast je posledni polozka seznamu a za ni pridej dalsi
	if ((pLast->pNext = new LIST_VIEW_STRUCT) == NULL) return(NULL);
	// a do ni to uloz
	pNext		= pLast->pNext;
	pNext->pNext	= NULL;
	pNext->nType	= nType;
	wcscpy_s(pNext->wzName, MAX_PATH, pwzName);

	return(pNext);
}


void CListViewPanel::ClearList()
{
	LIST_VIEW_STRUCT*	pNext	= &m_sItemList;
	// projdi vsechny polozky seznamu a vycisti je
	while(pNext != NULL) {
		wcscpy_s(pNext->wzName, MAX_PATH, L"");
		pNext->nType = 0;
		pNext = pNext->pNext;
	}
}


void CListViewPanel::FreeList()
{
	LIST_VIEW_STRUCT*	pNext	= m_sItemList.pNext;
	LIST_VIEW_STRUCT*	pTemp;
	// pojdi vschny polozky seznamu
	while(pNext != NULL) {
		// ukladej si nasledovniky
		pTemp = pNext->pNext;
		// uvolnuj pamet soucasny polozkam
		delete pNext;
		// a pokracuj na ulozenem nasledovnikovi
		pNext = pTemp;
	}
	// samozrejme vynuluj i prvni (statickou) polozku - jako v konstruktoru
	m_sItemList.nType	= 0;
	m_sItemList.pNext	= NULL;
	wcscpy_s(m_sItemList.wzName, MAX_PATH, L"");
}

A samozřejmě nesmíme zapomenout seznam inicializovat v konstruktoru:

CListViewPanel::CListViewPanel() : CMyPanel()
{		
	m_sItemList.nType	= 0;
	m_sItemList.pNext	= NULL;
	wcscpy_s(m_sItemList.wzName, MAX_PATH, L"");
}

Nyní máme seznam, do kterého vždy uložíme zobrazené položky. Pointer na uložené informace (buňku) přiřadíme každé položce ListView jako její data. Rozlišuje vložené položky na adresáře a soubory. Seznam položek v daném adresáři projdeme stejně jako v předchozím díle, pouze doplníme uložení dat jednotlivým položkám seznamu a přestaneme odfiltrovávat speciální adresář "..":

LIST_VIEW_STRUCT*	pItem;

...

if ((sFindData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) == FILE_ATTRIBUTE_DIRECTORY) {
	if  ((wcscmp(sFindData.cFileName, L".") != 0)) {
		pItem = InsertListItem(sFindData.cFileName, 0xFF);
		m_hListView.InsertItem(nCount++, sFindData.cFileName, 0, (LPVOID)pItem);
	}
}
else {
	pItem = InsertListItem(sFindData.cFileName, 0x01);
	m_hListView.InsertItem(nCount++, sFindData.cFileName, 1, (LPVOID)pItem);
}

A po naplnění seznamu je vhodná chvíle na setřídění položek, tj. zavolání SortItems. V našem případě nepotřebujeme předávat žádný parametr (proto NULL), ale jak jsem zmínil výše, často se předává this. To se dělá z toho důvodu, aby se třídící funkce dostala k instančním proměnným a funkcím dané třídy, neboť třídicí funkce musí být definována jako statická (v době překladu musí být známa její adresa). Třídění tedy spustíme zavoláním SortItems a jako parametr předáme naši třídicí funkci:

m_hListView.SortItems(CompareFunc, NULL);

Třídicí funkce není nijak složitá. Funguje na podobném principu jako funkce strcmp (wcscmp), tj. vrací 0, pokud jsou položky stejné, -1, pokud má první položka předcházet druhou, a 1, pokud je tomu naopak. Musíme dodržet 3 zásady:

  • první je vždy speciální adresář ".."
  • adresáře vždy předcházejí soubory
  • adresáře/soubory jsou setříděné podle abecedy (bez rozlišení velikosti písmen)

Třídicí funkce bude tedy vypadat následovně:

int CALLBACK CListViewPanel::CompareFunc(LPARAM lParam1, LPARAM lParam2, LPARAM lParamSort)
{
	LIST_VIEW_STRUCT*	pItem1	= (LIST_VIEW_STRUCT*)lParam1;
	LIST_VIEW_STRUCT*	pItem2	= (LIST_VIEW_STRUCT*)lParam2;

	// adresare jsou vzdy nad soubory
	if (pItem1->nType < pItem2->nType) return(1);
	if (pItem1->nType > pItem2->nType) return(-1);	

	// specialni adresar ".." je vzdy prvni
	if (wcscmp(pItem1->wzName, L"..") == 0) return(-1);
	if (wcscmp(pItem2->wzName, L"..") == 0) return(1);

	// jinak tridime podle jmena souboru/adresaru
	return(_wcsicmp(pItem1->wzName, pItem2->wzName));
}

Nyní máme položky setříděné a ještě tomuto seznamu rychle vdechneme život. Budeme zachytávat notifikaci při aktivaci (dvojklik apod.) položky ListView - LVN_ITEMACTIVATE. Využijeme toho, že v datech položky je její typ i název. U adresářových položek přepošleme jejich název do hlavní okna (rodičovského) opět jako WM_COMMAND:

if (pItem->nType == 0xFF) {
	::SendMessage(GetParent(m_hWnd), WM_COMMAND, MAKEWPARAM(ID_LIST_VIEW, 1), (LPARAM)pItem->wzName);
}

V hlavním okně tuto zprávu předáme do TreeView, kde na ni budeme reagovat podobně, jako by došlo k požadavku na prohlížení obsahu adresáře přímo z TreeView. V případě, že se jedná o speciální adresář "..", tak provedeme výběr nadřazené položky:

hItem = m_hTreeView.GetSelection();

if (wcscmp(pwzItem, L"..") == 0) {
	hNext = m_hTreeView.GetParent(hItem);
	m_hTreeView.SelectItem(hNext);
	return;
}

Pro ostatní adresáře expandujeme aktuální výběr, projdeme všechny podsložky a vybereme tu, pro kterou byla změna adresáře z ListView volána:

m_hTreeView.Expand(hItem, TVE_EXPAND);

hNext = m_hTreeView.GetChild(hItem);
while(hNext != NULL) {
	sItem.mask			= TVIF_TEXT | TVIF_HANDLE;
	sItem.hItem			= hNext;
	sItem.pszText		= wzItem;
	sItem.cchTextMax	= MAX_PATH;

	if ((m_hTreeView.GetItem(&sItem)) && (wcscmp(wzItem, pwzItem) == 0)) {
		m_hTreeView.SelectItem(hNext);
		return;
	}
	
	hNext = m_hTreeView.GetNextSibling(hNext);
}

Nyní se lze pohybovat v adresářové struktuře i přes ListView. Tuto oblast teď opustíme a dáme se do přidávání menu, tlačítkové a stavové lišty.

Nejjednodušší je menu, které stačí definovat jako součást resource (.rc) souboru a následně předat jeho ID (IDC_MAIN_MENU) při vytváření hlavního okna aplikace. Funkce menu bude v tomto případě úzce spjata s tlačítkovou a stavovou lištou. Mimo ovládání zobrazení těchto dvou lišt bude z menu možné už jenom aplikaci uzavřít.

Obě lišty vykazují jisté společné vlastnosti (jde o "proužek" v aplikaci, lze je schovávat/zobrazovat atd.), takže si pro ně vytvoříme společnou třídu (CMyBar), která zapouzdří společné vlastnosti a vytvoří společné rozhraní. Jedná se o vytvoření lišty, změnu a detekce stavu viditelnosti a zrušení lišty. Dále si do naší hierarchie tříd přidáme dceřiné třídy CMyToolbar a CMyStatusbar, které budou obsahovat obecné věci týkající se daných lišt. Všechno ostatní musíme implementovat v dceřiných třídách (v aplikaci), které budou obsahovat i iniciliazaci lišt apod. Z kódu třídy CMyBar si zde uvedeme jako příklad změnu stavu viditelnosti (s výpočtem výšky lišty):

BOOL CMyBar::ChangeViewStatus()
{
	if (m_hBar == NULL) return(FALSE);
	// je-li vyska > 0, pak je lista zobrazena -> musime ji skryt
	if (m_nHeight > 0) {		
		ShowWindow(m_hBar, SW_HIDE);
		m_nHeight = 0;
	}
	// jinak neni zobrazena -> musime ji zobrazit
	else {
		RECT rtBar;
		ShowWindow(m_hBar, SW_SHOW);
		GetWindowRect(m_hBar, &rtBar);
		m_nHeight = rtBar.bottom - rtBar.top;
	}
	return(TRUE);
}

Z třídy CMyStatusbar a CMyToolbar si zde jako příklad uvedeme nastavení textu ve stavovém řádku a změnu stavu tlačítka:

BOOL CMyStatusbar::SetBarText(LPCWSTR pwzText)
{
	if (m_hBar == NULL) return(FALSE);	

	return(SendMessage(m_hBar, SB_SETTEXT, (WPARAM)0, (LPARAM)pwzText));
}

...

void CMyToolbar::EnableButton(UINT nButtonID, BOOL bEnabled)
{
   if (m_hBar == NULL) return;

   SendMessage(m_hBar, TB_ENABLEBUTTON, (WPARAM)nButtonID, (LPARAM)MAKELONG(bEnabled, 0));
}

Všechny další specializované věci (dané aplikací) je nutné implementovat až v aplikaci. Z těchto tříd si zde jako příklad uvedeme vytvoření statvové lišty, kterou v tomto případě tvoří jedna část přes celou délku stavového řádku:

BOOL CStatusbar::BarCreate()
{
	INT  			nParts[1];
	HINSTANCE	hInstance	= (HINSTANCE)::GetWindowLong(m_hParentWnd, GWL_HINSTANCE);

	if (m_hParentWnd == NULL) return(FALSE);

	InitCommonControls();

	m_hBar = CreateStatusWindow(WS_CHILD | WS_VISIBLE, L"", m_hParentWnd, IDC_SB_STATUSBAR);

	if (m_hBar == NULL) return(FALSE);

	// az do konce -> -1
	nParts[0]	= -1;	

	SendMessage(m_hBar, SB_SETPARTS, (WPARAM)1, (LPARAM)&nParts);

	return(TRUE);
}

Vytvoření lišt provedeme, stejně jako ostatní prvky, v OnCreate. Celková velikost zobrazené plochy se zmenší o velikost jednotlivých lišt. To musíme zohlednit i v reakci na změnu velikost okna:

rtRect.top += m_hToolbar.GetBarHeight();
rtRect.bottom -= m_hStatusbar.GetBarHeight();

Samozřejmě s tím, že tlačítková lišta je zobrazena v horní části a stavový řádek v dolní části.

Změnu viditelnosti (ovládanou z menu) můžeme provést přes společné rozhraní dané CMyBar. Musíme změnit stav dané lišty, změníme zaškrtnutí v menu a zavoláme přepočítání velikosti okna:

void CMainView::OnShowBar(CMyBar* pBar, UINT nMenuID)
{	
	HMENU	hMenu	= GetMenu(m_hWnd);

	pBar->ChangeViewStatus();
			
	CheckMenuItem(hMenu, nMenuID, pBar->IsShowed());

	OnSize(0, 0, 0);
}

Ještě nám zbývá reagovat na událost od tlačítka "adresář výše" a do stavového řádku vypisovat adresář, jehož obsah je v ListView aktuálně zobrazen. Reakce na tlačítko bude velmi jednoduchá. Opět využijeme stávajícího kódu, tj. do TreeView odešleme jako parametr speciální adresář "..":

m_hTreeViewPanel.ChangeItem(L"..");

Stejně jednoduché to bude i v případě stavové lišty. Ve chvíli předávání aktuálního adresáře z TreeView do ListView (přes MainView) můžeme tento adresář vypsat:

m_hStatusbar.SetBarText((LPCWSTR)lParam);

To je pro tentokráte vše. Zdrojový kód aplikace je ke stažení zde [ http://programujte.com/storage/mydll_5.zip ]. V dalším díle si aplikaci ještě trochu rozšíříme - vytvoříme si záložky na "otevřené" soubory, tj. použijeme TabCtrl.


Článek stažen z webu Programujte.com [ http://programujte.com/clanek/2010030101-aplikace-ve-win32-api-pruzkumnik-pokracovani/ ].