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

Przykład C#

Przeciąganie myszą Program w C# (mucha) Wypłacalność kwoty Formularz aplikacji Implementacja banknotów i monet Generowanie portfela Gromadzenie kwoty Metoda prób i błędów Program w C# (kwota) Poprzedni przykład Kontakt

Przeciąganie myszą

Jednym z szeroko rozpowszechnionych udogodnień grafi­cznego inter­fejsu użytko­wnika (ang. Graphical User Interface, GUI) jest przecią­ganie obiektu (ikony pliku, karty pasjansa itp.) myszą. Wszystkie kontrolki Windows Forms C# generują kilka­naście zdarzeń związa­nych z kliknię­ciem przycisku myszy i śledze­niem jej ruchów. W zaprezen­towanych poniżej programach technika przecią­gania jest oparta na przechwy­tywaniu trzech zdarzeń formu­larza głównego:

Wygenerowane przez Visual Studio metody ich obsługi zawierają oprócz tradycyj­nego argu­mentu sender klasy object, którego wartością jest refe­rencja do kontrolki wysyła­jącej zdarzenie (tu: okno główne programu), argu­ment e klasy Mouse­EventArgs określa­jący bieżący stan myszy, w tym współ­rzędne jej kursora (wskaźnika) w odnie­sieniu do lewego górnego rogu kontrolki i naci­śnięty lub zwol­niony przycisk udostę­pniane we właści­wościach:

Właściwość Button może przyjmować wartości typu wyliczeniowego MouseButtons identyfi­kujące przycisk myszy, m.in. Left (lewy), Right (prawy) i Middle (środkowy). W zapowie­dzianych progra­mach do przecią­gania myszą używany jest tylko lewy przycisk, a przecią­ganym obiektem jest duszek klasy Sprite (por. program Space­rujący skrzat). Precyzo­wanie operacji zaczynamy od zadekla­rowania trzech zmiennych (pól klasy Form1):

private bool dragging = false;      // Przeciąganie (true/false)
private int xOffset, yOffset;       // Przesunięcie myszy

Zmienna dragging będzie wskazywać, czy odbywa się przecią­ganie obiektu myszą (true – tak, false – nie), zaś zmienne xOffsetyOffset będą określać wielkość przesu­nięcia kursora względem lewego górnego rogu prosto­kąta zajmowa­nego przez duszka (rys.).

Metoda obsługi zdarzenia MouseDown rozpoczyna proces przecią­gania duszka, gdy użytko­wnik nacisnął lewy przycisk myszy w momencie, gdy kursor znajdował się w obrębie prosto­kąta duszka. Zakładając, że duszek jest wskazy­wany przez refe­rencję mucha klasy Sprite, rozpo­częcie przecią­gania go możemy zaprogra­mować następu­jąco:

private void Form1_MouseDown(object sender, MouseEventArgs e)
{
    if ((e.Button == MouseButtons.Left) && mucha.Contains(e.X, e.Y))
    {
        dragging = true;
        xOffset = e.X - mucha.X;
        yOffset = e.Y - mucha.Y;
    }
}

Metody obsługi dwóch pozostałych zdarzeń myszy, MouseMoveMouseUp, są prostsze. Stosowne działania są oczywiście podejmo­wane tylko wtedy, gdy duszek jest przecią­gany, tj. gdy wartością zmiennej dragging jest true. W przy­padku MouseMove duszek zostaje przesu­nięty do miejsca określo­nego przez nową pozycję kursora myszy zmodyfi­kowaną o wartości zapamię­tane w zmiennych xOffsetyOffset:

private void Form1_MouseMove(object sender, MouseEventArgs e)
{
    if (dragging) mucha.Move(e.X - xOffset, e.Y - yOffset);
}

Natomiast w przypadku zdarzenia MouseUp informują­cego o zwol­nieniu przycisku myszy operacja przecią­gania duszka zostaje zakończona – rzecz jasna pod warunkiem, że jest to lewy przycisk:

private void Form1_MouseUp(object sender, MouseEventArgs e)
{
    if (dragging && (e.Button == MouseButtons.Left)) dragging = false;
}

Program w C# (mucha)

Formularz aplikacji umożliwiającej przeciąganie myszą obiektu klasy Sprite przedstawia­jącego muchę zawiera proste menu z ele­mentem Centruj (rys.) poleca­jącym przesu­nięcie obiektu na środek obszaru roboczego okna. Polecenie to jest przydatne zwłaszcza wtedy, gdy użytkownik przecią­gnie obiekt poza okno, przez co stanie się on niedostępny. Rozmiar okna będzie można zmieniać w trakcie wykonania programu (właści­wości FormBorder­StyleMaxi­mizeBox formu­larza pozosta­wione bez zmian).

Pełny kod źródłowy C# klasy formularza zawarty w pliku Form1.cs programu wyświetla­jącego muchę na środku okna i umożliwia­jącego jej przesu­wanie jest poka­zany na poniższym listingu. Obrazem duszka jest jednoklat­kowa bitmapa klatka z warstwą przezroczy­stości pobierana z zasobów programu (plik Mucha.png), zaś tłem bitmapa siatka przedsta­wiająca siatkę rysowaną jasnobłę­kitnym piórem na białej powierz­chni. Wielkość oczek siatki określa stała oczko, a rozmiar bitmapy właści­wość WorkingArea (prostokąt obszaru roboczego ekranu z wyłą­czeniem paska zadań i innych zadoko­wanych elementów) klasy System­Information (infor­macje o bieżącym środo­wisku syste­mowym). Obie bitmapy i duszek są tworzone w metodzie obsługi zdarzenia Load formu­larza.

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using Sprites;

