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

WinAPI C++

Liczby rzymskie Algorytmy konwersji Skrypt zasobów programu Okno dialogowe oknem głównym programu Wybór rodzaju konwersji Komunikaty użytkownika Program konwersji Poprzedni przykład Kontakt

Liczby rzymskie

Zadaniem programu okienkowego, który zamierzamy zbudować w WinAPI C++, jest konwersja zapisu dodatnich liczb całkowi­tych z systemu dziesię­tnego na rzymski (por. program konso­lowy Zapis liczby w notacji rzymskiej) i z systemu rzymskiego na dziesiętny. Przypo­mnijmy, że w systemie rzymskim używa się siedmiu symboli jednolite­rowych, którym przypi­sane są wybrane wartości całko­wite. W celu uspra­wnienia algory­tmów konwersji wprowa­dzamy dodatkowo sześć złożonych z nich symboli dwulite­rowych. Zestaw wszystkich symboli i przypi­sanych im liczb całko­witych, które nazwiemy wagami, ma postać:

    I     1          IV     4
    V     5          IX     9
    X     10         XL     40
    L     50         XC     90
    C     100        CD     400
    D     500        CM     900
    M     1000

Ciąg takich symboli reprezentuje liczbę równą sumie odpowiada­jących im wag. Na przykład MCMLXXXIII oznacza liczbę 1983 (1000 + 900 + 50 + 10 + 10 + 10 + 1 + 1 + 1). Zapis liczby w notacji rzymskiej powinien składać się z możliwie jak najmniej­szej liczby znaków z uwzglę­dnieniem następu­jących zasad:

  1. Symbol o niższej wadze nie może wystąpić przed symbolem o wyższej wadze (każdy symbol dwuliterowy jest wyjątkiem od tej reguły, gdyż składa się z dwóch symboli jednoliterowych ustawionych w odwrotnej kolejności oznaczających odjęcie mniejszej wagi od większej, np. XL określa liczbę 40 = 50 – 10).
  2. Obok siebie mogą stać co najwyżej trzy takie same symbole jednoliterowe I, X, C i M (wagami tych symboli są całkowite potęgi liczby 10).
  3. Obok siebie nie mogą wystąpić dwa takie same symbole jednoliterowe V, L, D i symbole dwuliterowe.

Klasyczna notacja rzymska umożliwia zapis liczb z przedziału 1–3999 (zero nie było uznawane jako liczba). Nie zawsze trzymano się powyż­szych reguł. Na przykład zamiast popra­wnego zapisu IX (liczba 9) można było spotkać zapis VIIII lub VIV, zamiast MCMXCIX (liczba 1999) zapis IM, a nawet w obe­cnych czasach na tarczach zegarków zamiast IV (godz. 4) umieszczany bywa napis IIII. Przy zapisy­waniu większych liczb radzono sobie na różne sposoby, np. używając dodatko­wych symboli jednolite­rowych o wyższych wagach, ujmując zapis pomiędzy dwiema pionowymi kreskami (||) oznacza­jącymi wartości pomnożone przez 100 lub stosując nadkreski dla wartości pomno­żonych przez 1000. Liczby większe od 3999 będą w programie zapisy­wane z użyciem większej od 3 liczby powtórzeń litery M.

Algorytmy konwersji

Obie funkcje konwersji liczby całkowitej z systemu dziesiętnego na rzymski i z systemu rzymskiego na dziesiętny umieścimy w odrębnym module C++ o nazwie RomConv. Plik nagłów­kowy modułu zawiera­jący dodatkowo defi­nicję stałej określa­jącej maksy­malną długość łańcucha reprezen­tującego zapis rzymski liczby ma postać:

// RomConv.h - Liczby rzymskie
// --------------------------------------------------
// IntToRom - konwersja liczby całkowitej na rzymską
// RomToInt - konwersja liczby rzymskiej na całkowitą
// Parametry:
//   n      - liczba całkowita
//   s      - liczba rzymska (łańcuch znaków)
//   maxLen - maksymalna długość łańcucha
// Wartość zwrotna:
//   true   - pomyślny przebieg konwersji
//   false  - błąd danej wejściowej
// --------------------------------------------------

#ifndef H_ROMCONV
#define H_ROMCONV

