본문 바로가기
WPF

WPF MVVM으로 공학용 계산기 만들기: 초보자 가이드

by 개발하는 늑대 2025. 3. 11.
728x90
WPF와 MVVM으로 공학용 계산기 만들기
안녕하세요! 이번 포스트에서는 WPF(Windows Presentation Foundation)와 MVVM(Model-View-ViewModel) 패턴을 활용해 실제 공학용 계산기를 만드는 방법을 소개합니다. 사칙연산뿐만 아니라 삼각함수(sin, cos, tan), 로그, 제곱근 같은 고급 기능도 포함했습니다. 소스 코드를 단계별로 설명하고, 완성된 결과물을 제공하니 따라 해보세요!
프로젝트 개요
  • 목표: WPF로 공학용 계산기 UI를 만들고, MVVM 패턴으로 로직을 분리.
  • 네임스페이스: EngineeringCalculator
  • 주요 기능: 숫자 입력, 사칙연산, 삼각함수, 로그, 초기화(C 버튼)
  • 기술 스택: C#, XAML, WPF, MVVM
프로젝트 구조
  1. Model: CalculatorModel.cs - 계산 로직
  2. ViewModel: CalculatorViewModel.cs - UI와 Model 연결
  3. View: MainWindow.xaml - UI 레이아웃
  4. Code-Behind: MainWindow.xaml.cs - 최소한의 초기화 코드

1. CalculatorModel.cs - 계산 로직
Model은 UI와 독립적으로 수학 연산을 처리합니다.
namespace EngineeringCalculator
{
    // 계산 로직을 담당하는 모델 클래스
    public class CalculatorModel
    {
        public double Add(double a, double b) => a + b; // 덧셈
        public double Subtract(double a, double b) => a - b; // 뺄셈
        public double Multiply(double a, double b) => a * b; // 곱셈
        public double Divide(double a, double b) => b == 0 ? throw new DivideByZeroException() : a / b; // 나눗셈
        public double Power(double baseNum, double exp) => Math.Pow(baseNum, exp); // 거듭제곱
        public double SquareRoot(double num) => Math.Sqrt(num); // 제곱근
        public double Sin(double angle) => Math.Sin(angle * Math.PI / 180); // 사인 (도 단위)
        public double Cos(double angle) => Math.Cos(angle * Math.PI / 180); // 코사인 (도 단위)
        public double Tan(double angle) => Math.Tan(angle * Math.PI / 180); // 탄젠트 (도 단위)
        public double Log(double num) => Math.Log10(num); // 상용로그
    }
}

2. CalculatorViewModel.cs - ViewModel
ViewModel은 UI와 Model을 연결하며, 데이터 바인딩과 명령 처리를 담당합니다. nullable 참조 타입을 고려해 안전하게 설계했습니다.
using System;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Windows.Input;

namespace EngineeringCalculator
{
    public class CalculatorViewModel : INotifyPropertyChanged
    {
        private readonly CalculatorModel _model; // 계산 모델
        private string _display = "0"; // 디스플레이 초기값
        private double _firstNumber; // 첫 번째 숫자
        private string? _operation; // 연산자 (nullable)
        private bool _isNewInput; // 새 입력 여부

        public string Display
        {
            get => _display;
            set
            {
                _display = value ?? "0"; // null 방지
                OnPropertyChanged();
            }
        }

        // 명령 속성
        public ICommand EnterNumberCommand { get; }
        public ICommand SetOperationCommand { get; }
        public ICommand CalculateCommand { get; }
        public ICommand ClearCommand { get; }

        public CalculatorViewModel()
        {
            _model = new CalculatorModel();
            _isNewInput = true;
            EnterNumberCommand = new RelayCommand(EnterNumber);
            SetOperationCommand = new RelayCommand(SetOperation);
            CalculateCommand = new RelayCommand(Calculate);
            ClearCommand = new RelayCommand(Clear);
        }