namespace Mucha
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        private const int oczko = 16;       // Rozmiar oczek siatki
        private const int dist = 2;         // Przesunięcie jednostkowe
        private Bitmap klatka = null;       // Obrazek duszka (mucha)
        private Bitmap siatka = null;       // Siatka (tło)
        private Sprite mucha = null;        // Duszek (mucha)
        private bool dragging = false;      // Przeciąganie (true/false)
        private int xOffset, yOffset;       // Przesunięcie myszy

        private void Form1_Load(object sender, EventArgs e)
        {
            klatka = new Bitmap(Properties.Resources.Mucha);
            Rectangle r = SystemInformation.WorkingArea;
            siatka = new Bitmap(r.Width, r.Height);
            using (Graphics g = Graphics.FromImage(siatka))
            {
                g.Clear(Color.White);
                for (int y = menuStrip1.Height + 2 * oczko / 3; y < siatka.Height; y += oczko)
                    g.DrawLine(Pens.LightSkyBlue, 0, y, siatka.Width, y);
                for (int x = 2 * oczko / 3; x < siatka.Width; x += oczko)
                    g.DrawLine(Pens.LightSkyBlue, x, 0, x, siatka.Height);
            }
            mucha = new Sprite(this, siatka, klatka);
        }

        private void Form1_Paint(object sender, PaintEventArgs e)
        {
            e.Graphics.DrawImage(siatka, 0, 0);
        }

        private void Form1_Shown(object sender, EventArgs e)
        {
            if (!mucha.Visible)
            {
                centrujToolStripMenuItem_Click(sender, e);
                mucha.Show();
            }
        }

        private void Form1_FormClosed(object sender, FormClosedEventArgs e)
        {
            if (siatka != null) siatka.Dispose();
            if (klatka != null) klatka.Dispose();
        }

        private void centrujToolStripMenuItem_Click(object sender, EventArgs e)
        {
            mucha.Move((ClientSize.Width - mucha.Width) / 2,
                       (ClientSize.Height - mucha.Height + menuStrip1.Height) / 2);
        }

        private void zakończToolStripMenuItem_Click(object sender, EventArgs e)
        {
            Close();
        }

        private void Form1_KeyDown(object sender, KeyEventArgs e)
        {
            switch (e.KeyCode)
            {
                case Keys.Left:
                    mucha.Move(mucha.X - dist, mucha.Y);
                    break;
                case Keys.Right:
                    mucha.Move(mucha.X + dist, mucha.Y);
                    break;
                case Keys.Up:
                    mucha.Move(mucha.X, mucha.Y - dist);
                    break;
                case Keys.Down:
                    mucha.Move(mucha.X, mucha.Y + dist);
                    break;
            }
        }

        private void Form1_MouseDown(object sender, MouseEventArgs e)
        {
            if ((e.Button == MouseButtons.Left) && mucha.Contains(e.X, e.Y))
            {
                dragging = true;
                xOffset = e.X - mucha.X;
                yOffset = e.Y - mucha.Y;
            }
        }

        private void Form1_MouseMove(object sender, MouseEventArgs e)
        {
            if (dragging) mucha.Move(e.X - xOffset, e.Y - yOffset);
        }
    }

        private void Form1_MouseUp(object sender, MouseEventArgs e)
        {
            if (dragging && (e.Button == MouseButtons.Left)) dragging = false;
        }
}

Uwaga. Metoda Form1_Shown obsługi zdarzenia Shown generowa­nego po utworzeniu okna i pierwszym wyświe­tleniu go centruje niewi­doczny jeszcze obiekt mucha i ukazuje go. Oczywiście można by zawarty w niej kod przenieść do metody Form1_Paint obsługi zdarzenia Paint, ale byłoby to niezbyt eleganckie, gdyż metoda ta udostę­pnia w argu­mencie e obiekt klasy Graphics, który byłby powtórnie tworzony w metodzie Show klasy Sprite.

Dopowiedzmy jeszcze, że program umożliwia również przesu­wanie duszka za pomocą klawiszy strzałek. Operacja ta jest realizo­wana przez metodę obsługi zdarzenia KeyDown wyzwala­nego, gdy naciśnięty zostanie klawisz znaku lub klawisz funkcyjny. Kod wciśnię­tego klawisza udostę­pnia właści­wość KeyCode argumentu e klasy KeyEvent­Args tej metody przyjmu­jąca wartości typu wylicze­niowego Keys. W rozpatry­wanym przypadku są to wartości Left, Right, UpDown, a efektem ich wystąpienia jest przesu­nięcie duszka o dist pikseli w lewo, prawo, górę lub dół.

Poniższy rysunek przedstawia okno programu wyświe­tlone po jego urucho­mieniu.

Wypłacalność kwoty

Problem wypłacalności kwoty polega na określeniu, czy żądana kwota może być dokładnie wypłacona z portfela, a jeśli tak, to z wykorzy­staniem jakich nominałów i jakiej ich liczby. Przyjmiemy, że zawartość portfela jest podana w postaci listy nominałów polskich banknotów i monet wraz z liczbami ich wystąpień. Naszym zamie­rzeniem jest opraco­wanie aplikacji okienkowej umożliwia­jącej użytko­wnikowi uzbie­ranie wygene­rowanej losowo kwoty z wygene­rowanej losowo zawartości portfela lub uzyskanie gotowego rozwią­zania zadania. Program będzie można więc uznać jako grę edukacyjną dla dzieci zaznaja­miającą je z rodzimą walutą i uczącą posługi­wania się nią.

Ze względu na ograniczony obszar roboczy okna aplikacji i rzadziej spotykane w powsze­chnym obiegu banknoty 200 zł, a zwłaszcza o najwyż­szym nominale 500 zł, zawartość portfela będzie obejmo­wała monety od 1 gr do 5 zł i banknoty od 10 do 100 zł. Wszystkie ich nominały od najwyż­szego do najniż­szego możemy zapisać w tablicy liczb całko­witych wyraża­jących ich wartości w przeli­czeniu na grosze:

private int[] Nom = { 10000, 5000, 2000, 1000, 500, 200, 100, 50, 20, 10, 5, 2, 1 };

Formularz aplikacji

