728x90
WPF와 MVVM으로 공학용 계산기 만들기
안녕하세요! 이번 포스트에서는 WPF(Windows Presentation Foundation)와 MVVM(Model-View-ViewModel) 패턴을 활용해 실제 공학용 계산기를 만드는 방법을 소개합니다. 사칙연산뿐만 아니라 삼각함수(sin, cos, tan), 로그, 제곱근 같은 고급 기능도 포함했습니다. 소스 코드를 단계별로 설명하고, 완성된 결과물을 제공하니 따라 해보세요!
프로젝트 개요
-
목표: WPF로 공학용 계산기 UI를 만들고, MVVM 패턴으로 로직을 분리.
-
네임스페이스: EngineeringCalculator
-
주요 기능: 숫자 입력, 사칙연산, 삼각함수, 로그, 초기화(C 버튼)
-
기술 스택: C#, XAML, WPF, MVVM
프로젝트 구조
-
Model: CalculatorModel.cs - 계산 로직
-
ViewModel: CalculatorViewModel.cs - UI와 Model 연결
-
View: MainWindow.xaml - UI 레이아웃
-
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 초기화
}
}
}
실행 방법
-
Visual Studio에서 새 WPF 프로젝트를 만들고 이름을 EngineeringCalculator로 설정합니다.
-
위 코드를 각각 파일에 붙여넣습니다.
-
.csproj에 아래를 추가해 nullable 참조 타입을 활성화합니다:xml
<PropertyGroup> <Nullable>enable</Nullable> </PropertyGroup>
-
빌드하고 실행하면 공학용 계산기가 작동합니다!
동작 설명
-
숫자 입력: 버튼을 눌러 디스플레이에 숫자를 입력.
-
연산자: +, -, *, /, 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 |