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

WinAPI C++

Przeciąganie myszą Tworzenie bitmapy maski duszka Komunikaty myszy Program "Mucha" Implementacja banknotów i monet Generowanie portfela i gromadzenie kwoty Program "Kwota" Poprzedni przykład Następny przykład Kontakt

Przeciąganie myszą

Przesuwanie duszka po ekranie (powierz­chni roboczej okna) może być stero­wane nie tylko komuni­katami zegaro­wymi (jak np. w progra­mach Rotacja gwiazdkiSpace­rujący skrzat). Duszek może być również przecią­gany za pomocą myszy lub przesu­wany za pomocą klawia­tury. Ta technika zostanie zaprezen­towana na prostym przykła­dzie wyświe­tlania muchy. W pro­gramie wykorzy­stany będzie obiekt klasy animacji Sprite, lecz tym razem obraz duszka (muchy) nie będzie się zmieniał w czasie (jedna klatka animacji).

Tworzenie bitmapy maski duszka

Jak wiadomo, klasyczna metoda wyświetlania w WinAPI obiektu o nieprosto­kątnym kształcie na niejedno­litym tle polega na dwukro­tnym wykonaniu funkcji BitBlt z różnymi kodami operacji rastrowej – pierwszy raz dla maski duszka i kodu SRCAND, drugi raz dla mapki duszka i kodu SRCPAINT. Obie bitmapy reprezen­tują w rozpatry­wanym przypadku jedną klatkę animacji. Mapka jest kolo­rowym obrazkiem muchy na czarnym tle, natomiast maska obrazkiem czarnej muchy o dokła­dnie takim samym kształcie jak poprzednia, lecz na białym tle:

Maskę można tworzyć "ręcznie", posługując się edytorem grafi­cznym. Gdy obrazek duszka nie zawiera czarnego koloru, łatwo jest tę żmudną operację zlecić do wykonania kompu­terowi, precy­zując prostą funkcję, która czarne piksele mapki zastępuje kolorem białym, zaś pozostałe czarnym. Funkcję warto wydzielić do odrębnego modułu, który może się przydać w innych programach. Jego plik nagłów­kowy maska.h ma postać:

// CreateMask - funkcja tworzenia maski
// ------------------------------------
// hMapka - uchwyt bitmapy mapki duszka
// wynik  - uchwyt bitmapy maski duszka
// ------------------------------------

#ifndef H_MASKA
#define H_MASKA

#include <windows.h>

HBITMAP CreateMask(HBITMAP hMapka);

#endif // H_MASKA

Najprostszą realizacją funkcji CreateMask jest przetwa­rzanie pikseli bitmapy w podwójnej pętli (w zasobach interne­towych można znaleźć szybsze metody):

#include "maska.h"

HBITMAP CreateMask(HBITMAP hMapka)
{
    const COLORREF black = RGB(0, 0, 0);
    const COLORREF white = RGB(255, 255, 255);
    BITMAP bitmap;
    HBITMAP hMaska;
    HDC hdcMem, hdcMem2;
    GetObject(hMapka, sizeof(BITMAP), &bitmap);
    hMaska = CreateBitmap(bitmap.bmWidth, bitmap.bmHeight, 1, 1, NULL);
    hdcMem = CreateCompatibleDC(NULL);
    hdcMem2 = CreateCompatibleDC(NULL);
    SelectObject(hdcMem, hMapka);
    SelectObject(hdcMem2, hMaska);
    for (int x = 0; x < bitmap.bmWidth; x++)
        for (int y = 0; y < bitmap.bmHeight; y++)
            SetPixel(hdcMem2, x, y, (GetPixel(hdcMem, x, y) == black) ? white : black);
    DeleteDC(hdcMem2);
    DeleteDC(hdcMem);
    return hMaska;
}

W przeciwieństwie do CreateCompa­tibleBitmap funkcja CreateBitmap tworzy bitmapę nieza­leżną od kontekstu jakiego­kolwiek urzą­dzenia. Pierwsze jej dwa argumenty określają szerokość i wysokość bitmapy, trzeci jest liczbą płaszczyzn barw, czwarty liczbą bitów na piksel, a piąty tablicą zawartości początkowej (wskaźnik NULL oznacza brak takiej tablicy). Zaleca się, aby tej funkcji używać tylko do tworzenia bitmap monochroma­tycznych (argu­menty trzeci i czwarty równe 1), gdyż w przypadku bitmap koloro­wych mogą pojawić się problemy z ich wyświe­tlaniem z powodu niekompaty­bilności z konte­kstem urzą­dzenia.

Komunikaty myszy

Komunikaty myszy docierają do procedury okna za każdym razem, gdy kursor myszy jest przesu­wany nad obszarem roboczym okna lub gdy zostanie nad nim naciśnięty którykol­wiek z jej przycisków. W para­metrze LPARAM komuni­katu przeka­zywane są współ­rzędne kursora myszy w odnie­sieniu do lewego górnego rogu tego obszaru. Młodsze słowo określa współrzę­dną x kursora, a starsze współrzę­dną y. Obie wartości można pobrać, posługując się makrami LOWORDHIWORD. Najważniej­szymi komuni­katami myszy są:

W pewnych sytuacjach konieczne jest, by program otrzymywał komuni­katy myszy nawet wtedy, gdy jej kursor znajduje się poza oknem. Aby wtedy umożliwić kontrolo­wanie ruchu myszy, trzeba przechwycić mysz (ang. mouse capture) za pomocą funkcji SetCapture, która wymaga podania uchwytu okna. Powrót do normal­nego przekazy­wania komuni­katów myszy umożliwia bezargu­mentowa funkcja Release­Capture. Najbezpie­czniej jest przechwycić mysz, gdy nad obszarem roboczym okna został naciśnięty jeden z jej przycisków, a zwolnić ją wraz ze zwolnie­niem tego przycisku.

Możliwe jest również uwięzienie myszy (ang. mouse clipping) wewnątrz określo­nego prosto­kąta za pomocą funkcji ClipCursor. Uwięziony zostaje oczywiście kursor myszy, który zatrzy­muje się na brzegu prostokąta, gdy użytko­wnik próbuje przesunąć mysz na zewnątrz. Argu­mentem funkcji jest wskaźnik na strukturę RECT określa­jącą prostokąt, który kursor nie może opuścić, bądź wskaźnik NULL oznacza­jący, że kursor ma być uwolniony.

W zapowiedzianym programie przeciągania myszą używany jest tylko jej lewy przycisk, a przecią­ganym obiektem jest duszek klasy Sprite wyobra­żający muchę. Przy precyzo­waniu operacji przecią­gania posłużymy się następu­jącymi zmiennymi staty­cznymi:

static int xOffset, yOffset;     // Przesunięcie kursora myszy
static BOOL Dragging = FALSE;    // Trwa przeciąganie (tak/nie)

Wartości całkowite xOffsetyOffset określają wielkość przesu­nięcia kursora myszy względem lewego górnego rogu prosto­kąta zajmowa­nego przez duszka (rys.), a wartość logiczna Dragging wskazuje, czy w danej chwili obiekt jest przecią­gany myszą, czy nie.

Komunikat WM_LBUTTONDOWN informuje, że w obszarze roboczym okna został naciśnięty lewy przycisk myszy. Jego obsługa powinna się rozpocząć od sprawdzenia, czy nastą­piło to w momencie, gdy kursor myszy znajdował się w obrębie prosto­kąta duszka. Zakładając, że duszek jest wskazy­wany przez refe­rencję mucha klasy Sprite i współ­rzędne kursora są przesłane do procedury okna w para­metrze lParam, obsługę komuni­katu możemy zaprogra­mować następu­jąco:

case WM_LBUTTONDOWN:
    if (!mucha->Contains(LOWORD(lParam), HIWORD(lParam))) return 0;
    SetCapture(hWnd);
    xOffset = LOWORD(lParam) - mucha->X();
    yOffset = HIWORD(lParam) - mucha->Y();
    Dragging = TRUE;
    return 0;

Wartość false wywołania metody Contains klasy Sprite dla obiektu mucha oznacza, że naciśnięcie miało miejsce poza nim i należy je zignorować, a true, że zdarzenie nastą­piło w obrębie obiektu i należy rozpocząć proces przecią­gania go. Operacja ta polega na:

Obsługa dwóch pozostałych komunikatów myszy, WM_MOUSEMOVEWM_LBUTTONUP, jest prostsza. Stosowne działania są oczywiście podejmo­wane tylko wtedy, gdy obiekt jest przecią­gany, tj. gdy wartością zmiennej Dragging jest TRUE. W przypadku komuni­katu WM_MOUSEMOVE duszek zostaje przesu­nięty w konte­kście urzą­dzenia wyświetla­jącego za pomocą metody Move klasy Sprite do miejsca określo­nego przez nową pozycję kursora przekazaną w para­metrze lParam zmodyfi­kowaną o wartości zapamię­tane w zmiennych xOffsetyOffset:

case WM_MOUSEMOVE:
    if (!Dragging) return 0;
    hdc = GetDC(hWnd);
    mucha->Move(hdc, LOWORD(lParam) - xOffset, HIWORD(lParam) - yOffset);
    ReleaseDC(hWnd, hdc);
    return 0;

Natomiast w przypadku komunikatu WM_LBUTTONUP informującego o zwol­nieniu lewego przycisku myszy przecią­ganie obiektu zostaje zakończone. Następuje wówczas powrót do normal­nego przekazy­wania komunikatów myszy poprzez uwolnienie kursora oraz przypisanie zmiennej Dragging wartości FALSE, która oznacza, że zakoń­czono przeciąganie obiektu:

case WM_LBUTTONUP:
    if (!Dragging) return 0;
    ReleaseCapture();
    Dragging = FALSE;
    return 0;

Program "Mucha"

Kod źródłowy programu wyświetlającego muchę i umożliwia­jącego jej przesu­wanie za pomocą myszy i klawia­tury jest przedsta­wiony na poniższym listingu. Skrypt zasobów programu zawarty w pliku Mucha.rc definiuje proste menu (dwa elementy MENUITEM generu­jące komuni­katy WM_COMMAND o numerach ID nazwanych w pliku nagłów­kowym Resource.h jako IDM_CENTERIDM_KONIEC) oraz odwołania do ikony Mucha.ico i bitmapy Mucha.bmp przedsta­wiającej mapkę muchy. Maska muchy jest tworzona w programie za pomocą omówionej powyżej funkcji CreateMask.

#include <windows.h>
#include "resource.h"
#include "sprite.h"
#include "maska.h"

const int OCZKO = 16,       // Rozmiar oczek siatki
          DIST  = 2;        // Przesunięcie jednostkowe

LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);

int WINAPI WinMain(HINSTANCE hInst, HINSTANCE hPrevInst, PSTR szCmdLine, int iCmdShow)
{
    static char szClassName[] = "Mucha";
    HWND hWnd;
    MSG msg;
    WNDCLASS wc;

    wc.style         = CS_HREDRAW | CS_VREDRAW;
    wc.lpfnWndProc   = WndProc;
    wc.cbClsExtra    = 0;
    wc.cbWndExtra    = 0;
    wc.hInstance     = hInst;
    wc.hIcon         = LoadIcon(hInst, "Ikona");
    wc.hCursor       = LoadCursor(NULL, IDC_ARROW);
    wc.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
    wc.lpszMenuName  = "Menu";
    wc.lpszClassName = szClassName;

    if (!RegisterClass(&wc))
    {
        MessageBox(NULL, "Ten program wymaga Win32!", szClassName, MB_ICONERROR);
        return 0;
    }
    hWnd = CreateWindow(szClassName, "Mucha (przeciąganie)", WS_OVERLAPPEDWINDOW,
                 CW_USEDEFAULT, CW_USEDEFAULT, 472, 356, NULL, NULL, hInst, NULL);
    ShowWindow(hWnd, iCmdShow);
    UpdateWindow(hWnd);
    while (GetMessage (&msg, NULL, 0, 0))
    {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }
    return msg.wParam;
}

LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    const int OCZKO_P = 3 * OCZKO / 4;
    static HBITMAP hMapka = NULL, hMaska = NULL;
    static Sprite *mucha = NULL;
    static int cx, cy, xOffset, yOffset;
    static BOOL Dragging = FALSE;
    HDC hdc;
    PAINTSTRUCT ps;
    HPEN hPen;

    switch (message)
    {
        case WM_CREATE:
            hMapka = LoadBitmap(((LPCREATESTRUCT)lParam)->hInstance, "Mapka");
            hMaska = CreateMask(hMapka);
            mucha = new Sprite(hMapka, hMaska);
            return 0;

        case WM_SIZE:
            cx = LOWORD(lParam);
            cy = HIWORD(lParam);
            return 0;

        case WM_PAINT:
            hdc = BeginPaint(hWnd, &ps);
            hPen = (HPEN)SelectObject(hdc, CreatePen(PS_SOLID, 1, RGB(135, 206, 250)));
            for (int k = OCZKO_P; k < cx; k += OCZKO)
            {
                MoveToEx(hdc, k, 0, NULL);
                LineTo(hdc, k, cy);
            }
            for (int k = OCZKO_P; k < cy; k += OCZKO)
            {
                MoveToEx(hdc, 0, k, NULL);
                LineTo(hdc, cx, k);
            }
            DeleteObject(SelectObject(hdc, hPen));
            if (!mucha->Visible())
                mucha->Move(hdc, (cx - mucha->Width()) / 2, (cy - mucha->Height()) / 2);
            mucha->Show(hdc);
            EndPaint(hWnd, &ps);
            return 0;

        case WM_KEYDOWN:
            hdc = GetDC(hWnd);
            switch (wParam)
            {
                case VK_UP:
                    mucha->Move(hdc, mucha->X(), mucha->Y() - DIST);
                    break;

                case VK_DOWN:
                    mucha->Move(hdc, mucha->X(), mucha->Y() + DIST);
                    break;

                case VK_LEFT:
                    mucha->Move(hdc, mucha->X() - DIST, mucha->Y());
                    break;

                case VK_RIGHT:
                    mucha->Move(hdc, mucha->X() + DIST, mucha->Y());
            }
            ReleaseDC(hWnd, hdc);
            break;

        case WM_LBUTTONDOWN:
            if (!mucha->Contains(LOWORD(lParam), HIWORD(lParam))) return 0;
            SetCapture(hWnd);
            xOffset = LOWORD(lParam) - mucha->X();
            yOffset = HIWORD(lParam) - mucha->Y();
            Dragging = TRUE;
            return 0;

        case WM_MOUSEMOVE:
            if (!Dragging) return 0;
            hdc = GetDC(hWnd);
            mucha->Move(hdc, LOWORD(lParam) - xOffset, HIWORD(lParam) - yOffset);
            ReleaseDC(hWnd, hdc);
            return 0;

        case WM_LBUTTONUP:
            if (!Dragging) return 0;
            ReleaseCapture();
            Dragging = FALSE;
            return 0;

        case WM_COMMAND:
            switch(wParam)
            {
                case IDM_CENTER:
                    hdc = GetDC(hWnd);
                    mucha->Move(hdc, (cx - mucha->Width()) / 2, (cy - mucha->Height()) / 2);
                    ReleaseDC(hWnd, hdc);
                    return 0;

                case IDM_KONIEC:
                    SendMessage(hWnd, WM_CLOSE, 0, 0);
                    return 0;
            }
            break;

        case WM_DESTROY:
            delete mucha;
            DeleteObject(hMaska);
            DeleteObject(hMapka);
            PostQuitMessage(0);
            return 0 ;
    }
    return DefWindowProc(hWnd, message, wParam, lParam);
}

