Aplikacje desktopowe · Windows Forms

Notatnik 4P: edytor tekstu w Windows Forms

Zbudujesz własny notatnik: nowy dokument, otwieranie i zapisywanie pliku .txt, cofanie, kopiowanie, zmiana czcionki, zawijanie wierszy i drukowanie. Jeden projekt łączy menu, zdarzenia, pliki i okna dialogowe, czyli umiejętności z zadań INF.04.

  • około 90 minut
  • Windows Forms · menu · pliki · dialogi
  • INF.04.5

Po tej lekcji: zbudować formularz z MenuStrip, RichTextBox i StatusStrip, zapisać i odczytać plik przez okna dialogowe oraz pamiętać nazwę bieżącego pliku w zmiennej aktualnyPlik.

Zacznijmy od notatek ze spotkania

Prosty edytor, który robi prawdziwą robotę

Na spotkaniu grupy projektowej zapisujesz listę rzeczy do zrobienia: szybko tworzysz plik .txt, poprawiasz tekst, zmieniasz czcionkę i drukujesz ustalenia. Ta wersja uczy podstaw działania edytora. Ochronę przed utratą niezapisanych zmian dodasz w zadaniu 3.

Liczy się nie tylko wygląd okna, ale poprawne połączenie kontrolek z logiką. To właśnie sprawdza egzamin.

Laboratorium · ćwiczenie w przeglądarce, do którego odsyłają rozgrzewki na końcu lekcji

1 · Zobacz, do czego dążysz

Notatnik 4P w działaniu

Wpisz tekst i wybierz Plik → Zapisz: pojawi się okno z nazwą pliku. Dopisz coś i zapisz ponownie: tym razem bez pytania. Zwróć uwagę na wartość aktualnyPlik po prawej. Wypróbuj „Zapisz jako”, „Otwórz”, „Drukuj” z bardzo długim tekstem i włącz ochronę zmian.

Ta interakcja wymaga włączonego JavaScriptu.

Zmienna aktualnyPlik decyduje, czy „Zapisz” musi zapytać o nazwę. Pusty napis oznacza „ten dokument jeszcze nie ma pliku”.

Efekt końcowy

Tak ma wyglądać formularz

Formularz Notatnika 4P w projektancie: menu, pole tekstowe i pasek stanu
U góry menu, w środku duże pole tekstowe, na dole pasek ze znakami i wierszami. Pod formularzem leżą komponenty niewidoczne podczas działania programu.

Kontrola efektu

Okno wyboru czcionki

Okno wyboru czcionki w Notatniku 4P
Kontrola efektu: Format → Czcionka otwiera standardowe okno Windows, a wybrana czcionka trafia do pola tekstowego.

2 · Zacznij od projektu

Utwórz projekt Windows Forms

Wybierz Windows Forms App w C# (nowy szablon .NET, nie stary „.NET Framework”). Zaznaczaj kroki w trakcie.

3 · Krok 1

Właściwości formularza

WłaściwośćWartośćPo co?
(Name)oknoNotatnikaOd razu wiadomo, że to główne okno programu.
TextNotatnik 4PTytuł okna.
StartPositionCenterScreenStart na środku ekranu.
MinimumSize700; 450Ochrona czytelnego układu.
Kliknij puste tło formularza, naciśnij F4 i ustaw właściwości.

4 · Krok 2

RichTextBox zamiast zwykłego TextBox

Przeciągnij z Toolbox RichTextBox. Zmień nazwę z richTextBox1 na **PoleTekstowe**, bo będziesz odwoływać się do niej w prawie każdej metodzie. Ustaw Dock = Fill, AcceptsTab = True i DetectUrls = True.

Dlaczego RichTextBox? Ma gotowe operacje Undo, Redo, Copy, Cut i Paste, więc nie piszesz ich od zera.

Punkt kontrolny: naciśnij F5, zmień rozmiar okna i sprawdź, czy białe pole wypełnia całe dostępne miejsce.

