Artykuł

cze 05 2011
0

WPF Tutorial - kalkulator

Witajcie w trzeciej części tutoriala do WPF. W dwóch poprzednich wpisach (WPF Tutorial - wprowadzenie oraz WPF Tutorial - obsługa kontrolek), powiedzieliśmy sobie m.in o:

  • WPFie ogółem
  • Języku znaczników XAML
  • Podstawowych właściwościach WPF
  • Kontrolkach

Ponieważ wiemy już całkiem sporo, to czas dowiedzieć się, czy z tych informacji można zrobić jakiś sensowny użytek, np. budując prosty, tytułowy kalkulator:) Jeśli zatem chcecie wykorzystać zgromadzoną dotychczas wiedzę, do zbudowania czegoś praktycznego, to zapraszam do dalszej części wpisu:)

Założenia

Na początek, musimy założyć co chcemy zbudować. Na pewno wiemy, że będzie to kalkulator. Kalkulator, kalkulatorowi jednak nie równy, dlatego musimy poczynić kolejne założenia:

  • Nasz kalkulator, będzie posiadać przyciski umożliwiające wprowadzanie liczb - osobny przycisk na każdą cyfrę
  • Na razie, nie możliwa będzie obsługa kalkulatora za pomocą klawiatury, co nie znaczy, że nie można będzie takiej funkcjonalności dodać w przyszłości
  • Kalkulator obsługiwać będzie cztery podstawowe działania arytmetyczne, czyli: dodawanie, odejmowanie, mnożenie oraz dzielenie
  • Aplikacja umożliwi pracę z liczbami zmiennoprzecinkowymi
  • Aplikacja umożliwi kasowanie historii działań
  • Nie będzie to kalkulator idealny, ponieważ wiele rzeczy, z bardziej zaawansowaną wiedzą, można zrealizować lepiej - co nie znaczy, że nie zrobimy tego w przyszłości

Podsumowując, zbudujemy coś takiego jak na screenie 1.

Wizualna strona aplikacji - XAML

To od czego powinniśmy zacząć, to wizualizacja naszego projektu. Na początek, należy pomyśleć o odpowiednim rozplanowaniu elementów. Nasz kalkulator, posiadać będzie następujące elementy:

  • TextBox zawierający wartość będącą pierwszym czynnikiem aktualnej operacji - jeśli rozpoczęliśmy wprowadzanie drugiego elementu
  • TextBox zawierający typ aktualnie wykonywanej operacji
  • Przyciski od 0-9 pozwalające na wprowadzenie liczb
  • Cztery przyciski dla operacji (+,-,*,/)
  • Przycisk pozwalający na wprowadzanie wartości zmiennoprzecinkowych
  • Przycisk wyniku
  • Przycisk C kasujący wszystkie pola

Każda z grup przycisków, posiadać będzie również odpowiednią metodę obsługującą zdarzenie kliknięcia.

Mając listę wszystkich oczekiwanych elementów, należy zastanowić się teraz jaki układ (bądź układy) zastosować do rozmieszczenia tych elementów. Jeśli spojrzycie na screena, zauważycie że większość przycisków ma raczej dokładnie takie same wymiary, a tylko niektóre - te bardziej istotne zajmują większy obszar. W takim przypadku, dla mnie najlepszym rozwiązaniem okazał się Grid - widać tutaj wyraźną siatkę elementów. Nasz Grid, posiada sześć wierszy oraz pięć kolumn.

Ponieważ nakreśliliśmy już wstępnie założenia naszego układu, to przyszła pora, żeby zaprezentować kod XAML:

<Window x:Class="CalculatorWPF.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Kalkulator" Width="250" Height="250" ResizeMode="NoResize">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="*" />
            <RowDefinition Height="*" />
            <RowDefinition Height="*" />
            <RowDefinition Height="*" />
            <RowDefinition Height="*" />
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*" />
            <ColumnDefinition Width="*" />
            <ColumnDefinition Width="*" />
            <ColumnDefinition Width="*" />
            <ColumnDefinition Width="*" />
        </Grid.ColumnDefinitions>
        <TextBox Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="4" 
            TextAlignment="Right" Name="txtDisplayMemory" Margin="5" 
            BorderBrush="Gray" IsEnabled="False" />
        <TextBox Grid.Row="0" Grid.Column="4" Name="txtDisplayOperation" 
            Margin="5" BorderBrush="Gray" IsEnabled="False" />
        <TextBox Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="5" 
            TextAlignment="Right" Name="txtDisplay"  Margin="5" 
            BorderBrush="Gray" IsEnabled="False" />
        <Button Content="7" Grid.Row="2" Grid.Column="0" Margin="5" 
            Click="NumberButton_Click" />
        <Button Content="8" Grid.Row="2" Grid.Column="1" Margin="5" 
            Click="NumberButton_Click" />
        <Button Content="9" Grid.Row="2" Grid.Column="2" Margin="5" 
            Click="NumberButton_Click" />
        <Button Content="C" Grid.Row="2" Grid.Column="3" Margin="5" 
            Click="EraseButton_Click" Grid.ColumnSpan="2"  ToolTip="Wyczyść" />
        <Button Content="4" Grid.Row="3" Grid.Column="0" Margin="5" 
            Click="NumberButton_Click" />
        <Button Content="5" Grid.Row="3" Grid.Column="1" Margin="5" 
            Click="NumberButton_Click" />
        <Button Content="6" Grid.Row="3" Grid.Column="2" Margin="5" 
            Click="NumberButton_Click" />
        <Button Content="*" Grid.Row="3" Grid.Column="3" Margin="5" 
            Click="OperationButton_Click" />
        <Button Content="/" Grid.Row="3" Grid.Column="4" Margin="5" 
            Click="OperationButton_Click" />
        <Button Content="1" Grid.Row="4" Grid.Column="0" Margin="5" 
            Click="NumberButton_Click" />
        <Button Content="2" Grid.Row="4" Grid.Column="1" Margin="5" 
            Click="NumberButton_Click" />
        <Button Content="3" Grid.Row="4" Grid.Column="2" Margin="5" 
            Click="NumberButton_Click" />
        <Button Content="-" Grid.Row="4" Grid.Column="3" Margin="5" 
            Click="OperationButton_Click" />
        <Button Content="=" Grid.Row="4" Grid.Column="4" Margin="5" 
            Click="ResultButton_Click" Grid.RowSpan="2"  />
        <Button Content="0" Grid.Row="5" Grid.Column="0" Margin="5" 
            Click="NumberButton_Click" Grid.ColumnSpan="2"  />
        <Button Content="," Grid.Row="5" Grid.Column="2" Margin="5" 
            Click="CommaButton_Click" />
        <Button Content="+" Grid.Row="5" Grid.Column="3" Margin="5" 
            Click="OperationButton_Click" />
    </Grid>
</Window>

Jak widzicie, nie jest tego wiele, jedynie skromne 64 linie:) Postaram się teraz opisać pokrótce poszczególne elementy.

W liniach 1-4, definiujemy właściwości okna. Nasze okno, ma być zatem kwadratem, którego użytkownik nie może zmieniać wymiarów - ustawiamy odpowiednią wartość właściwości ResizeMode.

W liniach 6-20, definiujemy wiersze i kolumny naszego Grida. Po przeczytaniu dwóch poprzednich części tutoriala, nie jest to dla nas żadna nowość, dlatego przechodzimy też dalej:)

W kolejnym kroku, w liniach 21-28 ustawiamy właściwości trzech naszych TextBoxów. Każdy z nich, będzie zablokowany dla użytkownika (IsEnabled = false), dzięki czemu unikniemy wprowadzenia nieprawidłowych danych przez użytkownika z klawiatury. Oprócz tego, ustawiamy dla nich właściwości marginesu, kolor obramowania oraz definiujemy odpowiednią wielkość wykorzystując poznane wcześniej właściwości Grida. TextBox txtDisplay, jest tym, do którego wprowadzamy wartości liczbowe za pomocą przycisków.

Pozostałe elementy naszego układu, to przyciski. Cechują się one następującymi właściwościami:

  • Grid.Row - definiuje wiersz Grida
  • Grid.Column - definiuje kolumnę Grida
  • ToolTip dla przycisku C - definiuje podpowiedź wyświetlaną po najechaniu kursorem myszy nad przycisk
  • Content - tekst dla przycisku
  • Grid.RowSpan oraz Grid.ColumnSpan - definiowanie ile wierszy/kolumn ma zajmować określony przycisk

Poza wymienionymi wyżej właściwościami, każdy z przycisków wykorzystuje obsługę zdarzenia Click. Niektóre z przycisków wykorzystują tą samą metodę.

