Rozdział 10. Biblioteka Windows Forms.

Adam Boduch

Z doświadczenia wiem, że początkującym programistom najwięcej frajdy sprawia wizualne projektowanie aplikacji. Umieszczanie w programie wszelkiego rodzaju przycisków, kontrolek, łączenie całości w działającą aplikację przynosi najwięcej satysfakcji. W poprzednich rozdziałach większość przykładów prezentowana była na aplikacjach konsolowych. Było to celowe działanie, gdyż użycie biblioteki wizualnej wymaga zastosowania większej ilości kodu źródłowego, konstrukcji, których wcześniej nie znałeś. Teraz, gdy nieobce jest Ci programowanie obiektowe, wiesz, czym są klasy i tablice, możemy przejść do programowania wizualnego. Odstawimy na bok aplikacje konsolowe, z których mało kto dzisiaj korzysta. Zajmiemy się budową programów na podstawie komponentów, które są w dzisiejszych czasach podstawą do budowania aplikacji okienkowych, działających w systemach Windows.

W gruncie rzeczy do tej pory powiedzieliśmy już sobie całkiem sporo o aplikacjach wizualnych. Potrafisz już tworzyć nowe projekty WinForms przy pomocy środowiska Visual C# Express Edition. W rozdziale 5. omówiłem podstawowy kod aplikacji wizualnej, a w rozdziale 6. wyjaśniłem, czym są zdarzenia. Wiesz już, czym są właściwości, w jaki sposób je modyfikować oraz jak obsługiwać zdarzenia. Brakuje Ci jedynie wiedzy praktycznej, umożliwiającej wykorzystanie podstawowych komponentów biblioteki Windows Forms.

1 Podzespół System.Windows.Forms
     1.1 Okno Object Browser
     1.2 Przestrzeń System.Windows.Forms
2 Podstawowe klasy
     2.3 System.ComponentModel.Component
     2.4 System.Windows.Forms.Control
          2.4.1 Właściwości klasy
     2.5 System.Windows.Forms.Application
3 Przykład działania
     3.6 Przygotowanie klasy
     3.7 Projektowanie interfejsu
     3.8 Rozwiązania programistyczne
4 Technika przeciągnij i upuść
5 Tworzenie menu
     5.9 Właściwości menu
     5.10 Ikony dla menu
     5.11 Skróty klawiaturowe
     5.12 Menu podręczne
6 Paski narzędziowe
7 Pasek statusu
8 Zakładki
9 Kontrolki tekstowe
     9.13 Komponent RichTextBox
10 Okna dialogowe
     10.14 Właściwości okien dialogowych
     10.15 Aplikacja — edytor tekstów
11 Tworzenie nowego formularza
12 Podsumowanie

Podzespół System.Windows.Forms

Przestrzeń nazw System.Windows.Forms znajduje się w podzespole o tej samej nazwie. Jest to podstawowa biblioteka służąca do projektowania wizualnego. Kompilując dany projekt z użyciem kompilatora csc.exe, w linii komend musisz zaznaczyć, iż ma on być kompilowany wraz z podzespołem System.Windows.Forms.dll. Jest to bardzo niewygodne, dlatego zalecam korzystanie z wizualnych środowisk typu Visual C# Express Edition. Podczas tworzenia nowego projektu WinForms odpowiednie podzespoły umożliwiające jego prawidłową kompilację są włączane automatycznie. Możesz to sprawdzić, rozwijając gałąź References w oknie Solution Explorer (rysunek 10.1).

csharp10.1.jpg
Rysunek 10.1. Okno Solution Explorer

Gdy klikniemy daną gałąź oznaczającą podzespół i wybierzemy pozycję Properties z menu podręcznego, wyświetli się lista właściwości podzespołu. Wyświetlone zostaną m.in. informacje o wersji oraz nazwie pliku danego podzespołu.

O podzespołach .NET opowiem w rozdziale 11.

Okno Object Browser

Żaden programista nie jest w stanie zapamiętać setek typów, jakie oferuje biblioteka klas platformy .NET Framework. Podejrzewam, że sami twórcy czasami muszą sięgnąć do dokumentacji, aby przypomnieć sobie nazwę danej klasy. Dlatego środowiska takie jak Visual C# Express zapewniają wsparcie, aby programowanie było jeszcze prostsze i szybsze. Skoro jesteśmy przy temacie podzespołów, to warto wspomnieć o oknie Object Browser. Umożliwia nam ono przeglądanie zawartości danych podzespołów, przestrzeni nazw i typów. Całość jest ładnie przedstawiona w sposób hierarchiczny (rysunek 10.2).

csharp10.2.jpg
Rysunek 10.2. Okno Object Browser

Okno Object Browser możemy wyświetlić, jeśli klikniemy dwukrotnie nazwę podzespołu w gałęzi References lub przejdziemy do menu View/Other Windows/Object Browser.

Przestrzeń System.Windows.Forms

Najważniejszą przestrzenią nazw w podzespole System.Windows.Forms.dll jest ta o takiej samej nazwie (czyli System.Windows.Forms), włączana do każdego projektu typu WinForms. Zawiera ona podstawowe klasy obsługi aplikacji, ale również wszystkie klasy komponentów. Musisz zdać sobie sprawę, że każdy komponent jest jednocześnie klasą, taką samą, jaką tworzyliśmy w rozdziale 5. Taka klasa zawiera elementy, o których wspominałem już niejednokrotnie w tej książce, czyli właściwości, metody, zdarzenia, indeksery, typy wyliczeniowe czy struktury. Ogólnie, klasy biblioteki WinForms można zakwalifikować do kilku kategorii, które omówiłem w tabeli 10.1.

Tabela 10.1. Kategorie klasy przestrzeni System.Windows.Forms

GrupaOpis
KomponentyO komponentach potocznie mówi się jak o kontrolkach wizualnych. W rzeczywistości można powiedzieć, że są to niewidoczne elementy programu, „klocki” działające w tle, które mogą mieć duży wpływ na działanie aplikacji, lecz nie są częścią jej interfejsu.
KontrolkiKontrolki to inaczej komponenty wizualne. Dzięki nim możesz tworzyć interfejs swojego programu. Do tej kategorii można zaliczyć przyciski, listy rozwijane, pola edycyjne itp.
Elementy pozycjonowaniaBiblioteka WinForms posiada kilka komponentów, które umożliwiają zarządzanie elementami interfejsu. Jest to np. komponent umożliwiający tworzenie zakładek czy komponent Panel, który służy tylko do tego, aby grupować „w sobie” inne kontrolki.
Menu i paski narzędzioweW programach wizualnych często zachodzi potrzeba utworzenia menu czy pasków narzędziowych. W bibliotece WinForms mamy do dyspozycji kilka komponentów, dzięki którym jesteśmy w stanie zrealizować to zadanie.
Okna dialogoweSą to ukryte komponenty, które odpowiadają za wyświetlanie okien dialogowych obecnych w wielu aplikacjach biurowych (np. Otwórz, Zapisz jako…, Drukuj itp.).

Podstawowe klasy

Oczywiście cała biblioteka WinForms zbudowana jest na mechanizmie dziedziczenia oraz polimorfizmu (jak mogłoby być inaczej?), toteż istnieje pewien zestaw klas bazowych dla wszystkich komponentów, o których powinieneś wiedzieć. Nie jest to wiedza niezbędna w pracy nad komponentami, ale być może ciekawostka dla osób, które chcą wiedzieć, jak to wszystko jest zbudowane.

System.ComponentModel.Component

Klasa — o dziwo — nie znajduje się w przestrzeni System.Windows.Forms, lecz w System.ComponentModel. Jest to podstawowa klasa dla wszystkich komponentów z biblioteki WinForms, implementuje interfejs IComponent.

