Blog for Junior Developers C#/.NET

Friday, July 26, 2024

In the previous article, we started implementing the calculator as a desktop application in Windows Forms. If you haven't seen the previous article yet, be sure to start there (part 1 - User Interface) and only then come back to this material.

We already have the user interface ready, so now it's time to implement all the logic so that the entire application is complete. Of course, such an application - a calculator, can be implemented in various ways, and now I will show you one of many approaches to writing an example calculator.

first-windows-forms-desktop-application-in-csharp-ui-2-2.jpg

Please open visual studio and then our previously created solution with the Calculator.WindowsFormsApp project. Our application currently looks like this:

first-windows-forms-desktop-application-in-csharp-ui-2-2-1.jpg

For our calculator to work properly, we will have to operate on events in our application. You can see a list of all events that are available for each control in Windows Forms in the Properties window.

first-windows-forms-desktop-application-in-csharp-ui-2-2-2.jpg

We need to distinguish several different events in our application. We could create a separate Click event for each button and then handle it appropriately in codebehind. To go to the codebehind, i.e. the C# code of the form, you can right-click on the form and then View Code (F7), or right-click on the name of the form in Solution Explorer and also View Code.

first-windows-forms-desktop-application-in-csharp-ui-2-2-3.jpg
first-windows-forms-desktop-application-in-csharp-ui-2-2-4.jpg

However, we can handle this much more easily. We can create one common event for all green (with numbers) buttons and then in codebehind we will check, using the button's Text property, which button exactly was clicked. This will let us know exactly what was clicked. I'll show you exactly what I mean in a moment. Let's try to add a new event for 1 button now. We will add this event on button 0. Left-click on button 0 and then go to the Properties window and enter the name of the method that we will attach to the Click event, it may be OnBtnNumberClick.

first-windows-forms-desktop-application-in-csharp-ui-2-2-5.jpg

Copy the name of this event right away, because we will need it in a moment, and click enter. When you hit enter, codebehind should create a method that will be called when this event is triggered. That is, if someone clicks the 0 button, this method will be called.

private void OnBtnNumberClick(object sender, EventArgs e)
{
}


Please go back to the form and add the name of this event in the same places where there are green buttons, i.e. where our numbers are. Thanks to this, after clicking any button with a number, we will go to the OnBtnNumberClick method and we will handle it appropriately in a moment.

We will still need a few other methods. We will attach a separate method to the Click event for all buttons with operations. It may be a method called OnBtnOperationClick and later in codebehind we will verify which exact button with the operation was clicked, based on the text of this button.

first-windows-forms-desktop-application-in-csharp-ui-2-2-6.jpg

private void OnBtnOperationClick(object sender, EventArgs e)
{
}


We do the same for all operations, i.e. division, multiplication, subtraction and addition.

There will also be a separate method for displaying the result, i.e. OnBtnResultClick.

first-windows-forms-desktop-application-in-csharp-ui-2-2-7.jpg

private void OnBtnOperationClick(object sender, EventArgs e)
{
}


There will also be a separate method for the Clear button, i.e. for clearing our screen - OnBtnClearClick.

first-windows-forms-desktop-application-in-csharp-ui-2-2-8.jpg

private void OnBtnClearClick(object sender, EventArgs e)
{
}


You can now rebuild the project and make sure the code compiles and does not have any error.

Then go to the OnBtnNumberClick event. We can introduce a new clickedValue variable here and, based on the object passed in the parameter to this event, we can check which button was clicked in our interface.

var clickedValue = (sender as Button).Text;


So we project this object to Button and we are interested in its Text property - thanks to this we will know exactly what was clicked. For example, if the number 9 is clicked, this method will be called and the Button 9 button will be passed to the method and inside the OnBtnNumberClick method we will retrieve the Text that is on this button and we will know that the 9 was clicked.

Then in our TextBox, i.e. tbScreen, we will set the value of this button, or rather add it to what has already been entered there.

tbScreen.Text += clickedValue;


We can now immediately verify whether it works.

first-windows-forms-desktop-application-in-csharp-ui-2-2-9.jpg


