× Aktuálně z oboru

Vychází Game Ready ovladače pro Far Cry 5 [ clanek/2018040603-vychazi-game-ready-ovladace-pro-far-cry-5/ ]
Celá zprávička [ clanek/2018040603-vychazi-game-ready-ovladace-pro-far-cry-5/ ]

Aplikace ve Win32 API - 'průzkumník' a záložky

[ http://programujte.com/profil/17127-libor-benes/ ]Google [ ?rel=author ]       [ http://programujte.com/profil/14523-martin-simecek/ ]Google [ ?rel=author ]       8. 10. 2010       22 411×

V předchozím dílu jsme si do naší aplikace přidali menu a tlačítkovou a stavovou lištu. V tomto díle si rozšíříme aplikaci ještě o jednu komponentu, a to TabCtrl (záložky). A ukážeme si použití víceřádkového editačního pole.

V tuto chvíli naše aplikace umí zobrazovat stromovou strukturu adresářů, vypsat obsah aktuálně vybraného adresáře a procházení mezi adresáři. Se soubory neumí nic jiného než vypsat jejich seznam a to se v dnešním díle změní.

Co lze se soubory dělat? Mohli bychom je třeba mazat :). Lepší bude, když je budeme jen otevírat/zobrazovat. Problém je ovšem v tom, že typů souborů je velké množství, takže v naší aplikaci budeme zobrazovat jen ty nejjednodušší, tj. textové, a ostatní se pokusíme otevřít jinou aplikací, která je k tomu určena (zaregistrována).

Slíbil jsem demonstraci využití komponenty TabCtrl, takže v aplikaci půjde otevřít více souborů najednou a všechny budou "na úrovni" obsahu aktuálního adresáře - na záložkách této komponenty (podobně jako třeba otevřené stránky v prohlížeči). Je tedy čas říci si plán toho, co by měla na konci tohoto dílu naše aplikace umět:

  • otevřít a zobrazit textové soubory,
  • ostatní soubory se pokusit otevřít přes zaregistrovanou aplikaci,
  • každý textový soubor otevřít pouze jednou, při opakovaném pokusu o otevření přejít na jeho záložku,
  • odlišit ikonou v seznamu souborů skupiny souborů (textové/netextové),
  • ikonou v seznamu souborů dále odlišit (v aplikaci) otevřené textové soubory,
  • zavírat otevřené soubory kliknutím prostředním tlačítkem myši na jejich záložku.

Začneme, jak to tak bývá, od dat. Stejně jako si ukládáme seznam souborů v aktuálním adresáři, tak si musíme ukládat i otevřené soubory. Jednak z toho důvodu, abychom uměli rozlišit otevřené soubory ikonou v ListView, a samozřejmě i pro kontrolu, jestli už soubor otevřený je a můžeme přejít na jeho záložku. Ke každému otevřenému souboru si tak musíme uložit adresář, ve kterém se nachází, jeho jméno (nebo úplnou cestu) a následně i index záložky pro případné zobrazení opětovným pokusem o otevření. Opět nevíme, kolik záznamů (otevřených souborů) nakonec bude, takže to vede na spojitý seznam. Jeden už v aplikaci máme a když se na něj podíváme, tak obsahuje přesně ty položky, které potřebujeme - řetězec na uložení cesty k souboru a číslo (sice se "špatným" datovým typem, ale to hned napravíme).

Seznamy bychom tak měli dva, a proto si vytvoříme třídu seznamu, která nám vše bude pěkně zapouzdřovat. Základem třídy bude původní seznam (pouze datový typ u položky nType změníme na int, abychom do ní mohli ukládat i index záložky v TabCtrl) i s funkcemi, které můžeme v podstatě jen přesunout.

// definice polozky seznamu
typedef struct _LIST_VIEW_STRUCT {
	// jmeno nebo uplna cesta k souboru/adresari
	wchar_t				wzName[MAX_PATH];
	// typ souboru/adresare
	int					nType;
	// pointer na dalsi polozku seznamu
	_LIST_VIEW_STRUCT*	pNext;

} LIST_VIEW_STRUCT;