#include <string.h>

#define ROMSTR_MAXLEN 20    // Maksymalna długość łańcucha

bool IntToRom(unsigned int n, char *s, int maxLen = ROMSTR_MAXLEN);
bool RomToInt(char *s, unsigned int &n);

#endif // H_ROMCONV

Argumentami funkcji IntToRom są: n – liczba całko­wita, która ma być zamie­niona na postać rzymską, s – tablica znaków, w której ma być umieszczony wynik konwersji, maxLen – maksy­malna długość tej tablicy bez znaku '\0' kończą­cego łańcuch wynikowy (w przy­padku pominięcia przyjmo­wana jest wartość domyślna ROMSTR_MAXLEN). Z kolei argu­mentem funkcji RomToInt jest tablica s zawiera­jąca łańcuch znaków reprezen­tujący liczbę rzymską i przeka­zywana przez referencję zmienna n, w której ma być zapisany wynik konwersji. Obie funkcje zwracają wartość logiczną true, gdy konwersja przebiegła pomyślnie, lub false, gdy wystąpił błąd. Ich implemen­tacje zawiera drugi plik modułu:

#include "romconv.h"

const char *Symbol[] = { "M", "CM", "D", "CD", "C", "XC", "L", "XL", "X", "IX", "V",
                         "IV", "I" };
const unsigned int Waga[] = { 1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1 };

bool IntToRom(unsigned int n, char *s, int maxLen)
{
    *s = '\0';
    for (int r = 0; n > 0; )
        if (Waga[r] <= n)
        {
            if ((maxLen -= (int)strlen(Symbol[r])) < 0) return false;
            strcat(s, Symbol[r]);
            n -= Waga[r];
        }
        else
            r++;
    return (n == 0) && (*s != '\0');
}

bool RomToInt(char *s, unsigned int &n)
{
    const int Nast[] = { 1, 5, 4, 5, 5, 9, 8, 9, 9, 13, 12, 13, 13 };
    int k = 256;
    for (int r = n = 0; (r < 13) && (*s != 0); k = (r % 4) ? 1 : 3)
    {
        bool b = false;
        while ((k > 0) && (strncmp(s, Symbol[r], strlen(Symbol[r])) == 0))
        {
            n += Waga[r];
            s += strlen(Symbol[r]);
            b = true;
            k--;
        }
        if (b)
            r = Nast[r];
        else
            r++;
    }
    return (n > 0) && (*s == 0);
}

Wszystkie symbole rzymskie i ich wagi uporządko­wane od najwyż­szej do najniższej wartości są reprezen­towane przez dwie tablice skorelo­wane za pomocą indeksu przebiega­jącego liczby całko­wite od 0 do 12. Funkcja IntToRom przebiega kolejne symbole, poczy­nając od M, i podejmuje w każdym kroku decyzję, czy napotkany symbol ma być uwzglę­dniony w zapisie rzymskim liczby, czy nie. Jeżeli waga symbolu jest mniejsza od wartości n lub jest jej równa, dołącza ten symbol na końcu wstępnie pustego łańcucha s i wagę odejmuje od n, zaś w przeci­wnym razie pomija ten symbol i przechodzi do nastę­pnego. Postępo­wanie to, charaktery­styczne dla algory­tmów zachłan­nych (por. program Wydawanie reszty), prowadzi ze względu na rozsze­rzony o dodatkowe symbole system wag do rozwią­zania zadania. Iterację kończy próba przekro­czenia maksy­malnej długości łańcucha wyniko­wego lub uzyskanie zerowej wartości zmiennej n. Wynik konwersji jest poprawny, gdy wartość n została zreduko­wana do zera i łań­cuch s jest niepusty.

Nieco bardziej złożony jest algorytm konwersji, według którego działa funkcja RomToInt. Przebiega ona listę symboli i sprawdza, czy mają one wystąpić w zapisie rzymskim liczby, czy nie. Jeżeli kolejny symbol rozpo­czyna łańcuch s, funkcja dodaje jego wagę do wstępnie wyzero­wanej zmiennej n i usuwa początek łańcucha s zgodny z tym symbolem, zwiększając wskaźnik s o długość symbolu, zaś w prze­ciwnym razie przechodzi do symbolu o niższej wadze. Lokalna zmienna k służy do ograni­czenia liczby wystą­pień symbolu obok siebie (256 dla M, 3 dla D, C i I, 1 dla pozosta­łych), a tablica Nast i zmienna logiczna b do pomijania symboli, których użycie prowadzi­łoby do uznania błędnego zapisu rzymskiego za poprawny (rys.). Pętla kończy się, gdy lista symboli została przejrzana lub łańcuch s został wyczer­pany. Wynik funkcji jest poprawny, gdy uzyskana wartość n jest dodatnia i łańcuch s jest pusty.

