R03-03, ## Documents ##, C++Builder 5


Interfejs użytkownika

Jedną z zalet C++Buildera, jako narzędzia RAD, jest możliwość łatwego i szybkiego budowania interfejsu użytkownika aplikacji za pomocą użytecznych narzędzi wizualnych. Same narzędzia jednak nie wystarczą - aby interfejs aplikacji spełnił oczekiwania użytkownika i spotkał się z jego akceptacją, konieczne jest kierowanie się pewnymi zasadami jego budowy. Zasady te, wynikające przede wszystkim ze sposobu użytkowania komputera przez (typowego) użytkownika, muszą być znane każdemu projektantowi, jeżeli chce on tworzyć aplikacje obsługiwane w sposób nieskomplikowany, zgodnie z intuicją i wymogami wygody użytkownika.

Rozdział ten rozpoczniemy od przedstawienia ogólnych przesłanek funkcjonowania interfejsu użytkownika i wynikających stąd zasad jego konstrukcji; w pozostałej jego części zaprezentujemy praktyczne zastosowanie tych zasad na przykładzie kilku wybranych aplikacji.

Podstawowe zasady konstrukcji interfejsu użytkownika

Gdy przyjrzeć się przedstawionym poniżej regułom, określającym zasady budowania interfejsu użytkownika, większość z nich wydaje się być intuicyjnie jasna, ale nie wszystkie są aż tak oczywiste. Ich przestrzeganie jest jednak niezbędne do tego, by interfejs użytkownika - stanowiący zgodnie z nazwą pewną platformę współpracy użytkownika z aplikacją - nie stał się w tej współpracy przeszkodą. Sekwencyjny charakter tekstu wymusza co prawda omówienie wspomnianych reguł w określonej kolejności, jednak każda z nich jest nie mniej ważna od pozostałych.

Jak już napisaliśmy, większość z przedstawionych zasad wydaje się niemal oczywista, dlatego też często zapomina się o nich i nie od rzeczy było przypomnieć je tutaj - spędzając długie godziny nad debuggerem lub studiowaniem kodu źródłowego zapominamy niekiedy, iż aplikacja nasza trafić może do rąk człowieka nie tylko posługującego się innym językiem, lecz być może nie umiejącego wskazać naszego kraju na mapie…

Przykładowe projekty wykorzystywane w tym rozdziale

Jeden przykład wart jest tysiąca słów, więc po zwięzłym przedstawieniu zasad, jakimi rządzi się nowoczesny interfejs użytkownika, pora teraz pokazać, jak zasady te wykorzystywane powinny być w praktyce. W tym celu przedstawimy kilka przykładowych projektów i w kolejnych podrozdziałach omawiać będziemy poszczególne aspekty ich funkcjonowania, oczywiście ze szczególnym uwzględnieniem ich związku z interfejsem użytkownika.

Najczęściej odwoływać się będziemy do projektu realizującego prosty kalkulator - jego kompletny materiał źródłowy znajduje się na dołączonej do książki płycie CD-ROM. Niektóre podrozdziały, omawiające zagadnienia nie związane bezpośrednio z tym projektem, ilustrowane będą dodatkowymi aplikacjami, których wykaz - w kolejności pojawiania się w treści rozdziału - przedstawia tabela 3.1.

Tabela 3.1. Wykaz uzupełniających aplikacji demonstracyjnych używanych w tym rozdziale

Projekt

Wykorzystywany w podrozdziałach…

Focus.bpr

Kontrola migracji skupienia pomiędzy elementami interfejsu

MDIProject.bpr

Dostosowanie tła formularza głównego MDI

Scentralizowane sterowanie akcjami obiektu

Panels.bpr

Wyrównanie - właściwość Align

Zakotwiczenie - właściwość Anchors

Ograniczenia swobody zmiany rozmiarów - właściwość Constraints

ProgressCursor.bpr

Wykorzystanie komponentów TProgressbar i TCGauge

Wygląd kursora

ScreenInfo.bpr

Zróżnicowane konfiguracje graficzne

Wśród wymienionych w tabeli projektów uzupełniających jeden z nich - MDIProject.bpr - wyraźnie odróżnia się od pozostałych, gdyż realizuje wielodokumentowy model interfejsu użytkownika (ang. MDI - Multiple Document Interface), podczas gdy pozostałe należą do kategorii aplikacji jednodokumentowych (ang. SDI - Single Document Interface). Aplikacje jednodokumentowe posiadają jeden formularz główny i opcjonalnie dowolną liczbę formularzy pomocniczych, z których każdy może być wyświetlany w sposób modalny lub niemodalny; owe formularze pomocnicze nie są jednak związane z formularzem głównym relacjami „potomny - rodzicielski” (ang. child - parent). W przypadku aplikacji wielodokumentowej formularze pomocnicze są formularzami potomnymi w stosunku do formularza głównego, co w pierwszym rzędzie uwidacznia się w ten sposób, iż obszar ich widoczności ograniczony jest do obszaru klienta formularza głównego - innymi słowy formularz główny spełnia rolę swoistego „kontenera wizualnego” dla swych formularzy potomnych. Formularze potomne wyświetlane są w sposób niemodalny, co umożliwia łatwe przełączanie się pomiędzy nimi. Funkcjonalność aplikacji wielodokumentowej charakteryzuje się kilkoma unikatowymi cechami, m.in. możliwością rozmaitego aranżowania układu formularzy potomnych, łączenia menu formularza głównego z menu formularza potomnego (ang. menu merging) - można je prześledzić, studiując wspomniany projekt MDIProject.bpr.

Kalkulator - wprowadzenie do projektu

Projekt MiniCalculatorProject.bpr realizuje aplikację oferującą podstawowe funkcje prostego kalkulatora, poszerzone o elementy charakterystyczne dla typowej aplikacji dla Windows. To ważny aspekt projektowy - interfejs użytkownika skonstruowany w konwencji (zrozumiałej dla wszystkich) obsługi kalkulatora uwalnia jednocześnie użytkownika od wielu uciążliwości związanych z tą obsługą; zajmiemy się dokładniej tym zagadnieniem w jednym z następnych podrozdziałów.

Rysunek 3.1 przedstawia widok działającego programu, oczywisty dla każdego, kto choć raz w życiu posługiwał się kalkulatorem, nietrudno jednak zauważyć dodatkowe elementy charakterystyczne nie dla samego kalkulatora, lecz właśnie dla jego realizacji w postaci aplikacji dla Windows.

0x01 graphic

Rysunek 3.1. Kalkulator jako aplikacja dla Windows

W projekcie MiniCalculatorProject.bpr wykorzystano wyłącznie rodzime komponenty C++Buildera - bez uciekania się do komponentów kategorii third-party - co samo w sobie stanowi niezłą ilustrację jego możliwości. Użyte komponenty, w podziale na zawierające je strony palety komponentów, przedstawione zostały w tabeli 3.2.

Tabela 3.2. Komponenty wykorzystane w projekcie MiniCalculatorProject.bpr

Strona palety komponentów

Komponenty

Standard

TActionList

TMainMenu

TPanel

TPopupMenu

Additional

TApplicationEvents

TBevel

TBitBtn

TControlBar

ITImage

TSpeedButton

Win32

TImageList

TStatusBar

Dialog

TColorDialog

Podczas projektowania aplikacji spory wysiłek włożony został w nadanie jej odpowiedniego wyglądu graficznego, w szczególności - w wykonanie niezbędnych do tego ikon i obrazków; posłużono się przy tym pakietem Paint Shop Pro firmy JASC, którego ewaluacyjna wersja 7.00 znajduje się na dołączonej do książki płycie CD-ROM.

UWAGA - w tym miejscu jest mowa o tym, iż na CD-ROMie znajduje się evaluate version PaintShop Pro 7.00

Zwiększanie użyteczności aplikacji drogą sprzężenia zwrotnego

Idea sprzężenia zwrotnego realizowana jest w aplikacjach Windows za pomocą rozmaitych środków, udostępniających użytkownikowi informacje na temat stanu aplikacji lub stopnia zaawansowania wykonywanych przez nią operacji. Do środków tych należą m.in. wszelkiego rodzaju komunikaty (message boxes), paski statusowe (status bars), rozmaite odmiany pasków i wskaźników postępu (progress bars, gauges), a także podpowiedzi (hints) czy sugestywny wygląd kursora, sugerujący użytkownikowi możliwość wykonania określonych operacji w kontekście wskazywanej aktualnie przez ten kursor kontrolki - na przykład kursor w kształcie dłoni sugeruje najczęściej, iż mamy właśnie do czynienia z hiperłączem.

W kolejnych punktach przyjrzymy się dokładniej zasadom funkcjonowania wymienionych przed chwilą komponentów.

Wykorzystanie komponentów TProgressbar i TCGauge

Paski postępu (progress bars) stanowią wygodne narzędzie uwidaczniania stopnia zaawansowania długotrwałych operacji wykonywanych przez aplikacje; w niektórych sytuacjach są one jedynym świadectwem tego, iż dana aplikacja wciąż funkcjonuje, a zlecone jej zadanie, chociaż powoli, wciąż jest realizowane.

Reprezentujący pasek postępu komponent TProgressBar znajduje się na stronie Win32 palety komponentów. Jej strona Samples również zawiera komponent tego rodzaju o nazwie TCGauge. Jest on nieco bardziej zaawansowany od swego kolegi, może bowiem przybierać różne formy geometryczne, zaś wyświetlana przez niego informacja ma bardziej czytelną postać i może zostać wyrażona w formie liczbowej. Segmentowana, „skokowa” natura paska TProgressBar wydaje się nie mieć odniesienia do posuwającej się wciąż naprzód operacji, a wyświetlana przezeń informacja umożliwia co najwyżej szacunkowe określenie ilości wykonanej przez aplikację pracy.

Tym niemniej operacje wykonywane przez obydwa komponenty są do siebie podobne. Zakładając, iż komponenty te noszą w naszej aplikacji nazwy (odpowiednio) ProgressBar i CGauge, możemy ustawić wartości odpowiadające ich wskazaniom minimalnym i maksymalnym oraz bieżącej pozycji:

// minimum

ProgressBar->Min = 0;

CGauge->MinValue = 0;

// maksimum

ProgressBar->Max = 0;

CGauge->MaxValue = 0;

// bieżąca pozycja

ProgressBar->Position = 0;

CGauge->Progress = 0;

Możemy także zwiększyć o 1 wskazanie każdej z kontrolek:

// inkrementacja

ProgressBar->Position = ProgressBar->Position + 1;

CGauge->Progress = CGauge->Progress + 1;

W przypadku obydwu omawianych komponentów próba zwiększenia bieżącego wskazania poza wartość maksymalną nie daje żadnego efektu. TProgressBar dysponuje jednak metodami StepIt() oraz StepBy(), dokonującymi zwiększenia bieżącego wskazania o (odpowiednio) wartość właściwości Step lub specyfikowaną wartość typu int bez respektowania tegoż ograniczenia: wskazanie nie zatrzymuje się na wartości maksymalnej, lecz „zawija się” wokół wskazania minimalnego.

Porównanie możliwości komponentów TProgressBar i TCGauge jest treścią projektu ProgressCursor.bpr, znajdującego się na załączonej płycie CD-ROM. Wybór konkretnej kontrolki jest każdorazowo kwestią konkretnego zastosowania i preferencji projektowych, w każdym razie aplikacja realizująca długotrwałe operacje musi być wyposażona w jakikolwiek element interfejsu, komunikujący użytkownikowi swe funkcjonowanie.

Wygląd kursora

Zmiana wyglądu kursora myszy stosownie do jego położenia i bieżącego stanu aplikacji jest powszechnie stosowanym elementem współczesnych interfejsów użytkownika. W aplikacjach tworzonych z użyciem C++Buildera zmiany wyglądu kursora dokonywać można w dwojaki sposób.

Pierwszy z nich polega na zmianie właściwości typu TCursor kontrolki, w obszarze której kursor przybierać ma określony wygląd - i tak np. właściwość Cursor określa wygląd kursora w sytuacji jego znalezienia się na kontrolce w „zwykłych” warunkach, zaś właściwość DragCursor odpowiedzialna jest za wygląd kursora podczas „przeciągania” kontrolki.

Należy jednak pamiętać, iż wymienione właściwości mają znaczenie jedynie wówczas, gdy „globalny kursor ekranu” (Screen->Cursor) ustawiony jest na wartość domyślną (crDefault). Inna wartość tej właściwości określa wygląd kursora niezależnie od innych okoliczności - i to jest właśnie drugi ze sposobów operowania kursorem. Zmieniając wartość właściwości Screen->Cursor, nie wolno zapomnieć o zachowaniu jej poprzedniej wartości i późniejszym jej przywróceniu:

TCursor OriginalCursor = Screen->Cursor;

Screen->Cursor = crXXXX; //ustalenie nowego wyglądu kursora

try

{

.....

}

__finally

{

Screen->Cursor = OriginalCursor; //odtworzenie poprzedniego wyglądu kursora

}

Metodę tę ilustruje wspomniany już projekt ProgressCursor.bpr.

Definiowanie własnych kursorów

Przed wykorzystaniem niestandardowego kursora należy skojarzyć go z tablicową właściwością Cursors obiektu Screen. Każdy z używanych kursorów reprezentowany jest w tej tablicy przez uchwyt (typ HCURSOR) tworzony w momencie ładowania kursora; ładowanie to realizują funkcje LoadCursor() i LoadCursorFromFile() - pierwsza z nich używana jest w odniesieniu do zasobu (identyfikowanego przez nazwę lub numer), druga ładuje kursor wprost z pliku (określonego przez nazwę) i używana może być do ładowania kursorów animowanych. Pozycje tablicy Cursors identyfikowane ujemnymi indeksami przeznaczone są dla kursorów standardowych, pozycja 0 odpowiada kursorowi domyślnemu (crDefault), tak więc dla kursorów definiowanych przez użytkownika przeznaczone są pozycje o indeksach dodatnich.

Poniższa instrukcja dokonuje załadowania animowanego kursora zapisanego w pliku face.ani i skojarzenia go z elementem tablicy Cursors o indeksie crFaceAnimatedCursor:

Screen->Cursors[crFaceAnimatedCursor] = LoadCursorFromFile("face.ani");

Przypisując odpowiedni indeks do właściwości Screen->Cursor, spowodujemy zmianę globalnej postaci kursora stosownie do zawartości identyfikowanego tym indeksem elementu tablicy Screen->Cursors, tak więc instrukcja:

Screen->Cursor = crFaceAnimatedCursor

spowoduje, iż kursor przybierze wygląd stosowny do (załadowanej) zawartości pliku face.ani. Podstawienie pod właściwość Screen->Cursor wartości 0 (crDefault) spowoduje, iż kursor zmieniał się będzie stosownie do wspomnianych właściwości Cursor i DragCursor znajdujących się pod nim kontrolek.

Funkcja LoadCursor() ładuje stosowny kursor ze wskazanego zasobu. Projekt ProgressCursor.bpr wykorzystuje zasób o nazwie EyeCursor definiowany na bazie pliku Eye.cur w postaci pliku Cursors.rc o następującej zawartości:

EyeCursor CURSOR Eye.cur

Załadowany kursor przypisywany kojarzony jest następnie z indeksem o wartości określonej przez stałą crCustomEyeCursor:

Screen->Cursors[crCustomEyeCursor] = LoadCursor(HInstance, "EyeCursor");

Jak więc widać, zarządzanie własnymi kursorami nie jest skomplikowane, o wiele trudniejsze jest natomiast sporządzanie samych plików zawierających graficzne obrazy kursorów. Można w tym celu wykorzystać edytor graficzny dostarczany wraz z C++Builderem (Image Editor), można też skorzystać z bogatej oferty internetowej w tym względzie. Do problematyki tworzenia zasobów graficznych powrócimy w rozdziale 8.

Wykorzystanie paska statusu TStatusBar

Komponent TStatusBar, znajdujący się na stronie Win32 palety komponentów, jest kolejnym elementem interfejsu użytkownika, realizującym ideę sprzężenia zwrotnego. Posiada on bogate możliwości raportowania informacji o stanie aplikacji oraz dodatkowe możliwości współpracy z myszą.

W najprostszym wariancie TStatusBar ma postać pojedynczego panelu (jego właściwość SimplePanel ustawiona jest wówczas na true), wyświetlającego jedynie informację tekstową (SimpleText = true). W wielu przypadkach jest to zachowanie wystarczające, jednak komponent ten potrafi zdziałać znacznie więcej: może on mianowicie wyświetlać informację na kilku niezależnych panelach, których zawartość podlega tzw. specyficznemu rysowaniu (ang. owner drawing). Pojedynczy panel paska TStatusBar reprezentowany jest przez klasę TStausPanel; najważniejsze właściwości tej klasy przedstawia tabela 3.3.

Tabela 3.3. Najczęściej używane właściwości klasy TStatusPanel

Właściwość

Znaczenie

Alignment

Określa sposób wyrównywania tekstu zawartego pod właściwością Text; możliwe jest wyrównanie do lewej lub prawej krawędzi (taLeftJustify lub taRightJustify) albo centrowanie w poziomie (taCenter).

Bevel

Określa sposób symulowania trójwymiarowego charakteru panelu poprzez uczynienie go wypukłym (pbRaised), wklęsłym (pbLowered) bądź pozostawienie go „płaskim” (pbNone); jeżeli panel podlega rysowaniu specyficznemu, funkcja wykonująca to rysowanie może zniwelować wygląd nadawany przez właściwość Bevel.

Index

Jest właściwością tylko do odczytu i określa indeks panelu w ramach paska statusu (pierwszy panel ma indeks 0). Właściwością komponentu TStatusBar, reprezentującą jego panele, jest kolekcja Panels klasy TStatusPanels wywodzącej się z TCollection. Liczba wszystkich paneli paska ukrywa się pod właściwością Count tej kolekcji, zaś jej poszczególne elementy reprezentowane są przez tablicową właściwość Items[] i do niej właśnie odnosi się indeks zawarty w opisywanej tu właściwości.

Style

Określa sposób wyświetlania panelu i może przyjmować dwie wartości: psText i psOwnerDraw. W pierwszym przypadku (stanowiącym ustawienie domyślne) na panelu wypisywany jest łańcuch znaków zawarty we właściwości Text, z użyciem domyślnej czcionki paska (TStatutsBar.Font) i z uwzględnieniem wyrównywania określonego przez właściwość Alignment. Wartość psOwnerDraw oznacza tzw. rysowanie specyficzne: każdorazowo, gdy panel wymaga odrysowania, generowane jest zdarzenie OnDrawPanel, w ramach którego żądana zawartość panelu powinna być narysowana na płótnie paska statusu, ukrywającym się pod jego właściwością Canvas.

Text

Zawiera łańcuch znaków wypisywany na panelu w sytuacji, gdy jego właściwość Style posiada wartość psText.

Width

Określa szerokość panelu. Wysokość wszystkich paneli jest taka sama i równa właściwości ClientHeight, zawierającego je paska statusu. Ustawienie właściwości Width na 0 powoduje ukrycie panelu - jest to o tyle użyteczne, iż panele paska statusu nie posiadają właściwości Visible.

Dostęp do poszczególnych paneli paska realizowany jest za pośrednictwem jego właściwości Panels. Jest ona kolekcją; liczba wszystkich paneli ukrywa się pod jej właściwością Count, zaś wskaźniki do poszczególnych paneli reprezentowane są przez tablicową właściwość Items[]. Przypisanie wyświetlanego tekstu pierwszemu z paneli paska StatusBar1 będzie więc miało następującą postać:

StatusBar1->Panels->Items[0]->Text = "To jest pierwszy panel";

Nasz przykładowy kalkulator używa paska statusu TStatusBar z trzema panelami. Pierwszy i trzeci z nich podlegają rysowaniu specyficznemu (ich właściwość Style ma wartość psOwnerDraw), funkcja drugiego ogranicza się do wyświetlania tekstu.

Zajmijmy się najpierw pierwszym panelem. Naturalne jest, iż dane do programu mogą być wprowadzane z klawiatury (w tym z klawiatury numerycznej). Użytkownik ma jednak możliwość zablokowania klawiatury, wówczas pozostaje tylko klikanie przycisków kalkulatora. Ponadto, jak wiadomo, naciśnięcia klawiszy w Windows kierowane są do aktualnego okna; jeżeli więc mowa o wykorzystaniu klawiatury do sterowania kalkulatorem, to istotną może być kwestia, czy okno kalkulatora jest w danej chwili oknem skupionym (focused). Trzy wynikające stąd stany aplikacji („zablokowana” klawiatura, okno skupione, brak skupienia) odzwierciedlane są na omawianym panelu za pomocą trzech obrazków reprezentowanych przez komponenty TImage o nazwach (odpowiednio): DisableKeyboardImage, HasKeyboardFocusImage i HasNoKeyboardFocusImage. W celu wyświetlania tych obrazków należy odpowiednio zaprogramować funkcję obsługi zdarzenia OnDrawPanel. Funkcja ta posiada trzy parametry, którymi są (kolejno): wskaźnik do paska statusu, wskaźnik do odnośnego panelu i referencja do prostokąta obejmującego ten panel (w ramach paska):

TStatusBar* StatusBar,

TStatusPanel Panel,

const TRect& Rect

Czynność „rysowania” sprowadzać się będzie do wyświetlenia żądanego obrazka na płótnie paska (reprezentowanym przez jego właściwość Canvas) w miejscu określonym przez prostokąt panelu. Oto fragment funkcji zdarzeniowej obsługującej rysowanie specyficzne paneli - dla przejrzystości usunięto z niej kod związany z pozostałymi dwoma panelami:

Wydruk 3.1. Funkcja obsługująca rysowanie specyficzne paneli paska statusowego kalkulatora

void __fastcall TMainForm::StatusBar1DrawPanel(TStatusBar *StatusBar,

TStatusPanel *Panel, const TRect &Rect)

{

if(Panel->Index == 0)

{

if(EnableKeyboardInput)

{

if(CanUseKeyboard)

{

// narysuj na panelu klawiaturę

StatusBar->Canvas->Draw(Rect.Left,

Rect.Top,

HasKeyboardFocusImage->Picture->Bitmap);

}

else

{

// narysuj na panelu klawiaturę na przyciemnionym tle

StatusBar->Canvas->Draw(Rect.Left,

Rect.Top,

HasNoKeyboardFocusImage->Picture->Bitmap);

}

}

else

{

// narysuj na panelu przekreśloną klawiaturę

StatusBar->Canvas->Draw(Rect.Left,

Rect.Top,

DisableKeyboardImage->Picture->Bitmap);

}

} // koniec rysowania pierwszego panelu

else

......

}

Treść powyższej funkcji nie jest szczególnie skomplikowana. Najpierw upewniamy się, czy została ona wywołana w związku z rysowaniem pierwszego panelu - sprawdzając, czy indeks panelu w ramach paska równy jest zero. Potem następuje wybór jednego z trzech obrazków - kryterium tego wyboru stanowi wartość dwóch właściwości boolowskich EnableKeyboardInput i CanUseKeyboard (właściwości, nie zmiennych - zajmiemy się tym za chwilę) - pierwsza z nich decyduje o tym, czy użytkownik dopuścił wprowadzanie z klawiatury, a jeżeli tak, to druga określa, czy aplikacja w ogóle przyjmuje zdarzenia z klawiatury (tzn. czy jest aplikacją aktywną). Metoda rysowania obrazków jest taka sama niezależnie od tego, który obrazek zostaje wybrany do rysowania. Rysowanie odbywa się na płótnie paska (Canvas) i realizowane jest przez metodę Draw() tegoż płótna. Metoda ta dokonuje rysowania zawartości komponentu klasy TGraphic lub pochodnej (w tym przypadku TBitmap) i oprócz wskaźnika do samego komponentu wymaga podania współrzędnych punktu, w którym narysowany zostanie lewy górny róg obrazka; w naszym przypadku są to współrzędne lewego górnego rogu prostokąta przekazanego do metody StatusBar1DrawPanel() w postaci trzeciego parametru. W naszym projekcie wysokość paska statusowego wynosi 30 pikseli, zaś szerokość pierwszego panelu - 72 piksele. Rozmiary te obejmują jednopikselowe obrzeże (bevel), decydujące o „wklęsłości” panelu oraz dwupikselowy odstęp pomiędzy górnymi krawędziami paska i panelu (patrz rys. 3.2). Tak więc prostokąt przekazany do funkcji zdarzeniowej będzie miał rozmiary następujące:

wysokość = StatusBar->Height - 4 - 2*StatusBat->BorderWidth;

szerokość = Panel->Width - 2;

Wyliczenia te dają prostokąt o wymiarach 70×26 pikseli - i taka jest też wielkość każdego z trzech obrazków.

Tutaj proszę umieścić rysunek znajdujący się w pliku ORIG-5-2.BMP i uzupełnić go opisami ze strony 122 oryginału, z następującymi zmianami:

image is 70×26 pikels -> obrazek: 70×26 pikseli

2 pixels -> 2 piksele

Rysunek 3.2. Wyliczenia rozmiarów obrazka rysowanego na pierwszym panelu

Pasek statusu kalkulatora jest również wrażliwy na klikanie - kliknięcie w pierwszy panel (dokładniej - naciśnięcie lewego przycisku myszy w obrębie pierwszego panelu) powoduje zmianę statusu „zezwolenia na użycie klawiatury”, reprezentowanego przez właściwość EnableKeyboardInput. Naciśnięcie któregokolwiek klawisza myszy powoduje wygenerowanie zdarzenia OnMouseDown. Funkcja obsługująca to zdarzenie musi sprawdzić, czy kursor znajduje się w obrębie pierwszego panelu i czy naciśnięto lewy klawisz - jeżeli obydwa te warunki są spełnione, wartość właściwości EnableKeyboardInput zostaje zmieniona na przeciwną (patrz wydruk 3.2):

Wydruk 3.2. Obsługa kliknięcia w pierwszy panel paska statusu

void __fastcall TMainForm::StatusBar1MouseDown(TObject *Sender,

TMouseButton Button, TShiftState Shift, int X, int Y)

{

if(Button == mbLeft) // czy naciśnięto lewy przycisk?

{

// Czy kursor myszki znajduje się wewnątrz pierwszego panelu

// z wyłączeniem obrzeża?

if( X > 1

&& X < (StatusBar1->Panels->Items[0]->Width - 1)

&& Y > 3

&& Y < (StatusBar1->Height - 1) )

{

// jeżeli tak, to zaneguj wartość właściwości EnableKeyboardInput

if(EnableKeyboardInput) EnableKeyboardInput = false;

else EnableKeyboardInput = true;

}

}

}

Ponowne przyjrzenie się rysunkowi 3.2 wyjaśni sens obliczeń wykonywanych w powyższej metodzie.

Każdorazowej zmianie właściwości EnableKeyboardInput towarzyszy wywołanie jej funkcji dostępowej Set…(patrz wydruk 3.3):

Wydruk 3.3. Funkcja dostępowa zmieniająca wartość właściwości EnableKeyboardInput

void __fastcall TMainForm::SetEnableKeyboardInput(bool NewEnableKeyboardInput)

{

if(EnableKeyboardInput != NewEnableKeyboardInput)

{

FEnableKeyboardInput = NewEnableKeyboardInput;

if(EnableKeyboardInput)

{

OnKeyDown = CalculatorKeyDown;

OnKeyUp = CalculatorKeyUp;

StatusBar1->Panels->Items[1]->Width = 60;

}

else

{

OnKeyDown = 0;

OnKeyUp = 0;

StatusBar1->Panels->Items[1]->Width = 0;

}

StatusBar1->Invalidate();

}

}

Treść funkcji pokazuje wyraźnie, w jaki sposób aplikacja staje się niewrażliwa na naciskanie klawiszy: naciśnięcie klawisza powoduje wygenerowanie zdarzenia OnKeyDown, zaś zwolnienie klawisza - OnKeyUp; przy „czynnej” klawiaturze zdarzenia te obsługiwane są przez funkcje CalculatorKeyDown i CalculatorKeyUp, przy nieczynnej natomiast - zupełnie pozbawione są obsługi.

W sytuacji, gdy aplikacja niewrażliwa jest na klawiaturę, nie jest potrzebny drugi panel paska statusu, wyświetlający (w większości przypadków) oznaczenie klawisza, nad którym aktualnie znajduje się kursor myszki; panel ten jest wówczas ukrywany poprzez nadanie mu zerowej szerokości.

Wywołanie metody Invalidate() stanowi sygnał dla kontrolki, iż jej obraz graficzny mógł ulec zniekształceniu i konieczne jest jego odtworzenie; w powyższej funkcji sygnał taki wysyłany jest do paska statusu w celu zaktualizowania wyglądu jego paneli.

