티스토리 뷰

반응형

 

2022.02.21 - [WPF] - Microsoft.Toolkit.Mvvm을 이용한 간단한 프레임워크 part1

 

이번에는 프레임워크에서 사용할 수 있는, 금액 표시 컨트롤을 만들어 보도록 하겠습니다. 

커스텀 컨트롤을 만드는데, 기존 프로젝트에 추가하면, 나중에 컨트롤만 따로 다른 프로젝트에서 사용하기가 번거로우니, 커스텀 컨트롤 전용 프로젝트를 만들어서 재사용성을 높이도록 하겠습니다.

 

1. 프로젝트 추가하기

WPF로 검색하면, WPF Application, WPF Class Library, WPF Custom Control Library, WPF User Control Library 이렇게 4가지가 나옵니다, 

  • WPF Application : WPF 응용 프로그램 프로젝트로 빌드하면, exe 파일이 만들어지고, 실행할 수 있습니다.
  • WPF Class Library : 일반적인 라이브러리 프로젝트로가 생각하시면 됩니다. 각종 모델이나 서비스 등을 포함해서 개발할 수 있습니다. dll 확장자를 가지며 단독 실행은 않됩니다. 물론 여기에 UserControl이나 CustomControl을 넣어도 상관은 없습니다. 그런데, UI 컨트롤과 섞이면, 클래스 라이브러리의 재사용성이 떨어질 수도 있기 때문에, 이번에는 여기에 컨트롤을 만들지 않았습니다.
  • WPF Custom Control Library : 커스텀 컨트롤들만 모아 놓은 라이브러리 개발
  • WPF User Control Library : 사용자 컨트롤만 모아 놓은 라이브러리 개발

2. AmountKoreanControl 만들기

프로젝트를 생성했을 때 기본으로 추가된 CustomControl.cs 파일을 열어서 이름을 AmountKoreanControl로 수정한후 Ctrl+.을 눌러서 이름 바꾸기를 선택해서 CustomControl로 되어있는 모든 이름을 AmountKoreanControl로 변경해 줍니다. Generic.xaml에도 이름이 변경되어야 합니다.

 

Generic.xaml 

Custom Control을 추가하면 이 파일에 컨트롤의 기본 모양이 정의되고, 이 내용을 수정해서 AmountKoreanControl에서 구현하고자 하는 목적에 맞게 수정을 해주어야 합니다.

 

아래 코드를 보시면 Grid를 하나 추가하고, 2개의 Row를 넣어서 위에는 TextBlock을 아래는 TextBox를 넣어주었습니다.

TextBlock은 PART_KoreanDisplay, TextBox는 PART_Amount라는 이름을 지어 주었습니다.

PART는 CustomControl에서 사용하는 중요 부분이라는 의미로 붙여줍니다.
CustomControl에 대한 더 자세한 사항은 여기를 참고하시기 바랍니다.
    <Style TargetType="{x:Type local:AmountKoreanControl}">
        <Setter Property="Focusable" Value="False"/>
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type local:AmountKoreanControl}">
                    <Border Background="{TemplateBinding Background}"
                            BorderBrush="{TemplateBinding BorderBrush}"
                            BorderThickness="{TemplateBinding BorderThickness}">
                        <Grid>
                            <Grid.RowDefinitions>
                                <RowDefinition/>
                                <RowDefinition/>
                            </Grid.RowDefinitions>
                            <TextBlock x:Name="PART_KoreanDisplay"/>
                            <TextBox Grid.Row="1" x:Name="PART_Amount"
                                     InputScope="Number"
                                     TextAlignment="Right"
                                     Padding="2"/>
                        </Grid>
                    </Border>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>

AmountKoreanControl.cs

처음할 작업은 PART 컨트롤들을 제어할 수 있도록 가져오는 것입니다.

각 PART의 이름을 const로 정의하고, 그 이름을 이용해서 OnApplyTemplate()에서 컨트롤을 가져다가 내부 컨트롤에 연결시켜 줍니다.

    [TemplatePart(Name = _textBlockName, Type = typeof(TextBlock))]
    [TemplatePart(Name = _textBoxName, Type = typeof(TextBox))]
    public class AmountKoreanControl : Control
    {
        /// <summary>
        /// AmountKoreanControl 컨트롤에서 제어할 컨트롤들의 이름
        /// </summary>
        private const string _textBlockName = "PART_KoreanDisplay";
        private const string _textBoxName = "PART_Amount";
        /// <summary>
        /// AmountKoreanControl 컨트롤에서 제어할 컨트롤들
        /// </summary>
        private TextBlock _koreanDisplayTextBlock;
        private TextBox _amountTextBox;
        /// <summary>
        /// 커스텀 컨트롤에 템플릿이 적용될때
        /// </summary>
        public override void OnApplyTemplate()
        {
            //커스텀 컨트롤 각 PART를 내부에서 사용할 수 있도록 가져옴
            _koreanDisplayTextBlock = GetTemplateChild(_textBlockName) as TextBlock;
            _amountTextBox = GetTemplateChild(_textBoxName) as TextBox;

            if (_amountTextBox == null
                || _koreanDisplayTextBlock == null)
            {
                throw new NullReferenceException("컨트롤의 PART를 찾을 수 없습니다.");
            }

            _amountTextBox.TextChanged += AmountTextBox_TextChanged;
            _amountTextBox.PreviewKeyDown += AmountTextBox_PreviewKeyDown;
        }
        
        ...
        
 }