Właściwie nigdy nie będziesz miał potrzeby używania tej klasy samodzielnie, służy ona jedynie jako klasa bazowa. Zawiera kilka elementów, m.in. metodę Events() zwracającą listę zdarzeń przypisaną do danego komponentu.

System.Windows.Forms.Control

W przestrzeni nazw System.Windows.Forms znajduje się klasa Control, która jest bazowa dla każdej wizualnej kontrolki biblioteki WinForms. Co oznacza, że kontrolka jest wizualna? Przede wszystkim kontrolki tego typu tworzą interfejs aplikacji, są widoczne podczas jej działania. Dodatkowo mają one możliwość reagowania na zdarzenia użytkownika, takie jak naciskanie klawiszy klawiatury czy myszy.

Właściwości klasy

Klasa Control zawiera cale mnóstwo właściwości publicznych oraz chronionych, które dziedziczone są w klasach potomnych. Właściwości te służą do określania podstawowych cech komponentów, takich jak położenie, rozmiar, kolor itp. W tabeli 10.2 zawarłem podstawową listę właściwości, które z pewnością będą przez Ciebie wykorzystywane najczęściej.

Tabela 10.2. Podstawowa lista właściwości klasy Control

WłaściwośćOpis
`AllowDrop`Właściwość typu bool określa, czy dany komponent może być rodzicem dla innych, tzn. czy za pomocą techniki „przeciągnij i upuść” (ang. drag & drop) można w nim umieszczać inne kontrolki.
`Anchor`Określa, w jaki sposób komponent ma zmieniać swoje rozmiary, jeżeli kontrolka znajdująca się w nim zmienia swój rozmiar.
`AutoSize`Właściwość typu `bool` określa, czy komponent ma mieć rozmiar zgodny z wymiarami treści, która się w nim znajduje.
`BackColor`Umożliwia określenie koloru tła dla komponentu.
`BackgroundImage`Określa obrazek, który będzie używany jako tło komponentu.
`Cursor`Określa kursor myszy, jaki ma być wyświetlany, jeżeli jej wskaźnik zostanie naprowadzony nad daną kontrolkę.
`Enabled` Właściwość typu `bool` określa, czy komponent będzie wyłączony (nieaktywna kontrolka jest specjalnie oznaczana, nie reaguje na zdarzenia).
`Font`Umożliwia ustawienie czcionki, jaka będzie wyświetlana, jeżeli kontrolka umożliwia wyświetlanie tekstu.
`ForeColor`Określa kolor tekstu dla komponentu.
`HasChildren`Określa, czy w komponencie znajdują się inne kontrolki (tzw. kontrolki potomne).
`Height`Umożliwia odczytanie lub przypisanie szerokości kontrolki.
`Left`Umożliwia określenie (w pikselach) odległości lewej krawędzi od obszaru formularza lub innego komponentu.
`Location`Właściwość typu `Point` umożliwia określenie położenia dla komponentu.
`Name`Bardzo ważna właściwość. Określa nazwę komponentu.
`Padding`Umożliwia określenie lub odczytanie odstępów (minimalnej odległości) wewnątrz danej kontrolki.
`Parent`Umożliwia odczytanie lub przypisanie kontrolki macierzystej dla danego komponentu.
`Right`Określa odstęp (w pikselach) od prawej krawędzi obszaru formularza lub innego komponentu.
`Size`Właściwość typu `Point` umożliwia określenie rozmiaru kontrolki.
`TabIndex`Umożliwia przypisanie lub odczytanie kolejności przechodzenia do danego komponentu przy pomocy klawisza Tab.
`Tag`Właściwość może być wykorzystana do dowolnych celów. Nie robi nic konkretnego, może przechowywać dowolne wartości.
`Top`Określa odległość (w pikselach) od górnej krawędzi formularza lub komponentu.
`Visible`Właściwość typu bool umożliwia określenie, czy komponent będzie widoczny, czy też nie.
`Width`Umożliwia odczytanie lub nadanie szerokości dla komponentu.

Oczywiście chciałbym zaznaczyć, że to tylko niektóre z właściwości. Starałem się zamieścić w tabeli komponenty, które są w większości wspólne dla wszystkich kontrolek wizualnych.

Nie wszystkie właściwości prezentowane w tabeli 10.2 muszą być widoczne na liście okna Properties. Niektóre mogą być dostępne jedynie z poziomu kodu programu.

System.Windows.Forms.Application

Klasa Application ma duże znaczenie dla naszego programu. Zawiera bowiem bardzo ważną metodę odpowiedzialną za wyświetlenie głównego formularza projektu.

Wykonajmy małe ćwiczenie pozwalające napisać najprostszy program, który będzie wykorzystywał bibliotekę WinForms. Nasza aplikacja będzie wyświetlała jedynie puste okno formularza.

Formularzem nazywamy okno systemu Windows.

#Z menu File wybierz pozycję New Project.
#Zaznacz pozycję Empty project i naciśnij przycisk OK.
#W oknie Solution Explorer zaznacz, a następnie kliknij prawym przyciskiem pozycję References.
#Kliknij Add Reference.
#W oknie, w zakładce .NET odszukaj pozycję System, a następnie kliknij przycisk OK.
#W ten sam sposób dodaj pozycję System.Windows.Forms.
#Z menu Project wybierz pozycję Add New Item.
#Zaznacz pozycję Empty file i kliknij OK.

Utworzyliśmy w ten sposób nowy projekt, ręcznie dodaliśmy odwołania do odpowiednich podzespołów (rysunek 10.3). Mamy przed sobą pustą zawartość pliku źródłowego.

csharp10.3.jpg
Rysunek 10.3. Okno Solution Explorer z gałęzią prezentującą odwołania

Kod odpowiedzialny za utworzenie nowego formularza prezentuje listing 10.1.

Listing 10.1. Kod odpowiedzialny za wyświetlenie nowego formularza

using System;
using System.Windows.Forms;

public class WinForms : Form
{
    static void Main()
    {
        Application.Run(new WinForms());
    }
}

Jak widzisz, kod jest prosty i krótki. Wywołujemy metodę Run(), która jako parametru wymaga instancji klasy Form. Klasa Form jest podstawową klasą obsługi formularzy, dlatego WinForms musi po niej dziedziczyć.

Jeżeli kod z listingu 10.1 oprócz okna formularza wyświetla również okno konsoli, musisz sprecyzować typ projektu. Z menu Project wybierz Properties. W zakładce Application znajduje się lista rozwijana Output type. Wybierz z niej pozycję Windows Application.

Klasa Application ma wiele metod oraz właściwości odpowiadających za obsługę tzw. komunikatów oraz wątków. Ze względu na ograniczenia objętości tej książki, te tematy nie zostaną zaprezentowane. Klasa zawiera jednak dwie metody, które mogą Ci się przydać w dalszej pracy z językiem C#. Mam tu na myśli metodę Exit(), umożliwiającą zakończenie pracy oraz Restart(), która ponownie uruchamia aplikację. Rozbudujmy nasz program z listingu 10.1, dodając do niego przyciski umożliwiające zamknięcie oraz ponowne uruchomienie aplikacji.

Aby zrealizować to zadanie, musimy umieścić w programie kod tworzący dwa komponenty Button oraz przypisać im odpowiednie zdarzenia. Oczywiście moglibyśmy to prosto zrobić w trybie projektowania w środowisku Visual C#. Aby jednak utrudnić sobie to zadanie, wpiszmy odpowiedni kod ręcznie.

Utworzenie nowego komponentu na formularzu polega zwyczajnie na utworzeniu nowej instancji (np. komponentu Button) oraz ustawieniu odpowiednich właściwości:

RestartBtn = new Button();
RestartBtn.Parent = this;
RestartBtn.Top = 10;
RestartBtn.Left = 10;
RestartBtn.Text = "Restartuj";
RestartBtn.Click += new System.EventHandler(OnRestareClick);

Kluczowe w tym kodzie jest przypisanie odpowiedniej wartości dla właściwości Parent. Musimy określić komponent macierzysty dla naszej kontrolki (będzie to formularz), co prezentuje listing 10.2.

Listing 10.2. Dynamiczne tworzenie komponentów na formularzu

using System;
using System.Windows.Forms;

public class MyForm : Form
{
    private Button RestartBtn;
    private Button ExitBtn;

    private void OnRestareClick(object sender, EventArgs e)
    {
        Application.Restart();
    }

    private void OnExitClick(object sender, EventArgs e)
    {
        Application.Exit();
    }

    public MyForm()
    {
        this.Text = Application.StartupPath;

        RestartBtn = new Button();
        RestartBtn.Parent = this;
        RestartBtn.Top = 10;
        RestartBtn.Left = 10;
        RestartBtn.Text = "Restartuj";
        RestartBtn.Click += new System.EventHandler(OnRestareClick);

        ExitBtn = new Button();
        ExitBtn.Parent = this;
        ExitBtn.Top = 10;
        ExitBtn.Left = 120;
        ExitBtn.Text = "Zamknij";
        ExitBtn.Click += new System.EventHandler(OnExitClick);        
    }
}

public class WinForms
{
    static void Main()
    {
        Application.Run(new MyForm());
    }
}

Rysunek 10.4 prezentuje działanie takiego programu. Zwróć uwagę, że pasek tytułowy formularza zawiera ścieżkę do uruchomionego programu. Odpowiada za to linia:

this.Text = Application.StartupPath;

Właściwość StartupPath zwraca pełną ścieżkę do programu.

W klasie Application znajduje się podobna właściwość — ExecutablePath — która również wyświetla ścieżkę do programu, włączając w to jego nazwę.

csharp10.4.jpg
Rysunek 10.4. Działanie aplikacji WinForms

Przykład działania

Zostawmy na chwilę suchą teorię, a zajmijmy się tworzeniem graficznego interfejsu użytkownika (GUI — ang. Graphical User Interface). Pamiętasz, jak w rozdziale 6. pisaliśmy grę „Kółko i krzyżyk”? Wówczas cała gra odbywała się w trybie konsolowym. Ponieważ całe engine gry jest już gotowe, wystarczy zaprojektować interfejs, który obsługiwałby jej klasę. Do roboty!

Przygotowanie klasy

Utwórz nowy projekt aplikacji Windows Forms. Zapisz go na dysku, po czym skopiuj moduł, w którym znajdowała się klasa GomokuEngine, do katalogu z projektem. Należy dodać ten moduł do projektu:

#Prawym przyciskiem kliknij w obrębie okna Solution Explorer.
#Wybierz pozycję Add/Existing Item.
#Wskaż plik, w którym znajduje się kod gry, i naciśnij OK.

Projektowanie interfejsu

Zacznijmy od tytułu okna głównego. Domyślny napis Form1 nie odpowiada nazwie naszej gry. W oknie Properties możesz zmienić wartość właściwości Text, wpisując własną nazwę okna — np. Gomoku Win.

Jeżeli okno Properties nie wyświetla prawidłowo właściwości formularza, być może nie jest on zaznaczony. Kliknij więc okno formularza, aby wyświetlić listę jego właściwości.

Kolejna rzecz, jakiej potrzebujemy, to plansza do gry. W celu jej utworzenia proponuję umieścić na formularzu 9 komponentów typu Button. Na razie umieść jeden komponent, gdyż musimy ustalić właściwości jego wyglądu. Zacznijmy od rozmiaru. W oknie Properties odnajdź właściwość Size i rozwiń jej gałąź. Wartość Width zmień na 50, a Height na 30. Teraz przyszedł czas na zmianę czcionki. Rozwiń gałąź Font i zmień następujące pozycje:

*Name (nazwa czcionki) — Arial Black;
*Size (rozmiar czcionki) — 14.

Teraz — używając kombinacji klawiszy Ctrl+C (kopiuj) oraz Ctrl+V (wklej) — możesz dziewięć razy wkleić skopiowany wcześniej komponent Button. Oszczędzi Ci to czasu na modyfikację właściwości kolejnych przycisków.

Potrzebujemy jeszcze dwóch kontrolek tekstowych, w których użytkownicy będą mogli wpisać swe imiona. Moja wersja interfejsu użytkownika zaprezentowana została na rysunku 10.5.

csharp10.5.jpg
Rysunek 10.5. Interfejs gry

Ramkę otaczającą podstawowe elementy uzyskałem, umieszczając na formularzu komponent GroupBox. Dodatkowo ustawiłem właściwość Text tego komponentu na wartość pustą.

Etykiety, w których znajdują się napisy Gracz #1 oraz Gracz #2, to komponenty Label — ich jedynym zadaniem jest przechowywanie krótkich tekstów.

Rozwiązania programistyczne

W każdym projekcie tworzenie interfejsu aplikacji to dopiero początek. Naszym zadaniem jest odpowiednia obsługa klasy Gomoku, gdyż cały „silnik” gry mamy już gotowy. Zacznijmy od rzeczy najprostszej, czyli oprogramowania zdarzenia Click komponentu btnStart (start gry):

private void btnStart_Click(object sender, EventArgs e)
{
    if (textPlayer1.Text.Length > 0 && textPlayer2.Text.Length > 0)
    {
        GomokuObj.Player1 = textPlayer1.Text;
        GomokuObj.Player2 = textPlayer2.Text;

        GomokuObj.Start();

        Ready = true;
    }
}

W instrukcji warunkowej sprawdzamy, czy użytkownik wpisał imiona graczy. Następnie imiona przypisujemy do odpowiednich właściwości klasy, po czym wywołujemy metodę Start(). Na samym końcu znajduje się instrukcja zmieniająca wartość pola Ready na true. To pole jest używane na wewnętrzny potrzeby naszego programu (wykorzystywane jest w dalszej części kodu).

Zakładam, że nie masz problemu z generowaniem i obsługą zdarzeń. Gdybyś jednak miał — odsyłam do wcześniejszych fragmentów książki.

Teraz pomyślmy chwilę nad planszą do gry. Mamy 9 przycisków, których zdarzenia trzeba oprogramować. Ale żeby nie generować 9 procedur zdarzeniowych, wygenerujemy jedną i przypiszemy ją do pozostałych przycisków.

Skoro jedna metoda będzie obsługiwać 9 przycisków, to skąd mamy wiedzieć, który przycisk został przez użytkownika wciśnięty. Po lekturze poprzednich rozdziałów powinieneś się domyślić, że wykorzystamy do tego parametr sender. Pamiętaj jednak, że do metody Set() klasy Gomoku musimy przekazać współrzędne pola (przycisku), które wybrał użytkownik. Do tego wykorzystamy właściwość Tag, która obecna jest we wszystkich komponentach.

Właściwość Tag typu System.Object może przechowywać dowolne dane na użytek programu. W naszym przypadku będzie przechowywać „współrzędne” danego przycisku. Otwórz plik przechowujący ustawienia formularza (u mnie jest to Form1.Designer.cs) i odnajdź kod, który odpowiedzialny jest za utworzenie przycisków:

this.button1.Font = new System.Drawing.Font("Arial Black", 14F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(238)));
this.button1.Location = new System.Drawing.Point(12, 12);
this.button1.Name = "button1";
this.button1.Size = new System.Drawing.Size(50, 30);
this.button1.TabIndex = 0;
this.button1.TextAlign = System.Drawing.ContentAlignment.TopCenter;
this.button1.UseVisualStyleBackColor = true;
this.button1.Click += new System.EventHandler(this.button1_Click);
this.button1.Tag = new System.Drawing.Point(1, 1);