As you can see, everything works fine. So, if I click 9, 9 appears on the screen. When I click again, the selected digits are added to the screen each time.

We can also make it so that 0 is immediately displayed on the screen after running. Just enter in the constructor:

tbScreen.Text = "0";


It is important that this assignment occurs after calling the InitializeComponent() method. If you run the application now, the 0 will be visible immediately. However, now we have a different problem, because if you click on another digit, e.g. 9, 09 is displayed on the screen, i.e. 9 is added to 0. It would be good if this 9, i.e. the first number, was substituted instead of the default 0, so that 0 wasn't there then. This is what it often looks like in calculators. This means that we need to add a conditional statement to the method assigned to the OnBtnNumberClick event. If our tbScreen.Text has the value 0 (only 0), then we remove this zero and only then add the digit that was clicked. This entry might look like this:

if (tbScreen.Text == "0")
    tbScreen.Text = string.Empty;


When you run the application, you will notice that it is now displayed correctly. At the beginning we have 0, we click 9 and 0 disappears, and 9 is displayed correctly.

This button may look like this at the moment. We will be correcting a few things here in a moment, so we will definitely come back here in a moment.

Now let's try to handle buttons with operations. Our application expects the user to first enter one digit, then the operation, then the second digit and finally display the result. So now we are at the second step, i.e. some operation has been clicked. We will also add a new variable in the OnBtnOperationClick method, this time called operation. Similarly to what we did earlier, based on the text we will check which button exactly was clicked. We can retrieve this operation like this:

var operation = (sender as Button).Text;


Before that, we can add a new variable here, or rather, it is best to create a field called _firstValue and assign the value from the TextBox to it. So we add the field:

private string _firstValue;


It will be a string, because here we are taking the value from the TextBox, so it can be a string, and we will convert it later. The OnBtnOperationClick method will assign the value from the screen to this field:

_firstValue = tbScreen.Text;


This means that if someone clicked on the operation, we assign all the text that was previously on the screen (i.e. 1 number) to the _firstValue field, so we know what the 1 number that was selected is. Then we also want to display in our TextBox what was clicked, i.e. the value of the operation variable.

tbScreen.Text += $" {operation} ";


This variable stores the operation we will perform. We can keep this operation in a string variable, but I think it will be much better if we introduce a new Enum and enter all possible operations there. Please add the enuma Operation definition above the Form1 class and we will complete it with all possible operations. It will be addition, subtraction, division, multiplication and we will also add the none operation.

public enum Operation
{
    None,
    Addition,
    Subtraction,
    Division,
    Multiplication
}


Additionally, we will also need a new field in which the currently selected operation will be stored:

private Operation _currentOperation = Operation.None;


The default value is Operation.None because we do not have any operation selected at the beginning. If we click the OnBtnOperationClick button, we can assign the selected operation to _currentOperation. It is best to use a switch in this case:

_currentOperation = operation switch
{
    "+" => Operation.Addition,
    "-" => Operation.Subtraction,
    "/" => Operation.Division,
    "*" => Operation.Multiplication,
    _ => Operation.None,
};


If, for example, the plus button was clicked, the current operation will be addition, i.e. Operation.Addition. I did the same for all operations, i.e. subtraction, division, multiplication. Finally, the default value, which is _, will be Operation.None.

Currently, in the _firstValue field we have the value of the first selected number, and in _currentOperation we have the value of the selected action. We also need to store the value of the second number. So let's go back to the OnBtnNumberClick event and here we will add a conditional instruction such that if an operation has already been selected, i.e. the value of the _currentOperation field is different from Operation.None, we assign this value to the _secondValue field.

if (_currentOperation != Operation.None)
    _secondValue += clickedValue;


We need to add a new field, just like before for the first value, and thanks to this we have everything we need to calculate the result.

private string _secondValue;


If our result is now clicked, we will perform calculations. That is, in OnBtnResultClick we first cast these numbers to double:

var firstNumber = double.Parse(_firstValue);
var secondNumber = double.Parse(_secondValue);


Then we perform the calculations:

var result = 0d;
switch (_currentOperation)
{
    case Operation.None:
        result = firstNumber;
        break;
    case Operation.Addition:
        result = firstNumber + secondNumber;
        break;
    case Operation.Subtraction:
        result = firstNumber - secondNumber;
        break;
    case Operation.Division:
        result = firstNumber / secondNumber;
        break;
    case Operation.Multiplication:
        result = firstNumber * secondNumber;
        break;
}


This means that the appropriate action will be performed based on _currentOperation. We can first declare and initialize the result variable, of type double, and perform appropriate calculations based on the operation. If no operation has been selected, the result variable will be equal to the value of the first number. Moreover, when dividing, we can also add a conditional instruction that will check whether the value of the second number is different from 0. If it is 0, then we will display an appropriate message.

if (secondNumber == 0)
{
    MessageBox.Show("Nie można dzielić przez 0!");
    result = 0;
    break;
}


As you can see, this OnBtnResultClick method has grown a bit, so in this case it is best to move the calculation logic to a new private method. The method may be called Calculate, it will accept 2 parameters and will return the result of double calculations. We will pass the value of the first number and the value of the second number to it.

private double Calculate(double firstNumber, double secondNumber)
{
    switch (_currentOperation)
    {
        case Operation.None:
            return firstNumber;
        case Operation.Addition:
            return firstNumber + secondNumber;
        case Operation.Subtraction:
            return firstNumber - secondNumber;
        case Operation.Division:
            if (secondNumber == 0)
            {
                MessageBox.Show("Nie można dzielić przez 0!");
                return 0;
            }
            return firstNumber / secondNumber;
        case Operation.Multiplication:
            return firstNumber * secondNumber;
    }

    return 0;
}


This is what the call will look like:

var result = Calculate(firstNumber, secondNumber);


We will then display the result on the screen:


tbScreen.Text = result.ToString();


and we will prepare the application to perform further calculations. For this we need to assign all initial values:

_secondValue = string.Empty;
_currentOperation = Operation.None;


We will not reset the first value at this point, because it may happen that the user will want to add further numbers one by one and then he will not want to reset the first value, but add the result to the next number. So we don't want to zero the first number here, only the second one. At this point, we can run our application and try to test it a bit manually and see if everything works as it should.

first-windows-forms-desktop-application-in-csharp-ui-2-2-10.jpg

I noticed a few things that we still need to improve. First of all, we want to make it so that if the result is displayed on the screen and we select another number, we want this result to be deleted. To do this, we will need 1 new field that will tell us whether the result is currently displayed on the screen.

private bool _isTheResultOnTheScreen;


By default it can be false. If we display the result, we set this field to true in the OnBtnResultClick method.

_isTheResultOnTheScreen = true;


Thanks to this, we know that a result is currently displayed, also in the OnBtnNumberClick event, first we need to check whether this result is displayed. If so, we set this field to false to change it and set tbScreen.Text to string.Empty to delete this result.

if (_isTheResultOnTheScreen)
{
    _isTheResultOnTheScreen = false;
    tbScreen.Text = string.Empty;
}


Let's see if this is enough. As you can see, if we now display the result and then click on a number, it replaces the result, so this case now works properly.

We still have 1 button left to handle, that is the Clear button. Let's first consider how this button is supposed to work. First of all, it needs to clear the calculator screen, i.e. display 0 on the screen. Then, it should remove both numbers and set the current operation to None. So this will be the initial state again.

private void OnBtnClearClick(object sender, EventArgs e)
{
    tbScreen.Text = "0";
    _firstValue = string.Empty;
    _secondValue = string.Empty;
    _currentOperation = Operation.None;
}


You can check if everything works fine. It looks like it's ok for now.

Let's check some marginal situations, i.e. what will happen if, after starting the application, we first click a comma instead of a number. Unfortunately, our application will now delete 0 and display only a comma. Also here we have an error that we need to correct.

first-windows-forms-desktop-application-in-csharp-ui-2-2-11.jpg

We need to introduce some protection for these commas here. We can do this so that if it is the first number, i.e. if there is 0 on the screen and a value other than the comma was clicked, then we remove the 0. However, if there was 0 on the screen, but the comma was clicked, then this comma will be appended normally after this zero.