택스트 박스의 키다운 이벤트 핸들러에서 입력가능한 키를 정의합니다.

메인 숫자키와 키패드의 숫자키의 Code값이 달라서 switch를 이용해서 한번 만들어 보았습니다. 이 switch문은 Visual Studio 2019 이상에서만 가능할 것으로 생각됩니다.

숫자키와 백스페이스, 왼쪽 화살표, 오른쪽 화살표 키만 입력 가능하도록 설정합니다.

        /// <summary>
        /// 키다운 이벤트 핸들러
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void AmountTextBox_PreviewKeyDown(object sender, KeyEventArgs e)
        {
            bool isDigit = false;
            switch((int)e.Key)
            {
                //D0~D9 NumPad0 ~ 9 74-83
                case int n when ((34 <= n && 43 >= n) || (74 <= n && 83 >= n)):
                    isDigit = true;
                    break;
            }
            if (!(isDigit || e.Key == Key.Back 
                || e.Key == Key.Left || e.Key == Key.Right))
            {
                e.Handled = true;
            }
        }

택스트박스 텍스트체인지 이벤트 핸들러의 코드입니다.

_isWork의 기능은 텍스트 체인지 이벤트가 1번 발상한 후 동일 이벤트가 또 발생해서 처리되는 것을 방지하기 위한 것입니다. 

Text를 decimal로 변환해서 DecimalToFormatString() 메소드를 호출합니다. 

        /// <summary>
        /// 택스트 체인지 이벤트 핸들러
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void AmountTextBox_TextChanged(object sender, TextChangedEventArgs e)
        {
            //중복실행방지
            if(_isWork)
            {
                return;
            }
            _isWork = true;

            //전처리
            var numberTextOnly = _amountTextBox.Text.Trim().Replace(",", "");
            if(decimal.TryParse(numberTextOnly, out decimal decimalValue))
            {
                DecimalToFormatString(decimalValue);
            }
            else
            {
                Amount = 0;
                _koreanDisplayTextBlock.Text = "";
            }
            _isWork = false;
        }

위에서 Amount가 등장하는데, 이 녀석은 AmountKoreanControl에 추가한 DependencyProperty입니다. 이 프로퍼티를 이용해서 뷰모델에 바인딩을 할 수 있습니다. 이 프로퍼티가 변경되면, SetAmount 메소드를 실행시켜 TextBox와 TextBlock에 데이터를 업데이트 시켜 줍니다.

control.SetAmount()를 호출하는 이유는 AmountChanged 이벤트 핸들러는 static이기 때문에, 컨트롤에 종속되어있는 SetAmount() 메소드를 호출해 주는 것입니다.

        public decimal Amount
        {
            get { return (decimal)GetValue(AmountProperty); }
            set { SetValue(AmountProperty, value); }
        }

        /// <summary>
        /// Amount DP
        /// </summary>
        public static readonly DependencyProperty AmountProperty =
            DependencyProperty.Register(nameof(Amount), typeof(decimal), typeof(AmountKoreanControl), 
                new PropertyMetadata(decimal.Zero, AmountChanged));

        private static void AmountChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            var control = (AmountKoreanControl)d;
            control.SetAmount();
        }

        private void SetAmount()
        {
            if(_isWork)
            {
                return;
            }
            _isWork = true;
            DecimalToFormatString(Amount);
            _isWork = false;
        }

마지막으로 DecimalToFormatString과 Number2Hangle입니다. Number2Hangle는 블로그를 참고해서 만들었습니다.