Pogrubiona linia oznacza przypisanie wartości do właściwości Tag. Wpisałem ją ręcznie. Zrób tak z każdym przyciskiem, zmieniając jednak współrzędne, czyli następnie będzie to: (1, 2), (1, 3), (2, 1), (2, 2) itd.

Oto kod metody zdarzeniowej dla komponentów typu Button:

private void button1_Click(object sender, EventArgs e)
{
    if (!Ready)
    {
        MessageBox.Show("Podaj imiona graczy!");
        return;
    }
    Point p = new Point();
    p = (Point)(sender as Button).Tag;

    (sender as Button).Text = GomokuObj.Active.Type == FieldType.ftCircle ? "O" : "X";

    GomokuObj.Set(p.X, p.Y);
    
    if (GomokuObj.Winner)
    {
        MessageBox.Show(String.Format("Brawo dla {0}", GomokuObj.Active.Name), "Wygrana!");
    }            
}

Pierwsza instrukcja sprawdza, czy pole Ready ma wartość false. Jeżeli tak, wyświetla ostrzeżenie, iż nie podano imion graczy. Następnie tworzymy nową strukturę Point i przypisujemy do niej informacje z właściwości Tag naciśniętego przycisku. Kolejna instrukcja umieszcza na przycisku symbol odpowiadający danemu graczowi, po czym wywołuje metodę Set() z odpowiednimi parametrami. Na samym końcu instrukcja sprawdza, czy nie zakończyć gry, jeżeli któryś z użytkowników wygrał.

Omówienia wymaga również metoda zdarzeniowa przycisku btnNewGame:

private void btnNewGame_Click(object sender, EventArgs e)
{
    for (int i = 0; i < Controls.Count; i++)
    {
        if (Controls[i] is Button && (Controls[i] as Button).Tag != null)
        {
            (Controls[i] as Button).Text = "";
        }
    }
    GomokuObj.NewGame();
}

Jej zadaniem jest wywołanie metody NewGame(), lecz wcześniej czyści wartości Text przycisków planszy. Właściwość Controls zwraca informacje na temat kontrolek umieszczonych — w tym przypadku — na formularzu. W pętli sprawdzamy każdą kontrolkę i jeżeli jest typu Button oraz jej właściwość Tag nie jest pusta, czyścimy zawartość jej właściwości Text. Kod źródłowy gry (głównego modułu aplikacji) zawarty jest na listingu 10.3.
Rysunek 10.6 prezentuje grę w trakcie działania.

Listing 10.3. Kod źródłowy gry Gomoku

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;

namespace Gomoku
{
    public partial class Form1 : Form
    {
        private GomokuEngine GomokuObj;
        private bool Ready = false;

        public Form1()
        {
            InitializeComponent();
        }

        private void Form1_Load(object sender, EventArgs e)
        {
            GomokuObj = new GomokuEngine();                       
        }

        private void btnStart_Click(object sender, EventArgs e)
        {
            if (textPlayer1.Text.Length > 0 && textPlayer2.Text.Length > 0)
            {
                GomokuObj.Player1 = textPlayer1.Text;
                GomokuObj.Player2 = textPlayer2.Text;

                GomokuObj.Start();

                Ready = true;
            }
        }

        private void btnNewGame_Click(object sender, EventArgs e)
        {
            for (int i = 0; i < Controls.Count; i++)
            {
                if (Controls[i] is Button && (Controls[i] as Button).Tag != null)
                {
                    (Controls[i] as Button).Text = "";
                }
            }
            GomokuObj.NewGame();
        }

        private void button1_Click(object sender, EventArgs e)
        {
            if (!Ready)
            {
                MessageBox.Show("Podaj imiona graczy!");
                return;
            }
            Point p = new Point();
            p = (Point)(sender as Button).Tag;

            (sender as Button).Text = GomokuObj.Active.Type == FieldType.ftCircle ? "O" : "X";

            GomokuObj.Set(p.X, p.Y);
            
            if (GomokuObj.Winner)
            {
                MessageBox.Show(String.Format("Brawo dla {0}", GomokuObj.Active.Name), "Wygrana!");
            }            
        }
    }
}

csharp10.6.jpg
Rysunek 10.6. Gra Gomoku w trakcie działania

Technika przeciągnij i upuść

Technika „przeciągnij i upuść” (ang. Drag & Drop) z powodzeniem stosowana jest w wielu programach komputerowych. Sam z pewnością nieraz z niej skorzystałeś. Przykładowo: przenoszenie plików za pomocą kliknięcia i przesuwania kursora myszy jest już wykorzystaniem techniki przeciągnij i upuść! Dzięki temu obsługa aplikacji jest przyjemniejsza i bardziej intuicyjna. Ponieważ obsługa tej techniki w środowisku .NET Framework nie należy do najprostszych, postanowiłem zaprezentować przykłady jej wykorzystania.

W naszej przykładowej aplikacji użytkownik będzie miał możliwość przenoszenia elementów pomiędzy dwoma komponentami ListBox. Kontrolka ListBox służy do przechowywania kolejnych linii tekstu (pozycji). Rysunek 10.7 prezentuje działanie takiej aplikacji.

csharp10.7.jpg
Rysunek 10.7. Aplikacja prezentująca działanie techniki przeciągnij i upuść

Pierwsze, co musimy zrobić, to zmienić wartość właściwości AllowDrop na true w obu kontrolkach ListBox. Właściwość ta określa, czy dany komponent może obsługiwać dane przychodzące przy pomocy techniki drag & drop.

Ponieważ zależy nam na tym, aby można było dowolnie przemieszczać dane, procedury zdarzeniowe będą takie same dla obydwu kontrolek. Zacznijmy od oprogramowania zdarzenia MouseDown, które występuje w momencie kliknięcia klawiszem myszy w obrębie kontrolki:

private void lb_MouseDown(object sender, MouseEventArgs e)
{
    DragDropEffects effects = 
        (sender as ListBox).DoDragDrop((sender as ListBox).SelectedItem, DragDropEffects.Move);


    if (effects != DragDropEffects.None)
    {
        (sender as ListBox).Items.RemoveAt((sender as ListBox).SelectedIndex);
    }
}

Najważniejsza w tym kodzie jest metoda DoDragDrop(), która uruchamia proces przeciągania danych. Pierwszym parametrem są dane, które będą przeciągane, czyli w naszym przypadku — zaznaczony element w ListBox. Drugim parametrem jest tryb przenoszenia. Metoda zwraca obiekt klasy DragDropEffects.

Sami musimy zadbać o usuwanie elementu z komponentu źródłowego. Realizuje to metoda RemoveAt() z właściwości Items. W parametrze tej metody musimy podać numer pozycji do usunięcia. Usuwamy zaznaczoną pozycję, więc skorzystamy z właściwości SelectedIndex, która dostarcza nam informacji o zaznaczonej pozycji.

Właściwość Items komponentu typu ListBox zwraca listę pozycji (elementów) komponentów w formie kolekcji.

Kolejnym krokiem będzie obsłużenie zdarzenia DragEnter, które wykonywane jest w momencie, gdy kursor myszy z przeciąganym elementem wejdzie w obszar kontrolki:

private void lb_DragEnter(object sender, DragEventArgs e)
{
    StatusBar.Text = "Przesyłam dane...";

    if (e.Data.GetDataPresent(typeof(System.String)))
    {
        e.Effect = DragDropEffects.Move;
    }      
}

