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