PROGRAMOWANIE – PRZYKŁADY
IDE C++ WinBGI C# Wpadki

Przykład C#

Kalendarz miesięczny Operacje konsolowe Okno i bufor konsoli Pobieranie i analiza kodu znaku Program w Visual C# Program w Visual C# (wersja 2) Poprzedni przykład Następny przykład Program w C++ Kontakt

Kalendarz miesięczny

Numery dni w kalendarzu miesięcznym są zazwy­czaj zesta­wione w siedmiu kolu­mnach lub wier­szach odpowia­dających kolejnym dniom tygodnia opisanych nazwami lub ich skrótami. Na przy­kład paździer­nik 2018 roku może być przedsta­wiony w ukła­dzie kolu­mnowym nastę­pująco:

    Październik 2018
  Nd Po Wt Śr Cz Pt So
      1  2  3  4  5  6
   7  8  9 10 11 12 13
  14 15 16 17 18 19 20
  21 22 23 24 25 26 27
  28 29 30 31

Podobnie jak w przykładzie C++, program w C# ma rozpo­czynać działanie od wyświe­tlenia w oknie konsoli bieżą­cego miesiąca w powyż­szym układzie i umożli­wiać przecho­dzenie do kolejnych miesięcy za pomocą klawiszy strzałek (góra, dół), a kończyć wyko­nanie, gdy użytko­wnik wybierze klawisz Esc.

Operacje konsolowe

Klasa Console zdefiniowana w przestrzeni System pełni ważną rolę w aplika­cjach konso­lowych, reali­zuje bowiem wiele skompliko­wanych operacji związanych z pobie­raniem danych z klawia­tury i wyświe­tlaniem tekstu na ekranie. Najczę­ściej używanymi metodami klasy ConsoleReadLineWriteLine. Klasa udostępnia wiele innych metod i właści­wości. Część z nich przypomina funkcje i podpro­gramy modułu Crt języka Turbo Pascal i biblio­teki conio Borland C++ 5.5 (niektóre, jak stero­wanie pozycją kursora i rozmiarem okna, znalazły zastoso­wanie w programie genero­wania trójkąta Pascala). Nawet kolory tekstu i tła, których jest również 16, mają podobne nazwy i numery, np. kolor czarny ma nazwę Black (typ wylicze­niowy ConsoleColor) i numer 0. Należy zazna­czyć, że szero­kość okna konso­lowego jest wyra­żana w liczbie kolumn tekstu (czcionka stałej szero­kości), wyso­kość w liczbie jego wierszy, a pozycja kursora jest określana jako liczba kolumn tekstu od lewego brzegu okna i liczba wierszy od jego górnego brzegu (lewy górny róg okna ma pozycję 0,0). Oto podsta­wowe składowe klasy Console:

BackgroundColor Kolor tła (właściwość typu ConsoleColor)
BufferHeight Wysokość obszaru bufora konsoli (właściwość)
BufferWidth Szerokość obszaru bufora konsoli (właściwość)
Clear Czyści bufor i okno konsoli, ustawia kursor w lewym górnym rogu okna
CursorLeft Odległość kursora od lewego brzegu okna (właściwość)
CursorTop Odległość kursora od górnego brzegu okna (właściwość)
CursorVisible Wskazuje, czy widoczny jest kursor (właściwość typu bool)
ForegroundColor Kolor tekstu (właściwość typu ConsoleColor)
KeyAvailable Wskazuje, czy naciśnięty jest klawisz (właściwość typu bool)
LargestWindowHeight Największa możliwa wysokość okna (właściwość)
LargestWindowWidth Największa możliwa szerokość okna (właściwość)
Read Wczytuje jeden znak i zwraca jego kod ASCII
ReadKey Wczytuje jeden znak i zwraca wartość typu ConsoleKeyInfo
ReadLine Wczytuje łańcuch aż do naciśnięcia klawisza Enter
ResetColor Ustawia domyślny kolor tła i tekstu
SetBufferSize Ustawia rozmiar obszaru buffora konsoli
SetCursorPosition Ustawia kursor na określonej pozycji znakowej
SetWindowPosition Ustawia pozycję okna konsoli w buforze
SetWindowSize Ustawia rozmiar okna konsoli
Title Tytuł okna konsoli (właściwość)
WindowHeight Wysokość okna konsoli (właściwość)
WindowLeft Pozycja pozioma okna konsoli w buforze (właściwość)
WindowTop Pozycja pionowa okna konsoli w buforze (właściwość)
WindowWidth Szerokość okna konsoli (właściwość)
Write Wysyła tekst do okna bez powrotu karetki
WriteLine Wysyła tekst do okna wraz z powrotem karetki