Parametr e tej procedury zdarzeniowej zawiera informacje o przesyłanych danych.
Akceptujemy jedynie dane typu System.String, więc musimy upewnić się, czy mamy do czynienia z prawidłowym typem.

Operator typeof zwraca informacje o typie danych w postaci obiektu System.Type.

Dodatkowo umieszczamy na pasku statusu tekstową informację o wykonywanych czynnościach. Skorzystałem z komponentu StatusStrip, do którego dodałem etykietę (rysunek 10.8) i nazwałem ją StatusBar.

csharp10.8.jpg
Rysunek 10.8. Dodawanie etykiety w komponencie StatusStrip

Ostatnie zdarzenie, które musimy obsłużyć, to DragDrop, wykonywane w momencie zwolnienia klawisza myszy. Procedura zdarzeniowa musi zawierać kod informujący aplikację o tym, co zrobić z dostarczonymi danymi:

private void lb_DragDrop(object sender, DragEventArgs e)
{
  if (e.Data.GetDataPresent(typeof(System.String)))
  {
    Object item = (object)e.Data.GetData(typeof(System.String));

    (sender as ListBox).Items.Add(item);
    StatusBar.Text = "";
  }
}

Pierwsza instrukcja służy do sprawdzenia poprawności danych, podobnie jak w procedurze zdarzeniowej lb_DragEnter(). Następnie odczytujemy dane i przypisujemy je do obiektu item, którego zawartość wstawiania jest do listy w komponencie typu ListBox. Listing 10.4 zawiera kod źródłowy aplikacji.

Listing 10.4. Przykładowy program korzystający z techniki Drag & Drop

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;

namespace DockApp
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        private void btnAdd_Click(object sender, EventArgs e)
        {
            int selectedItem = lbSource.SelectedIndex;
            object item = lbSource.SelectedItem;

            if (selectedItem > 0)
            {
                lbSource.Items.RemoveAt(selectedItem);
                lbDest.Items.Add(item);
            }
        }

        private void btnRemove_Click(object sender, EventArgs e)
        {
            int selectedItem = lbDest.SelectedIndex;
            object item = lbDest.SelectedItem;

            if (selectedItem > 0)
            {
                lbDest.Items.RemoveAt(selectedItem);
                lbSource.Items.Add(item);
            }
        }

        private void lb_DragEnter(object sender, DragEventArgs e)
        {
            StatusBar.Text = "Przesyłam dane...";

            if (e.Data.GetDataPresent(typeof(System.String)))
            {
                e.Effect = DragDropEffects.Move;
            }      
        }

        private void lb_DragLeave(object sender, EventArgs e)
        {
            StatusBar.Text = "";
        }

        private void lb_MouseDown(object sender, MouseEventArgs e)
        {
            DragDropEffects effects = 
                (sender as ListBox).DoDragDrop((sender as ListBox).SelectedItem, DragDropEffects.Move);


            if (effects != DragDropEffects.None)
            {
                (sender as ListBox).Items.RemoveAt((sender as ListBox).SelectedIndex);
            }
        }

        private void lb_DragDrop(object sender, DragEventArgs e)
        {
            if (e.Data.GetDataPresent(typeof(System.String)))
            {
                Object item = (object)e.Data.GetData(typeof(System.String));

                (sender as ListBox).Items.Add(item);
                StatusBar.Text = "";
            }
        }
    }
}

Tworzenie menu

W wielu aplikacjach, nie tylko tych przeznaczonych dla systemu Windows, obecne jest menu główne. Nie muszę Ci chyba tłumaczyć, czym jest menu, ale warto wspomnieć o tym, jak się je tworzy w środowisku Visual C# Express Edition. Wbrew pozorom, w środowisku wizualnym projektowanie menu jest bardzo proste i przyjemne.

Za wyświetlanie i obsługę menu odpowiada komponent MenuStrip. Umieść ten komponent na formularzu. Zostanie on umieszczony na pasku podręcznym (rysunek 10.9), gdyż należy do komponentów niewizualnych.

csharp10.9.jpg
Rysunek 10.9. Komponent MenuStrip umieszczony na pasku podręcznym

Po zaznaczeniu tego komponentu u góry formularza wyświetlona zostanie pozycja z napisem Type here, służąca do wpisania etykiety pierwszego menu (rysunek 10.10).

csharp10.10.jpg
Rysunek 10.10. Projektowanie menu

Po kliknięciu tej pozycji kursor zostanie zmieniony, co da nam możliwość wpisania etykiety. Po naciśnięciu klawisza Enter środowisko da nam możliwość dodania kolejnego elementu menu.

Rysunek 10.11 prezentuje moją propozycję menu, które zawiera podstawowe elementy każdej aplikacji biurowej (menu Plik, Edycja, Pomoc).

csharp10.11.jpg
Rysunek 10.11. Menu w trakcie edycji

Separator w menu można utworzyć, wpisując znak -.

Właściwości menu

Menu możesz również edytować, jeśli klikniesz prawym przyciskiem ikonę komponentu i wybierzesz pozycję Edit Items. Okno Items Collection Editor daje możliwość określenia szczegółowych właściwości dla menu, z których najważniejsze opisałem w tabeli 10.3.

Tabela 10.3. Właściwości pozycji menu

WłaściwośćOpis
BackColorUmożliwia określenie koloru tła pozycji menu.
BackgroundImageUmożliwia określenie obrazka tła.
BackgroundImageLayoutOkreśla pozycję i zachowanie obrazka tła.
CheckedZmiana tej właściwości na true spowoduje pojawienie się przy pozycji zaznaczenia (charakterystycznego „ptaszka”).
DisplayStyleOkreśla sposób wyświetlania pozycji (tekst i obrazek, tekst lub obrazek).
ImageUmożliwia określenie ikony, która będzie wyświetlana w pozycji.
ImageAlignOkreśla położenie obrazka.
ImageTransparentUmożliwia określenie koloru tła obrazka (jeżeli jest przezroczysty).
RightToLeftOkreśla, czy pozycje w menu będą wyrównane do prawej.
TextAlignUmożliwia szczegółowe określenie pozycji tekstu w menu.
TextDirectionOkreśla kierunek wyświetlania tekstu (domyślnie poziomy).
TextImageRelationUmożliwia określenie położenia tekstu względem obrazka.
AutoSizeDopasowanie rozmiaru elementu do jego zawartości.
AutoToolTipAutomatyczne wyświetlanie dymków podpowiedzi po naprowadzeniu kursora nad pozycję.
CheckOnClickKliknięta pozycja zostanie zaznaczona, jeżeli wartość tej właściwości to true.
ToolTipTextZawartość dymka podpowiedzi.
DropDownItemsUmożliwia edycję elementów podmenu.
ShortcutKeysUmożliwia określenie skrótu klawiaturowego dla elementu.
ShowShortcutKeysOkreśla, czy obok pozycji menu wyświetlać skrót klawiaturowy (jeżeli został określony).

Ikony dla menu

Aby nasze menu było bardziej intuicyjne i ładne, dodamy do niego ikony, które będą zdobić jego pozycje. Obrazek może być zapisany w formacie *.bmp, *.gif, *.jpeg lub *.png.

Nowy obrazek możemy dodać bardzo prosto, korzystając z właściwości Image. Po jej zaznaczeniu wyświetlona zostanie mała ikona — gdy ją naciśniemy otwarte zostanie okno Select Resource (rysunek 10.12).

csharp10.12.jpg
Rysunek 10.12. Okno Select Resource

Zaznacz pozycję Project resource file, a następnie kliknij przycisk Import. Zostaniesz poproszony o wskazanie pliku z obrazem, który zostanie dodany do listy. Po naciśnięciu OK dany obrazek zostanie przypisany do pozycji menu.

