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.
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
Kontrola efektu
Okno wyboru czcionki
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) | oknoNotatnika | Od razu wiadomo, że to główne okno programu. |
Text | Notatnik 4P | Tytuł okna. |
StartPosition | CenterScreen | Start na środku ekranu. |
MinimumSize | 700; 450 | Ochrona czytelnego układu. |
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 |
|---|---|---|
ToolStripStatusLabel | btznaki | Znaki: |
ToolStripStatusLabel | btwiersze | Wiersze: |
7 · Krok 5
Niewidoczne narzędzia pod formularzem
| Komponent z Toolbox | `(Name)` | Zadanie |
|---|---|---|
OpenFileDialog | openFileDialog1 | Wybór istniejącego pliku. |
SaveFileDialog | saveFileDialog1 | Wybór nazwy i miejsca zapisu. |
PrintDocument | printDocument1 | Przygotowanie treści strony do druku. |
PrintDialog | printDialog1 | Wybór drukarki i ustawień. |
FontDialog | fontDialog1 | Wybór kroju, stylu i rozmiaru tekstu. |
8 · Krok 6
Które metody podpiąć
| Element | Zdarzenie | Metoda |
|---|---|---|
PoleTekstowe | TextChanged | PoleTekstowe_TextChanged (podpinana w konstruktorze, nie w projektancie) |
| pozycje menu Plik | Click | nowyToolStripMenuItem_Click, otworzToolStripMenuItem_Click, zapiszToolStripMenuItem_Click, zapiszJakoToolStripMenuItem_Click, drukujToolStripMenuItem_Click |
| pozycje menu Edycja | Click | cofnijToolStripMenuItem_Click, ponowToolStripMenuItem_Click, wytnijToolStripMenuItem_Click, kopiujToolStripMenuItem_Click, wklejToolStripMenuItem_Click, usunToolStripMenuItem_Click |
| menu Format | Click | zawijajWierszeToolStripMenuItem_Click, czcionkaToolStripMenuItem_Click |
printDocument1 | PrintPage | printDocument1_PrintPage |
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.
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;
}
}
}
}
string aktualnyPlik = ""Ścieżka bieżącego pliku. Pusty napis oznacza, że dokument jeszcze nie był zapisany.PoleTekstowe.TextChanged += PoleTekstowe_TextChangedPodpięcie zdarzenia w konstruktorze. Przy każdej zmianie tekstu aktualizujemy pasek stanu.PoleTekstowe.Lines.LengthLiczba wierszy: tablica linii tekstu w kontrolce.openFileDialog1.ShowDialog() == DialogResult.OKOkno wyboru pliku jest modalne. Dalej idziesz tylko po kliknięciu OK.LoadFileiSaveFilezRichTextBoxStreamType.PlainTextWczytanie i zapis zwykłego tekstu bez formatowania RTF.if (aktualnyPlik == "")Pierwszy zapis pyta o nazwę (SaveFileDialog). Następne zapisują wprost doaktualnyPlik.printDialog1.Document = printDocument1Łączy okno wyboru drukarki z dokumentem do druku. Bez tego drukuje pustą stronę.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.PoleTekstowe.WordWrap = !PoleTekstowe.WordWrapPrzełącznik: odwraca bieżącą wartość. Ptaszek w menu odzwierciedla stan (Checked).fontDialog1.Font = PoleTekstowe.FontOkno 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.
// 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();
}
SelectedTextTekst zaznaczony w polu. Gdy nic nie zaznaczono, przypisanie wstawia napis w miejscu kursora.IndexOf(…, OrdinalIgnoreCase)Zwraca położenie pierwszego trafienia albo-1, gdy tekstu nie ma.e.Cancel = truePrzerywa zamykanie okna. ZdarzenieFormClosingpodpinasz w oknie Właściwości formularza.
10 · Krok 7
Uruchom i sprawdź każdą funkcję
| Test | Oczekiwany 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.txt | Plik powstaje w wybranym folderze. |
| Dopisz tekst i znów Plik → Zapisz | Zapis do tego samego pliku, bez pytania o nazwę. |
| Otwórz zapisany plik | Zapisana treść wraca do pola. |
| Cofnij, Ponów, Wytnij, Kopiuj, Wklej | Każda pozycja zmienia tekst zgodnie z nazwą. |
| Format → Zawijaj wiersze, bardzo długi tekst | Tekst przełącza się między zawijaniem a przewijaniem poziomym. |
| Format → Czcionka | Zmienia się krój lub rozmiar tekstu. |
| Plik → Drukuj | Pojawia się systemowe okno wyboru drukarki. |
11 · Czego unikać
Cztery najczęstsze kłopoty
Nazwa nie istnieje w bieżącym kontekście
poleTekstowe.Clear(); // w projektancie: PoleTekstoweBłąd CS0103: wielkość liter ma znaczenie.
PoleTekstowe.Clear();Nazwa w kodzie musi być identyczna z (Name) w projektancie.
„Zapisz” zawsze pyta o nazwę
saveFileDialog1.ShowDialog();
// brak: aktualnyPlik = saveFileDialog1.FileName;Zmienna aktualnyPlik zostaje pusta, więc program nie pamięta pliku.
aktualnyPlik = saveFileDialog1.FileName;Po pierwszym zapisie przypisz ścieżkę do aktualnyPlik.
Drukuje pustą stronę
printDocument1.Print(); // bez przypisania DocumentWydruk jest pusty lub nie powstaje.
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
public Form1()
{
InitializeComponent();
}Etykiety Znaki i Wiersze pokazują tylko tekst początkowy.
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.
Podsumowanie
Co warto zapamiętać
RichTextBoxdaje gotowe Undo, Redo, Copy, Cut, Paste orazLoadFileiSaveFile.- Nazwy z
(Name)używasz w kodzie, a muszą być identyczne (także wielkość liter). - Okna
OpenFileDialog,SaveFileDialog,FontDialogsą modalne, a wynik sprawdzasz przezDialogResult.OK. aktualnyPlikodróżnia pierwszy zapis (pytanie o nazwę) od kolejnych (zapis wprost).- Druk wymaga
PrintDialog.Document = printDocument1oraz zdarzeniaPrintPage.
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ę →