Skrypt zasobów programu

Zaprezentowane na poniższym rysunku okno dialogowe zaproje­ktowane w Visual Studio zawiera dwie kontrolki edycyjne i opisu­jące je etykiety, dwa przyciski radiowe umożli­wiające wybór rodzaju konwersji, przycisk domyślny poleca­jący wykonanie konwersji i przycisk zwykły zamyka­jący okno. Pierwsza kontrolka edycyjna ma służyć do wprowa­dzania z klawia­tury zarówno zapisu dziesię­tnego liczby, jak i zapisu rzymskiego, a druga jedynie do wyświe­tlania wyniku konwersji. Oznacza to, że zmiana ustawień przycisków radiowych powinna pociągać za sobą zmianę tekstu etykiet opisu­jących pola edycji bądź ich przesta­wienie.

Jest oczywiste, że do obu etykiet trzeba będzie się odwoływać, co wymaga przypi­sania im numerów ID innych niż IDC_STATIC, a spośród ośmiu kontrolek jedynie przyciskowi Zamknij pasuje nadać numer standar­dowy IDCANCEL. Numery ID siedmiu pozosta­łych kontrolek definiujemy w pliku nagłów­kowym resource.h:

#define IDC_EDIT1     101
#define IDC_EDIT2     102
#define IDC_LABEL1    103
#define IDC_LABEL2    104
#define IDC_RADIO1    105
#define IDC_RADIO2    106
#define IDC_CONVERT   107

Wyjaśnijmy, że obszerny skrypt zasobów wygenerowany przez Visual Studio w trakcie projekto­wania powyż­szego okna dialogo­wego nie zostanie w programie konwersji liczb całko­witych pomiędzy systemami dziesię­tnym i rzymskim użyty. Poglądowy rysunek okna można nawet sporzą­dzić na papierze, a rozmiar okna i para­metry kontrolek wyznaczyć eksperymen­talnie, urucha­miając wstępną wersję programu. Skrypt zasobów programu zawiera­jący ostateczną definicję okna i odwołanie do ikony zawarty w pliku Liczby­Rzymskie.rc ma postać:

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

Okno DIALOG 0, 0, 240, 112
STYLE DS_MODALFRAME | WS_POPUP | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX
CAPTION "Liczby rzymskie"
FONT 8, "Verdana"
BEGIN
    EDITTEXT        IDC_EDIT1, 71, 14, 95, 14, ES_UPPERCASE | WS_GROUP | WS_TABSTOP
    LTEXT           "Liczba dziesiętna", IDC_LABEL1, 10, 16, 56, 8
    EDITTEXT        IDC_EDIT2, 71, 34, 95, 14, ES_READONLY | ES_AUTOHSCROLL
    LTEXT           "Liczba rzymska", IDC_LABEL2, 10, 36, 56, 8
    AUTORADIOBUTTON "Liczba dziesiętna -> Liczba rzymska", IDC_RADIO1, 10, 72, 128, 10,
                    WS_GROUP | WS_TABSTOP
    AUTORADIOBUTTON "Liczba rzymska -> Liczba dziesiętna", IDC_RADIO2, 10, 90, 128, 10
    DEFPUSHBUTTON   "&Konwertuj", IDC_CONVERT, 178, 14, 50, 14, WS_GROUP | WS_TABSTOP
    PUSHBUTTON      "&Zamknij", IDCANCEL, 178, 86, 50, 14
END

Ikona ICON "Ikona.ico"