Prostszym rozwiązaniem będzie skorzystanie z opcji podręcznego menu. Po kliknięciu danej pozycji menu w trakcie projektowania wybierz pozycję Set Image, która również wyświetli okno Select Resource (rysunek 10.13).

csharp10.13.jpg
Rysunek 10.13. Opcje podręcznego menu w trakcie projektowania

Obrazki przypisane do menu będą zapisane w tzw. zasobach programu. Zasoby (takie jak np. obrazy, teksty, dźwięki) są dołączane do pliku wykonywalnego w trakcie kompilacji. Nie ma więc potrzeby dołączania do aplikacji dodatkowych plików graficznych czy dźwiękowych. Informacje o zasobach są przechowywane w formacie XML, w pliku Resources.resx, który znajduje się w podkatalogu Properties Twojego projektu.

Jeżeli zajdzie taka potrzeba, możesz odczytać informacje związane z obrazkami przypisanymi do menu. Oczywiście przy pomocy właściwości, które mogą zostać wyświetlone w oknie Properties. W trakcie projektowania wybierz dla przykładu menu Plik/Nowy (tak jak to pokazałem na rysunku 10.13), kliknij go prawym przyciskiem myszy i wybierz pozycję Properties.

W oknie Properties, znajduje się właściwość Image (to już wiesz), której pozycje mogą zostać rozwinięte. Uzyskasz wówczas informacje o ikonie przypisanej do danego menu (rysunek 10.14).

csharp10.14.jpg
Rysunek 10.14. Informacje o ikonie przypisanej do pozycji menu

Pozycje w oknie Properties oznaczone kolorem szarym przeznaczone są jedynie do odczytu, nie można ich modyfikować.

Skróty klawiaturowe

Aby nadać naszemu menu bardziej profesjonalny charakter, należy niektórym pozycjom przypisać skróty klawiaturowe. Umożliwi to szybkie wybranie danej pozycji przy pomocy klawiatury, co znacznie ułatwi pracę z aplikacją. Do ustawiania skrótu klawiaturowego służy właściwość ShortcutKeys. Dzięki niej możemy wybrać dowolną kombinację klawiaturową, która spowoduje wybranie danej pozycji (rysunek 10.15).

csharp10.15.jpg
Rysunek 10.15. Ustawianie skrótu klawiaturowego

Właściwość ShowShortcutKeys określa, czy skrót będzie wyświetlony obok etykiety pozycji w menu. Zalecane jest pozostawienie wartości domyślnej (czyli true).

Inną zalecaną techniką jest umieszczenie w etykiecie znaku &. Przykładowo, zaznacz menu Plik i przejdź do właściwości Text. Zmień jej wartość na &Plik. Spowoduje to dodanie pod literą P charakterystycznego podkreślenia. Dzięki temu użytkownik będzie mógł przejść do tej pozycji menu, używając skrótu Lewy Alt+P.

Menu podręczne

Opcje menu podręcznego wyświetlane są w momencie, gdy użytkownik kliknie prawym przyciskiem myszy w obrębie danego elementu. Do tworzenia menu podręcznego służy komponent ContextMenuStrip. Pozycje tego menu tworzone są na takiej samej zasadzie jak we wspomnianym komponencie MenuStrip.

Jedyne, o czym warto wspomnieć, to przypisywanie menu podręcznego do danego elementu. Większość kontrolek wizualnych posiada rozwijaną właściwość ContextMenuStrip, która służy do kojarzenia danego elementu z menu podręcznym. Jeżeli więc chcesz, aby menu podręczne było wyświetlane po kliknięciu prawym przyciskiem myszy w obrębie formularza, zaznacz formularz i odszukaj właściwość ContextMenuStrip. Z listy wybierz menu, które ma obsługiwać formularz.

Paski narzędziowe

Paski narzędziowe (ang. tool bar) to również często spotykany element interfejsu aplikacji. Zawiera przyciski z najczęstszymi opcjami programu ozdobione ikoną. Podczas projektowania aplikacji w menu powinieneś umieszczać wszystkie opcje, jakie oferuje program, a na pasku jedynie te najczęściej używane.

W bibliotece WinForms do tworzenia pasków narzędziowych możemy wykorzystać komponent ToolStrip. Umieszczony na formularzu, automatycznie dopasowuje się do jego górnej krawędzi. Zwróć uwagę na małą ikonę wyświetlaną na pasku w trybie projektowania (rysunek 10.16).

csharp10.16.jpg
Rysunek 10.16. Pasek narzędziowy w trybie projektowania

Rozwinięcie jego pozycji wyświetla spis komponentów, które możemy umieścić na pasku. Wybór jest na tyle spory, że powinien zaspokoić Twoje wymagania. Najczęściej bowiem na pasku umieszczamy przyciski (Button) oraz separatory (Separator).

Przypisywanie obrazków do przycisków wygląda tak samo jak w przypadku menu. Po kliknięciu przycisku prawym klawiszem myszy należy wybrać pozycję Set Image.

Projektowanie pasków narzędziowych i menu jest podobne, a to za sprawą właściwości, które są bardzo zbliżone w obu przypadkach. Wierzę, że poradzisz sobie z zaprojektowaniem menu pasującego do aplikacji, więc postanowiłem pominąć ponowne opisywanie właściwości komponentu ToolStrip.

Pasek statusu

Pasek statusu również często gości we wszelkiego rodzaju aplikacjach okienkowych. Jest to pasek, który automatycznie dopasowuje się do dolnej krawędzi okna i służy do wyświetlania podstawowych informacji w trakcie działania programu.

W Windows Forms za wyświetlanie paska statusu odpowiada komponent StatusStrip. Po jego wyświetleniu, podobnie jak w przypadku pasków narzędziowych, możemy umieścić na nim wiele elementów, w tym etykietę czy pasek postępu (rysunek 10.17).

csharp10.17.jpg
Rysunek 10.17. Pasek statusu w trakcie projektowania

Oczywiście najczęstszym zastosowaniem paska jest wyświetlanie informacji tekstowych, stąd pewnie pozycja StatusLabel będzie tą, którą będziesz wybierać najczęściej.

Zakładki

Często gdy mamy rozbudowany interfejs, zachodzi potrzeba podzielenia go na kilka części. Świetnie nadaje się do tego mechanizm zakładek, które możemy tworzyć dzięki kontrolce TabControl.

TabControl (rysunek 10.18) jest komponentem-rodzicem. Oznacza to, że w jego wnętrzu możemy umieszczać inne komponenty.

csharp10.18.jpg
Rysunek 10.18. Komponent TabControl

TabControl domyślnie posiada dwie zakładki, które możemy dodawać lub usuwać. Wystarczy je kliknąć i wybrać z menu podręcznego pozycję Add lub Remove. Jeżeli chcesz edytować właściwości danej zakładki, musisz wybrać właściwość TabPages z okna właściwości Properties. Właściwość TabPages oferuje wiele opcji, dzięki którym będziesz mógł ustawić cechy każdej zakładki z osobna. Kilka interesujących właściwości komponentu TabControl prezentuje tabela 10.4.

Tabela 10.4. Najważniejsze właściwości komponentu TabControl

WłaściwośćOpis
AlingmentOkreśla położenie zakładek. Domyślnie są one położone u góry (Top), ale możliwe są również wartości: Bottom (dół), Left (lewa strona), Right (prawa strona).
AppearanceOkreśla sposób prezentacji zakładek. Można wybrać wartość Button (przyciski) lub Flat Buttons (płaskie przyciski).
HotTrackJeżeli właściwość ma wartość true, zakładka zostanie podświetlona, gdy naprowadzimy na nią kursor.
MultilineOkreśla, czy tytuł zakładki może mieścić się w więcej niż jednej linii (domyślnie false — nie może).
SizeMode Tryb rozmiaru zakładek. Domyślnie szerokość zakładki dopasowywana jest do treści. Można ustawić wartości: FillToRight (wyrównanie do prawej) lub Fixed (każda zakładka będzie miała taką samą szerokość).

