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). 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í 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.