Słowa kluczowe EDITTEXT definiują kontrolki edycyjne, LTEXT – etykiety, AUTORADIO­BUTTON – przyciski radiowe, DEFPUSHBUTTON – przycisk domyślny, PUSHBUTTON – przycisk zwykły. W odróż­nieniu od zwykłych przycisków radiowych definio­wanych za pomocą słowa RADIOBUTTON przyciski AUDIORADIO­BUTTON są same odpowie­dzialne za wyświe­tlanie i ukry­wanie czarnej kropki wewnątrz niewiel­kiego kółka, dlatego zdecydo­wano się na ich użycie.

Okno ma trzy kontrolki mające styl WS_GROUP. Każda rozpo­czyna grupę kontrolek, wewnątrz której można zmieniać fokus za pomocą klawiszy strzałek. Oczywiście naturalne przejście od kontrolki do kontrolki odbywa się przy użyciu klawisza Tab lub kombi­nacji Shift-Tab. Fokus uzyskują wówczas tylko te kontrolki, które mają styl WS_TABSTOP. Domyślnie styl ten mają kontrolki edycyjne i przyciski, a teksty statyczne (etykiety) nie. Pierwsza kontrolka edycji ma styl ES_UPPERCASE, a druga ES_READONLYES_AUTOHSCROLL. Oznacza to, że pierwsza będzie automaty­cznie zamieniać wprowa­dzane małe litery na duże, zaś druga będzie jedynie wyświe­tlać tekst bez możli­wości edycji, a gdy będzie zbyt długi, będzie go można poziomo przewijać.

Okno dialogowe oknem głównym programu

Zapowiedziany program konwersji liczb może w świetle zaprezen­towanych na poprze­dnich stronach niniej­szej witryny programów wydawać się dziwny, ponieważ nie wywołuje funkcji CreateWindow w celu utwo­rzenia okna głównego, nie ma pętli komuni­katów, nie przetwarza ani komuni­katów WM_PAINTWM_DESTROY, ani komuni­katów klawia­tury i myszy, a pomimo tego udaje się mu utworzyć w pełni funkcjo­nalne okno umożli­wiające wykony­wanie sensownych obliczeń. Program po prostu tworzy okno dialogowe, które pełni rolę zwykłego okna.

W rzeczywistości program korzysta z menedżera okien dialo­gowych zawartego w Windows, który jest odpowie­dzialny za utwo­rzenie okna dialogo­wego na podstawie skryptu i dostar­czanie komuni­katów do proce­dury okna dialogo­wego. Rzecz jasna program mógłby tworzyć trady­cyjne okno wraz ze wszystkimi kontrol­kami za pomocą funkcji CreateWindow i obsłu­giwać je podobnie jak kontrolkę wyświe­tlającą kwotę w pro­gramie wypła­cania kwoty z zawartości portfela.

Wstępna wersja programu jest przedstawiona na poniższym listingu. Prosta funkcja WinMain jedynie wywołuje funkcję DialogBox, która tworzy okno dialogowe, korzystając ze skryptu zawartego w zasobach programu. Jest ono oknem głównym programu, o czym świadczy wartość NULL trzeciego argu­mentu wywołania (brak okna nadrzę­dnego). Druga funkcja o nazwie DlgProc jest proce­durą okna dialogo­wego obsługu­jącą tylko dwa komuni­katy.

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

BOOL CALLBACK DlgProc(HWND, UINT, WPARAM, LPARAM);

int WINAPI WinMain(HINSTANCE hInst, HINSTANCE hPrevInst, PSTR szCmdLine, int iCmdShow)
{
    DialogBox(hInst, "Okno", NULL, (DLGPROC)DlgProc);
    return 0;
}

BOOL CALLBACK DlgProc(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam)
{
    int dx, dy;
    RECT rect;

    switch (message)
    {
        case WM_INITDIALOG:
            GetWindowRect(hDlg, &rect);
            dx = rect.right - rect.left;
            dy = rect.bottom - rect.top;
            MoveWindow(hDlg, (GetSystemMetrics(SM_CXSCREEN) - dx) / 2,
                             (GetSystemMetrics(SM_CYSCREEN) - dy) / 2, dx, dy, FALSE);
            return TRUE;

        case WM_COMMAND:
            switch (wParam)
            {
                case IDCANCEL:
                    EndDialog(hDlg, wParam);
                    return TRUE;
            }
    }
    return FALSE;
}