Kontrolka TabControl jest najczęściej dopasowywana do szerokości formularza albo komponentu-rodzica. Jeżeli chcesz, aby rozmiar komponentu był dopasowany do obiektu macierzystego, wystarczy zmienić właściwość Dock na Fill.

Kontrolki tekstowe

Osobiście dzielę kontrolki tekstowe na jednowierszowe i wielowierszowe. Wielowierszową kontrolką edycyjną jest komponent RichTextBox. Jednowierszową kontrolką edycyjną jest np. TextBox.

Do często używanych kontrolek można zaliczyć również:
*ComboBox,
*ListBox.

Ta pierwsza prezentuje kolejne linie tekstu w formie listy rozwijanej. Komponent ListBox działa bardzo podobnie, tyle że kolejne linie prezentowane są w formie listy nierozwijanej.

Na nowej zakładce komponentu TabControl umieściłem listę rozwijaną (ComboBox) oraz komponent WebBrowser (rysunek 10.19). Ten drugi służy do wyświetlania stron internetowych.

csharp10.19.jpg
Rysunek 10.19. Komponent ComboBox umieszczony na zakładce

Komponent ComboBox będzie umożliwiał wpisanie adresu strony, która ma zostać wyświetlona w komponencie typu WebBrowser. Wpisane adresy stron WWW będą zapamiętywane i umieszczane na liście rozwijanej.

Oprogramujmy więc zdarzenie KeyDown komponentu ComboBox. Musimy wychwycić moment naciśnięcia klawisza Enter. Jeżeli to nastąpi, ładujemy stronę o określonym URL, a sam adres dodajemy do listy rozwijanej (rysunek 10.20).

csharp10.20.jpg
Rysunek 10.20. Lista adresów WWW w komponencie ComboBox

Procedura zdarzeniowa zdarzenia KeyDown wygląda następująco:

private void comboBox1_KeyDown(object sender, KeyEventArgs e)
{
    if (e.KeyValue == (int)Keys.Enter)
    {
        IE.Navigate(comboBox1.Text);

        comboBox1.Items.Add(comboBox1.Text);
    }
}

Jeżeli warunek zostanie spełniony, wywołana będzie metoda Navigate(), która nakazuje przejść do określonego URL wpisanego w komponencie ComboBox. Kolejna instrukcja dodaje wpisany adres do listy. Musimy się w niej odwołać do właściwości Items, która reprezentuje kolekcję. Za dodanie nowej pozycji odpowiada metoda Add().

Kilka interesujących właściwości komponentu ComboBox zostało zaprezentowanych w tabeli 10.5.
Tabela 10.5. Właściwości komponentu ComboBox

WłaściwośćOpis
DropDownStyleReprezentuje styl listy rozwijanej. Wartość Simple oznacza usunięcie ikony służącej do rozwijania listy. Wartość DropDownList uniemożliwia nadanie wartości właściwości Text.
DropDownHeightReprezentuje wysokość listy (w pikselach).
DropDownWidthReprezentuje szerokość listy (w pikselach).
MaxDropDownItemsOznacza maksymalną liczbę pozycji, jaka może znaleźć się na liście rozwijanej.
SortedWartość true oznacza, że pozycje na liście będą sortowane.

Właściwości oraz użycie komponentu ListBox są bardzo podobne, więc pozostawię to zagadnienie bez opisu.

Komponent RichTextBox

Można powiedzieć, że komponent RichTextBox jest miniedytorem tekstu. Zawiera metody służące do kopiowania, wycinania czy wklejania tekstu, jak również takie, które umożliwią zmianę koloru zaznaczonego tekstu czy ilości wcięć.

Komponent RichTextBox idealnie nadaje się do reprezentowania dużych porcji tekstu — np. zawartości plików tekstowych. Skoro pokazałem już, w jaki sposób tworzyć menu, paski narzędziowe czy zakładki, może warto by było przeobrazić nasz program w coś więcej niż tylko pusty interfejs.

Oprogramujmy zdarzenia Click poszczególnych pozycji menu.

Zacznijmy od pozycji Wytnij, Kopiuj, Wklej oraz Zaznacz wszystko. Wygeneruj zdarzenia Click dla tych pozycji.

Kod procedur zdarzeniowych powinien wyglądać tak:

private void mmCut_Click(object sender, EventArgs e)
{
    MyRichBox.Cut();
}

private void mmCopy_Click(object sender, EventArgs e)
{
    MyRichBox.Copy();
}

private void mmPaste_Click(object sender, EventArgs e)
{
    MyRichBox.Paste();
}

private void mmSelectAll_Click(object sender, EventArgs e)
{
    MyRichBox.SelectAll();
}

Jak widzisz, proces kopiowania czy wycinania tekstu jest bardzo prosty. Wystarczy wywołać metodę Cut(), aby wyciąć zaznaczony tekst, Copy(), aby go skopiować, i Paste(), aby wkleić tekst ze schowka. Za zaznaczenie całego tekstu w komponencie RichTextBox odpowiada metoda SelectAll().

Ponieważ na pasku narzędziowym również mamy ikony odpowiadające za kopiowanie, wycinanie i wklejanie, należy przypisać im odpowiednie procedury zdarzeniowe.

Inne ciekawe metody komponentu RichTextBox wymieniłem w tabeli 10.6.

Tabela 10.6. Interesujące metody komponentu RichTextBox

WłaściwośćOpis
`AppendText()`Umożliwia dodanie tekstu na końcu aktualnej linii.
`CanPaste()`Zwraca wartość `true`, jeżeli w schowku są dane, które użytkownik może wkleić.
`Clear()`Czyści zawartość komponentu `RichTextBox`.
`ClearUndo()`Czyści informacje na temat ostatnio cofanych operacji.
`DeselectAll()`Usuwa ewentualne zaznaczenie tekstu.
`GetCharFromPosition()`Zwraca znak znajdujący się w określonej pozycji tekstu.
`GetFirstCharIndexFromLine()` Zwraca indeks pierwszego znaku z podanej linii.
`GetFirstCharIndexOfCurrentLine()`Zwraca indeks pierwszego znaku aktualnej linii.
`LoadFile()`Umożliwia wczytanie zawartości pliku.
`Redo()`Powtarza operację, która została uprzednio cofnięta.
`SaveFile()`Zapisuje tekst wpisany w kontrolce do określonego pliku.
`Select()`Zaznacz określony tekst znajdujący się w kontrolce.
`Undo()`Cofnij ostatnio wykonaną operację.

Okna dialogowe

Zapomnijmy na chwilę o komponencie RichTextBox. Nasza przykładowa aplikacja ma pozycje w menu służące do otwierania, zapisywania pliku. Aby zrealizować to zadanie i nadać naszej aplikacji więcej profesjonalizmu, możemy skorzystać z okien dialogowych Otwórz… oraz Zapisz… Są to standardowe okna systemu Windows, które zapewne nieraz widziałeś. Umożliwiają one wskazanie pliku, który zostanie otwarty lub zapisany.

Komponenty umożliwiające wyświetlanie okien dialogowych znajdują się w kategorii Dialogs okna Toolbox. Utwórz na formularzu komponenty OpenFileDialog oraz SaveFileDialog. Są to komponenty niewizualne, więc reprezentujące je ikony zostaną umieszczone w panelu podręcznym zakładki projektowania.

Wygeneruj zdarzenie Click pozycji Otwórz z menu głównego. Kod metody zdarzeniowej może wyglądać tak:

private void mmOpen_Click(object sender, EventArgs e)
{
    if (openFileDialog1.ShowDialog() == DialogResult.OK
       && openFileDialog1.FileName.Length > 0)
    {
        MyRichBox.LoadFile(openFileDialog1.FileName);
        Text = AppName + " [" + openFileDialog1.FileName + "]";
    }
}

Jak widzisz, sposób otwarcia nowego dokumentu jest prosty. Przede wszystkim należy wyświetlić okno dialogowe (metoda ShowDialog()). Jeżeli użytkownik wskaże plik i naciśnie przycisk OK, zmieniony zostanie tytuł okna dialogowego, ale przede wszystkim zawartość pliku zostanie załadowana do komponentu (metoda LoadFile()).

Właściwość FileName komponentu OpenFileDialog zwraca ścieżkę do pliku, który wybrał użytkownik.

Oprogramujmy teraz zdarzenie Click pozycji odpowiadającej za zapisanie dokumentu (pozycja Zapisz oraz Zapisz jako):

private void mmSave_Click(object sender, EventArgs e)
{
    if (FileName == null)
    {
        mmSaveAs_Click(sender, e);
    }
    else
    {
        MyRichBox.SaveFile(FileName);
        Modified = false;
    }
}

private void mmSaveAs_Click(object sender, EventArgs e)
{
    if (saveFileDialog1.ShowDialog() == DialogResult.OK 
        && saveFileDialog1.FileName.Length > 0)
    {
        MyRichBox.SaveFile(saveFileDialog1.FileName);
        Text = AppName + " [" + saveFileDialog1.FileName + "]";

        FileName = saveFileDialog1.FileName;
        Modified = false;
    }
}

W programie zadeklarowałem pole prywatne FileName typu string. Jeżeli wartość tego pola jest równa null, należy wywołać okno dialogowe, w którym użytkownik musi podać ścieżkę oraz nazwę pliku. Zapis zawartości komponentu RichTextBox realizuje metoda SaveFile(), w której należy podać ścieżkę do zapisu.

Metoda SaveFile() zapisuje plik w formacie RTF (ang. Rich Text Format), który akceptuje potem także przy odczycie.

Właściwości okien dialogowych

Zarówno komponent OpenFileDialog, jak i SaveFileDialog posiada kilka interesujących właściwości, o których warto wspomnieć. Kilka z nich omówiłem w tabeli 10.7.

Tabela 10.7. Właściwości okien dialogowych

WłaściwośćOpis
TitleTytuł okna dialogowego.
AddExtensionWłaściwość określa, czy automatycznie dodawać rozszerzenie, jeżeli użytkownik je pominął.
CheckFileExistsOkreśla, czy generować ostrzeżenie (true), jeżeli plik wskazany przez użytkownika nie istnieje.
CheckPathExistsOkreśla, czy generować ostrzeżenie (true), jeżeli wskazana ścieżka nie istnieje.
DefaultExtDomyślnie rozszerzenie (np. txt, rtf).
FilterFiltry plików, jakie wyświetlone są w liście rozwijanej okna dialogowego (np. Pliki RTF|*.rtf).
ShowHelpOkreśla, czy wyświetlać przycisk pomocy (domyślnie false).
FileNameDomyślna nazwa dla pliku (wyświetlana w kontrolce tekstowej w oknie dialogowym).
InitialDirectoryDomyślny katalog, którego zawartość zostanie wyświetlona zaraz po otwarciu okna dialogowego.

Aplikacja — edytor tekstów

Na podstawie tego, co zaprezentowałem w tym rozdziale, stworzyliśmy prosty edytor tekstów. Zaprezentowałem również procedury zdarzeniowe dla niektórych zdarzeń Click. Czas zaprezentować kod pozostałych procedur zdarzeniowych, których zresztą nie ma zbyt wiele. Zacznijmy od pozycji Nowy w naszym menu:

private void mmNew_Click(object sender, EventArgs e)
{
    FileName = null;
    Text = AppName;

    MyRichBox.Clear();
    Modified = true;
}

Pierwsza instrukcja nadaje wartość null dla pola FileName (określającego ścieżkę aktualnie edytowanego pliku). Kolejna instrukcja przypisuje do właściwości Text wartość pola AppName, które określa nazwę programu:

private const string AppName = "Notepad Extra";

Ostatnia instrukcja czyści zawartość komponentu typu RichTextBox.

Idąc dalej, należy również oprogramować zdarzenie Click pozycji Zakończ:

private void mmExit_Click(object sender, EventArgs e)
{
    if (Modified)
    {
        DialogResult dr = MessageBox.Show("Plik nie został zapisany. Zapisać?", 
            "Plik niezapisany", MessageBoxButtons.YesNoCancel);

        if (dr == DialogResult.Cancel)
        {
            return;
        }
        else if (dr == DialogResult.Yes)
        {
            mmSave_Click(sender, e);
        }
        Application.Exit();
    }
}

Po wybraniu tej pozycji aplikacja zostanie zamknięta. Uprzednio jednak program zapyta użytkownika, czy zapisać zmiany w pliku, jeżeli został on zmodyfikowany.

Metoda Show() z klasy MessageBox (która notabene była często używana przeze mnie w tej książce) zwraca informacje o naciśniętym klawiszu w formie typu wyliczeniowego DialogResult. W naszym przykładzie zawartość pliku zostanie zapisana tylko wtedy, gdy użytkownik wybierze opcję Yes.

Tworzenie nowego formularza

Powróćmy raz jeszcze do naszego przykładowego programu — edytora tekstu. Zapomnieliśmy oprogramować jeszcze jednej opcji z menu, a mianowicie — O programie. Chcielibyśmy, aby po wybraniu tej opcji wyświetlony został nowy formularz informacji o autorze aplikacji i adres e-mail autora.
Pracując w środowisku Visual C# Express Edition, z menu Project wybierz Add Windows Form. W oknie Add New Item wskaż pozycję Windows Form. Na kolejnej zakładce zostanie wyświetlony formularz. Możesz umieścić na nim etykiety informujące o autorze programu oraz dane kontaktowe (rysunek 10.21).

csharp10.21.jpg
Rysunek 10.21. Okno O programie…

Aby uniemożliwić zmiany rozmiarów okna formularza, zmieniłem właściwość FormBorderStyle na FixedToolWindow. Zwróć uwagę, że formularz zaprezentowany na rysunku 10.21 nie ma ikon minimalizacji oraz maksymalizacji okna.

Aby wyświetlić formularz O programie, należy zwyczajnie utworzyć nowy egzemplarz jego klasy, a następnie wywołać metodę Show():

private void mmAbout_Click(object sender, EventArgs e)
{
   AboutForm About = new AboutForm();
   About.Show();
}

Podsumowanie

Opisanie całej biblioteki WinForms wymaga odrębnej publikacji. Ja w tym rozdziale opisałem jedynie kilka podstawowych komponentów. Mam nadzieję, że na tej podstawie będziesz w stanie wykorzystać użyteczność pozostałych komponentów, jakie udostępnia biblioteka Windows Forms. Uwierz mi, że w poznaniu tej biblioteki duże znaczenie odgrywa doświadczenie. Im częściej będziesz wykorzystywał w swoich aplikacjach bibliotekę WinForms, tym lepiej ją zgłębisz i wkrótce nie będzie ona kryła przed Tobą żadnych tajemnic.

[[C_Sharp/Wprowadzenie|Spis treści]]

[[C_Sharp/Wprowadzenie/Prawa autorskie|©]] Helion 2006. Autor: Adam Boduch. Zabrania się rozpowszechniania tego tekstu bez zgody autora.

0 komentarzy