Zaprojektowany formularz aplikacji (rys.) zawiera proste menu z polec­eniami Portfel (wyloso­wanie zawar­tości portfela i kwoty do wypła­cenia), Rozwią­zanie (wygenero­wanie gotowego rozwią­zania zadania), Koniec (zakoń­czenie programu) oraz elementem Info zabloko­wanym i wyrównanym do prawego brzegu (właści­wości EnabledAlignment ustawione na FalseRight). Element ten posłuży do wyświe­tlenia kwoty, która ma być wypła­cona z portfela.

Aby uniknąć skalowania obrazu wynikającego ze zmiany rozmiaru okna, właści­wości FormBorder­StyleMaxi­mizeBox formu­larza ustawiamy na Fixed­Single (pojedyncza stała ramka) i False (zablo­kowana maksyma­lizacja). Faktyczny rozmiar okna i wygląd jego powierz­chni roboczej określimy w metodzie obsługi zdarzenia Load formu­larza. Dekla­rujemy w tym celu następu­jące pola klasy Form1:

private const int szer = 1076, wys = 604, xGr = 666;
private const int oczko = 16;     // Rozmiar oczek siatki
private Bitmap siatka = null;     // Tło obszaru roboczego okna

Stałe szer i wys określają szerokość i wysokość obszaru roboczego okna, stała xGr współrzędną x podziału tego obszaru na część lewą (portfel) i prawą (wypłata), oczko rozmiar oczek jasnobłę­kitnej siatki rysowanej na bitmapie wypełnia­jącej obszar roboczy okna, zaś zmienna siatka refe­rencję do tej bitmapy. Bitmapę z podziałem na część lewą o białym tle i prawą o jasnotur­kusowym tle tworzymy w pomocni­czej metodzie Utwórz_Tło, którą wywo­łujemy w metodzie obsługi zdarzenia Load:

private void Utwórz_Tło()
{
    siatka = new Bitmap(szer, wys);
    using (Graphics g = Graphics.FromImage(siatka))
    {
        g.Clear(Color.White);
        g.FillRectangle(Brushes.LightCyan, xGr, menuStrip1.Height, szer, wys);
        for (int y = menuStrip1.Height + 2 * oczko / 3; y < wys; y += oczko)
            g.DrawLine(Pens.LightSkyBlue, 0, y, szer, y);
        for (int x = 2 * oczko / 3; x < szer; x += oczko)
            g.DrawLine(Pens.LightSkyBlue, x, 0, x, wys);
    }
}

private void Form1_Load(object sender, EventArgs e)
{
    Utwórz_Tło();
    ClientSize = siatka.Size;
    Rectangle r = SystemInformation.WorkingArea;
    Location = new Point((r.Width - Width) / 2, (r.Height - Height) / 2);
    ...                           // Operacje przygotowawcze związane
    ...                           // z implementacją portfela i kwoty
}

W niedokończonej jeszcze motodzie Form1_Load po utworzeniu bitmapy tła oraz ustawieniu rozmiaru obszaru roboczego okna i wyzna­czeniu prosto­kąta wolnej części ekranu okno zostaje wyśrod­kowane. Gdy metoda obsługi zdarzenia Paint będzie wykony­wana po raz pierwszy, wyświetli na ekranie czystą siatkę wyobraża­jącą pusty portfel (rys.). Użytkownik nie zdąży jej jednak zobaczyć, gdyż natych­miast tło okna zostanie w metodzie obsługi zdarzenia Shown uzupeł­nione wizerun­kami banknotów i monet wygenero­wanej losowo zawar­tości portfela.

Implementacja banknotów i monet

Reprezentantami banknotów i monet będą w programie obiekty klasy wywodzącej się od klasy Sprite. Nazwijmy ją Money (pieniądze). Nowa klasa powinna udostę­pniać nominał banknotu lub monety. Jej defi­nicję ze względu na małą obiętość kodu źródło­wego i wykorzy­stanie tylko w rozpatry­wanym programie warto umieścić wewnątrz klasy Form1. Nowym polem klasy Money jest przekazy­wany w argu­mencie konstru­ktora indeks elementu omówionej powyżej tablicy Nom określa­jącego nominał banknotu lub monety reprezen­towanej przez tworzony obiekt:

private class Money : Sprite            // Klasa zagnieżdżona
{
    public int indeks;

    public Money(Form okno, Bitmap tło, Bitmap klatka, int iNom, int x = 0, int y = 0)
          : base(okno, tło, klatka, 1, x, y)
    {
        indeks = iNom;
    }
}

Rzecz jasna konstruktor zagnieżdżonej w ten sposób klasy pochodnej inicja­lizuje wszystkie pola odziedzi­czone po klasie bazowej, wywołując jej konstru­ktora. Grafi­czny wizerunek obiektu reprezentu­jącego banknot lub monetę nie zmienia się w czasie, toteż podobnie jak w przy­padku duszka muchy jego bitmapa jest pojedynczą klatką animacji. Potrzebne wzory polskich banknotów i monet obiegowych zostały ściągnięte z serwisu interne­towego Narodo­wego Banku Polskiego i po dostoso­waniu do wymogów programu zapisane w jego zasobach (pliki g01.png, g02.png, ..., z50.pngzs1.png odpowia­dające nominałom 1 gr, 2 gr, ..., 50 zł i 100 zł). Mając obrazki banknotów i monet, możemy przystąpić do uzupeł­nienia klasy formularza o następu­jące pola:

private Bitmap[] klatka = null;         // Bitmapy banknotów i monet
private Stack<Money>[] portfel = null;  // Obiekty zawartości portfela
private List<Money> wypłata = null;     // Obiekty zebrane do wypłaty
private Random gen;                     // Generator liczb pseudolosowych