6 · Krok 4

StatusStrip z dwoma etykietami

Element`(Name)`Tekst początkowy
ToolStripStatusLabelbtznakiZnaki:
ToolStripStatusLabelbtwierszeWiersze:
Sam pasek (`statusStrip1`) może zostać z domyślną nazwą, bo nie sterujemy nim w kodzie. Nazwy dostają tylko elementy, które program zmienia.

7 · Krok 5

Niewidoczne narzędzia pod formularzem

Komponent z Toolbox`(Name)`Zadanie
OpenFileDialogopenFileDialog1Wybór istniejącego pliku.
SaveFileDialogsaveFileDialog1Wybór nazwy i miejsca zapisu.
PrintDocumentprintDocument1Przygotowanie treści strony do druku.
PrintDialogprintDialog1Wybór drukarki i ustawień.
FontDialogfontDialog1Wybór kroju, stylu i rozmiaru tekstu.
Przeciągnij je na formularz. Visual Studio umieści je w dolnym panelu projektanta. Nazwa w oknie właściwości i w kodzie musi być taka sama.

8 · Krok 6

Które metody podpiąć

ElementZdarzenieMetoda
PoleTekstoweTextChangedPoleTekstowe_TextChanged (podpinana w konstruktorze, nie w projektancie)
pozycje menu PlikClicknowyToolStripMenuItem_Click, otworzToolStripMenuItem_Click, zapiszToolStripMenuItem_Click, zapiszJakoToolStripMenuItem_Click, drukujToolStripMenuItem_Click
pozycje menu EdycjaClickcofnijToolStripMenuItem_Click, ponowToolStripMenuItem_Click, wytnijToolStripMenuItem_Click, kopiujToolStripMenuItem_Click, wklejToolStripMenuItem_Click, usunToolStripMenuItem_Click
menu FormatClickzawijajWierszeToolStripMenuItem_Click, czcionkaToolStripMenuItem_Click
printDocument1PrintPageprintDocument1_PrintPage
Zaznacz element, naciśnij F4, kliknij ikonę błyskawicy i wybierz lub wpisz metodę przy zdarzeniu `Click`.

9 · Cały kod

Form1.cs

Nie wklejaj kodu do Form1.Designer.cs (tworzy go projektant). Dodawaj metody etapami: pole aktualnyPlik, statystyki, zapis i odczyt, edycja, formatowanie, druk. Po każdym etapie wybierz Build → Build Solution. Szablon .NET sam dodaje potrzebne using.

Form1.cscsharp
namespace Notatnik4P
{
    public partial class Form1 : Form
    {
        // Ścieżka aktualnie otwartego lub zapisanego pliku
        string aktualnyPlik = "";

        public Form1()
        {
            InitializeComponent();

            PoleTekstowe.TextChanged += PoleTekstowe_TextChanged;
        }

        // STATYSTYKI DLA STATUSSTRIP
        private void PoleTekstowe_TextChanged(object? sender, EventArgs e)
        {
            // Liczba znaków
            btznaki.Text = "Znaki: " + PoleTekstowe.Text.Length;

            // Liczba wierszy
            btwiersze.Text = "Wiersze: " + PoleTekstowe.Lines.Length;
        }

        // OTWÓRZ
        private void otworzToolStripMenuItem_Click(object sender, EventArgs e)
        {
            openFileDialog1.Filter =
                "Pliki tekstowe (*.txt)|*.txt|Wszystkie pliki (*.*)|*.*";

            if (openFileDialog1.ShowDialog() == DialogResult.OK)
            {
                // Zapamiętujemy otwarty plik
                aktualnyPlik = openFileDialog1.FileName;

                // Wczytujemy jego zawartość
                PoleTekstowe.LoadFile(
                    aktualnyPlik,
                    RichTextBoxStreamType.PlainText
                );
            }
        }

