WPF dateedit 에 달력 ui 수정

wpf에 devexpress 사용하고 있습니다.

dxe:Dateedit에서 날짜를 입력하려면 뜨는 달력을 수정하고 싶습니다.

그 달력에 특정 날짜는 빨간색으로 칠한다든가, 선택이 안 되게 하고 싶은데 어떻게 하는지 알 수 있을까요?

1 Like

아마 dxe:DataEdit의 Style과 Control을 상속받는 CustomControl을 정의하셔야 할 것입니다.
ControlTemplate을 통해 기존 디자인을 무시하고 새로 디자인을 하시는데, TemplateBinding으로 기존 속성을 가져다가 쓰실 수 있어서 아마 약간의 변경 정도는 할 수 있지 않을까 싶네요.

1 Like

저도 조금 호기심이 생겨 시간 날 때 한번 도전해보고 싶네요~
혹시 실례가 되지 않는다면 DevExpress 버전을 알려주실 수 있을까요?
혹시 제가 가진 버전들 중에 있다면, 기왕이면 같은 버전으로 해보는 것이 좋을 것 같아서요~

2 Likes

답변 감사합니다. 한번 해보겠습니다.

2 Likes

데브 24.2.7.0 버전입니다. 감사합니다.

2 Likes

제가 회사에서 DevExpress를 꽤 오랫동안 사용해 와서 그런지 문의하신 사항에 호기심이 발동했습니다.

그렇기도 하지만 이런 문의사항 등에 대한 해결책을 하나둘씩 모아두면 닷넷 생태계가 그나마 나아지지 않을까 하는 기대를 하며 글을 작성해봅니다.

DevExpress 24.2 버전을 기준으로 작성합니다.

일단, 문의하신 내용은 다음과 같습니다.

처음에는 스타일이나 템플릿 재정의를 써야하지 않을까 했지만, DevExpress에서 관련 기능을 만들어 둔 것이 있더군요~

예를 들어 다음과 같은 뷰모델이 있다고 하고,

public class ViewModel
{
    public ViewModel()
    {
        Holidays = new ObservableCollection<DateTime> { new DateTime(2025, 11, 11) };
        DisabledDates = new ObservableCollection<DateTime> { new DateTime(2025, 11, 12) };
        SpecialDates = new ObservableCollection<DateTime> { new DateTime(2025, 11, 13) };
    }

    public ObservableCollection<DateTime> Holidays { get; }
    public ObservableCollection<DateTime> DisabledDates { get; }
    public ObservableCollection<DateTime> SpecialDates { get; }
}

다음과 같이 달력 컨트롤인 DateNavigator를 사용한다면,

<Grid>
    <Grid.DataContext>
        <local:ViewModel/>
    </Grid.DataContext>
        
    <dxe:DateNavigator Holidays="{Binding Holidays}" DisabledDates="{Binding DisabledDates}" SpecialDates="{Binding SpecialDates}"/>
        
</Grid>

이렇게 실행될 것입니다.

보시는 바와 같이 11월 11일이 휴일로 인식되어 빨갛게 표시되고, 12일은 선택할 수 없게 되고, 13일은 글자가 굵게(별로 표시는 안 나지만..) 나옵니다.

그런데 원하시는 것은 DateEdit에 입력하기 위해 열린 팝업에 나타난 달력(DateNavigator)을 수정하시는 것이죠. 사실 DateEdit가 팝업에 자동으로 생성하는 DateNavigator 객체를 직접 찾아서 수정하는 것은 쉽지 않습니다.

그렇지만 WPF는 스타일을 이용해서 속성을 설정할 수 있습니다.
다음과 같이 DateEdit의 리소스에 DateNavigator의 스타일을 정의하면 됩니다.

<Grid>
    <Grid.DataContext>
        <local:ViewModel/>
    </Grid.DataContext>

    <dxe:DateEdit HorizontalAlignment="Center" VerticalAlignment="Top" Width="150" Margin="60">
        <dxe:DateEdit.Resources>
            <Style TargetType="dxe:DateNavigator">
                <Setter Property="Holidays" Value="{Binding Holidays}"/>
                <Setter Property="DisabledDates" Value="{Binding DisabledDates}"/>
                <Setter Property="SpecialDates" Value="{Binding SpecialDates}"/>
            </Style>
        </dxe:DateEdit.Resources>
        <dxe:DateEdit.StyleSettings>
            <dxe:DateEditNavigatorStyleSettings />
        </dxe:DateEdit.StyleSettings>
    </dxe:DateEdit>