if(_amountTextBox == null)은 컨트롤이 초기화 되기전에 Amount DP의 체인지 이벤트가 먼저 발생하는 경우 오류를 방지하기 위해서 추가한 코드입니다.

        /// <summary>
        /// 데시멀을 포멧스트링으로 입력과 한글 금액 출력
        /// </summary>
        /// <param name="decimalValue"></param>
        private void DecimalToFormatString(decimal decimalValue)
        {
            //DP 변경 이벤트로 호출되는 경우 초기화 전에 들어오는 것 방지
            if(_amountTextBox == null)
            {
                return;
            }
            //StringFormat 출력
            _amountTextBox.Text = string.Format("{0:#,##0}", decimalValue);
            //캐럿을 맨뒤로
            _amountTextBox.SelectionStart = _amountTextBox.Text.Length;

            //DP에 값 입력
            Amount = decimalValue;
            var korean = Number2Hangle(Amount);
            _koreanDisplayTextBlock.Text = korean;
        }
        /// <summary>
        /// 숫자를 한글로 - 이 부분은 따로 Utility로 빼서 사용하는 것이 더 좋을듯
        /// </summary>
        /// <param name="lngNumber"></param>
        /// <returns></returns>
        /// <remarks>
        /// http://redqueen-textcube.blogspot.com/2009/12/c-%EC%88%AB%EC%9E%90%EA%B8%88%EC%95%A1-%EB%B3%80%ED%99%98.html
        /// </remarks>
        private string Number2Hangle(decimal lngNumber)
        {
        	...
        }

AmountKoreanControl.cs 전체 소스

using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;

namespace WpfFramework.Controls
{
    [TemplatePart(Name = _textBlockName, Type = typeof(TextBlock))]
    [TemplatePart(Name = _textBoxName, Type = typeof(TextBox))]
    public class AmountKoreanControl : Control
    {
        /// <summary>
        /// AmountKoreanControl 컨트롤에서 제어할 컨트롤들의 이름
        /// </summary>
        private const string _textBlockName = "PART_KoreanDisplay";
        private const string _textBoxName = "PART_Amount";
        /// <summary>
        /// AmountKoreanControl 컨트롤에서 제어할 컨트롤들
        /// </summary>
        private TextBlock _koreanDisplayTextBlock;
        private TextBox _amountTextBox;
        private bool _isWork;

        static AmountKoreanControl()
        {
            DefaultStyleKeyProperty.OverrideMetadata(typeof(AmountKoreanControl), new FrameworkPropertyMetadata(typeof(AmountKoreanControl)));
        }

        /// <summary>
        /// 커스텀 컨트롤에 템플릿이 적용될때
        /// </summary>
        public override void OnApplyTemplate()
        {
            //커스텀 컨트롤 각 PART를 내부에서 사용할 수 있도록 가져옴
            _koreanDisplayTextBlock = GetTemplateChild(_textBlockName) as TextBlock;
            _amountTextBox = GetTemplateChild(_textBoxName) as TextBox;

            if (_amountTextBox == null
                || _koreanDisplayTextBlock == null)
            {
                throw new NullReferenceException("컨트롤의 PART를 찾을 수 없습니다.");
            }
            //Amount 초기값을 설정
            DecimalToFormatString(Amount);

            _amountTextBox.TextChanged += AmountTextBox_TextChanged;
            _amountTextBox.PreviewKeyDown += AmountTextBox_PreviewKeyDown;
        }

        /// <summary>
        /// 키다운 이벤트 핸들러
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void AmountTextBox_PreviewKeyDown(object sender, KeyEventArgs e)
        {
            bool isDigit = false;
            switch ((int)e.Key)
            {
                //D0~D9 NumPad0 ~ 9 74-83
                case int n when ((34 <= n && 43 >= n) || (74 <= n && 83 >= n)):
                    isDigit = true;
                    break;
            }
            if (!(isDigit || e.Key == Key.Back
                || e.Key == Key.Left || e.Key == Key.Right))
            {
                e.Handled = true;
            }
        }
        /// <summary>
        /// 택스트 체인지 이벤트 핸들러
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void AmountTextBox_TextChanged(object sender, TextChangedEventArgs e)
        {
            //중복실행방지
            if (_isWork)
            {
                return;
            }
            _isWork = true;

            //전처리
            var numberTextOnly = _amountTextBox.Text.Trim().Replace(",", "");
            if (decimal.TryParse(numberTextOnly, out decimal decimalValue))
            {
                DecimalToFormatString(decimalValue);
            }
            else
            {
                Amount = 0;
                _koreanDisplayTextBlock.Text = "";
            }
            _isWork = false;
        }

        /// <summary>
        /// 데시멀을 포멧스트링으로 입력과 한글 금액 출력
        /// </summary>
        /// <param name="decimalValue"></param>
        private void DecimalToFormatString(decimal decimalValue)
        {
            //DP 변경 이벤트로 호출되는 경우 초기화 전에 들어오는 것 방지
            if (_amountTextBox == null)
            {
                return;
            }
            //StringFormat 출력
            _amountTextBox.Text = string.Format("{0:#,##0}", decimalValue);
            //캐럿을 맨뒤로
            _amountTextBox.SelectionStart = _amountTextBox.Text.Length;

            //DP에 값 입력
            Amount = decimalValue;
            var korean = Number2Hangle(Amount);
            _koreanDisplayTextBlock.Text = korean;
        }

