Panel użytkownika
Nazwa użytkownika:
Hasło:
Nie masz jeszcze konta?
Autor: 'Złośliwiec'
Biblioteki C++

Okna dialogowe, cz. 3

[lekcja]

Powszechnie używane dialogi

Nie trzeba być koniecznie programistą by wiedzieć, że nie ma sensu wyważać otwartych drzwi. A programista to w dodatku skrajnie wygodne stworzenie i lubi wykorzystywać czyjąś pracę, jeśli tylko się da. I słusznie, bo gdyby każdy musiał wszystko robić na własną rękę i od zera, to nie byłoby postępu ;-).

Kierując się tymi mądrościami panowie z Microsoftu wykombinowali sobie, coby dołączyć do swego systemu kilka predefiniowanych dialogów. I tak na przykład prawie każdy program odczytuje lub zapisuje jakieś pliki - wcześniej jednak użytkownik musi podać nazwę i ścieżkę do takiego pliku. Nie trzeba być specjalnie spostrzegawczym by skonstatować, że większość programów używa do wyboru pliku jednego i tego samego okienka dialogwego. Podobnie ma się rzecz z dialogiem wyboru czcionki czy koloru. Takie dialogi noszą urocze miano ''common dialogs'', co można przetłumaczyć jako "wspólne" lub "powszechnie używane", a najlepiej jedno i drugie naraz :-).

Dialog wyboru pliku

W ramach praktycznej nauki zmajstrujemy sobie coś w rodzaju windowsowego Notatnika - aplikację, która po wciśnięciu odpowiedniego przycisku będzie pokazywała dialog wyboru pliku, po czym będzie wczytywała wybrany przez użytkownika plik do pola tekstowego. A więc coś takiego:

Nasz pseudo-notatnik w akcji (Windows 98)
Nasz pseudo-notatnik w akcji (Windows 98)
 
Samo wywołanie dialogu wyboru pliku na scenę to wyjątkowo proste zadanie:

C/C++
OPENFILENAME ofn;
// jeśli otwieramy plik do odczytu...
GetOpenFileName( & ofn );
// ...lub GetSaveFileName (&ofn), jeśli zapisujemy

Pewnie się domyślasz, że struktura OPENFILENAME zawiera rozmaite przydatne opcje naszego dialogu. Tak więc taki sposób użycia jej jak powyżej spowoduje w najlepszym wypadku otwarcie dialogu zupełnie inaczej wyglądającego, niż sobie zaplanowaliśmy. Tak więc strukturę należałoby wypełnić. Aby wypełnić, musimy coś niecoś o niej wiedzieć, więc rzućmy okiem na szczegóły (uwaga, jest ich dość sporo ;-)):
 
SkładowaTypZnaczenie
lStructSizeDWORDRozmiar struktury w bajtach
hwndOwnerHWNDUchwyt okna-właściciela dialogu, może być NULL
lpstrFilterLPCTSTRFiltr dla wyświetlanych w dialogu plików, może być NULL
lpstrCustomFilterLPTSTRBufor na filtr zdefiniowany przez użytkownika w trakcie trwania dialogu
nMaxCustFilterDWORDMaksymalny rozmiar powyższego bufora
nFilterIndexDWORDZawiera indeks filtra wybranego przez użytkownika
lpstrFileLPTSTRNajważniejszy element struktury - adres bufora, który będzie zawierał nazwę(-y) pliku(-ów) wybranego(-ych) przez użytkownika
nMaxFileDWORDMaksymalny rozmiar powyższego bufora
lpstrFileTitleLPTSTRAdres bufora, który po zakończeniu dialogu zawiera tytuł wybranego pliku (nazwę bez ścieżki dostępu)
nMaxFileTitleDWORDMaksymalny rozmiar powyższego bufora
lpstrInitialDirLPCTSTRStartowy katalog dialogu (NULL - bieżący katalog)
lpstrTitleLPCTSTRTytuł dialogu (NULL - tytuł domyślny)
FlagsDWORDRóżne flagi :-)
nFileOffsetWORDIndeks znaku w buforze lpstrFile, od którego zaczyna się właściwa nazwa (tytuł) pliku
nFileExtensionWORDJak wyżej, tylko w odniesieniu do rozszerzenia
lpstrDefExtLPCTSTRDomyślne rozszerzenie pliku