        // ZAPISZ
        private void zapiszToolStripMenuItem_Click(object sender, EventArgs e)
        {
            // Jeżeli plik nie ma jeszcze nazwy
            if (aktualnyPlik == "")
            {
                saveFileDialog1.Filter =
                    "Pliki tekstowe (*.txt)|*.txt|Wszystkie pliki (*.*)|*.*";

                saveFileDialog1.DefaultExt = "txt";

                if (saveFileDialog1.ShowDialog() == DialogResult.OK)
                {
                    aktualnyPlik = saveFileDialog1.FileName;

                    PoleTekstowe.SaveFile(
                        aktualnyPlik,
                        RichTextBoxStreamType.PlainText
                    );
                }
            }
            else
            {
                // Plik już istnieje - zapisujemy bez pytania
                PoleTekstowe.SaveFile(
                    aktualnyPlik,
                    RichTextBoxStreamType.PlainText
                );
            }
        }

        // ZAPISZ JAKO
        private void zapiszJakoToolStripMenuItem_Click(object sender, EventArgs e)
        {
            saveFileDialog1.Filter =
                "Pliki tekstowe (*.txt)|*.txt|Wszystkie pliki (*.*)|*.*";

            saveFileDialog1.DefaultExt = "txt";

            if (saveFileDialog1.ShowDialog() == DialogResult.OK)
            {
                aktualnyPlik = saveFileDialog1.FileName;

                PoleTekstowe.SaveFile(
                    aktualnyPlik,
                    RichTextBoxStreamType.PlainText
                );
            }
        }

        // NOWY DOKUMENT
        private void nowyToolStripMenuItem_Click(object sender, EventArgs e)
        {
            PoleTekstowe.Clear();
            aktualnyPlik = "";
        }

        // DRUKOWANIE 1 - WYWOŁANIE METODY WYBORU DRUKARKI
        private void drukujToolStripMenuItem_Click(object sender, EventArgs e)
        {
            printDialog1.Document = printDocument1;

            if (printDialog1.ShowDialog() == DialogResult.OK)
            {
                printDocument1.Print();
            }
        }

        // DRUKOWANIE 2 - WYSYŁKA TREŚCI DO DRUKARKI Z DOKUMENTU
        private void printDocument1_PrintPage(object sender, System.Drawing.Printing.PrintPageEventArgs e)
        {
            e.Graphics.DrawString(PoleTekstowe.Text, PoleTekstowe.Font, Brushes.Black, e.MarginBounds);
        }

        // COFNIJ
        private void cofnijToolStripMenuItem_Click(object sender, EventArgs e)
        {
            if (PoleTekstowe.CanUndo)
            {
                PoleTekstowe.Undo();
            }
        }

        // PONÓW
        private void ponowToolStripMenuItem_Click(object sender, EventArgs e)
        {
            if (PoleTekstowe.CanRedo)
            {
                PoleTekstowe.Redo();
            }
        }

        // KOPIUJ
        private void kopiujToolStripMenuItem_Click(object sender, EventArgs e)
        {
            PoleTekstowe.Copy();
        }
        // WKLEJ
        private void wklejToolStripMenuItem_Click(object sender, EventArgs e)
        {
            PoleTekstowe.Paste();
        }

        // WYTNIJ
        private void wytnijToolStripMenuItem_Click(object sender, EventArgs e)
        {
            PoleTekstowe.Cut();
        }

        // USUŃ
        private void usunToolStripMenuItem_Click(object sender, EventArgs e)
        {
            PoleTekstowe.SelectedText = "";
        }

        // ZAWIJAJ WIERSZE
        private void zawijajWierszeToolStripMenuItem_Click(object sender, EventArgs e)
        {
            PoleTekstowe.WordWrap = !PoleTekstowe.WordWrap;
            zawijajWierszeToolStripMenuItem.Checked = PoleTekstowe.WordWrap;
        }