if (tbScreen.Text == "0" && clickedValue != ",")
    tbScreen.Text = string.Empty;


1 conditional statement solves this problem for us. However, we must do the same below. If some result is displayed and the comma was clicked, then we can set here that it would be as if 0 and the comma were clicked. Thanks to this, we will also protect ourselves against this error.

if (_isTheResultOnTheScreen)
{
    _isTheResultOnTheScreen = false;
    tbScreen.Text = string.Empty;

    if (clickedValue == ",")
        clickedValue = "0,";
}


These 2 instructions solve our problem.

first-windows-forms-desktop-application-in-csharp-ui-2-2-12.jpg


Of course, this is not the end of the problems. We still have something to improve. When someone clicks a number, e.g. 6, and then an operation, e.g. + then another operation of any kind, various strange things appear here. We should not allow this to happen.

first-windows-forms-desktop-application-in-csharp-ui-2-2-13.jpg

We should add appropriate security for this. We cannot count on the user always clicking as we expect. This means that if an operation button is clicked, it cannot be clicked a second time. We can add a new private method that will help us manage the state of the buttons. Let it be a method called SetOperationBtnState, which will accept a bool value. Depending on what we pass, this will set the state of the buttons.

private void SetOperationBtnState(bool value)
{
    btnAdd.Enabled = value;
    btnMultiplication.Enabled = value;
    btnDivision.Enabled = value;
    btnSubtraction.Enabled = value;
}


So if true is passed to this method, all buttons will be unlocked, and if false, then they will be locked. All operation buttons will behave the same way. The same method will also be useful for our button, after clicking which the result is displayed. Also, please add a method called SetResultBtnState. The state of the btnResult button will also be set here, because it will be blocked in slightly different cases.

private void SetResultBtnState(bool value)
{
    btnResult.Enabled = value;
}


So now what we will do is that if a button with an operation has been clicked, we will set all buttons to locked. Both the buttons with operations and the button with displaying the result, because our action has not been completed yet, therefore the result cannot be displayed yet.

SetOperationBtnState(true);
SetResultBtnState(true);


In the same method, i.e. OnBtnOperationClick, we should also set the _isTheResultOnTheScreen field to false when it is true.

if (_isTheResultOnTheScreen)
    _isTheResultOnTheScreen = false;


This means that if someone displayed the result and then clicked the operation, this field must also be set to false. We've now set our buttons to locked, but we also need to remember to unlock them later, and we can unlock them in a few cases. Firstly, when a number is clicked, i.e. in the OnBtnNumberClick method:

SetOperationBtnState(true);


So we want to unlock these buttons with operations, but only if no operation has been previously selected. So this call will be in this place in the conditional statement:

if (_currentOperation != Operation.None)
    _secondValue += clickedValue;
else
    SetOperationBtnState(true);


As for the result button, we can unlock it here in any case, so we can add a call:

SetResultBtnState(true);


If any numbered button is clicked, then this result may be available. Similarly, if the result button is clicked, we want to unlock all buttons so that the next operation can be clicked.

SetOperationBtnState(true);
SetResultBtnState(true);


We can add one more conditional statement in the OnBtnResultClick method. That is, if no operation is selected, then there is no point in us performing any action here at all.

if (_currentOperation == Operation.None)
    return;


I think that now we have analyzed all possible cases, so our application should be complete. Finally, of course, you still need to click the application a bit and test it thoroughly.

This time everything works fine. Certainly, such an application can be further extended, but here I think we have protected ourselves against the most common situations where an error could occur. So this application already works quite nicely.


Code of the entire application:


using System;
using System.Windows.Forms;

namespace Calculator.WindowsFormsApp
{
    public enum Operation
    {
        None,
        Addition,
        Subtraction,
        Division,
        Multiplication
    }

    public partial class Form1 : Form
    {
        private string _firstValue;
        private string _secondValue;
        private Operation _currentOperation = Operation.None;
        private bool _isTheResultOnTheScreen;