Elementami tablicy klatka będą bitmapy stanowiące graficzne wizerunki używanych w pro­gramie banknotów i monet, po jednej bitmapie dla każdego nominału wyszczegól­nionego w tablicy Nom. Z kolei elemen­tami tablicy portfel będą struktury danych implemen­tujące stos genery­czny złożone z obiektów klasy Money o jedna­kowym indeksie, po jednej strukturze dla każdego nominału. Tak więc zawartość portfela będzie składać się ze stosu banknotów o nomi­nale 100 zł, stosu banknotów o nomi­nale 50 zł itd., aż do stosu monet o nominale 1 gr. Ich wyso­kości (liczby elementów) będą losowane za pomocą genera­tora liczb pseudolo­sowych klasy Random (może się zdarzyć, że niektóre stosy będą puste). Zestaw banknotów i monet groma­dzony w celu wypła­cenia żądanej kwoty udostę­pniany przez refe­rencję wypłata będzie listą genery­czną złożoną z elementów klasy Money o różnych indeksach (nominałach). Kod źródłowy odpowia­dający za utworzenie wszystkich tych obiektów może wyglądać następu­jąco:

klatka = new Bitmap[] {
    new Bitmap(Properties.Resources.zs1), new Bitmap(Properties.Resources.z50),
    new Bitmap(Properties.Resources.z20), new Bitmap(Properties.Resources.z10),
    new Bitmap(Properties.Resources.z05), new Bitmap(Properties.Resources.z02),
    new Bitmap(Properties.Resources.z01), new Bitmap(Properties.Resources.g50),
    new Bitmap(Properties.Resources.g20), new Bitmap(Properties.Resources.g10),
    new Bitmap(Properties.Resources.g05), new Bitmap(Properties.Resources.g02),
    new Bitmap(Properties.Resources.g01) };
portfel = new Stack<Money>[Nom.Length];
for (int k = 0; k < portfel.Length; k++)
    portfel[k] = new Stack<Money>();
wypłata = new List<Money>();
gen = new Random();

Kod ten umieszczamy w miejscu oznaczonym trójkropkami i komentarzami w przedsta­wionej powyżej metodzie obsługi zdarzenia Load formu­larza. Naturalnie wszystkie bitmapy tablicy klatka i bitmapę siatka niszczymy w metodzie obsługi zdarzenia FormClosed.

Generowanie portfela

Zawartość portfela jest generowana losowo na początku wykonania programu (w metodzie obsługi zdarzenia Shown przy zerowej kwocie do wypłacenia) i po każdym wybraniu pole­cenia Portfel. Wyświe­tlanie banknotów i monet wymaga precyzo­wania ich położenia w obrzarze roboczym okna. Przyjmiemy, że elementy leżące na dnie stosu zajmują pozycje o współrzę­dnych określo­nych w tabli­cach xPozyPoz, a każdy następny element stosu jest przesu­nięty względem poprze­dniego, na którym leży, o dist pikseli w prawo i w dół. Wyznaczone eksperymen­talnie współ­rzędne w obydwu tablicach, wielkość przesu­nięcia elementów stosu, maksymalną wysokość stosu i wstępną wartość kwoty do wydania dekla­rujemy w następu­jących polach klasy Form1:

private int[] xPoz = { 13,  13,  13, 344, 386, 386, 372, 562, 504, 576, 492, 480, 578 };
private int[] yPoz = { 32, 230, 422, 430,  32, 154, 264,  32, 104, 192, 232, 332, 340 };
private const int dist = 4;         // Przesunięcie elementu stosu
private const int nMax = 6;         // Maksymalna wysokość stosu
private int Kwota = 0;              // Kwota do wypłacenia (w groszach)

Nietrudno jest teraz sformułować metodę pomocniczą wstawiającą banknot lub monetę reprezen­towaną przez argu­ment klasy Money na wierz­chołek stosu o określonym przez ten argument indeksie:

private void Do_Portfela(Money p)
{
    int d = dist * portfel[p.indeks].Count;
    portfel[p.indeks].Push(p);
    p.Move(xPoz[p.indeks] + d, yPoz[p.indeks] + d);
    if (!p.Visible) p.Show();
}

Do portfela mogą być wstawiane zarówno nowe obiekty, które są bezpo­średnio po utworzeniu niewi­doczne i wymagają wyświe­tlenia, jak i obiekty widoczne na liście zgroma­dzonej do wypła­cenia kwoty, które są przeno­szone z powrotem do portfela celem przywró­cenia jego pierwotnej zawartości. Operację opróżniania listy możemy sformu­łować w postaci pętli zdejmu­jącej ostatni element listy aż do jej wyczer­pania i wstawia­jącej go do portfela:

private void Przywróć_Portfel()
{
    while (wypłata.Count > 0)
    {
        Money p = wypłata.Last();
        wypłata.RemoveAt(kwota.Count - 1);
        Do_Portfela(p);
    }
}

Metoda obsługi zdarzenia Click polecenia Portfel generuje nową zawartość portfela i kwotę do wypła­cenia. Najpierw przywraca dotychcza­sową zawartość portfela, a następnie koryguje wysokości jego stosów banknotów i monet zgodnie z wyloso­wanymi liczbami elementów. Każdy stos o niższej od wyloso­wanej wysokości jest uzupe­łniany nowymi elemen­tami klasy Money, a każdy o wyższej wysokości jest obniżany poprzez zdejmo­wanie z jego wierz­chołka elementów nadmia­rowych i ukry­wanie ich:

private void portfelToolStripMenuItem_Click(object sender, EventArgs e)
{
    Przywróć_Portfel();
    int s = 0, n;
    for (int k = 0; k < Nom.Length; k++)
    {
        n = gen.Next(nMax + 1);
        while (n > portfel[k].Count)
            Do_Portfela(new Money(this, siatka, klatka[k], k));
        while (n < portfel[k].Count)
            portfel[k].Pop().Hide();
        s += n * Nom[k];
    }
    Kwota = gen.Next(Math.Max(1, s / 4), s + 1);
    infoToolStripMenuItem.Text = string.Format("Kwota do wypłacenia: {0:C}", Kwota * 0.01);
}

Referencje do zdjętych i ukrytych obiektów są gubione, toteż mechanizm odśmie­cania pamięci niszczy je, zwalniając zajmo­waną przez nie pamięć. Na koniec metoda losuje liczbę całkowitą wyraża­jącą kwotę do wypła­cenia w groszach i po przeli­czeniu na kwotę złotową wyświetla ją, korzystając z waluto­wego specyfi­katora formato­wania łańcucha. Przykła­dową zawar­tość portfela i kwotę do wypła­cenia przedstawia poniższy rysunek.