        /// <summary>
        /// 컨트롤 종료자
        /// </summary>
        ~AmountKoreanControl()
        {
            _amountTextBox.TextChanged -= AmountTextBox_TextChanged;
            _amountTextBox.PreviewKeyDown -= AmountTextBox_PreviewKeyDown;
        }

        /// <summary>
        /// 숫자를 한글로 - 이 부분은 따로 Utility로 빼서 사용하는 것이 더 좋을듯
        /// </summary>
        /// <param name="lngNumber"></param>
        /// <returns></returns>
        /// <remarks>
        /// http://redqueen-textcube.blogspot.com/2009/12/c-%EC%88%AB%EC%9E%90%EA%B8%88%EC%95%A1-%EB%B3%80%ED%99%98.html
        /// </remarks>
        private string Number2Hangle(decimal lngNumber)
        {
            string sign = "";
            string[] numberChar = new string[] { "", "일", "이", "삼", "사", "오", "육", "칠", "팔", "구" };
            string[] levelChar = new string[] { "", "십", "백", "천" };
            string[] decimalChar = new string[] { "", "만", "억", "조", "경" };

            string strValue = string.Format("{0}", lngNumber);
            string numToKorea = sign;
            bool useDecimal = false;

            int i;
            for (i = 0; i < strValue.Length; i++)
            {
                int Level = strValue.Length - i;
                if (strValue.Substring(i, 1) != "0")
                {
                    useDecimal = true;
                    if (((Level - 1) % 4) == 0)
                    {
                        numToKorea = numToKorea + numberChar[int.Parse(strValue.Substring(i, 1))] + decimalChar[(Level - 1) / 4];
                        useDecimal = false;
                    }
                    else
                    {
                        if (strValue.Substring(i, 1) == "1")
                        {
                            numToKorea = numToKorea + levelChar[(Level - 1) % 4];
                        }
                        else
                        {
                            numToKorea = numToKorea + numberChar[int.Parse(strValue.Substring(i, 1))] + levelChar[(Level - 1) % 4];
                        }
                    }
                }
                else
                {
                    if ((Level % 4 == 0) && useDecimal)
                    {
                        numToKorea = numToKorea + decimalChar[Level / 4];
                        useDecimal = false;
                    }
                }
            }
            return numToKorea;
        }

        public decimal Amount
        {
            get { return (decimal)GetValue(AmountProperty); }
            set { SetValue(AmountProperty, value); }
        }

        /// <summary>
        /// Amount DP
        /// </summary>
        public static readonly DependencyProperty AmountProperty =
            DependencyProperty.Register(nameof(Amount), typeof(decimal), typeof(AmountKoreanControl),
                new PropertyMetadata(decimal.Zero, AmountChanged));

        private static void AmountChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            var control = (AmountKoreanControl)d;
            control.SetAmount();
        }

        private void SetAmount()
        {
            if (_isWork)
            {
                return;
            }
            _isWork = true;
            DecimalToFormatString(Amount);
            _isWork = false;
        }
    }
}

HomePage.xaml

테스트를 위해 HomePage에 AmountKoreanControl을 올려 놓았습니다.

Amount 프로퍼티는 뷰모델의 Price와 바인딩을 했고, Init에 Price = 12345678; 이라는 코드를 추가했습니다.

        <StackPanel Grid.Column="1">
            <Button Command="{Binding BusyTestCommand}" Content="Busy Test" />
            <Button Command="{Binding LayerPopupTestCommand}" Content="Layer Popup Test" />
            <TextBox/>
            <controls:AmountKoreanControl x:Name="akc" Amount="{Binding Price, Mode=TwoWay}"/>
            <TextBlock Text="{Binding ElementName=akc, Path=Amount}"/>
        </StackPanel>

3. 실행 결과

숫자 입력 창위에 한글로 금액이 출력됩니다. 하단부에는 Amount가 택스트 블럭에 출력되고 있습니다.

맨 뒤로 이동해서 백스페이스를 누르면 숫자를 지울 수 있습니다.

모든 숫자를 지우면, 한글로된 숫자도 지워지며, 택스트박스에도 아무 내용이 없습니다. 하지만, Amount는 0값을 가지고 있습니다.

4. 소스

kaki104/WpfFramework at part5/add-customcontrol (github.com)

 

GitHub - kaki104/WpfFramework

Contribute to kaki104/WpfFramework development by creating an account on GitHub.

github.com

이 컨트롤에 대한 기능 추가나 스타일 변경이 필요한 경우에는 리플로 요청하시면 확인하고 가능하면 업데이트를 하도록 하겠습니다.

반응형
댓글