Pasek postępu w konsoli C# — kolory, spinner i PadRight

Twój program kopiuje pliki. Trwa to dziesięć sekund, a przez te dziesięć sekund na ekranie nic się nie dzieje. Użytkownik nie wie, czy program pracuje, czy się zawiesił — więc zamyka okno. Pasek postępu rozwiązuje ten problem i w konsoli C# zrobisz go w kilkunastu linijkach, bez żadnej biblioteki.
Ten wpis to wersja tekstowa odcinka Program .NET Developer #5 na YouTube. Kod jest ten sam co w filmie, a każdy wynik uruchomiłem na .NET 10. Po drodze program napisze na ekranie Gotowe!nie: 100% — pokażę, skąd się bierze to „nie”, bo ten sam błąd zrobisz przy każdym napisie, który zmienia się w miejscu. Zakładam, że znasz pętle w C#; resztę wyjaśniam po drodze.
Punkt wyjścia: WriteLine i ściana tekstu
Najprostsza wersja: pętla liczy od 0 do 100 i wypisuje każdy procent.
for (int i = 0; i <= 100; i++)
{
Console.WriteLine($"Kopiowanie: {i}%");
Thread.Sleep(100);
}
Console.WriteLine("Gotowe!");Console.WriteLine wypisuje tekst i przechodzi do nowej linii. Thread.Sleep(100) usypia program na 100 milisekund — tu udaje prawdziwą pracę, a 101 obrotów pętli to około 10 sekund. Wynik:
Kopiowanie: 0%
Kopiowanie: 1%
Kopiowanie: 2%
…
Kopiowanie: 100%
Gotowe!Działa, ale to 102 linie. Każdy procent w osobnym wierszu, a wszystko, co było na ekranie wcześniej, ucieka do góry. Chcemy jednej linii, w której liczba zmienia się w miejscu. Więcej o tym, jak konsola przyjmuje i wypisuje dane, przeczytasz we wpisie o wejściu i wyjściu w C#.
Zasada:
WriteLine= tekst + nowa linia. Do napisu, który ma się zmieniać w miejscu, potrzebujesz czegoś innego.
Write i powrót karetki \r — tekst zmienia się w miejscu
Dwie zmiany: Write zamiast WriteLine i znak \r na początku napisu.
for (int i = 0; i <= 100; i++)
{
Console.Write($"\rKopiowanie: {i}%");
Thread.Sleep(100);
}
Console.WriteLine("\rGotowe!");Console.Write wypisuje tekst, ale nie przechodzi do nowej linii — kursor zostaje na końcu napisu. Znak \r (ukośnik i litera r) to jeden znak specjalny: powrót karetki. Nazwa pochodzi z maszyn do pisania, w których karetka wracała na początek wiersza. W konsoli jest tak samo: kursor wraca na początek tej samej linii, więc kolejny procent pisze się na poprzednim. Ten sam znak stoi przed Gotowe!, żeby słowo zaczęło się od początku linii.
Procenty rzeczywiście zmieniają się w miejscu. A na końcu na ekranie zostaje:
Gotowe!nie: 100%Program skończył pracę i sam sobie zaprzeczył.
Pułapka: powrót karetki nie czyści linii
\r przenosi kursor na początek linii — i nic więcej. Nie kasuje tego, co tam było. Na ekranie stało Kopiowanie: 100%, czyli 16 znaków. Gotowe! ma 7 znaków, więc nadpisało tylko siedem pierwszych liter: Kopiowa. Reszta starego napisu — nie: 100% — została na miejscu.
Sprawdź sam, jak to wygląda znak po znaku, także dla innych napisów:
Co zostanie na ekranie po \r?
Wszystkie warianty ze wstawki uruchomiłem na .NET 10 i odtworzyłem tak, jak rysuje je konsola:
| Stara linia | Nowy napis | Na ekranie |
|---|---|---|
Kopiowanie: 100% (16 zn.) | "\rGotowe!" (7 zn.) | Gotowe!nie: 100% |
Kopiowanie: 100% | "\rOK" (2 zn.) | OKpiowanie: 100% |
Kopiowanie: 100% | "\rSkopiowano 100 plików" (21 zn.) | Skopiowano 100 plików |
Kopiowanie: 100% | "\r" + "OK".PadRight(50) | OK |
Problem wychodzi tylko wtedy, gdy nowy napis jest krótszy od starego. Dlatego łatwo go przeoczyć: dopóki teksty rosną (9% → 10% → 100%), wszystko wygląda dobrze.
Naprawa: PadRight dopełnia napis spacjami
for (int i = 0; i <= 100; i++)
{
Console.Write($"\rKopiowanie: {i}%");
Thread.Sleep(100);
}
Console.WriteLine("\r" + "Gotowe!".PadRight(50));PadRight(50) — po polsku „dopełnij z prawej do 50 znaków” — dokleja do napisu tyle spacji, żeby miał razem 50 znaków. Spacja też jest znakiem, więc zamazuje resztki starego tekstu. Na ekranie zostaje samo Gotowe!. Liczba 50 jest z zapasem, bo nasza linia jeszcze się wydłuży. PadRight przydaje się też do wyrównywania kolumn — przykłady znajdziesz we wpisie o generowaniu dokumentów i raportów.
Zasada:
\rnie czyści linii. Jeśli nowy tekst jest krótszy od starego, zamaż różnicę spacjami — najprościej przezPadRight.
Pasek z kratek: new string('█', ile)
Teraz prawdziwy pasek z 20 kratek:
Console.OutputEncoding = System.Text.Encoding.UTF8;
for (int i = 0; i <= 100; i++)
{
int pelne = i / 5;
string pasek = new string('█', pelne) + new string('░', 20 - pelne);
Console.Write($"\rKopiowanie [{pasek}] {i}%");
Thread.Sleep(100);
}
Console.WriteLine("\r" + "Gotowe!".PadRight(50));Trzy rzeczy w tym kodzie:
Console.OutputEncoding = System.Text.Encoding.UTF8;mówi konsoli, w jakim zapisie dostaje znaki. Dzięki temu kratki█i░wyglądają tak samo na każdym komputerze.int pelne = i / 5;— ile kratek jest pełnych. 100 / 5 = 20, czyli pasek pełny; 37 / 5 = 7. To dzielenie liczb całkowitych, więc część po przecinku po prostu odpada (37 / 5 to 7, a nie 7,4). Więcej o tym we wpisie o operatorach w C#.new string('█', pelne)tworzy napis zpelnetakich samych znaków. Do niego doklejamy jasne kratki na resztę:20 - pelne. Razem zawsze 20 znaków, więc pasek ma stałą szerokość.
W trakcie, przy 37%, na ekranie jest:
Kopiowanie [███████░░░░░░░░░░░░░] 37%Na końcu PadRight(50) zamazuje cały pasek i zostaje Gotowe!.
Kolory: ForegroundColor i ResetColor
Console.OutputEncoding = System.Text.Encoding.UTF8;
for (int i = 0; i <= 100; i++)
{
int pelne = i / 5;
string pasek = new string('█', pelne) + new string('░', 20 - pelne);
if (i < 34)
Console.ForegroundColor = ConsoleColor.Red;
else if (i < 67)
Console.ForegroundColor = ConsoleColor.Yellow;
else
Console.ForegroundColor = ConsoleColor.Green;
Console.Write($"\rKopiowanie [{pasek}] {i}%");
Console.ResetColor();
Thread.Sleep(100);
}
Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine("\r" + "Gotowe!".PadRight(50));
Console.ResetColor();Console.ForegroundColor to kolor liter, które konsola napisze od tej chwili. Poniżej 34% pasek jest czerwony, poniżej 67% żółty, dalej zielony. Zaraz po wypisaniu linii Console.ResetColor() przywraca kolor konsoli. Bez tego konsola zostałaby kolorowa także po końcu programu — kolor to ustawienie konsoli, a nie jednego napisu.
Zasada: kolor ustawiasz przed
Write, a zaraz po nim przywracasz go przezResetColor.
Spinner i schowany kursor
Ostatnie dwa szlify: kręcący się znak na początku linii i schowany kursor.
Console.OutputEncoding = System.Text.Encoding.UTF8;
Console.CursorVisible = false;
char[] spinner = { '|', '/', '-', '\\' };
for (int i = 0; i <= 100; i++)
{
int pelne = i / 5;
string pasek = new string('█', pelne) + new string('░', 20 - pelne);
if (i < 34)
Console.ForegroundColor = ConsoleColor.Red;
else if (i < 67)
Console.ForegroundColor = ConsoleColor.Yellow;
else
Console.ForegroundColor = ConsoleColor.Green;
Console.Write($"\r{spinner[i % 4]} Kopiowanie [{pasek}] {i}%");
Console.ResetColor();
Thread.Sleep(100);
}
Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine("\r" + "Gotowe!".PadRight(50));
Console.ResetColor();
Console.CursorVisible = true;Spinner to tablica czterech znaków: |, /, - i \. Ostatni zapisujemy jako '\\' — pojedynczy ukośnik w C# zaczyna znak specjalny (jak \r), więc sam ukośnik trzeba zapisać podwójnie. Który znak pokazać, wybiera i % 4 — reszta z dzielenia przez 4, czyli zawsze 0, 1, 2 albo 3. Znaki idą po kolei w kółko i wygląda to, jakby kreska się kręciła.
Console.CursorVisible = false chowa mrugający kursor, który psułby efekt na końcu paska. Na końcu programu oddajemy go: Console.CursorVisible = true.
Sprawdź sam: pasek dla dowolnego procentu
Przesuń suwak — wstawka liczy linię dokładnie tak jak 06-Spinner.cs: kratki z i / 5, znak spinnera z i % 4 i kolor z progów 34 i 67.
06-Spinner.cs — jedna klatka pętli
Pułapki, które wyjdą w praktyce
1. CursorVisible przy przekierowanym wyjściu rzuca wyjątek. Uruchomiłem 06-Spinner.cs z wynikiem zapisywanym do pliku (dotnet run 06-Spinner.cs > wynik.txt):
Unhandled exception. System.IO.IOException: Nieprawidłowe dojście.
at System.ConsolePal.set_CursorVisible(Boolean value)Przy przekierowaniu nie ma okna konsoli, którego kursor można schować. Ten sam plik uruchomiony w zwykłym oknie konsoli (albo w Visual Studio przez Ctrl+F5) kończy się poprawnie. Jeśli twój program może działać bez okna — na przykład w zaplanowanym zadaniu albo z wynikiem do pliku — sprawdź najpierw Console.IsOutputRedirected.
2. Kolory też znikają przy przekierowaniu. W pliku z wynikiem 05-Kolory.cs nie ma ani jednego kodu koloru — .NET wysyła kolory tylko do prawdziwej konsoli. To nie błąd, ale nie licz, że kolory zobaczysz w logu.
3. Krótszy tekst po dłuższym. To nasze Gotowe!nie. Dotyczy każdego napisu z \r: licznika, czasu do końca, nazwy kopiowanego pliku. Dopełnij spacjami albo pisz zawsze tak samo długi tekst.
4. Zapomniany ResetColor. Jeśli program skończy się (albo rzuci wyjątek) po ustawieniu koloru, konsola może zostać w tym kolorze. Przywracaj kolor zaraz po wypisaniu.
Ściągawka
| Co | Kod | Po co |
|---|---|---|
| tekst bez nowej linii | Console.Write(…) | kursor zostaje na końcu napisu |
| powrót na początek linii | "\r…" | kolejny tekst pisze się na poprzednim |
| zamazanie resztek | "Gotowe!".PadRight(50) | spacje do 50 znaków |
| napis z jednego znaku | new string('█', pelne) | kratki paska |
| kolor liter | Console.ForegroundColor = ConsoleColor.Green | od tej chwili zielone |
| przywrócenie koloru | Console.ResetColor() | konsola wraca do swojego koloru |
| znak w kółko | spinner[i % 4] | 0, 1, 2, 3, 0, 1… |
| kursor | Console.CursorVisible = false | tylko w prawdziwym oknie konsoli |
Zadanie: 40 kratek
Zrób pasek z 40 kratek zamiast 20:
Console.OutputEncoding = System.Text.Encoding.UTF8;
for (int i = 0; i <= 100; i++)
{
int pelne = i * 40 / 100;
string pasek = new string('█', pelne) + new string('░', 40 - pelne);
Console.Write($"\rKopiowanie [{pasek}] {i}%");
Thread.Sleep(100);
}
Console.WriteLine("\r" + "Gotowe!".PadRight(70));- Uruchom i sprawdź, ile kratek jest pełnych przy 37%.
- Celowo zepsuj: usuń
.PadRight(70)z ostatniej linii (zostaw"\rGotowe!") i zobacz, co zostanie na ekranie.
Napisz w komentarzu pod filmem, jakie słowo ci wyszło. Odpowiedź jest w przypiętym komentarzu.
Podsumowanie
Write+\rzmienia tekst w miejscu:Writenie przechodzi do nowej linii, a\rwraca na jej początek.\rnie czyści linii. Krótszy napis zostawia resztki starego — stądGotowe!nie: 100%. Naprawa:PadRight.- Pasek to
new string('█', pelne) + new string('░', 20 - pelne), gdziepelne = i / 5. - Kolor ustawiasz przed
Writei od razu przywracasz przezResetColor. CursorVisibledziała tylko w prawdziwym oknie konsoli — przy przekierowanym wyjściu rzucaIOException.
🎥 Wersja wideo: ten sam przykład krok po kroku, kod pisany na żywo w Visual Studio — Program .NET Developer #5 — Pasek postępu w konsoli
🎓 Więcej takich programów: Program .NET Developer — od podstaw C# do aplikacji webowych, krok po kroku.
📘 Darmowa roadmapa .NET Developera: dev-hobby.pl/lista-vip
Zobacz
- Interpolacja stringów w C# — logi i błędy — skąd się bierze
$"…{i}%" - TimeSpan w C# — Hours vs TotalHours i pełne godziny — jak wypisać pozostały czas obok paska
- Debugowanie w C# — breakpointy i watch — gdy pasek pokazuje coś dziwnego
- Pierwszy program w C# — krok po kroku z kodem — jeśli dopiero zaczynasz
🚀 Co dalej?
Zobacz to w praktyce na wideo i pobierz darmową roadmapę, żeby ułożyć naukę w spójną ścieżkę do pierwszej pracy.
- 🗺️ Pobierz darmową roadmapę Junior .NET Developer — 12 poziomów od podstaw C# do pierwszej pracy: dev-hobby.pl
- 🎬 Subskrybuj kanał YouTube — nowe filmy co tydzień.
Zamień wiedzę w umiejętności
Pobierz darmową Roadmapę .NET i ułóż takie tematy jak ten w spójną ścieżkę do pierwszej pracy.
Pobieram roadmapę →