        // 숫자 입력
        private void EnterNumber(object? parameter)
        {
            string number = parameter?.ToString() ?? "0";
            if (_isNewInput)
            {
                Display = number;
                _isNewInput = false;
            }
            else
            {
                Display += number;
            }
        }

        // 연산자 설정
        private void SetOperation(object? parameter)
        {
            if (!_isNewInput) Calculate(null); // 이전 계산 완료
            _firstNumber = double.Parse(Display);
            _operation = parameter?.ToString();
            _isNewInput = true;
        }

        // 계산 실행
        private void Calculate(object? parameter)
        {
            if (string.IsNullOrEmpty(_operation)) return;
            double secondNumber = double.Parse(Display);
            double result;

            try
            {
                switch (_operation)
                {
                    case "+": result = _model.Add(_firstNumber, secondNumber); break;
                    case "-": result = _model.Subtract(_firstNumber, secondNumber); break;
                    case "*": result = _model.Multiply(_firstNumber, secondNumber); break;
                    case "/": result = _model.Divide(_firstNumber, secondNumber); break;
                    case "^": result = _model.Power(_firstNumber, secondNumber); break;
                    case "√": result = _model.SquareRoot(_firstNumber); break;
                    case "sin": result = _model.Sin(_firstNumber); break;
                    case "cos": result = _model.Cos(_firstNumber); break;
                    case "tan": result = _model.Tan(_firstNumber); break;
                    case "log": result = _model.Log(_firstNumber); break;
                    default: return;
                }
                Display = result.ToString("F6"); // 소수점 6자리
                _isNewInput = true;
            }
            catch (Exception ex)
            {
                Display = $"오류: {ex.Message}";
            }
        }

        // 초기화
        private void Clear(object? parameter)
        {
            Display = "0";
            _firstNumber = 0;
            _operation = null;
            _isNewInput = true;
        }

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

    // 명령 구현
    public class RelayCommand : ICommand
    {
        private readonly Action<object?> _execute;
        private readonly Func<object?, bool>? _canExecute;

        public RelayCommand(Action<object?> execute, Func<object?, bool>? canExecute = null)
        {
            _execute = execute ?? throw new ArgumentNullException(nameof(execute));
            _canExecute = canExecute;
        }

        public event EventHandler? CanExecuteChanged
        {
            add { CommandManager.RequerySuggested += value; }
            remove { CommandManager.RequerySuggested -= value; }
        }

        public bool CanExecute(object? parameter) => _canExecute == null || _canExecute(parameter);
        public void Execute(object? parameter) => _execute(parameter);
    }
}

3. MainWindow.xaml - UI
XAML로 버튼과 디스플레이를 배치하며, ViewModel과 바인딩합니다.
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="400" Width="300">
    <Window.DataContext>
        <local:CalculatorViewModel />
    </Window.DataContext>
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>

        <TextBox Text="{Binding Display, UpdateSourceTrigger=PropertyChanged}" 
                 IsReadOnly="True" FontSize="20" Margin="10" HorizontalAlignment="Right"/>

        <Grid Grid.Row="1" Margin="10">
            <Grid.RowDefinitions>
                <RowDefinition/><RowDefinition/><RowDefinition/><RowDefinition/><RowDefinition/>
            </Grid.RowDefinitions>
            <Grid.ColumnDefinitions>
                <ColumnDefinition/><ColumnDefinition/><ColumnDefinition/><ColumnDefinition/>
            </Grid.ColumnDefinitions>

            <Button Content="7" Grid.Row="0" Grid.Column="0" Command="{Binding EnterNumberCommand}" CommandParameter="7"/>
            <Button Content="8" Grid.Row="0" Grid.Column="1" Command="{Binding EnterNumberCommand}" CommandParameter="8"/>
            <Button Content="9" Grid.Row="0" Grid.Column="2" Command="{Binding EnterNumberCommand}" CommandParameter="9"/>
            <Button Content="/" Grid.Row="0" Grid.Column="3" Command="{Binding SetOperationCommand}" CommandParameter="/"/>

