Przegląd Windows Forms
Windows Forms to podstawowa biblioteka do konstruowania „klasycznego” interfejsu Windows. U podstaw tej biblioteki można znaleźć pewne elementy kontrolek „wbudowanych” z VB 6.0, pewne składowe JFC czy wiele koncepcji z innych bibliotek przeznaczonych do tworzenia interfejsu użytkownika. W każdym razie wraz z .NET programista otrzymuje bardzo rozbudowaną bibliotekę pozwalającą stworzyć taki interfejs użytkownika, który zadowoli najbardziej wymagającego klienta.
Omówienie wszystkich obiektów Windows Forms jest bardzo trudne - ale tak naprawdę większość obiektów jest albo już dobrze znana (z na przykład z VB 6.0) lub można szybko zapoznać się z nimi już po kilku próbach. Warto jednak wyróżnić kilka nowych elementów, które mogą przydać się przy programowaniu z WinForms.
/RAMKA
Co z kontrolkami ActiveX?
W projektach Windows Forms można także skorzystać z kontrolek ActiveX. Należy tylko... dodać je do paska narzędziowego. VS.NET samo zajmie się wygenerowaniem odpowiedniej warstwy pośredniej (tzw. COM Interopt) która zapewnia komunikację ze starszą technologią. Należy także pamiętać, że nie wszystkie kontrolki mogą być używane w świecie .NET. Problemy dotyczą zwłaszcza tzw. kontrolek windowless. Warto także sprawdzić, czy czasem analogiczna funkcjonalność nie jest realizowana przez jakiś obiekt .NET-owy.
/KONIEC RAMKI
Każda kontrolka Windows Forms ma 2 ciekawe właściwości - Anchor i Dock. Anchor określa te brzegi pojemnika (np. formatki), z którymi krawędzie kontrolki mają zachowywać stałą odległość. Na przykład, jeżeli Anchor dla pola tekstowego będzie równe „Left, Right”, to pole będzie się rozszerzać w miarę jak kontrolka zwiększa swój rozmiar. Właściwość Dock określa sposób dokowanie w obrębie pojemnika kontrolki (np. do górnej krawędzi).
Dokładne opisanie zasad działania Dock oraz Anchor mija się z celem - po kilku próbach każdy programista będzie dokładnie rozumiał, jak one działają. Warto tylko pamiętać o jeszcze jednym obiekcie - pojemniku wprowadzonym w WinFroms - Panel. Kontrolka Panel przypomina kontrolkę GroupBox. Jednak - Panel może zawierać elementy które nie mieszczą sie w zadanym obszarze - zostaną wtedy pokazane paski przewijania. Natomiast GroupBox może mieć tytuł.
Wśród nowych kontrolek warto wymienić kilka:
CheckedListBox - kontrolka, która zawiera listę elementów wraz z opcją wyboru każdego elementu
Splitter - który pozwala zmieniać rozmiar zadokowanej kontrolki (w ten sposób można łatwo stworzyć taki interfejs jak np. w Microsoft Outlook; z listą ikon po lewej stronie)
NotyfiIcon - kontrolka pozwala łatwo wyświetlać własne ikony w pasku koło zegara
OpenFileDialog/PrintDialog - w .NET zamiast jednego dużego obiektu realizującego wszystkie standardowe okna dialogowe Windows, dostępnych jest kilka kontrolek, które odpowiadają za konkretny typ okna.
PrintPreviewControl - kontrolka, która pozwala „podejrzeć” wygląd dokumentu przed jego wydrukiem
W .NET nie jest dostępny specjalny typ formatki MDI. Natomiast dowolnej formatce można ustawić właściwośc IsMdiContainer, co spowoduje, że zacznie zachowywać się jak pojemnik MDI. Warto dodać, że tą właściwość można zmieniać podczas działania aplikacji. Aby inna formatka stała się formatką typu child danej formatki MDI, należy odpowiednio zainicjować właściwość MDIParent, jak w poniższym przykładzie:
Me.IsMdiContainer = True
'[...]
Dim NewMDIChild As New Form1()
NewMDIChild.MDIParent = Me
NewMDIChild.Show()
W przypadku projektu Windows Forms należy wybrać tą formatkę, od której rozpoczyna się wykonywanie programu. Programista VB.NET ma do wyboru - albo w ustawieniach projektu wybiera formatkę startową, albo podaje, że wykonywanie programu ma się rozpocząć od procedury Main (musi być ona zdefiniować w jakimś module). Aby uruchomić główną formatkę, w kodzie Main należy napisać:
Application.Run(New MojaFormatka())
W przypadku kodu w C#, w pierwszej dodanej formatce generowana jest funkcja Main, np:
[STAThread]
static void Main() {
Application.Run(new Form1());
}
W przypadku gdy program ma się rozpocząć od uruchomienia innej formatki - należy odpowiednio zmodyfikować ten kod.
/RAMKA/
Warto zwrócić uwagę, że w metodzie Main, w projekcie Windows Forms w C# definiowany jest atrybut [STA Thread]. Powoduje to, że wiele równoległych wątków musi podlegać rozrządowi (ang. marshal) przed odwołaniem się do dowolnego elementu danego pakietu. Wynika to stąd, że Windows Forms wykorzystuje niektóre obiekty OLE (na przykład - te które służą do obsługi schowka) - a akurat metody związane ze schowkiem muszą być wywoływane z wątku, który zainicjował OLE.
/KONIEC RAMKI/
W VS.NET dostępny jest specjalny typ projektu - Windows Application. Wtedy automatycznie generowany jeszt szkielet programu wykorzystującego Windows Forms. Można także np. do projektu typu Console Application dodać po prostu nowy element - Windows Form.
Po uruchomieniu Visual Studio (lub otworzeniu odpowiedniego elementu projektu), na ekranie pokazany zostanie standardowy (znany z wielu narzędzi RAD) ekran projektanta, w którym można „narysować” odpowiednią formatkę.
Jednak w odróżnieniu od innych narzędzi, to co jest „rysowane” w projektancie przekłada się tak naprawdę na ciąg poleceń w danym języku programowania. Jeżeli np. ustalamy rozmiar pola tekstowego, to VS.NET generuje odpowiedni fragment kodu (w sekcji pliku o nazwie „ Windows Form Designer generated code ”). Poniżej pokazany jest fragment kodu (w VB.NET), który powstaje po wstawieniu pola tekstowego i zmianie tła oraz początkowego tekstu wyświetlanego w tym polu (proszę zwrócić uwagę, że po utworzeniu kontrolek, muszą być one dodane do kolekcji Controls danej formatki)
Friend WithEvents TextBox1 As System.Windows.Forms.TextBox
<System.Diagnostics.DebuggerStepThrough()> Private Sub InitializeComponent()
[...]
Me.TextBox1 = New System.Windows.Forms.TextBox()
Me.SuspendLayout()
[...]
'
'TextBox1
'
Me.TextBox1.BackColor = System.Drawing.SystemColors.InactiveBorder
Me.TextBox1.Location = New System.Drawing.Point(16, 56)
Me.TextBox1.Name = "TextBox1"
Me.TextBox1.TabIndex = 2
Me.TextBox1.Text = "Tekst"
[...]
Me.Controls.AddRange(New System.Windows.Forms.Control() {Me.MyTextBox1, [...]})
[...]
Me.ResumeLayout(False)
(metody SuspendLayout i ResumeLayout blokują/odblokowują zgłaszanie zdarzenia o zmianie układu elementów na formatce - przyspiesza to proces inicjowania kontrolek)
W .NET obsługa zdarzeń oparta jest o tzw. delegaty (patrz artykuł poświęcony językom C# i VB.NET). Tak więc nie ma tu znanych z VB procedur obsługi zdarzeń o ustalonej nazwie. Delegaty są znacznie wygodniejszym mechanizmem który pozwala programiście dynamicznie dołączać/odłączać się od danego zdarzenia, czy też tworzyć procedury obsługujące wiele różnych zdarzeń. Jest natomiast bardzo ważne aby deklaracja procedury/funkcji odpowiadała danej deklaracji delegatu - oraz oczywiście by została „zarejestrowana” tak by była wywoływana w momencie wystąpienia zdarzenia.
Aby dodać obsługę nowego zdarzenia w projekcie C# należy wybrać w oknie właściwości przycisk odpowiadający za zdarzenia, a następnie wybrać odpowiednią pozycję. Można albo dwukrotnie kliknąć na odpowiednie zdarzenie, albo wpisać własną nazwę funkcji.
Dodawanie zdarzeń w C# #WF1.PNG
W projektach VB.NET, należy (podobnie jak w VB 6) wybrać w oknie kodu odpowiednią kontrolkę, a nastepnie z belki obok - konkretne zdarzenie. VB.NET wygeneruje odpowiednią „prostą” obsługę zdarzenia.
Dodawanie zdarzeń w VB.NET #WF2.PNG
W C#, aby zarejestrować procedurę obsługi zdarzenia (w tym przypadku TextChanged), generowany jest następujący kod:
[...]
private void InitializeComponent()
{
[...]
this.textBox1.TextChanged += new System.EventHandler(this.textBox1_TextChanged);
[...]
}
private void textBox1_TextChanged(object sender, System.EventArgs e) {
[...]
}
Aby usunąć procedurę obsługi zdarzenia, trzeba napisać (operator new nie jest tu pomyłką!):
this.textBox1.TextChanged -= new System.EventHandler(this.textBox1_TextChanged);
W VB.NET są do wyboru dwie opcje. Standardowy projektant tworzy następujący kod
Friend WithEvents MyTextBox As System.Windows.Forms.TextBox
[...]
Private Sub TextBox1_TextChanged(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyTextBox.TextChanged
End Sub
Słowo kluczowe Handles określa jakie zdarzenie obsługuje dana procedura. Można je ręcznie zmienić, tak by w jednej procedurze obsługiwane były dwa lub więcej zdarzeń:
[...] Handles MyTextBox.TextChanged, MyTextBox1.TextChanged
Oczywiście - nic nie stoi na przeszkodzie, by zdarzenie MyTextBox.TextChanged było obsługiwane także przez inną procedurę.
Można także dynamicznie określić procedurę obsługi zdarzeń. W VB.NET, trzeba wykorzystać słowo kluczowe AddHandler - które dla danego delegatu (czyli - tego elementu który jest wywoływany w momencie zajścia zdarzenia), przypisuje „adres” procedury obsługi. Można to wykonać np. w procedurze obsługi Load:
Private Sub Form1_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles MyBase.Load
AddHandler MyTextBox.TextChanged, AddressOf MyOwnProc
End Sub
Private Sub MyOwnProc(ByVal sender As System.Object, ByVal e As System.EventArgs)
End Sub
Aby usunąć procedurę obsługi zdarzenia - należy użyć słowa kluczowego RemoveHandler
Poniżej pokazany jest przykład prostej formatki, gdzie dynamicznie tworzonych jest 10 przycisków - każdy z nich ma te same procedury obsługi zdarzenia. W tym przypadku zdefiniowane zostały dwie - jedna wyświetla nazwę a druga tekst przycisku. Warto podkreślić, że nazwa (właściwość Name) kontrolek WinForms nie musi być unikatowa, ani nawet nie musi zostać poprawnie zainicjowana.
Private Sub Form2_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
Dim i As Integer
Dim btn As System.Windows.Forms.Button
For i = 0 To 10
btn = New System.Windows.Forms.Button()
btn.Location = New Point(5, 5 + i * 25)
btn.Height = 20 : btn.Width = 100
btn.Text = "Przycisk " + i.ToString()
btn.Name = "BTN"
AddHandler btn.Click, AddressOf MyClick
AddHandler btn.Click, AddressOf MyClick1
Me.Controls.Add(btn)
Next
End Sub
Private Sub MyClick(ByVal sender As System.Object, ByVal e As System.EventArgs)
Dim btn As System.Windows.Forms.Button
btn = sender
MsgBox("Nazwa przycisku: " + btn.Name)
End Sub
Private Sub MyClick1(ByVal sender As System.Object, ByVal e As System.EventArgs)
Dim btn As System.Windows.Forms.Button
btn = sender
MsgBox("Tekst na przycisku: " + btn.Text)
End Sub
/RAMKA/
Co prawda przestrzeń kontrolek i dostępne obiekty ASP.NET i Windows.Forms znacznie się różnią, ale podstawowa filozofia tworzenia formatek - czy to Windows czy to Web - pozostaje taka sama. W Web Froms tak samo obsługiwane są zdarzenia, podobnie możemy dynamicznie dodawać kontrolki do formatki w czasie działania aplikacji. Nad prawidłowym przekazywaniem zdarzeń pomiędzy przeglądarką klienta a serwerem czuwa .NET Framework (a dokładniej - odpowiednio wygenerowane formularze). Programista, aby obsłużyć akcję „naciśnięcia” przycisku dodaje procedurę obsługi zdarzenia Click - tak samo jak dla przycisku na Windows.Forms.
/KONIEC RAMKI/
Dziedziczenie z innej formatki
Kolejną bardzo ciekawą możliwością Windows Forms jest dziedziczenie wizualne formatek. Dzięki temu, można zdefiniować ogólny „szablon” formatki, ze standardową obsługa przycisków czy ustalonym wyglądem, a następnie dziedziczyć tą bazową formatkę w pozostałych elementach projektów - co znacznie upraszcza zachowanie spójności interfejsu w całym projekcie (a przy okazji bardzo upraszcza pracę programisty). Oczywiście po dziedziczeniu, wszelkie zmiany w formatce bazowej są uwzględniane w formatkach dziedziczących (nie tak jak w innych narzędziach, gdzie układ wizualny przy dziedziczeniu jest „kopiowany” do formatki potomnej”)
Poniżej zostanie omówiony przykład zastosowania mechanizmu dziedziczenia formatek. W formatce bazowej zdefiniowane są 2 przyciski (Button1 i Button2; w procedurze obsługi Click wyświetlane są odpowiednie komunikaty)
Public Class frmBase
Inherits System.Windows.Forms.Form
[...]
Friend WithEvents Button1 As System.Windows.Forms.Button
Friend WithEvents Button2 As System.Windows.Forms.Button
[...]
Protected Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click
MsgBox("frmBase - Button1")
End Sub
Private Sub Button2_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button2.Click
MsgBox("frmBase - Button2")
End Sub
End Class
Aby dodać formatkę potomną, należy dodać nowy element o nazwie Inherited Form. W tym przypadku jako obiekt bazowy należy wybrać frmBase.
Uwaga! Można dziedziczyć dowolną formatkę, znajdującą się w dowolnym pakiecie DLL czy EXE. Standardowy kreator analizuje skompilowane informacje - tak więc najpierw należy napisać formatkę bazową (nawet bez pełnej implementacji), a następnie skompilować projekt, tak by można go było wybrać w kreatorze.
Wygenerowana formatka potomna bardzo przypomina „zwykłą” definicję. Jednak obiekt nie dziedziczy z System.Windows.Forms.Form, a z dowolnie wybranej innej formatki - tu frmBase
W tym przypadku do formatki potomnej został dodany jeszcze jeden przycisk (Button4). Przycisk Button2 obsługiwany jest przez formatkę bazową. Natomiast - z przycisku Button1 usuwana jest „oryginalna” procedura obsługi (poleceniem RemoveHandler), a następnie dodawana jest nowa procedura obsługi (tu - MyButtonClick):
Public Class frmInherit
Inherits frmBase
[...]
Friend WithEvents Button4 As System.Windows.Forms.Button
[...]
Private Sub Button4_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button4.Click
MsgBox("frmInherit - Button 4")
End Sub
Private Sub frmInherit_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
RemoveHandler Button1.Click, AddressOf MyBase.Button1_Click
AddHandler Button1.Click, AddressOf MyButtonClick
End Sub
Private Sub MyButtonClick(ByVal sender As System.Object, ByVal e As System.EventArgs)
MsgBox("frInherit - Button1")
End Sub
End Class
Po uruchomieniu projektu, gdy kliknie się na przycisk Button2, wyświetlony zostanie komunikat "frmBase - Button2". Po kliknięciu na przycisk Button1, zostanie wywołana procedura MyButtonClick. Proszę zauważyć, że jeżeli nie byłoby polecenia RemoveHandler, to najpierw wywołana byłaby funkcja z formatki bazowej, a następnie procedura z formatki potomnej (kolejność wykonywanych procedur obsługi zdarzenia odpowiada kolejności „rejestrowania” tych procedur).
ROBERT NUTRYCZ
Metryka artykułu:
Poruszane zagadnienia: Windows Forms, obsługa zdarzeń, dziedziczenie formatek
Kod przykładowy w katalogu \SAMPLES\WINFORMS\GENERAL
/Albo oddzielny art, albo ciąg dalszy - jak wyjdzie/
Własna kontrolka
Aby pokazać dodatkowe możliwości Windows Forms oraz projektanta formatek, najlepiej jest stworzyć własną kontrolkę.
W tym przypadku zostanie zaprojektowana prosty „nagłówek” do umieszczenia na formatce. Kontrolka zawiera znak zapytania oraz pasek z napisem. Ma także własne menu kontekstowe - w tym przypadku wybrania pozycji zmieniany jest tekst na pasku oraz zgłaszane jest zdarzenie. Na poniższym rysunku pokazany jest przykład działania kontrolki na testowym formularzu:
Własna kontrolka #WF3.PNG
Kontrolka została napisana w C# - w tym języku łatwiej pokazać kluczowe elementy własnej kontrolki. Tworzona kontrolka zawiera odpowiednią procedurę rysowania, własne zdarzenia oraz kilka właściwości:
Zdarzenia:
QMClick - zgłaszane w momencie kliknięcia na znak zapytania
ChangeHeader - W momencie zmiany nagłówka (tekstu); gdy użytkownik wybrał pozycję menu
Ważniejsze właściwości:
MenuItems - własna kolekcja zawierająca składniki MyMenuItem, definiujące kolejne pozycje menu
MyStyle - własciwość określa „styl” wyglądu kontrolki - przyjmuje jedną z wartości wymienionych w DefaultStyles. Wykorzystywana m. innymi we własnej procedurze wyświetlania.
HeaderText - określa tekst nagłówka (można go zmieniać niezależnie od wyboru pozycji z menu).
Kontrolka ukrywa także właściwości BackColor/ ForeColor (ponieważ są one ustawiane razem ze stylem).
Poniżej pokazane są główne składowe kontrolki, a także wstępna inicjacja obiektu.
using System;
using System.Collections;
using System.ComponentModel;
using System.Diagnostics;
using System.Drawing;
using System.Windows.Forms;
using System.IO;
using System.Reflection;
namespace FormHeader
{
[
DefaultProperty("Text"),
DefaultEvent("QMClick"),
]
public class FormHeader : System.Windows.Forms.UserControl,IMenuItemConsumer
{
private System.Windows.Forms.PictureBox pctQuestion;
private System.Windows.Forms.ContextMenu contextMenu1;
private System.ComponentModel.Container components = null;
public FormHeader()
{
InitializeComponent();
}
[...]
#region Component Designer generated code
private void InitializeComponent()
{
[...]
this.pctQuestion.Dock = System.Windows.Forms.DockStyle.Right;
this.pctQuestion.Image = ((System.Drawing.Bitmap)(resources.GetObject("pctQuestion.Image")));
[...]
this.pctQuestion.Click += new System.EventHandler(this.pctQuestion_Click);
[...]
this.BackColor = System.Drawing.Color.Silver;
this.ContextMenu = this.contextMenu1;
this.Controls.AddRange(new System.Windows.Forms.Control[] {this.pctQuestion});
this.Name = "FormHeader";
this.Size = new System.Drawing.Size(248, 40);
// Zmiana wielkości kontrolki
this.SizeChanged += new System.EventHandler(this.MySize);
// Własna procedura rysowania
this.Paint += new System.Windows.Forms.PaintEventHandler(this.MyPaint);
// Mysz wchodzi w obszar kontrolki
this.MouseEnter += new System.EventHandler(this.MyEnter);
// Mysz opuszcza obszar kontrolki
this.MouseLeave += new System.EventHandler(this.MyLeave);
this.ResumeLayout(false);
}
#endregion
Kontrolka ma zgłaszać własne zdarzenie, po kliknięciu na znak zapytania. W tym celu trzeba zdefiniować delegata - tu onQMClick, a także procedurę dopisywania i „zarejestrowanych” funkcji, powiadamianych w momencie zgłoszenia zdarzenia. Samo wyzwolenie zdarzenia sprowadza się do wywołania procedury Invoke delegata - w momencie gdy użytkownik kliknie na pctQuestion. Proszę zauważyć, że to, iż procedura pctQuestion_Click jest wywoływana w momencie „kliknięcia” zostało zdefiniowane przy definicji kontrolki.
#region Definicja zdarzenia obsługującego naciśnięcie przcisku ?
private System.EventHandler onQMClick;
[Description("Zgłaszane, gdy użytkownik kliknie na przycisk '?'")]
public event EventHandler QMClick {
add {
onQMClick += value;
}
remove {
onQMClick -= value;
}
}
private void pctQuestion_Click(object sender, System.EventArgs e) {
//Zgłoszenie zdarzenia
if (onQMClick !=null) onQMClick.Invoke(this,e);
}
#endregion
/RAMKA
Jak wygląda to w VB.NET
Tworzenie kontrolki w VB.NET wykonywane jest w analogiczny sposób (z dokładnością do składni). Warto jednak pokazać, w jaki sposób definiowane są zdarzenia i w jaki sposób kontrolka je „zgłasza”. Przykładowa kontrolka zawiera tylko etykietę Label1. Po jej kliknięciu, zgłaszane jest nowe zdarzenie - MyClick. W VB.NET nie ma potrzeby by tworzyć pełne procedury obsługi delegatów. Wystarczy tylko defnijcja publicznego „zdarzenia”:
<Description("Zgłaszane po kliknięciu")> _
Public Event MyClick(ByVal sender As Object, ByVal ev As EventArgs) 'As EventHandler
Private Sub Label1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Label1.Click
RaiseEvent MyClick(Me, e)
End Sub
/KONIEC RAMKI
Definicja obsługi menu (a zwłaszcza - kolekcji menu) jest bardziej skomplikowana. Wykorzystywana jest tu własna kolekcja (MenuItemCollection) z mocną kontrolą typów - dzięki temu można skorzystać ze standardowego projektanta kolekcji. Atrybut Editor określa, jaki obiekt odpowiada za edytowanie danej właściwości. Interfejs IMenuItemConsumer implementowany przez FormHeader wymaga, by istniała właściwość która zwraca kolekcję ArrayList - MenuItemsArr. Kontrolka w tej kolekcji przechowuje pozycje menu (i tak naprawdę MenuItemCollection wewnętrznie przechowuje obiekty właśnie w kolekcji ArrayList) Pełna implementacja pomocniczych interfejsów znajduje się w pliku support.cs. Poniżej prezentowana jest tylko część kodu w kontrolce, która odpowiada za implementację obsługi menu:
#region Obsługa menu
//Kolekcja przechowująca pozycje menu
internal MenuItemCollection m_col;
//Edytor tej kolekcji
[
Category("Appearance"),
Description("MenuItems"),
DefaultValue(DefaultStyles.eDark),
Bindable(true),
Browsable(true),
Editor(typeof(System.ComponentModel.Design.CollectionEditor),
typeof(System.Drawing.Design.UITypeEditor)),
DesignerSerializationVisibility(DesignerSerializationVisibility.Content)
]
public MenuItemCollection MenuItems {
get {
if (m_col==null)
m_col=new MenuItemCollection(this);
return m_col;
}
}
private ArrayList m_arr=new ArrayList();
public System.Collections.ArrayList MenuItemsArr {
get {
return m_arr;
}
}
protected override void OnCreateControl() {
InitMenu();
}
Inicjacja menu jest dosyć prosta - przeglądana jest kolekcja pozycji m_col, i dodawane są odpowiednie składniki do menu kontekstowego (dostępnego pod prawym przyciskiem myszy). Proszę zwrócić uwagę, że dla każdej pozycji definiowana jest ta sama procedura obsługi zdarzeń - tu - MenuClick.
void InitMenu() {
contextMenu1.MenuItems.Clear();
if (m_col!=null) {
foreach(MyMenuItem item in m_col) {
contextMenu1.MenuItems.Add(
new System.Windows.Forms.MenuItem(
item.Label,new EventHandler(this.MenuClick)
)
);
}
}
}
Zdarzenie zgłaszane w momencie wyboru pozycji w menu ma zgodnie z założeniami zwracać klucz (identyfikujący wybraną pozycję). Aby to było możliwe, należy zdefiniować własną klasę dziedziczącą z EventArgs.
public class MyArgs:System.EventArgs {
public string Key;
public MyArgs(string pkey) {
Key=pkey;
}
}
Po wybraniu pozycji z menu kontekstowego, należy po pierwsze zmienić odpowiednio tekst w FormHeader. Następnie należy zgłosić zdarzenie - przekazując jako argument klucz wybranej pozycji z menu. Można też było zdefiniować własny delegat (dziedziczący z System.EventHandler). Sama obsługa „rejestracji” w delegacie jest dokładnie taka sama jak w przypadku QMClick:
//Wybór pozycji w menu
private void MenuClick(Object sender, EventArgs e) {
MenuItem item=(MenuItem)sender;
MyMenuItem m=MenuItems[item.Index];
HeaderText=m.Label;
if (onChangeHeader!=null) {
//Zgłoszenie zdarzenia - przekazanie dodatkowego argumentu (klucza)
//Odpowiadającego wybranej pozycji
onChangeHeader.Invoke(this,new MyArgs(m.Key));
}
}
private System.EventHandler onChangeHeader;
[Description("Wybór pozycji z menu")]
public event EventHandler ChangeHeader {
add { onChangeHeader+= value; }
remove { onChangeHeader -= value; }
}
#endregion
Aby zmienić kolory kontrolki, programista będzie mógł tylko wybierać jeden ze zdefiniowanych styli. Nie będzie możliwa edycja właściwości BackColor czy ForeColor. W tym celu, należy przesłonić te właściwości (słowo kluczowe override) a dodatkowo ustawić im atrybut Browseable na false. Dzięki temu projektant VS.NET nie będzie pokazywał tych elementów w oknie właściwości.
#region Definicja wyglądu
[Browsable(false)]
public override Color BackColor {
get {return base.BackColor;}
set {}
}
[Browsable(false)]
public override Color ForeColor {
get {return base.ForeColor;}
set {}
}
Zdefiniowane style mają przypisaną wartość zgodnie z wyliczeniem DefaultStyles. Aby programista wykorzystujący tą kontrolkę mógł kontrolować styl wyświetlania, w FormHeader została dodana nowa właściwość - MyStyle. Przy użyciu atrybutów została ona przypisana do kategorii Appearance (kontroluje przecież wygląd), a także określony został opis i domyślna właściwość. Warto tu dodać, że wystarczy aby właściwość była określonego typu wyliczeniowego (tu - DefaultStyle), by projektant formatek VS.NET automatycznie wygenerował listę typu ComboBox do wyboru właściwego stylu.
public enum DefaultStyles {
eDark=1,
eLight,
eColorFull
}
private DefaultStyles m_style=DefaultStyles.eDark;
[
Category("Appearance"),
Description("Styl wyświetlania"),
DefaultValue(DefaultStyles.eDark),
Browsable(true)
]
public DefaultStyles MyStyle {
get {
return m_style ;
}
set {
m_style=value;
Invalidate();
}
}
W podobny sposób określona została właściwość, która określa początkową zawartość etykiety na FormHeader. Warto zwrócić uwagę, że programista kontrolki nie musi troszczyć się o zapisanie „stanu” nowo utworzonych właściwości - tym zajmuje się już VS.NET - dzięki mechanizmowi refleksji.
string m_string;
[
Category("Appearance"),
Description("Początkowy tekst wyświetlany na kontrolce"),
DefaultValue("<Tu proszę wprowadzić tekst>"),
Browsable(true)
]
public string HeaderText {
get {
return m_string;
}
set {
m_string=value;
Invalidate();
}
}
#endregion
Kontrolka zmienia swój wygląd w momencie, gdy kursor myszy znajdzie się w obszarze zajmowanym przez FormHeader. Dwie procedury obsługi zdarzenia odpowiednio ustawiają flagę (bEnter). Po zmianie stanu znacznika, wymuszają odrysowanie całego obszaru kontrolki.
#region Rysowanie kontrolki
private bool bEnter = false;
private void MyEnter(object sender, System.EventArgs e) {
//Kursor wszedł w obszar kontrolki
bEnter=true;
Invalidate();
}
private void MyLeave(object sender, System.EventArgs e) {
//Kursor wyszedł z obszaru kontrolki
bEnter=false;
Invalidate();
}
Samo rysowanie kontrolki jest dosyć proste. Znak zapytania (rysunek) „sam” się wyświetla. Kontrolka musi tylko w odpowiedni sposób wyświetlić tekst. Warto tu zwrócić uwagę, że przy wyświetlaniu wykorzystywana jest „standardowa” właściwość Font - kontrolki WinForms mają pewną liczbę standardowych właściwości, które automatycznie są obsługiwane przez klasę bazową.
private void MyPaint(object sender, System.Windows.Forms.PaintEventArgs e) {
System.Drawing.Graphics G=e.Graphics;
System.Drawing.Point[] pt={ new Point(0,0),new Point(8,0),new Point(4,6)};
SolidBrush MyBrush,MyBrushBack;
switch (m_style) {
default:
case DefaultStyles.eLight:
MyBrushBack = new SolidBrush(System.Drawing.Color.Beige);
MyBrush = new SolidBrush(System.Drawing.Color.Black);
break;
[...]
}
if (bEnter) {
MyBrush=new SolidBrush(System.Drawing.Color.AntiqueWhite);
}
Size textSize = e.Graphics.MeasureString(HeaderText, Font).ToSize();
float xPos = (ClientRectangle.Width/2) - pctQuestion.Width - (textSize.Width/2);
float yPos = (ClientRectangle.Height/2) - (textSize.Height/2);
G.FillRectangle(MyBrushBack,G.VisibleClipBounds);
G.DrawString(HeaderText, Font, MyBrush, xPos, yPos);
pt[2].Y=textSize.Height /2;
G.TranslateTransform(xPos+10 + textSize.Width,yPos + textSize.Height / 4 );
G.FillPolygon(MyBrush ,pt,System.Drawing.Drawing2D.FillMode.Winding);
}
Nie należy także zapomnieć o obsłudze zdarzenia, które jest zgłaszane w momencie zmiany rozmiaru kontrolki. Jeżeli go zabraknie, podczas projektowania kontrolka nie będzie poprawnie odrysowywana przy zmianach wielkości.
private void MySize(object sender, System.EventArgs e) {
Invalidate();
}
#endregion
}
}
W kolejnym kroku zostanie utworzona „mocna” nazwa dla kontrolki FormHeader. W tym celu należy określić klucz, który będzie jednoznacznie identyfikował pakiet. Aby go wygenerować, skorzystamy z narzędzia sn.exe (część pakietu .NET Framework; instalowane razem z VS.NET).
/RAMKA
Należy pamiętać, że aby skorzystać z narzędzi z poziomu linii poleceń, muszą być zainicjowane odpowiednie zmienne środowiskowe w Windows. Podczas instalacji VS.NET tworzony jest odpowiedni skrót - proszę uruchomić Start - Programs - Microsoft Visual Studio .NET - Visual Studio .NET Tools - Visual Studio .NET Command Prompt.
/KONIEC RAMKI
Aby wygenerować klucz, należy napisać sn.exe -k <nazwa pliku do którego ma być zapisany klucz>
W tym przypadku polecenie może mieć postać:
E:\Moje Dokumenty\Visual Studio Projects\CHIP\FormHeader\FormHeader>sn -k MyOwnFormHeaderKey.snk
Microsoft (R) .NET Framework Strong Name Utility Version 1.0.3705.0
Copyright (C) Microsoft Corporation 1998-2001. All rights reserved.
Key pair written to MyOwnFormHeaderKey.snk
W przykładzie, który znajduje się na CD-ROM jest gotowy, wygenerowany klucz. Jednak, jeżeli czytelnik chce zmieniać coś w tej kontrolce, to koniecznie powinien wygenerować własny klucz. W ten sposób utworzy własną „mocną” nazwę kontrolki. Równocześnie umożliwi to, że w systemie będą mogły istnieć dwie różne kontrolki, o tej samej nazwie - coś, co dotychczas w świecie Windows było zupełnie niemożliwe.
Aby zobaczyć klucz publiczny, który identyfikuje jednoznacznie „wystawcę” kontrolki (co pozwoli np. ustalić odpowiednie zasady bezpieczeństwa związane z danym wystawcą), należy napisać:
E:\...\FormHeader\FormHeader\bin\Debug>sn -T FormHeader.dll
Microsoft (R) .NET Framework Strong Name Utility Version 1.0.3705.0
Copyright (C) Microsoft Corporation 1998-2001. All rights reserved.
Public key token is d6d41fb17c7a9686
W projekcie znajduje się specjalny plik o nazwie AssemblyInfo.cs. Jest to plik, który w zasadzie zawiera tylko kilka atrybutów - takich jak opis pakietu itp. Oprócz tego, zawiera także informacje o kluczu
using System.Reflection;
using System.Runtime.CompilerServices;
[...]
[assembly: AssemblyVersion("1.2.*")]
/*
* Wersja główna - Major - 1
* Numer podwersji - Minor - 2
* Wersja kompilacji i poprawek jest generowana podczas kompilacji
*/
[assembly: AssemblyDelaySign(false)]
/*
* Ten parametr określa, że nie jest stosowany "opóźniony" mechanizm podpisywania pakietu (assembly).
* Jeżeli byłby równy true, wtedy proces podpisywania byłby dwustopniowy - w ten sposób można zapewnić centralną autoryzację tworzonych pakietów w firmie i wymusić, by każdy tworzony i w pełni podpisany pakiet spełniał np. wymogi jakościowe.
*/
[assembly: AssemblyKeyFile("..\\..\\MyOwnFormHEaderKey.snk")]
/*
* To jest względna ścieżka do pliku zawierającego klucz. W tym przypadku plik FormHeader.dll
* jest umieszczany albo w podkatalogu bin\debug lub bin\release
*/
[assembly: AssemblyKeyName("")] //Tu można by podać nazwę podpisu przechowywanego w CSP
Aby sprawdzić, czy ta kontrolka działa poprawnie, należy utworzyć nowy projekt Windows Forms (w dowolnym języku - w przykładowym kodzie do tego samego rozwiązania które zawiera FormHeader został dodany nowy projekt w VB.NET). Następnie należy dodać tą kontrolkę do paska Toolbox (najprościej jest wskazać plik DLL). Na poniższym rysunku pokazana jest formatka (w trybie edycji), z zaznaczoną naszą kontrolką. W tym przypadku została założona nowa zakładka na „testowe” kontrolki. Dodatkowo otworzony został projektant pozycji menu.
Własny projektant kolekcji #WF4.PNG
Warto zwrócić uwagę, że każda pozycja Menu zapamiętywana jest w projektancie jako nowy obiekt typu MyMenuItem. Dopiero w kodzie inicjalizującym kontrolki, obiekty są dodawane do kolekcji:
[...]
Me.FormHeader1.MenuItems.AddRange(New Object() {Me.MyMenuItem1, Me.MyMenuItem2, Me.MyMenuItem3})
[...]
Zdarzenie QMClick może być obsługiwane w standardowy sposób:
Private Sub FormHeader1_QMClick_1(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles FormHeader1.QMClick
MsgBox("FormHeader1_QMClick - Ktoś mnie kliknął")
End Sub
Trochę inaczej obsługiwane jest zdarzenie zgłaszane w momencie wyboru pozycji z Menu. Ponieważ nie został zdefiniowany własny typ delegatu, to nagłówek zdarzenia jest standardowy. Natomiast wiadomo, że przekazany obiekt e zawierajacy argumenty jest typu MyArgs - stąd jest możliwe takie rzutowanie jak poniżej:
Private Sub FormHeader1_ChangeHeader(ByVal sender As Object, ByVal e As System.EventArgs) Handles FormHeader1.ChangeHeader
Dim mye As FormHeader.MyArgs
mye = e
MsgBox("FormHeader1_ChangeHeader - wybrano : " + mye.Key)
'Można też wykorzystać CType
'MsgBox("FormHeader1_ChangeHeader - wybrano : " + CType(e, FormHeader.MyArgs).Key)
End Sub
Na koniec, przedstawię zasady dodawania informacji o licencjonowaniu kontroliki - tak, by mógł z niej skorzystać tylko ten programista który „ma” licencję. Aby kontrolka sprawdzała, czy programista ma prawo ją używać, należy nieznacznie defnicja klasy kontrolki - konieczne jest dodanie dodatkowego atrybutu, który określa providera licencji. W .NET Framework znajduje się LicFileLicenseProvider, który sprawdza licencję w oparciu o plik LIC.
Aby sprawdzić, czy kontrolka wykorzystywana jest zgodnie z licencją, należy albo wywołać funkcję IsValid, albo skorzytsać z metody Validate. W tym drugim przypadku, gdy licencja nie zostanie znaleziona, system zgłosi wyjątek. Jeżeli taki kod będzie znajdował się w konstruktorze kontrolki - nie będzie można utworzyć instancji FormHeader. Należy tylko pamiętać, by podczas kasowania kontrolki (np. w Dispose) usunąć instancję licencji. Poniżej przedstawiony jest fragment kodu C# zmieniony w związku z obsługą licencji:
namespace FormHeader
{
[
DefaultProperty("Text"),
DefaultEvent("QMClick"),
LicenseProviderAttribute(typeof(LicFileLicenseProvider))
]
public class FormHeader : System.Windows.Forms.UserControl,IMenuItemConsumer
{
private License license = null;
public FormHeader() {
InitializeComponent();
license = LicenseManager.Validate(typeof(FormHeader), this);
}
protected override void Dispose( bool disposing ) {
if( disposing ) {
if (license != null) {
license.Dispose();
license = null;
}
if( components != null )
components.Dispose();
}
base.Dispose( disposing );
}
[...]
LicFileLicenseProvider sprawdza, czy istnieje plik <nazwaklasy>.lic w tym samym katalogu co pakiet (plik dll). nazwaklasy to pełna nazwa klasy - w tym przypadku może być to FormHeader.FormHeader. Jeżeli plik zawiera tekst postaci: „FormHeader.FormHeader is a licensed component”, to .NET uzna, że kontrolka jest licencjonowana. Warto wspomnieć, że aby zaimplementować własny system licencjonowania (np. - oparty o rejestr) należy stworzyć klasę dziedziczącą z LicenseProvider i odpowiednio skonstruować metodę GetLicense.
ROBERT NUTRYCZ