Obsługa komunikatu WM_INITDIALOG, który dociera do procedury okna po utwo­rzeniu go, polega na przesu­nięciu okna na środek ekranu. Operacja ta wymagała uprze­dniego uzyskania infor­macji o rozmiarze okna i ekranu. W tym celu użyto funkcji GetWin­dowRect udostępnia­jącej prostokąt zajmowany przez okno oraz funkcji GetSystem­Metrics zwraca­jącej szero­kość ekranu dla argu­mentu SM_CXSCREEN i wyso­kość ekranu dla argu­mentu SM_CYSCREEN. Zmienne dxdy zawierają szero­kość i wyso­kość okna. Ostatni argument funkcji MoveWindow przesuwa­jącej okno określa, czy ma być ono przema­lowane, czy nie (okno jest jeszcze niewi­doczne, więc będzie malowane bez względu na tę wartość).

Procedura okna otrzymuje komunikat WM_COMMAND z para­metrem wParam równym IDCANCEL, gdy naciśnięty zostanie przycisk Zamknij. Procedura wywołuje wtedy funkcję EndDialog, która nakazuje Windows zniszczenie okna dialo­gowego i zakoń­czenie wykonania programu. Wynikiem urucho­mienia programu jest wyświe­tlenie zaprojekto­wanego okna dialogo­wego (rys.). Program nie wykonuje żadnych obliczeń, jedynym pożytkiem z niego jest możliwość łatwego wyzna­czenia metodą empiry­czną parame­trów okna i jego kontrolek.

Wybór rodzaju konwersji

Zaprezentowane powyżej okno jest gotowe do wprowadzania liczby w systemie dziesię­tnym, o czym świadczą etykiety usytuowane przed kontrol­kami edycyjnymi i migający kursor (karetka). Wpisana do pierwszego pola edycji liczba ma być przekształ­cona na system rzymski i pokazana w drugim polu edycji. Żądanie to nie znajduje jednak odzwiercie­dlenia w usta­wieniu przycisków radiowych. To niedocią­gnięcie możemy łatwo naprawić, ustawiając podczas obsługi komuni­katu WM_INITDIALOG czarną kropkę wewnątrz białego kółka pierw­szego przycisku radiowego:

CheckDlgButton(hDlg, IDC_RADIO1, BST_CHECKED);

Trzeci argument funkcji CheckDlgButton określa stan, w jakim ma znaleźć się przycisk (BST_CHECKED – zazna­czony, BST_UNCHECKED – zazna­czenie usunięte). Stan przycisków radiowych udostę­pnia funkcja IsDlgButton­Checked. Gdy użytko­wnik zmieni ich usta­wienie za pomocą myszki lub klawiszy strzałek, można poprzez jej wywołanie sprawdzić, który rodzaj konwersji został przez niego wybrany.

Każdy z czterech przycisków okna dialogowego, gdy zostanie naciśnięty, wysyła komunikat WM_COMMAND z para­metrem wParam równym jego numerowi ID. Jeżeli komunikat pochodzi od przycisku radiowego, procedura okna dialogo­wego powinna w zależności, czy był to pierwszy czy drugi z nich, zmienić łańcuchy etykiet informu­jące o rodzaju konwersji i wyczyścić kontrolki edycyjne, wysyłając do nich łańcuchy puste. Jeśli nato­miast komunikat pochodzi od przycisku Konwertuj, procedura powinna wykonać obliczenia stosowne do wybranego rodzaju konwersji. Uwzglę­dniając przycisk Zamknij, obsługę komuni­katu WM_COMMAND możemy więc sformu­łować następu­jąco:

static char L1[] = "Liczba dziesiętna", L2[] = "Liczba rzymska";
BOOL b;
...
case WM_COMMAND:
    switch (wParam)
    {
        case IDC_RADIO1:
        case IDC_RADIO2:
            b = IsDlgButtonChecked(hDlg, IDC_RADIO1) == BST_CHECKED;
            SetDlgItemText(hDlg, IDC_LABEL1, b ? L1 : L2);
            SetDlgItemText(hDlg, IDC_LABEL2, b ? L2 : L1);
            SetDlgItemText(hDlg, IDC_EDIT1, "");
            SetDlgItemText(hDlg, IDC_EDIT2, "");
            SetFocus(GetDlgItem(hDlg, IDC_EDIT1));
            return TRUE;

        case IDC_CONVERT:
            b = IsDlgButtonChecked(hDlg, IDC_RADIO1) == BST_CHECKED;
            if (b)
                ...       // Konwersja liczby dziesiętnej na rzymską
            else
                ...       // Konwersja liczby rzymskiej na dziesiętną
            return TRUE;

        case IDCANCEL:
            EndDialog(hDlg, wParam);
            return TRUE;
    }
    break;

