[WPF] 계산기 만들기

eunne2·2023년 8월 3일
0

WPF

목록 보기
1/2
post-thumbnail

계산기 만들기

1. 기능 정의서 작성
2. 임시 화면 구현
3. 기능 구현
4. 디자인 수정

응애.. 나 WPF 처음 써봐서 태그가 뭐가 뭔지 하나도 모르는 상태로 시작...^_ㅠ


1. 기능 정의서 작성


2. 임시 화면 구현

WPF 알못이기 때문에 최대한 간단하게 구현

숫자와 연산자가 들어갈 Button을 만들고, 입출력 값이 나올 TextBlock 만들어두기


3. 기능 구현

제일 중요한 기능 구현 단계

(1) 필드 설정

private bool op_after; //operator가 눌린 바로 다음에 true
private double? l_value; //처음 입력 값
private double? r_value; //연산자 적용 이후 두번째 입력 값
private char m_Op; //어떤 연산자를 사용했는지 기억

-연산을 위해 이전 값을 저장하고 관리할 필드 설정
(나중에 피드백 들은 거는 enum으로 상태관리 하면서 시작하는게 좋다고 했다 흑흑)

(2) 생성자

public MainWindow()
{
    InitializeComponent();

    // txtResult 기본값 == 0, 출력 위치 조정
    txtResult.Text = "0";
    txtResult.TextAlignment = TextAlignment.Right;
    txtResult.VerticalAlignment = VerticalAlignment.Bottom;

    // txtHistory 기본값 == empty 출력 위치 조정
    txtHistory.Text = string.Empty;
    txtHistory.TextAlignment = TextAlignment.Right;
    txtHistory.VerticalAlignment = VerticalAlignment.Bottom;
}

-박스들 위치 대충 조정해주고 넘어가기

아, 참고로 InitializeComponent();

라고 한다네용,,

(3) 숫자 0~9

private void NumberBtn_Click(object sender, RoutedEventArgs e)
{
    var btn = sender as Button;
    var number = btn.Content.ToString();

    // txtResult의 값이 0이거나 연산자 이후에 눌리는 숫자면 txtResult에 넘버 표시하고 연산자 토글 끄기
    if (txtResult.Text == "0" || op_after == true)
    {
        txtResult.Text = number;
        op_after = false;
    }
    // 소수점이 있다면 소수점 처리
    else if (txtResult.Text.IndexOf('.') != -1)
    {
        txtResult.Text += number;
    }
    else if (double.Parse(txtResult.Text) == 0)
    {
        txtResult.Text = number;
    }
    else
    {
        txtResult.Text += number;
    }
}

-0~9까지 메서드를 다 따로 만들 수도 있지만 그냥 한 번에 넣어버렸읍니다요.

(4) 연산자 +, -, *, /

private void OperBtn_Click(object sender, RoutedEventArgs e)
{
    var btn = sender as Button;
    op_after = true;
    m_Op = char.Parse(btn.Content.ToString());
    l_value = double.Parse(txtResult.Text);
    txtHistory.Text = txtResult.Text.ToString() + " " + btn.Content.ToString();
}

-연산자도 그냥 하나에 다 묶어버렸습니다요^^,,
-연산자 누르면 op_after가 활성화 되면서 다음 숫자가 눌릴 수 있게끔 설정해주고
-이전 값을 l_value에 저장합니다요

(5) Equal =

private bool HasCalcHistory()
{
    return txtHistory.Text.EndsWith("=");
}

private void EqualBtn_Click(object sender, RoutedEventArgs e)
{
    if (HasCalcHistory())
    {
        l_value = double.Parse(txtResult.Text);
    }
    else
    {
        r_value = double.Parse(txtResult.Text);
    }

    txtHistory.Text = $"{l_value} {m_Op} {r_value} =";

    switch (m_Op)
    {
        case '+':
        txtResult.Text = (l_value + r_value).ToString();
        break;

        case '-':
        txtResult.Text = (l_value - r_value).ToString();
        break;

        case '×':
        txtResult.Text = (l_value * r_value).ToString();
        break;

        case '÷':
        if (r_value != 0)
        {
            txtResult.Text = (l_value / r_value).ToString();
        }
        // 0으로 나누는 예외 처리
        else
        {
            ErrorTxt.Visibility = Visibility.Visible;
            txtResult.Visibility = Visibility.Collapsed;
            ErrorTxt.Text = "0으로 나눌 수 없습니다.";

            foreach (UIElement ele in grid.Children)
            {
                if (ele is Button btn && btn.Name.Contains("Oper"))
                {
                    btn.IsEnabled = false;
                }
            }
        }
        break;
    }

    l_value = r_value;

    op_after = true;
}

