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











Menu w stylu Office 2003

28-02-2005 22:31 | radakon
Artykuł opisuje stworzenie kontrolki głównego menu, która wyglądem przypominałaby menu zawarte w pakiecie Office 2003.

Wstęp

 

Wszyscy powinniśmy zdawać sobie sprawę z tego, że interfejs użytkownika jest jednym z najważniejszych elementów tworzonej aplikacji. To poprzez niego użytkownik postrzega stworzony przez nas system. Dlatego jeśli chcemy, aby został on od razu polubiony, musimy zadbać o to, żeby się dobrze prezentował. W tym artykule opiszę w jaki sposób stworzyć menu, które przypominałoby to pochodzące z pakietu Office 2003.

Standardowe menu dostępne w Windows Forms, pod względem wizualnym, jest bardzo ubogie w porównaniu do tego znajdującego się w pakiecie Office lub chociażby w VS.NET. Poniżej mamy przedstawioną różnicę pomiędzy menu podobnym do Office 2003, a tym standardowym.

 

 

Pomimo tego, że oba prezentują tą samą funkcjonalność, użytkownik patrząc na nie, będzie przekonany, że aplikacja posiadająca to bardziej efektowne menu jest lepsza.

 

Własne rysowanie obiektów MenuItem

 

Zanim zacznę omawiać szczegóły, przedstawię w jaki sposób można samemu kreować  wygląd obiektów MenuItem, które składają się na całe menu.

Otóż obiekt MenuItem posiada właściwość OwnerDraw, która wskazuje nam, czy za rysowanie obiektu ma odpowiadać zaimplementowany przez nas kod, czy Windows. Kod odpowiedzialny za rysowanie znajduje się w obsłudze zdarzeń DrawItem oraz MeasureItem.

Poniżej pokazane jest jak to wygląda:

 