Podczas przetwarzania komunikatu WM_CREATE procedura okna ładuje z zasobów programu mapkę muchy, tworzy jej maskę i wstępnie niewido­cznego duszka klasy Sprite wyobraża­jącego muchę. Obie bitmapy i obiekt duszka niszczy, gdy obsłu­guje komunikat WM_DESTROY. W trakcie przetwa­rzania komuni­katu WM_PAINT używa jasnonie­bieskiego pióra do rysowania siatki w obszarze roboczym okna. Jeśli jest to pierwszy komunikat WM_PAINT, który dotarł do procedury okna, niewidoczny duszek reprezen­tujący muchę jest przesuwany na środek obszaru roboczego. Następnie nieza­leżnie, czy był wcześniej widoczny, czy nie, jest pokazywany na wymalo­wanym tle za pomocą metody Show klasy Sprite.

Gdy do procedury okna dotrze komunikat WM_KEYDOWN, analizo­wany jest kod klawisza przesłany w parame­trze wParam. Jeżeli jest nim VK_UP, VK_DOWN, VK_LEFT lub VK_RIGHT, czyli gdy naciśnięty został jeden z klawiszy strzałek (góra, dół, lewo, prawo), duszek jest przesu­wany w kierunku zgodnym z ozna­czeniem naciśnię­tego klawisza o liczbę pikseli określoną przez stałą DIST. Zauważmy, że obsługa komuni­katu kończy się instrukcją break, dzięki czemu zdarzenia dotyczące innych klawiszy nie są ignorowane, lecz pozosta­wione do analizy domyślnej procedurze DefWindowProc. Podobna sytuacja ma miejsce przy przeglą­daniu komuni­katów WM_COMMAND – każdy inny od wygene­rowanego przez menu jest obsługiwany przez procedurę domyślną. Gdyby użyć instrukcji return, nigdy nie zostałyby obslużone, co mogłoby dopro­wadzić do niepopra­wnego działania programu.

A oto okno utworzone przez program po jego uruchomieniu:

Implementacja banknotów i monet

