본문 바로가기
WPF

WPF 계산기 구현

by 개발하는 늑대 2025. 3. 17.
728x90
WPF와 MVVM으로 구현한 공학용 계산기
이번 포스트에서는 WPF와 MVVM 패턴을 활용해 공학용 계산기를 만드는 방법을 소개합니다. 사칙연산, 지수 연산, 삼각 함수를 지원하며, 실시간 입력 반영과 키보드 입력까지 구현했습니다. 소스 코드를 단계별로 살펴보고, 직접 실행해 보세요!
구현 기능
  • 사칙연산: +, -, ×, ÷
  • 지수 연산: 제곱(^), 루트(√), 역수(1/x)
  • 삼각 함수: sin, cos, tan
  • 실시간 입력 반영: 입력 시 즉시 UI 업데이트
  • 키보드 입력 지원: 숫자와 연산자 키 입력 가능
  • MVVM 패턴: 데이터 바인딩과 명령 패턴 적용
프로젝트 구조
 
📂 EngineeringCalculator
├── 📄 MainWindow.xaml (뷰)
├── 📄 MainWindow.xaml.cs (뷰의 코드 비하인드)
├── 📄 EngineeringCalculatorViewModel.cs (ViewModel)
├── 📄 RelayCommand.cs (명령 패턴)

1. MainWindow.xaml (XAML)
UI를 정의하며, 버튼과 텍스트 박스를 ViewModel과 바인딩합니다. 키보드 입력을 위해 KeyDown 이벤트도 추가했습니다.
xml
 
<Window x:Class="EngineeringCalculator.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:EngineeringCalculator"
        Title="공학용 계산기" Height="600" Width="400"
        KeyDown="Window_KeyDown">
    <Window.DataContext>
        <local:EngineeringCalculatorViewModel/>
    </Window.DataContext>
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>

        <!-- 입력 및 결과 창 -->
        <StackPanel Grid.Row="0" Margin="10">
            <TextBox Text="{Binding Input, UpdateSourceTrigger=PropertyChanged}" 
                     FontSize="30" HorizontalAlignment="Stretch" IsReadOnly="True" 
                     TextAlignment="Right" Margin="0,0,0,5"/>
            <TextBlock Text="{Binding Result}" FontSize="32" FontWeight="Bold" 
                       HorizontalAlignment="Right" Foreground="Blue"/>
        </StackPanel>

        <!-- 버튼 레이아웃 -->
        <Grid Grid.Row="1" Margin="10">
            <Grid.RowDefinitions>
                <RowDefinition/><RowDefinition/><RowDefinition/><RowDefinition/><RowDefinition/><RowDefinition/>
            </Grid.RowDefinitions>
            <Grid.ColumnDefinitions>
                <ColumnDefinition/><ColumnDefinition/><ColumnDefinition/><ColumnDefinition/>
            </Grid.ColumnDefinitions>

            <!-- 첫 번째 줄: 초기화, 백스페이스, 제곱, 나누기 -->
            <Button Content="C" Grid.Row="0" Grid.Column="0" Command="{Binding ClearCommand}"/>
            <Button Content="⌫" Grid.Row="0" Grid.Column="1" Command="{Binding BackspaceCommand}"/>
            <Button Content="x²" Grid.Row="0" Grid.Column="2" Command="{Binding ButtonCommand}" CommandParameter="^2"/>
            <Button Content="÷" Grid.Row="0" Grid.Column="3" Command="{Binding ButtonCommand}" CommandParameter="÷"/>

            <!-- 두 번째 줄: 숫자 및 곱셈 -->
            <Button Content="7" Grid.Row="1" Grid.Column="0" Command="{Binding ButtonCommand}" CommandParameter="7"/>
            <Button Content="8" Grid.Row="1" Grid.Column="1" Command="{Binding ButtonCommand}" CommandParameter="8"/>
            <Button Content="9" Grid.Row="1" Grid.Column="2" Command="{Binding ButtonCommand}" CommandParameter="9"/>
            <Button Content="×" Grid.Row="1" Grid.Column="3" Command="{Binding ButtonCommand}" CommandParameter="×"/>

            <!-- 세 번째 줄: 숫자 및 뺄셈 -->
            <Button Content="4" Grid.Row="2" Grid.Column="0" Command="{Binding ButtonCommand}" CommandParameter="4"/>
            <Button Content="5" Grid.Row="2" Grid.Column="1" Command="{Binding ButtonCommand}" CommandParameter="5"/>
            <Button Content="6" Grid.Row="2" Grid.Column="2" Command="{Binding ButtonCommand}" CommandParameter="6"/>
            <Button Content="-" Grid.Row="2" Grid.Column="3" Command="{Binding ButtonCommand}" CommandParameter="-"/>

            <!-- 네 번째 줄: 숫자 및 덧셈 -->
            <Button Content="1" Grid.Row="3" Grid.Column="0" Command="{Binding ButtonCommand}" CommandParameter="1"/>
            <Button Content="2" Grid.Row="3" Grid.Column="1" Command="{Binding ButtonCommand}" CommandParameter="2"/>
            <Button Content="3" Grid.Row="3" Grid.Column="2" Command="{Binding ButtonCommand}" CommandParameter="3"/>
            <Button Content="+" Grid.Row="3" Grid.Column="3" Command="{Binding ButtonCommand}" CommandParameter="+"/>

            <!-- 다섯 번째 줄: 0, 소수점, 루트, 역수 -->
            <Button Content="0" Grid.Row="4" Grid.Column="0" Grid.ColumnSpan="2" Command="{Binding ButtonCommand}" CommandParameter="0"/>
            <Button Content="." Grid.Row="4" Grid.Column="2" Command="{Binding ButtonCommand}" CommandParameter="."/>
            <Button Content="√" Grid.Row="4" Grid.Column="3" Command="{Binding ButtonCommand}" CommandParameter="√"/>

            <!-- 여섯 번째 줄: 삼각 함수 및 계산 -->
            <Button Content="sin" Grid.Row="5" Grid.Column="0" Command="{Binding ButtonCommand}" CommandParameter="sin"/>
            <Button Content="cos" Grid.Row="5" Grid.Column="1" Command="{Binding ButtonCommand}" CommandParameter="cos"/>
            <Button Content="tan" Grid.Row="5" Grid.Column="2" Command="{Binding ButtonCommand}" CommandParameter="tan"/>
            <Button Content="=" Grid.Row="5" Grid.Column="3" Command="{Binding CalculateCommand}" Background="Orange"/>
        </Grid>
    </Grid>