        // CZCIONKA
        private void czcionkaToolStripMenuItem_Click(object sender, EventArgs e)
        {
            fontDialog1.Font = PoleTekstowe.Font;
            if (fontDialog1.ShowDialog() == DialogResult.OK)
            {
                PoleTekstowe.Font = fontDialog1.Font;
            }
        }
    }
}
  1. string aktualnyPlik = "" Ścieżka bieżącego pliku. Pusty napis oznacza, że dokument jeszcze nie był zapisany.
  2. PoleTekstowe.TextChanged += PoleTekstowe_TextChanged Podpięcie zdarzenia w konstruktorze. Przy każdej zmianie tekstu aktualizujemy pasek stanu.
  3. PoleTekstowe.Lines.Length Liczba wierszy: tablica linii tekstu w kontrolce.
  4. openFileDialog1.ShowDialog() == DialogResult.OK Okno wyboru pliku jest modalne. Dalej idziesz tylko po kliknięciu OK.
  5. LoadFile i SaveFile z RichTextBoxStreamType.PlainText Wczytanie i zapis zwykłego tekstu bez formatowania RTF.
  6. if (aktualnyPlik == "") Pierwszy zapis pyta o nazwę (SaveFileDialog). Następne zapisują wprost do aktualnyPlik.
  7. printDialog1.Document = printDocument1 Łączy okno wyboru drukarki z dokumentem do druku. Bez tego drukuje pustą stronę.
  8. e.Graphics.DrawString(..., e.MarginBounds) Rysuje tekst w obrębie marginesów. Długi tekst zostanie ucięty po pierwszej stronie, co poprawisz w zadaniu 3.
  9. PoleTekstowe.WordWrap = !PoleTekstowe.WordWrap Przełącznik: odwraca bieżącą wartość. Ptaszek w menu odzwierciedla stan (Checked).
  10. fontDialog1.Font = PoleTekstowe.Font Okno startuje z bieżącą czcionką, a po OK nowa trafia do pola.

Do zadań

Wstawianie daty, wyszukiwanie i pytanie o zapis

Zadania wymagają trzech rzeczy: tekstu w miejscu kursora, szukania bez rozróżniania wielkości liter oraz zdarzenia zamknięcia formularza.

Form1.cscsharp
// Wstaw datę w miejscu kursora (zastępuje też zaznaczenie)
PoleTekstowe.SelectedText = DateTime.Now.ToString("dd.MM.yyyy HH:mm");

// Szukanie bez rozróżniania wielkości liter
private void Szukaj(string szukane)
{
    if (string.IsNullOrWhiteSpace(szukane)) return;

    int indeks = PoleTekstowe.Text.IndexOf(szukane, StringComparison.OrdinalIgnoreCase);
    if (indeks >= 0)
    {
        PoleTekstowe.Focus();
        PoleTekstowe.Select(indeks, szukane.Length);   // zaznacza znalezione słowo
        PoleTekstowe.ScrollToCaret();
    }
    else
    {
        MessageBox.Show("Nie znaleziono podanego tekstu.", "Szukaj");
    }
}

// Skrót Ctrl+F: we właściwościach pozycji menu ustaw ShortcutKeys na Ctrl+F

// Pytanie o zapis przy zamykaniu
bool zmieniony = false;                                // ustaw na true w PoleTekstowe_TextChanged

private void Form1_FormClosing(object sender, FormClosingEventArgs e)
{
    if (!zmieniony) return;

    var odp = MessageBox.Show("Zapisać zmiany?", "Notatnik",
        MessageBoxButtons.YesNoCancel, MessageBoxIcon.Question);

    if (odp == DialogResult.Cancel)
        e.Cancel = true;                               // formularz zostaje otwarty
    else if (odp == DialogResult.Yes)
        zapiszToolStripMenuItem.PerformClick();
}
  1. SelectedText Tekst zaznaczony w polu. Gdy nic nie zaznaczono, przypisanie wstawia napis w miejscu kursora.
  2. IndexOf(…, OrdinalIgnoreCase) Zwraca położenie pierwszego trafienia albo -1, gdy tekstu nie ma.
  3. e.Cancel = true Przerywa zamykanie okna. Zdarzenie FormClosing podpinasz w oknie Właściwości formularza.

