Kontakt  |  PRS 170  |  ALK 420  |  PZR 420  |  SIK 420  |  JPR 222  |  SOP 121


Języki programowania - ćwiczenia 13


Temat zajęć: Programowanie w C# na platformie .NET (c.d.).

Literatura:
  • [1] J. Liberty, "Programowanie C#" (wyd. O'Reilly, druk w Polsce: Helion)
  • [2] A. Troelsen, "Pro C# 2005 and the .NET 2.0 Platform, 3rd edition" (wyd. APress)

Materiały dodatkowe:

Na dzisiejszych zajęciach zaimplementujemy aplikację wspomagającą tworzenie tzw. map myślowych (mindmapping) - techniki stosowanej na etapie tzw. burzy muzgów przy planowaniu projektów. Mapy myślowe zostały wprowadzone przez Toniego Buzana (brytyjskiego badacza funkcjonowania mózgu), a przykłady ich zastosowania można znaleźć m. in. w książce Davida Allena "Getting Things Done" (polskie wydanie nosi tytuł "Sztuka efektywności").
Przykładowo, projekt "nauczyć się na kolokwium" mógłby posiadać następującą mapę:

NAUCZYĆ SIĘ NA KOLOKWIUM
  • uzupełnić notatki
    • sprawdzić, których notatek brakuje
    • skserować brakujące notatki
      • zadzwonić do kolegi/koleżanki i pożyczyć notatki
      • znaleźć działające ksero
  • przejrzeć literaturę
    • sporządzić listę książek
    • wybrać pozycje do przejrzenia
      • sprawdzić, czy są dostępne w bibliotece
    • przejrzeć pozycje w bibliotece
      • sprawdzić godziny otwarcia biblioteki
      • pojechać do biblioteki
  • przystąpić do nauki
    • zebrać książki i notatki
    • odwołać wszystkie spotkania
    • zapewnić sobie warunki
      • wyłączyć telefon
      • wyłączyć telewizor
      • zaparzyć kawę
        • czy wystarczy kawy?
Zadaniem map myślowych jest zebranie wszystkich problemów związanych z realizacją danego projektu i ustrukturyzowanie ich w formie diagramu lub drzewa (my zastosujemy tą drugą strukturę).

Założenia:
  • aplikacja powinna umożliwić wprowadzanie opisów problemów dotyczących danego projektu,
  • każdy problem może mieć dowolnie wiele podproblemów, z których każdy może mieć kolejne podproblemy, itd.,
  • z punktu widzenia użytkownika wszystkie problemy powinny być zebrane w formie drzewa,
  • użytkownik powinien mieć możliwość usuwania poddrzew problemów,
  • aplikacja powinna posiadać funkcję opcjonalnego przedstawienia drzewa problemów w formie tekstowej (np. do wydruku), gdzie poszczególne poziomy zagnieżdżenia problemów powinny być odpowiednio numerowane (1,2,3 dla pierwszego poziomu, 1.1, 1.2, ... dla drugiego, 1.1.1, 1.1.2, ... dla trzeciego itd.).
Najpierw musimy zdecydować, jak będzie wyglądał nasz model danych. Z definicji potrzebne nam będzie drzewo problemów. Każdy węzeł tego drzewa posiada atrybut - opis problemu - oraz dowolną liczbę podwęzłów (dzieci). Jako typ danych węzła drzewa problemów przyjmiemy TreeNode.
W interfejsie użytkownika zastosujemy komponent typu TreeView (do przedstawienia struktury drzewiastej) oraz typu RichTextBox (do przedstawienia wersji tekstowej). Oprócz tego skorzystamy jeszcze z RichTextBox (pole tekstu), ContextMenuStrip (menu kontekstowe) i MenuStrip (pasek menu).

Zaczniemy od stworzenia nowego projektu w Visual Studio. Jako typ projektu należy wybrać Windows Application w kategorii C#. Jako nazwę projektu podamy MindMapping. Po wygenerowaniu szkieletu aplikacji w Solution Explorer (po prawej u góry) powinniśmy zobaczyć dwa pliki źródłowe: Form1.cs (okno) i Program.cs (program główny).
Zmienimy nazwę klasy naszego okna z Form1 na OknoDrzewa. Proszę kliknąć prawym przyciskiem na Form1.cs w Solution Explorer i z menu kontekstowego wybrać Rename. Następnie proszę wprowadzić jako nową nazwę OknoDrzewa.cs. Visual Studio zapyta, czy zamienić wszystkie odniesienia w programie do Form1 na odniesienia do OknoDrzewa. Wybieramy Tak.
Wstawmy teraz do naszego okna (należy przełączyć się do widoku projektu wizualnego przez View | Designer) komponent TreeView (należy wybrać go z palety po lewej i umieścić w oknie tak, aby wypełniał całą jego zawartość, z zachowaniem domyślnego marginesu sugerowanego przez Visual Studio). W edytorze właściwości (Properties - po prawej u dołu) ustawmy właściwość (Name) na mapa oraz LabelEdit na True (użytkownik będzie mógł edytować opisy węzłów).
Dodajmy jeszcze do naszego okna pasek menu u góry - w palecie komponentów należy rozwinąć kategorię Menus & Toolbars i wstawić do naszego okna komponent MenuStrip (być może będzie trzeba nieco zmniejszyć od góry nasz TreeView, aby menu się zmieściło), a następnie wypełnić go tak, aby wyglądał jak niżej:

Menu

Ustawmy jeszcze właściwość Anchor naszego TreeView mapa tak, aby miała wartość Top, Bottom, Left, Right (czyli aby widok drzewa rozciągał się i kurczył wraz z oknem).
Dodamy teraz inicjalizację drzewa do konstruktora klasy OknoDrzewa (wcześniej dodamy metodę czyszczącą zawartość drzewa - przyda się ona jeszcze przy implementacji obsługi pozycji Nowa z menu):
 protected void NowaMapa()
{
mapa.Nodes.Clear();
mapa.Nodes.Add(new TreeNode("Nowy projekt"));
}

public OknoDrzewa()
{
InitializeComponent();
NowaMapa();
}
Obiekt klasy TreeView posiada właściwość Nodes, który jest kolekcją jego "głównych węzłów". Każdy węzeł to obiekt klasy TreeNode, który również posiada właściwość Nodes, która definiuje listę jego podwęzłów. Dodatkowo każdy TreeNode posiada tekstowy opis. Jest więc to narzędzie idealnie pasujące do naszych założeń o modelu danych.
Powstaje pytanie, dlaczego nie pytamy użytkownika jaki ma być opis węzła. Otóż wcześniej ustawiliśmy właściwość  LabelEdit obiektu map na True, zatem użytkownik może sam zmieniać opis węzła, po prostu klikając na nim myszką. W naszej aplikacji dodawane węzły będą zawsze miały etykietę "Nowy problem" (za wyjątkiem korzenia, który ma etykietę "Nowy projekt"), a użytkownik zmieni ją wg własnego uznania.
Dodamy teraz menu kontekstowe, które będzie umożliwiać manipulację drzewem problemów. Proszę do okna OknoDrzewa dodać komponent typu ContextMenuStrip (kategoria Menus & Toolbars w palecie) i wypełnić go jak niżej:

Menu kontekstowe

Należy teraz "podpiąć" nasze menu kontekstowe do komponentu mapa. Proszę zaznaczyć komponent mapa, a następnie ustawić jego właściwość  ContextMenuStrip na contextMenuStrip1 (komponent, który przed chwilą dodaliśmy).

Możemy teraz dla próby uruchomić nasz projekt. Powinniśmy dostać w wyniku okno z menu u góry, widokiem drzewa z jednym węzłem - Nowy projekt - którego opis można zmienić po kliknięciu, oraz menu kontekstowe dostępne za pomocą prawego klawisza myszki.

Zaimplementujemy teraz reakcje na poszczególne zdarzenia. Zacznijmy od menu głównego. Proszę zaznaczyć obiekt menuStrip1, rozwinąć menu Mapa i wybrać pozycję Nowa. Następnie w edytorze właściwości (po prawej u dołu) proszę przejść do kategorii Events (symbol Events) i kliknąć na opis zdarzenia Click (w pustym polu na prawo od Click), po czym wcisnąć Enter. Zostanie wygenerowany szkielet metody obsługującej pozycję menu Nowa, którą uzupełniamy jak niżej (wystarczy skorzystać z wcześniej zaimplementowanej metody NowaMapa):
 private void nowaToolStripMenuItem_Click(object sender, EventArgs e)
{
NowaMapa();
}

Analogicznie postępujemy w przypadku pozycji Koniec z głównego menu, którego implementacja powinna wyglądać tak:
 private void koniecToolStripMenuItem_Click(object sender, EventArgs e)
{
Application.Exit();
}

Implementację pozycji Jako tekst zostawimy sobie na później.

Przejdziemy teraz do implementacji reakcji na zdarzenia związane z naszym menu kontekstowym. Proszę w contextMenuStrip1 zaznaczyć pozycję Dodaj dziecko i, podobnie jak wcześniej, wcisnąć Enter w polu reakcji na zdarzenie Click. Dodawanie nowego dziecka do danego węzła może wyglądać np. tak:
 private void dodajDzieckoToolStripMenuItem_Click(object sender, EventArgs e)
{
TreeNode selected = mapa.SelectedNode;
selected.Nodes.Add(new TreeNode("Nowy problem"));
selected.Expand();
}
W pierwszym kroku pobieramy z TreeView referencję na zaznaczony węzeł (właściwość SelectedNode). Następnie dodajemy do jego kolekcji Nodes nowy element TreeNode z opisem "Nowy problem". Na koniec wywołujemy Expand(), co powoduje, że węzeł selected staje się "rozwinięty" (każdy węzeł posiadający dzieci może być w stanie zwiniętym - collapsed - lub rozwiniętym - expanded - sterują tym metody Collapse() i Expand(), a użytkownik może zmieniać stan klikając symbol umieszczony obok opisu węzła).
Podobnie oprogramujemy Dodaj brata, z tym, że nie możemy pozwolić na dodawanie brata do korzenia naszego drzewa (przyjmiemy założenie, że mapa opisuje jeden projekt). Zatem musimy dodatkowo badać, czy wybrany węzeł jest korzeniem i jeśli tak, odmawiać dodania nowego węzła:
 private void dodajBrataToolStripMenuItem_Click(object sender, EventArgs e)
{
TreeNode selected = mapa.SelectedNode;
if (selected.Parent == null)
{
MessageBox.Show("Nie można dodawać nowych projektów",
"Błąd", MessageBoxButtons.OK, MessageBoxIcon.Exclamation);
}
else
{
selected.Parent.Nodes.Add(new TreeNode("Nowy problem"));
}
}
Aby stwierdzić, czy zaznaczony węzeł jest korzeniem, możemy zbadać jego właściwość Parent - korzeń nie ma rodzica, zatem właściwość ta będzie miała wartość null (każdy inny węzeł ma rodzica). W takim przypadku pokazujemy komunikat o błędzie. Jeśli zaznaczono jakikolwiek inny węzeł, to dodajemy jego brata poprzez dodanie dziecka do rodzica (Parent). Nie musimy już wywoływać Expand() rodzica, ponieważ skoro użytkownik był w stanie zaznaczyć dziecko, to rodzic musi już być w stanie expanded.

Pozostała nam obsługa pozycji Usuń:
 private void usuńToolStripMenuItem_Click(object sender, EventArgs e)
{
TreeNode selected = mapa.SelectedNode;
if (selected.Parent == null)
{
MessageBox.Show("Nie możesz usunąć projektu",
"Błąd", MessageBoxButtons.OK, MessageBoxIcon.Exclamation);
}
else
{
if (MessageBox.Show("Czy na pewno usunąć " + selected.Text +
" wraz z podwęzłami?", "Potwierdzenie",
MessageBoxButtons.YesNo, MessageBoxIcon.Question)
== DialogResult.Yes)
{
selected.Parent.Nodes.Remove(selected);
}
}
}
Ponownie sprawdzamy, czy użytkownik nie próbuje przypadkiem usunąć projektu (jeśli tak, odmawiamy). W przeciwnym przypadku prosimy użytkownika o potwierdzenie swojej decyzji (ale tylko raz - nie działamy zgodnie z duchem Windows) i ewentualnie usuwamy zaznaczony węzeł z listy dzieci jego rodzica.

Pozostała nam implementacja reprezentacji tekstowej naszej mapy. Dodamy najpierw do projektu drugie okno. Proszę kliknąć prawym przyciskiem myszy na projekcie MindMapping (UWAGA: na pogrubionym projekcie MindMapping, a nie na Solution 'MindMapping' (1 project)) i z menu kontekstowego wybrać Add, a następnie New Item. Jako typ elementu proszę wybrać Windows Form, a jako nazwę podać OknoTekstu.cs. W edytorze wizualnym proszę rozciągnąć nieco okno i wstawić do niego komponent RichTextBox (tak, aby wypełniał okno od marginesu do marginesu), po czym ustawić właściwość (Name) nowego komponentu na poleTekstu, właściwość Anchor na Top, Bottom, Left, Right, oraz właściwość ReadOnly na True.
Konstruktor naszego okna pozostawimy bez zmian, jednak dodamy jedną (ale za to treściwą) metodę, która przyjmie jako parametr obiekt TreeNode i wygeneruje jego postać tekstową:
 protected void generujWezel(TreeNode n, string prefix)
{
poleTekstu.AppendText(prefix + " " + n.Text + "\n");
for (int numer = 0; numer < n.Nodes.Count; numer++)
{
string newPrefix;
if (prefix.Equals(""))
newPrefix = (numer+1).ToString();
else
newPrefix = "\t" + prefix + "." + (numer+1).ToString();
generujWezel(n.Nodes[numer], newPrefix);
}
}

public void jakoTekst(TreeView mapa)
{
poleTekstu.Text = "";
generujWezel(mapa.Nodes[0], "");
}
Metoda jakoTekst(TreeView mapa) korzysta z chronionej metody generujWezel(TreeNode n, string prefix), której zadaniem jest rekurencyjne przejście drzewa i wstawienie do poleTekstu opisu wszystkich węzłów. Chcemy jednak (patrz założenia programu) uzyskać hierarchiczną numerację problemów, zatem na każdym etapie rekurencji przekazywany jest również parametr prefix, który określa przedrostek numeracji (dla poziomu korzenia jest on pusty, a dla każdego poziomu zagnieżdżenia ma postać "x.y.z..."). Dodatkowo jeszcze każdy kolejny poziom posiada odpowiednie wcięcie (stosujemy symbol tabulatora - "\t").

Wystarczy teraz dodać obsługę pozycji Jako tekst w głównym menu OknoDrzewa:
 private void jakoTekstToolStripMenuItem_Click(object sender, EventArgs e)
{
OknoTekstu o = new OknoTekstu();
o.Text = mapa.Nodes[0].Text;
o.jakoTekst(mapa);
o.ShowDialog(this);
}
Okno tekstowe uruchamiamy modalnie (stąd o.ShowDialog(this), a nie po prostu o.Show()). Podstawienie o.Text = mapa.Nodes[0].Text ustala tytuł okna tekstu na nazwę projektu.

Proszę skompilować i przetestować aplikację. Dla projektu nauki do kolokwium wynik powinien być jak niżej.

Okno drzewa:

Przykładowe okno drzewa

Okno tekstu:

Przykładowe okno tekstu


 Zadanie 1

Zabezpieczyć pozycje menu Nowy i Koniec dodatkową prośbą o potwierdzenie (ale tylko jedną :) ).

 Zadanie 2

Proszę dodać do naszej aplikacji funkcję zapisu mapy do pliku (w formie tekstowej). Opcja zapisania do pliku powinna być dostępna w menu kontekstowym pola z reprezentacją tekstową mapy (należy dodać również menu kontekstowe z pozycją Zapisz do pliku).

Wskazówki:
  • proszę przeczytać w dokumentacji o klasie System.IO.StreamWriter,
  • kompoment RichTextBox posiada właściwość Lines, która umożliwia pobieranie z niego kolejnych wierszy tekstu,
  • w zakładce Dialogs w palecie komponentów znajduje się komponent SaveFileDialog - proszę z niego skorzytać.


Valid HTML 4.01!