Uwaga. Starsze słowo wParam zawiera tzw. kod powiado­mienia, którym jest tu BN_CLICKED o wartości zero, co pozwo­liło na spraw­dzanie wartości całego parametru wParam. W przy­padkach szczegól­nych wersji przycisków należy sprawdzać wartość LOWORD(wParam), czyli młodsze słowo tego para­metru, by zidenty­fikować przycisk.

Komunikaty użytkownika

Ostatnim etapem budowy programu jest sprecyzowanie operacji konwersji liczby całkowitej z systemu dziesię­tnego na rzymski i z systemu rzymskiego na dziesiętny, oczywiście przy wykorzy­staniu omówionych powyżej funkcji IntToRomRomToInt. Aby kod obsługi komuni­katu powstałego w wyniku naciśnięcia przycisku Konwertuj nie stał się zbyt obszerny, przez co program straciłby na czytel­ności, definiu­jemy dwa własne komuni­katy:

#define WM_DECROM   (WM_USER + 1)   // Konwersja liczba dziesiętna -> rzymska
#define WM_ROMDEC   (WM_USER + 2)   // Konwersja liczba rzymska -> dziesiętna

Dokładniej, WM_DECROM i WM_ROMDEC są symbolami, które określają dwie liczby całkowite trakto­wane w programie jako numery komuni­katów. Użytkownik może defi­niować komuni­katy, nadając im numery od WM_USER do 0x7FFF (dziesię­tnie 32767). Numery spoza tego zakresu są zarezer­wowane przez system. Korzy­stając z obydwu komuni­katów niestandar­dowych, obsługę komuni­katu pochodzą­cego od przycisku Konwertuj możemy zaprogra­mować następu­jąco:

case IDC_CONVERT:
    b = IsDlgButtonChecked(hDlg, IDC_RADIO1) == BST_CHECKED;
    SendMessage(hDlg, b ? WM_DECROM : WM_ROMDEC, 0, 0);
    return TRUE;

Zatem gdy do procedury okna dialogo­wego dotrze komunikat WM_COMMAND z parame­trem wParam równym IDC_CONVERT, wysyła ona (do siebie) za pomocą SendMessage jeden z dwóch zdefinio­wanych na potrzeby programu komuni­katów WM_DECROMWM_ROMDEC stoso­wnie do wybranego rodzaju konwersji. Ich obsługa wymaga określenia dwóch wartości grani­cznych, których nie powinno się przekroczyć:

#define SMAX     ROMSTR_MAXLEN   // Maksymalna długość zapisu rzymskiego (20)
#define NMAX     (1000 * SMAX)   // Maksymalna liczba dziesiętna

(stała ROMSTR_MAXLEN została zdefiniowana w module RomConv). W przy­padku konwersji liczby dziesię­tnej na rzymską łańcuch cyfr wprowa­dzony do pierwszej kontrolki edycyjnej jest najpierw za pomocą funkcji GetDlgItemInt zamie­niany na liczbę całko­witą bez znaku (zmienna n), a ta jest nastę­pnie przekształ­cana za pomocą funkcji IntToRom na łańcuch reprezen­tujący zapis rzymski (tablica s), który z kolei jest przesy­łany za pomocą funkcji SetDlgItemText do drugiej kontrolki edycyjnej:

char s[81];
unsigned int n;
...
case WM_DECROM:
    n = GetDlgItemInt(hDlg, IDC_EDIT1, &b, FALSE);
    if (b && (n > 0) && (n <= NMAX))
    {
        if (IntToRom(n, s))
        {
            SetDlgItemText(hDlg, IDC_EDIT2, s);
            SetFocus(GetDlgItem(hDlg, IDC_EDIT2));
            return TRUE;
        }
        wsprintf(s, "Zbyt długi zapis rzymski liczby (ponad %d znaków)", SMAX);
    }
    else
        wsprintf(s, "Nieprawidłowy zapis lub zakres liczby (1 - %d)", NMAX);
    SetDlgItemText(hDlg, IDC_EDIT2, "");
    MessageBox(hDlg, s, "Błąd", MB_ICONERROR);
    SetFocus(GetDlgItem(hDlg, IDC_EDIT1));
    return TRUE;

