BETA
Aby się zalogować, najpiew wybierz portal.
Aby się zarejestrować, najpiew wybierz portal.
Podaj słowa kluczowe
Słowa kluczowe muszą mieć co najmniej 3 sąsiadujące znaki alfanumeryczne
Pole zawiera niedozwolone znaki

Baza wiedzy











MenuImages - obrazki w menu

14-11-2005 23:21 | abadura
MenuImages to komponent umożliwiający łatwe (z poziomu designera) dodawanie do opcji menu, tak głownego jak i kontekstowego, obrazków.

MenuImages - obrazki w menu

1 Wstęp

W wielu obecnych aplikacjach występuje konieczność użycia ikon w menu, tak głównym jak i kontekstowym. Typowym zastosowaniem jest umieszczanie obok opcji menu, która ma odpowiednik “przyciskowy” na pasku narzędziowym aplikacji, ikony z paska właśnie. Popularność takich rozwiązań nietrudno spostrzec, wystarczy przyjrzeć się menu większości programów, jak na przykład MS Office, OpenOffice, Adobe Reader, czy nawet sam Visual Studio.

Standardowo Windows nie udostępnia jednak takiej opcji inaczej niż poprzez menu typu owner draw. Dziwi w prawdzie, czemu MS nie wprowadził standardowego komponentu, czy opcji na platformie .NET, szczególnie, że rozwiązania takie udostępniał na przykład Borland na przykład w C++ Builder 6.0, a więc żadna to nowość. Brak zaś standardowego komponentu rodzi, jak dalej pokażę, wiele trudności.

W internecie można znaleźć (niektóre odnośniki podaję na końcu publikacji) wiele różnych rozwiązań. Ważne jednak dla mnie były (w kolejności) jak najbardziej standardowy wygląd menu oraz możliwość korzystania ze standardowego designera środowiska.

Niestety nie znalazłem w internecie takiego rozwiązania. Większość z nich czyniło bardzo niewiele, by menu wyglądało na standardowe, szczególnie pod systemami innymi niż Windows XP. Nie ma w tym nic dziwnego, bowiem pracując nad kodem sam odniosłem wrażenie, że MS chyba starał się utrudnić to zadanie, tym niemniej udało mi się dojść wyraźnie dalej. Zaś niektóre inne rozwiązania wymuszają na przykład użycie własnej podklasy dla MenuItem i jej ręczne inicjowanie, lub nawet użycie własnej klasy dla całego menu. To utrudnia lub uniemożliwia pracę z designerem, co też dyskwalifikuje.

2 Rozwiązanie

2.1 Dołączanie do designera

By móc użyć klasy z poziomu designera, należy dodać ją jako komponent użytkownika. Robi się to dość prosto. Trzeba z okienka Toolbox (jeśli nie jest widoczne, to można je włączyć z menu View) wybrać jedną z grup narzędziowych (na przykład Components) i prawym przyciskiem myszy rozwinąć menu kontekstowe na tej grupie, po czym wybrać opcję Add/Remove Items....

Otworzy to okienko dialogowe, w którym należy wybrać Browse... z lewej strony okna tuż pod listą komponentów. To z kolei otworzy okienko poszukiwania pliku. Należy z tego okienka wybrać plik DLL z naszym komponentem. Automatycznie doda to komponent, lista komponentów przesunie się tak, by był on widoczny i będzie on zaznaczony.

Teraz można zamknąć okienko dodawania. Komponent jest już dodany (w wybranej zakładce) i można go używać jak wszystkich innych.

2.2 Klasa

W swoim rozwiązaniu zastosowałem następującą klasę:

[Kod C#]

[ProvideProperty("ImageIndex", typeof(MenuItem))]
public class MenuImages : Component, IExtenderProvider
{
 /* ... */

 public ImageList ImageList
 {
  /* ... */
 }

 /* ... */
}

W ten sposób MenuImages jest komponentem, więc można go używać tak jak i innych (na przykład ImageList, MainMenu, czy ContextMenu).

Ma ona jedną własną właściwość, mianowicie ImageList. Jest to listą obrazków, z której będzie można wybierać ikony dla opcji menu – działa to analogicznie do paska zadań (ToolBar), dla którego też wybiera się ImageList, z której następnie przydziela się obrazki dla przycisków. W typowym zastosowaniu będzie to ta sama lista. Należy pamiętać, że nie wybranie żadnej listy może doprowadzić do wyjątków w czasie wykonania.

Ponadto obecność tego komponentu, dzięki fragmentowi [ProvideProperty("MenuImages", typeof(MenuItem))] i implementowaniu IExtenderProvider powoduje dodanie opcji “ImageIndex on X” do opcji menu, które spełnią dalej opisane warunki. X zaś jest nazwą jaką nadaliśmy komponentowi MenuImages, gdy go dodaliśmy do formy w designerze.

2.3 IExtenderProvider i ProvideProperty

Implementowanie interfejsu IExtenderProvider wymusza na klasie posiadanie funkcji

[Kod C#]

public bool CanExtend(object component)
{
 /* ... */
}

która dla zadanego (poprzez parametr) komponentu stwierdza, czy nadaje się on do rozszerzania poprzez tę klasę (w naszym wypadku do dodania właściwości ImageIndex). Należy zauważyć, że funkcja ta nie jest wykonywana w czasie działania programu, tylko w czasie działania designera.

Klasa MenuImages zezwala jedynie na rozszerzanie tych komponentów, które są opcjami menu (ale nie menu głównego – opcje na listwie menu głównego nie mogą mieć dołączanych obrazków, tak jak nie mogą mieć na przykład ustawionego znacznika check) i nie są separatorami (oczywiście separator też nie może mieć ustawionego obrazka).

Obecność atrybutu ProvideProperty wymusza dodanie do klasy funkcji

public int GetImageIndex(MenuItem menuItem)

[Kod C#]

{
 /* ... */
}

public void SetImageIndex(MenuItem menuItem, int indexValue)
{
 /* ... */
}

Pierwsza z nich zwraca wartość własności ImageIndex dla zadanej opcji menu (domyślna wartość to -1 oznaczająca brak obrazka), a druga ustawia dla danej opcji menu jej własność ImageIndex. Wybranie indeksu, który jest mniejszy niż -1 lub większy niż maksymalny indeks obrazka z wybranej listy prowadzi do wyjątku w czasie wykonania.

Klasa wewnętrznie tworzy sobie tablicę haszującą, której kluczami są opcje menu (MenuItem), a wartościami są dane o opcji (MenuItemData) z których jedną jest właśnie indeks obrazka.

2.4 Działanie

Po ustawieniu indeksu obrazka (przez kreatora albo ręcznie) opcji menu, jej własność OwnerDraw jest ustawiana i komponent rejestruje swoje funkcje OnMeasureItem i OnDrawItem jako odpowiedzialne za rysowanie opcji.

Fakt iż opcje są typu owner draw ma jednak pewne konsekwencje. Jedną z istotniejszych jest fakt iż nie będą one dobrze wyglądały wraz z opcjami rysowanymi standardowo, głównie ze względu na wyrównanie. Jeśli jakaś opcja menu w danej grupie ma obrazek, to wszystkie opcje są przesuwane w prawo, by zrobić miejsc na obrazek, nawet gdy go nie mają. Dzięki temu wszystkie opcję są wyrównane. Użycie w tym momencie opcji nie rysowanej przez komponent spowoduje, że nie będzie ona wyrównana odpowiednio.

Dodatkowo rysowanie samemu jest dużym utrudnieniem, gdy chce się zachować wygląd standardowego menu, rozszerzonego jedynie przez obrazki. Szczególnie zaś, jeśli uwzględnić fakt, iż Windows XP rysuje menu zupełnie inaczej, niż poprzednie wersje systemu.

2.4.1 OnMeasureItem

Funkcja ta mierzy zadaną opcję menu i zwraca jej wysokość oraz szerokość. Windows następnie wylicza wielkości wszystkich opcji w menu, wyświetla odpowiednie pole po czym kieruje do wszystkich opcji żądania narysowania się na tym polu.

Uwzględnić przy mierzeniu trzeba rozmiar obrazka, rozmiar zaznaczenia (check albo radio), rozmiar tekstu i rozmiar tekstu skrótu. Nie trzeba zaś obliczać rozmiaru dla symbolu rozwijanego podmenu, gdyż Windows automatycznie dodaje ten rozmiar. Więcej nawet, automatycznie rysuje ów symbol, ale o tym dalej.

Wyliczenie niestety nie może być proste, a więc nie można bazować jedynie na właśnie mierzonej opcji, a trzeba badać wszystkie opcje w grupie. Wynika to z różnych powodów.

Pierwszym jest ucinanie zbędnego miejsca. Jeśli żadna opcja w grupie nie ma przypisanego obrazka, to nie powinno się zostawiać miejsca na obrazki, gdyż śmiesznie by wyglądał tak duży margines. Choć należy tu zauważyć, że z analogiczną sytuacją mamy do czynienia w wypadku znaczników check i radio, dla których Windows w standardowym menu zostawia miejsce, nawet jeśli żadna opcja w grupie nie ma któregoś z tych znaczników ustawionego. Wybór ten nie jest więc podyktowany standardem (nie widzę tu bowiem standardu wśród rozwiązań), a jedynie osobistymi względami estetycznymi.

Innym powodem jest na przykład właściwe wyrównanie nazw opcji i skrótów klawiszowych. W standardowym menu nazwy opcji i skróty tworzą dwie kolumny obie wyrównane do lewej. Inaczej jest na przykład w menu Visual Studio, gdzie skróty wyrównane są do prawej i kolumna skrótów zachodzi na kolumnę nazw opcji. Ponieważ jednak chciałem pozostać możliwie wierny wyglądowi standardowemu, więc pozostałem przy standardowym rozwiązaniu. To jednak wymaga obliczenia jaką długość (nie w znakach, a w jednostkach ekranowych) ma najdłuższa z nazw opcji w grupie, by móc właściwie wybrać rozmiar. Nie wystarczy bowiem zsumować rozmiaru opcji i jej skrótu. Mogłoby się bowiem okazać, że w jakiejś grupie mamy dwie opcje, jedna z bardzo długa nazwą i krótkim skrótem, a druga krótką nazwą i długim skrótem (na przykład Ctrl+Shift+F12). Wtedy wyrównanie wszystkich skrótów do jednej linii spowoduje, że długi skrót się nie zmieści.

Przy okazji też mierzone są długości (nie w znakach, a w jednostkach ekranowych) wszystkich skrótów, dzięki czemu łatwo dodać coś na końcu opcji (na przykład jakieś dodatkowe informacje), czego jednak kod nie robi, ale ułatwia to ewentualne rozszerzanie.

Informacje te są zapisywane w tablicy haszującej klasy pod indeksem opcji menu dla której zostały obliczone. Może się to jednak okazać długie obliczenie, więc żeby nie robić go wielokrotnie, skoro raz wystarczy, obliczone dane zapisywane są dla każdej opcji menu w grupie zaraz po obliczeniu ich dla pierwszej opcji dla której wywołano OnMeasureItem i dalsze wywołania w tej grupie korzystają z zapisanych danych.

Z obserwacji wnoszę jednak, że metoda ta nie jest wierną kopią systemowej, gdyż menu wymierzone standardową opcją często jest nieco krótsze. Wystarczy porównać pierwsze menu z programu demonstracyjnego z menu standardowym i programu demonstracyjnego z menu rysowanym przez komponent. Te drugie jest wyraźnie dłuższe. Po części można to tłumaczyć tym, że Windows przy mierzeniu nie uwzględnia opcji domyślnych i ich pogrubionej czcionki. Można to łatwo zauważyć w programie z menu standardowym, gdzie końcowe opcje menu które mają najdłuższe teksty i są pogrubione zachodzą napisami skrótów na kolumnę, w której umieszczony jest już znacznik podmenu. Inną prawdopodobną przyczyną jest dodawanie przez Windows miejsca dla owego znacznika w wypadku opcji typu owner draw. Dodaje on zapewne z zapasem, w skutek czego powstają takie puste miejsca. Wreszcie przyczyna może też leżeć w funkcjach mierzących napisy, które być może zwracają wielkości większe niż prawdziwe. Zaś trudność ustalenia przyczyny dodatkowo jeszcze utrudnia jakieś przeciwdziałania.

Funkcje mierzące napisy (System.Drawing.Graphics.MeasureString) nie mierzą ich bowiem dokładnie. Z budowy klasy wynika bowiem, iż gdyby funkcja mierząca zwracała rozmiar z dokładnością do piksela, to skrót dla najdłuższej opcji menu zaczynałby się zaraz za tą opcją. A jest między nimi odstęp. Ten odstęp to właśnie ten nadmiar, który zwraca funkcja mierząca. Dla menu standardowego jest on taki sam, ale być może nie ma go już przy mierzeniu skrótu, co może tłumaczyć fakt iż standardowe menu jest nieco krótsze.

Jak widać trudno jest, jeśli w ogóle jest to możliwe, skutecznie to rozwiązać. Efekt zaś nie jest tak duży, a dodatkowo widoczny tylko przy na prawdę sporych menu, zaś w praktyce nikt nie używa tak długich opcji, więc jest to nieco teoretyczny problem.

2.4.2 OnDrawItem

Funkcja rysuje kolejne elementy menu: obrazek (jeśli takowy jest), znaczek check lub radio (jeśli taki jest ustawiony), tekst opcji i tekst skrótu opcji. Nie jest zaś rysowany znacznik podmenu, gdyż o dziwo Windows sam go dorysowuje.

W funkcji tej najwięcej jest problemów z trzymaniem się standardu.

Trudno na przykład samodzielnie rysować separatory, szczególnie, że inaczej rysuje je Windows XP, a inaczej pozostałe Windowsy (przytaczane odnośniki trzymają się konwencji XP). Problem ten rozwiązałem przez zastosowanie standardowego rysowanie odnośnie separatorów. Separatory nie są bowiem typu owner draw, tylko zwyczajne, a więc rysuje je Windows, a co za tym idzie rysuje je standardowo. Było to możliwe tylko dlatego, że separator nie wymaga żadnego formatowania i wyrównania. Jest po prostu odpowiednią kreską przez całą szerokość menu. Należy jednak zauważyć, że wprawdzie nie ma tu standardu, ale większość aplikacji które widziałem nie rysuje separatora pod obrazkiem, przerywając go wcześniej. Takie rozwiązanie nie jest zaś możliwe w przypadku tego komponentu, bowiem tu separator będzie miał zawsze szerokość całego menu.

Innym problemem jest zaznaczanie opcji niedostępnych. Platforma .NET oferuje wprawdzie funkcje (w ramach klasy System.Windows.Forms.ControlPaint) do rysowania niedostępnych obrazków i napisów, jednak sam Windows inaczej rysuje niedostępne opcje menu. Wyniki na pierwszy rzut oka wydają się takie same, ale po bliższym przyjrzeniu się (na przykład poprzez print screen i odpowiednie powiększenie w Paintcie) zauważyć można, że są one rysowane inaczej. Dodatkowo problemem jest wybór koloru. Niedostępny string rysuję w kolorze standardowym (klasa System.Drawing.SystemColors) Control, a nie jak podpowiadałaby intuicja GrayText, gdyż ten drugi daje jeszcze gorsze wyniki.

Kolejnym problemem jest czcionka dla domyślnych opcji menu. Ja domyślne opcje menu rysuję czcionką zrobioną na podstawie standardowej czcionki dla menu (System.Windows.Forms.SystemInformation.MenuFont) poprzez dodanie do niej stylu Bold. Znów optycznie daje to zbliżony rezultat, ale uważne przyjrzenie się (być może znów w Paintcie) wyglądowi czcionki w domyślnych opcjach na pasku głównym menu (które są rysowane przez Windowsa) i tym z menu szybko uwidacznia różnice.

Problemem jest też ustalenie tekstu dla skrótu. Zastosowanie funkcji ToString wobec obiektu skrótu nie daje pożądanych wyników (na przykład CtrlShiftF1 zamiast Ctrl+Shift+F1). Użyłem więc konwertera (System.ComponentModel.TypeDescriptor.GetConverter), ale nie mam pewności, czy nie sprawi to problemów z lokalizacją.

Przykrą, choć dopiero po chwili zauważalną, uciążliwością jest fakt, iż począwszy od Windowsa 2000 w opcjach menu nie są wyświetlane podkreślenia dla liter, które są skrótami, chyba, że menu zostanie wyświetlone przez naciśnięcie klawisza Alt. Nie da się jednak pobrać informacji o sposobie wywołania menu (lub byłoby to niewygodne i czasochłonne) z poziomu funkcji rysującej i mierzącej, więc trzeba wybrać pomiędzy rysowaniem podkreśleń zawsze, lub nigdy. Oczywiście są one rysowane zawsze, ale jest to niezgodne z konwencją menu standardowego na nowych systemach.

Wreszcie ostatnim problemem jest fakt, iż z niewiadomych powodów Windows oczekuje od nas samodzielnego rysowania wszystkiego, poza znacznikiem podmenu (małą strzałką umieszczoną z prawej strony opcji). Nie byłoby to kłopotem, gdyby nie fakt, iż kompletnie przy tym ignoruje dostępność opcji. Choć standardowe opcje maja ten znacznik szary, gdy nie są dostępne, to opcje typu owner draw mają go zawsze taki sam, wyglądający jak dostępny. Trudno zaś próbować zarysowywać go własnym, gdyż może to kiedyś spowodować niestandardowość wyglądu, w dodatku pojawiają się problemy z wymierzeniem, gdzie ma się on zacząć, co poruszałem przy opisie OnMeasureItem.

Są to niedociągnięcia wobec standardowego wyglądu, z którymi nie potrafiłem sobie w żaden sposób poradzić, gdyż wsparcie funkcjami standardowymi (a tylko one mogą to zrobić dobrze i przenośnie) jest za małe i nie wystarcza.

Nie znaczy to jednak, że menu nie nadaje się do użytku. Nadaj się, tyle, że w tych miejscach odchodzi od standardowego wyglądu. Ale inne tego typu rozwiązania także odchodzą od standardowego wyglądu, a więc nie jest to aż taka wadą, wobec braku alternatyw.

2.4.3 Funkcje pomocnicze

Zasadniczo całość przetwarzania umieszczona jest w funkcjach OnMeasureItem i OnDrawItem. Klasa definiuje jednak pare pomocniczych funkcji, które dzielą się na trzy grupy.

Do pierwszej należą funkcje rysujące. Rysują one zadany obiekt (obrazek lub napis) w zadanym stanie (dostępny lub nie).

Do drugiej należą funkcje zwracające standardowe obiekty graficzne zależne od stanu obiektu (wybrany lub nie, domyślny lub nie).

Ostatnią grupę tworzą funkcje które zwracają wartość logiczną zależną od stanów opcji, takich jak wybranie, dostępność, czy obecność skrótu.

3 Wsparcie funkcji standardowych

By menu miało wygląd choć przypominający wygląd standardowy i to na platformie tak Windowsa XP jak i pozostałych Windowsów, trzeba sięgnąć po wsparcie standardowych funkcji.

Pragnę jednak zaznaczyć tu, że poziom wsparcia w funkcjach jest tak mały, że można przypuszczać, iż MS nie chciał, by robiono własne menu. Często trudno znaleźć właściwą funkcję, lub nie robi ona dokładnie tego co należy.

3.1 Rysowanie standardowe

Opcje menu na listwie menu głównego oraz separatory są rysowane przez Windowsa (nie są OwnerDraw), dzięki czemu mają wygląd takie jak standardowe menu.

3.2 Standardowe obiekty graficzne

Poprzez klasy System.Drawing.SystemColors, System.Drawing.SystemBrushes i System.Windows.Forms.SystemInformation pobierane są standardowe obiekty graficzne dotyczące menu. Są to kolory: Highlight, Menu, HighlightText i MenuText, pędzle: Highlight, Menu i HighlightText oraz czcionka MenuFont.

3.3 Standardowe funkcje rysujące

Klasa System.Windows.Forms.ControlPaint dostarcza szeregu funkcji do rysowania standardowo wyglądających obiektów. Są to funkcje DrawMenuGlyph, DrawImageDisabled i DrawStringDisabled. Nie są one jednak tak wygodne, jakby się mogło wydawać. DrawMenuGlyph wymaga rysowania na osobnej powierzchni, by uwzględnić tło menu i ewentualną niedostępność. DrawStringDisabled nie daje zaś wyników nawet podobnych do tego, co widać standardowo w menu. Eksperymentowanie z kolorami pozwala dojść do koloru który daje podobne wyniki, ale prawdopodobnie okaże się to nieprzenośne lub wrażliwe na zmiany ustawień systemowych.

4 Podsumowanie

Udało mi się stworzyć komponent, który jest łatwy w użytkowaniu (da się z niego korzystać z poziomu designera) i jednocześnie daje wyniki najbliższe standardowemu wyglądowi menu spośród rozwiązań jakie widziałem.

Wprawdzie wiele można poprawić, ale trudno to zrobić (lub w ogóle nie da się) bez odpowiednich funkcji systemowych. Pozostaje jedynie mieć nadzieję, że w kolejnych wersjach platformy .NET ten problem zostanie rozwiązany.

Ponadto kod jest zwięzły i nie trudno go zrozumieć już po przejrzeniu (bo w istocie samo zadanie jest łatwe, problemem jest tylko upodabnianie do standardu), a co za tym idzie łatwo wprowadzić jakieś poprawki i zmiany zgodne z własnym upodobaniem. Jest to dodatkowo zaleta, której nie ma większość rozwiązań dostępnych w internecie.

5 Kod źródłowy

Kod zawiera trzy projekty. Pierwszym jest sam komponent rysujący menu, a kolejne dwa to przykładowa aplikacja z rozbudowanym menu standardowym i analogiczna aplikacja z tym samym menu, ale rysowanym przez komponent. Obie aplikacje mają też menu kontekstowe, które również nadaj się do rozszerzania o obrazki.

6 Odnośniki

MenuExtenderComponent - http://www.misterdotnet.com/blog/?p=183

MenuExtender - http://www.codeproject.com/cs/menu/MenuExtender.asp

MenuImageLib - http://www.codeproject.com/cs/menu/menuimage.asp

Załączniki:

Podobne artykuły

Komentarze 0

pkt.

Zaloguj się lub Zarejestruj się aby wykonać tę czynność.