Gromadzenie kwoty

Wyświetlone na obszarze robocznym okna aplikacji obiekty reprezen­tujące monety i banknoty można przeciągać myszą z jego lewej części (portfel) na prawą (wypłata) i odwrotnie. Celem takich działań jest zgroma­dzenie żądanej kwoty po prawej stronie. Spełnienie tego warunku wyraża prosta metoda zwraca­jąca wartość true (kwota uzyskana) lub false (nie, wymagane dalsze próby):

private bool KwotaOK()
{
    int s = 0;
    foreach (Money p in wypłata)
        s += Nom[p.indeks];
    return s == Kwota;
}

Podobnie jak w przypadku przeciągania duszka muchy przy precyzo­waniu operacji przecią­gania obiektu wyobraża­jącego banknot lub monetę możemy posłużyć się trzema zmiennymi informu­jącymi, czy operacja ta jest w obecnej chwili wykonywana i jaka jest wielkość przesu­nięcia kursora myszy względem lewego górnego rogu przecią­ganego obiektu. Potrzebna jest również refe­rencja do tego obiektu. Ponieważ może ona jedno­cześnie wskazywać, czy w danym momencie odbywa się przecią­ganie (null – żaden obiekt nie jest przecią­gany, różna od null – trwa przecią­ganie), użyjemy następu­jących pól klasy Form1:

private int xOffset, yOffset;       // Przesunięcie kursora myszy
private Money pDragged = null;      // Przeciągany banknot/moneta

Kluczową kwestią metody obsługi zdarzenia MouseDown wymaga­jącą rozstrzy­gnięcia i decydu­jącą o rozpo­częciu operacji przecią­gania jest identyfi­kacja obiektu wybranego przez użytko­wnika lewym przyciskiem myszy. Narzuca­jącym się rozwiąza­niem jest iteracja przebie­gająca kolekcję obiektów i sprawdza­jąca, czy prostokąt obiektu rozpatry­wanego w jej kolejnym kroku obejmuje kursor myszy o współ­rzędnych określonych w polach X i Y argu­mentu e metody. W przy­padku lewej części obszaru robo­czego okna kolekcję stanowią obiekty wierzchoł­kowe stosów – elementów tablicy portfel. Operację tę można sformu­łować następu­jąco:

pDragged = null;
foreach (Stack<Money> stosik in portfel)
    if ((stosik.Count > 0) && stosik.Peek().Contains(e.X, e.Y))
    {
        pDragged = stosik.Pop();
        break;
    }

Przeszukiwanie kończy się po znalezieniu obiektu lub osiągnięciu końca kolekcji. Znaleziony obiekt zostaje zdjęty ze stosu, określa go refe­rencja pDragged. Jeśli jest równa null, żaden obiekt nie został wybrany do przecią­gania.

Nieco bardziej skomplikowane są czynności przygoto­wawcze do przecią­gania obiektu wybra­nego z prawej części obszaru roboczego okna. Kolekcją obiektów jest wówczas lista wypłata. Przed usunię­ciem znalezio­nego obiektu z listy należy ukryć wszystkie znajdujące się za nim jej elementy i jego samego w kolej­ności odwrotnej do ich wstawiania, zaczynając od osta­tniego elementu listy i na znale­zionym elemencie kończąc, a po usunięciu pokazać ponownie ukryte elementy w kolej­ności występo­wania ich w liście i usunięty z listy obiekt:

pDragged = null;
for (int k = wypłata.Count - 1; k >= 0; k--)
    if (wypłata[k].Contains(e.X, e.Y))
    {
        for (int m = wypłata.Count - 1; m >= k; m--)
            wypłata[m].Hide();
        pDragged = wypłata[k];
        wypłata.RemoveAt(k);
        for (int m = k; m < wypłata.Count; m++)
            wypłata[m].Show();
        pDragged.Show();
        break;
    }

Inny porządek ukrywania i wyświetlania obiektów prowa­dziłby do zaśmie­cania powierz­chni roboczej okna (por. defekt latających spodków). Co prawda możnaby tych operacji i zarazem rozbły­sków będących efektem migotania ekranu uniknąć, ograni­czając przecią­ganie tylko do ostatniego elementu listy, użytko­wnik mógłby jednak przy jego wyborze poczuć się zagubiony. Lepiej zezwolić na przecią­ganie dowolnego obiektu nawet częściowo zakrytego, jeśli tylko daje się go wybrać myszą. Tak więc metodę obsługi zdarzenia MouseDown można sformu­łować następu­jąco:

private void Form1_MouseDown(object sender, MouseEventArgs e)
{
    if (e.Button != MouseButtons.Left) return;
    pDragged = null;
    if (e.X < xGr)              // Lewa strona okna (portfel)
    {
        foreach (Stack<Money> stosik in portfel)
            if ((stosik.Count > 0) && stosik.Peek().Contains(e.X, e.Y))
            {
                pDragged = stosik.Pop();
                break;
            }
    }
    else                        // Prawa strona okna (wypłata)
    {
        for (int k = wypłata.Count - 1; k >= 0; k--)
            if (wypłata[k].Contains(e.X, e.Y))
            {
                for (int m = wypłata.Count - 1; m >= k; m--)
                    wypłata[m].Hide();
                pDragged = wypłata[k];
                wypłata.RemoveAt(k);
                for (int m = k; m < wypłata.Count; m++)
                    wypłata[m].Show();
                pDragged.Show();
                break;
            }
    }
    if (pDragged == null) return;
    xOffset = e.X - pDragged.X;
    yOffset = e.Y - pDragged.Y;
}