Rysowaniu specyficznemu podlega również trzeci panel paska statusu. Służy on do wyświetlania opisu klawisza, nad którym znajduje się kursor - znaczenie niektórych klawiszy, na przykład tych niebieskich, reprezentujących kolejno: liczbę π, podstawę logarytmu naturalnego, stałą Plancka, stałą Boltzmanna i prędkość światła, nie musi być oczywiste dla każdego użytkownika. Tak się składa, iż szerokość tego panelu jest niekiedy niewystarczająca do wyświetlenia danego napisu w całości; w takim przypadku napis ten jest nie tylko obcinany do szerokości panelu (w tym nie byłoby nic nadzwyczajnego), lecz także uzupełniany wielokropkiem i to w taki sposób, by wielokropek ten zmieścił się jeszcze na panelu. Wyświetlania takiego dokonuje funkcja z biblioteki Win32 API, nosząca nazwę DrawText - jest ona na tyle często używana, iż poświęcimy jej tutaj kilka słów. Jej deklaracja wygląda następująco:

int DrawText(

HDC hDC, // uchwyt płótna

LPCTSTR lpString, // łańcuch do wyświetlenia

int nCount, // długość wyświetlanego łańcucha lub -1

LPRECT lpRect, // wskaźnik do prostokąta wyświetlania

UINT uFormat // znaczniki

);

Pierwszy parametr jest uchwytem płótna (Canvas), ukrywającym się pod jego właściwością Handle. Drugi i trzeci parametr określają wskaźnik i długość wyświetlanego łańcucha; jeżeli trzeci parametr ma wartość -1, to o długości łańcucha decyduje zerowy ogranicznik. Czwarty parametr wskazuje na strukturę określającą współrzędne (względem lewego górnego rogu płótna) prostokąta ograniczającego obszar wyświetlania, zaś znaczenie poszczególnych znaczników, których sumą logiczną jest ostatni parametr, przedstawia tabela 3.4.

Tabela 3.4. Znaczniki określające szczegóły zachowania funkcji DrawText()

Znacznik

Znaczenie

DT_CALCRECT

Nie jest wyświetlany żaden tekst, natomiast do struktury określonej przez parametr lpRect wpisywane są wymiary prostokąta niezbędnego do pomieszczenia tekstu określonego przez parametry lpString i nCount.

DT_LEFT

Wyświetlany tekst wyrównywany jest do lewej strony.

DT_CENTER

Wyświetlany tekst centrowany jest w poziomie.

DT_RIGHT

Wyświetlany tekst wyrównywany jest do prawej strony.

DT_BOTTOM

Wyświetlany tekst wyrównywany jest do dolnej krawędzi prostokąta; znacznik ten musi być stosowany łącznie ze znacznikiem DT_SINGLELINE.

DT_VCENTER

Wyświetlany tekst centrowany jest w pionie; znacznik ten musi być stosowany łącznie ze znacznikiem DT_SINGLELINE.

DT_TOP

Wyświetlany tekst wyrównywany jest do górnej krawędzi prostokąta; znacznik ten musi być stosowany łącznie ze znacznikiem DT_SINGLELINE.

DT_SINGLELINE

Tekst wyświetlany jest w jednym wierszu, ewentualne znaki powrotu karetki (ASCII 13) i nowego wiersza (ASCII 10) nie powodują łamania wierszy.

DT_END_ELLIPSIS

Jeżeli tekst jest zbyt długi do wyświetlenia w ramach zadanego prostokąta, zostaje ucięty i uzupełniony wielokropkiem; jeżeli równocześnie użyty jest znacznik DT_MODIFYSTRING, tak zmodyfikowany łańcuch podstawiany jest w miejsce oryginalnego łańcucha wskazywanego przez lpString.

DT_PATH_ELLIPSIS

Intencją tego znacznika jest eleganckie skracanie zbyt długich specyfikacji ścieżek dostępu do plików: początek i koniec łańcucha jest zachowywany, zastępowaniu wielokropkiem podlegają natomiast poszczególne człony ścieżki ograniczone lewymi ukośnikami (\). Nazwa właściwa pliku jest w miarę możności zachowywana bez skrótów - po ostatnim lewym ukośniku zachowywane jest tak dużo tekstu, jak tylko jest to możliwe.
Jeżeli równocześnie użyty jest znacznik DT_MODIFYSTRING, tak zmodyfikowany łańcuch podstawiany jest w miejsce oryginalnego łańcucha wskazywanego przez lpString.

DT_MODIFYSTRING

Jeżeli równocześnie użyty jest znacznik DT_END_ELLIPSIS lub DT_PATH_ELLIPSIS, oryginalny łańcuch wskazywany przez lpString zastępowany jest łańcuchem faktycznie wyświetlanym.

DT_EDITCONTROL

Tekst wyświetlany jest w sposób charakterystyczny dla wielowierszowej kontrolki edycyjnej: częściowo widoczne wiersze końcowe nie są wyświetlane, obliczana jest także średnia szerokość znaku.

DT_EXPANDTABS

Znaki tabulacji zastępowane są ciągami spacji - standardowo osiem spacji na każdą tabulację, przelicznik ten można zmienić za pomocą znacznika DT_TABSTOP.

DT_EXTERNALLEADING

Do wysokości wiersza wyświetlania wliczana jest interlinia (ang. external leading).

DT_NOCLIP

Tekst wyświetlany jest bez obcinania na granicach prostokąta; wyświetlanie przebiega wówczas nieco szybciej.

DT_NOPREFIX

Użycie tego znacznika powoduje, iż ampersandy (&) w wyświetlanym łańcuchu traktowane są na równi z innymi znakami. Standardowo pojedynczy ampersand nie jest wyświetlany, lecz traktowany jako polecenie podkreślenia następującego po nim znaku, zaś para ampersandów powoduje wyświetlenie jednego ampersanda.

DT_RTLREADING

Znacznik ten sygnalizuje, iż wyświetlany tekst czytany będzie od strony prawej do lewej, jeżeli aktualna czcionka płótna jest czcionką o takiej orientacji.

DT_TABSTOP

Użycie tego znacznika powoduje, iż zawartość bardziej znaczącego bajtu uFormat (bity 8 - 15) traktowana jest jako przelicznik znaków tabulacji na spacje, gdy użyty jest znacznik DT_EXPANDTABS. Standardowo każdy znak tabulacji zamieniany jest na osiem spacji.

DT_WORDBREAK

Jeżeli tekst jest zbyt długi do wyświetlenia, łamany jest pomiędzy poszczególnymi słowami. Sekwencje „powrót karetki - nowy wiersz” (ASCII 13 - ASCII 10) również traktowane są jako polecenia łamania wierszy.

Wyświetlanie trzeciego panelu paska statusowego realizowane jest w przytaczanej już funkcji obsługi zdarzenia OnDrawPanel:

Wydruk 3.4. Wyświetlanie trzeciego panelu paska statusowego

void __fastcall TMainForm::StatusBar1DrawPanel(TStatusBar *StatusBar,

TStatusPanel *Panel, const TRect &Rect)

{

if(Panel->Index == 0)

{

.....

}

else if(Panel->Index == 2)

{

TFontStyles FontStyle;

TColor OldBrushColor = StatusBar->Canvas->Brush->Color;

TFontStyles OldFontStyle = StatusBar->Canvas->Font->Style;

StatusBar->Canvas->Font->Style = FontStyle;

StatusBar->Canvas->Brush->Color = clSilver;

StatusBar->Canvas->FillRect(Rect);

TRect PanelRect = Rect;

PanelRect.Left += 2;

PanelRect.Right -= 2,

DrawText( StatusBar->Canvas->Handle,

Panel->Text.c_str(),

-1,

&PanelRect,

DT_LEFT

|DT_NOPREFIX

|DT_END_ELLIPSIS

|DT_SINGLELINE

|DT_VCENTER

|DrawTextBiDiModeFlagsReadingOnly() );

StatusBar->Canvas->Font->Style = OldFontStyle;

StatusBar->Canvas->Brush->Color = OldBrushColor;

}

}

Wyświetlanie to odbywa się w czterech głównych etapach:

  1. Przechowywane są aktualne wartości tych właściwości płótna (StatusBar->Canvas), które zostaną zmienione w procesie wyświetlania.

  2. Zmniejszane są rozmiary obszaru wyświetlania (PanelRect) tak, by uwzględnić obrzeże (bevel) o szerokości 1 piksela. Ustawiany jest także styl czcionki, a tło panelu wypełniane jest żądanym kolorem.

  3. Wywoływana jest funkcja DrawText(); tekst wyświetlany jest w pojedynczym wierszu (DT_SINGLELINE), wyrównany lewostronnie (DT_LEFT) i wycentrowany w pionie (DT_VCENTER), fakt jego obcięcia sygnalizowany jest wielokropkiem (DT_END_ELLIPSIS), a ewentualnym ampersandom nie przypisuje się szczególnego znaczenia (DT_NOPREFIX). Aby prawidłowo wykryć przypadek, w którym aktualna czcionka płótna jest czcionką o odwrotnej orientacji (od prawej do lewej), wywoływana jest funkcja DrawTextBiDiModeFlagsReadingOnly(), zwracająca w takim przypadku wartość DT_RTLREADING (dla pozostałych czcionek funkcja ta zwraca 0).

  4. Przywracane są poprzednie ustawienia właściwości płótna.

Funkcja DrawText() stanowi wygodny środek wypisywania tekstu w prostokątnych obszarach płótna, zaś jej znaczniki pomagają uporać się z różnymi problemami występującymi przy tej okazji. Będziemy z niej wielokrotnie korzystać w tym rozdziale, warto więc przyswoić sobie znaczenie jej parametrów i poszczególnych znaczników.

Podpowiedzi kontekstowe

Podpowiedzi kontekstowe (ang. hints) to doraźnie wyświetlane informacje pojawiające się w przypadku chwilowego zatrzymania kursora myszy na określonej kontrolce. Treść podpowiedzi związanej z daną kontrolką ukrywa się pod jej właściwością Hint; o tym, czy w stosownej chwili rzeczywiście zostanie ona wyświetlona, decydują właściwości boolowskie ShowHint i ParentShowHint.

Gdy ParentShowHint = false, kontrolka sama decyduje o wyświetlaniu swej podpowiedzi: istotna jest mianowicie wartość jej właściwości ShowHint. Gdy ParentShowHint = true, istotna jest jedynie wartość właściwości ShowHint kontrolki rodzicielskiej (parent). Zasada ta ułatwia zaprogramowanie jednolitego zachowania się całego formularza pod względem wyświetlania lub niewyświetlania podpowiedzi.

Typowa podpowiedź kontekstowa dzieli się na dwie części, nazywane odpowiednio częścią krótką (short hint) i długą (long hint). Części te oddzielone są od siebie (w treści właściwości Hint) znakiem pionowej kreski (|), a ich nazwy nie wynikają wbrew pozorom z ich długości, gdyż ta nie jest jakoś specjalnie ograniczona. Po prostu krótka część podpowiedzi wyświetlana jest w pobliżu kursora i jako taka ma raczej zwięzłą (a więc - krótką) postać; długa część podpowiedzi jest natomiast zwykle przeznaczona do wyświetlenia na pasku statusowym, może mieć więc formę bardziej opisową (a więc - bywa zazwyczaj dłuższa). Automatycznemu wyświetlaniu podlega jedynie krótka część podpowiedzi, długa natomiast musi być obsłużona w ramach zdarzenia OnHint obiektu Application, na przykład w celu wyświetlania jej na pasku statusowym, jak w poniższym fragmencie:

void __fastcall TMainForm::FormCreate(TObject* /*Sender*/)

{

.....

Application->OnHint = &ShowHint;

.....

}

....

void __fastcall TMainForm::ShowHint(TObject* /*Sender*/)

{

StatusBar->SimpleText = Application->Hint;

}

....

Uważny czytelnik zapytać mógłby w tej chwili, dlaczego wartość właściwości Hint pobierana jest z obiektu Application, nie zaś z odnośnej kontrolki? Otóż ta ostatnia automatycznie dokonuje kopiowania do tegoż obiektu długiej części swej podpowiedzi w sytuacji, gdy podpowiedź ta ma być wyświetlona. Notabene, jeżeli w tekście podpowiedzi brak jest pionowej kreski rozdzielającej (|), to cała podpowiedź traktowana jest zarówno jako krótka, jak i długa część, to znaczy podlega w całości automatycznemu wyświetleniu i w całości przekazywana jest do zdarzenia OnHint obiektu Application.

Obiekt Application posiada kilka właściwości umożliwiających regulowanie pewnych aspektów zachowania się podpowiedzi kontekstowych danej aplikacji. Właściwości te zestawione są w tabeli 3.5.

Tabela 3.5. Właściwości obiektu Application mające związek w podpowiedziami kontekstowymi

Właściwość

Znaczenie

Hint

Zawiera długą część podpowiedzi kontekstowej podlegającej ostatnio wyświetleniu.

HintColor

Określa kolor tła, na którym wyświetlane będą podpowiedzi kontekstowe; domyślnie jest to systemowy kolor clInfoBk.

HintHidePause

Określa (w milisekundach) maksymalny czas wyświetlania „krótkiej” podpowiedzi (w czasie gdy kursor myszy wciąż pozostaje w obrębie kontrolki). Wartością domyślną jest 2500 milisekund.

HintPause

Określa (w milisekundach) zwłokę czasową pomiędzy zatrzymaniem kursora na kontrolce, a początkiem wyświetlania podpowiedzi. Wartością domyślną jest 500 milisekund.

HintShortPause

Określa (w milisekundach) minimalną przerwę pomiędzy wyświetleniem dwóch różnych podpowiedzi. Wartością domyślną jest 500 milisekund.

ShowHint

Warunkuje wyświetlanie podpowiedzi kontekstowych w aplikacji - gdy ma wartość false, w całej aplikacji nie są wyświetlane żadne podpowiedzi niezależnie od właściwości Hint, ShowHint i ParentShowHint poszczególnych kontrolek. Domyślną wartością jest true, co oznacza normalne funkcjonowanie podpowiedzi.

W uzupełnieniu do powyższej tabeli należy wspomnieć o metodzie ActivateHint() obiektu Application. Jest ona zadeklarowana w pliku Include\Vcl\Forms.hpp jako:

void __fastcall ActivateHint(const Windows::TPoint& CursorPos);

i powoduje wyświetlenie podpowiedzi kontekstowej związanej z kontrolką znajdującą się we wskazanym punkcie ekranu; bieżącą pozycję kursora myszy odczytać można z właściwości CursorPos globalnego obiektu Mouse:

Application->ActivateHint(Mouse->CursorPos);

Ponadto właściwość HintFont globalnego obiektu Screen umożliwia określenie czcionki, którą wyświetlane będą podpowiedzi kontekstowe w całej aplikacji.

Zaawansowane sterowanie podpowiedziami kontekstowymi

W naszym przykładowym projekcie kalkulatora każdy z trzech paneli paska statusowego wymaga innej podpowiedzi kontekstowej; stwarza to pewien problem, bowiem poszczególne panele nie posiadają odrębnych właściwości Hint - właściwość ta jest wspólna dla całego paska statusowego. Rozwiązaniem tego problemu jest oczywiście różnicowanie treści podpowiedzi w zależności od tego, nad którym panelem znajduje się kursor myszy, pozostaje jednak problem znacznie poważniejszy. Otóż „kontrolką, nad którą aktualnie znajduje się kursor” jest pasek statusowy jako całość; jeżeli więc zatrzymamy kursor na jednym z paneli i ujrzymy podpowiedź kontekstową, to po przejściu na inny panel nowa podpowiedź nie zostanie wyświetlona - obecność kursora w obrębie paska statusowego została już bowiem skwitowana stosowną podpowiedzią. Należałoby wówczas usunąć na chwilę kursor z paska statusowego, a następnie ustawić go nad drugim panelem; takie rozwiązanie jest oczywiście nie do przyjęcia - chcielibyśmy, aby poszczególne panele zachowywały się (pod względem podpowiedzi kontekstowych) jak niezależne kontrolki.

Aby sprostać tak postawionym wymaganiom, nie pozostaje nam nic innego, jak pełne kontrolowanie ruchu kursora myszy w obrębie paska statusowego, czyli samodzielna obsługa generowanego w jego kontekście zdarzenia OnMouseMove. Oto stosowna funkcja obsługi tego zdarzenia:

Wydruk 3.5. Kontrolowanie ruchu kursora w obrębie paska statusowego

void __fastcall TMainForm::StatusBar1MouseMove(TObject *Sender,

TShiftState Shift, int X, int Y)

{

int BorderWidth = StatusBar1->BorderWidth;

TRect Panel0(StatusBar1->ClientOrigin.x + BorderWidth + 1, // Lewa krawędź

StatusBar1->ClientOrigin.y + 3 + BorderWidth, // Górna krawędź

StatusBar1->ClientOrigin.x + BorderWidth // Prawa krawędź

+ StatusBar1->Panels->Items[0]->Width - 1,

StatusBar1->ClientOrigin.y + 3 // Dolna krawędź

+ StatusBar1->Height - 1);

TRect Panel1(Panel0.Right + 2 + 1, // Lewa krawędź

Panel0.Top, // Górna krawędź

// taka sama jak

// w Panel0

Panel0.Right + 2

+ StatusBar1->Panels->Items[1]->Width - 1, // Prawa krawędź

Panel0.Bottom); // Dolna krawędź

// taka sama jak

// w Panel0

TRect Panel2(Panel1.Right + 2 + 1, // Lewa krawędź

Panel0.Top, // Górna krawędź

// taka sama jak

// w Panel0

Panel1.Right + 2

+ StatusBar1->Panels->Items[2]->Width - 1, // Prawa krawędź

Panel0.Bottom); // Dolna krawędź

// taka sama jak

// w Panel0

// Określ, nad którym panelem znajduje się kursor

// i ustaw właściwą treść podpowiedzi; jeżeli nie znajduje

// się nad żadnym panelem, ustaw pusty łańcuch

// funkcja PtInRect sprawdza, czy podany punkt znajduje się

// wewnątrz podanego prostokąta

//BOOL PtInRect(

// CONST RECT *lprc, // wskaźnik do prostokąta

// POINT pt // punkt

// );

if(PtInRect(&Panel0, Mouse->CursorPos))

{

if(EnableKeyboardInput) StatusBar1->Hint =

"Click to Disable Keyboard Input|";

else StatusBar1->Hint =

"Click to Enable Keyboard Input|";

if(StatusBar1->Tag != 0)

{

StatusBar1->Tag = 0;

Application->ActivateHint(Mouse->CursorPos); //***

}

}

else if(PtInRect(&Panel1, Mouse->CursorPos))

{

StatusBar1->Hint = "Keyboard Short Cut|";

if(StatusBar1->Tag != 1)

{

StatusBar1->Tag = 1;

Application->ActivateHint(Mouse->CursorPos); //***

}

}

else if(PtInRect(&Panel2, Mouse->CursorPos))

{

StatusBar1->Hint = "Button Function|";

if(StatusBar1->Tag != 2)

{

StatusBar1->Tag = 2;

Application->ActivateHint(Mouse->CursorPos); //***

}

}

else

{

// bez podpowiedzi

StatusBar1->Tag = StatusBar1->Panels->Count;

StatusBar1->Hint = "";

}

}

Zwróć uwagę na to, w jaki sposób konstruowane są prostokąty reprezentujące poszczególne panele: uwzględniane są mianowicie dwupikselowe separatory pomiędzy panelami, jak również jednopikselowe obrzeże każdego z nich, nie zaliczane w skład panelu. Pouczające może być skonfrontowanie tych wyliczeń z rysunkiem 3.2.

Dysponując precyzyjnie określonymi granicami paneli, wystarczy teraz odczytać bieżącą pozycję kursora myszy (Mouse->CursorPos), sprawdzić, nad którym panelem się on znajduje (jest co najwyżej jeden taki panel, bowiem poszczególne panele są nawzajem rozłączne), ustawić odpowiednią dla tego panelu treść właściwości Hint paska statusowego i wyświetlić ją za pomocą metody ActivateHint obiektu Application.

Zwróćmy przy okazji uwagę na to, do czego wykorzystywana jest właściwość Tag paska statusowego. Zawiera ona mianowicie indeks panelu (0, 1 lub 2), w kontekście którego nastąpiło wygenerowanie podpowiedzi; w sytuacji, gdy kursor znajdował się ostatnio poza którymkolwiek panelem (lecz w obrębie paska) właściwość ta zawiera liczbę paneli (3), a więc wartość, która nie jest indeksem żadnego panelu. Jeżeli teraz, w reakcji na ruch kursora myszy, wywołana zostanie powyższa funkcja zdarzeniowa, przed wywołaniem funkcji Application->ActivateHint() konfrontuje się indeks aktualnego panelu z wartością StatusBar1->Tag, co zabezpiecza przed permanentnym jej wywoływaniem przy każdym ruchu kursora. Na uwagę zasługuje jeszcze jeden ciekawy szczegół - otóż nieprzypadkowo wszystkie panele tak są zlokalizowane w obrębie paska statusowego, iż kursor myszy, by znaleźć się nad którymkolwiek panelem, chcąc nie chcąc musi przejść przez obszar nie należący do żadnego panelu; eliminuje to możliwość, iż właściwość Tag użyta do porównania z indeksem panelu będzie miała wartość przypadkową - przy pierwszym ze wspomnianych porównań będzie ona miała wartość 3.

Po dokładniejszej analizie treści opisywanej funkcji zdarzeniowej można zauważyć, iż generowane w jej ramach podpowiedzi kontekstowe wykazują pewną specyficzną cechę zachowania: otóż funkcja Application->ActivateHint() wywoływana jest natychmiast po znalezieniu się kursora w obrębie danego panelu, bez zachowania zwyczajowej zwłoki czasowej (określonej przez właściwość Application->HintPause - patrz tabela 3.5). Być może nie jest to zbytnią uciążliwością dla przeciętnego użytkownika aplikacji, w każdym jednak razie uporanie się z tym problemem może być na swój sposób pouczające.

Aby spowodować wyświetlenie podpowiedzi po określonej zwłoce czasowej, konieczne jest posłużenie się funkcjami zegarowymi. Jedną z takich funkcji jest funkcja SetTimer() z biblioteki Win32 API, instalująca funkcję zwrotną (ang. callback function) wywoływaną asynchronicznie po upłynięciu określonego interwału czasowego; właśnie w ramach tak zainstalowanej funkcji można by dokonywać wyświetlania podpowiedzi. Aby zrealizować tak postawione zadanie, konieczne jest zastosowanie kilku innych jeszcze funkcji; niezbędne deklaracje przedstawia wydruk 3.6.

Wydruk 3.6. Deklaracje funkcji niezbędnych do zaimplementowania zwłoki czasowej w wyświetlaniu podpowiedzi

UINT HintTimerHandle;

static void CALLBACK HintTimerCallback(HWND Wnd,

UINT Msg,

UINT TimerID,

DWORD Time);

void __fastcall HintTimerExpired();

void __fastcall DisplayHint(int Pause);

void __fastcall StopHintTimer();

Pierwsza z deklarowanych funkcji - HintTimerCallback() - zaimplementowana zostanie jako statyczna funkcja w klasie formularza głównego; jest to konieczne, bowiem jej wskaźnik przekazany zostanie jako parametr do funkcji Win32 API, musi być więc „zwykłym” wskaźnikiem funkcyjnym. Aby możliwe było zatrzymanie uruchomionego zegara, konieczne jest zapamiętanie uchwytu zwracanego przez funkcję SetTimer(); temu celowi służy zmienna HintTimerHandle.

Implementację zadeklarowanych wyżej funkcji przedstawia wydruk 3.7.

Wydruk 3.7. Implementacja funkcji realizujących zwłokę czasową w wyświetlaniu podpowiedzi

void CALLBACK TMainForm::HintTimerCallback(HWND Wnd,

UINT Msg,

UINT TimerID,

DWORD Time)

{

TObject* VCLObject = reinterpret_cast<TObject*>(Wnd);

TMainForm* Form1Object = dynamic_cast<TMainForm*>(VCLObject);

if(Form1Object) Form1Object->HintTimerExpired();

}

//---------------------------------------------------------------------//

//- -------------------------------------------------------------------//

//

// Funkcje zegarowe

//

//--------------------------------------------------------------------//

void __fastcall TMainForm::HintTimerExpired()

{

StopHintTimer();

Application->ActivateHint(Mouse->CursorPos);

}

//--------------------------------------------------------------------//

void __fastcall TMainForm::DisplayHint(int Pause)

{

StopHintTimer();

HintTimerHandle = SetTimer(this,

0,

Pause,

reinterpret_cast<TIMERPROC>(HintTimerCallback));

if(HintTimerHandle == 0) Application->CancelHint();

}

----------------------------------------------------------------------//

void __fastcall TMainForm::StopHintTimer()

{

if(HintTimerHandle != 0)

{

KillTimer(this, HintTimerHandle);

HintTimerHandle = 0;

}

}

//-------------------------------------------------------------------//

Cała zabawa rozpoczyna się od funkcji DisplayHint(). Jest ona wywoływana w funkcji StatusBar1MouseMove() zamiast funkcji ActivateHint(), a jej argumentem jest właśnie żądane opóźnienie - w związku z tym instrukcje oznaczone na wydruku 3.5 znacznikami //*** należy zmienić z dotychczasowej postaci:

Application->ActivateHint(Mouse->CursorPos);

na:

DisplayHint(Application->HintPause);

Kiedy funkcja DisplayHint() zostanie wywołana, rozpoczyna swą pracę od zatrzymania ewentualnie funkcjonującego zegara uruchomionego przy poprzednim wywołaniu. Następnie uruchamia nowy zegar, wywołując funkcję SetTimer() z parametrami reprezentującymi kolejno: obiekt formularza, identyfikator zdarzenia (nieistotny), żądany interwał czasowy i wskaźnik do funkcji zwrotnej (ze względów składniowych rzutowany na typ TIMEPROC). Jeżeli uruchomienie zegara nie uda się, funkcja SetTimer() zwróci wartość 0 i cała sprawa skończy się bez wyświetlenia podpowiedzi; należy wówczas skasować ewentualną wyświetlaną właśnie podpowiedź - służy do tego funkcja Application->CancelHint(). Jeżeli jednak zegar zostanie uruchomiony, funkcja zwróci jego niezerowy uchwyt, który zapamiętany zostanie pod zmienną HintTimerHandle.

Gdy upłynie zadany czas, wywołana zostanie funkcja zwrotna HintTimerCallback(). Posiada ona cztery parametry, z których w tej chwili najważniejszy jest pierwszy, zawierający wskaźnik do instancji formularza. Parametr ten jest jednakże postrzegany przez wywołaną funkcję jako liczba całkowita (UINT), musi by więc rzutowany na typ TForm1*. Można by tego dokonać w jednym akcie rzutowania reinterpretacyjnego, w naszym przykładzie wykonamy jednak tę czynność w dwóch etapach: najpierw za pomocą rzutowania reinterpretacyjnego uczynimy z naszego parametru wskaźnik typu TObject*, następnie za pomocą rzutowania dynamicznego przekształcimy go we wskaźnik typu TForm1*. Wykonalność tego ostatniego rzutowania upewni nas, iż to, co zawarte jest w parametrze wywołania funkcji, istotnie jest poprawnym wskaźnikiem formularza.

Skoro mamy już wskaźnik formularza głównego, wywołajmy jego funkcję składową HintTimerExpired(). Zatrzyma ona najpierw działający wciąż zegar, a następnie wywoła znajomą funkcję Application->ActivateHint().

Zatrzymanie zegara realizowane jest przez funkcję StopHintTimer(). Sprawdza ona, czy zegar aktualnie funkcjonuje (świadczy o tym niezerowy uchwyt przechowywany w zmiennej HintTimerHandle) i jeżeli tak, dokonuje jego zatrzymania za pomocą funkcji KillTimer() oraz zeruje zmienną HintTimerHandle.

W ten oto sposób sprawiliśmy, iż generowane przez nas podpowiedzi nie odróżniają się już od tych generowanych w sposób standardowy.

Podpowiedzi definiowane przez użytkownika

Podpowiedzi kontekstowe w standardowej postaci nie zawsze są wystarczające dla użytkownika, który za ich pomocą chciałby zrealizować pewne specyficzne funkcje w tworzonej przez siebie aplikacji. W naszym projekcie kalkulatora zastosowaliśmy taką niestandardową formę podpowiedzi dla wyświetlenia zawartości pamięci, gdy kursor zatrzyma się na przycisku oznaczonym „MR” - podpowiedź standardowa byłaby tu równie komunikatywna, chcieliśmy jednak zachować formę wyświetlenia charakterystyczną dla głównego wyświetlacza.

Zmianę standardowego wyglądu i zachowania podpowiedzi kontekstowych można osiągnąć, przedefiniowując standardową dla podpowiedzi klasę okna THintWindow. Zdefiniowaną na jej bazie klasę pochodną należy „zarejestrować”, podstawiając ją pod zmienną HintWindowClass. Typ tej zmiennej - TMetaClass* - jest tzw. typem referencyjnym klas (class-reference type); wartościami składającymi się na typ referencyjny są klasy zdefiniowane w programie. Wartość reprezentującą daną klasę w ramach typu referencyjnego uzyskuje się za pomocą operatora __classid:

HintWindowClass = __classid(TCalculatorHintWindow);