Opracowana w poprzednim przykładzie C# klasa obliczeń kalenda­rzowych Kalend ułatwia zbudo­wanie programu wyświe­tlającego w oknie konsoli kalen­darz miesię­czny, pozwala bowiem ustalić, ile dni ma dany miesiąc (dwuargu­mentowa metoda Dmax) i jaki dzień tygodnia go rozpo­czyna (metoda Dtyg). Wiadomo więc, od której kolumny zacząć wypisy­wanie numerów dni i na jakim numerze zakończyć. Wstępna wersja metody Miesiąc pokazu­jącej kalen­darz dla określo­nego miesiąca może wyglądać nastę­pująco:

string[] Mc = {"Styczeń", "Luty", "Marzec", "Kwiecień", "Maj", "Czerwiec", "Lipiec",
               "Sierpień", "Wrzesień", "Październik", "Listopad", "Grudzień"};

void Miesiąc(int m, int r)       // m - miesiąc, r - rok
{
    Console.Clear();
    Console.CursorLeft = (17 - Mc[m-1].Length) / 2;
    Console.WriteLine("{0} {1}", Mc[m-1], r);
    Console.WriteLine(" Nd Po Wt Śr Cz Pt So");
    int n = Kalend.Dtyg(1, m, r), max = Kalend.Dmax(m, r);
    Console.CursorLeft = 3 * n;
    for (int d = 1; d <= max; d++)
    {
        Console.Write("{0,3}", d);
        if ((d + n) % 7 == 0)
            Console.WriteLine();
    }
}

Metoda będzie wywoływana w programie wiele razy dla różnych miesięcy, dlatego na początku czyści okno. Następnie wypisuje nagłówek z nazwą miesiąca i numerem roku (pierwszy wiersz) oraz skrótami nazw dni tygodnia (drugi wiersz). Właściwość Length zwracająca długość łańcucha C# została użyta w wyra­żeniu wylicza­jącym wielkość odstępu od lewego brzegu okna potrze­bnego do wycentro­wania pierw­szego wiersza zależną od długości nazwy miesiąca. Po nagłówku metoda wypisuje w pętli for numery dni do pól o szero­kości 3 znaków. Przed pętlą oblicza, w jakim dniu tygodnia przypada pierwszy dzień miesiąca (wartość zmiennej n) i ile dni ma miesiąc (wartość zmiennej max), a przed wypisaniem pierwszego numeru tworzy odpowie­dniej wielkości odstęp, aby jedynka znalazła się dokładnie pod skrótem nazwy pierw­szego dnia miesiąca. Po każdym numerze odpowia­dającym sobocie przechodzi na początek nastę­pnego wiersza, aby numery przypi­sane niedzieli były wypisy­wane przy lewym brzegu okna.

Okno i bufor konsoli

Bufor konsoli można traktować jak prostokątny obszar służący do przecho­wywania tekstu, którego część lub całość ukazuje okno umożli­wiające wykony­wanie różnych operacji dotyczą­cych wyświe­tlania zawartego w nim tekstu (rys.). Rozmiar okna (bez ramki i paska tytu­łowego) z uwzglę­dnieniem jego pozycji w buforze nie może przekra­czać rozmiaru bufora, w prze­ciwnym razie genero­wany jest wyjątek. Jeżeli rozmiar bufora jest większy niż rozmiar okna, pojawia się jeden lub dwa paski przewi­jania przy dolnym i prawym brzegu okna.

Poniższy kod na wstępie wypro­wadza do okna konsoli sześć wierszy tekstu informu­jącego o rozmiarze bufora (66x300), rozmiarze okna (66x19) i jego pozycji w buforze (0,0). Okno zajmuje górną część bufora, a ponieważ jego wysokość jest mniejsza od wysokości bufora, przy prawym brzegu okna widoczny jest pionowy pasek przewijania.