</Grid>

DateEdit의 팝업에 HolidaysDisabledDatesSpecialDates가 적용된 것을 알 수 있습니다.
또한, DisabledDates에 포함된 11월 12일은 선택할 수 없습니다.

그렇지만, DateEdit에 키보드로 직접 입력하면 11월 12일도 입력 가능합니다.
이건 DateEditValidate이벤트나 DevExpress의 Validation 관련 기능을 찾아보시고 직접 구현하셔야 할 것입니다.

그리고.. 음..

지금까지는 Holidays, DisabledDates, SpecialDates 같은 속성을 이용한 방법이었습니다만, 좀 더 하드코어(…)하게 달력 숫자들을 직접 다양한 색상으로 표현하고 싶다면 템플릿을 직접 정의해야 합니다.

제가 작성해본 코드는 일월화수목금토를 빨주노초파남보로 표시하는 예제입니다.

일단, 뷰모델에 DateTime을 ARGB 색상 코드로 바꿔주는 Func<DateTime, uint> 형식의 속성을 추가해 줍니다.

public class ViewModel
{
    public ViewModel()
    {
        Holidays = new ObservableCollection<DateTime> { new DateTime(2025, 11, 11) };
        DisabledDates = new ObservableCollection<DateTime> { new DateTime(2025, 11, 12) };
        SpecialDates = new ObservableCollection<DateTime> { new DateTime(2025, 11, 13) };

        DateTimeToColor = dateTime =>
        {
            switch (dateTime.DayOfWeek)
            {
                case DayOfWeek.Sunday:    return 0XFFFF0000u;
                case DayOfWeek.Monday:    return 0XFFFF8000u;
                case DayOfWeek.Tuesday:   return 0XFFFFFF00u;
                case DayOfWeek.Wednesday: return 0XFF00FF00u;
                case DayOfWeek.Thursday:  return 0XFF0000FFu;
                case DayOfWeek.Friday:    return 0XFF8000FFu;
                case DayOfWeek.Saturday:  return 0XFFFF00FFu;
                default: return 0;
            }
        };
    }

    public ObservableCollection<DateTime> Holidays { get; }
    public ObservableCollection<DateTime> DisabledDates { get; }
    public ObservableCollection<DateTime> SpecialDates { get; }

    public Func<DateTime, uint> DateTimeToColor { get; }
}

그리고 멀티 바인딩을 위한 컨버터도 만들어줍니다.

public class DateColorConverter : IMultiValueConverter
{
    public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
    {
        if (values.Length >= 2 &&
            values[0] is DateTime dateTime &&
            values[1] is Func<DateTime, uint> func)
        {
            var argb = func(dateTime);

            return Color.FromArgb(
                (byte)(argb >> 24),
                (byte)(argb >> 16),
                (byte)(argb >> 8),
                (byte)argb);
        }
        return null;
    }

    public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

그리고 마지막으로 DateNavigatorCellButton(달력의 각 날짜들)에 대한 템플릿을 작성합니다. 원본 템플릿은 DevExpress의 소스코드에 있었습니다.

<dx:ThemedWindow
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:local="clr-namespace:DevExpressDateEditSample"
    xmlns:dx="http://schemas.devexpress.com/winfx/2008/xaml/core"
    xmlns:dxi="http://schemas.devexpress.com/winfx/2008/xaml/core/internal"
    xmlns:dxt="http://schemas.devexpress.com/winfx/2008/xaml/core/themekeys"
    xmlns:dxe="http://schemas.devexpress.com/winfx/2008/xaml/editors"
    xmlns:dxei="http://schemas.devexpress.com/winfx/2008/xaml/editors/internal"
    xmlns:dxet="http://schemas.devexpress.com/winfx/2008/xaml/editors/themekeys"
    x:Class="DevExpressDateEditSample.MainWindow"
    mc:Ignorable="d"
    Title="MainWindow" Height="550" Width="800"
    dx:ThemeManager.ThemeName="VS2019Dark">
    <Grid x:Name="root">
        <Grid.DataContext>
            <local:ViewModel/>
        </Grid.DataContext>