Metoda obsługi zdarzenia MouseMove przesuwająca obiekt przedsta­wiający banknot lub monetę do miejsca określo­nego przez pozycję kursora myszy skorygo­waną o wartości xOffsetyOffset jest równie prosta jak metoda przesuwa­jąca duszka muchy. Natomiast metoda obsługi zdarzenia MouseUp kończąca przecią­ganie obiektu dodatkowo dodaje go do kolekcji reprezen­tującej portfel lub kwotę do wypłaty zależnie od tego, po której stronie linii grani­cznej wyzna­czonej przez współ­rzędną xGr znajduje się kursor myszy:

private void Form1_MouseUp(object sender, MouseEventArgs e)
{
    if (!dragging || (e.Button != MouseButtons.Left)) return;
    if (e.X < xGr)
        Do_Portfela(pDragged);
    else
    {
        int x = pDragged.X, y = pDragged.Y;
        if (x < xGr + dist)
            x = xGr + dist;
        else if (x + pDragged.Width > szer - dist)
            x = szer - pDragged.Width - dist;
        if (y < menuStrip1.Height + dist)
            y = menuStrip1.Height + dist;
        else if (y + pDragged.Height > wys - dist)
            y = wys - pDragged.Height - dist;
        if (x != pDragged.X || y != pDragged.Y)
            pDragged.Move(x, y);
        wypłata.Add(pDragged);
    }
    pDragged = null;
    if (KwotaOK())
        MessageBox.Show("Brawo! Kwota wypłacona.", "Gratulacje");
    }
}

Pozycja obiektu wstawianego na koniec listy wypłata zostaje w razie potrzeby skorygo­wana, by jego obraz mieścił się wewnątrz prawej części obszaru roboczego okna pomniej­szonej o niewielki margines (dist pikseli), a gdy żądana kwota zostaje osiągnięta, wyświetlony zostaje stosowny komunikat. Przykła­dowy widok okna w trakcie przecią­gania banknotu o nominale 20 zł z lewej części obszaru roboczego na prawą jest przedsta­wiony na poniższym rysunku.

Metoda prób i błędów

Polecenie Rozwiązanie umożliwia wygenerowanie przez program gotowego rozwią­zania problemu wypłacal­ności kwoty z zawar­tości portfela. Jest oczywiste, że zagadnienie może mieć jedno lub większą liczbę rozwiązań, może też nie mieć żadnego. Zastoso­wany algorytm polega na podejmo­waniu kolejnych prób dobie­rania dostę­pnych banknotów i monet aż do zebrania żądanej kwoty lub wyczer­pania wszystkich możliwych kombi­nacji. Wynik działania algorytmu zostanie umieszczony w następu­jących polach formu­larza:

private int[] Ile = null;           // Liczby banknotów i monet (rozwiązanie)
private bool Stop;                  // Istnieje rozwiązanie (true/false)

Tablicę Ile tworzymy w metodzie Form1_Load formularza. Jej elementami będą dobrane liczby banknotów i monet w kolej­ności zgodnej z nomi­nałami tablicy Nom, tj. od najwyż­szego do najniż­szego nominału. Wartością zmiennej Stop będzie true, gdy żądana kwota została zgroma­dzona, lub false, gdy kwoty nie udało się złożyć. Aby kwota została wyrażona za pomocą jak najmniejszej liczby banknotów i monet, należy kierować się zachłan­nością. Po pierwsze, próby dobierania nominałów powinny być podejmo­wane w kolej­ności od najwyż­szego do najniż­szego. Po drugie, poszuki­wanie liczby wystąpień każdego nominału należy rozpo­czynać od wartości możliwie jak najwię­kszej. Rozumo­wanie to prowadzi do następu­jącego sformuło­wania algorytmu:

private void Próbuj(int k, int kwota)
{
    Ile[k] = Math.Min(kwota / Nom[k], portfel[k].Count);
    if (k < Nom.Length - 1)
    {
        do
        {
            Próbuj(k + 1, kwota - Ile[k] * Nom[k]);
            if (!Stop) Ile[k]--;
        } while (!Stop && (Ile[k] >= 0));
    }
    else
        if (kwota == Ile[k] * Nom[k]) Stop = true;
}

Argumenty metody Próbuj określają, że po próbach dobie­rania wyższych nominałów wyszczegól­nionych w elemen­tach tablicy Nom o indeksach od 0 do k–1 pozostała jeszcze do wypła­cenia część kwoty, którą należy złożyć z pozosta­łych nomi­nałów. Dlatego po dobraniu jak najwię­kszej liczby niewykorzy­stanego jeszcze nominału Nom[k] metoda wywołuje samą siebie dla nastę­pnego nominału i kwoty pomniej­szonej o wartość dobranej liczby nominału Nom[k]. Jeśli po dojściu rekuren­cyjnym do najniż­szego nominału postępo­wanie nie doprowadzi do złożenia kwoty, czego oznaką jest przypi­sanie zmiennej Stop wartości true, jest kontynuo­wane dla mniej­szych liczb wystąpień nominału Nom[k] i niższych nominałów, aż w końcu cała kwota zostanie zebrana lub wszystkie próby zostaną przeprowa­dzone bez osiągnięcia sukcesu. Rzecz jasna metodę Próbuj należy wywołać po raz pierwszy z argumen­tami 0 i Kwota przy wartości zmiennej Stop równej false.

Inna strategia wypłacalności kwoty polega na dobieraniu nominałów w kolej­ności od najni­ższego do najwyż­szego. W przeciwień­stwie do strategii opartej na działaniu zachłannym prowadzi ona do pozby­wania się bilonu, dlatego nie powinna mieć zastoso­wania w kasach sklepowych. Klient nie lubi bowiem czekać, dopóki kasjer nie rozmieni banknotu na drobne w drugiej kasie czy nawet w innym sklepie.

Opisany powyżej algorytm zachłanny jest używany w metodzie obsługi zdarzenia Click polecenia Rozwią­zanie. Metoda najpierw przywraca dotychcza­sową zawartość portfela, a po przypi­saniu zmiennej Stop wartości false wywołuje metodę Próbuj z argu­mentami 0 i Kwota. Gdy rozwią­zanie zostaje znalezione, metoda przenosi stosowne obiekty z lewej części obszaru roboczego na prawą reprezen­tującą zestaw banknotów i monet do wypła­cenia żądanej kwoty, a gdy rozwią­zanie nie istnieje, wyświetla komunikat informu­jący, że wypła­cenie kwoty jest niemożliwe:

private void rozwiązanieToolStripMenuItem_Click(object sender, EventArgs e)
{
    Przywróć_Portfel();
    Stop = false;
    Próbuj(0, Kwota);
    if (Stop)                   // Znaleziono rozwiązanie
    {
        for (int k = 0; k < Nom.Length; k++)
        {
            int x = xPoz[k] + ((k < 3) ? 664 : 414);
            int y = yPoz[k];
            if (k > 2) y += ((k == 3) ? -28 : 80);
            for (int m = 0; m < Ile[k]; m++)
            {
                Money p = portfel[k].Pop();
                p.Move(x, y);
                wypłata.Add(p);
                x += dist;
                y += dist;
            }
        }
    }
    else
        MessageBox.Show("Kwota nie do wypłacenia.", "Problem");
}

Przenoszenie obiektów wymagało określania ich nowego miejsca po prawej stronie okna. Pozycje obiektów o nomi­nałach uwzglę­dnianych po raz pierwszy (m=0) są wyznaczane w oparciu o współ­rzędne zawarte w tabli­cach xPozyPoz według następu­jących zasad:

Każdy następny obiekt o takim samym nominale jak poprzedni (m>0) zostaje podobnie jak przy wyświe­tlaniu zawar­tości portfela przesu­nięty względem poprze­dniego o dist pikseli w prawo i w dół. Przykła­dowe rozwią­zanie zagadnienia wypłacal­ności żądanej kwoty z portfela wygenero­wane przez program przedstawia poniższy rysunek.

Program w C# (kwota)

Kod źródłowy w języku C# zawarty w pliku Form1.cs programu okienko­wego umożliwia­jącego wypłacenie (uzbie­ranie) wygene­rowanej losowo kwoty z wygene­rowanej losowo zawartości portfela lub uzyskanie gotowego rozwią­zania zadania jest przedsta­wiony na poniższym listingu. Dodatkowe pole logiczne Komunikat klasy formu­larza blokuje nadgorliwe wyświe­tlanie okien dialogo­wych informu­jących o uzbie­raniu żądanej kwoty do wypłacenia. Na przykład okno pochwalne nie pojawi się, gdy po rozwią­zaniu zadania przez program użytko­wnik przecią­gnie obiekt z prawej części obszaru roboczego na lewą, a następnie ten sam obiekt z lewej części na prawą.

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using Sprites;