10 · Krok 7

Uruchom i sprawdź każdą funkcję

TestOczekiwany rezultat
Wpisz Ala ma kota.Pasek: Znaki: 11 i Wiersze: 1.
Naciśnij Enter i wpisz Drugi wiersz.Liczba wierszy zmienia się na 2.
Plik → Zapisz, nazwa proba.txtPlik powstaje w wybranym folderze.
Dopisz tekst i znów Plik → ZapiszZapis do tego samego pliku, bez pytania o nazwę.
Otwórz zapisany plikZapisana treść wraca do pola.
Cofnij, Ponów, Wytnij, Kopiuj, WklejKażda pozycja zmienia tekst zgodnie z nazwą.
Format → Zawijaj wiersze, bardzo długi tekstTekst przełącza się między zawijaniem a przewijaniem poziomym.
Format → CzcionkaZmienia się krój lub rozmiar tekstu.
Plik → DrukujPojawia się systemowe okno wyboru drukarki.
Samo pojawienie się okna nie oznacza, że program jest gotowy. Sprawdź każdy wiersz tabeli.

11 · Czego unikać

Cztery najczęstsze kłopoty

Nazwa nie istnieje w bieżącym kontekście
Tak nie
poleTekstowe.Clear();   // w projektancie: PoleTekstowe

Błąd CS0103: wielkość liter ma znaczenie.

Tak
PoleTekstowe.Clear();

Nazwa w kodzie musi być identyczna z (Name) w projektancie.

„Zapisz” zawsze pyta o nazwę
Tak nie
saveFileDialog1.ShowDialog();
// brak: aktualnyPlik = saveFileDialog1.FileName;

Zmienna aktualnyPlik zostaje pusta, więc program nie pamięta pliku.

Tak
aktualnyPlik = saveFileDialog1.FileName;

Po pierwszym zapisie przypisz ścieżkę do aktualnyPlik.

Drukuje pustą stronę
Tak nie
printDocument1.Print();   // bez przypisania Document

Wydruk jest pusty lub nie powstaje.

Tak
printDialog1.Document = printDocument1;
if (printDialog1.ShowDialog() == DialogResult.OK)
    printDocument1.Print();

Okno drukarki musi znać dokument, a PrintPage musi być podpięte.

Pasek stanu nie zmienia liczb
Tak nie
public Form1()
{
    InitializeComponent();
}

Etykiety Znaki i Wiersze pokazują tylko tekst początkowy.

Tak
public Form1()
{
    InitializeComponent();
    PoleTekstowe.TextChanged += PoleTekstowe_TextChanged;
}

Zdarzenie trzeba podpiąć. Tu robimy to w konstruktorze.

12 · Sprawdź się

Notatnik w praktyce

Cztery sytuacje z projektu.

Ta interakcja wymaga włączonego JavaScriptu.

Podsumowanie

Co warto zapamiętać

  1. RichTextBox daje gotowe Undo, Redo, Copy, Cut, Paste oraz LoadFile i SaveFile.
  2. Nazwy z (Name) używasz w kodzie, a muszą być identyczne (także wielkość liter).
  3. Okna OpenFileDialog, SaveFileDialog, FontDialog są modalne, a wynik sprawdzasz przez DialogResult.OK.
  4. aktualnyPlik odróżnia pierwszy zapis (pytanie o nazwę) od kolejnych (zapis wprost).
  5. Druk wymaga PrintDialog.Document = printDocument1 oraz zdarzenia PrintPage.

Dalej na swoim koncie

Rozgrzewki, quiz i zadania są w biurku ucznia.

Teoria jest otwarta dla wszystkich. Żeby rozwiązywać zadania, zapisywać szkice i widzieć swój postęp, zaloguj się.

Zaloguj się →
Źródła i dalsza nauka