Console.Title = "Bufor i okno konsoli";
Console.WriteLine("Wiersz 0   Rozmiar bufora:");
Console.WriteLine("Wiersz 1   bx = {0}, by = {1}", Console.BufferWidth, Console.BufferHeight);
Console.WriteLine("Wiersz 2   Rozmiar okna:");
Console.WriteLine("Wiersz 3   wx = {0}, wy = {1}", Console.WindowWidth, Console.WindowHeight);
Console.WriteLine("Wiersz 4   Pozycja okna:");
Console.WriteLine("Wiersz 5   px = {0}, py = {1}", Console.WindowLeft, Console.WindowTop);
Console.ReadKey(true);
Console.WindowTop = 6;
Console.WriteLine("Wiersz 6   Nowa pozycja okna:");
Console.WriteLine("Wiersz 7   nx = {0}, ny = {1}", Console.WindowLeft, Console.WindowTop);
Console.ReadKey(true);
Console.WindowTop = 0;
Console.WriteLine("Wiersz 8   Powrót do początkowej pozycji okna:");
Console.WriteLine("Wiersz 9   rx = {0}, ry = {1}", Console.WindowLeft, Console.WindowTop);

Gdy użytkownik naciśnie dowolny klawisz, nastąpi przewi­nięcie okna w dół o 6 wierszy i dopisanie dwóch wierszy informu­jących o jego nowej pozycji (0,6). Gdy użytko­wnik ponownie naciśnie klawisz, okno zostaje przewi­nięte do pierwo­tnej pozycji i w dwóch nowych wierszach zostaje podana infor­macja o jego nowym poło­żeniu. Wszystkie trzy stany okna są pokazane na poniższym rysunku.

Warto zwrócić uwagę, że dwuargumentowa metoda SetWindowSize klasy Console zmienia rozmiar okna i w razie potrzeby również rozmiar bufora, jeśli jest za mały. Sytuacja taka miała miejsce w pro­gramie genero­wania trójkąta Pascala dla n=18, kiedy to początkowo bufor miał rozmiar 66x300 i okno 66x19, a po zmianie rozmiaru okna zarówno nowa szero­kość okna jak i bufora wyniosła 116 znaków (wysokość bufora okazała się wystar­czająca, toteż pozostała bez zmiany). Wypada jeszcze dodać, że wstępny rozmiar bufora i okna jest ustalany w apli­kacji konsolowej C# na podstawie rozdziel­czości ekranu monitora. I tak np. dla ekranu o rozdziel­czości 1366x768 pikseli rozmiar bufora wynosi 66x300 i okna 66x19, a dla 1600x900 pikseli rozmiary te wynoszą, odpowiednio, 80x300 i 80x25.

Pobieranie i analiza kodu znaku

Metoda ReadKey klasy Console wstrzy­muje wykonanie programu do momentu naciśnię­cia przez użytko­wnika klawisza znaku lub klawisza funkcyj­nego i pobiera znak genero­wany przez ten klawisz. Jeśli jednak bufor klawia­tury nie jest pusty, co można sprawdzić za pomocą właści­wości KeyAvailable, metoda pobiera z niego znak bez oczeki­wania na działanie użytko­wnika. Klawisz znaku lub klawisz funkcyjny można nacisnąć w kombi­nacji z jednym lub kilkoma klawi­szami modyfi­kującymi Alt, Shift i Ctrl (samo naciśnię­cie klawisza modyfi­kującego jest ignoro­wane). Bezargu­mentowa metoda Readky wyświetla w oknie konsoli znak odpowia­dający wciśniętemu klawiszowi, ale można temu zapobiec, wywołując przecią­żoną jej wersję z argu­mentem true. Wartością zwracaną przez metodę jest struktura (uproszczona klasa) typu ConsoleKeyInfo zawie­rająca m.in. składowe:

Znaki generowane przez klawisze funkcyjne mają kody niższe od 32, ich wyświe­tlanie może prowadzić do różnych efektów, jak np. przesu­nięcie kursora. Typ wyliczeniowy ConsoleModifiers obejmuje trzy wartości bitowe: Alt (1), Shift (2) i Control (4). Zatem aby określić, czy naciśnięty został klawisz modyfi­kujący, należy użyć operatora bitowego &. Oto prosty program pozwala­jący prześle­dzić informa­cje o wprowa­dzanych z klawia­tury znakach i ewentu­alnym użyciu klawiszy modyfi­kujących (naciśnięcie klawisza Esc kończy wykonanie programu):

using System;