A to jeszcze nie wszystko, podałem tylko najważniejsze pola :D. Na szczęście nie musimy tego wszystkiego wypełniać jednocześnie. Dla świętego spokoju zerujemy więc naszą strukturę, coby się upewnić, że nie ma w niej żadnych losowych śmieci, po czym ustawiamy pole z rozmiarem struktury na odpowiednią wartość:

C/C++
ZeroMemory( & ofn, sizeof( ofn ) );
ofn.lStructSize = sizeof( ofn );

Jeśli używasz Windows XP (a ze swych tajnych żródeł wiem, że przeszło połowa odwiedzających moją stronę go używa ;-)) albo 2000, to pole lStructSize ustawiasz na sizeof(OPENFILENAME).

Jadąc dalej: pole hwndOwner wypełniamy aż po brzegi uchwytem do okna, które wywołuje dialog wyboru pliku. Następnie trzeba zdefiniować '''filtr'''. Jest to ciąg par łańcuchów, które... Aaa, co będę się silił na uczone formułki, najlepiej podam od razu przykład takiego filtru:
 
Bitmapy (*.bmp)|*.bmp
Pliki tekstowe (*.txt)|*.txt
Dokumenty internetowe (*.htm; *.html)|*.htm;*.html
Wszystkie pliki|*.*

Teraz chyba łapiesz ;-). Tak, filtr służy do wyświetlania tylko plików danego rodzaju.

Lewa część to opis filtru, czyli tekst, który pojawia się w ComboBox-ie na dole dialogu. Prawa część to właściwy filtr, czyli maska, składająca się najczęściej z gwiazdki i rozszerzenia. Możemy podać kilka masek oddzielonych średnikiem, tak jak powyżej w przypadku plików HTML. Poszczególne pary filtrów powinno się oddzielać znakami zerowymi, ja wypisałem je w oddzielnych linijkach, dla lepszej czytelności. Prawą część każdej pary oddziela się od lewej znakiem zerowym, ja użyłem powyżej znaku |. Na samym końcu filtru dajemy podwójny znak zerowy.

Cóż, całe to filtrowanie może wyglądać na bardzo skomplikowane, ale w istocie nie jest tak źle. Przekonasz się, kiedy zaczniesz sam to robić. Może zresztą wystarczy ci spojrzeć na mały przykładzik:

C/C++
ofn.lpstrFilter = "Pliki tekstowe (*.txt)\0*.txt\0Wszystkie pliki\0*.*\0";

Okiem zwykłego człowieka jest to tak zwany szum informacyjny, okiem rasowego kodera jest to zupełnie przejrzysty i czytelny filtr :-). Sekwencje ucieczki \0 to oczywiście terminalne znaki zerowe, które w większości przypadków oznaczają Absolutny Koniec Stringa, natomiast tutaj pełnią rolę separatorów, zaś "prawdziwy" znak terminalny jest podwójny. Wprawdzie na końcu widać tylko jedną sekwencję \0, ale jak wiemy, kompilator zawsze dostawia jeszcze jeden znak zerowy na końcu każdego stringa w instrukcji przypisania.

Pora zająć się najważniejszym, czyli nazwą pliku, bo przecież właśnie po to, żeby ją uzyskać od użytkownika, wyświetlamy cały ten dialog. Potrzebny nam będzie na nią bufor. Najlepiej go zrobić tak:

C/C++
char sNazwaPliku[ MAX_PATH ] = "";

Użyliśmy systemowej stałej MAX_PATH, dzięki czemu mamy pewność, że nasz dialog nie będzie używał zbyt długich nazw ścieżek. Maksymalny rozmiar bufora oraz jego adres trzeba teraz wstawić do naszej struktury:

C/C++
ofn.nMaxFile = MAX_PATH;
ofn.lpstrFile = sNazwaPliku;

Warto jeszcze ustawić domyślne rozszerzenie. To dla czystej wygody użytkownika, a zarazem jest to pewne zabezpieczenie przed lamerami - jak wiadomo, Windows ma opcję ukrywania rozszerzeń plików (z której oczywiście żaden szanujący się Fachowiec nie korzysta, ale nie każdy jest wszak Fachowcem ;-)), która to opcja powoduje czasem niezłe zamieszanie. Na wypadek więc, gdyby ktoś wpisał samą nazwę bez rozszerzenia, ustawienie rozszerzenia domyślnego (które w takim wypadku zostanie automatycznie dołączone do wpisanej nazwy pliku) zwiększa prawdopodobieństwo uniknięcia błędu w rodzaju ''file not found'':