Aby w nowo definiowanej klasie podpowiedzi uzyskać zamierzone efekty, należy oczywiście poznać najpierw znaczenie jej metod wirtualnych, z których prawdopodobnie jedna lub więcej polegać będzie przedefiniowaniu. Wykaz tych metod, wraz z deklaracjami i krótkim opisem, przedstawia tabela 3.6.

Tabela 3.6. Metody wirtualne klasy THintWindow

Metoda

Przeznaczenie

CreateParams()

virtual void __fastcall CreateParams(
TCreateParams &Params)

Określa parametry tworzonego okna podpowiedzi.

Paint()

void Paint(void)

Określa sposób wyświetlenia podpowiedzi na płótnie jej okna. Płótno reprezentowane jej przez właściwość Canvas, zaś prostokąt reprezentujący obszar klienta ukrywa się pod właściwością ClientRect.

CalcHintRect()

Windows::TRect __fastcall CalcHintRect(
int MaxWidth,
const System::AnsiString AHint,
void* AData)

Oblicza rozmiar prostokąta niezbędnego do wyświetlenia podpowiedzi określonej przez parametry AHint i AData. Algorytm stosowany przez tę funkcję stara się zmieścić wyświetlany tekst w jednym wierszu, jeżeli jednak nie będzie to możliwe mimo zwiększenia szerokości okna do wielkości MaxWidth, tekst zostanie podzielony między kilka wierszy.

ActivateHint()

void __fastcall ActivateHint(
const Windows::TRect &Rect,
const System::AnsiString AHint)

Dokonuje wyświetlenia okna podpowiedzi we współrzędnych określonych przez parametr Rect. Jeżeli prostokąt określony przez ten parametr wykracza choć częściowo poza ekran, metoda THintWindow->ActivateHint() zmienia jego położenie (i być może rozmiar) na najbardziej zbliżone, lecz całkowicie mieszczące się w obrębie ekranu. Metoda ta dokonuje także przypisania tekstu podpowiedzi do właściwości Caption okna jeszcze przed jego wyświetleniem; za pośrednictwem tej właściwości tekst podpowiedzi przekazywany jest do metody Paint().

ActivateHintData()

void __fastcall ActivateHintData(
const Windows::TRect &Rect,
const System::AnsiString AHint,
void * AData)

Stanowi alternatywny (do metody ActivateHint) sposób spowodowania wyświetlenia podpowiedzi z jednoczesnym przekazaniem danych pomocniczych; wskaźnik do tych danych - przekazany pod postacią ostatniego parametru - dostępny jest w metodzie CalcHintRect() jako ostatni parametr jej wywołania. Standardowo (tj. w klasie THintWindow) implementacje metod ActivateHintData() i CalcHitRect() ignorują zupełnie parametr AData, metoda ActivateHintData() jest więc równoważna metodzie ActivateHint().

IsHintMsg()

bool IsHintMsg(tagNSG& Msg)

Klasyfikuje dany komunikat pod kątem tego, czy w wyniku jego wystąpienia powinno nastąpić zamknięcie okna podpowiedzi; gdy otwarte jest okno podpowiedzi kontekstowej, obiekt Application przekazuje do jego metody IsHintMsg() każdy z komunikatów otrzymanych przez aplikację i w przypadku otrzymania wyniku true likwiduje podpowiedź.

Przedefiniowując metody wymienione w tabeli 3.6, zdefiniowaliśmy na potrzeby naszego projektu nową klasę podpowiedzi - TCalculatorHintWindow(). Jej deklarację prezentujemy na wydruku 3.8.

Wydruk 3.8. Deklaracja alternatywnej klasy podpowiedzi - TCalculatorHintWindow

class TCalculatorHintWindow : public THintWindow

{

typedef THintWindow inherited;

protected:

virtual void __fastcall Paint(void);

virtual void __fastcall CreateParams(TCreateParams &Params);

public:

__fastcall virtual TCalculatorHintWindow(Classes::TComponent* AOwner);

virtual void __fastcall ActivateHint(const Windows::TRect& Rect,

const AnsiString AHint);

virtual void __fastcall ActivateHintData(const Windows::TRect& Rect,

const AnsiString AHint,

void* AData);

virtual Windows::TRect __fastcall CalcHintRect(int MaxWidth,

const AnsiString AHint,

void* AData);

virtual bool __fastcall IsHintMsg(tagMSG& Msg);

__property BiDiMode ;

__property Caption ;

__property Color ;

__property Canvas ;

__property Font ;

public:

inline __fastcall virtual ~TCalculatorHintWindow(void)

{ }

public:

inline __fastcall TCalculatorHintWindow(HWND ParentWindow) : THintWindow(ParentWindow)

{ }

};

Faktycznemu przedefiniowaniu uległy trzy z wymienionych metod: CreateParams(), Paint() i CalcHintRect(); definicję klasy przedstawia wydruk 3.9.

Wydruk 3.9. Implementacja metod klasy TCalculatorHintWindow

__fastcall TCalculatorHintWindow::TCalculatorHintWindow(
Classes::TComponent* AOwner): THintWindow(AOwner)

{

Canvas->Font->Name = "Arial";

Canvas->Font->Color = clBlack;

}

//--------------------------------------------------------------------------//

void __fastcall TCalculatorHintWindow::CreateParams(TCreateParams &Params)

{

inherited::CreateParams(Params);

Params.Style = WS_POPUP;

Params.WindowClass.style = Params.WindowClass.style | CS_SAVEBITS;

if(NewStyleControls)

{

Params.ExStyle = WS_EX_TOOLWINDOW;

AddBiDiModeExStyle(Params.ExStyle);

}

}

//--------------------------------------------------------------------------//

void __fastcall TCalculatorHintWindow::Paint(void)

{

TRect Rect = ClientRect;

Canvas->Brush->Color = clBlack;

Canvas->FillRect(Rect);

Rect.Left += 4;

Rect.Top += 4;

Rect.Right -= 4;

Rect.Bottom -= 4;

Frame3D(Canvas, Rect, clBtnShadow, clBtnHighlight, 1);

Canvas->Brush->Color = TColor(0xB4CDBB);

Canvas->FillRect(Rect);

Rect.Left += 1;

Rect.Top += 5;

Rect.Right -= 1;

Rect.Bottom -= 1;

DrawText( Canvas->Handle,

Caption.c_str(),

-1,

&Rect,

DT_RIGHT|DT_NOPREFIX|DT_WORDBREAK|DrawTextBiDiModeFlagsReadingOnly() );

}

//--------------------------------------------------------------------------//

void __fastcall TCalculatorHintWindow::ActivateHint(

const Windows::TRect& Rect,

const AnsiString AHint)

{

inherited::ActivateHint(Rect, AHint);

}

//--------------------------------------------------------------------------//

void __fastcall TCalculatorHintWindow::ActivateHintData(

const Windows::TRect& Rect,

const AnsiString AHint,

void* AData)

{

inherited::ActivateHintData(Rect, AHint, AData);

}

//--------------------------------------------------------------------------//

bool __fastcall TCalculatorHintWindow::IsHintMsg(tagMSG& Msg)

{

return inherited::IsHintMsg(Msg);

}

//--------------------------------------------------------------------------//

Windows::TRect __fastcall TCalculatorHintWindow::CalcHintRect(int MaxWidth,

const AnsiString AHint,

void* AData)

{

TRect Rect(0, 0, MaxWidth, 0);

DrawText( Canvas->Handle,

AHint.c_str(),

-1,

&Rect,

DT_CALCRECT|DT_SINGLELINE|DT_NOPREFIX|DrawTextBiDiModeFlagsReadingOnly() );

// ze względów estetycznych szerokość okna powinna być co najmniej

// trzykrotnie większa od jego wysokości:

if((Rect.Right - Rect.Left) < 3*(Rect.Bottom - Rect.Top))

{

Rect.Right = Rect.Left + 3*(Rect.Bottom - Rect.Top);

}

Rect.Right += 20;

Rect.Bottom += 12;

return Rect;

}

Analizę przedstawionej implementacji rozpoczniemy od konstruktora. Jego czynności inicjacyjne ograniczają się do ustawienia czcionki: treść podpowiedzi wyświetlana będzie czarną czcionką Arial - tą samą, której używamy w głównym wyświetlaczu.

Przedefiniowanie metody CreateParams ma na celu zmianę niektórych aspektów standardowego wyglądu i zachowania okna podpowiedzi. Styl okna (WS_POPUP) określa je jako okno „wyskakujące” w bieżącej pozycji kursora (ang. popup), natomiast do stylu klasy okna dodano znacznik CS_SAVEBITS. Klasyfikuje on okienko jako „sprzątające po sobie” na ekranie - przed wyświetleniem dokonuje ono mianowicie przechowania (w postaci bitmapy) bieżącej zawartości obszaru, w którym ma być wyświetlone, zaś po zamknięciu zawartość tę odtwarza. Nie jest więc konieczne wysyłanie do „naruszonych” okien komunikatu WM_PAINT w celu ich odrysowania. Zabieg taki jest jednakże sensowny tylko w odniesieniu do małego okienka, jakim właśnie jest okno podpowiedzi.

Zmienna NewStyleControls zawiera informację o tym, czy kontrolki aplikacji używają „nowego” stylu okien, wprowadzonego w Windows 95 („stary” styl okien używany będzie np. wówczas, gdy aplikacja uruchomiona zostanie w środowisku Windows 3.x wzbogaconym o Win32S). Jeżeli ów nowy styl jest obsługiwany, „rozszerzony” styl okna ustawiany jest na WS_EX_TOOLWINDOW, co nadaje mu cechy okienka „narzędziowego”, a ponadto ustawiona zostaje „dwukierunkowa” obsługa czcionek.

Zasadnicza funkcjonalność klasy TCalculatorHintWindow zawarta jest jednakże w jej metodach CalcHintRect() i Paint(). Pierwszą czynnością wykonywaną przez metodę CalcHintRect()jest określenie minimalnych rozmiarów prostokąta niezbędnych do zmieszczenia tekstu podpowiedzi. Zadanie to wykonuje funkcja DrawText() wywołana ze znacznikiem DT_CALCRECT - nie jest wówczas wyświetlany żaden tekst, a wyniki obliczeń podstawiane są pod parametr Rect:

DrawText( Canvas->Handle,

AHint.c_str(),

-1,

&Rect,

DT_CALCRECT

|DT_SINGLELINE

|DT_NOPREFIX

|DrawTextBiDiModeFlagsReadingOnly() );

Metoda DrawText() nie przejmuje się zbytnio proporcjami „obliczanego” prostokąta, my natomiast chcielibyśmy nadać mu proporcje zbliżone do zasadniczego wyświetlacza tak, by przypominał jego miniaturę. Związana z tym „operacja plastyczna” polega na (ewentualnym) rozciągnięciu go w poziomie tak, by jego szerokość była co najmniej trzykrotnie większa od wysokości, a także na dodaniu dodatkowych pikseli u dołu okna i z jego prawej strony:

if((Rect.Right - Rect.Left) < 3*(Rect.Bottom - Rect.Top))

{

Rect.Right = Rect.Left + 3*(Rect.Bottom - Rect.Top);

}

Rect.Right += 20;

Rect.Bottom += 12;

Użyte wartości proporcji i marginesów dobrane zostały arbitralnie stosownie do upodobań Autorów.

Wyświetlenie okna podpowiedzi, stanowiące treść metody Paint(), realizowane jest w dwóch etapach: najpierw rysowane jest tło i obrzeże okna, a dopiero potem wypisywany jest tekst podpowiedzi.

Okno podpowiedzi ma być otoczone czarną ramką o grubości czterech pikseli, wewnątrz której, na tle w kolorze wyświetlacza, wypisany zostanie tekst podpowiedzi. Aby uzyskać taki efekt, wypełniamy najpierw całe okienko czarnym kolorem:

TRect Rect = ClientRect;

Canvas->Brush->Color = clBlack;

Canvas->FillRect(Rect);

Potem centrycznie wpisujemy w nie prostokąt w kolorze wyświetlacza, mniejszy z każdej strony o cztery piksele; prostokąt ten jest jednak uprzednio otaczany jednopikselową ramką symulującą efekt trójwymiarowy - kreśleniem ramek na płótnie kontrolki zajmuje się funkcja Frame3D z biblioteki VCL:

Rect.Left += 4;

Rect.Top += 4;

Rect.Right -= 4;

Rect.Bottom -= 4;

Frame3D(Canvas, Rect, clBtnShadow, clBtnHighlight, 1);

Canvas->Brush->Color = TColor(0xB4CDBB);

Canvas->FillRect(Rect);

Aby uniknąć nadmiernego zbliżenia wyświetlanego tekstu do obramowania, należy dodatkowo zmniejszyć obszar wyświetlania - o 5 pikseli u góry okna (dzięki czemu wyświetlana liczba plasować się będzie wyraźnie przy dolnej krawędzi, jak na wyświetlaczu) i po jednym pikselu na pozostałych krawędziach:

Rect.Left += 1;

Rect.Top += 5;

Rect.Right -= 1;

Rect.Bottom -= 1;

W tak przygotowany obszar można wreszcie wpisać tekst podpowiedzi, wyrównany (jak na wyświetlaczu) prawostronnie:

DrawText( Canvas->Handle,

Caption.c_str(),

-1,

&Rect,

DT_RIGHT

|DT_NOPREFIX

|DT_WORDBREAK

|DrawTextBiDiModeFlagsReadingOnly() );

Szczegółową anatomię okna TCalculatorHintWindow, wyświetlającego wartość 22/7 jako stosowane niekiedy przybliżenie liczby π, przedstawia rysunek 3.3.

Surowy rysunek, bez opisów znajduje się w pliku Orig-5-3.bmp. Proszę o wykonanie poszczególnych napisów i wymiarowań, z zamianą angielskiego „pixels” na polskie „piksele” w odpowiednim przypadku, a więc

32 piksele
1 piksel
5 pikseli
9 pikseli
16 pikseli
32 piksele
109 pikseli
129 pikseli

Rysunek 3.3. Anatomia okna TCalculatorHintWindow

Wykorzystanie zdarzenia OnHint klasy TApplication

Manipulowanie zdarzeniami klasy TApplication staje się łatwiejsze, gdy wykorzystać do niego komponent TApplicationEvents znajdujący się na stronie Additional palety komponentów.

Przypomnijmy - w momencie gdy generowane jest zdarzenie OnHint obiektu Application, właściwość Hint tegoż obiektu zawiera długą część podpowiedzi; jeżeli tekst podpowiedzi nie zawiera znaku (|) (a więc jeżeli nie podzielono go jawnie na część długą i krótką), zostaje on w całości użyty w obydwu tych rolach. Nieco wcześniej pokazaliśmy, jak w takiej sytuacji wyświetlić właściwość Application->Hint na pasku statusowym formularza, obecnie posuniemy się nieco dalej, bowiem w naszym kalkulatorze długa część podpowiedzi rozdzielona będzie pomiędzy dwa panele: środkowy, powielający (z nielicznymi wyjątkami) mnemoniczne oznaczenie klawisza i prawy, zawierający informację nieco bardziej opisową. Stajemy więc przed zadaniem takiego rozdzielenia długiej części podpowiedzi, by rozdzielenie to widoczne było już w treści właściwości Hint odnośnej kontrolki.

Nie pisaliśmy o tym wcześniej, ale tylko pierwszy znak „|” występujący w treści podpowiedzi traktowany jest jako separator oddzielający część długą i krótką; wyodrębnienie obydwu części jest zadaniem funkcji biblioteki VCL o nazwach GetShortHint() i GetLongHint(), których treść (w języku Object Pascal) prezentuje się następująco:

function GetShortHint(const Hint: string): string;

var

I: Integer;

begin

I := AnsiPos('|', Hint);

if I = 0 then

Result := Hint

else

Result := Copy(Hint, 1, I - 1);

end;

function GetLongHint(const Hint: string): string;

var

I: Integer;

begin

I := AnsiPos('|', Hint);

if I = 0 then

Result := Hint

else

Result := Copy(Hint, I + 1, Maxint);

end;

Następujące ich deklaracje znajdują się w pliku nagłówkowym Include\Vcl\controls.hpp:

extern PACKAGE AnsiString __fastcall GetShortHint(const AnsiString Hint);

extern PACKAGE AnsiString __fastcall GetLongHint(const AnsiString Hint);

Jeżeli więc użyjemy znaku „|” do podzielenia tekstu podpowiedzi na trzy części, to części te wyodrębnić można również przy użyciu wymienionych funkcji:

part1 = GetShortHint(HintText);