Operacja się nie powiedzie, gdy funkcja GetDlgItemInt zwróci w argu­mencie b wartość FALSE wskazu­jącą, że tekst kontrolki nie przedstawia liczby całko­witej bez znaku, gdy zwrócona przez tę funkcję wartość n wykracza poza zakres od 1 do NMAX, bądź gdy funkcja IntToRom zwróci wartość false oznacza­jącą, że zapis rzymski liczby przekracza SMAX znaków. O przy­czynie niepowo­dzenia informuje stosowne okienko komuni­katu. Użyta do formato­wania tekstu komuni­katu funkcja API o nazwie wsprintf jest odpowie­dnikiem tradycyjnej w C funkcji sprintf.

Nieco prostsza jest operacja odwrotna. Łańcuch znaków reprezen­tujący liczbę rzymską jest pobierany za pomocą funkcji GetDlgItemText z pierwszej kontrolki edycyjnej do tablicy s, po czym jest za pomocą funkcji RomToInt przekształ­cany na liczbę całkowitą (argument n), która dalej jest za pomocą funkcji SetDlgItemInt przesy­łana do drugiej kontrolki edycyjnej. Porażka nastąpi tylko wtedy, gdy funkcja RomToInt zwróci wartość false oznacza­jącą, że łańcuch pobrany z kontrolki nie stanowi popra­wnego zapisu liczby rzymskiej:

case WM_ROMDEC:
    GetDlgItemText(hDlg, IDC_EDIT1, s, sizeof(s));
    b = RomToInt(s, n);
    SetDlgItemInt(hDlg, IDC_EDIT2, n, FALSE);
    if (b)
        SetFocus(GetDlgItem(hDlg, IDC_EDIT2));
    else
    {
        MessageBox(hDlg, "Ciąg znaków nie przedstawia liczby rzymskiej.", "Błąd",
                   MB_ICONERROR);
        SetFocus(GetDlgItem(hDlg, IDC_EDIT1));
    }
    return TRUE;

Program konwersji

Kod źródłowy programu okienkowego w WinAPI C++ dokonu­jącego konwersji liczb całkowi­tych dodatnich pomiędzy systemami dziesię­tnym i rzymskim jest przedsta­wiony na poniż­szym listingu. W porów­naniu ze wstępną wersją program tworzy w funkcji WinMain okno dialo­gowe nie za pomocą funkcji DialogBox, lecz DialogBoxParam, aby w jej piątym argu­mencie przekazać do proce­dury okna DlgProc uchwyt programu. Uchwyt ten dostępny w jej argu­mencie lParam jest potrzebny do załado­wania ikony z zasobów programu ustawianej na pasku tytu­łowym okna w trakcie obsługi komuni­katu WM_INITDIALOG.

#include <windows.h>
#include "resource.h"
#include "romconv.h"

#define SMAX        ROMSTR_MAXLEN   // Maksymalna długość zapisu rzymskiego (20)
#define NMAX        (1000 * SMAX)   // Maksymalna liczba dziesiętna

#define WM_DECROM   (WM_USER + 1)   // Konwersja liczba dziesiętna -> rzymska
#define WM_ROMDEC   (WM_USER + 2)   // Konwersja liczba rzymska -> dziesiętna

BOOL CALLBACK DlgProc(HWND, UINT, WPARAM, LPARAM);

int WINAPI WinMain(HINSTANCE hInst, HINSTANCE hPrevInst, PSTR szCmdLine, int iCmdShow)
{
    DialogBoxParam(hInst, "Okno", NULL, (DLGPROC)DlgProc, (LPARAM)hInst);
    return 0;
}