C/C++
ofn.lpstrDefExt = "txt";

Pozostaje już tylko zająć się flagami. Oto najważniejsze z nich:
 
StałaZnaczenie
OFN_ALLOWMULTISELECTPozwala na zaznaczanie wielu plików jednocześnie
OFN_CREATEPROMPTW razie gdyby plik o wpisanej przez użytkownika nazwie nie istniał, powoduje, że system pyta czy utworzyć taki plik
OFN_EXTENSIONDIFFERENTOkreśla, że użytkownik wybrał plik o rozszerzeniu innym niż domyślne (o ile podano domyślne, rzecz jasna)
OFN_FILEMUSTEXISTPlik o podanej nazwie musi istnieć. W przeciwnym wypadku zostaje wyświetlone ostrzeżenie. Ustawienie tej flagi powoduje automatyczne włączenie OFN_PATHMUSTEXIST (co jest dość logiczne ;-))
OFN_HIDEREADONLYUkrywa pole Tylko do odczytu, pokazywane czasem pod polem wyboru filtru
OFN_NOCHANGEDIRNie pozwala na zmianę katalogu
OFN_NODEREFERENCELINKSOkreśla sposób traktowania skrótów (*.lnk). Jeśli ta flaga jest ustawiona, to dialog zwraca nazwę wybranego pliku *.lnk (czyli traktuje go jak zwykły plik), w przeciwnym wypadku zwraca nazwę pliku, do którego odnosi się ten skrót
OFN_NONETWORKBUTTONUkrywa przycisk Otoczenie sieciowe
OFN_OVERWRITEPROMPTJeśli nasz dialog to Zapisz jako..., to ta flaga sprawia, że w przypadku próby nadpisania istniejącego pliku pokazywany jest komunikat z pytaniem, czy na pewno :-)
OFN_PATHMUSTEXISTNietrudno się domyślić :-)
OFN_READONLYZaznacza pole Tylko do odczytu (jeśli użyjemy tej flagi przed pokazaniem dialogu) lub wskazuje, czy to pole zostało zaznaczone przez użytkownika (jeśli użyjemy jej po pokazaniu dialogu)
OFN_SHOWHELPPokazuje przycisk Pomoc

Żeby otworzyć plik tekstowy i wyświetlić jego zawartość w oknie, na pewno potrzebna nam będzie flaga OFN_FILEMUSTEXIST (bo nic dobrego się nie stanie, jeśli spróbujemy otworzyć nieistniejący plik :-)). Nie zaszkodzi też ustawić OFN_HIDEREADONLY. Tak więc wypełnienie pola Flags będzie wyglądało następująco:

C/C++
ofn.Flags = OFN_FILEMUSTEXIST | OFN_HIDEREADONLY;

Mamy ustawione co trzeba, teraz musimy jeszcze wywołać nasz dialog. Już wiemy, że robi to funkcja GetOpenFileName lub GetSaveFileName. Jeśli jednak nie chcemy wyjść na kompletnych lamerów, musimy się jeszcze zająć wartościami, zwracanymi przez te funkcje. Wynoszą one 0, jeśli użytkownik nie wybrał żadnego pliku (czyli albo wcisnął 'Anuluj', albo zamknął dialog, albo spowodował jakiś błąd), natomiast jeśli wszystko jest w porządku, to wartość zwrócona jest niezerowa.

Co do błędów, nie jest tajemnicą, że we wszelkich kontaktach z plikami można ich spowodować całe mnóstwo. Predefiniowane dialogi mają więc swoją własną funkcję do wykrywania błędów, nazywa się ona CommDlgExtendedError. Dokładny opis zwracanych przez nią wartości znajdziesz jak zawsze w MSDN, natomiast najbardziej "popularne" błędy to:
 
StałaZnaczenie
0Użytkownik nie wybrał pliku
FNERR_BUFFERTOOSMALLWybrana przez użytkownika nazwa pliku nie mieści się w buforze
FNERR_INVALIDFILENAMEPodana przed użytkownika nazwa pliku jest nieprawidłowa