part2 = GetShortHint (GetLongHint(HintText);

part3 = GetLongHint (GetLongHint(HintText));

Załóżmy więc, iż trzy części podpowiedzi przeznaczone będą do wyświetlenia (kolejno) w okienku, na środkowym panelu i prawym panelu; ponieważ właściwość Application->Hint zawiera tylko dwie ostatnie części (pierwsza została wcześniej „odfiltrowana” przez kontrolkę), wystarczy tylko rozdzielić te części pomiędzy właściwe panele:

// środkowy panel

StatusBar1->Panels->Items[1]->Text = GetShortHint(Application->Hint);

// prawy panel

StatusBar1->Panels->Items[2]->Text = GetLongHint(Application->Hint);

W poprzednim punkcie zaprezentowaliśmy tworzenie i wykorzystanie niestandardowej klasy okna podpowiedzi (TCalculatorHintWindow); problem w tym, iż nasza aplikacja posługuje się również podpowiedziami standardowymi. Aktualnie stosowana klasa okna podpowiedzi wskazywana jest (jak wcześniej pisaliśmy) przez zmienną HintWindowClass i zadaniem aplikacji jest właściwe przełączanie jej pomiędzy klasami THintWindow i TCalculatorHintWindow. „Przełączanie” takie dokonywane jest w ramach zdarzenia OnHint obiektu Application, które to zdarzenie delegowane jest do identycznie nazwanego zdarzenia komponentu ApplicationEvents1:

Wydruk 3.10. Obsługa zdarzenia Application->OnHint za pomocą komponentu TApplicationEvents

void __fastcall TMainForm::ApplicationEvents1Hint(TObject *Sender)

{

if(Application->Hint == "Ctrl+V|Memory Recall"

|| Application->Hint == "LCD")

{

// 10 sekund .. powinno wystarczyć.

Application->HintHidePause = 10000;

HintWindowClass = __classid(TCalculatorHintWindow);

}

else

{

Application->HintHidePause = HintDisplayTime;

HintWindowClass = __classid(THintWindow);

}

if(Application->Hint != "LCD")

{

// środkowy panel

StatusBar1->Panels->Items[1]->Text = GetShortHint(Application->Hint);

// prawy panel

StatusBar1->Panels->Items[2]->Text = GetLongHint(Application->Hint);

}

}

Konieczność użycia niestandardowych podpowiedzi (TCalculatorHintWindow) zdarza się tylko wówczas, gdy kursor znajduje się na klawiszu „MR” (podpowiedź wyświetla wówczas zawartość pamięci) lub w obrębie wyświetlacza (jeżeli wyświetlana liczba nie mieści się w całości na wyświetlaczu, można ja zobaczyć w całości właśnie dzięki podpowiedzi). Sytuacje te rozpoznawane są łatwo na podstawie treści podpowiedzi, których długa część brzmi wtedy (odpowiednio) „Ctrl+V|Memory Recall” oraz „LCD” - to właśnie jest treścią pierwszej instrukcji if. Druga instrukcja if zapobiega wypisaniu na środkowym panelu tekstu „LCD”, który pełni jedynie rolę identyfikacyjną. Dotychczas prześledziliśmy działanie podpowiedzi związanej z klawiszem „MR”, zobaczmy więc teraz, jak funkcjonuje podpowiedź dublująca zawartość wyświetlacza (patrz wydruk 3.11).

Wydruk 3.11. Odświeżanie zawartości wyświetlacza

void __fastcall TMainForm::UpdateLCDScreen(const AnsiString& NewNumber,

bool Constant)

{

int NumberWidth = LCDScreen->Canvas->TextWidth(NewNumber);

// parametr Constant dodany został w celu wymuszenia lewostronnego

// wyrównania gdy wyświetlana jest stała lub zawartość pamięci

if(Operation == coComplete || Constant)

{

if( (NumberWidth >= LCDScreen->Width)

&& (LCDScreen->Alignment == taRightJustify) )

{

LCDScreen->Alignment = taLeftJustify;

}

else if( (NumberWidth < LCDScreen->Width)

&& (LCDScreen->Alignment != taRightJustify) )

{

LCDScreen->Alignment = taRightJustify;

}

}

else if(LCDScreen->Alignment != taRightJustify)

{

LCDScreen->Alignment = taRightJustify;

}

LCDScreen->Caption = NewNumber;

int pos = LCDScreen->Hint.Pos("|");

int length = LCDScreen->Hint.Length();

AnsiString LCDScreenHint

= LCDScreen->Hint.SubString(pos, length-pos+1);

LCDScreen->Hint = NewNumber + LCDScreenHint;

if(NumberWidth >= LCDScreen->Width) LCDScreen->ShowHint = true;

else LCDScreen->ShowHint = false;

}

Powyższa funkcja wywoływana jest każdorazowo, gdy zawartość wyświetlacza ma się zmienić w wyniku np. wprowadzenia nowej cyfry czy zmiany reprezentacji wyświetlania. Po dokonaniu niezbędnych ustawień, związanych z wyrównaniem wyświetlania, nowa zawartość podstawiana jest pod właściwość Caption wyświetlacza (będącego etykietą TLabel). Drugi parametr wywołania ma za zadanie wymusić lewostronne wyrównanie wówczas, gdy na wyświetlaczu ma pojawić się jedna z predefiniowanych stałych - jeżeli bowiem stała nie mieści się w całości na wyświetlaczu, ważniejszy jest jej początek niż koniec. Parametr ten jest parametrem domyślnym i można go pominąć w wywołaniu - zakłada się wówczas, iż ma on wartość false, zgodnie z deklaracją funkcji:

void __fastcall UpdateLCDScreen(
const AnsiString& NewNumber,

bool Constant = false);

Jeżeli funkcja UpdateLCDScreen() stwierdzi, iż kursor myszy znajduje się w obrębie wyświetlacza, związana z wyświetlaczem podpowiedź (standardowo „|LCD”) uaktualniana jest teraz tak, iż jej krótka część staje się kopią zawartości wyświetlacza. Aby ograniczyć wyświetlenie podpowiedzi tylko do tych przypadków, gdy zawartość wyświetlacza staje się zbyt duża, by mógł on pokazać ją w całości, ustawiana jest odpowiednio właściwość ShowHint wyświetlacza:

if(NumberWidth >= LCDScreen->Width) LCDScreen->ShowHint = true;

else LCDScreen->ShowHint = false;

Kontrola migracji skupienia pomiędzy elementami interfejsu

Jednym z najważniejszych czynników, warunkujących wygodę obsługi aplikacji, jest sposób przekazywania jej danych wejściowych. W aplikacjach dla Windows - i generalnie innych aplikacjach sterowanych zdarzeniami i zorientowanych na obsługę graficzną - pierwszorzędne znaczenie ma sposób obsługi klawiatury. W przeciwieństwie bowiem do myszy (i innych urządzeń wskazujących) zdarzenia pochodzące od klawiatury nie są zdarzeniami pozycyjnymi, to znaczy nie są związane z określoną lokalizacją na ekranie; ich obsługą zajmuje się natomiast wyróżniona kontrolka interfejsu, nazywana kontrolką skupioną (focused), dlatego też zdarzenia pochodzące od klawiatury zaliczane są do kategorii zdarzeń skupionych.

Gdy zdarzenie skupione trafia do kontrolki skupionej, możliwe są dwa warianty postępowania: zdarzenie to może być mianowicie obsłużone przez tę kontrolkę, bądź też nastąpić może przeniesienie skupienia na inną kontrolkę wraz z przekazaniem jej rzeczonego zdarzenia w celu jego obsługi. Za chwilę pokażemy, jak obydwa te warianty realizowane są w naszym projekcie kalkulatora.

Obsługa zdarzeń skupionych

Nasza aplikacja kalkulator może być obsługiwana zarówno przez klawiaturę, jak i za pomocą myszy - stosownie do wygody i upodobań użytkownika. Obsługą zdarzeń pochodzących od klawiatury zajmują się funkcje zdarzeniowe CalculatorKeyDown() i CalculatorKeyUp() - ale tylko wówczas, gdy możliwość wprowadzania danych z klawiatury nie jest zablokowana; w przeciwnym razie zdarzenia OnKeyDown() i OnKeyUp() są „zaślepione” - nie są im przypisane żadne funkcje obsługi. Decyduje o tym, jak pamiętamy, funkcja SetEnableKeyboardInput(), zajmującą się przełączaniem statusu dostępności klawiatury; prezentowaliśmy ją już na wydruku 3.3, tutaj przytaczamy ją ponownie dla kompletności przykładu:

Wydruk 3.12. Implementacji funkcji SetEnableKeyboardInput()

void __fastcall TMainForm::SetEnableKeyboardInput(bool NewEnableKeyboardInput)

{

if(EnableKeyboardInput != NewEnableKeyboardInput)

{

FEnableKeyboardInput = NewEnableKeyboardInput;

if(EnableKeyboardInput)

{

OnKeyDown = CalculatorKeyDown;

OnKeyUp = CalculatorKeyUp;

StatusBar1->Panels->Items[1]->Width = 60;

}

else

{

OnKeyDown = 0;

OnKeyUp = 0;

StatusBar1->Panels->Items[1]->Width = 0;

}

StatusBar1->Invalidate();

}

}

W konstruktorze formularza głównego pole FEnableKeyboardInput inicjowane jest wartością false, tak więc przy pierwszym porównaniu właściwość EnableKeyboardInput ma wartość false; z kolei pierwsze wywołanie funkcji SetEnableKeyboardInput() następuje z wartością parametru true (klawiatura standardowo jest dostępna), tak więc warunek w instrukcji if jest spełniony; spełniony jest również warunek w drugiej instrukcji if (właściwość EnableKeyboardInput ma bowiem teraz wartość true), ergo - przy pierwszym wywołaniu funkcji SetEnableKeyboardInput() następuje przypisanie obsługi zdarzeniom OnKeyDown i OnKeyUp.

Funkcję obsługi pierwszego z wymienionych zdarzeń przedstawia wydruk 3.13.

Wydruk 3.13. Implementacja funkcji obsługi zdarzenia OnKeyDown

void __fastcall TMainForm::CalculatorKeyDown(TObject *Sender, WORD &Key,

TShiftState Shift)

{

switch(Key)

{

case VK_NUMPAD1 :

case '1' : ButtonDown(cb1);

ButtonPressNumber(cb1);

break;

case VK_NUMPAD2 :

case '2' : ButtonDown(cb2);

ButtonPressNumber(cb2);

break;

case VK_NUMPAD3 :

case '3' : ButtonDown(cb3);

ButtonPressNumber(cb3);

break;

case VK_NUMPAD4 :

case '4' : ButtonDown(cb4);

ButtonPressNumber(cb4);

break;

case VK_NUMPAD5 :

case '5' : ButtonDown(cb5);

ButtonPressNumber(cb5);

break;

case VK_NUMPAD6 :

case '6' : ButtonDown(cb6);

ButtonPressNumber(cb6);

break;

case VK_NUMPAD7 :

case '7' : ButtonDown(cb7);

ButtonPressNumber(cb7);

break;

case VK_NUMPAD8 :

case '8' : ButtonDown(cb8);

ButtonPressNumber(cb8);

break;

case VK_NUMPAD9 :

case '9' : ButtonDown(cb9);

ButtonPressNumber(cb9);

break;

case VK_NUMPAD0 :

case '0' : ButtonDown(cb0);

ButtonPress0();

break;

case VK_DECIMAL : ButtonDown(cbPoint);

ButtonPressPoint();

break;

case VK_PRIOR : ButtonDown(cbExponent);

ButtonPressExponent();

break;

case VK_ADD : ButtonDown(cbAdd);

ButtonPressOperation(coAdd);

break;

case VK_SUBTRACT : ButtonDown(cbSubtract);

ButtonPressOperation(coSubtract);

break;

case VK_MULTIPLY : ButtonDown(cbMultiply);

ButtonPressOperation(coMultiply);

break;

case VK_DIVIDE : ButtonDown(cbDivide);

ButtonPressOperation(coDivide);

break;

case VK_RETURN : ButtonDown(cbEquals);

ButtonPressEquals();

break;

case VK_BACK : ButtonDown(cbBackspace);

ButtonPressBackspace();

break;

case VK_DELETE : if(Shift.Contains(ssCtrl))

{

ButtonDown(cbAllClear);

ButtonPressAllClear(); }

else

{

ButtonDown(cbClear);

ButtonPressClear();

}

break;

case 'C' : if(Shift.Contains(ssCtrl))

{

ButtonDown(cbMemoryAdd);

ButtonPressMemoryAdd();

}

break;

case 'V' : if(Shift.Contains(ssCtrl))

{

ButtonDown(cbMemoryRecall);

ButtonPressMemoryRecall();

}

break;

}

}

Zadaniem monumentalnej instrukcji switch jest tutaj wybór szczegółowej obsługi stosownie do naciśniętego klawisza. W każdym z przypadków fakt naciśnięcia klawisza potwierdzany jest symulowanym naciśnięciem odpowiadającego mu przycisku na pulpicie kalkulatora - wizualna symulacja takiego „naciśnięcia” jest efektem ustawienia na true właściwości Down danego przycisku, którą to czynność wykonuje funkcja ButtonDown().

Obsługa zdarzenia OnKeyUp przebiega bardzo podobnie, jednak z kilkoma znaczącymi różnicami.

Wydruk 3.14. Implementacja funkcji obsługi zdarzenia OnKeyUp

void __fastcall TMainForm::CalculatorKeyUp(TObject *Sender, WORD &Key,

TShiftState Shift)

{

switch(Key)

{

case VK_NUMPAD1 :

case '1' : ButtonUp(cb1);

break;

case VK_NUMPAD2 :

case '2' : ButtonUp(cb2);

break;

case VK_NUMPAD3 :

case '3' : ButtonUp(cb3);

break;

case VK_NUMPAD4 :

case '4' : ButtonUp(cb4);

break;

case VK_NUMPAD5 :

case '5' : ButtonUp(cb5);

break;

case VK_NUMPAD6 :

case '6' : ButtonUp(cb6);

break;

case VK_NUMPAD7 :

case '7' : ButtonUp(cb7);

break;

case VK_NUMPAD8 :

case '8' : ButtonUp(cb8);

break;

case VK_NUMPAD9 :

case '9' : ButtonUp(cb9);

break;

case VK_NUMPAD0 :

case '0' : ButtonUp(cb0);

break;

case VK_DECIMAL : ButtonUp(cbPoint);

break;

case VK_PRIOR : // PgUp dla wykładnika

// funkcjonuje jak przełącznik,

// więc OnKeyUp niepotrzebne

break;

case VK_ADD : ButtonUp(cbAdd);

break;

case VK_SUBTRACT : ButtonUp(cbSubtract);

break;

case VK_MULTIPLY : ButtonUp(cbMultiply);

break;

case VK_DIVIDE : ButtonUp(cbDivide);

break;

case VK_RETURN : ButtonUp(cbEquals);

break;

case VK_BACK : ButtonUp(cbBackspace);

break;

case VK_DELETE : if(SpeedButtonAllClear->Down)

{

ButtonUp(cbAllClear);

}

else

{

ButtonUp(cbClear);

}

break;

case 'C' : ButtonUp(cbMemoryAdd);

break;

case 'V' : ButtonUp(cbMemoryRecall);

break;

}

}

W każdym przypadku likwidowany jest efekt opisanej przed chwilą wizualnej symulacji naciśnięcia przycisku na panelu kalkulatora - czynność tę wykonuje funkcja ButtonUp(), przywracająca wartość false właściwości Down odnośnego przycisku. Nie dotyczy to jednak klawisza Page Up (VK_PRIOR), przełączającego pomiędzy ułamkowym a wykładniczym zapisem liczby - odpowiadający mu przycisk na panelu kalkulatora ma tak naprawdę charakter przełącznika i jego właściwość Down utrzymywana jest w stanie odpowiadającym obecności zapisu wykładniczego. Notabene takiego zachowania przycisku nie sposób dopatrzyć się w rzeczywistym kalkulatorze - to jeden z przykładów tego, w jaki sposób aplikacja symulująca zachowanie obiektów ze świata realnego wolna jest jednocześnie od wielu ograniczeń związanych organicznie z tymi obiektami.

Pewnego komentarza wymaga również sposób obsługi sekwencji klawiszy Ctrl+C. Otóż w sytuacji, gdy w momencie naciśnięcia klawisza z literą C (na klawiaturze komputera) naciśnięty jest również klawisz Ctrl, stan klawisza C ma być duplikowany przez wizualny stan naciśnięcia przycisku „M+”; ewentualne puszczenie klawisza Ctrl, gdy klawisz C jest jeszcze trzymany, nie ma tu żadnego znaczenia. Dlatego też w obsłudze zdarzenia OnKeyDown sprawdza się stan klawisza Ctrl (Shift.Contains(ssCtrl)) dla rozróżnienia, czy naciśnięto C czy Ctrl+C, natomiast w obsłudze zdarzenia OnKeyUp istotne jest tylko to, czy puszczono klawisz C, bez względu na stan klawisza Ctrl.

Nieco inaczej natomiast dokonuje się rozróżnienia pomiędzy klawiszami Del i Ctrl+Del. Przy naciśnięciu klawisza (klawiszy) zwyczajnie bada się stan klawisza Ctrl, jednak przy puszczeniu klawisza Del bada się status związanego z nim pierwszego przycisku „czyszczącego”: jeżeli nie jest on „naciśnięty” (Down = false) oznacza to, iż mieliśmy do czynienia z kombinacją Ctrl+Del (w wyniku czego naciśnięty jest drugi przycisk czyszczący).

Przenoszenie skupienia pomiędzy kontrolkami

Gdy nasza aplikacja kalkulator jest aplikacją aktywną, kontrolką skupioną jest jej formularz główny. Formularz ten może jednak utracić skupienie (focus) w dwóch przypadkach: gdy użytkownik „przełączy się” na inną aplikację - z punktu widzenia naszego projektu jest to przypadek mało interesujący - oraz gdy skupienie przeniesie się na kontrolkę nie zadokowaną w formularzu głównym. Taką „wydokowaną” kontrolką można w łatwy sposób uczynić wyświetlacz (patrz rysunek 3.4).

Proszę umieścić tu rysunek znajdujący się w pliku AGR-5-1.BMP

Rysunek 3.4. Wydokowanie kontrolki wyświetlacza LCD z głównego formularza

Próby uczynienia wydokowanego wyświetlacza kontrolką skupioną (za pomocą klikania jej myszą) okażą się jednak daremne. Jest to celowe działanie aplikacji, zmierzające do utrzymania za wszelką cenę skupienia w obrębie formularza głównego - na nim bowiem znajdują się przyciski i to on reagować ma na naciskanie klawiszy. W aplikacji tworzonej z użyciem C++Buildera przeniesienie skupienia na formularz (i w ogóle na dowolną kontrolkę okienkową) jest bardzo proste - służy do tego metoda SetFocus(). W wielu miejscach programu badany jest status zadokowania wyświetlacza - wartość true właściwości Floating świadczy o tym, iż aktualnie jest on wydokowany; w takiej sytuacji skupienie przenoszone jest w sposób wymuszony na formularz główny:

if(LCDPanel->Floating) SetFocus();

Zostawmy na chwilę nasz kalkulator i poświęćmy kilka słów pewnemu typowemu przypadkowi, w którym takie wymuszone przenoszenie skupienia jest dla użytkownika ze wszech miar pożyteczne: przypadek taki stanowi ciąg kontrolek edycyjnych TEdit - po skompletowaniu zawartości każdej z nich skupienie przenoszone jest na następną; po skompletowaniu ostatniej skupienie przenoszone jest na przycisk OK. Efekt ten zaobserwować można, uruchamiając przykładowy projekt Focus.bpr z załączonej do książki płyty CD-ROM - dla prostoty zastosowano w nim tylko dwie kontrolki edycyjne. W każdej z kontrolek edycyjnych przechwytywane jest zdarzenie OnKeyUp, występujące w momencie puszczenia naciśniętego klawisza. Bada się wówczas, czy zawartość kontrolki osiągnęła żądaną wartość i jeżeli tak, to pozbawia się ją skupienia. Po zapełnieniu pierwszej kontrolki edycyjnej skupienie przenosi się na drugą z wyjątkiem przypadku, gdy ta ostatnia jest już pełna - wówczas skupienie przenosi się na przycisk OK. W przypadku zapełnienia drugiej kontrolki skupienie przenosi się na pierwszą (jeżeli nie jest ona jeszcze zapełniona) albo na przycisk OK (w przeciwnym razie). Oto odpowiednie funkcje zdarzeniowe, dokonujące opisanych sprawdzeń:

Wydruk 3.15. Badanie stanu pierwszej kontrolki edycyjnej

void __fastcall TFromToForm::Edit1KeyUp(TObject* Sender,

WORD& Key,

TShiftState Shift)

{

if(Edit1->Text.Length() == Edit1->MaxLength)

{

FinishNumberComplete = true;

if(StartNumberComplete)

{

BitBtnOK->Enabled = true;

BitBtnOK->SetFocus();

}

else Edit2->SetFocus();

}

else

{

FinishNumberComplete = false;

BitBtnOK->Enabled = false;

}

}

Wydruk 3.16. Badanie stanu drugiej kontrolki edycyjnej

void __fastcall TFromToForm::Edit2KeyUp(TObject* Sender,

WORD& Key,

TShiftState Shift)

{

if(Edit2->Text.Length() == Edit2->MaxLength)

{

StartNumberComplete = true;

if(FinishNumberComplete)

{

BitBtnOK->Enabled = true;

BitBtnOK->SetFocus();

}

else Edit1->SetFocus();

}

else

{

StartNumberComplete = false;

BitBtnOK->Enabled = false;

}

}

Aby upewnić się, iż w momencie uruchomienia dialogu kontrolką skupioną jest pierwsza kontrolka edycyjna, umieściliśmy ją jako pierwszą w „cyklu Tab”, nadając wartość 0 jej właściwości TabOrder; w dalszej kolejności „cyklu Tab” umieściliśmy drugą kontrolkę edycyjną (TabOrder=1), przycisk OK (TabOrder=2) i przycisk Anuluj (TabOrder=3). Jak wiadomo, kolejność wynikająca z właściwości TabOrder ma zastosowanie przy przenoszeniu skupienia za pomocą klawisza Tab (w działającej aplikacji). Oryginalnie kolejność kontrolek w „cyklu Tab” wynika z kolejności umieszczania ich na formularzu podczas budowania aplikacji; po skompletowaniu formularza należy zawsze sprawdzić, czy kolejność ta odpowiednia jest do warunków działającej aplikacji.

Dbałość o wygląd interfejsu

Jednym ze sposobów usprawniania interfejsu aplikacji jest wzbogacanie jego wyglądu. Nie chodzi tu jednak li tylko o walory stricte plastyczne - znacznie ważniejsze jest użycie symboli, kolorystyki, figur geometrycznych itp. w celu uczynienia interfejsu bardziej komunikatywnym. Interfejs przyjazny dla użytkownika i wygodny w obsłudze wymaga mniej czasu na naukę tejże obsługi, dzięki czemu użytkownik będzie mógł szybciej zacząć korzystać z dobrodziejstw oferowanych przez aplikację.

Jeżeli aplikacja modelować ma obiekty ze świata rzeczywistego, jej interfejs powinien w miarę możności odzwierciedlać wygląd i naśladować zachowanie się tych obiektów; ponieważ jednak zachowanie to związane może być z szeregiem ograniczeń i niedogodności, należy w tworzonej aplikacji starać się niedogodności te zniwelować - właśnie w zgodzie z tą zasadą nasza aplikacja kalkulator umożliwia wprowadzanie danych również z klawiatury, niekoniecznie zmuszając użytkownika do mniej wygodnego klikania przycisków na pulpicie, ponadto rozbudowany mechanizm podpowiedzi umożliwia natychmiastowy wgląd w informacje, które na typowym kalkulatorze pozostają niewidoczne.

W charakterze przykładu rozpatrzmy prosty program służący do wybierania numeru telefonicznego. Dla podkreślenia związku z rzeczywistym aparatem telefonicznym interfejs użytkownika powinien przypominać wygląd tegoż aparatu, w każdym razie przyciski o wyglądzie typowych przycisków aparatu telefonicznego wyglądają znacznie bardziej przyjaźnie niż standardowe przyciski TButton. Niezależnie od wyglądu, są one jednak i tak mniej wygodne w obsłudze niż klawiatura komputera, ponadto niektóre dodatkowe funkcje telefonu - głównie pamięć ostatnio wybieranych numerów - dadzą się w aplikacji graficznej zrealizować znacznie efektywniej niż w rzeczywistym aparacie telefonicznym.

Ponadto, jeżeli chodzi o samą czynność wybierania numeru (w aplikacji dla Windows), to sprawa realizującego ją interfejsu (lub jego części) nie jest wcale aż tak oczywista. Dawno już bowiem minęły czasy, gdy wybieranie numeru miało związek wyłącznie z telefonowaniem i np. w aplikacjach służących do wysyłania faksów samo wybieranie numeru jest tylko jedną z czynności, wcale nie najważniejszą i najczęściej całkowicie lub połowicznie zautomatyzowaną. Interwencja użytkownika związana z wybieraniem numeru ogranicza się najczęściej do wpisania tego ostatniego do książki adresowej, sam zaś model interfejsu zdeterminowany jest przez zasadniczą funkcję aplikacji - dla aplikacji faksowej najodpowiedniejszy wydaje się model wielodokumentowy (MDI).

Wspominaliśmy już o tym, iż uwarunkowania ludzkiego postrzegania świata czynią rozmaite symbole bardziej komunikatywnymi od opisów tekstowych. W aplikacjach komputerowych symbole takie, początkowo zagadkowe, traktowane są najczęściej jako sympatyczne dodatki do opcji tekstowych; użytkownik w miarę oswajania się z aplikacją zaczyna je coraz częściej kojarzyć z funkcjami wykonywanymi przez aplikację - w pewnym momencie może on nawet dojść do wniosku, iż towarzyszące symbolom opisy tekstowe (proszę zwrócić uwagę na zmianę hierarchii) są w zasadzie zbędne i tylko niepotrzebnie zajmują miejsce. Projektanci aplikacji powinni mieć to na uwadze i zapewnić opcję ukrycia opisów tekstowych w tych przypadkach, gdy wolne miejsce w okienkach rzeczywiście jest na wagę złota - jak na przykład w aplikacjach wielodokumentowych, gdzie wszystkie formularze potomne muszą dzielić się ograniczoną powierzchnią obszaru klienta głównego formularza.

Symbole mają ponadto tę przewagę nad opisami tekstowymi, iż niezależne są od konkretnego języka i przez to bardziej uniwersalne. Gdyby w naszym kalkulatorze zastosować opisy słowne zamiast symboli (np. „Clear the current value” lub „Wyczyść zawartość wyświetlacza” zamiast intuicyjnego zera ze strzałką ), wymagałoby to wielu dodatkowych zasobów, i to w różnych wersjach językowych.

Zobaczmy teraz, jak opisane zasady zastosowane zostały w naszym kalkulatorze.

Symbole na przyciskach

Dla realizacji przycisków nie zawierających opisu tekstowego (lub zawierających jego szczątkową postać) najodpowiedniejszy jest komponent TSpeedButton. Podobnie jak w przypadku przycisku TBitBtn jego wygląd graficzny przybierać może cztery różne postaci, reprezentujące pozycję „zwolniony”, pozycję „naciśnięty”, kliknięcie i niedostępność - bitmapę zawierającą niezbędne wzorce określa właściwość Glyph. W przeciwieństwie jednak do przycisku TBitBtn brak jest ramki informującej o skupieniu i zdolnej zeszpecić najbardziej nawet wymyślnie zaprojektowany obrazek. Przyciski TSpeedButton mogą być również powiązane w grupy, co powoduje ich funkcjonowanie w konwencji zbliżonej do przycisków „radiowych” (radiobuttons); o przynależności przycisku do określonej grupy decyduje jego właściwość GroupIndex; jeżeli ma ona wartość 0, przycisk funkcjonuje niezależnie od innych.

Słabą stroną przycisków TSpeedButton jest fakt, iż nie są one kontrolkami okienkowymi, wywodzą się bowiem z klasy TGraphicControl, nie TWinControl, nie posiadają więc „windowsowego” uchwytu Handle (nie mają tej wady przyciski TBitBtn), nie mogą więc przyjmować skupienia - nie pojawia się na nich wspomniana ramka.

Tak czy inaczej, wybór konkretnej klasy przycisków - TSpeedButton czy TBitBtn - zależny jest od projektanta i podyktowany wymaganiami aplikacji; w naszym kalkulatorze użyliśmy przycisków TSpeedButton.

Cztery wspomniane bitmapy odpowiedzialne za wygląd przycisku powinny być tego samego rozmiaru i następować bezpośrednio po sobie w porządku poziomym, w ramach pliku przypisanego do właściwości Glyph, w kolejności odpowiadającej stanom: „zwolniony”, „niedostępny”, „kliknięty” i „naciśnięty”. Zawartość odpowiedniego pliku dla przycisku „Dodaj” przedstawia rysunek 3.5.

0x08 graphic

0x01 graphic
0x08 graphic

Rysunek 3.5. Bitmapy odpowiedzialne za wygląd jednego z przycisków kalkulatora

Na powyższym rysunku dwie ostatnie bitmapy (odpowiadające stanom „kliknięty” i „naciśnięty”) są identyczne; jest to często stosowana praktyka, chociaż oczywiście bitmapy te nie są w żaden sposób od siebie zależne. Projektując poszczególne bitmapy, należy w dwóch pierwszych (dla stanów „zwolniony” i „niedostępny”) pozostawić jednopikselowy margines u góry i z lewej strony oraz dwupikselowy z prawej strony i u dołu. Dwie ostatnie bitmapy („kliknięty” i „naciśnięty”) pojawią się natomiast na przycisku z przesunięciem o 2 piksele w kierunkach w prawo i ku dołowi, należy więc uczynić je odpowiednio mniejszymi (nie ma potrzeby zachowywania lewego i górnego marginesu); zwolnione w wyniku tego przesunięcia miejsce (u góry i z lewej strony) będzie miało kolor czarny, o czym należy pamiętać, projektując wspomniane bitmapy. Przedstawia to poglądowo rysunek 3.6.

0x01 graphic

Proszę o naniesienie niezbędnego wymiarowania -oryginalny rysunek znajduje się na stronie 158 oryginału, powyższy rysunek jest w pliku orig-5-4.bmp

Rysunek 3.6. Niezbędne marginesy w bitmapach przycisku

Uwaga (od tłumacza):

Kolor dolnego lewego piksela każdej z bitmap traktowany jest jak kolor przezroczysty (transparent); żaden z posiadających ów kolor pikseli tejże bitmapy nie zostanie więc wyświetlony.

Przypisując plik do właściwości Glyph, nie należy zapomnieć o przypisaniu do właściwości NumGlyps liczby bitmap zawartych w tym pliku. Możliwe jest bowiem przekazanie mniejszej liczby bitmap (nawet tylko jednej); brakujące bitmapy będą wówczas symulowane na podstawie istniejących, chociaż w stanach „zwolniony” i „naciśnięty” przycisk będzie wyglądał tak samo.

Dwa spośród przycisków kalkulatora wyłamują się nieco z podanych na wstępie zasad - mowa tu o przyciskach M+ i MR, nie zawierających symboli, lecz skrótowe oznaczenia wywodzące się z języka angielskiego (w dodatku mylące, bowiem przycisk M+ wcale nie powoduje dodania zawartości wyświetlacza do pamięci, lecz umieszczenie jej tamże, z wymazaniem poprzedniej zawartości - przyp. tłum.). Wymyślenie i opracowanie bardziej odpowiednich symboli pozostawiamy czytelnikowi jako ćwiczenie.

Grupowanie przycisków

Przyciski kalkulatora intensywnie wykorzystują swą właściwość GroupIndex. Jedną z grup tworzą przyciski decydujące o podstawie reprezentacji, w jakiej wyświetlane będą liczby (dziesiętna, szesnastkowa albo ósemkowa). Należą one do grupy identyfikowanej numerem 2 (tyle wynosi ich właściwość GroupIndex), ponadto właściwość AllowAllUp ustawiona jest we wszystkich na false, co oznacza, iż jeden z nich musi być w danej chwili „naciśnięty”.

Jednoelementową grupę identyfikowaną numerem 3 tworzy przycisk decydujący o wyświetlaniu liczby w postaci „tradycyjnej” albo wykładniczej. Zaliczenie tego przycisku do grupy ma tylko jeden cel, mianowicie stabilne pozostawianie go w stanie „naciśnięty”; aby w ogóle mógł on być zwolniony, należy nadać wartość true jego właściwości AllowAllUp.

Pozostałe przyciski należą do grupy nr 1; z powodu grupowania co najwyżej jeden z nich może być w danej chwili naciśnięty (AllowAllUp = true dla wszystkich przycisków tej grupy).

Słowo na temat migotania

Aby zminimalizować migotanie podczas przemieszczania kontrolki, należy nadać jej status „nieprzezroczystości” przez dodanie do jej właściwości ControlStyle wartości csOpaque, oznaczającej, iż kontrolka całkowicie wypełnia swój obszar klienta, przykrywając całkowicie znajdujące się pod nim inne kontrolki, dzięki czemu te ostatnie wymagają odrysowania dopiero w momencie ich odsłonięcia. Staje się to istotne zwłaszcza w przypadku dużej liczby kontrolek zgrupowanych na pasku narzędziowym lub formularzu - przyciskom kalkulatora status „nieprzezroczystości” nadawany jest w konstruktorze formularza głównego.

Wzbogacanie tekstu symbolami

W myśl zasady stwierdzającej generalną wyższość symboli nad objaśnieniami tekstowymi postanowiliśmy wzbogacić tekstowe opcje naszego menu głównego o stosowną grafikę. Komponent TMainMenu dysponuje możliwością nadawania oznaczeń graficznych swym opcjom, co jest zupełnie, zadowalające pod warunkiem, iż ograniczony rozmiar obrazków (16×16 pikseli) jest wystarczający, a opcje menu nie będą wyłączane (nie można dostarczyć osobnej bitmapy dla wyłączonej opcji, zaś spreparowana wówczas bitmapa oryginalna nie zawsze jest do zaakceptowania). Wykorzystaliśmy tę standardową możliwość w naszych menu - wyjątkiem jest podmenu View menu głównego, którego opcje wyświetlane są z użyciem rysowania specyficznego (owner drawing).

Rysowanie specyficzne opcji menu nie jest tak trudne, jak można by się tego spodziewać w pierwszej chwili. Jest ono realizowane z wykorzystaniem zdarzeń OnDrawItem lub OnAdvancedDrawItem - drugie z nich stwarza nieco większe możliwości, wykorzystamy je więc dla naszych potrzeb. Pojedyncza funkcja obsługi tego zdarzenia dzielona będzie pomiędzy wszystkie opcje rysowane w sposób specyficzny, zaś poszczególne opcje rozróżniane będą na podstawie właściwości Tag. Przypisanie tej funkcji do odpowiedniej właściwości każdej z opcji dokonywane jest w treści konstruktora formularza głównego:

ViewDisplay->OnAdvancedDrawItem = ViewMenuItemsAdvancedDrawItem;

ViewFunctionButtons->OnAdvancedDrawItem = ViewMenuItemsAdvancedDrawItem;

ViewNumberButtons->OnAdvancedDrawItem = ViewMenuItemsAdvancedDrawItem;

ViewConstantsButtons->OnAdvancedDrawItem = ViewMenuItemsAdvancedDrawItem;

ViewStatusBar->OnAdvancedDrawItem = ViewMenuItemsAdvancedDrawItem;

Kod źródłowy funkcji ViewMenuItemsAdvancedDrawItem() przedstawia wydruk 3.17.

Wydruk 3.17. Rysowanie specyficzne opcji menu View

void __fastcall TMainForm::ViewMenuItemsAdvancedDrawItem(TObject* Sender,

TCanvas* ACanvas,

const TRect& ARect,

TOwnerDrawState State)

{

// pierwszy parametr jest wskaźnikiem do odnośnej opcji menu

TMenuItem* MenuItem = dynamic_cast<TMenuItem*>(Sender);

if(MenuItem)

{

// Krok 1 - zachowaj wartość tych właściwości płótna,

// które będą zmieniane

TColor OldFontColor = ACanvas->Font->Color;

TColor OldBrushColor = ACanvas->Brush->Color;

int TextOffset = ARect.Left+1;

try

{

// Krok 2 - narysuj opcję na płótnie w odpowiednim regionie;

// należy sprawdzić, czy opcja jest wybrana i czy jest ona

// aktualnie podświetlona

std::auto_ptr<Graphics::TBitmap> CheckedImage(

new Graphics::TBitmap());

std::auto_ptr<Graphics::TBitmap> ToolbarImage(

new Graphics::TBitmap()

);

ViewMenuImageList->GetBitmap(MenuItem->Tag, ToolbarImage.get());

ToolbarImage.get()->Transparent = true;

if(State.Contains(odChecked))

{

if(State.Contains(odSelected)) // podświetlona

{

MenuCheckImageList->GetBitmap(1, CheckedImage.get());

}

else

{

// niepodświetlona

MenuCheckImageList->GetBitmap(0, CheckedImage.get());

}

ACanvas->Draw(ARect.Left+1,

ARect.Top+2,

CheckedImage.get());

}

ACanvas->Draw(ARect.Left+21,

ARect.Top+2,

ToolbarImage.get());

TextOffset = ARect.Left + 60;

}

__finally

{

if(State.Contains(odSelected)) // podświetlona

{

ACanvas->Font->Color = clHighlightText;

ACanvas->Brush->Color = clHighlight;

}

else // niepodświetlona

{

ACanvas->Font->Color = clWindowText;

ACanvas->Brush->Color = clMenu;

}

ACanvas->FillRect(

Rect(TextOffset, ARect.Top, ARect.Right, ARect.Bottom));

// wypisz tekst opcji; użyj funkcji DrawText, gdyż należy obsłużyć

// ampersandy stanowiące polecenie podkreślenia następnej litery

DrawText( ACanvas->Handle,

MenuItem->Caption.c_str(),

MenuItem->Caption.Length(),

&Rect(TextOffset+2, ARect.Top+2, ARect.Right, ARect.Bottom),

DT_EXPANDTABS|DT_SINGLELINE|DT_LEFT );

ACanvas->Font->Color = OldFontColor;

ACanvas->Brush->Color = OldBrushColor;

}

}

}

Powyższa funkcja rysuje najpierw symbol zaznaczenia opcji, o ile wskazuje na to jej status. Następnie rysowany jest obrazek o rozmiarze 36×18 pikseli, reprezentujący panel, do którego odnosi się opcja. Ostatnią czynnością jest wyświetlenie treści opcji - we właściwym kolorze, zależnym od ewentualnego podświetlenia, a także z odpowiednią obsługą ampersandów (&), które mogą wystąpić w treści opcji i oznaczać polecenie podkreślenia następnej litery; jak pamiętamy, obsługę taką zapewnia standardowo funkcja DrawText(), którą zajęliśmy się szczegółowo przy okazji omawiania paska statusu.

Obrazki przeznaczone do wyświetlenia przechowywane są w liście reprezentowanej przez komponent ViewMenuImageList, zaś indeks identyfikujący konkretny obrazek przechowywany jest pod właściwością Tag odnośnej opcji.

Szczegółowy scenariusz działania funkcji ViewMenuItemsAdvancedDrawItem() jest następujący:

  1. Zapamiętywane są kolory czcionki i pędzla płótna, bowiem w treści funkcji ulegać będą one modyfikacjom.

  2. Tworzone są dwie bitmapy, zawierające (odpowiednio) znak zaznaczenia opcji oraz obrazek reprezentujący element, do którego opcja ta się odnosi. Wykorzystaliśmy w tym celu szablon auto_ptr<> z biblioteki standardowej, co zapewnia destrukcję obydwu bitmap nawet w przypadku wystąpienia wyjątku. Tworzenie bitmap rozpoczyna blok try…, który obejmuje etapy 2. - 5. scenariusza; etap 6. - dokonujący wypisywania tekstu opcji - realizowany jest w bloku __finally…, wykonany więc zostanie niezależnie od powodzenia bądź niepowodzenia całej zabawy z bitmapami.

  3. W liście obrazków (ViewMenuImageList) wyszukiwany jest obrazek związany z bieżącą opcją - jego indeks ukrywa się pod właściwością Tag; z listy MenuCheckImageList wybierany jest natomiast obrazek z „ptaszkiem” właściwy dla bieżącego stanu podświetlenia opcji. Zwróć uwagę na użycie metody Get(), udostępniającej wskaźnik związany z szablonem auto_ptr<>.

  4. Jeżeli opcja jest zaznaczona (na co wskazuje znacznik odChecked właściwości State), rysowany jest symbol zaznaczenia („ptaszek”) w postaci zależnej od tego, czy opcja jest aktualnie podświetlona, czy też nie (informuje o tym znacznik odSelected właściwości State).

  5. Rysowany jest zarówno ewentualny symbol zaznaczenia, jak i odpowiedni obrazek; jednocześnie zmienna TextOffset, określająca początkową pozycję dla wypisania tekstu opcji, ustawiana jest na wartość 60. Zmienna ta inicjowana jest wartością 0 na początku funkcji i z taką wartością pozostaje, jeżeli w trakcie wykonania etapów 2. - 5. wystąpi wyjątek - tekst wypisywany jest wówczas bez przesunięcia.

  6. Wypisywany jest tekst opcji w pozycji wskazanej przez zmienną TextOffset. Przed jego właściwym wypisaniem (za pomocą funkcji DrawText()) ustawiony zostaje kolor czcionki i pędzla odpowiedni do stanu podświetlenia opcji, po czym obszar przeznaczony na wypisanie tekstu wypełniany jest kolorem pędzla.

  7. Przywracane są wartości właściwości płótna przechowane w etapie 1.

Do właściwego przebiegu specyficznego rysowania opcji menu konieczne jest jeszcze określenie rozmiarów - wysokości i szerokości - każdej z opcji. Gdy wymiary te są potrzebne, generowane jest zdarzenie OnMeasureItem, któremu w konstruktorze formularza głównego przypisywana jest funkcja obsługi, ta sama dla wszystkich opcji:

ViewDisplay->OnMeasureItem = ViewMenuItemsMeasureItem;

ViewFunctionButtons->OnMeasureItem = ViewMenuItemsMeasureItem;

ViewNumberButtons->OnMeasureItem = ViewMenuItemsMeasureItem;

ViewConstantsButtons->OnMeasureItem = ViewMenuItemsMeasureItem;

ViewStatusBar->OnMeasureItem = ViewMenuItemsMeasureItem;

Sama treść funkcji nie jest specjalnie skomplikowana - wysokość opcji ustalana jest na 22 piksele (wysokość obrazka + po 2 piksele u dołu i u góry), zaś wyliczona szerokość tekstu powiększana jest o 62 piksele (= 1 piksel odstępu + 18 pikseli na „ptaszek” + 2 piksele przerwy + 36 pikseli na obrazek + 3 piksele odstępu + 2 piksele zapasu):

Wydruk 3.18. Określanie rozmiarów opcji menu View

void __fastcall TMainForm::ViewMenuItemsMeasureItem(TObject* Sender,

TCanvas* ACanvas,

int& Width,

int& Height)

{

TMenuItem* MenuItem = dynamic_cast<TMenuItem*>(Sender);

if(MenuItem)

{

Height = 22;

Width = ACanvas->TextWidth(MenuItem->Caption) + 62;

}

}

Jak więc widać, użycie specyficznego rysowania opcji menu w celu nadania im pożądanego wyglądu nie jest specjalnie skomplikowane, zwłaszcza jeżeli zrealizować je w postaci pojedynczej funkcji dzielonej przez wszystkie opcje lub dużą ich grupę - oszczędza się wówczas wiele fatygi związanej z kodowaniem.

Kolorystyka interfejsu

Użycie odpowiedniej kolorystyki w znacznym stopniu czyni wygląd aplikacji bardziej przejrzystym i intuicyjnym. W naszym przykładzie użyliśmy zróżnicowanych kolorów do funkcjonalnego pogrupowania przycisków - i tak przyciski w kolorze szaroniebieskim związane są z operacjami arytmetycznymi, zielone umożliwiają zmianę reprezentacji wyświetlania, pomarańczowe przeznaczone są do edycji lub „czyszczenia” danych, niebieskie - do funkcji związanych z pamięcią, zaś kolor czarny zarezerwowany jest dla przycisków „cyfrowych”.

Przy różnicowaniu kolorystyki aplikacji w celu uwidocznienia różnic w poszczególnych grupach jej interfejsu należy bardzo starannie zastanowić się nad poszczególnymi kolorami - muszą one być wyraźnie odróżnialne od siebie, także przez ewentualnych użytkowników mających kłopoty z prawidłowym postrzeganiem barw.

Użycie nieprostokątnych okien

Przyzwyczajeni - czy raczej przymuszeni - do prostokątnego kształtu okien z przyjemnością spoglądamy na wszelkie odmiany nieprostokątnych kontrolek w interfejsie aplikacji. Windows nie są bowiem tak bezduszne, by skazywać swych użytkowników na żywot w prostokątnym świecie bez żadnej alternatywy. Istnieje kilka sposobów uzyskania kontrolek o zróżnicowanych kształtach, w tym miejscu zaprezentujemy jeden z nich, bardzo prosty i najczęściej używany.

Uzyskanie nieprostokątnego kształtu kontrolki okienkowej sprowadza się do wydzielenia w jej prostokątnych (a jakże!) granicach obszaru zwanego regionem; wszelkie związane z tą kontrolką operacje ograniczać się będą odtąd tylko do tegoż regionu, aż do jego zlikwidowania. Z punktu widzenia Win32 API region jest obiektem graficznego interfejsu Windows (GDI) i jako taki musi być stworzony przed użyciem. Funkcje umożliwiające tworzenie regionów o różnych kształtach zestawione są w tabeli 3.7.

Tabela 3.7. Funkcje używane do tworzenia regionów GDI

Funkcja

Tworzy region w kształcie…

CreateEllipticRgn()

… elipsy wpisanej w prostokątne granice kontrolki.

CreateEllipticRgnIndirect()

… elipsy wpisanej w prostokąt o podanych wymiarach.

CreatePolygonRgn()

… wielokąta, na podstawie wektora wierzchołków i podanego stylu wypełnienia.

CreatePolyPolygonRgn()

… kilku wielokątów, na podstawie tablicy wektorów wierzchołków i podanego stylu wypełnienia.

CreateRectRgn()

… prostokąta o podanych współrzędnych podanych jako parametry.

CreateRectRgnIndirect()

… prostokąta podanego jako parametr.

CreateRoundRectRgn()

… prostokąta o zaokrąglonych narożnikach; zaokrąglenia mają kształt elipsy o podanych parametrach.

ExtCreateRegion()

… stanowiącym wynik żądanej transformacji wskazanego regionu.

CombineRgn()

… stanowiącym wynik żądanej kombinacji dwóch wskazanych regionów.

Informacje na temat szczegółów deklaracji ww. funkcji znaleźć można w systemie pomocy Win32 SDK.

Po utworzeniu regionu należy skojarzyć go z odnośną kontrolką - służy do tego funkcja SetWindowRgn() z biblioteki Win32 API:

int SetWindowRgn(

HWND hWND, // uchwyt okna odnośnej kontrolki

HRGN hRgn, // uchwyt regionu zwracany przez tworzącą go funkcję

BOOL bRedraw // znacznik decydujący o odrysowaniu okna - zazwyczaj true dla

// widocznych kontrolek

);

Niezerowy wynik funkcji wskazuje, iż operacja zakończyła się pomyślnie. Ponieważ pierwszym parametrem funkcji jest uchwyt okna, regiony mogą być kojarzone tylko z kontrolkami okienkowymi (klasy TWinControl i pochodnych). Przykładowym efektem zastosowania nieprostokątnych regionów w naszej aplikacji są przyciski odpowiadające stałym matematycznym. Są to prostokąty z koliście zaokrąglonymi narożnikami - dla przycisku reprezentującego stałą π region tworzony jest przez następującą instrukcję:

Wydruk 3.19. Tworzenie prostokątnego regionu z zaokrąglonymi narożnikami

// utworzenie regionu

HRGN hRoundRectRegion1 =

CreateRoundRectRgn(

0, // lewa krawędź

0, // górna krawędź

ConstantPieBitBtn->Width+1, // prawa krawędź

ConstantPieBitBtn->Height+1, // dolna krawędź

14, // zaokrąglenie - okrąg

14 // o średnicy 14

);

// przypisanie regionu do kontrolki

SetWindowRgn(ConstantPieBitBtn->Handle, hRoundRectRegion1, TRUE);

Jeżeli region nie jest już dłużej potrzebny, można go usunąć za pomocą makra DeleteRgn():

#define DeleteRgn(hrgn) DeleteObject((HGDIOBJ)(HRGN)(hrgn))

Nie należy tego jednak robić, jeżeli region przypisany został do kontrolki za pomocą funkcji SetWindowRgn(), bowiem z chwilą przypisania stał się on własnością systemu operacyjnego i jego uchwyt nie może być już dłużej wykorzystywany przez użytkownika do jakichkolwiek celów.

Odłączenie regionu od kontrolki następuje w wyniku wywołania funkcji SetWindowRgn() z wartością NULL w miejscu uchwytu regionu.

Konfigurowalność interfejsu

Interfejs użytkownika złożonej aplikacji nie zasługuje na miano solidnego, jeżeli nie posiada minimalnego przynajmniej zasobu funkcji konfiguracyjnych. Konfigurowanie aplikacji to pojęcie bardzo szerokie, obejmujące czynności zarówno tak proste, jak zmiana kolorystyki, jak i dość złożone, jak wydokowywanie kontrolek celem zmiany ich układu. Zmiana kolorów wydaje się być przy tym czynnością najprostszą, wymaga bowiem jedynie dostępu do właściwości Color odpowiedniej kontrolki i jednego ze standardowych okien dialogowych. Zbyt daleko posunięta ingerencja użytkownika w kolorystykę interfejsu może jednak nie tylko pogorszyć jego wygląd, lecz także odbić się ujemnie na jego funkcjonalności; przy interfejsie tak złożonym, jak interfejs aplikacji kalkulatora zagrożenie takie staje się całkiem realne. Należy zatem dążyć do tego, by kolorystyka interfejsu oparta była raczej na standardowych kolorach systemowych, których wykaz zawarty jest w tabeli 3.8.

Tabela 3.8. Kolory systemowe

Oznaczenie

Element, do którego odnosi się oznaczenie koloru

clBackground

Tło Pulpitu Windows

clActiveCaption

Pasek tytułowy aktywnego okna

clInactiveCaption

Pasek tytułowy nieaktywnego okna

clMenu

Tło menu

clWindow

Ramka okna

clMenuText

Tekst opcji menu

clWindowText

Tekst w oknie

clCaptionText

Tekst na pasku tytułowym aktywnego okna

clActiveBorder

Obrzeże aktywnego okna

clInactiveBorder

Obrzeże nieaktywnego okna

clAppWorkSpace

Obszar roboczy aplikacji

clHighlight

Tło wybranego tekstu

clHighlightText

Czcionka wybranego tekstu

clBtnFace

Czoło przycisku

clBtnShadow

Cień przycisku

clGrayText

Przyciemniony tekst

clBtnText

Tytuł przycisku

clInactiveCaptionText

Tekst na pasku tytułowym nieaktywnego okna

clBtnHighlight

Rozjaśnienie na przycisku

cl3DDkShadow

Cień obiektu trójwymiarowego

cl3DLight

Jasna (skierowana w stronę źródła światła) powierzchnia obiektu trójwymiarowego

clInfoText

Tekst podpowiedzi

clInfoBk

Tło podpowiedzi

I tak, wypisując tekst w okienku, należy nadać mu kolor clWindowText, zaś jego wyróżnionym częściom - clHighlightText.

W dalszej części zajmiemy się innymi aspektami konfiguracji interfejsu: widocznością poszczególnych jego elementów, ich wyrównywaniem i zmianą rozmiarów oraz dokowaniem. Wszystkie one zostały wykorzystane przy budowie naszego kalkulatora.

Dokowanie

Zgodnie z tym, co pokazuje rysunek 3.4, wyświetlacz kalkulatora może zostać „wydokowany” z reszty interfejsu i funkcjonować jako samodzielne okno. Wyświetlacz ten jest komponentem TPanel o nazwie LCDPanel. Aby możliwe było jego wydokowanie z głównego formularza, należy ustawić trzy następujące właściwości:

Można tego dokonać za pomocą inspektora obiektów na etapie projektowania aplikacji; obsługa wszelkich konsekwencji wydokowania panelu wymaga już natomiast znacznie więcej zachodu.

Pierwszą rzeczą jest oczywiście wychwycenie samej operacji wydokowywania i wydaje się, że należałoby najpierw stworzyć funkcję obsługi zdarzenia OnUndock w głównym formularzu. Niestety, z powodu błędu w bibliotece VCL zdarzenie to nie jest generowane przy pierwszym wydokowaniu kontrolki. Znacznie lepszym wyjściem w tej sytuacji jest przechwycenie zdarzenia OnEndDock, występującego w kontekście wydokowywanego panelu, i kontrolowanie właściwości Floating tego ostatniego - jeżeli właściwość ta ma wartość true i zdarzenie OnEndDock generowane jest po raz pierwszy, oznacza to, że właśnie nastąpiło wydokowanie panelu; dokładniej - zakończyło się „ciągnięcie” (dragging) panelu związane z jego wydokowywaniem. Do sprawdzania, czy zdarzenie OnEndDock wystąpiło po raz pierwszy, użyjemy zmiennej FirstLCDPanelEndDock inicjowanej wartością true; w procedurze zdarzeniowej należy zmienić jej wartość na false, o ile właściwość Floating ma wartość true:

Wydruk 3.20. Obsługa wydokowania panelu wyświetlacza

void __fastcall TMainForm::LCDPanelEndDock(TObject *Sender, TObject *Target,

int X, int Y)

{

if(LCDPanel->Floating)

{

SetFocus();

}

if(FirstLCDPanelEndDock)

{

if(LCDPanel->Floating) FirstLCDPanelEndDock = false;

Height = Height - LCDPanel->Height;

}

}

Pierwszą czynnością, którą wykonuje powyższa funkcja, jest przekazanie skupienia do formularza głównego. Kliknięcie okna wydokowanego panelu pozbawia formularz główny skupienia, odbierając mu jednocześnie możliwość reagowania na naciskanie klawiszy, tak więc przywrócenie mu skupienia jest konieczne dla prawidłowego funkcjonowania aplikacji. Po wydokowaniu panelu wyświetlacza należy także zmniejszyć wysokość formularza głównego. Dzieje się to w ostatniej instrukcji powyższej funkcji przy pierwszym jej wywołaniu niezależnie od właściwości Floating - jeżeli jej wartość równa jest false, generowane jest zdarzenie OnDockDrop, w której wysokość formularza jest odpowiednio zwiększana.

Nie jest za to potrzebne jawne zmienianie współrzędnych kontrolek ButtonsControlBar i StatusBar1, mimo iż wraz ze zmianą wysokości formularza powinno zmienić się ich położenie względem jego górnej krawędzi. Zmiana tego położenia zagwarantowana jest jednak w sposób automatyczny, a to za sprawą właściwości Align obydwu tych kontrolek, równej odpowiednio alClient (wypełnienie całej wolnej pozostałości obszaru klienta formularza) i alBottom (ulokowanie przy dolnej krawędzi formularza).

Ponowne zadokowanie wyświetlacza w formularzu głównym jest nieco bardziej skomplikowane. Jego realizację rozpoczniemy od obsłużenia zdarzenia OnGetSiteInfo. Jeden z parametrów jego funkcji zdarzeniowej (InfluenceRect) określa obszar formularza „wrażliwy” na dokowanie (dock site) - jeżeli kontrolka zostanie „upuszczona” w tym obszarze, zostanie zadokowana. W naszym przykładzie jest to obszar o rozmiarach samego panelu ulokowany w górnej części formularza - oto związane z tym przypisania dokonywane przez funkcję obsługi zdarzenia:

Wydruk 3.21. Obsługa zdarzenia OnGetSiteInfo głównego formularza

void __fastcall TMainForm::FormGetSiteInfo(TObject *Sender,

TControl *DockClient,

TRect &InfluenceRect,

TPoint &MousePos,

bool &CanDock)

{

if(DockClient->Name == "LCDPanel")

{

InfluenceRect.Left = ClientOrigin.x;

InfluenceRect.Top = ClientOrigin.y;

InfluenceRect.Right = ClientOrigin.x + ClientWidth;

InfluenceRect.Bottom = ClientOrigin.y + DockClient->Height;

}

}

Funkcja FormGetSiteInfo() rozpoczyna swą pracę od sprawdzenia, czy upuszczaną kontrolką jest rzeczywiście panel wyświetlacza i jeżeli tak, to ustawia odpowiednio współrzędne obszaru dokowania. Nie są wykorzystywane pozostałe parametry - MousePos (określający położenie kursora myszki) i CanDock (informujący o tym, czy dokowanie w ogóle jest dozwolone).

Kolejnym zdarzeniem, które musimy obsłużyć w związku z dokowaniem, jest OnDockOver. Jest ono generowane wówczas, gdy kursor myszki ciągnący kontrolkę (a właściwie - obrys kontrolki) znajdzie się nad obszarem wrażliwym na dokowanie. Jeden z parametrów funkcji obsługującej to zdarzenie jest wskaźnikiem obiektu klasy TDragDockObject asystującego w procesie przeciągania dokowanego obiektu. Jego właściwość DockRect reprezentuje współrzędne obrysu przeciąganej kontrolki; gdy kursor ciągnący ów obrys znajdzie się nad obszarem dokowania, funkcja obsługi zdarzenia dokona ulokowania obrysu dokładnie w tym obszarze - co stanowi sygnał dla użytkownika, iż może on już puścić przycisk myszki. Notabene stanowi to wyraźny przykład sprzężenia zwrotnego „użytkownik - interfejs”, omawianego wcześniej w tym rozdziale.

Wydruk 3.22. Obsługa zdarzenia OnDockOver głównego formularza

void __fastcall TMainForm::FormDockOver(TObject *Sender,

TDragDockObject *Source,

int X, int Y,

TDragState State,

bool &Accept)

{

if(Source->Control->Name == "LCDPanel")

{

TRect DockingRect( ClientOrigin.x,

ClientOrigin.y,

ClientOrigin.x + ClientWidth,

ClientOrigin.y + Source->Control->Height );

Source->DockRect = DockingRect;

}

}

Pozostałe parametry funkcji nie są używane; gdyby w treści funkcji ustawić Accept = false, kontrolka nie zostałaby zadokowana.

Ostatnim generowanym zdarzeniem, którego obsługę musimy zaimplementować, jest OnDockDrop. Zdarzenie to generowane jest w kontekście kontrolki (tu: formularza głównego), gdy jest w niej dokowana inna kontrolka i umożliwia wykonanie kilku czynności kończących proces dokowania, jak np. zmianę rozmiaru formularza czy też zresetowanie właściwości Anchors i Align dokowanej kontrolki. Implementację funkcji obsługującej zdarzenie OnDockDrop przedstawia wydruk 3.23.

Wydruk 3.23. Obsługa zdarzenia OnDockDrop głównego formularza

void __fastcall TMainForm::FormDockDrop(TObject *Sender,

TDragDockObject *Source,

int X, int Y)

{

if(Source->Control->Name == "LCDPanel")

{

Source->Control->Top = 0;

Source->Control->Left = 0;

Source->Control->Width = ClientWidth;

Height = Height + Source->Control->Height;

Source->Control->Align = alTop;

FirstLCDPanelEndDock = true;

}

}

Dokowany panel reprezentowany jest przez właściwość Control obiektu Source. Funkcja FormDockDrop() po upewnieniu się, iż ma do czynienia z właściwą kontrolką, przywraca panelowi jego oryginalne rozmiary (mogły one zostać zmienione w czasie, gdy kontrolka była wydokowana), zwiększa odpowiednio wysokość formularza, wymusza przywiązanie panelu do górnej krawędzi formularza (Align = alTop), a następnie „resetuje” zmienną FirstLCDPanelEndDock, nadając jej wartość true. Zwróć uwagę na to, iż zmiana rozmiarów formularza dokonywana jest przed zresetowaniem właściwości Align panelu, w przeciwnym razie wysokość formularza mogłaby zostać zwiększona dwukrotnie. Stałoby się tak w sytuacji, gdyby wysokość formularza była zbyt mała, by pomieścić wyświetlacz - wówczas w momencie ustawienia jego wyrównania na alTop formularz zostałby automatycznie powiększony. Jeżeli jednak wykonać zwiększenie wysokości formularza jako pierwszą operację, to na pewno nie zabraknie miejsca na wyświetlacz.

Zwróć również uwagę, iż pasek kontrolny z przyciskami (ButtonsControlBar) oraz pasek statusu (StatusBar1) nie wymagają repozycjonowania, bowiem właściwe położenie zapewnia im właściwość Align - wspominaliśmy już o tym przy obsłudze zdarzenia OnEndDock.

Operacja dokowania (wydokowania) wyświetlacza naszego kalkulatora, jakkolwiek dość pouczająca, jest jednak w gruncie rzeczy operacją bardzo prostą. Bardziej skomplikowane przykłady dokowania znaleźć można w podkatalogu Examples\Docking lokalnej instalacji C++Buildera.

Zmiana rozmiarów kontrolek

Ze zmianą rozmiarów kontrolek w czasie wykonania programu związane są dwa zdarzenia: OnResize i OnConstrainedResize; wybór konkretnego z nich zależy od efektów, które chcemy uzyskać. W aplikacji kalkulatora wykorzystano obydwa zdarzenia - gdy zmieniane są rozmiary panelu LCDPanel, konieczne jest uaktualnienie położenia zawartych w nim etykiet, z kolei podczas zmiany rozmiarów paska kontrolnego ButtonsControlBar należy uważać, by rozmiary te nie zostały zmniejszone poniżej rozmiarów niezbędnych do zmieszczenia zawartych w nim widocznych paneli z przyciskami. W pierwszym przypadku wystarczająca będzie obsługa zdarzenia OnResize, w drugim natomiast musimy wykorzystać zdarzenie OnConstrainedResize, którego obsługa - w przeciwieństwie do zdarzenia OnResize - zdolna jest powstrzymać operację zmiany rozmiarów kontrolki.

Zarządzanie rozmiarami kontrolek odbywać się może także na szczeblu bardziej ogólnym, z wykorzystaniem właściwości: Align, Anchors, AutoSize, Constraints, Height, Left, Top i Width. Zaprezentujemy wykorzystanie ich wszystkich - z wyjątkiem Autosize, powodującej (gdy ustawić ją na true) automatyczne dostosowywanie rozmiarów kontrolki do jej aktualnej zawartości.

Wyrównanie - właściwość Align

Gdy umieścić na formularzu komponent StatusBar ze strony Win32 palety komponentów, ulokuje się on przy dolnej krawędzi formularza i będzie przy niej tkwił niezależnie od ewentualnej zmiany jego rozmiarów; na podobnej zasadzie komponenty ToolBar i CoolBar z tejże strony wykazują przywiązanie do górnej krawędzi formularza, zaś np. komponent Splitter ze strony Standard - do lewej.

Za tego rodzaju przywiązanie określonej krawędzi kontrolki do jej kontrolki - kontenera odpowiedzialna jest właściwość Align. Jest to właściwość typu wyliczeniowego, a jej poszczególne elementy umożliwiają przywiązanie kontrolki do (odpowiednio) lewej (alLeft), prawej (alRight), górnej (alTop) lub dolnej (alBottom) krawędzi pojemnika, bądź też do pozostałego jeszcze wolnego miejsca w jego obszarze klienta (alClient). Jeżeli połączyć to wszystko z właściwością Constraints (odpowiedzialną za ograniczenia w zakresie zmian rozmiarów kontrolki), możliwości tworzenia „elastycznych” interfejsów stają się wcale niemałe - czego namiastkę prezentuje przykładowy projekt Panels.bpr, bazujący na układzie odpowiednio „wyrównanych” paneli, spośród których dwóm (Panel1 i Panel2) narzucono dodatkowe ograniczenia minimalnej (odpowiednio) wysokości i szerokości, co stanowi zabezpieczenie przed zbytnim zmniejszeniem formularza.

Interfejs naszej aplikacji kalkulatora podzielony jest na trzy części. Górną część stanowi wyświetlacz LCDPanel, wyrównany do górnej krawędzi formularza (Align=alTop), dolną - pasek statusu StatusBar1 wyrównany do krawędzi dolnej (Align=alBottom), zaś pośrodku znajduje się pasek kontrolny z przyciskami (ButtonsControlBar) zajmujący pozostałą część obszaru klienta (Align=alClient). Dwa pierwsze z wymienionych mają zablokowaną możliwość zmiany wysokości z powodu ustawienia na tę samą wartość właściwości Constraints->MinHeight i Constraints->MaxHeight każdego z nich. Opisany układ przedstawia rysunek 3.7.

Tutaj należy umieścić rysunek znajdujący się w pliku ORIG-5-5.BMP i uzupełnić go oznaczeniami podobnie jak na rysunku ze strony 174 oryginału, jednak z następującymi różnicami:

1. LCDPanel ma być pisane razem

Rysunek 3.7. Wyrównanie elementów interfejsu kalkulatora

Tak więc gdy kalkulator zmienia swe rozmiary, wyświetlacz i pasek statusu mogą zmieniać jedynie swą szerokość, natomiast pasek kontrolny ButtonsControlBar zmienia swe rozmiary tak, by w dalszym ciągu wypełniać cały pozostały obszar klienta. Ponadto wszystkie trzy panele znajdujące się w tym pasku kontrolnym mają zablokowaną możliwość zmiany swych rozmiarów, skutkiem czego układ przycisków pozostaje bez zmian.

Zakotwiczenie - właściwość Anchors

Idea zakotwiczenia kontrolki podobna jest do jej wyrównywania i polega na utrzymywaniu stałej odległości pomiędzy odpowiednią krawędzią tejże kontroli i odpowiednią krawędzią kontrolki-kontenera, gdy zmieniają się rozmiary tej ostatniej. Zakotwiczenie może dotyczyć lewych (Anchors=akLeft), górnych (Anchors=akTop), prawych (Anchors=akRight) lub dolnych (Anchors=akBottom)krawędzi wspomnianych kontrolek, bądź też dowolnej ich kombinacji - właściwość Anchors jest bowiem właściwością zbiorową (Set<>) i wymienione zakotwiczenia mogą być ustalane niezależnie od siebie.

W naszym kalkulatorze zakotwiczenie odgrywa szczególną rolę w funkcjonowaniu wyświetlacza. Składa się on z dwóch paneli, trzech etykiet (TLabel) i trzech przycisków (TSpeedButton). Wzajemne powiązanie tych elementów wyjaśnia rysunek 3.8: na panelu LCDPanel (to ten w czarnym kolorze) ulokowane są przyciski oraz złocisty panel BackgroundPanel. Na tym ostatnim ulokowane są trzy etykiety: LCDScreen, ExponentLabel i HistoryLabel odpowiedzialne za wyświetlanie (odpowiednio): aktualnej zawartości rejestru kalkulatora, jego części wykładniczej (jeżeli wynik wyświetlany jest w takiej postaci) oraz historii obliczeń.

Tutaj ma się znaleźć rysunek odpowiadający rysunkowi ze strony 175 oryginału. Ten oryginalny mógłby w zasadzie zostać, jednak jego wskaźniki są bardzo nieczytelne. Proponuję więc moją wersję rysunku, co do której mam nadzieję, iż doprowadzona do właściwej postaci przez grafika nadawać się będzie do umieszczenia w książce. Znajduje się ona w pliku ORIG-5-6.BMP i wygląda tak:

0x01 graphic

Gdyby napisy okazały się niezbyt odpowiednie, w pliku ORIG-5-6-BEZ-NAPISOW.BMP jest wersja bez napisów:

0x01 graphic

Rysunek 3.8. Zakotwiczenia w panelu wyświetlacza

Stanowiący „podłoże” konstrukcyjne wyświetlacza panel LCDPanel nie może zmieniać swej wysokości - właściwość Constraints określa ją sztywno na 77 pikseli. Szerokość jego może się zmieniać, nie może jednak zejść poniżej granicy określonej przez Constraints->MinWidth na 227 pikseli.

Panel BackgroundPanel stanowi (zgodnie z nazwą) tło dla wyświetlanej informacji. Jest on zakotwiczony u wszystkich krawędzi panelu - podłoża LCDPanel, a więc podobnie jak i on nie może zmieniać wysokości w czasie wykonania programu. Ukazujące się na nim napisy są tytułami (Caption) trzech etykiet zakotwiczonych w specyficzny sposób. Górna etykieta (HistoryLabel) zakotwiczona jest u lewej, górnej i prawej krawędzi panelu - tła, dzięki czemu rozciąga się zawsze w poziomie na cały obszar wyświetlania - generalnie jeżeli kontrolka zakotwiczona jest u przeciwległych krawędzi swej kontrolki macierzystej, to może ona ulegać rozciąganiu lub kurczeniu, gdy kontrolka macierzysta zmienia swe rozmiary w odpowiednim kierunku. Dolna etykieta (LCDScreen) zakotwiczona jest u lewej, dolnej i prawej krawędzi panelu - tła; nie zakotwiczyliśmy jej u górnej krawędzi, by jej wysokość nie ulegała zmianie, gdy w czasie projektowania zmienimy wysokość panelu - tła (w czasie wykonania wysokości tej zmienić nie sposób, ze względu na zakotwiczenie panelu - tła u wszystkich krawędzi panelu LCDPanel).

Notabene ze względu na to, iż panel - tło nie może w czasie wykonania zmieniać swej wysokości, nie jest potrzebne żadne zakotwiczenie etykiet w pionie; ich zakotwiczenie u wszystkich przyległych krawędzi panelu - tła podyktowane zostało wyłącznie wygodą na etapie projektowania aplikacji.

Etykieta wyświetlająca wykładnik zakotwiczona została u prawej i dolnej krawędzi panelu - tła (zgodnie z uwagą w poprzednim akapicie zakotwiczenie u dolnej krawędzi ma znaczenie jedynie w czasie projektowania aplikacji). Widoczność etykiety wykładnika regulowana jest w ciekawy sposób - jest ona albo zasłonięta przez etykietę LCDScreen, albo odsłonięta, zależnie od szerokości tejże etykiety. Sama zmiana szerokości etykiety LCDScreen może jednak nie wystarczyć - dla kontrolki zakotwiczonej poziomo (tj. jednocześnie u lewej i prawej krawędzi) zmiana jej szerokości może powodować przesunięcie jej pionowych krawędzi. To, która krawędź zostanie przesunięta, zależy od poziomego wyrównania kontrolki: aby przesunięta została prawa krawędź (bez naruszenia pozycji lewej krawędzi), kontrolka musi być wyrównana lewostronnie, zatem na czas odsłaniania etykiety wykładnika wyrównanie takie trzeba wymusić w stosunku do etykiety LCDScreen, a następnie przywrócić jej poprzednie wyrównanie:

TAlign LCDSCreenAlign=LCDScreen->Align;

LCDScreen->Align = alLeft;

LCDScreen->Width = LCDScreen->Width - ExponentLabel->Width;

LCDScreen->Align = LCDSCreenAlign;

W oryginale autor pisze o właściwości Alignment, co jest zupełnie bez sensu, bo właściwość ta decyduje o wyrównaniu tekstu wewnątrz etykiety, nie zaś samych granic etykiety, z którymi związane jest kotwiczenie. Autorowi na pewno chodziło o Align, a pośpiech zrobił swoje.

Identyczny efekt uzyskać można jednak jeszcze prościej, mianowicie zapamiętując położenie lewej krawędzi etykiety LCDScreen przed zmianą jej rozmiarów i przywracając je po zmianie:

int LCDScreenLeft = LCDScreen->Left;

LCDScreen->Width = LCDScreen->Width - ExponentLabel->Width;

LCDScreen->Left = LCDScreenLeft;

Mechanizm kotwiczenia bywa szczególnie użyteczny w wielu różnych sytuacjach - przykładem takiej sytuacji może być rozmieszczenie przycisków w oknach dialogowych mogących zmieniać swe rozmiary. W cytowanym już projekcie Panels.bpr na panelu Panel2 (niebieskim) znajdują się dwa przyciski TBitBtn, zakotwiczone u lewej krawędzi panelu. Gdy zmienia się szerokość formularza, zmienia się również szerokość niebieskiego panelu, a obydwa przyciski pozostają przy jego lewej krawędzi.

Jeśli chodzi o projekt Panels.bpr, to w oryginale mowa jest o czerwonym panelu Panel5 i na nim też znajdują się oryginalnie obydwa przyciski, panel ten nie zmienia jednakże swoich rozmiarów, więc sposób zakotwiczenia przycisków w ogóle się nie ujawnia. Zmodyfikowałem więc projekt, przenosząc przyciski na panel niebieski.

Ograniczenia swobody zmiany rozmiarów - właściwość Constraints

Zmiana rozmiarów kontrolki ma zazwyczaj sens jedynie w pewnych granicach, w każdym razie dobrze jest tę zmianę kontrolować. Jednym z elementów takiej kontroli są ograniczenia (jedno- lub obustronne) na wysokość i (lub) szerokość kontrolki. Za ograniczenia te odpowiedzialna jest właściwość Constraints, która - sama będąc klasą - zawiera właściwości: MaxHeight, MaxWidth, MinHeight, MinWidth określające (zgodnie ze swymi nazwami) maksymalne i minimalne wartości wysokości i szerokości.

We wspominanym już wielokrotnie projekcie Panels.bpr wykorzystaliśmy tę właściwość do kontrolowania rozmiarów formularza głównego tak, by nie stał się on zbyt mały. Minimalna wysokość (MinHeight) panelu Panel1 ustawiona została na 300; minimalna szerokość (MinWidth) panelu Panel2 ustawiona została na taką samą wartość - a to oznacza, iż obszar klienta formularza zawierającego te panele nie może mieć rozmiarów mniejszych niż 300 × 300 pikseli. Ustawienie tych wartości bezpośrednio w formularzu głównym (Form1->Constraints) nie dałoby takich samych rezultatów, bowiem ograniczenia te odnosiłyby się do formularza jako całości, nie zaś tylko jego obszaru klienta.

Projekt kalkulatora również korzysta z kilku ograniczeń. Przede wszystkim zapewniono dolne ograniczenie rozmiarów formularza (248×52 piksele) tak, by zawsze widoczne było przynajmniej menu główne; uzyskanie jednakże tak nikłej wysokości możliwe jest tylko po ukryciu (za pomocą menu View) wszystkich obiektów kalkulatora, standardowo bowiem jego obszar klienta musi być wystarczający dla trzech kontrolek o niezmiennej wysokości: wyświetlacza (LCDPanel - 77 pikseli), paska kontrolnego zawierającego trzy panele z przyciskami (FunctionButtonsPanel i ConstantsButtonsPanel - po 27 pikseli, NumberButtonsPanel - 213 pikseli) oraz paska statusu (StatusBar1 - 30 pikseli).

Zdarzenie OnConstrainedResize

Zdarzenie OnConstrainedResize generowane jest każdorazowo przy zmianie rozmiarów kontrolki - czy to wyniku puszczenia np. ciągniętej krawędzi formularza, czy też zmiany spowodowanej np. zakotwiczeniem. Do jego funkcji obsługi przekazywane są przez referencję cztery parametry zawierające (kolejno): minimalną szerokość, minimalną wysokość, maksymalną szerokość i maksymalną wysokość zgodnie z bieżącymi ustawieniami właściwości Constraints dla kontrolki. Po wyjściu z funkcji wartości tych parametrów stają się nowymi ograniczeniami, tak więc możliwe jest modyfikowanie właściwości Constraints kontrolki w ramach obsługi jej zdarzenia OnConstrainedResize. Nowe rozmiary kontrolki nie zostaną zaakceptowane, jeżeli nie będą zgodne z tymi nowymi ograniczeniami.

W naszym projekcie wykorzystujemy zdarzenie OnConstrainedResize do dynamicznej weryfikacji ograniczeń rozmiarów panelu ButtonsControlBar:

Wydruk 3.24. Dynamiczna weryfikacja ograniczeń Constraints panelu przycisków

void __fastcall TMainForm::ButtonsControlBarConstrainedResize(TObject* Sender,

int& MinWidth,

int& MinHeight,

int& MaxWidth,

int& MaxHeight)

{

GetControlBarMinWidthAndHeight(ButtonsControlBar, MinWidth, MinHeight);

}

Do obliczenia minimalnych rozmiarów paska kontrolnego ButtonsControlBar wykorzystywana jest funkcja GetControlBarMinWidthAndHeight(), badająca położenie i rozmiary wszystkich kontrolek, dla których panel ten jest kontrolką macierzystą; wskaźniki do kontrolek potomnych danej kontrolki macierzystej znajdują się pod jej właściwością tablicową Controls[]. Oto treść tej funkcji:

Wydruk 3.25. Obliczanie minimalnych rozmiarów kontrolki macierzystej

void __fastcall TMainForm::GetControlBarMinWidthAndHeight(

TCustomControlBar* ControlBar,

int& MinWidth,

int& MinHeight)

{

int MinLeft = 0;

int MinTop = 0;

int MaxRight = 0;

int MaxBottom = 0;

bool FirstVisible = true;

for(int i=0; i<ControlBar->ControlCount; ++i)

{

if(ControlBar->Controls[i]->Visible)

{

if(FirstVisible)

{

MinLeft = ControlBar->Controls[i]->Left-11;

MinTop = ControlBar->Controls[i]->Top-2;

MaxRight = ControlBar->Controls[i]->Left

+ ControlBar->Controls[i]->Width + 2;

MaxBottom = ControlBar->Controls[i]->Top

+ ControlBar->Controls[i]->Height + 2;

FirstVisible = false;

}

else

{

if((ControlBar->Controls[i]->Left-11) < MinLeft)

{

MinLeft = ControlBar->Controls[i]->Left-11;

}

if((ControlBar->Controls[i]->Top-2) < MinTop)

{

MinTop = ControlBar->Controls[i]->Top-2;

}

if((ControlBar->Controls[i]->Left

+ ControlBar->Controls[i]->Width + 2) > MaxRight)

{

MaxRight = ControlBar->Controls[i]->Left

+ ControlBar->Controls[i]->Width + 2;

}

if((ControlBar->Controls[i]->Top

+ ControlBar->Controls[i]->Height + 2) > MaxBottom)

{

MaxBottom = ControlBar->Controls[i]->Top

+ ControlBar->Controls[i]->Height + 2;

}

}

}

}

MinWidth = (MaxRight - MinLeft);

MinHeight = (MaxBottom - MinTop);

}

Dla każdej kontrolki obliczane jest położenie jej czterech krawędzi i w wyniku porównań ustalane jest położenie najwyższej krawędzi górnej, najniżej krawędzi dolnej, najbardziej na lewo położonej krawędzi lewej i najbardziej na prawo położonej prawej krawędzi. Przy okazji należy jednak rozwiązać pewien istotny problem: otóż panel w momencie ulokowania go na pasku kontrolnym TControlBar zyskuje dodatkowe „obrzeże” o wielkości 11 pikseli z lewej strony (tzw. uchwyt przeciągania) i po 2 piksele na pozostałych krawędziach, lecz obrzeże to nie jest uwzględniane we właściwościach: Left, Top, Right, Bottom, Width i Height panelu, mimo iż jego obecność należy uwzględnić przy planowaniu układu paneli na pasku kontrolnym; do obliczeń należy więc wziąć nie oryginalne współrzędne i rozmiary paneli, lecz zmodyfikowane o wymienione rozmiary obrzeża.

Zdarzenie OnResize

Zdarzenie OnResize generowane jest po ustaleniu nowych rozmiarów kontrolki i umożliwia zmianę jej wyglądu bądź też wykonanie innych operacji związanych ze zmianą rozmiarów. Nie należy w ramach obsługi zdarzenia OnResize zmieniać właściwości Constraints - należy w tym celu posłużyć się zdarzeniem OnConstrainedResize.

W naszym kalkulatorze wykorzystujemy zdarzenie OnResize do adjustacji etykiet zawartych w panelu - tle wyświetlacza po zmianie jego szerokości:

Wydruk 3.26. Adjustacja etykiet wyświetlacza

void __fastcall TMainForm::LCDPanelResize(TObject *Sender)

{

UpdateHistoryLabel(HistoryLabel->Caption);

UpdateLCDScreen(LCDScreen->Caption);

if(LCDPanel->Floating && MainForm->Visible) SetFocus();

}

Powyższa funkcja wykonuje trzy zasadnicze czynności: uaktualnia etykietę historii obliczeń, uaktualnia „główną” zawartość wyświetlacza oraz zapewnia przeniesienie skupienia na formularz główny.

Uaktualnienie „głównej” zawartości, czyli etykiety LCDScreen, odbywa się przez porównanie nowej zawartości (przekazanej przez parametr) z obecną (ukrywającą się pod właściwością Caption), skonfrontowaniem tego porównania z nową szerokością panelu LCDPanel i ew. wyświetleniu nowej zawartości. Funkcję UpdateLCDScreen() prezentowaliśmy już na wydruku 3.11 - dla kompletności przytoczymy go tutaj ponownie:

Wydruk 3.27. Uaktualnianie „głównej” zawartości wyświetlacza

void __fastcall TMainForm::UpdateLCDScreen(const AnsiString& NewNumber)

{

int NumberWidth = LCDScreen->Canvas->TextWidth(NewNumber);

if(Operation == coComplete)

{

if( (NumberWidth >= LCDScreen->Width)

&& (LCDScreen->Alignment == taRightJustify) )

{

LCDScreen->Alignment = taLeftJustify;

}

else if( (NumberWidth < LCDScreen->Width)

&& (LCDScreen->Alignment != taRightJustify) )

{

LCDScreen->Alignment = taRightJustify;

}

}

else if(LCDScreen->Alignment != taRightJustify)

{

LCDScreen->Alignment = taRightJustify;

}

LCDScreen->Caption = NewNumber;

int pos = LCDScreen->Hint.Pos("|");

int length = LCDScreen->Hint.Length();

AnsiString LCDScreenHint

= LCDScreen->Hint.SubString(pos, length-pos+1);

LCDScreen->Hint = NewNumber + LCDScreenHint;

if(NumberWidth >= LCDScreen->Width) LCDScreen->ShowHint = true;

else LCDScreen->ShowHint = false;

}

Scenariusz realizowany przez powyższą funkcję składa się z trzech zasadniczych etapów:

  1. Sprawdzana jest szerokość (w pikselach) nowego tekstu do wyświetlenia (ten „nowy” tekst jest w tym przypadku tożsamy z zawartością „tytułu” etykiety (Caption)).

  1. Wybierany jest odpowiedni tryb wyrównywania zawartości etykiety w jej własnych granicach (właściwość Alignment) zależny od tego, czy żądana operacja już się zakończyła (tj. czy wyświetlony został wynik), czy jeszcze trwa. Jeżeli po zakończonej operacji etykieta nie jest dostatecznie szeroka do wyświetlenia kompletnego wyniku, wymuszane jest lewostronne wyrównanie zawartości, początek wyniku jest bowiem zazwyczaj ważniejszy od jego końcówki (jeżeli już oczywiście musimy wybierać); przy dostatecznej szerokości wybierane jest wyrównanie prawostronne. Jeżeli operacja jeszcze się nie zakończyła (Operation różne jest od coComplete), to oznacza, że wyświetlana wartość jest właśnie edytowana lub może być edytowana za chwilę - w takiej sytuacji najważniejsze są cyfry nowo wprowadzane, a więc wymusić należy wyrównanie prawostronne.

  2. Jeżeli nowa zawartość nie mieści się w oknie wyświetlacza, jest ona podstawiana pod właściwość Hint etykiety LCDScreen, by mogła być na żądanie wyświetlona (w całości) w formie podpowiedzi kontekstowej.

Uaktualnienie etykiety HistoryLabel jest nieco prostsze, bowiem sposób wyświetlania jej treści nie ma żadnego odniesienia do bieżącego statusu operacji:

Wydruk 3.28. Uaktualnianie etykiety historii obliczeń

void __fastcall TMainForm::UpdateHistoryLabel(const AnsiString& NewHistory)

{

int HistoryWidth = HistoryLabel->Canvas->TextWidth(NewHistory);

if( (HistoryWidth >= HistoryLabel->Width)

&& (HistoryLabel->Alignment == taLeftJustify) )

{

HistoryLabel->Alignment = taRightJustify;

}

else if( (HistoryWidth < HistoryLabel->Width)

&& (HistoryLabel->Alignment != taLeftJustify) )

{

HistoryLabel->Alignment = taLeftJustify;

}

HistoryLabel->Caption = NewHistory;

}

Podobnie jak w przypadku etykiety LCDScreen nowa zawartość konfrontowana jest z długością etykiety. Jeżeli etykieta nie jest dostatecznie szeroka, by wyświetlić swą zawartość, wymuszane jest wyrównanie prawostronne, by widoczne były elementy z „historii najnowszej”; w przeciwnym razie tekst wyrównywany jest lewostronnie.

Ostatnią czynnością związaną ze zmianą rozmiarów wyświetlacza jest przywrócenie skupienia formularzowi głównemu w sytuacji, gdy wyświetlacz jest wydokowany, sam zaś formularz główny jest widoczny - badanie tego ostatniego warunku jest konieczne, ponieważ zdarzenie OnResize kontrolki zawartej w formularzu może zostać wygenerowane jeszcze przed pierwszym wyświetleniem formularza.

Wykorzystanie paska kontrolnego TControlBar

Kontrolka TControlBar używana jest zwyczajowo w roli wizualnego kontenera dla pasków narzędziowych TToolBar, może jednak pełnić tę rolę w stosunku do innych kontrolek, które nawet nie muszą być tego samego typu. W naszym kalkulatorze komponent TControlBar nosi nazwę ButtonsControlBar (patrz rys. 3.7); w celu przystosowania go do warunków naszej aplikacji nadaliśmy w czasie projektowania niestandardowe wartości kilku jego właściwościom, a mianowicie:

W charakterze wstęg - pojemników na przyciski umieściliśmy w pasku kontrolnym trzy panele, które automatycznie przekształcone zostały do postaci ułatwiającej ich przeciąganie (vide 11-pikselowy uchwyt z lewej strony - pisaliśmy już o tym przy okazji obliczania minimalnych rozmiarów paska).

W czasie tworzenia projektu często okazywało się konieczne przenoszenie całych grup przycisków pomiędzy panelami; kłopot jednak w tym, iż nie sposób zaznaczyć grupy przycisków (zakreślając myszą stosowny prostokąt) bez jednoczesnego zaznaczenia ich kontrolki-pojemnika. Najlepszym wyjściem wydaje się wówczas sięgnięcie do tekstowej reprezentacji formularza (udostępnianej poprzez opcję View as Text z jego menu kontekstowego) i przenoszenie fragmentów tekstu odpowiadających poszczególnym grupom kontrolek.

Umieszczenie trzech różnych grup przycisków (stałe, sterujące i numeryczne) na trzech oddzielnych panelach paska kontrolnego samo w sobie stwarza już duże możliwości w zakresie elastycznego konfigurowania interfejsu, które jednak w naszym projekcie postanowiliśmy wesprzeć dodatkowymi funkcjami, głównie w celu wyeliminowania nakładania się poszczególnych kontrolek. W efekcie wprowadziliśmy możliwość poziomego wyrównywania poszczególnych paneli (jednego wskazanego albo wszystkich) do lewej lub prawej krawędzi oraz „resetowanie” paska kontrolnego do wymiarów minimalnych, niezbędnych do zmieszczenia paneli. Funkcje te dostępne są poprzez menu kontekstowe paska, zawierające następujące opcje:

  1. Snap to Fit - powoduje zresetowanie paska do rozmiarów minimalnych.

  2. Align Left - dokonuje wyrównania do lewej krawędzi paska tego panelu, w obrębie którego znajduje się kursor myszy.

  3. Align Right - dokonuje wyrównania do prawej krawędzi paska tego panelu, w obrębie którego znajduje się kursor myszy.

  4. Align All Left - dokonuje wyrównania wszystkich paneli do lewej krawędzi paska.

  5. Align All Right - dokonuje wyrównania wszystkich paneli do prawej krawędzi paska.

Opcje 2. i 3. odnoszą się do panelu wskazywanego aktualnie przez kursor myszy, jeżeli więc kursor znajduje się poza obszarami wszystkich paneli, opcje te w menu kontekstowym nie występują.

Resetowanie paska kontrolnego do rozmiarów minimalnych

Przyjrzyjmy się najpierw szczegółowo operacji resetowania paska kontrolnego, odpowiadającej opcji Snap to Fit. Funkcja zdarzeniowa tej opcji zawiera jedynie wywołanie pomocniczej funkcji FitToControlBar(), której treść przedstawia wydruk 3.29.

Wydruk 3.29. Resetowanie paska kontrolnego

void __fastcall TMainForm::FitToControlBar(TCustomControlBar* ControlBar)

{

int MinWidth = 0;

int MinHeight = 0;

GetControlBarMinWidthAndHeight(ControlBar, MinWidth, MinHeight);

int WidthDifference = ButtonsControlBar->Width - MinWidth;

int HeightDifference = ButtonsControlBar->Height - MinHeight;

Width = Width - WidthDifference;

Height = Height - HeightDifference;

}

Po obliczeniu niezbędnych, minimalnych rozmiarów paska następuje sprawdzenie, o ile rozmiary te są przekroczone; ów nadmiar jest następnie odejmowany od rozmiarów - uwaga - formularza głównego. To dziwne na pozór „okrężne” postępowanie (czyż nie prościej byłoby podstawić obliczone rozmiary minimalne wprost pod właściwości paska?) wynika z faktu, iż pasek kontrolny wyrównany jest do obszaru klienta formularza głównego (Align=alClient) i jakiekolwiek bezpośrednie manipulowanie jego rozmiarami na nic by się zdało; aby osiągnąć pożądany efekt, należy odpowiednio zmniejszyć właśnie ów obszar klienta.

Wyrównywanie paneli wewnątrz paska kontrolnego

Jak przed chwilą napisaliśmy, poszczególne panele mogą być wyrównywane grupowo (wszystkie naraz) albo niezależnie od siebie, stosownie do wybranej opcji menu kontekstowego. Jeżeli ponadto kursor myszy nie znajduje się nad żadnym panelem, opcje umożliwiające indywidualne wyrównywanie nie powinny się w tym menu pojawić. O repertuarze menu kontekstowego, noszącego tu nazwę ControlBarPopupMenu, decyduje poniższa funkcja obsługi zdarzenia OnContextPopup:

Wydruk 3.30. Obsługa zdarzenia OnContextPopup paska kontrolnego

void __fastcall TMainForm::ButtonsControlBarContextPopup(TObject *Sender,

TPoint &MousePos, bool &Handled)

{

TRect* ControlRects = new TRect[ButtonsControlBar->ControlCount];

try

{

for(int i=0; i<ButtonsControlBar->ControlCount; ++i)

{

if(ButtonsControlBar->Controls[i]->Visible)

{

ControlRects[i] = ButtonsControlBar->Controls[i]->BoundsRect;

ControlRects[i].Left -= 11;

ControlRects[i].Top -= 2;

ControlRects[i].Right += 2;

ControlRects[i].Bottom += 2;

}

else

{

ControlRects[i] = TRect(0,0,0,0);

}

}

for(int i=0; i<ButtonsControlBar->ControlCount; ++i)

{

if(PtInRect(&ControlRects[i], MousePos))

{

AlignLeft1->Visible = true;

AlignRight1->Visible = true;

ControlBarPopupMenu->Tag = ButtonsControlBar->Controls[i]->Tag;

break;

}

else

{

AlignLeft1->Visible = false;

AlignRight1->Visible = false;

ControlBarPopupMenu->Tag = cbpNone;

}

}

}

__finally

{

delete [] ControlRects;

}

}

W celu stwierdzenia, czy kursor myszy jest aktualnie związany z którymś panelem, tworzona jest tablica ControlRects[], której poszczególne elementy - postokąty zawierają rozmiary (BoundsRect) poszczególnych kontrolek (jeżeli któraś z kontrolek jest niewidoczna, odnośny element jest prostokątem o zerowych wymiarach). Każdy z tych prostokątów badany jest następnie - za pomocą funkcji Win32 API PtInRect() - na obecność w jego zakresie punktu o współrzędnych odpowiadających aktualnemu położeniu kursora. Ze względu na to, iż poszczególne kontrolki są panelami o charakterystycznym obrzeżu, należy obrzeże to wykluczyć z testu - inaczej mówiąc: jeżeli kursor znajduje się nad takim obrzeżem, to zakłada się, iż nie znajduje się on nad żadnym panelem. W związku z tym każdy element - prostokąt zmniejszany jest o wymiary tegoż obrzeża. Po zidentyfikowaniu kontrolki, nad którą znajduje się kursor, właściwość Tag tej kontrolki przepisywana jest do właściwości Tag menu kontekstowego, zaś opcje AlignLeft1 i AlignRight1 oznaczane są jako widoczne; jeżeli kursor nie znajduje się aktualnie nad żadną kontrolką, właściwości Tag menu kontekstowego przypisywana jest zarezerwowana wartość cbpNone, zaś wspomniane opcje oznaczane są jako niewidoczne.

Zauważ, iż zastosowana konstrukcja try…__finally zapewnia bezwarunkowe zwolnienie tablicy prostokątów niezależnie od ewentualnego wystąpienia wyjątku.

Zanim przystąpimy do szczegółowego rozpatrywania samej czynności wyrównywania paneli, przyjrzyjmy się sposobowi ich rozróżniania w ramach paska kontrolnego, dokładniej - jego właściwości Controls[]. Otóż rozróżnianie takie możliwe jest dzięki unikatowym wartościom, nadanym właściwości Tag poszczególnych paneli w konstruktorze formularza głównego:

FunctionButtonsPanel->Tag = cbpFunctionButtons;

NumberButtonsPanel->Tag = cbpNumberButtons;

ConstantsButtonsPanel->Tag = cbpConstantsButtons;

Wartości te stanowią elementy następującego typu wyliczeniowego:

enum TControlBarPanel { cbpFunctionButtons,

cbpNumberButtons,

cbpConstantsButtons,

cbpNone };

Tak się niestety składa, iż pasek kontrolny nie zawiera żadnej właściwości związanej z wyrównaniem jego kontrolek potomnych; całą więc obsługę wyrównywania naszych paneli zapewnić musimy we własnym zakresie.

Aby wyrównać dany panel u lewej krawędzi paska kontrolnego, należy mieć na uwadze fakt, iż jego właściwość Left odnosi się do lewej krawędzi poprzedzonej obecnie uchwytem o szerokości 11 pikseli; powinna być więc odsunięta od lewej krawędzi paska właśnie na szerokość tego uchwytu:

Panel->Left = 11; //szerokość uchwytu

Zapewnia to pozostanie panelu przy lewej krawędzi paska, gdy ten będzie zmieniał swoje rozmiary.

Wyrównanie panelu do prawej krawędzi paska jest nieco bardziej skomplikowane. Wydawałoby się, iż wystarczy tu odpowiednie ustawienie współrzędnej lewej krawędzi panelu, oczywiście z uwzględnieniem jego dwupikselowego obrzeża z prawej strony, nie wliczającego się do rozmiaru:

Panel->Left = ButtonControlBar->ClientWidth - Panel->Width - 2;

Spowoduje to co prawda prawostronne wyrównanie panelu i nawet zachowanie tego wyrównania w sytuacji, gdy szerokość paska kontrolnego ulegnie zmniejszeniu; wystarczy jednak, by pasek zwiększył swą szerokość, a jego prawa krawędź oddali się od prawej krawędzi panelu - którego lewa krawędź pozostanie w miejscu określonym przez właściwość Left. Aby zapobiec takiemu nieszczęściu, wystarczy po prostu ustawić tę właściwość na wartość „maksymalną”, czyli większą od największej możliwej szerokości paska kontrolnego:

Panel->Left = Screen->Widtn;

czyli na całą szerokość ekranu; jak widać rozwiązanie bardziej uniwersalne okazało się być trywialnie prostszym.

Funkcja obsługująca kliknięcie w opcję Align Left spycha całą swą pracę na uniwersalną funkcję ArrangeControlBarBands():

Wydruk 3.31. Obsługa kliknięcia opcji lewego wyrównywania panelu

void __fastcall TMainForm::AlignLeft1Click(TObject *Sender)

{

ArrangeControlBarBands(ButtonsControlBar,

TControlBarPanel(ControlBarPopupMenu->Tag),

cbaLeft);

}

Pierwszym parametrem wywołania tej funkcji pomocniczej jest wskaźnik do odnośnego paska kontrolnego, drugim - indeks panelu przepisany w jego właściwości Tag do identycznie nazwanej właściwości menu kontekstowego, trzeci parametr określa natomiast kierunek wyrównywania:

enum TControlBarAlignment { cbaLeft,

cbaRight };

Pełny tekst funkcji ArrangeControlBarBands() przedstawia wydruk 3.32.

Wydruk 3.32. Implementacja funkcji ArrangeControlBarBands()

void __fastcall TMainForm::ArrangeControlBarBands(

TControlBar* ControlBar,

TControlBarPanel CurrentBandTag,

TControlBarAlignment Alignment)

{

// krok 1

std::list<TControlBandInfo> BandList;

TControlBandInfo ControlBarBand;

// krok 2

for(int i=0; i<ControlBar->ControlCount; ++i)

{

if(ControlBar->Controls[i]->Tag == CurrentBandTag)

{

ControlBarBand = TControlBandInfo(ControlBar->Controls[i],

ControlBar->Controls[i]->Left,

ControlBar->Controls[i]->Top,

ControlBar->Controls[i]->Height + 2,

ControlBar->Controls[i]->Visible);

}

BandList.push_back(TControlBandInfo(ControlBar->Controls[i],

ControlBar->Controls[i]->Left,

ControlBar->Controls[i]->Top,

ControlBar->Controls[i]->Height + 2,

ControlBar->Controls[i]->Visible));

}

// krok 3

if(Alignment == cbaLeft)

{

// To samo co BandList.sort()-less<> jest operatorem domyślnym

BandList.sort(std::less<TControlBandInfo>());

ControlBarBand.Left = 11;

}

else if(Alignment == cbaRight)

{

BandList.sort(std::greater<TControlBandInfo>());

ControlBarBand.Left = Screen->Width;

}

// krok 4

std::list<TControlBandInfo>::iterator pos;

bool NoFreeColumn = false;

for(pos = BandList.begin(); pos != BandList.end(); ++pos)

{

// krok 5

if( pos->Control->Tag != CurrentBandTag

&& pos->Visible )

{

// krok 6

if( (Alignment == cbaLeft)

&& (pos->Left < (ControlBarBand.Control->Width + 2)))

{

NoFreeColumn = true;

}

if( (Alignment == cbaRight)

&& ( (ControlBar->ClientWidth-(pos->Left+

pos->Control->Width+2))

< (ControlBarBand.Control->Width + 2) ) )

{

NoFreeColumn = true;

}

// krok 7

if( ControlBarBand.Top >= pos->Top

&& ControlBarBand.Top < (pos->Top + pos->Height + 2) )

{

// Brak miejsca na następny rząd

if(NoFreeColumn) ControlBarBand.Top = pos->Top + pos->Height + 2;

else break;

}

// krok 8

else if( ControlBarBand.Top < pos->Top )

{

// wolne miejsce

std::list<TControlBandInfo>::iterator pos2;

int Offset = 0;

bool FirstVisibleControl = true;

// krok 9

for(pos2 = pos; pos2 != BandList.end(); ++pos2)

{

if(pos2->Visible && FirstVisibleControl &&

pos2->Control->Tag != CurrentBandTag)

{

// First control

Offset = 2 + ControlBarBand.Top + ControlBarBand.Height

- pos2->Top;

FirstVisibleControl = false;

}

if(pos2->Visible) pos2->Top = pos2->Top + Offset;

}

break;

}

NoFreeColumn = false;

}

}

// krok 10

for(pos = BandList.begin(); pos != BandList.end(); ++pos)

{

pos->Control->Visible = false;

if(pos->Control->Tag == CurrentBandTag)

{

pos->Left = ControlBarBand.Left;

pos->Top = ControlBarBand.Top;

}

}

// krok 11

if(Alignment == cbaLeft)

{

BandList.sort(std::less<TControlBandInfo>());

}

else if(Alignment == cbaRight)

{

BandList.sort(std::greater<TControlBandInfo>());

}

// krok 12

for(pos = BandList.begin(); pos != BandList.end(); ++pos)

{

pos->Control->Top = pos->Top;

pos->Control->Left = pos->Left;

pos->Control->Visible = pos->Visible;

}

}

Powyższa funkcja wykorzystuje strukturę TControlBandInfo, której definicję przedstawia wydruk 3.33. Na szczególną uwagę zasługuje tu implementacja operatorów „<” i „>”: obydwa operatory uznają za „mniejszą” tę kontrolkę, której właściwość Top ma mniejszą wartość; jeżeli jednak właściwości Top obydwu porównywanych kontrolek są identyczne, operator „<” uznaje za „mniejszą” tę kontrolkę, która ma mniejszą właściwość Left, zaś operator „>” - tę, której właściwość Left jest większa. Mówiąc obrazowo, obydwa operatory sortują „z góry na dół”, natomiast w przypadku kontrolek położonych na tym samym poziomie pierwszy z operatorów sortuje „z lewa na prawo”, drugi zaś „z prawa na lewo”; każdy z operatorów jest więc przydatny do sortowania w celu właściwego wyrównania kontrolek (odpowiednio) do lewej i do prawej krawędzi paska.

Szczegółowy scenariusz realizowany przez funkcję ArrangeControlBarBands() daje się przedstawić jako sekwencja następujących etapów, wyróżnionych w kodzie źródłowym stosownymi komentarzami:

  1. Tworzona jest lista list<> typu TControlBandInfo nazwana BandList oraz zmienna typu TControlBandInfo o nazwie ControlBarBand - ta ostatnia zawierać będzie dane na temat aktualnie wyrównywanej kontrolki. Aby móc używać listy list<>, należy dołączyć plik nagłówkowy <list>.

  2. Przeprowadzana jest iteracja po wszystkich kontrolkach paska. Informacja o każdej kontrolce dodawana jest do listy BandList; jeżeli badana kontrolka jest kontrolką aktualnie podlegającą wyrównywaniu (o czym świadczy odpowiednia wartość właściwości Tag), informacja o tej kontrolce dodawana jest także do obiektu ControlBarBand.

  3. Następuje sortowanie listy BandList przy użyciu jednego z operatorów „<” lub „>” (odpowiednio: std::less<TControlBandInfo>() lub std::greater<TControlBandInfo>()), zależnie od kierunku wyrównywania. Właściwości Left zmiennej ControlBarBand nadawana jest ponadto wartość stosowna do kierunku wyrównywania. Aby móc używać obiektów less<> i greater<>, należy dołączyć plik nagłówkowy <functional>.

  4. Tworzony jest iterator pos dla celów iterowania po liście BandList.

  5. Sprawdza się, czy kontrolka udostępniana aktualnie przez iterator nie jest kontrolką zapamiętaną w ControlBarBand i czy jest ona widoczna; jeżeli któryś z tych warunków nie jest spełniony, scenariusz przechodzi do kroku 10.

  6. Sprawdza się, czy z odpowiedniej strony kontrolki podlegającej wyrównaniu jest wystarczająco dużo miejsca; ma to znaczenie w sytuacji, gdy badana kontrolka zajmuje ten sam wiersz co kontrolka reprezentowana aktualnie przez ControlBarBand.

  7. Jeżeli w danym wierszu nie ma miejsca na kontrolkę, którą chcemy wyrównać, podejmuje się próbę przeniesienia jej do następnego wiersza.

  8. Jeżeli w poprzednim etapie nastąpiło przeniesienie kontrolki do nowego wiersza, ustawiana jest odpowiednio właściwość Top zmiennej ControlBarBand.

  9. Właściwość Top kontrolek leżących poniżej przesuniętej kontrolki jest odpowiednio uaktualniana.

  10. Kiedy nowe pozycje dla kontrolek zostaną już wyliczone, następuje ich ukrycie (przez ustawienie właściwości Visible na false); gdy iterując wśród kontrolek napotkamy kontrolkę podlegającą aktualnie wyrównywaniu, zawartość zmiennej ControlBarBand przepisywana jest do odpowiedniej pozycji listy BandList.

  11. Gdy już nowe pozycje wpisane zostaną do listy BandList, jest ona sortowana ponownie w celu zapewnienia, iż kontrolka znajdująca się najwyżej zajmuje w tej liście pierwszą pozycję.

  12. W poszczególnych kontrolkach ustawiane są odpowiednio właściwości Top i Left, a właściwość Visible ustawiona zostaje na true.

Mimo iż wygląda to znacznie mniej skomplikowanie, niż początkowo się wydawało, to jednak sama funkcja ArrangeControlBarBands() jest na swój sposób złożona. Nie jest ona jednak doskonała i może być usprawniona; ponadto nasz pasek kontrolny nie jest podzielony na kolumny, więc przemieszczanie kontrolek może wydawać się zbędne - niemniej jednak funkcja ta ilustruje ogólny algorytm wyrównywania kontrolek podzielonych na wstęgi.

Wydruk 3.33. Definicja klasy TControlBandInfo

class TControlBandInfo

{

public:

TControl* Control;

int Left;

int Top;

int Height;

bool Visible;

// konstruktor

inline __fastcall TControlBandInfo() : Control(0),

Left(0),

Top(0),

Height(0),

Visible(false)

{}

// konstruktor kopiujący

inline __fastcall TControlBandInfo(TControl* control,

int left,

int top,

int height,

bool visible) : Control(control),

Left(left),

Top(top),

Height(height),

Visible(visible)

{}

// konstruktor kopiujący

inline __fastcall TControlBandInfo(const TControlBandInfo& ControlBandInfo)

: Control(ControlBandInfo.Control),

Left(ControlBandInfo.Left),

Top(ControlBandInfo.Top),

Height(ControlBandInfo.Height),

Visible(ControlBandInfo.Visible)

{}

// OPERATOR =

TControlBandInfo& operator=(const TControlBandInfo& ControlBandInfo)

{

Control = ControlBandInfo.Control;

Left = ControlBandInfo.Left;

Top = ControlBandInfo.Top;

Height = ControlBandInfo.Height;

Visible = ControlBandInfo.Visible;

return *this;

}

// OPERATOR ==

bool operator==(const TControlBandInfo& ControlBandInfo) const

{

if( Control == ControlBandInfo.Control

&& Left == ControlBandInfo.Left

&& Top == ControlBandInfo.Top

&& Height == ControlBandInfo.Height

&& Visible == ControlBandInfo.Visible)

{

return true;

}

else return false;

}

// OPERATOR <

bool operator<(const TControlBandInfo& ControlBandInfo) const

{

if(Top < ControlBandInfo.Top) return true;

else if( Top == ControlBandInfo.Top

&& Left < ControlBandInfo.Left) return true;

else return false;

}

// OPERATOR >

bool operator>(const TControlBandInfo& ControlBandInfo) const

{

if(Top < ControlBandInfo.Top) return true;

else if( Top == ControlBandInfo.Top

&& Left > ControlBandInfo.Left) return true;

else return false;

}

};

Jedyną różnicą pomiędzy wyrównywaniem pojedynczej kontrolki a wyrównywaniem wszystkich kontrolek paska kontrolnego jest ta, iż w tym drugim przypadku należy wywołać funkcję ArrangeControlBarBands() dla każdej kontrolki. Wydruk 3.34 przedstawia funkcję dokonującą prawostronnego wyrównania wszystkich kontrolek, wywoływaną w reakcji na wybranie opcji Align All Right. Najpierw kontrolki zostają posortowane przy użyciu listy TControlBandInfo (z góry na dół, a w ramach poszczególnych wierszy z prawa na lewo), następnie przy użyciu iteratora z kolejnych kontrolek pobierana jest właściwość Tag i przekazywana jak argument wywołania funkcji ArrangeControlBarBands() w celu ich prawostronnego wyrównania. Po zakończeniu iteracji lista kontrolek jest ponownie sortowana, następnie właściwości Left każdej z kontrolek nadana zostaje wartość maksymalna.

Wydruk 3.34. Prawostronne wyrównywanie wszystkich kontrolek paska kontrolnego

void __fastcall TMainForm::AlignAllRight1Click(TObject *Sender)

{

std::list<TControlBandInfo> BandList;

for(int i=0; i<ButtonsControlBar->ControlCount; ++i)

{

BandList.push_back(TControlBandInfo(

ButtonsControlBar->Controls[i],

ButtonsControlBar->Controls[i]->Left,

ButtonsControlBar->Controls[i]->Top,

ButtonsControlBar->Controls[i]->Height + 2,

ButtonsControlBar->Controls[i]->Visible));

}

BandList.sort(std::greater<TControlBandInfo>());

std::list<TControlBandInfo>::iterator pos;

for(pos = BandList.begin(); pos != BandList.end(); ++pos)

{

ArrangeControlBarBands(ButtonsControlBar,

TControlBarPanel(pos->Control->Tag),

cbaRight);

}

BandList.sort(std::greater<TControlBandInfo>());

for(pos = BandList.begin(); pos != BandList.end(); ++pos)

{

pos->Control->Left = Screen->Width;

}

}

Opisana metoda sortowania kontrolek posiada jedną bardzo pożądaną cechę: wyrównywanie danej kontrolki nie burzy wyrównania innych kontrolek zajmujących ten sam wiersz. Nie jest ona co prawda wolna od wszelkich wad, lecz jednak zapewnia dość duży stopień kontroli nad interfejsem aplikacji.

Kontrolowanie widoczności obiektów interfejsu

Możliwość selektywnego ukrywania poszczególnych elementów interfejsu stanowi kolejną metodę jego konfigurowania: użytkownicy nie wykorzystujący pewnych elementów mogą je po prostu uczynić niewidocznymi. Należy jednak zaprojektować tę funkcję aplikacji w sposób przemyślany, by jakość interfejsu nie uległa przez nią drastycznemu pogorszeniu - i tak na przykład ukrywanie kontrolek nie może powodować powstawania dużych luk, zaś „odkrywane” kontrolki powinny pojawiać się w taki sposób, by nie zniszczyć istniejącego już uporządkowania w ramach pozostałych elementów interfejsu.

Nasza aplikacja kalkulator umożliwia niezależne ukrywanie każdego z paneli paska kontrolnego - przycisków stałych, przycisków funkcyjnych i przycisków numerycznych - oraz wyświetlacza i paska statusu. Zapewniają to poszczególne opcje podmenu View. Gdy kliknąć opcję View na pasku menu głównego, wywołana zostaje funkcja zdarzeniowa prezentowana na wydruku 3.35, dokonująca ustawienia stanu „wybrania” poszczególnych opcji stosownie do stanu widoczności poszczególnych elementów interfejsu.

Wydruk 3.35. Prekonfigurowanie opcji menu View

void __fastcall TMainForm::View1Click(TObject *Sender)

{

if(LCDPanel->Visible) ViewDisplay->Checked = true;

else ViewDisplay->Checked = false;

if(NumberButtonsPanel->Visible) ViewNumberButtons->Checked = true;

else ViewNumberButtons->Checked = false;

if(FunctionButtonsPanel->Visible) ViewFunctionButtons->Checked = true;

else ViewFunctionButtons->Checked = false;

if(ConstantsButtonsPanel->Visible) ViewConstantsButtons->Checked = true;

else ViewConstantsButtons->Checked = false;

if(StatusBar1->Visible) ViewStatusBar->Checked = true;

else ViewStatusBar->Checked = false;

}

Jakkolwiek możliwość ukrywania wyświetlacza wydaje się mało sensowna, to jednak może okazać się przydatna, gdy panel wyświetlacza jest wydokowany i zasłania właśnie jakiś bardziej interesujący obiekt; tak czy inaczej zawsze dostępna jest opcja menu powodująca jego natychmiastowe wyświetlenie, podobnie zresztą jak w przypadku innych paneli. Funkcję zdarzeniową związaną z opcją widoczności panelu wyświetlacza przedstawia wydruk 3.36.

Wydruk 3.36. Konfigurowanie widoczności wyświetlacza

void __fastcall TMainForm::ViewDisplayClick(TObject *Sender)

{

if(ViewDisplay->Checked)

{

LCDPanel->Visible = false;

ViewDisplay->Checked = false;

if(!LCDPanel->Floating)

{

// ustaw odpowiedni rozmiar formularza

Height = Height - LCDPanel->Height;

}

}

else

{

if(!LCDPanel->Floating)

{

// ustaw odpowiedni rozmiar formularza

Height = Height + LCDPanel->Height;

}

LCDPanel->Visible = true;

ViewDisplay->Checked = true;

if(LCDPanel->Floating)

{

SetFocus();

}

// wymuś ustawienie paska statusu u dołu formularza

if(StatusBar1->Visible) StatusBar1->Align = alBottom;

}

}

Operacja ukrywania (odkrywania) jest w przypadku panelu wyświetlacza nieco trudniejsza niż w przypadku pozostałych paneli, ponieważ te ostatnie nie posiadają możliwości dokowania i wydokowywania.

Gdy panel wyświetlacza jest widoczny (ViewDisplay->Checked == true), następuje jego ukrycie (LCDPanel->Visible = false) i likwidacja zaznaczenia opcji menu ((ViewDisplay->Checked = false). Jeżeli ponadto panel ten zadokowany jest w formularzu (!LCDPanel->Floating), następuje zmniejszenie wysokości formularza o wysokość panelu.

Jeżeli panel wyświetlacza jest aktualnie niewidoczny, sprawdza się najpierw, czy jest on aktualnie zadokowany (LCDPanel->Floating = false). Jeżeli tak, to następuje zwiększenie wysokości formularza i uwidocznienie panelu (LCDPanel->Visible = true) oraz zaznaczenie opcji menu; jeżeli jednak jest on wydokowany, to uczynienie go widocznym przekaże mu również skupienie, które natychmiast należy zwrócić na formularz główny (za pomocą jego metody SetFocus()). Jeżeli ponadto widoczny jest pasek statusu, wymusza się jego przesunięcie do dolnej krawędzi formularza.

Wyświetlenie ukrytego dotąd paska statusu jest natomiast całkiem proste; jedyne, o czym trzeba pamiętać, to odpowiednie zwiększenie wysokości formularza.

Wydruk 3.37. Uwidocznienie paska statusu

void __fastcall TMainForm::ViewStatusBarClick(TObject *Sender)

{

if(ViewStatusBar->Checked)

{

StatusBar1->Visible = false;

ViewStatusBar->Checked = false;

Height = Height - StatusBar1->Height;

}

else

{

Height = Height + StatusBar1->Height;

StatusBar1->Visible = true;

ViewStatusBar->Checked = true;

}

}

Uwidocznienie każdego z trzech paneli z przyciskami odbywa się w identyczny sposób; wydruk 3.38 ilustruje tę czynność dla środkowego panelu z przyciskami numerycznymi.

Wydruk 3.38. Uwidocznienie panelu z przyciskami

void __fastcall TMainForm::ViewNumberButtonsClick(TObject *Sender)

{

if(ViewNumberButtons->Checked)

{

NumberButtonsPanel->Visible = false;

ViewNumberButtons->Checked = false;

if(AutoFit) FitToControlBar(ButtonsControlBar);

}

else

{

NumberButtonsPanel->Visible = true;

ViewNumberButtons->Checked = true;

if(StatusBar1->Visible) StatusBar1->Align = alBottom;

}

}

Ukrycie panelu polega na podstawieniu wartości false pod jego właściwość Visible oraz pod właściwość odpowiedzialną za zaznaczenie opcji menu. Jeżeli ponadto zaznaczona jest opcja Auto Fit to Buttons w okienku opcji Tools|Settings, następuje dostosowanie rozmiarów paska kontrolnego do rozmiarów widocznych jeszcze paneli.

Uwidocznienie panelu rozpoczyna się od nadania wartości true wymienionym przed chwilą opcjom związanym z jego widocznością. Jeżeli ponadto widoczny jest pasek statusu, zostaje on wyrównany u dołu formularza. Nie jest potrzebne jawne zwiększanie wysokości paska kontrolnego, ponieważ dostosuje on automatycznie swoje rozmiary do nowo uwidocznionego panelu.

Dostosowanie tła formularza głównego MDI

Opatrzenie tła formularza MDI np. jakimś gustownym obrazkiem, mimo swojej oczywistości, nie jest bynajmniej oczywiste w wykonaniu. Wymaga bowiem posłużenia się tzw. subclassingiem, czyli przedefiniowaniem klasy okna głównego aplikacji po to, by można było przechwycić komunikat WM_ERASEBKGND wysyłany do okna w celu jego wyczyszczenia i zamiast faktycznego „czyszczenia” wyświetlać właśnie żądany obrazek.

Proces wyświetlania obrazka w tle formularza głównego - i związane z tym czynności „administracyjne” - prześledzić można na przykładzie projektu MDIProject.bpr, znajdującego się na załączonej płycie CD-ROM. Załadowany obrazek może być wycentrowany, powielony w sposób sąsiadujący (ang. tile) lub zmniejszony do rozmiarów formularza. Jest on najpierw rysowany na pomocniczej bitmapie (niewidocznej na ekranie), a następnie kopiowany na obszar klienta formularza głównego za pomocą funkcji WinAPI BitBlt() lub StretchBlt(); eliminuje to migotanie, które pojawiłoby się przy rysowaniu bezpośrednim.

Więcej informacji na temat subclassingu i bezpośredniej obsługi komunikatów znajdziesz w systemie pomocy Win32 SDK pod hasłem „Frame, Client, and Child Windows”.

Indywidualizacja ustawień

Najprostszym sposobem na przechowanie indywidualnych ustawień użytkownika jest przechowanie ich w Rejestrze Windows. C++Builder udostępnia w tym celu dwie klasy VCL: TRegistry i TRegIniFile. Pierwsza z nich opisana jest w dokumentacji C++Buildera i nie będziemy się tutaj zagłębiać w jej szczegóły, druga natomiast (notabene wywodząca się z pierwszej) zapewnia większą elastyczność obsługi i wyższy poziom abstrakcji. Aby używać którejś z wymienionych klas, należy dołączyć plik nagłówkowy <Registry.hpp>.

Najważniejsze z właściwości i metod klasy TRegIniFile opisane są w tabeli 3.9.

Tabela 3.9. Właściwości i metody klasy TRegIniFile

Właściwość (Metoda)

Opis

FileName

Właściwość tylko do odczytu; zawiera nazwę aktualnego klucza Rejestru (otwartego lub utworzonego) , który będzie kluczem najwyższego poziomu (root) w przyszłych operacjach wykonywanych przez obiekt.

RootKey

Zawiera uchwyt do klucza najwyższego poziomu; standardowo kluczem tym jest HKEY_CURRENT_USER.

ReadBool()

Udostępnia wartość boolowską ze wskazanej lokalizacji w Rejestrze; jeżeli specyfikowana lokalizacja nie istnieje, zwracana zostaje specyfikowana wartość domyślna.

ReadInteger()

Udostępnia wartość całkowitoliczbową ze wskazanej lokalizacji w Rejestrze; jeżeli specyfikowana lokalizacja nie istnieje, zwracana zostaje specyfikowana wartość domyślna.

ReadString()

Udostępnia łańcuch typu AnsiString ze wskazanej lokalizacji w Rejestrze; jeżeli specyfikowana lokalizacja nie istnieje, zwracana zostaje specyfikowana wartość domyślna.

WriteBool()

Zapisuje we wskazanej lokalizacji Rejestru wartość boolowską; jeżeli żądana lokalizacja nie istnieje, zostanie utworzona.

WriteInteger()

Zapisuje we wskazanej lokalizacji Rejestru wartość całkowitoliczbową; jeżeli żądana lokalizacja nie istnieje, zostanie utworzona.

WriteString()

Zapisuje we wskazanej lokalizacji Rejestru łańcuch typu AnsiString; jeżeli żądana lokalizacja nie istnieje, zostanie utworzona.

Ważną właściwością klasy TRegIniFile jest to, iż w przypadku nieistnienia w Rejestrze wskazanej lokalizacji nie jest generowany błąd: przy operacji odczytu zwracana jest wartość domyślna (podana jako ostatni parametr wywołania funkcji: ReadBool(), ReadInteger() lub ReadString()), zaś przy zapisie brakująca lokalizacja zostaje automatycznie utworzona. Ułatwia to znacznie operowanie Rejestrem.

W naszej aplikacji kalkulatorze dane zapisywane są w Rejestrze w dwóch przypadkach: gdy użytkownik skorzysta z opcji Tools|Save Configuration oraz przy zamykaniu aplikacji, o ile w oknie opcji Tools|Settings zaznaczona jest opcja Auto Save Configuration. Pierwszy z przypadków realizowany jest przez odpowiednią funkcję zdarzeniową opcji menu, przedstawioną na wydruku 3.39.

Wydruk 3.39. Zapis danych do Rejestru na żądanie użytkownika

void __fastcall TMainForm::SaveCurrentLayout1Click(TObject *Sender)

{

std::auto_ptr<TRegIniFile> Registry(

new RegIniFile("SOFTWARE\\MiniCalculator"));

// Zapis do Rejestru opcji okienka Tools|Settings

Registry->WriteBool("Options","AutoSaveLayout",AutoSaveLayout);

Registry->WriteBool("Options","AutoFit",AutoFit);

// zapis pozostałych ustawień

WriteSettingsToRegistry(Registry);

}

Obiekt typu TRegIniFile tworzony jest z udziałem „automatycznego wskaźnika” auto_ptr<>, co zapewnia jego automatyczne zwolnienie po zakończeniu funkcji. Ponieważ domyślnym kluczem „źródłowym” (root) jest HKEY_CURRENT_USER, więc wszelkie zapisy dokonywane będą w lokalizacji:

HKEY_CURRENT_USER\Software\MiniCalculator

Pierwszy parametr funkcji WriteBool() wskazuje podklucz w wyspecyfikowanej lokalizacji, tak więc np. instrukcja:

Registry->WriteBool("Options","AutoFit",AutoFit);

powoduje nadanie wartości danej o nazwie Autofit w lokalizacji:

HKEY_CURRENT_USER\Software\MiniCalculator\Options

(patrz rys. 3.9). Gdy wartość Autofit->Checked równa będzie true, do Rejestru wpisane zostanie 1, w przeciwnym razie wpisane zostanie 0.

0x01 graphic

Rysunek 3.9. Zapis opcji ustawień kalkulatora w Rejestrze Windows

Zapis pozostałych ustawień znajduje się w podkluczu Settings i dokonywany jest przez funkcję WriteSettingsToRegistry():

Wydruk 3.40. Zapis ustawień kalkulatora do Rejestru Windows

void __fastcall TMainForm::WriteSettingsToRegistry(

const std::auto_ptr<TRegIniFile>& Registry)

{

// panel LCD

// - Kolor

Registry->WriteInteger("Settings\\Display\\Color",

"SurroundColor",

LCDPanel->Color);

Registry->WriteInteger("Settings\\Display\\Color",

"BackgroundColor",

BackgroundPanel->Color);

Registry->WriteInteger("Settings\\Display\\Color",

"ExponentColor",

ExponentEditColor);

// - Dokowanie

Registry->WriteBool("Settings\\Display","Floating",LCDPanel->Floating);

Registry->WriteInteger("Settings\\Display","UndockWidth",

LCDPanel->UndockWidth);

Registry->WriteInteger("Settings\\Display","UndockHeight",

LCDPanel->UndockHeight);

if(LCDPanel->Floating)

{

TRect UndockedRect;

if(GetWindowRect(LCDPanel->HostDockSite->Handle, &UndockedRect))

{

Registry->WriteInteger(

"Settings\\Display","UndockLeft",UndockedRect.Left);

Registry->WriteInteger(

"Settings\\Display","UndockTop",UndockedRect.Top);

Registry->WriteInteger(

"Settings\\Display","UndockRight",UndockedRect.Right);

Registry->WriteInteger(

"Settings\\Display","UndockBottom",UndockedRect.Bottom);

}

}

// Formularz główny

Registry->WriteInteger("Settings\\Position","MainFormTop",Top);

Registry->WriteInteger("Settings\\Position","MainFormLeft",Left);

Registry->WriteInteger("Settings\\Size","MainFormHeight",Height);

Registry->WriteInteger("Settings\\Size","MainFormWidth",Width);

// Pasek statusu

Registry->WriteBool("Settings\\StatusBar","Visible",StatusBar1->Visible);

Registry->WriteBool("Settings","EnableKeyboard",EnableKeyboardInput);

// Pasek kontrolny

for(int i=0; i<ButtonsControlBar->ControlCount; ++i)

{

AnsiString ControlPath = "Settings\\ControlBar\\";

ControlPath += ButtonsControlBar->Controls[i]->Name;

Registry->WriteInteger(

ControlPath,"Left",ButtonsControlBar->Controls[i]->Left);

Registry->WriteInteger(

ControlPath,"Top",ButtonsControlBar->Controls[i]->Top);

Registry->WriteInteger(

ControlPath,"Height",

ButtonsControlBar->Controls[i]->Height+2);

Registry->WriteBool(

ControlPath,"Visible",

ButtonsControlBar->Controls[i]->Visible);

}

}

Jak widać, sposób zapisu danych do Rejestru nie jest szczególnie skomplikowany; o wiele trudniejszą decyzją jest natomiast wybór informacji, która podlegać będzie zapisowi do Rejestru. Jeżeli chodzi o panel wyświetlacza, to pożądane byłoby zachowanie jego pozycji podczas wydokowania i odtworzenie jej po następnym uruchomieniu programu. Klasa okienka zarządzającego wydokowaną kontrolką zapamiętana jest pod jej właściwością FloatingDockSiteClass; standardowo klasa ta uzupełnia kontrolkę o typowe obrzeże i standardowy pasek tytułowy. Aby wyświetlić kontrolkę dokładnie w tej samej pozycji, w której znajdowała się w momencie zapisu ustawień do Rejestru, musimy odczytać je za pomocą funkcji Win32 API GetWindowRect(), do tego potrzebny nam jednak będzie uchwyt okna zarządzającego wydokowaną kontrolką. W C++Builderze okno to reprezentowane jest przez właściwość HostDockSite wydokowanej kontrolki, zaś żądany uchwyt znajduje się pod właściwością Handle tegoż okna. Drugim parametrem wywołania funkcji GetWindowRect() jest prostokąt, pod który funkcja ta podstawia odczytane współrzędne; zostają one następnie wpisane do Rejestru jako cztery niezależne wartości:

if(LCDPanel->Floating)

{

TRect UndockedRect;

if(GetWindowRect(LCDPanel->HostDockSite->Handle, &UndockedRect))

{

Registry->WriteInteger(

"Settings\\Display","UndockLeft",UndockedRect.Left);

Registry->WriteInteger(

"Settings\\Display","UndockTop",UndockedRect.Top);

Registry->WriteInteger(

"Settings\\Display","UndockRight",UndockedRect.Right);

Registry->WriteInteger(

"Settings\\Display","UndockBottom",UndockedRect.Bottom);

}

}

Nie mniejszym kłopotem jest także właściwe wykorzystanie danych odczytanych z Rejestru. Gdy podczas startu aplikacji kalkulatora konstruowany jest jej formularz główny, w jego konstruktorze wywoływana jest następująca funkcja:

Wydruk 3.41. Odczyt opcji aplikacji z Rejestru

void __fastcall TMainForm::ReadAllValuesFromRegistry()

{

// Spróbuj odczytać żądane ustawienia z odpowiednich lokalizacji Rejestru

// Przy pierwszym uruchomieniu lokalizacje te nie istnieją, należy więc

// nie zmieniać ustawień domyślnych

std::auto_ptr<TRegIniFile> Registry(

new TRegIniFile("SOFTWARE\\MiniCalculator"));

// Załaduj opcje

AutoSaveLayout

= Registry->ReadBool("Options","AutoSaveLayout",AutoSaveLayout);

AutoFit

= Registry->ReadBool("Options","AutoFit",AutoFit);

// Załaduj ustawienia

ReadSettingsFromRegistry(Registry);

}

Zwróć uwagę, iż w charakterze wartości domyślnej występuje bieżąca wartość zmiennej, zatem brak żądanej lokalizacji w Rejestrze spowoduje, iż wartość uprzednio przypisana tej zmiennej nie zostanie zmieniona.

Załadowanie ustawień kalkulatora wykonywane jest przez funkcję ReadSettingsFromRegistry(); jej treść przedstawiamy na wydruku 3.42.

Wydruk 3.42. Odczyt ustawień aplikacji z Rejestru

void __fastcall TMainForm::ReadSettingsFromRegistry(const std::auto_ptr<TRegIniFile>& Registry)

{

// Panel LCD

// - Kolor

LCDPanel->Color

= static_cast<TColor>(Registry->ReadInteger("Settings\\Display\\Color",

"SurroundColor",

LCDPanel->Color));

BackgroundPanel->Color

= static_cast<TColor>(Registry->ReadInteger("Settings\\Display\\Color",

"BackgroundColor",

BackgroundPanel->Color));

ExponentViewColor = BackgroundPanel->Color;

ExponentEditColor

= static_cast<TColor>(Registry->ReadInteger("Settings\\Display\\Color",

"ExponentColor",

ExponentEditColor));

// - Dokowanie

LCDPanel->UndockWidth

= Registry->ReadInteger("Settings\\Display","UndockWidth",

LCDPanel->UndockWidth);

if(LCDPanel->UndockWidth > Screen->Width)

{

LCDPanel->UndockWidth = Screen->Width;

}

LCDPanel->UndockHeight

= Registry->ReadInteger("Settings\\Display","UndockHeight",

LCDPanel->UndockHeight);

bool Floating = Registry->ReadBool("Settings\\Display","Floating",

LCDPanel->Floating);

if(Floating)

{

int UndockLeft = Registry->ReadInteger("Settings\\Display",

"UndockLeft",

LCDPanel->Left);

int UndockTop = Registry->ReadInteger("Settings\\Display",

"UndockTop",

LCDPanel->Top);

TRect UndockedRect(UndockLeft,

UndockTop,

UndockLeft + LCDPanel->UndockWidth,

UndockTop + LCDPanel->UndockHeight);

int UndockRight = Registry->ReadInteger("Settings\\Display",

"UndockRight",

UndockedRect.Right);

int UndockBottom = Registry->ReadInteger("Settings\\Display",

"UndockBottom",

UndockedRect.Bottom);

if(UndockRight > Screen->Width)

{

int Offset = UndockRight - Screen->Width;

UndockedRect.Right -= Offset;

UndockedRect.Left -= Offset;

if(UndockedRect.Left < 0) UndockedRect.Left = 0;

}

if(UndockBottom > Screen->Height)

{

int Offset = UndockBottom - Screen->Height;

UndockedRect.Bottom -= Offset;

UndockedRect.Top -= Offset;

if(UndockedRect.Top < 0)

{

Offset = 0 - UndockedRect.Top;

UndockedRect.Top = 0;

UndockedRect.Bottom += Offset;

}

}

LCDPanel->ManualFloat(UndockedRect);

}

// Formularz główny

int top, left, height, width;

top = Registry->ReadInteger("Settings\\Position","MainFormTop",Top);

left = Registry->ReadInteger("Settings\\Position","MainFormLeft",Left);

height = Registry->ReadInteger("Settings\\Size","MainFormHeight",Height);

width = Registry->ReadInteger("Settings\\Size","MainFormWidth",Width);

if(width > Screen->Width) width = Screen->Width; // błąd !

if(left+width > Screen->Width)

{

left -= (left+width) - Screen->Width;

}

if(height > Screen->Height) height = Screen->Height; // błąd !

if(top+height > Screen->Height)

{

top -= (top+height) - Screen->Height;

}

Top = top;

Left = left;

Height = height;

Width = width;

// Pasek statusu

StatusBar1->Visible = Registry->ReadBool("Settings\\StatusBar",

"Visible",

StatusBar1->Visible);

//if(!StatusBar1->Visible) Height -= StatusBar1->Height;

EnableKeyboardInput = Registry->ReadBool("Settings",

"EnableKeyboard",

EnableKeyboardInput);

// Pasek kontrolny

std::list<TControlBandInfo> BandList;

for(int i=0; i<ButtonsControlBar->ControlCount; ++i)

{

AnsiString ControlPath = "Settings\\ControlBar\\";

ControlPath += ButtonsControlBar->Controls[i]->Name;

int ControlLeft = Registry->ReadInteger(ControlPath,

"Left",

ButtonsControlBar->Controls[i]->Left);

int ControlTop = Registry->ReadInteger(ControlPath,

"Top",

ButtonsControlBar->Controls[i]->Top);

int ControlHeight = Registry->ReadInteger(ControlPath,

"Height",

ButtonsControlBar->Controls[i]->Height+2);

bool ControlVisible = Registry->ReadBool(ControlPath,

"Visible",

ButtonsControlBar->Controls[i]->Visible);

BandList.push_back(TControlBandInfo(ButtonsControlBar->Controls[i],

ControlLeft,

ControlTop,

ControlHeight,

ControlVisible));

}

BandList.sort();

std::list<TControlBandInfo>::iterator pos;

for(pos = BandList.begin(); pos != BandList.end(); ++pos)

{

pos->Control->Visible = false;

}

for(pos = BandList.begin(); pos != BandList.end(); ++pos)

{

pos->Control->Top = pos->Top;

pos->Control->Left = pos->Left;

pos->Control->Visible = pos->Visible;

}

// zresetuj wymiary paska kontrolnego

Top = top;

Left = left;

Height = height;

Width = width;

}

Podobnie jak w przypadku zapisu do Rejestru, fizyczny odczyt danych z określonej lokalizacji nie przedstawia żadnego problemu, skomplikowana natomiast staje się właściwa interpretacja tych danych. Świadczy o tym chociażby sam rozmiar wydruku 3.42, którego zawartość, choć w większości wystarczająca czytelna, w dwóch przypadkach wymaga jednak dodatkowych wyjaśnień.

Gdy panel wyświetlacza pojawić ma się w pozycji wydokowanej, należy użyć jego metody ManualFloat(), której parametrem jest prostokąt określający miejsce pojawienia się kontrolki po jej wydokowaniu. Ustawienia zapisane w Rejestrze zawierają co prawda jego współrzędne (UndockLeft, UndockTop, UndockRight, UndockBottom) i wydawałoby się, że nie ma nic prostszego, jak utworzyć z nich rzeczony prostokąt; niestety, pewne cechy specyficzne funkcji ManualFloat() przesądzają o tym, iż „lewa” i „górna” współrzędna przekazanego prostokąta odnoszą się nie do samej kontrolki, lecz obudowującego ją okna, czego nie można powiedzieć o „dolnej” i „prawej” współrzędnej. Uzyskalibyśmy więc zniekształcony prostokąt, co przy tak drobiazgowych wyliczeniach, jakie zastosowaliśmy w przypadku naszego wyświetlacza, jest nie do przyjęcia. Zamiast tego użyliśmy więc „lewej” i „górnej' współrzędnej oraz niezależnie zapisanych rozmiarów samego wyświetlacza (UndockWidth i UndockHeight):

TRect UndockedRect(UndockLeft,

UndockTop,

UndockLeft + LCDPanel->UndockWidth,

UndockTop + LCDPanel->UndockHeight);

Tak naprawdę to nie można bezkrytycznie podchodzić do żadnej danej zapisanej w Rejestrze i to nawet nie z powodu ewentualnych przekłamań czy błędów: kontrolka stworzona w środowisku o dużej rozdzielczości (np. 1280×1024) może po prostu nie zmieścić się na ekranie o rozdzielczości dużo mniejszej (np. 640×480); dotyczy to szczególnie formularzy. Dlatego też konieczna jest weryfikacja każdej odczytanej współrzędnej i każdego odczytanego rozmiaru - oto jak nasza aplikacja przeprowadza taką weryfikację w stosunku do kontrolki wyświetlacza:

if(UndockRight > Screen->Width)

{

int Offset = UndockRight - Screen->Width;

UndockedRect.Right -= Offset;

UndockedRect.Left -= Offset;

if(UndockedRect.Left < 0) UndockedRect.Left = 0;

}

if(UndockBottom > Screen->Height)

{

int Offset = UndockBottom - Screen->Height;

UndockedRect.Bottom -= Offset;

UndockedRect.Top -= Offset;

if(UndockedRect.Top < 0)

{

Offset = 0 - UndockedRect.Top;

UndockedRect.Top = 0;

UndockedRect.Bottom += Offset;

}

}

Jeszcze bardziej złożone jest odtworzenie układu paneli na pasku kontrolnym - rozgrywa się tu mniej więcej ta sama historia, co przy poziomym wyrównywaniu kontrolek. Odczytane kontrolki formowane są w listę, lista ta jest sortowana w opisany wcześniej sposób, następnie poszczególne kontrolki zastępują kontrolki już istniejące na pasku, w kolejności od góry do dołu.

Zróżnicowane konfiguracje graficzne

Konfiguracje komputerów bywają zróżnicowane, fakt ten jednak nie zawsze jest należycie doceniany przez programistów. Dla aplikacji o orientacji graficznej szczególne znaczenie mają różnice w zakresie potocznie rozumianej „grafiki”, a szczególnie jej trzech aspektów: rozdzielczości ekranu, wielkości czcionki i liczby kolorów.

Różnice w rozdzielczości ekranu

Aktualną rozdzielczość ekranu, czyli liczbę pikseli wyświetlanych w poziomie i w pionie, odczytać można z właściwości (odpowiednio) Width i Height globalnego obiektu Screen. Ten sam obiekt (o ustalonych wymiarach wyrażonych w pikselach) będzie miał - w przeliczeniu na „bezwzględne” jednostki miar, np. milimetry - różną wielkość w zależności od bieżącej rozdzielczości ekranu. Zniwelowanie tego efektu - czyli zapewnienie jednakowej wielkości wyświetlanych obiektów, niezależnie od stosowanej rozdzielczości - uzyskać można wykorzystując metodę ScaleBy() klasy TWinControl, dokonującą „przeskalowania” wymiarów kontrolki (i wszystkich jej kontrolek potomnych) o czynniki wynikające z obydwu rozdzielczości - tej, w której aplikację zbudowano i tej, w warunkach której jest ona wykonywana. Nie zawsze jest to jednak wykonalne: przykładowo duży formularz, zaprojektowany w warunkach rozdzielczości (powiedzmy) 1024×768 może nie zmieścić się na ekranie o rozdzielczości 640×480, i vice versa - kontrolki o rozmiarach akceptowalnych przy rozdzielczości „grubej” 640×480 mogą stać się mikroskopijnej wielkości w rozdzielczości o dwa stopnie wyższej.

Więcej informacji na temat metody ScaleBy() i zasad jej stosowania znajduje się w podręczniku projektanta (Developer's Guide) wchodzącym w skład dokumentacji C++Buildera.

Różnice w wielkościach czcionek

Podczas tworzenia aplikacji właściwość PixelsPerInch jej formularza odzwierciedla wielkość czcionki systemowej używanej na etapie projektowania. Jeżeli właściwość Scaled formularza ustawiona jest na true (jest to ustawienie domyślne), formularz i zawarte w nim kontrolki automatycznie zmieniają swoje rozmiary proporcjonalnie do wysokości czcionki na komputerze, na którym aplikacja jest wykonywana. W przypadku aplikacji kalkulatora różnica wysokości czcionki nie powoduje żadnych poważnych konsekwencji, wspomniane skalowanie nie jest więc potrzebne i właściwość Scaled formularza głównego ustawiona jest na false. Pionową rozdzielczość ekranu, czyli liczbę pikseli przypadających na cal jego wysokości, odczytać można z właściwości PixelsPerInch globalnego obiektu Screen.

Różnice w liczbie kolorów

Ten czynnik ma potencjalnie najmniejsze znaczenie dla przenośności aplikacji pomiędzy różnymi „grafikami” komputerów i istotny jest tylko wówczas, gdy wyświetlenie danego obrazu w środowisku o mniejszej liczbie kolorów niż ta, przy której został stworzony, powoduje drastyczne obniżenie jego jakości. Jeżeli dany kolor nie może być uzyskany wprost przy danych ustawieniach karty graficznej, jest on przez system zastępowany kolorem możliwie najbardziej zbliżonym, przez co wiele niuansów artystycznych może ulec zatraceniu, a niektóre obiekty mogą wręcz stopić się ze swoim tłem.

Z problemem tym poradzić sobie można na dwa sposoby. Pierwsze, „uniwersalne” podejście polega na zaprojektowaniu danej grafiki przy ograniczonej liczbie kolorów; znakomita większość kart graficznych ma obecnie możliwość wyświetlania koloru 16-bitowego, co jest wystarczające dla większości aplikacji. Drugie podejście polega na dostosowaniu aplikacji do konkretnych ustawień komputera, na których jest ona wykonywana, w szczególności - zaprojektowaniu niektórych jej elementów osobno dla poszczególnych wartości liczby kolorów (16, 256, 65536 itd.). Liczbę kolorów wyświetlanych w danej konfiguracji komputera odczytać można z tzw. kontekstu graficznego urządzenia, dostępnego pod właściwością Handle płótna (Canvas) dowolnej kontrolki wizualnej, w następujący sposób:

int LiczbaKolorow = 0;

if(GetDeviceCaps(Canvas->Handle, RASTERCAPS) & RC_PALETTE)

{

LiczbaKolorow = GetDeviceCaps(Canvas->Handle, COLORRES);

}

else

{

LiczbaKolorow =

GetDeviceCaps(Canvas->Handle, BITSPIXEL) *

GetDeviceCaps(Canvas->Handle,PLANES)

}

Metodę tę ilustruje przykładowy projekt ScreenInfo.bpr, znajdujący się na załączonej do książki płycie CD-ROM, wyświetlający aktualną rozdzielczość ekranu, liczbę kolorów oraz wielkość czcionki.

Techniki łagodzące złożoność konstrukcji interfejsu

Tworzenie interfejsu profesjonalnej aplikacji jest zazwyczaj, mimo wszelkich ułatwień oferowanych przez narzędzia typu RAD, czynnością dosyć komplikowaną. W naszym przykładowym kalkulatorze - aplikacji wykonującej bądź co bądź elementarne obliczenia - kod wykonujący zasadnicze operacje (dodawanie, odejmowanie itp.) zajmuje około 20 wierszy; kolejne kilkaset wierszy to różnorodne operacje pomocnicze w rodzaju konwersji wyświetlanych liczb pomiędzy reprezentacjami. Znakomita większość z ponad 2500 wierszy kodu poświęcona jest więc właściwemu funkcjonowaniu interfejsu użytkownika! Przy tak złożonych zagadnieniach wszelkie techniki ułatwiające tworzenie oprogramowania stają się niezwykle pożądane; omówimy tutaj dwie z nich, mające bezpośredni związek z naszą aplikacją kalkulatorem.

Scentralizowane sterowanie akcjami obiektu

Centralizację obsługi zdarzeń poszczególnych obiektów lub ich grup umożliwiają komponenty należące do klasy TActionList i powiązane z nimi komponenty klasy TAction. Są one dość dobrze omówione (od strony teoretycznej i praktycznej) w dokumentacji C++Buildera, jednak jeden przykład więcej na pewno nie zaszkodzi.

W naszym kalkulatorze komponent ActionList1 centralizuje zarządzanie operacjami kopiowania wykonywanymi w związku z opcjami menu Copy i opcjami menu kontekstowych związanych z wyświetlaczem i pamięcią. Kopiowanie to polega na umieszczeniu w schowku zawartości (odpowiednio) etykiety LCDScreen („główna” zawartość wyświetlacza), etykiety HistoryLabel (historia obliczeń) lub zawartości pamięci i może być inicjowane na dwa sposoby: za pomocą odpowiedniej opcji menu Copy albo poprzez (jedyną) opcję odpowiedniego menu kontekstowego. Z określonym elementem podlegającym kopiowaniu (np. z etykietą historii obliczeń) związane są więc dwie opcje pochodzące z różnych menu, czyli - dwa różne komponenty; można by oczywiście utworzyć dla nich dwie identyczne funkcje obsługujące zdarzenie OnClick, jednak wspomniany mechanizm centralizacji zdarzeń pozwoli nam osiągnąć ten sam cel za pomocą wspólnej obsługi tegoż zdarzenia przez jedną funkcję.

Z każdym kopiowanym elementem powiążemy mianowicie jedną akcję kopiowania: dla etykiety historii akcję tę nazwiemy CopyHistoryAction, dla „głównej” zawartości wyświetlacza - CopyNumberAction, zaś za kopiowanie pamięci odpowiedzialna będzie akcja o nazwie CopyMemoryAction. Poszczególnym akcjom przypiszemy stosowne skróty klawiszowe (odpowiednio: Ctrl+H, Ctrl+N i Ctrl+M), odpowiednie symbole (ukazujące się obok powiązanych opcji menu) i oczywiście odpowiednie funkcje zdarzeniowe, wykonujące „zasadniczą” pracę związaną z danym zdarzeniem; funkcje te (odpowiadające zdarzeniu OnClick poszczególnych par opcji menu związanych z poszczególnymi elementami) przypisane będą do właściwości OnExecute odpowiednich akcji, jak ilustruje to wydruk 3.43. Przypisanie danej akcji do określonej opcji menu realizowane jest przez właściwość Action tej ostatniej.

Wydruk 3.43. Centralizacja kopiowania poszczególnych elementów interfejsu kalkulatora

// etykieta historii

void __fastcall TMainForm::CopyHistoryActionExecute(TObject *Sender)

{

AnsiString HistoryString = HistoryLabel->Caption;

Clipboard()->AsText = HistoryString;

}

// główna zawartość wyświetlacza

void __fastcall TMainForm::CopyNumberActionExecute(TObject *Sender)

{

AnsiString NumberString = LCDScreen->Caption;

if(ExponentLabel->Caption != "E+0" && ExponentLabel->Caption != "E-0")

{

NumberString += ExponentLabel->Caption;

}

Clipboard()->AsText = NumberString;

}

// zawartość pamięci

void __fastcall TMainForm::CopyMemoryActionExecute(TObject *Sender)

{

Clipboard()->AsText = MemoryString;

}

Przypisania poszczególnych akcji do konkretnego komponentu nadrzędnego klasy TActionList dokonuje się w edytorze tego ostatniego, uruchamianym dwukrotnym kliknięciem jego ikony na formularzu.

Powyższy przykład nie należy do szczególnie skomplikowanych, jednak (mamy nadzieję) wystarczająco wyjaśnia ideę centralizacji zarządzania zdarzeniami. Inny przykład tego rodzaju znaleźć można w cytowanym już kilkakrotnie projekcie MDIProject.bpr. W aplikacji wielodokumentowej często wykonywanymi czynnościami są odpowiednie aranżacje okien formularzy potomnych - ułożenie sąsiadujące, ułożenie kaskadowe, uporządkowanie ikon itd. Z każdą z tych czynności związaliśmy więc stosowną akcję, nie pisząc jednak w związku z tym ani jednego wiersza kodu: po prostu wykorzystane przez nas akcje należą do jednej ze standardowych kategorii (Window), a więc wszystko, co należy zrobić, to przypisanie ich do odpowiedniego komponentu TActionList (za pomocą jego edytora) i powiązanie z odpowiednimi kontrolkami (za pomocą właściwości Action tych ostatnich); „oprogramowywanie” zdarzenia OnExecute poszczególnych akcji nie jest już potrzebne.

Współdzielenie funkcji zdarzeniowych

Jednym z pożytków, płynących z wykorzystania komponentów TAction i TActionList, jest oszczędność kodowania - do danej akcji, uruchamiającej jedną funkcję zdarzeniową może być przywiązana dowolna liczba kontrolek. Podobną oszczędność osiągnąć można w sytuacji, gdy grupa podobnych kontrolek (np. 10 kontrolek edycyjnych TEdit) obsługiwana jest (w zakresie jednego lub więcej zdarzeń) w taki sam lub podobny sposób. Koncepcję tę zaprezentowaliśmy już przy okazji specyficznego rysowania opcji menu View za pomocą współdzielonej funkcji ViewMenuItemsAdvancedDrawItem(), lecz jeszcze wyraźniejsze efekty daje ona w przypadku obsługi zdarzenia OnClick wszystkich przycisków numerycznych za pomocą pojedynczej funkcji NumberSpeedButtonClick().

Rzecz jasna w przypadku współdzielenia pojedynczej funkcji zdarzeniowej przez kilka kontrolek konieczne jest zazwyczaj rozróżnianie, na rzecz której kontrolki funkcja ta została aktywowana. Wskaźnik do odnośnej kontrolki zawarty jest oczywiście w pierwszym parametrze wywołania funkcji zdarzeniowej, natomiast niezawodne rozróżnianie poszczególnych kontrolek możliwe jest dzięki właściwości Tag nie używanej poza tym do żadnego innego celu. W naszym kalkulatorze identyfikatory przypisywane właściwości Tag poszczególnych przycisków zgrupowane zostały w następujący typ wyliczeniowy:

enum TCalculatorButton { cb1='1',

cb2='2',

cb3='3',

cb4='4',

cb5='5',

cb6='6',

cb7='7',

cb8='8',

cb9='9',

cbA='A',

cbB='B',

cbC='C',

cbD='D',

cbE='E',

cbF='F',

cb0,

cbSign,

cbPoint,

cbExponent,

cbAdd,

cbSubtract,

cbMultiply,

cbDivide,

cbEquals,

cbBackspace,

cbClear,

cbAllClear,

cbMemoryAdd,

cbMemoryRecall };

Przyciskom oznaczonym pojedynczym numerem (literą) przypisaliśmy dla wygody identyfikatory tożsame z ich oznaczeniem, co umożliwia wykorzystanie właściwości Tag również do odświeżania zawartości łańcucha reprezentującego wyświetlaną wartość:

Wydruk 3.44. Wykorzystanie właściwości Tag przycisku numerycznego w jego funkcji zdarzeniowej

void __fastcall TMainForm::NumberSpeedButtonClick(TObject *Sender)

{

TSpeedButton* SpeedButton = dynamic_cast<TSpeedButton*>(Sender);

if(SpeedButton)

{

ButtonPressNumber(static_cast<TCalculatorButton>(SpeedButton->Tag));

ButtonUp(static_cast<TCalculatorButton>(SpeedButton->Tag));

}

}

.....

void __fastcall TMainForm::ButtonPressNumber(TCalculatorButton Button)

{

char ButtonNumber = static_cast<char>(Button);

if(!EditExponent)

{

if(LCDScreen->Caption != "0" && Operation != coComplete)

{

UpdateLCDScreen(LCDScreen->Caption + ButtonNumber);

.....

Funkcja NumberSpeedButtonClick() rozpoczyna swą pracę od dynamicznego rzutowania wskaźnika kontrolki generującej zdarzenie na typ wskaźnika przycisku; jeżeli z jakichś względów zdarzenie to generowane będzie w kontekście kontrolki innej niż przycisk, wynikiem tego rzutowania będzie NIL i funkcja zakończy swą pracę. W przeciwnym razie wywołana zostanie funkcja obsługująca naciśnięcie przycisku, a jedyny parametr wywołania równy jest właściwości Tag odnośnej kontrolki - statyczne rzutowanie jest tu konieczne z tego powodu, iż Tag jest właściwością typu int, zaś funkcje ButtonPressNumber() i ButtonUp() wymagają parametru typu TCalculatorButton.

Innym, niemal oczywistym, sposobem rozróżniania kontrolek generujących zdarzenie obsługiwane przez współdzieloną funkcję jest identyfikacja odnośnej kontrolki za pomocą ciągu porównań w rodzaju:

if(Sender == SpeedButton1)

{

.....

}

else if(Sender == SpeedButton2)

{

.....

}

else if(Sender == SpeedButton3)

{

.....

}

else if(Sender == SpeedButton4)

{

.....

}

i tak dalej. Wykorzystanie właściwości Tag ma jednak tę przewagę, iż umożliwia przekazanie dodatkowej informacji - jaką w naszym przypadku był znak identyfikujący przycisk numeryczny.

Przypisanie identyfikatorów poszczególnym przyciskom oraz skojarzenie z ich zdarzeniem OnClick funkcji zdarzeniowej odbywa się tu w ramach konstruktora formularza głównego. Ponieważ jednak przypisanie to ma charakter permanentny, powinno być dokonane na etapie budowania aplikacji przy użyciu inspektora obiektów (lub za pomocą edycji tekstowej reprezentacji formularza). Wymaga to co prawda nieco więcej fatygi niż w przypadku bezpośredniego wpisu w treść konstruktora (dzięki funkcjom przenoszenia i zamiany tekstu), jednak generalnie czyni aplikację bardziej logiczną (odpowiednie zależności widoczne są już na etapie projektowania, w oknie inspektora obiektów), zaś tekst konstruktora - bardziej przejrzystym. Konieczne jest w tym celu przeniesienie deklaracji funkcji NumberSpeedButtonClick() do publikowanej (__published) części definicji klasy formularza głównego (w pliku Unit1.h), by była ona widoczna dla projektanta formularzy.

Podsumowanie

W rozdziale tym dokonaliśmy szczegółowej analizy przykładowej aplikacji pod kątem jej interfejsu użytkownika, zwracając uwagę na te jego elementy i koncepcje, które czynią go intuicyjnie jasnym i zgodnym z oczekiwaniami potencjalnego użytkownika. Pokazaliśmy również ogromne możliwości C++Buildera, pomagające w osiągnięciu tego celu.

Rozpoczęliśmy od omówienia roli, jaką w interfejsie profesjonalnej aplikacji spełniać mogą standardowe kontrolki Windows - paski statusu, przyciski, etykiety, panele i paski kontrolne. Zaprezentowaliśmy również kilka zaawansowanych koncepcji, jak implementacja niestandardowych podpowiedzi, kontrolowane przenoszenie skupienia pomiędzy elementami interfejsu, dokowanie, kotwiczenie i wyrównywanie kontrolek itp. Pokazaliśmy także, jak w nieskomplikowany sposób wykorzystać można Rejestr systemowy do przechowywania w nim indywidualnych ustawień użytkownika związanych z aplikacją, przedstawiliśmy też wybrane zagadnienia związane ze zróżnicowaną konfiguracją komputerów i sposoby radzenia sobie z konsekwencjami tych różnic. Mimo szerokiego zakresu poruszanych w tym rozdziale zagadnień, zdajemy sobie jednak sprawę, iż poruszyliśmy jedynie przysłowiowy wierzchołek góry lodowej: wielu istotnych aspektów konstrukcji interfejsu użytkownika, jak chociażby przystosowanie aplikacji do użycia jej w różnych wersjach językowych, nie sposób poruszyć nawet w bardzo skrótowej formie w ramach ograniczonej objętości rozdziału.

Tworzenie funkcjonalnego interfejsu użytkownika stanowi dla projektantów aplikacji prawdziwe wyzwanie, za to oczekiwany wynik końcowy z pewnością stanowi satysfakcjonującą nagrodę za poniesione nakłady i wysiłki. Prosty kalkulator, będący przedmiotem rozważań rozdziału, może dla zainteresowanego czytelnika stać się punktem wyjścia do stworzenia aplikacji bardziej zaawansowanej.

Koncepcje i rozwiązania przedstawione w tym rozdziale powinny stanowić solidną podstawę dla projektantów aplikacji, a szczególnie tych jej elementów, które odpowiedzialne są za interakcję z użytkownikiem. Daje to wówczas dużą szansę na to, iż praca z aplikacją stanowić będzie niekwestionowaną przyjemność, nie zaś dostarczać powody do narzekań.

2 Część I Podstawy obsługi systemu WhizBang (Nagłówek strony)

2 D:\helion\C++Builder 5\R03-03.DOC

0x01 graphic

zwolniony niedostępny kliknięty naciśnięty



Wyszukiwarka

Podobne podstrony:
R14-03, ## Documents ##, C++Builder 5
R17-03, ## Documents ##, C++Builder 5
R18-03, ## Documents ##, C++Builder 5
R04-03, ## Documents ##, C++Builder 5
R13-03, ## Documents ##, C++Builder 5
R08-03, ## Documents ##, C++Builder 5
R09-03, ## Documents ##, C++Builder 5
R05-03, ## Documents ##, C++Builder 5
R07-03, ## Documents ##, C++Builder 5
R15-03, ## Documents ##, C++Builder 5
R16-03, ## Documents ##, C++Builder 5
R02-03, ## Documents ##, C++Builder 5
R11-03, ## Documents ##, C++Builder 5
r03 p 03 M6RLBDO3JNMO75ABJRRLVTFTCO2M4TUWEBHNX5I
r03-01, ## Documents ##, XML Vademecum profesjonalisty
r03-06, ## Documents ##, Windows 2000 Server. Vad. prof
r-13-00, ## Documents ##, C++Builder 5
r-12-00, ## Documents ##, C++Builder 5
2011 03 03 Document 001

więcej podobnych podstron