Niestety kompilacja powyższego kodu, zakończy się błędem. Dlaczego? Odpowiedź na to pytanie jest bardzo prosta. Wskazaliśmy do obsługi kliknięć przycisków metody, które jeszcze nie zostały zaimplementowane. Czas najwyższy zatem, przejść do logiki biznesowej:)

Logika biznesowa

Logikę biznesową rozpoczniemy od definicji enumeracji, zawierającą listę działań wykorzystywanych w naszej aplikacji:

enum Operation
{
    none = 0, // brak operacji
    addition, // dodawanie
    subtraction, // odejmowanie
    multiplication, // mnożenie
    division, // dzielenie
    result // wynik
}

Powyższy kod umieszczamy przed definicją klasy MainWindow. W samej klasie, dodajemy pole:

private Operation m_eLastOperationSelected = Operation.none;

Teraz możemy już przejść do implementacji kolejnych metod obsługujących przyciski.

NumberButton_Click

Najsilniejszą grupą przycisków, stanowią liczby i to właśnie one korzystać będą z tej metody. Zadaniem tej metody, będzie wyczyszczenie pola txtDisplay, w sytuacji gdy poprzednią operacją było wyświetlenie wyniku oraz dodanie liczby do tego pola:

private void NumberButton_Click(object oSender, RoutedEventArgs eRoutedEventArgs)
{
    if (Operation.result == m_eLastOperationSelected)
    {
        txtDisplay.Text = string.Empty;
        m_eLastOperationSelected = Operation.none;
    }
    Button oButton = (Button)oSender;
    txtDisplay.Text += oButton.Content;
}

Zwróćcie uwagę, na sprytny sposób pobierania liczby z przycisku, poprzez odczyt właściwości Content z aktualnie wysyłającego żądanie przycisku - czyli notabene przycisku aktualnie naciśniętego.

CommaButton_Click

W przypadku przycisku, obsługującego dodawanie przecinka, obsłużyć musimy dwie specyficzne sytuacje:

  • Ostatnią operacją był wynik - kasujemy wartość pola txtDisplay oraz ustawiamy na none wartość pola m_eLastOperationSelected będącego składową klasy. Następnie kontynuujemy wykonywanie dalszej części metody.
  • Drugi warunek, to pusta wartość pola txtDisplay, lub istnienie przecinka w tym polu w chwili obecnej. W takim przypadku, przerywamy pracę metody. Warto zauważyć, że jeśli zajdzie warunek pierwszy to musi również i zajść drugi

Jeśli doszliśmy do końca metody, to możemy dodać przecinek:

private void CommaButton_Click(object oSender, RoutedEventArgs eRoutedEventArgs)
{
    if (Operation.result == m_eLastOperationSelected)
    {
        txtDisplay.Text = string.Empty;
        m_eLastOperationSelected = Operation.none;
    }
    if ((txtDisplay.Text.Contains(',')) ||
        (0 == txtDisplay.Text.Length))
    {
        return;
    }
    txtDisplay.Text += ",";
}

EraseButton_Click

Przycisk kasowania, umożliwia wyczyszczenie wszystkich pól oraz składowych klasy, z których korzystamy w trakcie pracy:

private void EraseButton_Click(object oSender, RoutedEventArgs eRoutedEventArgs)
{
    txtDisplay.Text = string.Empty;
    txtDisplayMemory.Text = string.Empty;
    txtDisplayOperation.Text = string.Empty;
    m_eLastOperationSelected = Operation.none;
}

OperationButton_Click

Przed nami, zostały dwie ostatnie metody, które zarazem są również najbardziej skomplikowane. Zaczniemy od OperationButton_Click.

Celem tej metody, jest ustalenie odpowiedniej operacji jaka ma zostać wykonana w dalszej perspektywie pracy kalkulatora. Działamy tu w bardzo podobny sposób jak przy okazji metody obsługującej kliknięcia przycisków. Zanim jednak przypiszemy operację, to należy sprawdzić czy poprzednio wybrana operacja jest różna od pustej operacji i od operacji wyniku. Jeśli tak jest to oznacza, że musimy wykonać obliczenie dla aktualnych danych. Takie podejście umożliwia nam wykonywanie obliczeń w sposób ciągły - bez naciskania przycisku wyniku. Spójrzmy na kod tej metody:

private void OperationButton_Click(object oSender, RoutedEventArgs eRoutedEventArgs)
{
    // sprawdzenie czy poprzednia operacja czy jest rozna od none i od wyniku, jeśli nie to wykonać pozostałe operacje
    if((Operation.none != m_eLastOperationSelected) || (Operation.result != m_eLastOperationSelected))
    {
        ResultButton_Click(this, eRoutedEventArgs);
    }
    Button oButton = (Button)oSender;
    switch (oButton.Content.ToString())
    {
        case "+":
            m_eLastOperationSelected = Operation.addition;
            break;
        case "-":
            m_eLastOperationSelected = Operation.subtraction;
            break;
        case "*":
            m_eLastOperationSelected = Operation.multiplication;
            break;
        case "/":
            m_eLastOperationSelected = Operation.division;
            break;
        default:
            MessageBox.Show("Nieznana operacja!", "Błąd", MessageBoxButton.OK, MessageBoxImage.Error);
            return;
    }
    txtDisplayMemory.Text = txtDisplay.Text;
    txtDisplayOperation.Text = oButton.Content.ToString();
    txtDisplay.Text = string.Empty;
}

Na samym końcu, aktualną wartość pora txtDisplay, przenosimy do pola txtDisplayMemory - ponieważ staje się ona pierwszym czynnikiem wykonywanej operacji, a w polu txtDisplay będziemy mogli wpisywać wartość drugiego czynnika. Ustawiamy również operację w polu txtDisplayOperation oraz czyścimy pole txtDisplay.

ResultButton_Click

Na sam koniec, pozostawiłem metodę wyliczającą wynik. Niesie ze sobą ona kilka założeń:

  • Jeśli nie wybrano wcześniej żadnej operacji - nie obliczymy wyniku
  • Jeśli pole txtDisplay jest puste, to zakładamy, że jego wartość to 0
  • Obliczenia wykonujemy w zależności od wartości pola m_eLastOperationSelected
  • Po obliczeniu wyniku, kasujemy zawartość pól: txtDisplayMemory oraz txtDisplayOperation

W przełożeniu na kod, prezentuje się to następująco:

private void ResultButton_Click(object oSender, RoutedEventArgs eRoutedEventArgs)
{
    // Nie wykonywano żadnych operacji, nie można wyliczyć wyniku
    if ((Operation.result == m_eLastOperationSelected) || 
        (Operation.none == m_eLastOperationSelected))
    {
        return;
    }
    if (string.IsNullOrEmpty(txtDisplay.Text))
    {
        txtDisplay.Text = "0";
    }
    switch (m_eLastOperationSelected)
    {
        case Operation.addition:
            txtDisplay.Text = (double.Parse(txtDisplayMemory.Text) + 
                double.Parse(txtDisplay.Text)).ToString();
            break;
        case Operation.subtraction:
            txtDisplay.Text = (double.Parse(txtDisplayMemory.Text) - 
                double.Parse(txtDisplay.Text)).ToString();
            break;
        case Operation.multiplication:
            txtDisplay.Text = (double.Parse(txtDisplayMemory.Text) * 
                double.Parse(txtDisplay.Text)).ToString();
            break;
        case Operation.division:
            txtDisplay.Text = (double.Parse(txtDisplayMemory.Text) / 
                double.Parse(txtDisplay.Text)).ToString();
            break;
    }
    m_eLastOperationSelected = Operation.result;
    txtDisplayOperation.Text = string.Empty;
    txtDisplayMemory.Text = string.Empty;
}

Przy obliczaniu wyniku, należy tymczasowo sparsować wartości poszczególnych pól do typu double.

Słowo końcowe

Celem dzisiejszego wpisu, było stworzenie bardzo prostego kalkulatora. Jak widać w gruncie rzeczy nam się to udało:) Wspominałem również wcześniej, że nie jest to projekt idealny, ponieważ wielu rzeczy tu brakuje. Choćby wspomnianej obsługi klawiatury, czy też pewnej stylizacji wyglądu. Można by również wykorzystać bindowanie do przekazywania danych pomiędzy polami TextBox a logiką aplikacji. O tych i wielu innych rzeczach dowiecie się jednak z kolejnych części tego cyklu, a zaczniemy właśnie od bindowania:)

P.S. Cały, gotowy projekt, można znaleźć w dziale download.

Jeśli podoba Ci się ten wpis, sprawdź inne części tutoriala WPF.

Poprzednia część | Następna część

Data ostatniej modyfikacji: 28.08.2013, 14:33.

Podoba Ci się ten wpis? Powiedz o tym innym!

Send to Kindle

Komentarze

blog comments powered by Disqus