Z własnych niezbyt miłych doświadczeń wiem też, że równie często przy korzystaniu dialogów mogą wystąpić różne "dziwne" błędy, które powodują, że dialog w ogóle się nie wyświetla. Na przykład jeśli zapomnimy wyzerować bufor podawany w lpstrFile, to funkcja GetOpenFileName wróci natychmiast z wartością FNERR_INVALIDFILENAME bez wyświetlania czegokolwiek. Warto zajrzeć do MSDN do opisu funkcji CommDlgExtendedError i zapoznać się także z listą błędów, rozpoczynających się przedrostkiem CDERR_.

Chyba wystarczy już o tych błędach, bo jeszcze coś wykrakamy ;-). Przystępujemy do realizacji naszego wielkiego zadania, czyli stworzenia naszej Jednej Szesnastej Notatnika :-). Jak zwykle dołożyłem wszelkich starań, żebym nie musiał się zbytnio starać i przedstawię ci tylko najbardziej niezbędne fragmenty kodu. Jak wczytać plik - już wiesz, wystarczy zrobić z tego osobną funkcję, nazwiemy ją sobie WczytajPlik. Będzie ona pobierała uchwyt okna, do którego ma wczytać tekst oraz nazwę pliku. Oczywiście szkieletowego kodu okienkowej aplikacji też nie będę po raz setny wstawiał. Wszystko poza tymi dwiema rzeczami masz tutaj. Najpierw tworzenie kontrolki EDIT i ustawienie jej rozmiarów na rozmiar okna głównego, tego chyba nie muszę omawiać:

C/C++
hEdit = CreateWindowEx( WS_EX_CLIENTEDGE, "EDIT", "Tu będzie tekst z pliku", WS_CHILD |
WS_VISIBLE | WS_BORDER | ES_MULTILINE | WS_VSCROLL | ES_AUTOVSCROLL, 5, 5, 5, 5, hwnd, NULL,
hThisInstance, NULL );
RECT rcClient;
GetClientRect( hwnd, & rcClient );
MoveWindow( hEdit, 0, 0, rcClient.right, rcClient.bottom - 30, TRUE );

No, może jedną rzecz wytłumaczyć trzeba. Użyliśmy tu funkcji MoveWindow. Jak sama nazwa wskazuje, służy ona do przemieszczania okna, ale przy okazji ustawia też nową szerokość i wysokość dla tego okna. Tutaj chcemy powiększyć EDIT-a na cały obszar klienta naszego głównego okna, zatem pobieramy wymiary tego ostatniego funkcją GetClientRect. Ostatni argument funkcji MoveWindow to flaga, która określa, czy po zmianie wymiarów okno ma być odmalowane. Czemu nie, możemy odmalować, więc ustawiamy TRUE. Pamiętaj, by zadeklarować zmiennąhEdit w zasięgu globalnym.

Następnie tworzymy przycisk, który będzie nam wywoływał dialog. Tu też nie ma żadnych filozofii:

C/C++
CreateWindowEx( 0, "BUTTON", "Plik...", WS_CHILD | WS_VISIBLE | WS_BORDER,
0, rcClient.bottom - 27, 50, 25, hwnd, NULL, hThisInstance, NULL );

Po trzecie (i najważniejsze) - reakcja na przycisk, czyli pokazanie dialogu wyboru pliku, po czym wczytanie tego pliku i wywalenie jego cennej zawartości do pola tekstowego:

C/C++
case WM_COMMAND:
{
    OPENFILENAME ofn;
    char sNazwaPliku[ MAX_PATH ] = "";
   
    ZeroMemory( & ofn, sizeof( ofn ) );
    ofn.lStructSize = sizeof( ofn );
    ofn.lpstrFilter = "Pliki tekstowe (*.txt)\0*.txt\0Wszystkie pliki\0*.*\0";
    ofn.nMaxFile = MAX_PATH;
    ofn.lpstrFile = sNazwaPliku;
    ofn.lpstrDefExt = "txt";
    ofn.Flags = OFN_FILEMUSTEXIST | OFN_HIDEREADONLY;
    if( GetOpenFileName( & ofn ) )
    {
        WczytajPlik( sNazwaPliku, hEdit );
    }
}
break;

I to by było tyle o dialogach wyboru pliku.
Poprzedni dokument Następny dokument
Okna dialogowe, cz. 2 Okna dialogowe, cz. 4