Program okienkowy w Windows API C++, który zamierzamy zbudować, ma umożliwić użytko­wnikowi uzbie­ranie żądanej kwoty z wygene­rowanej losowo zawartości portfela. Bardziej ogólny problem wypłacal­noś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. Ze względu na zbyt obszerny kod źródłowy programu o pełnej funkcjo­nalności pominiemy problem znajdo­wania gotowego rozwią­zania (zob. metoda prób i błędów w języku C#). Żądana kwota będzie przez program tak dobierana do zawartości portfela, by dała się dokładnie wypłacić. Program będzie spełniał rolę prostej gry eduka­cyjnej dla młodszych dzieci zaznaja­miającej je z rodzimą walutą i uczącej posługi­wania się nią.

Z uwagi 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ł, możemy bez szkody dla ogólności rozważań przy dobie­raniu stosownego zestawu banknotów i monet ograni­czyć zawartość portfela do monet od 1 gr do 5 zł i banknotów od 10 do 100 zł. Ich reprezen­tantami będą obiekty klasy Money wywodzącej się od klasy Sprite i obiekty klasy Banknot wywodzącej się od klasy Money. Obie klasy defi­niujemy w module, którego plik nagłów­kowy money.h ma postać:

#ifndef H_MONEY
#define H_MONEY

#include "sprite.h"

const int LNOMIN = 13;      // Liczba nominałów
const int LBANKN = 4;       // Liczba banknotów

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

class Money : public Sprite
{
public:
    int indeks;
    Money(HBITMAP Mapka, HBITMAP Maska, int iNom, int x = 0, int y = 0);
};

class Banknot : public Money
{
public:
    Banknot(HBITMAP Mapka, int iNom, int x = 0, int y = 0);
    virtual void Show(HDC hdc);
    virtual void Move(HDC hdc, int x, int y);
};

#endif // H_MONEY

Nominały banknotów i monet są w przeliczeniu na grosze ustawione w kolej­ności od najwyż­szego do najniż­szego w tablicy liczb całko­witych Nom. Nowym polem klasy Money względem klasy Sprite jest przekazy­wany w argu­mencie iNom konstru­ktora indeks elementu tablicy nominałów. Grafi­czny wizerunek obiektu wyobra­żającego monetę lub banknot nie zmienia się w czasie, toteż podobnie jak w programie przesu­wania duszka muchy jest pojedynczą klatką animacji wymaga­jącą dwóch bitmap – mapki i maski monety w przy­padku klasy Money lub tylko mapki banknotu w przy­padku klasy Banknot, której metody wirtualne Show (pokaż duszka) i Move (przesuń duszka) są redefi­niowane, gdyż inny jest sposób wyświe­tlania prosto­kątnego obrazka banknotu niż kołowego obrazka monety. Druga część modułu zawarta w pliku money.cpp jest implemen­tacją zapowie­dzianych w pliku nagłów­kowym metod obu klas:

#include "money.h"

Money::Money(HBITMAP Mapka, HBITMAP Maska, int iNom, int x, int y)
     : Sprite(Mapka, Maska, 1, x, y), indeks(iNom)
{}

Banknot::Banknot(HBITMAP Mapka, int iNom, int x, int y)
     : Money(Mapka, NULL, iNom, x, y)
{}

void Banknot::Show(HDC hdc)
{
    HDC hdcMem;
    hdcMem = CreateCompatibleDC(hdc);
    SelectObject(hdcMem, hSave);
    BitBlt(hdcMem, 0, 0, cx, cy, hdc, dx, dy, SRCCOPY);
    SelectObject(hdcMem, hMapki);
    BitBlt(hdc, dx, dy, cx, cy, hdcMem, 0, 0, SRCCOPY);
    DeleteDC(hdcMem);
    wid = true;
}

void Banknot::Move(HDC hdc, int x, int y)
{
    HBITMAP hbmpTmp;
    HDC hdcMem, hdcTmp;
    RECT RectOld, RectNew, RectAll;
    if (wid)
    {
        SetRect(&RectOld, dx, dy, dx + cx, dy + cy);
        SetRect(&RectNew, x, y, x + cx, y + cy);
        UnionRect(&RectAll, &RectOld, &RectNew);
        hbmpTmp = CreateCompatibleBitmap(hdc,
                        RectAll.right - RectAll.left, RectAll.bottom - RectAll.top);
        hdcTmp = CreateCompatibleDC(hdc);
        SelectObject(hdcTmp, hbmpTmp);
        BitBlt(hdcTmp, 0, 0, RectAll.right - RectAll.left, RectAll.bottom - RectAll.top,
               hdc, RectAll.left, RectAll.top, SRCCOPY);
        hdcMem = CreateCompatibleDC(hdc);
        SelectObject(hdcMem, hSave);
        BitBlt(hdcTmp, dx - RectAll.left, dy - RectAll.top, cx, cy, hdcMem, 0, 0, SRCCOPY);
        BitBlt(hdcMem, 0, 0, cx, cy, hdcTmp, x - RectAll.left, y - RectAll.top, SRCCOPY);
        SelectObject(hdcMem, hMapki);
        BitBlt(hdcTmp, x - RectAll.left, y - RectAll.top, cx, cy, hdcMem, 0, 0, SRCCOPY);
        BitBlt(hdc, RectAll.left, RectAll.top,
               RectAll.right - RectAll.left, RectAll.bottom - RectAll.top,
               hdcTmp, 0, 0, SRCCOPY);
        DeleteObject(hdcMem);
        DeleteObject(hdcTmp);
        DeleteObject(hbmpTmp);
    }
    dx = x;
    dy = y;
}

Lista inicjalizująca konstruktora klasy Money składa się z wywo­łania konstru­ktora klasy Sprite z liczbą klatek równą 1 i wyra­żenia nadają­cego polu indeks wartość argu­mentu iNom, natomiast lista inicjali­zująca konstru­ktora klasy Banknot zawiera tylko wywołanie konstru­ktora klasy Money z uchwytem NULL wskazu­jącym na brak maski banknotu. Kod metod wirtua­lnych ShowMove klasy Banknot jest w porównaniu z adekwa­tnymi metodami klasy Money odziedzi­czonymi po klasie Sprite mniej złożony ze względu na użycie jedynie najpro­stszej operacji rastrowej kopio­wania bitów.

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 wymagań programu zapisane w plikach grafi­cznych g01.bmp, g02.bmp, ..., z50.bmpzs1.bmp jako mapki klatek odpowia­dających nominałom 1 gr, 2 gr, ..., 50 zł i 100 zł. Skrypt zasobów programu definiu­jący proste menu (genero­wanie zawar­tości portfela i koniec wykonania programu) oraz odwołania do bitmap ma postać:

#include "resource.h"

Menu MENU
BEGIN
    MENUITEM "&Portfel", IDM_PORTFEL
    MENUITEM "&Koniec", IDM_KONIEC
END

zs1 BITMAP "zs1.bmp"
z50 BITMAP "z50.bmp"
z20 BITMAP "z20.bmp"
z10 BITMAP "z10.bmp"
z05 BITMAP "z05.bmp"
z02 BITMAP "z02.bmp"
z01 BITMAP "z01.bmp"
g50 BITMAP "g50.bmp"
g20 BITMAP "g20.bmp"
g10 BITMAP "g10.bmp"
g05 BITMAP "g05.bmp"
g02 BITMAP "g02.bmp"
g01 BITMAP "g01.bmp"

Operacja ładowania mapek banknotów i monet z zasobów programu oraz tworzenia masek monet może być reali­zowana przez proce­durę okna, gdy dotrze do niej komunikat WM_CREATE informu­jący, że okno zostało utworzone, ale jeszcze nie zostało wyświe­tlone. Nazwy zasobów i uchwyty obu rodzajów bitmap najwygo­dniej jest umieścić w tablicach o takiej samej inde­ksacji elementów jak tablica nominałów, a obsługę komuni­katu rozpocząć od pobrania ze struktury okna uchwytu programu niezbę­dnego do ładowania bitmap:

const char *RES[] = { "zs1", "z50", "z20", "z10", "z05", "z02", "z01",
                      "g50", "g20", "g10", "g05", "g02", "g01" };
static HINSTANCE hInstance;
static HBITMAP hMapka[LNOMIN], hMaska[LNOMIN];
...
case WM_CREATE:
    hInstance = ((LPCREATESTRUCT)lParam)->hInstance;
    for (int k = 0; k < LNOMIN; k++)
    {
        hMapka[k] = LoadBitmap(hInstance, RES[k]);
        hMaska[k] = (k < LBANKN) ? NULL : CreateMask(hMapka[k]);
    }
    srand(unsigned(time(NULL)));
    PostMessage(hWnd, WM_COMMAND, IDM_PORTFEL, 0);
    return 0;

Obsługę kończy zainicjowanie generatora liczb pseudolosowych i wsta­wienie komuni­katu WM_COMMAND z para­metrem IDM_PORTFEL do kolejki komuni­katów programu. Gdy zatem okno zostanie utworzone i wywie­tlone, niemal natych­miast dotrze do niego komunikat poleca­jący wyloso­wanie zawartości portfela i kwoty do wypłacenia.

Wygodną reprezentacją zestawu banknotów i monet stanowią­cych zawartość portfela jest tablica wektorów – obiektów klasy vector ze standar­dowej bibli­oteki szalonów STL, po jednym wektorze dla każdego nomi­nału, zaś reprezen­tacją zestawu wypła­canych banknotów i monet jeden wektor:

static std::vector<Money*> portfel[LNOMIN], // Zawartość portfela
                           wyplata;         // Zestaw zgromadzony do wypłacenia

Elementami wektorów są obiekty klasy Money (lub klasy pochodnej Banknot), a dokła­dniej – wskaźniki na te obiekty. Zawartość portfela określona przez tablicę portfel składa się z wektora banknotów o nomi­nale 100 zł, wektora banknotów o nomi­nale 50 zł itd., aż do wektora monet o nomi­nale 1 gr (niektóre mogą być puste). Natomiast zestaw banknotów i monet groma­dzony w celu wypła­cenia żądanej kwoty określony przez zmienną wypłata jest wektorem złożonym z obiektów klasy Money (lub Banknot) o różnych nomi­nałach.

Gdy procedura okna otrzyma komunikat WM_PAINT, tło okna i aktualny zestaw banknotów i monet powinien być przez nią odtwo­rzony. Aby uniknąć kłopotli­wego skalo­wania obrazu przyjmiemy, że okno aplikacji ma stały rozmiar wynika­jący z usta­lonego rozmiaru obszaru robo­czego. Obszar ten będzie podzie­lony na część lewą o białym tle (portfel) i część prawą o jasnotur­kusowym tle (wypłata), a ponadto będzie cały, podobnie jak w pro­gramie przecią­gania duszka muchy, wypeł­niony jasnobłę­kitną siatką. Te cechy obszaru robo­czego parametry­zujemy, definiując stałe:

const int COX   = 1060;     // Szerokość obszaru roboczego
const int COY   = 580;      // Wysokość obszaru roboczego
const int OCZKO = 16;       // Rozmiar oczek siatki
const int XGR   = 666;      // Granica część lewa/prawa

Przy tych założeniach precyzujemy obsługę komunikatu WM_PAINT:

HDC hdc;
PAINTSTRUCT ps;
HBRUSH hBrush;
HPEN hPen;
...
case WM_PAINT:
    hdc = BeginPaint(hWnd, &ps);
    hBrush = (HBRUSH)SelectObject(hdc, CreateSolidBrush(RGB(224, 255, 255)));
    hPen = (HPEN)SelectObject(hdc, CreatePen(PS_SOLID, 1, RGB(135, 206, 250)));
    Rectangle(hdc, XGR, -1, COX + 1, COY + 1);
    for (int k = 2 * OCZKO / 3; k < COX; k += OCZKO)
    {
        MoveToEx(hdc, k, 0, NULL);
        LineTo(hdc, k, COY);
    }
    for (int k = 2 * OCZKO / 3; k < COY; k += OCZKO)
    {
        MoveToEx(hdc, 0, k, NULL);
        LineTo(hdc, COX, k);
    }
    DeleteObject(SelectObject(hdc, hPen));
    DeleteObject(SelectObject(hdc, hBrush));
    for (int k = 0; k < LNOMIN; k++)
        for (unsigned int i = 0; i < portfel[k].size(); i++)
            portfel[k][i]->Show(hdc);
    for (unsigned int i = 0; i < wyplata.size(); i++)
        wyplata[i]->Show(hdc);
    EndPaint(hWnd, &ps);
    return 0;

Jak widać, po wybraniu jasnoturkusowego pędzla i jasnobłę­kitnego pióra rysujemy w prawej części obszaru robo­czego prostokąt, którego trzy krawędzie – prawa, górna i dolna wykraczają poza obszar, zaś lewa, co łatwo sprawdzić, pokrywa się z linią rysowanej w nastę­pnej kolej­ności siatki. W efekcie uzyskujemy jasnotur­kusową część obszaru robo­czego. Standar­dowe pióro i pędzel przywra­camy po naryso­waniu siatki. Na koniec wyświe­tlamy za pomocą metody Show klasy Banknot lub Money wszystkie banknoty i monety reprezen­towane przez obiekty wskazywane przez elementy wektorów tablicy portfel i wektora wyplata.

Generowanie portfela i gromadzenie kwoty

Zawartość portfela jest generowana losowo na początku wykonania programu (zob. obsługa komuni­katu WM_CREATE) i po każdym wybraniu w menu pole­cenia Portfel. Wektory tablicy portfel zawierają obiekty reprezen­tujące banknoty albo monety tego samego nominału, możemy więc traktować każdy taki wektor jako stos jedna­kowych elementów ustawio­nych jeden na drugim. Przyjmiemy, że elementy leżące na dnie stosu zajmują pozycje o wyzna­czonych ekspery­mentalnie współrzę­dnych zapisa­nych w tablicach 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ół. Definicje obydwu tablic, wielkości przesu­nięcia elementów stosu i maksy­malnej wysokości stosów mają postać:

const int xPoz[] = { 13,  13,  13, 344, 386, 386, 372, 562, 504, 576, 492, 480, 578 };
const int yPoz[] = {  8, 202, 398, 402,   8, 126, 236,   8,  76, 164, 204, 304, 312 };
const int DIST  = 4;        // Przesunięcie elementu stosu
const int NMAX  = 6;        // Maksymalna wysokość stosu

Gdy do procedury okna dotrze komunikat WM_COMMAND z parametrem IDM_PORTFEL, prawa część powierz­chni robo­czej okna powinna zostać wyczyszczona, a w lewej powinna się ukazać nowa zawartość portfela. Narzuca­jącym się rozwią­zaniem pierwszego etapu zadania jest przenie­sienie wszystkich obiektów wektora wyplata do odpowie­dnich wektorów tablicy portfel, czyli przywró­cenie pierwo­tnej zawar­tości portfela, a drugiego etapu – dostoso­wanie liczby elementów każdego wektora tablicy portfel do wyloso­wanej liczby nominału poprzez uzupe­łnienie wektora nowymi obiektami klasy Banknot lub Money albo usunięcie obiektów nadmia­rowych:

static int kwota;           // Kwota do wypłacenia (w groszach)
...
case IDM_PORTFEL:
    hdc = GetDC(hWnd);
    while (!wyplata.empty())
    {
        Money *p = wyplata.back();
        wyplata.pop_back();
        int k = p->indeks;
        int dist = portfel[k].size() * DIST;
        p->Move(hdc, xPoz[k] + dist, yPoz[k] + dist);
        portfel[k].push_back(p);
    }
    kwota = 0;
    for (int k = 0; k < LNOMIN; k++)
    {
        Money *p;
        unsigned int n = rand() % (NMAX + 1);
        while (portfel[k].size() < n)
        {
            p = (k < LBANKN) ? new Banknot(hMapka[k], k) : new Money(hMapka[k], hMaska[k], k);
            int dist = portfel[k].size() * DIST;
            p->Move(hdc, xPoz[k] + dist, yPoz[k] + dist);
            p->Show(hdc);
            portfel[k].push_back(p);
        }
        while (portfel[k].size() > n)
        {
            p = portfel[k].back();
            portfel[k].pop_back();
            p->Hide(hdc);
            delete p;
        }
        kwota += (rand() % (n + 1)) * Nom[k];
    }
    ReleaseDC(hWnd, hdc);
    return 0;

Przypomnijmy, że metoda size podaje aktualną liczbę elementów wektora, push_back dodaje nowy element na końcu wektora, pop_back usuwa ostatni element wektora, a pop zwraca ostatni element bez usuwania. Tak więc metody te operują na wektorze według zasady dotyczącej stosu: elementy są zdejmowane ze stosu w kolej­ności odwrotnej do ich wsta­wiania.

Problemem pozostaje sposób wyświetlenia kwoty, która ma być wypłacona z portfela. Najprościej jest zamienić ją na łańcuch i umieścić na pasku tytu­łowym okna za pomocą funkcji SetTextWindow (por. program Fraktale). Znacznie trudniej jest skorzystać z dodatkowej pozycji menu o zmiennym napisie, zabloko­wanej i wyrównanej do prawego brzegu (por. analo­giczny program w C#). Wartą rozwa­żenia propo­zycją, którą urzeczywi­stnimy, jest wykorzy­stanie kontrolki klasy STATIC. Kontrolkę tworzymy podobnie jak okno główne za pomocą funkcji CreateWindow. W argu­mentach jej wywo­łania podajemy nazwę klasy, napis wewnątrz kontrolki, jej styl, współ­rzędne lewego górnego narożnika w odnie­sieniu do prze­strzeni roboczej okna nadrzę­dnego, szerokość i wysokość kontrolki, uchwyt okna nadrzę­dnego, wskaźnik NULL (brak menu), uchwyt programu i drugi wskaźnik NULL (brak dodatko­wych danych):

const int CNX = 160;        // Szerokość kontrolki napisu
const int CNY = 16;         // Wysokość kontrolki napisu
static HWND hNapis;         // Kontrolka z napisem
...
hNapis = CreateWindow("static", "", WS_CHILD | WS_VISIBLE | SS_CENTER | WS_BORDER,
               (COX + XGR - CNX) / 2, DIST, CNX, CNY, hWnd, NULL, hInstance, NULL);
SendMessage(hNapis, WM_SETFONT, (WPARAM)GetStockObject(DEFAULT_GUI_FONT), 0);

Styl kontrolki określają stałe WS_CHILD (okno potomne), WS_VISIBLE (widoczne), SS_CENTER (centro­wanie tekstu) i WS_BORDER (ramka). Kontrolkę tworzymy w ramach obsługi komuni­katu WM_CREATE wysła­nego do procedury okna, gdy zostało ono utworzone i ma być pokazane po raz pierwszy. Przy przyję­tych defi­nicjach stałych kontrolka zostaje umiejsco­wiona pośrodku górnej części obszaru robo­czego przezna­czonej dla wybiera­nego zestawu banknotów i monet. Domyślna czcionka tekstu kontrolki nie jest ładna, dlatego zmieniamy ją, wysyłając do kontrolki za pomocą funkcji SendMessage komunikat WM_SETFONT (ustaw czcionkę) z uchwytem dostępnej w zasobach Windows domyślnej czcionki grafi­cznego inter­fejsu użytko­wnika wygląda­jącej znacznie lepiej. Po tych przygoto­waniach można z łatwością wyświetlić kwotę, która ma być wypła­cona z wygene­rowanej losowo zawar­tości portfela:

char napis[30];             // Napis (żądana kwota)
...
snprintf(napis, sizeof(napis), "Kwota do wypłaty: %.2f zł", 0.01 * kwota);
SetWindowText(hNapis, napis);

Żądana kwota wyrażona w groszach zostaje po przeliczeniu na kwotę złotową i sforma­towaniu na łańcuch wstawiona za pomocą funkcji SetTextWindow do kontrolki. Przykła­dowe okno programu z wygene­rowaną losowo zawar­tością portfela i kwotą do wypła­cenia jest pokazane na poniższym rysunku.

Wyświetlone w oknie aplikacji obiekty reprezen­tujące banknoty i monety można przeciągać myszą z lewej części obszaru roboczego (portfel) na prawą (wypłata) i odwrotnie. Celem takich działań jest zgroma­dzenie żądanej kwoty po prawej stronie. Spełnienie tego warunku jest łatwo sprawdzić, wyko­nując prostą pętlę:

int suma = 0;
for (int k = wyplata.size() - 1; k >= 0; k--)
    suma += Nom[wyplata[k]->indeks];
if (suma == kwota)
    MessageBox(hWnd, "Brawo! Kwota wypłacona.", "Gratulacje", MB_ICONINFORMATION);

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. Potrzebny jest również wskaźnik na ten obiekt. Ponieważ może on jedno­cześnie wskazywać, czy w danym momencie odbywa się przecią­ganie (NULL – żaden obiekt nie jest przecią­gany, różny od NULL – trwa przecią­ganie), użyjemy następu­jących zmiennych w proce­durze okna:

static int xOffset, yOffset;    // Przesunięcie kursora myszy
static Money *pDragged = NULL;  // Przeciągana moneta/banknot

Kluczową kwestią obsługi komunikatu WM_LBUTTONDOWN 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 przyci­skiem myszy. Narzuca­jącym się rozwiąza­niem jest iteracja przebie­gająca kolekcję obiektów i sprawdza­jąca, czy zdarzenie nastąpiło w momencie, gdy kursor myszy znajdował się w obrębie prosto­kąta rozpatry­wanego w danym kroku iteracyjnym obiektu. W przy­padku lewej części obszaru robo­czego kolekcję stanowią ostatnie elementy wektorów tablicy portfel (obiekty wierzchoł­kowe stosów), zaś w przy­padku prawej wszystkie elementy wektora wyplata:

int mx, my;                     // Współrzędne kursora myszy
RECT rect;                      // Prostokąt uwięzienia kursora
...
case WM_LBUTTONDOWN:
    mx = LOWORD(lParam);
    my = HIWORD(lParam);
    pDragged = NULL;
    if (mx < XGR)               // Lewa strona okna (portfel)
    {
        for (int k = 0; k < LNOMIN; k++)
            if (!portfel[k].empty() && (portfel[k].back()->Contains(mx, my)))
            {
                pDragged = portfel[k].back();
                portfel[k].pop_back();
                break;
            }
    }
    else                        // Prawa strona okna (wypłata)
    {
        for (int k = wyplata.size() - 1; k >= 0; k--)
            if (wyplata[k]->Contains(mx, my))
            {
                hdc = GetDC(hWnd);
                for (int m = wyplata.size() - 1; m >= k; m--)
                    wyplata[m]->Hide(hdc);
                pDragged = wyplata[k];
                wyplata.erase(wyplata.begin() + k);
                for (int m = k; m < (int)wyplata.size(); m++)
                    wyplata[m]->Show(hdc);
                pDragged->Show(hdc);
                ReleaseDC(hWnd, hdc);
                break;
            }
    }
    if (pDragged == NULL) return 0;
    xOffset = mx - pDragged->X();
    yOffset = my - pDragged->Y();
    SetCapture(hWnd);
    GetClientRect(hWnd, &rect);
    MapWindowPoints(hWnd, HWND_DESKTOP, (POINT *)&rect, 2);
    ClipCursor(&rect);
    return 0;

W obu przypadkach przeszukiwanie kończy się po znalezieniu obiektu wskazanego przez kursor myszy lub osiągnięciu końca kolekcji. Znale­ziony obiekt zostaje usunięty z wektora, określa go wskaźnik pDragged. Jeśli jest równy NULL, żaden obiekt nie został wybrany do przecią­gania.

Operacja usuwania znalezionego elementu wektora wyplata (prawa strona powierzchni okna) jest nieco skompli­kowana. Przeglą­danie elementów odbywa się w kolej­ności odwrotnej do ich wsta­wiania, tj. od końca wektora w kierunku jego początku. Znale­ziony obiekt może nie być ostatnim elementem wektora, dlatego przed usunię­ciem go ukrywane są wszystkie elementy znajdu­jące się za nim i on sam, poczynając od osta­tniego i na znale­zionym kończąc, a po usunięciu ukryte elementy pokazy­wane są ponownie według wzrasta­jących indeksów i na koniec pokazany zostaje usunięty obiekt. Inny porządek ukrywania i wyświe­tlania obiektów prowa­dziłby do zaśmie­cania obszaru robo­czego. Co prawda możnaby ten proces uprościć, ograni­czając przecią­ganie tylko do ostatniego elementu wektora, 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ą. Wypada jeszcze zwrócić uwagę, że metoda erase wymaga użycia iteratora wskazu­jącego na usuwany element wektora.

Różny od NULL wskaźnik pDragged identyfikuje wybrany lewym przyci­skiem myszy obiekt, który będzie przecią­gany w inne miejsce powierz­chni roboczej okna. Operację rozpo­czyna wyzna­czenie przesu­nięcia kursora myszy (wartości xOffsetyOffset) względem lewego górnego rogu prosto­kąta zajmo­wanego przez wize­runek banknotu lub monety reprezen­towany przez obiekt, przechwy­cenie kursora myszy (wywo­łanie funkcji SetCapture) i uwię­zienie go wewnątrz obszaru roboczego okna (wywo­łanie funkcji ClipCursor). Tak więc dopóki obiekt będzie przecią­gany, dopóty wszystkie komuni­katy dotyczące myszy będą kierowane do proce­dury okna tej aplikacji i kursor myszy nie będzie mógł być przesu­nięty poza obszar roboczy tego okna. Funkcja ClipCursor wymaga podania współrzę­dnych ekranowych, dlatego wspó­łrzędne obszaru robo­czego (dwa punkty – lewy górny i prawy dolny róg prosto­kąta) są konwer­towane na ekranowe za pomocą funkcji MapWindowPoints.

Obsługa komunikatu WM_MOUSEMOVE polecającego przesunięcie obiektu reprezen­tującego banknot lub monetę do miejsca określo­nego przez pozycję kursora skory­gowaną o wartości xOffsetyOffset jest równie prosta jak w przy­padku przesu­wania duszka muchy. Natomiast obsługa komuni­katu WM_LBUTTONUP 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 określonej przez współ­rzędną XGR znajduje się kursor myszy:

case WM_LBUTTONUP:
    if (pDragged == NULL) return 0;
    hdc = GetDC(hWnd);
    if (LOWORD(lParam) < XGR)   // Lewa strona okna (portfel)
    {
        int k = pDragged->indeks;
        int dist = portfel[k].size() * DIST;
        pDragged->Move(hdc, xPoz[k] + dist, yPoz[k] + dist);
        portfel[k].push_back(pDragged);
    }
    else                        // Prawa strona okna (wypłata)
    {
        mx = pDragged->X();
        my = pDragged->Y();
        if (mx < XGR + DIST)
            mx = XGR + DIST;
        else if (mx + pDragged->Width() > COX - DIST)
            mx = COX - pDragged->Width() - DIST;
        if (my < 2 * DIST + CNY)
            my = 2 * DIST + CNY;
        else if (my + pDragged->Height() > COY - DIST)
            my = COY - pDragged->Height() - DIST;
        if ((mx != pDragged->X()) || (my != pDragged->Y()))
            pDragged->Move(hdc, mx, my);
        wyplata.push_back(pDragged);
    }
    ReleaseDC(hWnd, hdc);
    ClipCursor(NULL);
    ReleaseCapture();
    pDragged = NULL;
    return 0;

Pozycja obiektu wstawianego na koniec wektora wypłata zostaje w razie potrzeby skorygo­wana, by jego obraz mieścił się w prawej części obszaru roboczego okna pomniej­szonej o niewielki margines (DIST pikseli od lewej, prawej i dolnej krawędzi oraz DIST pikseli poniżej kontrolki pokazującej żądaną kwotę do wypłaty). Operację kończy uwol­nienie kursora myszy (wywo­łanie funkcji ClipCursor z argu­mentem NULL), powrót do normal­nego przekazy­wania komuni­katów myszy (wywo­łanie funkcji ReleaseCapture) i przypi­sanie zmiennej pDragged wartości NULL, co oznacza, że właśnie zakoń­czono przecią­ganie obiektu. Przykła­dowy widok okna w trakcie przecią­gania banknotu o nominale 100 zł z lewej części obszaru roboczego na prawą jest przedsta­wiony na poniższym rysunku.

Program "Kwota"

Kod źródłowy programu okienkowego w Windows API C++ umożliwia­jącego wypła­cenie (uzbie­ranie) wymaganej kwoty z wygene­rowanej losowo zawartości portfela jest przedsta­wiony na poniższym listingu. Utworzone w funkcji WinMain okno programu ukazuje się na środku ekranu. Takie pozycjo­nowanie umożliwia funkcja GetSystem­Metrics zwraca­jąca szero­kość ekranu, gdy argu­mentem jej wywo­łania jest SM_CXSCREEN, lub wyso­kość ekranu, gdy argu­mentem jest SM_CYSCREEN. Dodatkowa zmienna logiczna Komunikat zdefinio­wana w proce­durze okna WndProc jest używana do blokady nadmiernej liczby pochwal­nych okien dialogo­wych informu­jących o uzbie­raniu żądanej kwoty. Na przykład okno takie nie pojawi się powtórnie, gdy po rozwią­zaniu zadania 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ą.

#include <windows.h>
#include <vector>
#include <ctime>
#include "resource.h"
#include "maska.h"
#include "money.h"

const int COX   = 1060;     // Szerokość obszaru roboczego
const int COY   = 580;      // Wysokość obszaru roboczego
const int CNX   = 160;      // Szerokość kontrolki napisu
const int CNY   = 16;       // Wysokość kontrolki napisu
const int OCZKO = 16;       // Rozmiar oczek siatki
const int XGR   = 666;      // Granica część lewa/prawa
const int DIST  = 4;        // Przesunięcie elementu stosu
const int NMAX  = 6;        // Maksymalna wysokość stosu

const int xPoz[] = { 13,  13,  13, 344, 386, 386, 372, 562, 504, 576, 492, 480, 578 };
const int yPoz[] = {  8, 202, 398, 402,   8, 126, 236,   8,  76, 164, 204, 304, 312 };

LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);

int WINAPI WinMain(HINSTANCE hInst, HINSTANCE hPrevInst, PSTR szCmdLine, int iCmdShow)
{
    static char szClassName[] = "Kwota";
    HWND hWnd;
    MSG msg;
    WNDCLASS wc;
    RECT rect;

    wc.style         = 0;
    wc.lpfnWndProc   = WndProc;
    wc.cbClsExtra    = 0;
    wc.cbWndExtra    = 0;
    wc.hInstance     = hInst;
    wc.hIcon         = LoadIcon(NULL, IDI_APPLICATION);
    wc.hCursor       = LoadCursor(NULL, IDC_ARROW);
    wc.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
    wc.lpszMenuName  = "Menu";
    wc.lpszClassName = szClassName;

    if (!RegisterClass(&wc))
    {
        MessageBox(NULL, "Ten program wymaga Win32!", szClassName, MB_ICONERROR);
        return 0;
    }
    SetRect(&rect, 0, 0, COX, COY);
    AdjustWindowRect(&rect, WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX, TRUE);
    int dx = rect.right - rect.left;
    int dy = rect.bottom - rect.top;
    hWnd = CreateWindow(szClassName, "Wypłacanie kwoty",
                 WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX,
                 (GetSystemMetrics(SM_CXSCREEN) - dx) / 2,
                 (GetSystemMetrics(SM_CYSCREEN) - dy) / 2,
                 dx, dy, NULL, NULL, hInst, NULL);
    ShowWindow(hWnd, iCmdShow);
    UpdateWindow(hWnd);
    while (GetMessage(&msg, NULL, 0, 0))
    {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }
    return msg.wParam;
}

LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    const char *RES[] = { "zs1", "z50", "z20", "z10", "z05", "z02", "z01",
                          "g50", "g20", "g10", "g05", "g02", "g01" };
    static HINSTANCE hInstance;
    static HBITMAP hMapka[LNOMIN], hMaska[LNOMIN];
    static std::vector<Money*> portfel[LNOMIN], wyplata;
    static int kwota;               // Kwota do wypłacenia (w groszach)
    static int xOffset, yOffset;    // Przesunięcie kursora myszy
    static Money *pDragged = NULL;  // Przeciągana moneta/banknot
    static BOOL Komunikat = FALSE;  // Komunikat o zgromadzeniu kwoty
    static HWND hNapis;             // Kontrolka z napisem
    char napis[30];                 // Napis (żądana kwota)
    int mx, my;                     // Współrzędne kursora myszy
    HDC hdc;
    PAINTSTRUCT ps;
    HBRUSH hBrush;
    HPEN hPen;
    RECT rect;

    switch (message)
    {
        case WM_CREATE:
            hInstance = ((LPCREATESTRUCT)lParam)->hInstance;
            hNapis = CreateWindow("static", "",
                           WS_CHILD | WS_VISIBLE | SS_CENTER | WS_BORDER,
                           (COX + XGR - CNX) / 2, DIST, CNX, CNY,
                           hWnd, NULL, hInstance, NULL);
            SendMessage(hNapis, WM_SETFONT, (WPARAM)GetStockObject(DEFAULT_GUI_FONT), 0);
            for (int k = 0; k < LNOMIN; k++)
            {
                hMapka[k] = LoadBitmap(hInstance, RES[k]);
                hMaska[k] = (k < LBANKN) ? NULL : CreateMask(hMapka[k]);
            }
            srand(unsigned(time(NULL)));
            PostMessage(hWnd, WM_COMMAND, IDM_PORTFEL, 0);
            return 0;

        case WM_PAINT:
            hdc = BeginPaint(hWnd, &ps);
            hBrush = (HBRUSH)SelectObject(hdc, CreateSolidBrush(RGB(224, 255, 255)));
            hPen = (HPEN)SelectObject(hdc, CreatePen(PS_SOLID, 1, RGB(135, 206, 250)));
            Rectangle(hdc, XGR, -1, COX + 1, COY + 1);
            for (int k = 2 * OCZKO / 3; k < COX; k += OCZKO)
            {
                MoveToEx(hdc, k, 0, NULL);
                LineTo(hdc, k, COY);
            }
            for (int k = 2 * OCZKO / 3; k < COY; k += OCZKO)
            {
                MoveToEx(hdc, 0, k, NULL);
                LineTo(hdc, COX, k);
            }
            DeleteObject(SelectObject(hdc, hPen));
            DeleteObject(SelectObject(hdc, hBrush));
            for (int k = 0; k < LNOMIN; k++)
                for (unsigned int i = 0; i < portfel[k].size(); i++)
                    portfel[k][i]->Show(hdc);
            for (unsigned int i = 0; i < wyplata.size(); i++)
                wyplata[i]->Show(hdc);
            EndPaint(hWnd, &ps);
            return 0;

        case WM_LBUTTONDOWN:
            mx = LOWORD(lParam);
            my = HIWORD(lParam);
            pDragged = NULL;
            if (mx < XGR)               // Lewa strona okna (portfel)
            {
                for (int k = 0; k < LNOMIN; k++)
                    if (!portfel[k].empty() && (portfel[k].back()->Contains(mx, my)))
                    {
                        pDragged = portfel[k].back();
                        portfel[k].pop_back();
                        break;
                    }
            }
            else                        // Prawa strona okna (wypłata)
            {
                for (int k = wyplata.size() - 1; k >= 0; k--)
                    if (wyplata[k]->Contains(mx, my))
                    {
                        hdc = GetDC(hWnd);
                        for (int m = wyplata.size() - 1; m >= k; m--)
                            wyplata[m]->Hide(hdc);
                        pDragged = wyplata[k];
                        wyplata.erase(wyplata.begin() + k);
                        for (int m = k; m < (int)wyplata.size(); m++)
                            wyplata[m]->Show(hdc);
                        pDragged->Show(hdc);
                        ReleaseDC(hWnd, hdc);
                        break;
                    }
            }
            if (pDragged == NULL) return 0;
            xOffset = mx - pDragged->X();
            yOffset = my - pDragged->Y();
            SetCapture(hWnd);
            GetClientRect(hWnd, &rect);
            MapWindowPoints(hWnd, HWND_DESKTOP, (POINT *)&rect, 2);
            ClipCursor(&rect);
            return 0;

        case WM_MOUSEMOVE:
            if (pDragged == NULL) return 0;
            hdc = GetDC(hWnd);
            pDragged->Move(hdc, LOWORD(lParam) - xOffset, HIWORD(lParam) - yOffset);
            ReleaseDC(hWnd, hdc);
            return 0;

        case WM_LBUTTONUP:
            if (pDragged == NULL) return 0;
            hdc = GetDC(hWnd);
            if (LOWORD(lParam) < XGR)   // Lewa strona okna (portfel)
            {
                int k = pDragged->indeks;
                int dist = portfel[k].size() * DIST;
                pDragged->Move(hdc, xPoz[k] + dist, yPoz[k] + dist);
                portfel[k].push_back(pDragged);
            }
            else                        // Prawa strona okna (wypłata)
            {
                mx = pDragged->X();
                my = pDragged->Y();
                if (mx < XGR + DIST)
                    mx = XGR + DIST;
                else if (mx + pDragged->Width() > COX - DIST)
                    mx = COX - pDragged->Width() - DIST;
                if (my < 2 * DIST + CNY)
                    my = 2 * DIST + CNY;
                else if (my + pDragged->Height() > COY - DIST)
                    my = COY - pDragged->Height() - DIST;
                if ((mx != pDragged->X()) || (my != pDragged->Y()))
                    pDragged->Move(hdc, mx, my);
                wyplata.push_back(pDragged);
            }
            ReleaseDC(hWnd, hdc);
            ClipCursor(NULL);
            ReleaseCapture();
            pDragged = NULL;
            if (!Komunikat)
            {
                int suma = 0;
                for (int k = wyplata.size() - 1; k >= 0; k--)
                    suma += Nom[wyplata[k]->indeks];
                if (suma == kwota)
                {
                    MessageBox(hWnd, "Brawo! Kwota wypłacona.", "Gratulacje",
                               MB_ICONINFORMATION);
                    Komunikat = TRUE;
                }
            }
            return 0;

        case WM_COMMAND:
            switch (wParam)
            {
                case IDM_PORTFEL:
                    hdc = GetDC(hWnd);
                    while (!wyplata.empty())
                    {
                        Money *p = wyplata.back();
                        wyplata.pop_back();
                        int k = p->indeks;
                        int dist = portfel[k].size() * DIST;
                        p->Move(hdc, xPoz[k] + dist, yPoz[k] + dist);
                        portfel[k].push_back(p);
                    }
                    kwota = 0;
                    for (int k = 0; k < LNOMIN; k++)
                    {
                        Money *p;
                        unsigned int n = rand() % (NMAX + 1);
                        while (portfel[k].size() < n)
                        {
                            p = (k < LBANKN) ? new Banknot(hMapka[k], k)
                                             : new Money(hMapka[k], hMaska[k], k);
                            int dist = portfel[k].size() * DIST;
                            p->Move(hdc, xPoz[k] + dist, yPoz[k] + dist);
                            p->Show(hdc);
                            portfel[k].push_back(p);
                        }
                        while (portfel[k].size() > n)
                        {
                            p = portfel[k].back();
                            portfel[k].pop_back();
                            p->Hide(hdc);
                            delete p;
                        }
                        kwota += (rand() % (n + 1)) * Nom[k];
                    }
                    snprintf(napis, sizeof(napis), "Kwota do wypłaty: %.2f zł", 0.01 * kwota);
                    SetWindowText(hNapis, napis);
                    ReleaseDC(hWnd, hdc);
                    Komunikat = FALSE;
                    return 0;

                case IDM_KONIEC:
                    SendMessage(hWnd, WM_CLOSE, 0, 0);
                    return 0;
            }
            break;

        case WM_CLOSE:
            if (MessageBox(hWnd, "Czy rzeczywiście zakończyć program?", "Koniec",
                           MB_YESNO | MB_ICONQUESTION) == IDYES)
                DestroyWindow(hWnd);
            return 0;

        case WM_DESTROY:
            for (int i = wyplata.size() - 1; i >= 0; i--)
                delete wyplata[i];
            wyplata.clear();
            for (int k = 0; k < LNOMIN; k++)
            {
                for (int i = portfel[k].size() - 1; i >= 0; i--)
                    delete portfel[k][i];
                portfel[k].clear();
                if (hMaska[k]) DeleteObject(hMaska[k]);
                if (hMapka[k]) DeleteObject(hMapka[k]);
            }
            PostQuitMessage(0);
            return 0;
    }
    return DefWindowProc(hWnd, message, wParam, lParam);
}

Opracowanie przykładu: sierpień 2020