BOOL CALLBACK DlgProc(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam)
{
    static char L1[] = "Liczba dziesiętna", L2[] = "Liczba rzymska";
    char s[81];
    BOOL b;
    unsigned int n;
    int dx, dy;
    RECT rect;

    switch (message)
    {
        case WM_INITDIALOG:
            GetWindowRect(hDlg, &rect);
            dx = rect.right - rect.left;
            dy = rect.bottom - rect.top;
            MoveWindow(hDlg, (GetSystemMetrics(SM_CXSCREEN) - dx) / 2,
                             (GetSystemMetrics(SM_CYSCREEN) - dy) / 2, dx, dy, FALSE);
            SendMessage(hDlg, WM_SETICON,
                        ICON_SMALL, (LPARAM)LoadIcon(HINSTANCE(lParam), "Ikona"));
            CheckDlgButton(hDlg, IDC_RADIO1, BST_CHECKED);
            return TRUE;

        case WM_COMMAND:
            switch (wParam)
            {
                case IDC_RADIO1:
                case IDC_RADIO2:
                    b = IsDlgButtonChecked(hDlg, IDC_RADIO1) == BST_CHECKED;
                    SetDlgItemText(hDlg, IDC_LABEL1, b ? L1 : L2);
                    SetDlgItemText(hDlg, IDC_LABEL2, b ? L2 : L1);
                    SetDlgItemText(hDlg, IDC_EDIT1, "");
                    SetDlgItemText(hDlg, IDC_EDIT2, "");
                    SetFocus(GetDlgItem(hDlg, IDC_EDIT1));
                    return TRUE;

                case IDC_CONVERT:
                    b = IsDlgButtonChecked(hDlg, IDC_RADIO1) == BST_CHECKED;
                    SendMessage(hDlg, b ? WM_DECROM : WM_ROMDEC, 0, 0);
                    return TRUE;

                case IDCANCEL:
                    EndDialog(hDlg, wParam);
                    return TRUE;
            }
            break;

        case WM_DECROM:
            n = GetDlgItemInt(hDlg, IDC_EDIT1, &b, FALSE);
            if (b && (n > 0) && (n <= NMAX))
            {
                if (IntToRom(n, s))
                {
                    SetDlgItemText(hDlg, IDC_EDIT2, s);
                    SetFocus(GetDlgItem(hDlg, IDC_EDIT2));
                    return TRUE;
                }
                wsprintf(s, "Zbyt długi zapis rzymski liczby (ponad %d znaków)", SMAX);
            }
            else
                wsprintf(s, "Nieprawidłowy zapis lub zakres liczby (1 - %d)", NMAX);
            SetDlgItemText(hDlg, IDC_EDIT2, "");
            MessageBox(hDlg, s, "Błąd", MB_ICONERROR);
            SetFocus(GetDlgItem(hDlg, IDC_EDIT1));
            return TRUE;

        case WM_ROMDEC:
            GetDlgItemText(hDlg, IDC_EDIT1, s, sizeof(s));
            b = RomToInt(s, n);
            SetDlgItemInt(hDlg, IDC_EDIT2, n, FALSE);
            if (b)
                SetFocus(GetDlgItem(hDlg, IDC_EDIT2));
            else
            {
                MessageBox(hDlg, "Ciąg znaków nie przedstawia liczby rzymskiej.", "Błąd",
                           MB_ICONERROR);
                SetFocus(GetDlgItem(hDlg, IDC_EDIT1));
            }
            return TRUE;
    }
    return FALSE;
}

Uwaga. Kompilator Visual C++ traktuje funkcję strcat jako przesta­rzałą i poten­cjalnie niebez­pieczną ze względu na możliwość przepeł­nienia bufora pamięci i sugeruje użycie funkcji strcat_s. Sugestia ta jest bezprze­dmiotowa, ponieważ funkcja IntToRom sprawdza, czy podczas operacji strcat długość łańcucha docelo­wego nie zostanie przekro­czona. Aby uniknąć zbędnej w tym przypadku modyfi­kacji kodu źródło­wego modułu RomConv, zabezpie­czenia prepro­cesora przed używaniem przesta­rzałych funkcji zostały we właściwo­ściach projektu wyłączone.

Na poniższym rysunku pokazane jest okno programu przedsta­wiające przykła­dowy wynik konwersji notacji rzymskiej liczby na dziesiętną.


Opracowanie przykładu: sierpień 2020