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:
Samo wywołanie dialogu wyboru pliku na scenę to wyjątkowo proste zadanie:
OPENFILENAME ofn;
GetOpenFileName( & ofn );
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 ;-)):
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ść:
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:
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:
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:
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'':
Pozostaje już tylko zająć się flagami. Oto najważniejsze z nich:
Ż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:
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:
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ć:
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:
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:
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.