-어떤 operator가 눌렸는지 확인해서 switch를 돌려줍니다요

-연산이 끝난 상태에서 =을 연속으로 누르면 이전 연산자와 값이 반복적으로 수행되는 기능을 넣기 위해 HasCalcHistory()라는 메서드로 그 상태를 확인해줍니다요
-처음부터 enum 사용해서 상태관리 코드로 짰다면 쉬웠겠지만.. 와타시.. 코응애.. 아무고토 몰라효.. 몰랐어효..

그리고 나눗셈 부분에서 0으로 나누면 윈도우 계산기가

이렇게 나타나니까 이걸 따라해주는 코드도 넣어줍시다요

Visibility 속성을 사용해 결과값 박스는 가리고 안내 메세지를 띄울 라벨을 보이게 설정
UIElement ele in grid.Children 으로 grid의 자식놈들 즉, 버튼에 접근해서, 숫자와 Clear버튼 외의 연산 버튼에 lock을 걸어줍니다 → btn.IsEnabled = false; 하면 됨

(6) Clear (back space, clear entry, clear)

private void ClearBtn_Click(object sender, RoutedEventArgs e)
{
    var btn = sender as Button;

    // back space
    if (btn.Content.ToString() == "back")
    {
        txtResult.Text = txtResult.Text.Remove(txtResult.Text.Length - 1);

        if (txtResult.Text.Length == 0)
        {
            txtResult.Text = "0";
        }
    }

    // clear entry
    if (btn.Content.ToString() == "CE")
    {
        // 숫자 입력 상태에서 누르면 숫자만 초기화
        txtResult.Text = "0";

        // 연산 완료 상태에서 누르면 전체 초기화
        if (op_after)
        {
            txtHistory.Text = string.Empty;
            txtResult.Text = "0";
            l_value = null;
            r_value = null;
            op_after = false;
        }
    }

    // clear all
    if (btn.Content.ToString() == "C")
    {
        txtHistory.Text = string.Empty;
        txtResult.Text = "0";
        l_value = null;
        r_value = null;
        op_after = false;
    }

    // 오류 메시지 지움과 동시에 버튼 활성화
    ErrorTxt.Visibility = Visibility.Collapsed;
    txtResult.Visibility = Visibility.Visible;

    foreach (UIElement ele in grid.Children)
    {
        if (ele is Button button && button.Name.Contains("Oper"))
        {
        button.IsEnabled = true;
        }
    }
}

그렇습니다. 이것도 그냥 하나에 묶어버렸습니다.
다 따로 두는 게 더 나을지도 모르지만 그냥 묶어버렸읍니다...

(7) Dot .

private void DotBtn_Click(object sender, RoutedEventArgs e)
{
    // 연산자 입력 후 or 값이 0일 때
    if (op_after || txtResult.Text == "0")
    {
        txtResult.Text = "0.";
        op_after = false;
    }
    // 정수일 때
    else if (double.Parse(txtResult.Text) == (int)(double.Parse(txtResult.Text)))
    {
        txtResult.Text += ".";
    }
}

(8) Sign +/-

private void SignBtn_Click(object sender, RoutedEventArgs e)
{
    txtResult.Text = (-1 * double.Parse(txtResult.Text)).ToString();
}

(9) Percent %

private void PercentBtn_Click(object sender, RoutedEventArgs e)
{
    var percentValue = l_value * (double.Parse(txtResult.Text) * 0.01);
    txtHistory.Text += "" + percentValue.ToString();
    txtResult.Text = percentValue.ToString();
}

(10) Sqrt ²√x