class Program
{
    static void Main(string[] args)
    {
        ConsoleKeyInfo c;
        do
        {
            c = Console.ReadKey(true);
            int kod = c.GetHashCode();
            Console.Write("Znak: {0}  Kod: {1,-3}  Naciśnięto: ",
                          (kod >= 32) ? c.KeyChar : ' ', kod);
            if ((c.Modifiers & ConsoleModifiers.Alt) != 0) Console.Write("Alt+");
            if ((c.Modifiers & ConsoleModifiers.Shift) != 0) Console.Write("Shift+");
            if ((c.Modifiers & ConsoleModifiers.Control) != 0) Console.Write("Ctrl+");
            Console.WriteLine(c.Key.ToString());
        } while (c.Key != ConsoleKey.Escape);
    }
}

Program w Visual C#

Zgodnie z przyjętymi na początku niniejszej witryny sugestiami przyjmujemy, że program ma rozpoczynać działanie od pobrania bieżącej daty z systemu i wyświe­tlenia aktualnego miesiąca. Następnie ma wczytywać w pętli kolejne znaki z klawiatury i inter­pretować je nastę­pująco:

Jest oczywiste, że czytanie znaku za pomocą omówionej wyżej metody ReadKey klasy Console i porównanie go z warto­ścią Escape typu ConsoleKey można umieścić w warunku konty­nuacji pętli while, a konstru­kcję wyboru nastę­pnego lub poprze­dniego miesiąca można zaprogra­mować za pomocą trójwarian­towej instrukcji if—else—if:

...                         // bieżący miesiąc
ConsoleKeyInfo c;
while ((c = Console.ReadKey(true)).Key != ConsoleKey.Escape)
{
    if (c.Key == ConsoleKey.DownArrow)
    {
        ...                 // następny miesiąc
    }
    else if (c.Key == ConsoleKey.UpArrow)
    {
        ...                 // poprzedni miesiąc
    }
}

Wczytany znak jest kolejno porówny­wany z wartościami Escape (Esc), DownArrow (strzałka w dół) i UpArrow (strzałka w górę). Gdy jest różny od Escape, pętla jest kontynu­owana, przy czym wykonana zostaje tylko instru­kcja po pierw­szym speł­nionym porównaniu z DownArrow lub UpArrow. W przy­padku niezgo­dności z obu wartościami, żadna akcja wobec rozpatry­wanego znaku nie jest podejmo­wana. Skrajne nawiasy {} można pominąć, ale pozosta­wienie ich ułatwia zrozu­mienie kodu źródłowego.

Poniżej zaprezentowana jest pełna wersja programu wyświetla­jącego w oknie konsoli kalendarz miesięczny. Pobranie bieżącej daty w metodzie Main umożli­wiła właściwość Today struktury DateTime omówionej w programie wyzna­czania dnia tygodnia. Z kolei w metodzie Miesiąc, w porównaniu z jej omówionym wyżej pierwo­wzorem, można zauważyć drobne zmiany i uzupeł­nienia. Po pierwsze, nieco rozbu­dowano nagłówek z nazwą miesiąca (podkre­ślenie). Po drugie, na początku tablicy Mc wstawiono pustą refe­rencję null, która nie wska­zuje na żaden obiekt; pozwoliło to na natu­ralną numerację miesięcy od 1 do 12 zamiast od 0 do 11. Po trzecie wreszcie, od pozycji znakowej 2 wier­sza 10 okna umieszczono krótkie menu informu­jące użytko­wnika, jak ma sterować programem.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Kalendarz.Greg;

namespace KMies1
{
    class Program
    {
        static void Main(string[] args)
        {
            DateTime dt = DateTime.Today;
            int m = dt.Month, r = dt.Year;
            Miesiąc(m, r);
            ConsoleKeyInfo c;
            while ((c = Console.ReadKey(true)).Key != ConsoleKey.Escape)
            {
                if (c.Key == ConsoleKey.DownArrow)
                {
                    if (m++ < 12)
                        Miesiąc(m, r);
                    else
                        Miesiąc(m = 1, ++r);
                }
                else if (c.Key == ConsoleKey.UpArrow)
                {
                    if (--m > 0)
                        Miesiąc(m, r);
                    else
                        Miesiąc(m = 12, --r);
                }
            }
        }