[Kod C#]

//Dodanie obslugi zdarzenia MeasureItem

menuItem.MeasureItem += new MeasureItemEventHandler(menuItem_MeasureItem);

//Dodanie obslugi zdarzenia DrawItem

menuItem.DrawItem += new DrawItemEventHandler(menuItem_DrawItem);

//Ustawienie wlasciwosci OwnerDraw na true

menuItem.OwnerDraw = true;

 

Następnie w funkcji:

 

private void menuItem_MeasureItem( object sender, MeasureItemEventArgs e )

 

znajduje się kod odpowiedzialny za rozmiar obiektu. Ustalane są w nim odpowiednie właściwości argumentu MeasureItemEventArg. Natomiast w funkcji poniżej znajduje się kod odpowiedzialny za rysowanie obiektu

 

private void menuItem_DrawItem( object sender, DrawItemEventArgs e )

 

Kontrolka OfficeMainMenu

 

Aby stworzyć dokładnie takie menu jak w Office 2003, trzeba by było zaimplementować całkowicie nową kontrolkę, ponieważ dostarczona kontrolka System.Windows.Forms.MainMenu narzuca pewne ograniczenia. Przykładowo nie daje nam możliwości zmiany położenia menu. Ja wykorzystam tą istniejącą już kontrolkę, dlatego moje menu będzie podobne tylko z wyglądu do tego znajdującego się w Office 2003.

Zacznijmy od  stworzenia nowego projektu Windows Control Library. Następnie zmieńmy nazwę naszej kontrolki na OfficeMainMenu oraz jej klasę bazową na System.Windows.Forms.MainMenu. Następnie dodajmy do kontrolki funkcję InitMainMenu(), która będzie ustawiała nasz własny wygląd menu, oraz UninitMainMenu(), która będzie ustawiała z powrotem standardowy wygląd, a także właściwość, która będzie odpowiedzialna za ustawienie aktualnego wyglądu (stylu) menu.

 

Rysowanie obiektów MenuItem najwyższego poziomu

 

W głównym menu wyróżniamy dwa typy obiektów MenuItem: te na najwyższym poziomie, które są od razu widoczne bez rozwijania menu oraz te podrzędne, które są widoczne po rozwinięciu nadrzędnych opcji.

Na początek zajmiemy się rysowaniem nadrzędnych obiektów MenuItem. Zaczniemy od dodania obsługi zdarzeń DrawItem oraz MeasureItem. Do funkcji InitMainMenu() dodajmy następujący kod:

 

[Kod C#]

foreach ( MenuItem mainMenuItem in MenuItems )

{

      //Dodanie obslugi zdarzenia MeasureItem

      mainMenuItem.MeasureItem += new MeasureItemEventHandler( mainMenuItem_MeasureItem );

      //Dodanie obslugi zdarzenia DrawItem

      mainMenuItem.DrawItem += new DrawItemEventHandler( mainMenuItem_DrawItem );

      //Ustawienie wlasciwosci OwnerDraw na true

      mainMenuItem.OwnerDraw = true;                                  

}

Następnie stwórzmy klasę pomocniczą MainMenuItemHelper, w której będzie zawarty kod bezpośrednio odpowiedzialny za rysowanie obiektów MenuItem najwyższego poziomu, oraz klasę Globals, w której będą się znajdowały wszystkie wykorzystywane stałe wartości. Teraz do klasy MainMenuItemHelper dodajmy metodę odpowiedzialną za wymierzenie rozmiaru obiektu MenuItem:

[Kod C#]

public void MeasureMenuItem(MeasureItemEventArgs e, MenuItem menuItem)

{                

      SizeF size = e.Graphics.MeasureString(menuItem.Text, Globals.menuFont);

      e.ItemWidth = Convert.ToInt32(size.Width);

}

Poniżej natomiast znajduje się metoda odpowiedzialna za rysowanie obiektu:

 

[Kod C#]

public void DrawMenuItem(DrawItemEventArgs e, MenuItem menuItem)

{                                 

      // Sprawdzenie stanu w jakim znajduje się obiekt menuItem

      if ((e.State & DrawItemState.HotLight) == DrawItemState.HotLight)

      {

            // Rysowanie obiektu menuItem, gdy jest podswietlony

            DrawHotLightRect(e);

      }

      else if ((e.State & DrawItemState.Selected) == DrawItemState.Selected)

      {

            // Rysowanie obiektu menuItem, gdy jest wybrany

            DrawSelectedRect(e);

      }

      else

      {

            // Rysowanie obiektu menuItem, ktory jest niezaznaczony

            DrawDefaultRect(e);                                       

      }

      //Rysowanie tekstu obiektu menuItem

      DrawMenuText(e, menuItem);              

}

Widzimy, że w zależności od tego, w jakim stanie jest obiekt MenuItem, wykorzystujemy określoną funkcję do narysowania obiektu MenuItem. A oto już konkretne funkcje rysujące nasz obiekt:

[Kod C#]

private void DrawHotLightRect(DrawItemEventArgs e)

{

      // Stworzenie rysowanego prostokata

      Rectangle rect = new Rectangle(e.Bounds.X,

            e.Bounds.Y + 1,

            e.Bounds.Width,

            e.Bounds.Height - 2);

 

// Stworzenie pedzla do narysowania prostokata

      Brush rectBrush = new LinearGradientBrush(rect,

            Globals.MenuHotLightLightColor,

            Globals.MenuHotLightDarkColor,

            LinearGradientMode.Vertical);

                       

      // Wypelnienie prostokata

      e.Graphics.FillRectangle(rectBrush, rect);

 

      // Obrysowanie prostokata

      e.Graphics.DrawRectangle(new Pen(Globals.MenuHotLightBorderColor), rect);

}

Jak widzimy, aby samemu rysować obiekt MenuItem, wystarczy pobrać obszar graficzny z argumentu DrawItemEventArgs przekazywanego przez zdarzenie DrawItem. Możemy na nim rysować co tylko chcemy. Metoda DrawSelectedRect jest bardzo podobna, więc nie będę jej już tu zamieszczał. Natomiast metoda DrawDefaultRect różni się głównie tym, że dla każdego obiektu MenuItem wyliczane są kolory. Umożliwia to uzyskanie koloru gradientowego na przestrzeni całego menu.

[Kod C#]

private void DrawDefaultRect(DrawItemEventArgs e)

{

      // Stworzenie rysowanego prostokata

      Rectangle rect = new Rectangle(e.Bounds.X,

            e.Bounds.Y,

            e.Bounds.Width,

            e.Bounds.Height - 1);

 

      // Wyliczenie kolorow prostokata

      Color colorDark = GetMenuItemColor(rect.Left);

      Color colorLight = GetMenuItemColor(rect.Right);

 

      // Stworzenie pedzla do narysowania prostokata

      Brush rectBrush = new LinearGradientBrush(rect,

            colorDark,

            colorLight,

            LinearGradientMode.Horizontal);

 

      // Wypelnienie prostokata

      e.Graphics.FillRectangle(rectBrush, rect);

 

      // Obrysowanie prostokata

      e.Graphics.DrawRectangle(new Pen(rectBrush), rect);

}

A oto funkcja wyliczająca kolor w zależności od współrzędnej x:

[Kod C#]

private Color GetMenuItemColor(int x)

{

      // Wyznaczenie obszaru calego formularza, w ktorym wystepuje menu

      RectangleF rect = _menu.GetForm().ClientRectangle;

 

      // Wyznaczenie spolczynnika do wyliczenia koloru

      float p = (x - rect.X)/rect.Width;

 

      //Wyznaczenie składowych koloru

      int r,g,b;

      r = (int)(Globals.MainColorDark.R + (Globals.MainColorLight.R - Globals.MainColorDark.R) * p);

      g = (int)(Globals.MainColorDark.G + (Globals.MainColorLight.G - Globals.MainColorDark.G) * p);

      b = (int)(Globals.MainColorDark.B + (Globals.MainColorLight.B - Globals.MainColorDark.B) * p);

 

      return Color.FromArgb(r, g, b);

}

Po stworzeniu klasy MainMenuItemHelper, dodajemy jej instancję do naszej kontrolki. W funkcjach obsługujących zdarzenia MeasureItem i DrawItem wywołujemy odpowiednio funkcje MeasureMenuItem i DrawMenuItem obiektu MainMenuItemHelper. A oto dotychczasowe efekty naszej pracy:

To, co w tej chwili najbardziej razi, to brak koloru po ostatnim obiekcie MenuItem. Niestety kontrolka MainMenu nie pozwala nam na ustawienie jej koloru, dlatego jedynym sposobem jest dodanie na końcu jeszcze jednego elementu, który nie miałby tekstu i byłby nieaktywny lub wydłużenie ostatniego elementu. Ja postanowiłem dodawać na koniec jeszcze jeden element. Dlatego dodamy do kontrolki funkcje InitExtraMenuItem i UninitExtraMenuItem, które odpowiednio dodają nasz dodatkowy element oraz go usuwają. Na końcu metody InitMainMenu wywołujemy funkcję InitExtraMenuItem. Ma ona następującą postać:

[Kod C#]

private void InitExtraMenuItem()

{

      // Stworzenie dodatkowego obiektu MenuItem jesli jeszcze nie istnieje

      if (null == _extraMenuItem)

      {

            _extraMenuItem = new MenuItem();

            _extraMenuItem.Enabled = false;

      }

           

      // Dodanie dodatkowego elementu do menu

      this.MenuItems.Add(_extraMenuItem);

                 

      // Dodanie obslugi zdarzenia DrawItem dla obiektu

      _extraMenuItem.DrawItem += new DrawItemEventHandler(extraMenuItem_DrawItem);           

      _extraMenuItem.OwnerDraw = true;

}

Następnie dodajemy jeszcze metodę do klasy MainMenuItemHelper rysującą nasz dodatkowy obiekt:

[Kod C#]

public void DrawExtraMenuItem(DrawItemEventArgs e)

{

      // Pobranie obszaru formatki, na ktorej znajduje sie menu

      Rectangle clientRect = _menu.GetForm().ClientRectangle;

                 

      // Stworzenie rysowanego prostokata

      Rectangle rect = new Rectangle(e.Bounds.X,

            e.Bounds.Y,

            clientRect.Right-(e.Bounds.X-clientRect.X - 3),

            e.Bounds.Height - 1);

 

      // Wyznaczenie koloru dla lewej strony prostokata

      Color colorDark = GetColor(rect.X);

 

      // Stworzenie pedzla do narysowania prostokata

      Brush b = new LinearGradientBrush(rect,

            colorDark,

            Globals.MainColorLight,

            LinearGradientMode.Horizontal);

 

      // Wypelnienie prostokata

      e.Graphics.FillRectangle(b, rect);

                 

      // Obrysowanie prostokata

      e.Graphics.DrawRectangle(new Pen(b), rect);

      e.Graphics.DrawLine(new Pen(colorDark),

            e.Bounds.Left,

            e.Bounds.Top,

            e.Bounds.Left,

            e.Bounds.Bottom);

}

Po tym zabiegu nasze menu przedstawia się w następujący sposób:

 

 

Czyli obiekty MenuItem najwyższego poziomu wyglądają już tak, jak powinny. Teraz będziemy mogli się zająć obiektami MenuItem niższych poziomów.

 

Rysowanie obiektów MenuItem na niższych poziomach

 

Rysowanie tych obiektów odbywa się na tych samych zasadach, co obiektów najwyższego poziomu. Zanim jednak przejdziemy do rysowania obiektów niższego rzędu musimy dodać obsługę obrazków do naszej kontrolki, ponieważ chcemy, aby nasze obiekty MenuItem mogły zawierać ikony.

Najpierw tworzymy interfejs IPictureList zawierający metody umożliwiające przypisywanie obrazka do obiektu MenuItem oraz pobieranie obrazka powiązanego z obiektem MenuItem. Następnie implementujemy go w naszej kontrolce. Dodajemy do kontrolki zmienną przechowującą wszystkie wykorzystywane obrazki (jest ona typu ImageList) oraz zmienną kolekcyjną typu Hashtable, która będzie przechowywała powiązania pomiędzy obrazkami, a obiektami MenuItem, nazwijmy ją _pictureTable. Następnie implementujemy metody z naszego interfejsu:

 

[Kod C#]

public void AddPicture(MenuItem menuItem, int pictureIndex)

{

      _pictureList.Add(menuItem.Handle, pictureIndex);

}

 

public static Image GetPicture(MenuItem menuItem)

{

      if (null == _imageList)

      {

            return null;

      }

      object pictureIndex = _pictureTable[menuItem.Handle];

      if (null == pictureIndex)

      {

            return null;

      }

      return _imageList.Images[Convert.ToInt32(pictureIndex)];

}

Funkcja GetPicture zwraca wartość null w przypadku, gdy kontrolka nie ma przypisanej listy obrazków lub gdy dla danego obiektu MenuItem nie ma przypisanego żadnego obrazka. Teraz, gdy mamy już dołączoną obsługę obrazków, możemy zabrać się za rysowanie obiektów MenuItem. Podobnie jak wcześniej w przypadku obiektów najwyższego stopnia, teraz też stworzymy klasę pomocniczą. Tym razem będzie się ona nazywała MenuItemHelper. Analogicznie dodajemy metody odpowiedzialne za wymierzenie rozmiaru obiektu oraz za jego narysowanie:

[Kod C#]

public void MeasureMenuItem(MeasureItemEventArgs e, MenuItem menuItem)

{                                 

      // Jesli menuItem jest separatorem

      if ( menuItem.Text == "-" )

      {

            e.ItemHeight = Globals.SEPARATOR_HEIGHT;

      }

      else

      {

            // Pobieranie rozmiaru tekstu obiektu MenuItem

            SizeF miSize = e.Graphics.MeasureString(menuItem.Text, Globals.menuFont);

 

            // Pobieranie rozmiaru skrótu klawiszowego

            int scWidth = 0;

            if ( menuItem.Shortcut != Shortcut.None )

            {

                  SizeF scSize = e.Graphics.MeasureString(GetShortcutText(menuItem.Shortcut), Globals.menuFont);

                  scWidth = Convert.ToInt32(scSize.Width);

            }

 

            // Ustawianie rozmiaru

            int miHeight = Convert.ToInt32(miSize.Height) + Globals.SEPARATOR_HEIGHT;

            if (miHeight < Globals.MIN_MENU_HEIGHT)

            {

                  miHeight = Globals.MIN_MENU_HEIGHT;

            }

            e.ItemHeight = miHeight;

            e.ItemWidth = Convert.ToInt32(miSize.Width) + scWidth + (Globals.PIC_AREA_SIZE * 2);

      }                

}

Jeśli obiekt nie jest separatorem, to jego wysokość jest równa minimalnej wartości lub wysokości tekstu. Natomiast jego szerokość składa się z szerokości tekstu, skrótu dołączonego do obiektu i podwojonemu rozmiarowi obszaru na obrazek, który następnie występuje zarówno z lewej, jak i prawej strony.

Poniżej znajduje się metoda odpowiedzialna za rysowanie obiektu MenuItem:

 

[Kod C#]

public void DrawMenuItem(DrawItemEventArgs e, MenuItem menuItem)

{                                                   

      // Sprawdzenie stanu w jakim znajduje się obiekt menuItem

      if ( (e.State & DrawItemState.Selected) == DrawItemState.Selected )

      {

            // Jesli obiekt jest dostepny

            if (true == menuItem.Enabled)

            {

                  // Rysowanie obiektu menuItem, gdy jest wybrany

                  DrawSelectionRectMenuItem(e);

            }

            else

            {

                  // Rysowanie obszaru dla obrazka

                  DrawPictureArea(e);

            }

      }

      else

      {

            // Domyslne rysowanie obiektu, gdy nie jest wybrany

            e.Graphics.FillRectangle(new SolidBrush(Globals.MenuBgColor), e.Bounds);

            // Rysowanie obszaru dla obrazka

            DrawPictureArea(e);

      }

 

      // Narysowanie zaznaczenia obiektu, jesli jest zaznaczony

      if ((e.State & DrawItemState.Checked) == DrawItemState.Checked)

      {

            DrawCheckBox(e, menuItem);

      }

 

      // Rysowanie obrazka powiazanego z obiektem menuItem

      DrawPicture(e, menuItem);   

 

      // Rysowanie tekstu obiektu menuItem

      DrawMenuText(e, menuItem);              

}

Sądzę, że nie ma sensu, abym tu teraz zamieszczał treści wszystkich metod wywoływanych w powyższej funkcji. Skupię się może bardziej na metodzie DrawPicure, ponieważ tutaj jest coś nowego. Na początek niech nasza metoda rysująca obrazek ma następującą postać:

[Kod C#]

private void DrawPicture(System.Windows.Forms.DrawItemEventArgs e, MenuItem mi)

{                

      // Pobranie obrazka powiazanego z obiektem MenuItem

      Image img = _pictureList.GetPicture(mi);

 

      // Jesli istnieje obrazek, to go rysuj

      if (img != null)

      {

            // Jesli rozmiar przekracza dopuszczalne maksimum, to go napraw

            int width = img.Width > Globals.MAX_PIC_SIZE ? Globals.MAX_PIC_SIZE : img.Width;

            int height = img.Height > Globals.MAX_PIC_SIZE ? Globals.MAX_PIC_SIZE : img.Height;

                       

            // Ustaw polozenie obrazka

            int x = e.Bounds.X + 2;

            int y = e.Bounds.Y + ((e.Bounds.Height - height) / 2);

                       

            // Stworzenie prostokata dla obrazka

            Rectangle rect = new Rectangle(x,

                  y,

                  width,

                  height);

                 

            // Rysowanie obrazka

            e.Graphics.DrawImage(img, rect);        

      }

}

Jak widzimy nie ma w niej nic skomplikowanego. Najpierw pobieramy obrazek powiązany z obiektem MenuItem. Jeśli taki istnieje, wyznaczamy jego rozmiar i rysujemy go. Jednak czegoś tutaj jeszcze brakuje. Jak spojrzymy na menu z Office 2003, dostrzeżemy, że w przypadku, gdy opcja jest niedostępna, obrazek przy niej występujący jest na wpół przezroczysty. Spróbujemy uzyskać podobny efekt. Aby to uczynić będziemy musieli wykorzystać klasę ColorMatrix, która reprezentuje macierz przystosowania koloru. Pozwala ona na zmianę natężenia wszystkich składowych koloru wraz ze współczynnikiem alpha, dzięki czemu możemy uzyskać efekt przezroczystości obrazka. Zmodyfikujmy końcówkę naszej metody rysującej obrazek. Niech wygląda ona w następujący sposób:

[Kod C#]

if ( mi.Enabled )

{

      // Rysowanie obrazka

      e.Graphics.DrawImage(img, rect);

}

else

{

      //Stworzenie macierzy przystosowania koloru

      ColorMatrix colorMatrix = new ColorMatrix();

      colorMatrix.Matrix00 = 1f;   // Red

      colorMatrix.Matrix11 = 1f;   // Green

      colorMatrix.Matrix22 = 1f;   // Blue

      colorMatrix.Matrix33 = 0.3f; // Alpha

      colorMatrix.Matrix44 = 1f;   // W

 

      // Stworzenie obiektu ImageAttributes i ustawienie macierzy przystosowania koloru

      ImageAttributes imageAttributes = new ImageAttributes();

      imageAttributes.SetColorMatrix(colorMatrix,

            ColorMatrixFlag.Default,

            ColorAdjustType.Bitmap);

                            

      // Rysowanie przezroczystego obrazka

      e.Graphics.DrawImage(img,

            rect,

            0,

            0,

            width,

            height,

            GraphicsUnit.Pixel,

            imageAttributes);

}

Po stworzeniu klasy MenuItemHelper należy jeszcze troszkę zmodyfikować kod naszej kontrolki. Przede wszystkim musimy dodać metody InitMenuItem i UninitMenuItem. Pierwsza z nich ustawia styl Office2003, druga - przywraca standardowy widok obiektów MenuItem niższych poziomów. Metody te są wywoływane rekurencyjnie, zagłębiając się w strukturze menu oraz odpowiednio w metodach InitMainMenu i UninitMainMenu na końcu pętli foreach.

 

Końcowy efekt

Oczywiście musimy sobie zdawać sprawę z tego, że do ideału naszej kontrolce jeszcze sporo brakuje. Jednym z większych mankamentów jest to, że styl menu możemy ustalić dopiero po dodaniu wszystkich obiektów MenuItem. Jeśli chcielibyśmy zmieniać strukturę menu w czasie działania aplikacji, musielibyśmy jeszcze raz ustawić styl menu. Aby temu zapobiec musielibyśmy mieć zdarzenie dodawania obiektu MenuItem do menu. Wiązałoby się to z przeciążeniem całej kolekcji MenuItems, a wtedy już może warto byłoby się zastanowić nad stworzeniem takiej kontrolki całkowicie od początku. Można by znaleźć jeszcze parę braków, ale mam nadzieję, że mimo wszystko stworzone przeze mnie menu jest niezłą imitacją tego znajdującego się w Office 2003.

Gdyby ktoś chciał, to bez problemu może stworzyć sobie również menu kontekstowe w analogiczny sposób, wykorzystując już istniejącą klasę MenuItemHelper.

 

Materiały:

 

 

 

 

Załączniki:

tagi: Office

Podobne artykuły

Komentarze 1

User 121645
User 121645
1 pkt.
Nowicjusz
21-01-2010
oceń pozytywnie 0
Bardzo fajny i ciekawy pomysł, który zainspirował mnie do pokazania menu w stylu Visty. Zatem aktualne dzieło można jeszcze ulepszyć i uprościć. Może kiedyś... ;)
pkt.

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