            <Button Content="4" Grid.Row="1" Grid.Column="0" Command="{Binding EnterNumberCommand}" CommandParameter="4"/>
            <Button Content="5" Grid.Row="1" Grid.Column="1" Command="{Binding EnterNumberCommand}" CommandParameter="5"/>
            <Button Content="6" Grid.Row="1" Grid.Column="2" Command="{Binding EnterNumberCommand}" CommandParameter="6"/>
            <Button Content="*" Grid.Row="1" Grid.Column="3" Command="{Binding SetOperationCommand}" CommandParameter="*"/>

            <Button Content="1" Grid.Row="2" Grid.Column="0" Command="{Binding EnterNumberCommand}" CommandParameter="1"/>
            <Button Content="2" Grid.Row="2" Grid.Column="1" Command="{Binding EnterNumberCommand}" CommandParameter="2"/>
            <Button Content="3" Grid.Row="2" Grid.Column="2" Command="{Binding EnterNumberCommand}" CommandParameter="3"/>
            <Button Content="-" Grid.Row="2" Grid.Column="3" Command="{Binding SetOperationCommand}" CommandParameter="-"/>

            <Button Content="0" Grid.Row="3" Grid.Column="0" Command="{Binding EnterNumberCommand}" CommandParameter="0"/>
            <Button Content="=" Grid.Row="3" Grid.Column="1" Grid.ColumnSpan="2" Command="{Binding CalculateCommand}"/>
            <Button Content="+" Grid.Row="3" Grid.Column="3" Command="{Binding SetOperationCommand}" CommandParameter="+"/>

            <Button Content="C" Grid.Row="4" Grid.Column="0" Command="{Binding ClearCommand}"/>
            <Button Content="sin" Grid.Row="4" Grid.Column="1" Command="{Binding SetOperationCommand}" CommandParameter="sin"/>
            <Button Content="cos" Grid.Row="4" Grid.Column="2" Command="{Binding SetOperationCommand}" CommandParameter="cos"/>
            <Button Content="log" Grid.Row="4" Grid.Column="3" Command="{Binding SetOperationCommand}" CommandParameter="log"/>
        </Grid>
    </Grid>
</Window>

4. MainWindow.xaml.cs - 코드 비하인드
최소한의 코드만 유지합니다.
namespace EngineeringCalculator
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent(); // XAML 초기화
        }
    }
}

실행 방법
  1. Visual Studio에서 새 WPF 프로젝트를 만들고 이름을 EngineeringCalculator로 설정합니다.
  2. 위 코드를 각각 파일에 붙여넣습니다.
  3. .csproj에 아래를 추가해 nullable 참조 타입을 활성화합니다:
    xml
    <PropertyGroup>
        <Nullable>enable</Nullable>
    </PropertyGroup>
  4. 빌드하고 실행하면 공학용 계산기가 작동합니다!
동작 설명
  • 숫자 입력: 버튼을 눌러 디스플레이에 숫자를 입력.
  • 연산자: +, -, *, /, sin 등을 선택해 계산 준비.
  • 계산: = 버튼으로 결과 확인.
  • 초기화: C 버튼으로 모든 값 리셋.
추가 개선 아이디어
  • 소수점: . 버튼 추가.
  • 괄호: 복잡한 수식 처리를 위한 파서 구현.
  • 키보드 입력: 키보드 이벤트 추가.

마무리
이 프로젝트는 WPF와 MVVM의 기본을 익히기에 좋은 예제입니다. 코드를 수정하거나 확장하면서 자신만의 계산기를 만들어 보세요. 질문이 있다면 댓글로 남겨주세요!
728x90

'WPF' 카테고리의 다른 글

WPF 계산기 구현  (14) 2025.03.17
WPF 커스텀 텍스트 박스와 원형 버튼 구현하기  (14) 2025.03.10
WPF ComboBox와 MSSQL 연동 완벽 가이드  (8) 2025.03.07
WPF DataGrid 샘플 소스  (6) 2025.03.07
WPF로 간단한 계산기 만들기  (4) 2025.03.07