        static void Miesiąc(int m, int r)
        {
            Console.Clear();
            Console.CursorLeft = (17 - Mc[m].Length) / 2;
            Console.WriteLine("{0} {1}", Mc[m], r);
            Console.WriteLine(" --------------------\n Nd Po Wt Śr Cz Pt So");
            int n = Kalend.Dtyg(1, m, r), max = Kalend.Dmax(m, r);
            Console.CursorLeft = 3 * n;
            for (int d = 1; d <= max; d++)
            {
                Console.Write("{0,3}", d);
                if ((d + n) % 7 == 0)
                    Console.WriteLine();
            }
            Console.SetCursorPosition(2, 10);
            Console.Write("Menu: Up, Dn, Esc");
        }

        static readonly string[] Mc = {null,
            "Styczeń", "Luty", "Marzec", "Kwiecień", "Maj", "Czerwiec", "Lipiec",
            "Sierpień", "Wrzesień", "Październik", "Listopad", "Grudzień"};
    }
}

Warto zwrócić uwagę na specyficzną cechę języka C# wywodzącą się od języków C/C++ polegającą na umieszczaniu wyrażeń przypisu­jących w innych wyraże­niach, która prowadzi do zwartego i zarazem efekty­wnego kodu. Takie skróty występują m.in. w kodzie metody Main opisującym przejście od grudnia do stycznia nastę­pnego roku i od stycznia do grudnia poprze­dniego roku. Szcze­gólnie ważny bywa kontekst, w którym wystę­pują opera­tory zwięk­szania (++) i zmniej­szania (--), gdyż mogą one wystąpić zarówno przed zmienną, jak i po niej, wpływając w dwójnasób na wartość wyra­żenia. Na przykład w wyrażeniu

m++ < 12

wartość m zostaje zwiększona o 1 po wykorzy­staniu, czyli po porówna­niu jej z 12, zaś w wyrażeniu

--m > 0

wartość ta zostaje zmniejszona o 1 przed wykorzystaniem, czyli przed porówna­niem jej z zerem.

Wynik wykonania tego programu jest identyczny jak jego odpowie­dnika w Visual C++ (rys.).

Program w Visual C# (wersja 2)

W zaprezentowanym wyżej programie wyświetlania kalen­darza miesię­cznego można usunąć tablicę Mc nazw miesięcy i w metodzie Miesiąc zamiast klasy Kalend skorzystać z omówionej w programie wyznaczania dnia tygodnia stru­ktury DateTime i klasy DateTimeFormatInfo zdefinio­wanych w prze­strzeni System.Globalization. Istotnie,

Efektem tych zabiegów jest poniższa druga wersja programu generu­jąca niemal takie same wyniki jak pierwsza. Jedyna różnica tkwi w nazwie miesiąca: pierwszy program wypisuje ją dużą literą, drugi małą.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Globalization;

namespace KMies2
{
    class Program
    {
        static void Main(string[] args)
        {
            DateTime dt = DateTime.Today;
            int m = dt.Month, r = dt.Year;
            Miesiąc(m, r);
            ConsoleKeyInfo c;
            while ((c = Console.ReadKey(true)).Key != ConsoleKey.Escape)
            {
                if (c.Key == ConsoleKey.DownArrow)
                {
                    if (m++ < 12)
                        Miesiąc(m, r);
                    else
                        Miesiąc(m = 1, ++r);
                }
                else if (c.Key == ConsoleKey.UpArrow)
                {
                    if (--m > 0)
                        Miesiąc(m, r);
                    else
                        Miesiąc(m = 12, --r);
                }
            }
        }

        static void Miesiąc(int m, int r)
        {
            Console.Clear();
            string Mc = DateTimeFormatInfo.CurrentInfo.GetMonthName(m);
            Console.CursorLeft = (17 - Mc.Length) / 2;
            Console.WriteLine("{0} {1}", Mc, r);
            Console.WriteLine(" --------------------\n Nd Po Wt Śr Cz Pt So");
            DateTime dt = new DateTime(r, m, 1);
            int n = (int)dt.DayOfWeek;
            int max = DateTimeFormatInfo.CurrentInfo.Calendar.GetDaysInMonth(r, m);
            Console.CursorLeft = 3 * n;
            for (int d = 1; d <= max; d++)
            {
                Console.Write("{0,3}", d);
                if ((d + n) % 7 == 0)
                    Console.WriteLine();
            }
            Console.SetCursorPosition(2, 10);
            Console.Write("Menu: Up, Dn, Esc");
        }
    }
}

Opracowanie przykładu: październik 2018