Místo původního seznamu vytvoříme instanci této třídy (m_hItem) a v programu upravíme volání funkcí. Dále přidáme druhou instanci seznamu pro otevřené soubory (m_hOpenedFile) a cestu k souboru, který budeme otevírat, si do něj uložíme. Jediný podstatný rozdíl v těchto seznamech bude v tom, že u otevřených souborů budeme chtít odebírat položky jednotlivě a také budeme chtít přistupovat k jednotlivým položkám (vyhledávat je).

Vytvoříme si proto funkci na odebrání a získávání jednotlivých položek. Jelikož nám nezáleží na pořadí a seznam může být "děravý", tak nám stačí (stejně jako u celkového vyčištění seznamu) danou položku vynulovat. Kód pro smazání položky ze seznamu vypadá následovně:

BOOL CItemList::ClearListItem(LIST_VIEW_STRUCT* pItem)
{
	// vstupni kontrola
	if (pItem == NULL) return(FALSE);
	// vynulovani polozky seznamu
	pItem->nType	= 0;
	wcscpy_s(pItem->wzName, MAX_PATH, L"");
	
	return(TRUE);
}

Při hledání tak buď musíme projít celý seznam, nebo bychom mohli seznam "sklepávat", tj. nepoužité položky přesouvat na konec. V tuto chvíli se spokojíme s procházením celého seznamu a funkce pro nalezení položky seznamu podle jména by vypadala takto:

LIST_VIEW_STRUCT* CItemList::GetListItem(LPCWSTR pwzName)
{
	// zaciname na zacatku seznamu
	LIST_VIEW_STRUCT*	pNext	= &m_sItemList;
	// vstupni kontrola
	if (pwzName == NULL) return(NULL);
	// projdi cely seznam
	while(pNext != NULL) {
		// a hledej stejne pojmenovanou polozku
		if (wcscmp(pNext->wzName, pwzName) == 0) 
			return(pNext);		
		pNext = pNext->pNext;
	}
	// polozka nenalezena
	return(NULL);
}

Otevření souboru bude reakce na stejnou událost v ListView jako otevření adresáře. Tam jsme zatím filtrovali soubory a nijak jsme na pokus o otevření souboru nereagovali. Přidáme tedy kód na rozlišení druhu souboru. V plánu máme otevírat textové soubory. Byly tím myšleny všechny textové, nejenom ty s koncovkou txt, a rozlišovat je budeme podle MIME type. Otevření textových souborů ponecháme v naší režii, na ostatní použijeme volání API funkce ShellExecute.

// ziskej MIME type souboru z jeho uplne cesty  
if (FindMimeFromData(NULL, wzFilePath, NULL, 0, NULL, FMFD_URLASFILENAME, &pwzMimeType, 0) == NOERROR) {
	// pokud obsahuje MIME type "text" 
	if (wcsstr(pwzMimeType, L"text") != NULL) {
		// tak ho zkus najit v seznamu
		if ((pOpenedFile = m_hOpenedFile.GetListItem(wzFilePath)) == NULL) {
			// pokud tam neni, tak ho tam vloz
			pOpenedFile = m_hOpenedFile.InsertListItem(wzFilePath, 0);
			// a posli rodicovskemu oknu zpravu k otevreni souboru
			::SendMessage(GetParent(), WM_COMMAND, MAKEWPARAM(ID_LIST_VIEW, 2), (LPARAM)pOpenedFile);					
			// a nakonec zmen ikonu na 'textovy soubor otevreny'
			m_hListView.ChangeItemIcon(nItemIndex, 3);
		}
		else ::SendMessage(GetParent(), WM_COMMAND, MAKEWPARAM(ID_LIST_VIEW, 3), (LPARAM)pOpenedFile);
	}
	// jinak otevri souboru zaregistrovanou aplikaci  
	else ShellExecute(NULL, L"open", wzFilePath, NULL, m_wzFolder, SW_SHOW);

	// uvolneni pameti alokovane funkci FindMimeFromData
	CoTaskMemFree(pwzMimeType);			
}

Otevření souboru v aplikaci znamená, že musíme přidat záložku na TabCtrl (v programu ji reprezentuje instance m_hTabView, viz dále), resp. ho při prvním otevřeném souboru vytvořit, na první záložku umístit seznam s obsahem aktuálního adresáře a na druhou záložku umístit obsah souboru. Další otevřený soubor už jen přidá záložku. Vytvoření okna TabCtrl (zapouzdřenou v naší třídě) je další ukázka:

// zjisti, jestli uz jsou zalozky vytvorene
if (!m_hTabView.IsWindow()) {
	// a pokud ne, tak je vytvor
	GetClientRect(&rtRect);
	m_hTabView.CreateMyEx(L"", WS_CHILD | WS_VISIBLE, 0, this, 0, 0, 0, &rtRect);
	// a seznam souboru vloz jako prvni zalozku
	m_hListViewPanel.SetParent(&m_hTabView);		
	m_hTabView.InsertTab(L"Seznam", &m_hListViewPanel);
}

Třídu pro TabCtrl vytvoříme stejně, jako jsme ji vytvořili pro TreeCtrl nebo ListCtrl. Třída CMyTabControl, která bude v sobě zapouzdřovat funkčnost TabCtrl, bude mít funkce volající předdefinovaná makra (seznam maker pro TabCtrl [ http://msdn.microsoft.com/en-us/library/ff486046(v=VS.85).aspx ]). Z této třídy si zde uvedeme jako příklad zjištění aktuálně vybrané záložky a zjištění počtu záložek:

int CMyTabControl::GetCurSel()
{
	if (m_hWnd == NULL) return(-1);
	// vraci index aktualne vybrane zalozky
	return(TabCtrl_GetCurSel(m_hWnd));
}


int CMyTabControl::GetItemCount()
{
	if (m_hWnd == NULL) return(0);
	// vraci celkovy pocet zalozek
	return(TabCtrl_GetItemCount(m_hWnd));
}

Problém TabCtrl je ten, že zobrazuje "pouze" záložky a o změnu zobrazení obsahu vybrané záložky se musíme postarat sami. Instanci naší třídy CMyTabControl dále zapouzdříme do třídy CMyTabView, která se bude starat o tuto změnu a bude si ukládat seznam oken, která jsou na záložkách otevřena. A také do této třídy okna (pohledu), které bude rodičovským oknem TabCtrl (resp. CMyTabControl), budou chodit notifikace o změně aktuálního výběru a další. Jako ukázku zde uvedu kód pro vložení nového okna/záložky, které sestává z vytvoření nové záložky, uložení okna do seznamu otevřených oken a spárování této dvojice:

int CMyTabView::InsertTab(LPCWSTR pwzTitle, CMyWindow* pTabWindow)
{
	// zjisteni poctu zalozek
	int	nCount	= m_hTabControl.GetItemCount();
	int	nItem;
	// vstupni kontrola
	if ((pTabWindow == NULL) || (pwzTitle == NULL))
		return(-1);

	// vlozeni nove zalozky s danym jmenem
	if ((nItem = m_hTabControl.InsertItem(nCount, pwzTitle)) >= 0) {
		// pokud se vlozeni podarilo, tak si uloz okno do seznamu
		MY_TAB_VIEW_STRUCT*	pItem = InsertWindow(pTabWindow);
		// test uspesnosti
		if (pItem == NULL) return(-1);
		// sparovani ulozene polozky se zalozkou
		if (m_hTabControl.SetItemParam(nItem, (LPARAM)pItem)) return(nItem);
		else return(-1);
	}
	else return(-1);
}

Úplnou cestu k souboru potřebujeme předat do okna, které otevře tento soubor a přidá ho do TabCtrl. V našem případě se jedná o okno RightPanel. Když jsme si uložili jméno souboru, tak k němu ještě potřebujeme uložit index, který dostane po jeho přidání do TabCtrl. Předáme tedy do RightPanel (prostřednictvím zprávy) ukazatel na položku seznamu (m_hOpenedFile), ve které je uloženo jméno souboru a do které po otevření okna uložíme index. Pointer na instanci okna předáme do TabCtrl jako parametr dané stránky.

void CRightPanel::OpenNewTab(LIST_VIEW_STRUCT* pFile)
{
	RECT		rtRect;
	CMyEdit*	pEdit;
	// vytvoreni instance pohledu
	pEdit = new CMyEdit();
	if (pEdit == NULL) return;	
	// vytvoreni okna editacniho pole
	pEdit->CreateMyEx(L"", WS_CHILD | WS_VISIBLE | ES_MULTILINE | ES_AUTOHSCROLL | ES_AUTOVSCROLL | WS_HSCROLL | WS_VSCROLL, WS_EX_CLIENTEDGE, &m_hTabView, 0, 0, 0, &rtRect);
	pEdit->SetFont(m_hTextFont);	
	// otevreni souboru
	hFile = CreateFile(pFile->wzName, GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
	// podarilo se soubor otevrit
	if (hFile != INVALID_HANDLE_VALUE) {
		// zjisti jeho velikost
		dwSize	= GetFileSize(hFile, NULL);
		// podarilo se alokovat pamet pro data souboru
		if ((pnData = new BYTE[dwSize + 1]) != NULL) {
			// nacti data souboru
			ReadFile(hFile, pnData, dwSize, &dwRead, NULL);
			// pridej ukoncovaci znak retezce
			pnData[dwRead] = '\0';
			// nastav nactena data souboru jak o text editacniho okna
			pEdit->SetWindowTextA((LPCSTR)pnData);
			// smaz alokovanou pamet pro data souboru
			delete [] pnData;
		}
		// uzavri soubor
		CloseHandle(hFile);
	}
	// index zalozky souboru si uloz
	pFile->nType = m_hTabView.InsertTab(PathFindFileName(pFile->wzName), pEdit, TRUE);		 
	// zustan na prvni zalozce - na seznamu
	m_hTabView.SetCurSel(0);
	// aktualizuj rozmery okna
	GetClientRect(&rtRect);
	m_hTabView.Resize(&rtRect);
}

V případě, že je soubor již otevřený, tak nevytváříme novou záložku, ale pouze tuto záložku nastavíme jako aktuálně otevřenou. Tím máme hotové otevírání souborů v okně naší aplikace. Nyní se vrhneme na rozlišení souborů ikonou. Jak jsme si napsali do "zadání", rozlišovat budeme tyto soubory:

  • soubory, které otevřeme v naší aplikaci, ale nejsou ještě otevřeny,
  • soubory, které otevřeme v naší aplikaci a již jsou otevřeny,
  • ostatní soubory, které se pokusíme otevřít v externí aplikaci.

Půjde o to, upravit kód na zobrazení výpisu souborů, do kterého přiřadíme test na typ souboru a test na to, jestli již je soubor otevřen.

// vsechny soubory maji ikonu normalniho souboru
nIconIndex	= 1;
// pouze soubory s mime-type obsahujici slovo text maji ikonu textoveho souboru
if (FindMimeFromData(NULL, wzFilePath, NULL, 0, NULL, FMFD_URLASFILENAME, &pwzMimeType, 0) == NOERROR) {
	if (wcsstr(pwzMimeType, L"text") != NULL) {
		// pokud neni v seznamu, tak ma normalni ikonu
		if (m_hOpenedFile.GetListItem(wzFilePath) == NULL)
			nIconIndex = 2;
		// jinak ma svitivou ikonu - otevreny soubor
		else nIconIndex = 3;
	}
	// uvolneni pameti
	CoTaskMemFree(pwzMimeType);			
}
// pridani souboru do seznamu i s pridelenou ikonou (nIconIndex)
m_hListView.InsertItem(nCount++, sFindData.cFileName, nIconIndex, (LPVOID)pItem);

Poslední část, která nás čeká, je zavření záložky/souboru kliknutím prostředním tlačítkem na záložku. Nejprve tedy musíme dostat do komponenty tuto funkčnost. V třídě CMyTabControl musí odchytávat zprávy od myši resp. od prostředního tlačítka na myši (WM_MBUTTONDBLCLK - OnMButtonDblClk, WM_MBUTTONDOWN - OnMButtonDown, WM_MBUTTONUP - OnMButtonUp). Chceme dosáhnout takové funkce, že se zavření provede pouze a jenom po kliknutí na záložce, tj. stisk i puštění tlačítka myši bylo provedeno na stejné záložce. Musíme si tedy uložit index záložky při stisku tlačítka a porovnat ho se stiskem tlačítka při jeho puštění. Do možností ovladacího prvku si ještě přidáme možnost volit, jestli je možné zavírat záložky prostředním tlačítkem (m_bEnableClose).

BOOL CMyTabControl::OnMButtonDown(DWORD dwKeyIndicator, int nPositionX, int nPositionY)
{
	// je povoleno zavreni prostrednim tlacitkem
	if (m_bEnableClose) {
		TCHITTESTINFO	sHitTest;
		int				nIndex;
		// zjisti, na ktere jsme zalozce - dle polohy kurzoru
		sHitTest.flags	=	0;
		sHitTest.pt.x	= nPositionX;
		sHitTest.pt.y	= nPositionY;
		// a pokud jsme na nejake zalozce a klikame "na ni" (ONITEM)
		if (((nIndex = TabCtrl_HitTest(m_hWnd, &sHitTest)) > 0) && ((sHitTest.flags & TCHT_ONITEM) != 0)) {
			// tak si uloz index
			m_nCloseIndex = nIndex;
		}		
	}
	// sazmorejme nezapominame volat standardni zpracovani teto zpravy
	return(CMyWindow::OnMButtonDown(dwKeyIndicator, nPositionX, nPositionY));
}


BOOL CMyTabControl::OnMButtonUp(DWORD dwKeyIndicator, int nPositionX, int nPositionY)
{
	// pokud bylo provedeno kliknuti
	if (m_nCloseIndex != -1) {
		TCHITTESTINFO	sHitTest;
		int				nIndex;
		// tak opet zjisti, na jakem jsme indexu zalozky
		sHitTest.flags	=	0;
		sHitTest.pt.x	= nPositionX;
		sHitTest.pt.y	= nPositionY;
		// a pokud na nejakem jsme
		if (((nIndex = TabCtrl_HitTest(m_hWnd, &sHitTest)) > 0) && ((sHitTest.flags & TCHT_ONITEM) != 0)) {
			// a je to ten stejny, tak zavri zalozku
			if (nIndex == m_nCloseIndex) CloseTab(nIndex);			
		}
		// konec zavirani
		m_nCloseIndex = -1;
	}
	// standardni zpracovani zpravy
	return(CMyWindow::OnMButtonUp(dwKeyIndicator, nPositionX, nPositionY));
}

Reakce je mírně zjednodušená, ještě by se měl ošetřit případ, kdy se opustí po kliknutí oblast TabCtrl. To by se provedlo odchytáváním dodatečných událostí od myši zavoláním funkce TrackMouseEvent s parametrem TME_LEAVE.

Dvojklik je vlastně také klik, takže v reakci na něj budeme danou záložku zavírat také - opět volání CloseTab. Co se při takovém zavření musí všechno udělat? A další otázka je: mají se dát zavřít všechny záložky? I v našem případě nechceme zavřít první záložku se seznamem, takže to musíme nějak omezit. Mohli bychom to omezit "natvrdo", ale knihovna, kterou si tvoříme, by měla být obecná, takže je potřeba se "zeptat" aplikace, jestli záložku můžeme uzavřít.

K tomuto účelu opět využijeme sílu objektového programování. V nadřazené třídě (CMyTabView) vytvoříme rozhraní pro tyto účely, tj. funkci pro zjištění, jestli se daný index může zavřít. Dále budeme potřebovat funkci, která se zavolá po uzavření, abychom mohli reagovat na tuto událost v závislosti na aplikaci. Naše "rozhraní" tak bude mít dvě funkce - CanClose a ProcessClose. Funkce na uzavření záložky tak bude vypadat následovně.

void CMyTabControl::CloseTab(int nIndex)
{
	// zeltej se aplikace, jestli muzeme uzavrit danou zalozku (index)
	if (((CMyTabView*)m_pParentWindow)->CanClose(nIndex)) {
		// pokud ano, tak si uloz aktualni vyber
		int		nSelect	= GetCurSel();
		// a parametr zalozky, kteou budeme zavirat
		LPARAM	lParam	= GetItemParam(nIndex);		
		// zalozku smaz
		if (DeleteItem(nIndex)) {
			// pokud se to podarilo, tak dej vedet aplikaci
			((CMyTabView*)m_pParentWindow)->ProcessClose(nIndex, lParam);
			// jeste musime zmenit vyber, pokud byla zavrena polozka vybrana
			if (nSelect == nIndex) {
				NMHDR	sNotify;
				// pokud se jednalo o prvni polozku, tak ji zkus vybrat znovu (novou)
				if (nIndex == 0) SetCurSel(0);
				// jinak vyber predchozi
				else SetCurSel(nIndex - 1);
				// volani TabCtrl_SetCurSel nevyvola poslani TCN_SELCHANGE
				sNotify.code		= TCN_SELCHANGE;
				sNotify.hwndFrom	= m_hWnd;
				sNotify.idFrom		= 0;
				// tak ho musime poslat sami
				::SendMessage(GetParent(), WM_NOTIFY, (WPARAM)0, (LPARAM)&sNotify);
			}
		}
	}
}

V knihovní třídě CMyTabView bude implementace funkcí rozhraní jednoduchá. Všechny záložky mají povolené zavření, tj. CanClose bude vždy vracet TRUE a funkce ProcessClose smaže přidělené okno (proto se mu musí předat lParam). Pokud toto chování chceme změnit nebo rozšířit, tak musíme v aplikaci vytvořit dceřinou třídu, která funkce rozhraní změní. V tomto našem případě nepůjde uzavřít první záložka.

BOOL CFileTabView::CanClose(int nIndex)
{
	// prvni zalozku nezavirej
	if (nIndex == 0) return(FALSE);
	// ostatni muzes
	else return(TRUE);
}

Uzavření se také o něco rozšíří. U každého souboru jsme si uložili jeho index záložky. Co se stane v případě, že máme otevřených 10 záložek a třetí v pořadí uzavřeme? Všem souborům za uzavřenou záložkou se sníží index přidělené záložky. To musíme provést i v našem uloženém seznamu, a proto je nutné informovat o uzavření nadřazené záložky nadřazené okno. Stejné je to i v případě, že jsme uzavřeli všechny záložky až na seznam souborů. V takovém případě již nemá TabCtrl význam a je možné ho zrušit.

void CFileTabView::ProcessClose(int nIndex, LPARAM lParam)
{
	// nejprve zavolej puvodni funkci - smazani obsahu zalozky
	CMyTabView::ProcessClose(nIndex, lParam);
	// a dej nadrizenemu oknu vedet, ze jsme zavreli zalozku
	::SendMessage(GetParent(), WM_COMMAND, MAKEWPARAM(ID_FILE_TAB_VIEW, 0), (LPARAM)nIndex);
	// pokud je otevrena pouze jedna zalozka
	if (m_hTabControl.GetItemCount() <= 1) {
		// tak o tom take dej vedet nadrizenemu oknu
		::SendMessage(GetParent(), WM_COMMAND, MAKEWPARAM(ID_FILE_TAB_VIEW, 0xFFFF), (LPARAM)0);
	}
}

Reakcí na uzavření záložky bude tedy jen snížení indexů u všech otevřených položek, tj. budeme měnit obsah m_hOpenedFile. Složitější to bude u uzavření TabCtrl. V tomto případě musíme zrušit TabCtrl a seznam souborů (ListView) dát na jeho místo (inverzní případ, když jsme z ListView tvořili jednu ze záložek).

void CRightPanel::DestroyTab()
{
	RECT	rtRect;
	// nastav RightPanel jako rodicovske okno
	m_hListViewPanel.SetParent(this);		
	// zrus TabCtrl
	m_hTabView.DestroyWindow();
	// ziskej velikost klientske oblasti RightView
	GetClientRect(&rtRect);
	// povol a zobraz okno ListView (nemuselo byt aktivni)
	m_hListViewPanel.EnableWindow(TRUE);
	m_hListViewPanel.Show(SW_SHOW);
	// uprav velikost ListView do okna RightView
	m_hListViewPanel.Resize(&rtRect);
}

Tím jsme splnili všechny vytyčené úkoly. Výslednou aplikaci si můžete prohlédnout na obrázku. Součástí archivu ke stažení [ http://programujte.com/storage/mydll_6.zip ] je verze projektu, do které je přidán manifest, aby výsledná aplikace dostala tzv. XP vzhled, tj. používala novější verzi CommonControls. V příští části se vrhneme na dialogy, vytvoříme si třídu dialogu a hned ji použijme na dialog hledání a na dialog o aplikaci.


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