</Window>

2. MainWindow.xaml.cs (코드 비하인드)
키보드 입력을 처리하기 위해 KeyDown 이벤트를 추가했습니다.
csharp
 
using System.Windows;
using System.Windows.Input;

namespace EngineeringCalculator
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }

        // 키보드 입력 처리
        private void Window_KeyDown(object sender, KeyEventArgs e)
        {
            var vm = (EngineeringCalculatorViewModel)DataContext;
            switch (e.Key)
            {
                case Key.D0: case Key.NumPad0: vm.ButtonCommand.Execute("0"); break;
                case Key.D1: case Key.NumPad1: vm.ButtonCommand.Execute("1"); break;
                case Key.D2: case Key.NumPad2: vm.ButtonCommand.Execute("2"); break;
                case Key.D3: case Key.NumPad3: vm.ButtonCommand.Execute("3"); break;
                case Key.D4: case Key.NumPad4: vm.ButtonCommand.Execute("4"); break;
                case Key.D5: case Key.NumPad5: vm.ButtonCommand.Execute("5"); break;
                case Key.D6: case Key.NumPad6: vm.ButtonCommand.Execute("6"); break;
                case Key.D7: case Key.NumPad7: vm.ButtonCommand.Execute("7"); break;
                case Key.D8: case Key.NumPad8: vm.ButtonCommand.Execute("8"); break;
                case Key.D9: case Key.NumPad9: vm.ButtonCommand.Execute("9"); break;
                case Key.Add: vm.ButtonCommand.Execute("+"); break;
                case Key.Subtract: vm.ButtonCommand.Execute("-"); break;
                case Key.Multiply: vm.ButtonCommand.Execute("×"); break;
                case Key.Divide: vm.ButtonCommand.Execute("÷"); break;
                case Key.Decimal: vm.ButtonCommand.Execute("."); break;
                case Key.Enter: vm.CalculateCommand.Execute(null); break;
                case Key.Back: vm.BackspaceCommand.Execute(null); break;
                case Key.Escape: vm.ClearCommand.Execute(null); break;
            }
        }
    }
}

3. EngineeringCalculatorViewModel.cs (ViewModel)
실시간 입력 반영과 모든 기능을 처리합니다. DataTable을 사용해 수식 계산을 간소화하고, 삼각 함수와 지수 연산을 별도로 처리했습니다.
csharp
 
using System;
using System.ComponentModel;
using System.Windows.Input;
using System.Data;
using System.Text.RegularExpressions;

namespace EngineeringCalculator
{
    public class EngineeringCalculatorViewModel : INotifyPropertyChanged
    {
        private string _input = ""; // 사용자 입력 문자열
        private string _result = "0"; // 계산 결과

        public string Input
        {
            get => _input;
            set { _input = value; OnPropertyChanged(); }
        }

        public string Result
        {
            get => _result;
            set { _result = value; OnPropertyChanged(); }
        }

        public ICommand ButtonCommand { get; }
        public ICommand ClearCommand { get; }
        public ICommand BackspaceCommand { get; }
        public ICommand CalculateCommand { get; }