        <dxe:DateEdit HorizontalAlignment="Center" VerticalAlignment="Top" Width="150" Margin="60">
            <dxe:DateEdit.Resources>
                <Style TargetType="dxe:DateNavigator">
                    <Setter Property="Holidays" Value="{Binding Holidays}"/>
                    <Setter Property="DisabledDates" Value="{Binding DisabledDates}"/>
                    <Setter Property="SpecialDates" Value="{Binding SpecialDates}"/>
                </Style>
                <local:DateColorConverter x:Key="DateColorConverter"/>
                <Style TargetType="dxei:DateNavigatorCellButton">
                    <Setter Property="Template">
                        <Setter.Value>
                            <ControlTemplate TargetType="{x:Type dxei:DateNavigatorCellButton}">
                                <Grid Opacity="{Binding Path=Appearance.Opacity, RelativeSource={RelativeSource TemplatedParent}}">
                                    <Border x:Name="PART_Border"
                                        CornerRadius="{DynamicResource {dxet:DateNavigatorThemeKey ResourceKey=CellButtonCornerRadius}}" 
                                        BorderThickness="{Binding Path=Appearance.BorderThickness, RelativeSource={RelativeSource TemplatedParent}}" 
                                        BorderBrush="{Binding Path=Appearance.BorderBrush, RelativeSource={RelativeSource TemplatedParent}}" 
                                        Background="{Binding Path=Appearance.Background, RelativeSource={RelativeSource TemplatedParent}}">
                                    </Border>
                                    
                                    <!--기존 템플릿 내용 주석 처리-->
                                    <!--<ContentPresenter x:Name="PART_Content" 
                                        Margin="{TemplateBinding Padding}" 
                                        TextBlock.Foreground="{Binding Path=Appearance.Foreground, RelativeSource={RelativeSource TemplatedParent}}" 
                                        TextBlock.FontWeight="{Binding Path=Appearance.FontWeight, RelativeSource={RelativeSource TemplatedParent}}" 
                                        HorizontalAlignment="Stretch" 
                                        VerticalAlignment="Stretch"
                                        RecognizesAccessKey="True"/>-->
                
                                    <ContentPresenter x:Name="PART_Content" 
                                        Margin="{TemplateBinding Padding}" 
                                        TextBlock.FontWeight="{Binding Path=Appearance.FontWeight, RelativeSource={RelativeSource TemplatedParent}}" 
                                        HorizontalAlignment="Stretch" 
                                        VerticalAlignment="Stretch"
                                        RecognizesAccessKey="True">
                                        <TextBlock.Foreground>
                                            <SolidColorBrush>
                                                <SolidColorBrush.Color>
                                                    <MultiBinding Converter="{StaticResource DateColorConverter}">
                                                        <Binding Path="DataContext.DateTime" RelativeSource="{RelativeSource TemplatedParent}"/>
                                                        <Binding Path="DataContext.DateTimeToColor" ElementName="root"/>
                                                    </MultiBinding>
                                                </SolidColorBrush.Color>
                                            </SolidColorBrush>
                                        </TextBlock.Foreground>
                                    </ContentPresenter>
                                </Grid>
                            </ControlTemplate>
                        </Setter.Value>
                    </Setter>
                </Style>
            </dxe:DateEdit.Resources>
            <dxe:DateEdit.StyleSettings>
                <dxe:DateEditNavigatorStyleSettings />
            </dxe:DateEdit.StyleSettings>
        </dxe:DateEdit>
    </Grid>
</dx:ThemedWindow>

이렇게 하면 다음과 같이 실행됩니다.

뭔가 정신나간 달력처럼 보입니다만, 그래도 어떻게든 각 날짜의 색상을 변경했습니다.

멀티 바인딩에서 두 번째 바인딩의 ElementName에 지정한 "root"는 상위 부모 요소인 Grid의 이름입니다. GridDataContext에 지정한 뷰모델을 활용한 것입니다.

물론, 멀티 바인딩 말고 단일 값 컨버터로 구현해도 됩니다만, 뷰모델에서 색상을 결정하게 해보고 싶어서 이런 방법을 사용해봤습니다. 또한, 뷰모델에서는 WPF에 종속된 Color 구조체를 사용하지 않고 ARGB 색상 코드를 uint로 표현해봤습니다.

참고로 DateEdit.StyleSettings에 설정한 DateEditNavigatorStyleSettings는 굳이 설정 안 해도 기본적으로 DateEditNavigatorStyleSettings를 사용하게 되어 있습니다만, 혹시라도 DateEditCalendarStyleSettings로 설정했다면 제가 작성한 템플릿이 먹히지 않으므로 주의하셔야 할 것입니다.

조금은 도움이 되었으면 좋겠네요~ :grin:

10 Likes

정말 감사합니다ㅠㅠ 많은 도움이 됐어요. 이걸로 해보겠습니다!!!

2 Likes