private void SqrtBtn_Click(object sender, RoutedEventArgs e)
{
    txtResult.Text = Math.Sqrt(double.Parse(txtResult.Text)).ToString();
    op_after = true;
}

(11) Pow x²

private void PowBtn_Click(object sender, RoutedEventArgs e)
{
    txtResult.Text = Math.Pow(double.Parse(txtResult.Text), 2).ToString();
}

(12) Reciprocal 1/x

private void ReciprocalBtn_Click(object sender, RoutedEventArgs e)
{
    txtHistory.Text = $"1/( {txtResult.Text} )";
    txtResult.Text = (1.0 / double.Parse(txtResult.Text)).ToString();
    op_after = true;
}

(13) 기타 계산기 기능

1. 세로 길이가 일정 크기 이하로 줄어들면 기타 연산자 버튼이 사라짐

이거 진짜 화딱지나서 허공에 주먹질 많이 했는데...
사실 완벽하진 않지만 그냥 어쨌든... 해보긴 했다 ^_^...ㅜ

일단 cs 코드 먼저 살펴보자요

private void Window_SizeChanged(object sender, SizeChangedEventArgs e)
{
    // 프로그램의 세로 길이를 측정합니다.
    double windowHeight = e.NewSize.Height;

    // 프로그램의 세로 길이에 따라 상태를 변경합니다.
    if (windowHeight <= 400)
    {
        // 상태를 "Compact"로 변경합니다.
        // VisualStateManager.GoToState(this, "Compact", true);
        VisualStateManager.GoToElementState(this.Content as FrameworkElement, "Compact", true);

    }
    else
    {
        // 상태를 "Normal"로 변경합니다.
        // VisualStateManager.GoToState(this, "Normal", true);
        VisualStateManager.GoToElementState(this.Content as FrameworkElement, "Normal", true);
    }
}

주석 처리해둔
// VisualStateManager.GoToState(this, "Normal", true);
를 먼저 작성했었는데, 무슨 짓을 해도 자꾸 false가 뜨더라구효..? 폭풍 검색 결과..


https://hackss.tistory.com/entry/surface-WPF%EC%97%90%EC%84%9C-VisualState-%EC%93%B0%EA%B8%B0

ㅎ.. 버그라네요...
암튼 코드 수정하니까 뚝딱 적용 완료

--

그다음 xaml 코드..
원래 xaml 코드를 stackPanel, Grid를 사용했는데, 이거 싹 다 뜯어고침ㅋㅋㅋ큐ㅠㅠ

완성된 코드가 대충 어떤 느낌이냐면

<!--Window에 SizeChanged 속성-->
<Window
SizeChanged="Window_SizeChanged"
>
    <Grid>
        <!--VisualStateManager 길이에 따라 상태 분류하는 코드-->
        <VisualStateManager.VisualStateGroups>
        	<!--내용 채우기-->
        </VisualStateManager.VisualStateGroups>
        
        <!--입력 숫자, 연산 결과 등이 표시되는 창-->
        <TextBlock />
        
        <!--숫자, 연산자 버튼들-->
        <UniformGrid>
        	<Button />
            <Button />
            ...
        </UniformGrid>
    </Grid>
</Window>

처음에 VisualStateManager 이걸 UniformGrid 태그 안에 넣었더니 코드가 실행이 안 되길래 chatGPT한테 물어봤다.

글쿤.. 하고 바로 상위 태그로 빼버렸더니 띠롱 하고 실행 잘 됨ㅎㅎ

2. 프로그램 최상위 설정

Window 태그에 넣거나

속성에서 Topmost 체크해주면 끝

3. 키보드 이벤트 추가