        public EngineeringCalculatorViewModel()
        {
            ButtonCommand = new RelayCommand<string>(AppendInput);
            ClearCommand = new RelayCommand(Clear);
            BackspaceCommand = new RelayCommand(Backspace);
            CalculateCommand = new RelayCommand(Calculate);
        }

        // 입력 추가 및 실시간 처리
        private void AppendInput(string value)
        {
            Input += value;
            if (!Regex.IsMatch(value, "[+\\-×÷√^]")) // 연산자가 아닌 경우 실시간 계산
                CalculateSilently();
        }

        // 계산기 초기화
        private void Clear()
        {
            Input = "";
            Result = "0";
        }

        // 마지막 입력 삭제
        private void Backspace()
        {
            if (!string.IsNullOrEmpty(Input))
            {
                Input = Input.Substring(0, Input.Length - 1);
                CalculateSilently();
            }
        }

        // 실시간 계산 (결과만 갱신, 입력 유지)
        private void CalculateSilently()
        {
            try
            {
                string expression = PrepareExpression(Input);
                if (string.IsNullOrEmpty(expression)) return;
                DataTable dt = new DataTable();
                var result = dt.Compute(expression, "");
                Result = Convert.ToDouble(result).ToString("F6");
            }
            catch
            {
                Result = "0"; // 오류 시 기본값
            }
        }

        // 최종 계산
        private void Calculate()
        {
            try
            {
                string expression = PrepareExpression(Input);
                if (string.IsNullOrEmpty(expression))
                {
                    Result = "0";
                    return;
                }

                if (expression.StartsWith("sin") || expression.StartsWith("cos") || expression.StartsWith("tan"))
                {
                    double num = double.Parse(Regex.Match(expression, @"\d+\.?\d*").Value);
                    Result = expression.StartsWith("sin") ? Math.Sin(num * Math.PI / 180).ToString("F6") :
                            expression.StartsWith("cos") ? Math.Cos(num * Math.PI / 180).ToString("F6") :
                            Math.Tan(num * Math.PI / 180).ToString("F6");
                }
                else if (expression.StartsWith("√"))
                {
                    double num = double.Parse(expression.Substring(1));
                    Result = Math.Sqrt(num).ToString("F6");
                }
                else if (expression.EndsWith("^2"))
                {
                    double num = double.Parse(expression.Substring(0, expression.Length - 2));
                    Result = Math.Pow(num, 2).ToString("F6");
                }
                else
                {
                    DataTable dt = new DataTable();
                    var result = dt.Compute(expression, "");
                    Result = Convert.ToDouble(result).ToString("F6");
                }
                Input = Result; // 결과로 입력 갱신
            }
            catch
            {
                Result = "오류";
            }
        }

        // 수식 준비 (특수 연산 처리)
        private string PrepareExpression(string input)
        {
            return input.Replace("÷", "/").Replace("×", "*").Trim();
        }

        // 속성 변경 알림
        public event PropertyChangedEventHandler? PropertyChanged;
        protected void OnPropertyChanged([CallerMemberName] string? propertyName = null)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

4. RelayCommand.cs (Command Helper)
명령 패턴을 구현한 헬퍼 클래스입니다.
csharp
 
using System;
using System.Windows.Input;

namespace EngineeringCalculator
{
    public class RelayCommand<T> : ICommand
    {
        private readonly Action<T> _execute;
        public event EventHandler? CanExecuteChanged;

        public RelayCommand(Action<T> execute) => _execute = execute;

        public bool CanExecute(object? parameter) => true;
        public void Execute(object? parameter) => _execute((T)parameter!);
    }

    public class RelayCommand : RelayCommand<object>
    {
        public RelayCommand(Action execute) : base(_ => execute()) { }
    }
}

최종 실행 결과
  • 실시간 연산 지원: 숫자 입력 시 즉시 결과 반영
  • MVVM 패턴 완벽 적용: 데이터 바인딩과 명령으로 로직 분리
  • 사칙연산 및 고급 기능 추가: 제곱, 루트, 삼각 함수 구현
  • 키보드 입력 가능: 숫자, 연산자, Enter, Backspace, Esc 지원
실행 방법
  1. Visual Studio에서 새 WPF 프로젝트를 생성하고 이름을 EngineeringCalculator로 설정.
  2. 위 코드를 각 파일에 붙여넣기.
  3. .csproj<Nullable>enable</Nullable> 추가.
  4. 빌드 후 실행!
사용 예시
  • 5 + 3 입력 → 실시간 결과 8, = 클릭 시 입력도 8로 갱신
  • sin 30 입력 → = 클릭 시 0.5 출력
  • 키보드로 4 * 2 입력 후 Enter → 8 출력
이제 블로그에 올려서 많은 분들과 공유해 보세요! 추가 질문 있으면 언제든 댓글 남겨주세요.
사실 제가 공부중이라 잊어 버리지 않기 위해 기록합니다.
728x90