        public Form1()
        {
            InitializeComponent();
            tbScreen.Text = "0";
        }

        private void OnBtnNumberClick(object sender, EventArgs e)
        {
            var clickedValue = (sender as Button).Text;

            if (tbScreen.Text == "0" && clickedValue != ",")
                tbScreen.Text = string.Empty;

            if (_isTheResultOnTheScreen)
            {
                _isTheResultOnTheScreen = false;
                tbScreen.Text = string.Empty;

                if (clickedValue == ",")
                    clickedValue = "0,";
            }

            tbScreen.Text += clickedValue;
            SetResultBtnState(true);

            if (_currentOperation != Operation.None)
                _secondValue += clickedValue;
            else
                SetOperationBtnState(true);
        }

        private void OnBtnOperationClick(object sender, EventArgs e)
        {
            _firstValue = tbScreen.Text;

            var operation = (sender as Button).Text;

            _currentOperation = operation switch
            {
                "+" => Operation.Addition,
                "-" => Operation.Subtraction,
                "/" => Operation.Division,
                "*" => Operation.Multiplication,
                _ => Operation.None,
            };

            tbScreen.Text += $" {operation} ";

            if (_isTheResultOnTheScreen)
                _isTheResultOnTheScreen = false;

            SetOperationBtnState(false);
            SetResultBtnState(false);
        }

        private void OnBtnResultClick(object sender, EventArgs e)
        {
            if (_currentOperation == Operation.None)
                return;

            var firstNumber = double.Parse(_firstValue);
            var secondNumber = double.Parse(_secondValue);

            var result = Calculate(firstNumber, secondNumber);

            tbScreen.Text = result.ToString();
            _secondValue = string.Empty;
            _currentOperation = Operation.None;
            _isTheResultOnTheScreen = true;
            SetOperationBtnState(true);
            SetResultBtnState(true);
        }

        private double Calculate(double firstNumber, double secondNumber)
        {
            switch (_currentOperation)
            {
                case Operation.None:
                    return firstNumber;
                case Operation.Addition:
                    return firstNumber + secondNumber;
                case Operation.Subtraction:
                    return firstNumber - secondNumber;
                case Operation.Division:
                    if (secondNumber == 0)
                    {
                        MessageBox.Show("Nie można dzielić przez 0!");
                        return 0;
                    }
                    return firstNumber / secondNumber;
                case Operation.Multiplication:
                    return firstNumber * secondNumber;
            }

            return 0;
        }

        private void OnBtnClearClick(object sender, EventArgs e)
        {
            tbScreen.Text = "0";
            _firstValue = string.Empty;
            _secondValue = string.Empty;
            _currentOperation = Operation.None;
        }

        private void SetOperationBtnState(bool value)
        {
            btnAdd.Enabled = value;
            btnMultiplication.Enabled = value;
            btnDivision.Enabled = value;
            btnSubtraction.Enabled = value;
        }

        private void SetResultBtnState(bool value)
        {
            btnResult.Enabled = value;
        }
    }
}



SUMMARY


Our application is ready. We managed to write the first desktop application in Windows Forms in C#. It's not an extensive application yet, but you have to start somewhere. Thanks to practice and writing various applications, you will definitely improve your skills. In the following materials, I will also show you how to create applications in other frameworks, e.g. WPF, ASP.NET and Xamarin.

If you liked this article, be sure to join my community. Sign up for the free newsletter, where every week I share valuable materials, especially regarding C# and the .NET platform (free subscription - newsletter).

Author of the article:
Kazimierz Szpin

KAZIMIERZ SZPIN
Software Developer C#/.NET, Freelancer. Specializes in ASP.NET Core, ASP.NET MVC, ASP.NET Web API, Blazor, WPF and Windows Forms.
Author of the blog CodeWithKazik.com

Previous article - First Windows Forms Desktop Application in C# – UI (1/2)
Next article - Programming and Studies. Does a .NET Developer Need Studies?
Dodaj komentarz

Search engine

© Copyright 2024 CodeWithKazik.com. All rights reserved. Privacy policy.
Design by Code With Kazik