private void Window_KeyDown(object sender, KeyEventArgs e)
{
    // 아래 조건을 제외한 모든 키는 true 처리하여 이벤트 체인에서 중단
    if (!((Key.D0 <= e.Key && e.Key <= Key.D9)
        || (Key.NumPad0 <= e.Key && e.Key <= Key.NumPad9)
        || (e.Key == Key.Back) || (e.Key == Key.Enter)
        || ("+-*/".Contains(e.Key.ToString())
        || (e.Key == Key.Oem5) || (e.Key == Key.OemPeriod))))
    {
        e.Handled = true;
    }

    // 숫자 처리
    switch (e.Key)
    {
        case Key.D0:
        case Key.NumPad0:
            NumberBtn_Click(number0, e);
            break;
        case Key.D1:
        case Key.NumPad1:
            NumberBtn_Click(number1, e);
            break;
        case Key.D2:
        case Key.NumPad2:
            NumberBtn_Click(number2, e);
            break;
        case Key.D3:
        case Key.NumPad3:
            NumberBtn_Click(number3, e);
            break;
        case Key.D4:
        case Key.NumPad4:
            NumberBtn_Click(number4, e);
            break;
        case Key.D5:
        case Key.NumPad5:
            NumberBtn_Click(number5, e);
            break;
        case Key.D6:
        case Key.NumPad6:
            NumberBtn_Click(number6, e);
            break;
        case Key.D7:
        case Key.NumPad7:
            NumberBtn_Click(number7, e);
            break;
        case Key.D8:
        case Key.NumPad8:
            NumberBtn_Click(number8, e);
            break;
        case Key.D9:
        case Key.NumPad9:
            NumberBtn_Click(number9, e);
            break;
    }

    // 사칙연산 처리
    if (e.Key == Key.Add)
    {
        OperBtn_Click(Oper8, e);
    }
    else if (e.Key == Key.Subtract)
    {
        OperBtn_Click(Oper7, e);
    }
    else if (e.Key == Key.Multiply)
    {
        OperBtn_Click(Oper6, e);
    }
    else if (e.Key == Key.Divide)
    {
        OperBtn_Click(Oper5, e);
    }

    // % 처리
    if (e.Key == Key.Oem5 && Keyboard.IsKeyDown(Key.LeftShift)
        || e.Key == Key.Oem5 && Keyboard.IsKeyDown(Key.RightShift))
    {
        PercentBtn_Click(OperAdditional, e);
    }

    // .
    if (e.Key == Key.OemPeriod)
    {
        DotBtn_Click(Oper10, e);
    }

    // back 처리
    if (e.Key == Key.Back)
    {
        ClearBtn_Click(Clear3, e);
    }

    // enter 처리
    if (e.Key == Key.Enter)
    {
        EqualBtn_Click(Enter, e);
    }
}

위와 같이 작성해서 Window 태그에 프로퍼티로 KeyDown 이벤트 추가하면

될 줄 알았지...

근데 키보드 이벤트를 주니 문제가 하나 생겼는데, 그건 바로 버튼 클릭 후 키보드를 누르면 버튼에 포커스가 가서 엔터를 눌렀을 때 연산이 제대로 실행되지 않는 문제가 발생해버렸읍니다 흑흑

해결하기 위해 버튼에 잡힌 포커스 제거

btn.Focusable = false;

↑ 이거를 버튼 클릭 이벤트마다 추가해줬더니 버튼 클릭 포커스 해제 완료


4. 디자인 수정

이건 뭐.. 얼렁뚱땅 이렇게 저렇게 하다보면 대충 만들어진닿
CSS랑 얼추 비슷해서 대충 지피티한테 css에서 이거 wpf에서 머야? 하고 물어보면 알려주도라,,,

디테일한 디자인은 생략한다 (힘드니까..)

이런식으로 Resources 안에 setter로 전달해줬다.

CornerRadius랑 Min/Max, text 위치 조절만 신경 좀 써주고 다른 부분 얼렁뚱땅 하다보면 끝


5. 결과

실행해보면

왼쪽이 내가 만든 거, 오른쪽이 실제 윈도우 계산기

버튼도 잘 줄어들고, 상단 고정도 잘 된다요^_^

키보드 이벤트도 잘 먹히고 버튼 블럭 처리도 되고...

암튼 계산기 버전1은 여기까지...

이제 버전2로 MVVM 공부해서 코드 뜯어 고쳐야함ㅎㅎ..ㅜㅜ

끝!!!

profile
코딩이 하고 싶은 응애 은네...

1개의 댓글

comment-user-thumbnail
2023년 8월 3일

개발자로서 배울 점이 많은 글이었습니다. 감사합니다.

답글 달기