namespace Kwota
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        private class Money : Sprite        // Klasa zagnieżdżona
        {
            public int indeks;

            public Money(Form okno, Bitmap tło, Bitmap klatka, int iNom, int x = 0, int y = 0)
                  : base(okno, tło, klatka, 1, x, y)
            {
                indeks = iNom;
            }
        }

        private const int szer = 1076, wys = 604, xGr = 666;
        private const int oczko = 16;       // Rozmiar oczek siatki
        private const int dist = 4;         // Przesunięcie elementu stosu
        private const int nMax = 6;         // Maksymalna wysokość stosu

        private int[] Nom = { 10000, 5000, 2000, 1000, 500, 200, 100, 50, 20, 10, 5, 2, 1 };
        private int[] xPoz = { 13,  13,  13, 344, 386, 386, 372, 562, 504, 576, 492, 480, 578 };
        private int[] yPoz = { 32, 230, 422, 430,  32, 154, 264,  32, 104, 192, 232, 332, 340 };

        private Bitmap siatka = null;       // Tło obszaru roboczego okna
        private Bitmap[] klatka = null;     // Bitmapy banknotów i monet
        private Stack<Money>[] portfel = null;  // Obiekty zawartości portfela
        private List<Money> wypłata = null;     // Obiekty zebrane do wypłaty
        private Random gen;                 // Generator liczb pseudolosowych
        private int Kwota = 0;              // Kwota do wypłacenia (w groszach)
        private int[] Ile = null;           // Liczby banknotów i monet (rozwiązanie)
        private bool Stop;                  // Istnieje rozwiązanie (true/false)
        private bool Komunikat;             // Informacja o rozwiązaniu (true/false)
        private int xOffset, yOffset;       // Przesunięcie kursora myszy
        private Money pDragged = null;      // Przeciągany banknot/moneta

        private void Utwórz_Tło()
        {
            siatka = new Bitmap(szer, wys);
            using (Graphics g = Graphics.FromImage(siatka))
            {
                g.Clear(Color.White);
                g.FillRectangle(Brushes.LightCyan, xGr, menuStrip1.Height, szer, wys);
                for (int y = menuStrip1.Height + 2 * oczko / 3; y < wys; y += oczko)
                    g.DrawLine(Pens.LightSkyBlue, 0, y, szer, y);
                for (int x = 2 * oczko / 3; x < szer; x += oczko)
                    g.DrawLine(Pens.LightSkyBlue, x, 0, x, wys);
            }
        }

        private void Form1_Load(object sender, EventArgs e)
        {
            Utwórz_Tło();
            ClientSize = siatka.Size;
            Rectangle r = SystemInformation.WorkingArea;
            Location = new Point((r.Width - Width) / 2, (r.Height - Height) / 2);
            klatka = new Bitmap[] {
                new Bitmap(Properties.Resources.zs1), new Bitmap(Properties.Resources.z50),
                new Bitmap(Properties.Resources.z20), new Bitmap(Properties.Resources.z10),
                new Bitmap(Properties.Resources.z05), new Bitmap(Properties.Resources.z02),
                new Bitmap(Properties.Resources.z01), new Bitmap(Properties.Resources.g50),
                new Bitmap(Properties.Resources.g20), new Bitmap(Properties.Resources.g10),
                new Bitmap(Properties.Resources.g05), new Bitmap(Properties.Resources.g02),
                new Bitmap(Properties.Resources.g01) };
            portfel = new Stack<Money>[Nom.Length];
            for (int k = 0; k < portfel.Length; k++)
                portfel[k] = new Stack<Money>();
            wypłata = new List<Money>();
            gen = new Random();
            Ile = new int[Nom.Length];
        }

        private void Form1_Paint(object sender, PaintEventArgs e)
        {
            e.Graphics.DrawImage(siatka, 0, 0);
        }

        private void Form1_Shown(object sender, EventArgs e)
        {
            if (Kwota == 0) portfelToolStripMenuItem_Click(sender, e);
        }

        private void Form1_FormClosed(object sender, FormClosedEventArgs e)
        {
            for (int k = klatka.Length - 1; k >= 0; k--)
                if (klatka[k] != null) klatka[k].Dispose();
            if (siatka != null) siatka.Dispose();
        }

        private void Do_Portfela(Money p)
        {
            int d = dist * portfel[p.indeks].Count;
            portfel[p.indeks].Push(p);
            p.Move(xPoz[p.indeks] + d, yPoz[p.indeks] + d);
            if (!p.Visible) p.Show();
        }

        private void Przywróć_Portfel()
        {
            while (wypłata.Count > 0)
            {
                Money p = wypłata.Last();
                wypłata.RemoveAt(kwota.Count - 1);
                Do_Portfela(p);
            }
        }

        private void portfelToolStripMenuItem_Click(object sender, EventArgs e)
        {
            Przywróć_Portfel();
            int s = 0, n;
            for (int k = 0; k < Nom.Length; k++)
            {
                n = gen.Next(nMax + 1);
                s += n * Nom[k];
                while (n > portfel[k].Count)
                    Do_Portfela(new Money(this, siatka, klatka[k], k));
                while (n < portfel[k].Count)
                    portfel[k].Pop().Hide();
            }
            Kwota = gen.Next(Math.Max(1, s / 4), s + 1);
            infoToolStripMenuItem.Text = string.Format("Kwota do wypłacenia: {0:C}", Kwota * 0.01);
            Komunikat = false;
        }

        private void Próbuj(int k, int kwota)
        {
            Ile[k] = Math.Min(kwota / Nom[k], portfel[k].Count);
            if (k < Nom.Length - 1)
            {
                do
                {
                    Próbuj(k + 1, kwota - Ile[k] * Nom[k]);
                    if (!Stop) Ile[k]--;
                } while (!Stop && (Ile[k] >= 0));
            }
            else
                if (kwota == Ile[k] * Nom[k]) Stop = true;
        }

        private void rozwiązanieToolStripMenuItem_Click(object sender, EventArgs e)
        {
            Przywróć_Portfel();
            Stop = false;
            Próbuj(0, Kwota);
            if (Stop)                   // Znaleziono rozwiązanie
            {
                for (int k = 0; k < Nom.Length; k++)
                {
                    int x = xPoz[k] + ((k < 3) ? 664 : 414);
                    int y = yPoz[k];
                    if (k > 2) y += ((k == 3) ? -28 : 80);
                    for (int m = 0; m < Ile[k]; m++)
                    {
                        Money p = portfel[k].Pop();
                        p.Move(x, y);
                        wypłata.Add(p);
                        x += dist;
                        y += dist;
                    }
                }
            }
            else
                MessageBox.Show("Kwota nie do wypłacenia.", "Problem",
                           MessageBoxButtons.OK, MessageBoxIcon.Warning);
            Komunikat = true;
        }

        private void koniecToolStripMenuItem_Click(object sender, EventArgs e)
        {
            Close();
        }

        private bool KwotaOK()
        {
            int s = 0;
            foreach (Money p in wypłata)
                s += Nom[p.indeks];
            return s == Kwota;
        }

        private void Form1_MouseDown(object sender, MouseEventArgs e)
        {
            if (e.Button != MouseButtons.Left) return;
            pDragged = null;
            if (e.X < xGr)              // Lewa strona okna (portfel)
            {
                foreach (Stack<Money> stosik in portfel)
                    if ((stosik.Count > 0) && stosik.Peek().Contains(e.X, e.Y))
                    {
                        pDragged = stosik.Pop();
                        break;
                    }
            }
            else                        // Prawa strona okna (wypłata)
            {
                for (int k = wypłata.Count - 1; k >= 0; k--)
                    if (wypłata[k].Contains(e.X, e.Y))
                    {
                        for (int m = wypłata.Count - 1; m >= k; m--)
                            wypłata[m].Hide();
                        pDragged = wypłata[k];
                        wypłata.RemoveAt(k);
                        for (int m = k; m < wypłata.Count; m++)
                            wypłata[m].Show();
                        pDragged.Show();
                        break;
                    }
            }
            if (pDragged == null) return;
            xOffset = e.X - pDragged.X;
            yOffset = e.Y - pDragged.Y;
        }

        private void Form1_MouseMove(object sender, MouseEventArgs e)
        {
            if (pDragged != null) pDragged.Move(e.X - xOffset, e.Y - yOffset);
        }
    }

        private void Form1_MouseUp(object sender, MouseEventArgs e)
        {
            if ((pDragged == null) || (e.Button != MouseButtons.Left)) return;
            if (e.X < xGr)
                Do_Portfela(pDragged);
            else
            {
                int x = pDragged.X, y = pDragged.Y;
                if (x < xGr + dist)
                    x = xGr + dist;
                else if (x + pDragged.Width > szer - dist)
                    x = szer - pDragged.Width - dist;
                if (y < menuStrip1.Height + dist)
                    y = menuStrip1.Height + dist;
                else if (y + pDragged.Height > wys - dist)
                    y = wys - pDragged.Height - dist;
                if (x != pDragged.X || y != pDragged.Y)
                    pDragged.Move(x, y);
                wypłata.Add(pDragged);
            }
            pDragged = null;
            if (!Komunikat && KwotaOK())
            {
                MessageBox.Show("Brawo! Kwota wypłacona.", "Gratulacje",
                           MessageBoxButtons.OK, MessageBoxIcon.Information);
                Komunikat = true;
            }
        }
}

Opracowanie przykładu: czerwiec 2020