[C#프로그래밍] [WPF] PC 카카오톡 만들기 #03 - 타이틀 바 만들기

728x90

 

https://www.youtube.com/watch?v=Q6VxSPlkcCg&list=PLlrfTSXS0LLKHfOfwM31jJw3SHuDnkF49&index=4

 

📌 세팅 리셋

  • 앞전에 생성한 Service 및 MainWindow 삭제

 

  • 폴더 생성 후 wpf 창 생성

 

  • App.xaml 수정
    public partial class App : Application
    {

        public App()
        {
            Services = ConfigureServices();
            Startup += App_Startup;

        }

        private void App_Startup(object sender, StartupEventArgs e)
        {
            var mainView = App.Current.Services.GetService<MainView>();
            mainView.Show();

        }



        public new static App Current => (App)Application.Current;

        public IServiceProvider Services { get; }

        private static IServiceProvider ConfigureServices()
        {
            var services = new ServiceCollection();

            //Views
            /* 의존성이 추가 된 MainWindow를 만들어준다. */
            services.AddSingleton<MainView>();

            return services.BuildServiceProvider();
        }

    }

 

📌 타이틀과 바디 부분 나누기

  • MainView.xaml 에 아래와 같이 코드 추가
<Window x:Class="seungjjangTalk.Views.MainView"
        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:seungjjangTalk.Views"
        mc:Ignorable="d"
        WindowStyle="None"
        Title="MainView" Height="550" Width="350">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="auto"/>
            <RowDefinition/>
        </Grid.RowDefinitions>
        
        <!-- 타이틀 바 -->
        <StackPanel Background="Red" Height="20"/>

        <!-- 바디 -->
        <StackPanel Grid.Row="1" Background="Blue"/>
    </Grid>
  • WindowStyle="None" 으로 스타일 삭제해주기

 

◾ Controls 파일 생성

  • TitleBar.xaml
<UserControl x:Class="seungjjangTalk.Controls.TitleBar"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:local="clr-namespace:seungjjangTalk.Controls"
             mc:Ignorable="d" 
             d:DesignHeight="20" d:DesignWidth="200">
    <DockPanel Background="#FFFEE500"/>
</UserControl>
  • DesignHeight, DesignWidth 추가 및 색상 지정
  • #FFFEE500 색상을 다른 곳에서도 사용 할 예정이니 스타일 추가

 

📌 Dictionary에 스타일 추가하기

- Styles 폴더 추가

- 리소스사전 선택 후 Colors 파일 생성

 

- Colors.xaml

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    <SolidColorBrush x:Key="ColorPrimary" Color="#FFFEE500"/>
</ResourceDictionary>

 

- App.xaml

<Application x:Class="seungjjangTalk.App"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:local="clr-namespace:seungjjangTalk"
             StartupUri="MainWindow.xaml">
    <Application.Resources>
        <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>
                <ResourceDictionary Source="/Styles/Colors.xaml"/>
            </ResourceDictionary.MergedDictionaries>
        </ResourceDictionary>
    </Application.Resources>
</Application>

 

- TitleBar.xaml

<UserControl x:Class="seungjjangTalk.Controls.TitleBar"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:local="clr-namespace:seungjjangTalk.Controls"
             mc:Ignorable="d" 
             d:DesignHeight="20" d:DesignWidth="200">

    <DockPanel Background="{StaticResource ColorPrimary }"/>

</UserControl>

 

📌  상단 버튼 만들기

- TitleBar.xaml

    <UserControl.Resources>
        <Style TargetType="{x:Type Button}">
            <Setter Property="Width" Value="25"/>
            <Setter Property="Height" Value="20"/>
            <Setter Property="DockPanel.Dock" Value="Right"/>
            <Setter Property="Background" Value="Transparent"/>
            <Setter Property="BorderBrush" Value="Transparent"/>
        </Style>
    </UserControl.Resources>
    
    <DockPanel Background="{StaticResource ColorPrimary }" LastChildFill="False">
        <Button/>
        <Button/>
        <Button/>
        
    </DockPanel>

 

  아이콘 추가하기

button 안에서 SvgAwesome 입력 후 컨트롤 + . 을 누르면 앞전에 설치 한 SvgAwesome 을 사용 할 수 있다.

 

- TitleBar.xaml

    <DockPanel Background="{StaticResource ColorPrimary }" LastChildFill="False">
        <Button>
            <fa6:SvgAwesome Icon="Solid_Xmark"/>
        </Button>
        
        <Button>
            <fa6:SvgAwesome Icon="Regular_Square"/>
        </Button>
        
        <Button>
            <fa6:SvgAwesome Icon="Solid_Minus"/>
        </Button>

    </DockPanel>

 

  버튼 색 Dictionary에 스타일 추가하기

- Colors.xaml

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    <SolidColorBrush x:Key="ColorPrimary" Color="#FFFEE500"/>
    <SolidColorBrush x:Key="ColorGray" Color="#7F7200"/>
</ResourceDictionary>

 

- TitleBar.xaml

     <UserControl.Resources>
 			<Style TargetType="{x:Type fa6:SvgAwesome}">
            <Setter Property="PrimaryColor" Value="{StaticResource ColorGray}"/>
        </Style>
    </UserControl.Resources>

  DockPanel 및 버튼 이름 설정하기

- TitleBar.xaml

    <DockPanel x:Name="pnlTitle" Background="{StaticResource ColorPrimary }" LastChildFill="False">
			<Button x:Name="btnExit">
            <fa6:SvgAwesome Icon="Solid_Xmark"/>
        </Button>

        <Button x:Name="btnMaximize">
            <fa6:SvgAwesome Icon="Regular_Square"/>
        </Button>

        <Button x:Name="btnMinimize">
            <fa6:SvgAwesome Icon="Solid_Minus"/>
        </Button>
    </DockPanel>

 

  비하인드 코드 작성 

- TitleBar.xaml.cs

    public partial class TitleBar : UserControl
    {

        public TitleBar()
        {
            InitializeComponent();
            btnExit.Click += btnExit_Click;
            btnMaximize.Click += btnMaximize_Click;
            btnMinimize.Click += btnMinimize_Click;
        }

        private void btnMinimize_Click(object sender, RoutedEventArgs e)
        {
            btnMinimize.FindParent<Window>()!.WindowState = WindowState.Minimized;
        }

        private void btnMaximize_Click(object sender, RoutedEventArgs e)
        {
            btnMaximize.FindParent<Window>()!.WindowState = WindowState.Maximized;

        }

        private void btnExit_Click(object sender, RoutedEventArgs e)
        {
            btnExit.FindParent<Window>()!.Close();
        }
    }
}

 

- 라이브러리 추가 

 

라이브러리 생성 후 빌드 한 번 해 준 후 Extensions 폴더 추가 한 후 FindParentExtension 클래스 생성 

 

- FindParentExtension.cs

namespace wpfLib.Extensions
{
    public static class FindParentExtension
    {

        //타입만 넘겨서 parent를 구하는 경우를 위해 오버로드
        public static T? FindParent<T>(this DependencyObject child)
            where T : DependencyObject
        {
            return FindParent<T>(child, null);
        }


        //재귀함수를 만든다.  T?는 Null을 타입을 나타냄. 곧 Null을 허용한다는 것
        public static T? FindParent<T>(this DependencyObject child, string? parentName)
            //형식 매개변수는 DependencyObject만 들어올 수 있도록 해 준다. 
            //T 가 DependencyObject의 하위 클래스임을 보장한다.
            where T : DependencyObject
        {
            //VisualTreeHelper를 이용해서 parent의 child를 찾는다.
            var parent = VisualTreeHelper.GetParent(child);
            
            //parent 가 null 인경우 return null
            if(parent == null) { return null; }

            //parent를 FrameworkElement로 타입변경 
            var frameworkElement = (FrameworkElement)parent;

            //만약 parentName == null인 경우 타입만 체크 
            //frameworkElement Name과 parentName이 같고  FrameworkElement 타입이 맞다면 parent를 리턴하고 그렇지 않으면 재귀함수를 태운다.
            if ((parentName == null) || frameworkElement.Name == parentName && frameworkElement is T) return (T)parent; else return FindParent<T>(parent, parentName);
          
        }
    }
}

 

- 참조 추가

 

 

📌 타이틀 바 추가하기

- MainView.xaml

<Window x:Class="seungjjangTalk.Views.MainView"
        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:seungjjangTalk.Views" 
        xmlns:controls="clr-namespace:seungjjangTalk.Controls"
        mc:Ignorable="d"
        Title="MainView" Height="550" Width="350">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="auto"/>
            <RowDefinition/>
        </Grid.RowDefinitions>

        <!-- 타이틀 바 -->
        <controls:TitleBar/>

        <!-- 바디 -->
        <StackPanel Grid.Row="1" Background="Blue"/>
    </Grid>
</Window>

 

이후 실행 해 보면 창이 늘어나고, 최소화 되고 닫히는 것을 확인 할 수 있다. 

 

- TitleBar.xaml.cs

        private void btnMaximize_Click(object sender, RoutedEventArgs e)
        {
            var window = btnMinimize.FindParent<Window>();
            window.WindowState = window.WindowState == WindowState.Maximized ? WindowState.Normal : WindowState.Maximized;

- 창이 Maximized 인 경우 nomal로 바꾸고 아닌 경우 Maximized처리 하도록 수정

 

  WindowChrome 설정

- 타이틀 바를 잡고 드래그하여 창을 옮길 수 있도록 설정

- MainView.xaml

...
    <WindowChrome.WindowChrome>
        <WindowChrome CaptionHeight="20" ResizeBorderThickness="2"/>
    </WindowChrome.WindowChrome>
    
    <Grid>
...

 

위 설정을 해 주고 나면 버튼이 잘 눌러지지 않는데

- TitleBar.xaml 

    <UserControl.Resources>
        <Style TargetType="{x:Type Button}">
            <Setter Property="WindowChrome.IsHitTestVisibleInChrome" Value="True"/>
        </Style>
        ...

위 코드를 추가 해 주면 된다.

 

  효율성을 위한 재귀함수 수정 

- 기존 TitleBar.xaml.cs에서는 버튼을 클릭 할 때마다 재귀함수가 돌아가게되는데, 이는 효율성이 떨어진다.
  아래와 같이 ParentWindow를 추가하여 window가 있으면 재귀함수를 타지 않도록 수정해준다.

- TitleBar.xaml.cs

    public partial class TitleBar : UserControl
    {

        private Window? _parentWindow;

        public Window ParentWindow
        {
            get {
                //Window가 있으면 재귀함수를 타지 않는다. 
                if (_parentWindow == null) _parentWindow = this.FindParent<Window>()!;
                return _parentWindow; }
            set { _parentWindow = value; }
        }

        public TitleBar()
        {
            InitializeComponent();
            btnExit.Click += btnExit_Click;
            btnMaximize.Click += btnMaximize_Click;
            btnMinimize.Click += btnMinimize_Click;
        }

        private void btnMinimize_Click(object sender, RoutedEventArgs e)
        {
            ParentWindow.WindowState = WindowState.Minimized;
        }

        private void btnMaximize_Click(object sender, RoutedEventArgs e)
        {
            
            ParentWindow.WindowState = ParentWindow.WindowState == WindowState.Maximized 
                ? WindowState.Normal 
                : WindowState.Maximized;

        }

        private void btnExit_Click(object sender, RoutedEventArgs e)
        {
            ParentWindow.Close();
        }
    }

 

📌 WindowState 구독하여 높낮이 조정하기

- Titlebar.xaml.cs

    [ObservableObject]
    public partial class TitleBar : UserControl
    {

        private Window? _parentWindow;
        private WindowState _winstate;

        public WindowState WindowState
        {
            get { return _winstate; }
            set { SetProperty(ref _winstate, value); }
        }
        
        ...
        
        private void btnMinimize_Click(object sender, RoutedEventArgs e)
        {
            //WindowState 는 변환이 이루어질 때 구독 
            WindowState = WindowState.Minimized;
            ParentWindow.WindowState = WindowState;
        }

        private void btnMaximize_Click(object sender, RoutedEventArgs e)
        {
            WindowState = ParentWindow.WindowState = ParentWindow.WindowState == WindowState.Maximized 
                ? WindowState.Normal 
                : WindowState.Maximized;

        }
        
        ...

  [ObservableObject]을 통해 MVVM프레임워크 사용
- 여기서 SetProperty(ref _winstate, value); 를 하게 value가 바뀌게되면 PropertyChanged 이벤트가 호출하게 된다.

 

📌 최대화 크기 조정

- 현재 최대화하는 경우 화면을 튀어나가게 되어 Margin을 바인딩처리 하여 해결한다.

 

- TitleBar.xaml

<UserControl x:Class="seungjjangTalk.Controls.TitleBar"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:local="clr-namespace:seungjjangTalk.Controls" 
             xmlns:fa6="http://schemas.fontawesome.com/icons/svg"
             mc:Ignorable="d" 
             d:DesignHeight="20" d:DesignWidth="200"
             Name ="root">
             
             
             ...
             
             
        <Button x:Name="btnMaximize" Margin="{Binding WinState, ElementName=root, Converter={StaticResource WindowStateMarginCnverter}}">
            <fa6:SvgAwesome Icon="Regular_Square"/>
        </Button>
        
        
        ...

 

 

- Converter 파일 추가 

 

- WindowStateMarginCnverter.cs

    class WindowStateMarginCnverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
           WindowState state = (WindowState)value;
            //크기가 Normal인 경우 크기 조정 하지 않고 그렇지 않은 경우 마진 설정 
            if (state == WindowState.Normal) 
            { 
                return new Thickness(0); 
            }
            else 
            {

                var param = (string)parameter;
                var right = param == "Exit" ? 7 : 0;
            
                return new Thickness(0, 7, right, 0);
            }
        }

 

- TitleBar.xaml

    <UserControl.Resources>
        <converters:WindowStateMarginCnverter x:Key="WindowStateMarginCnverter"/>
        
        ...
    </UserControl.Resources>

	<DockPanel x:Name="pnlTitle" Background="{StaticResource ColorPrimary }" LastChildFill="False">

        <Button x:Name="btnExit" Margin="{Binding Winstate, ElementName=root, Converter={StaticResource WindowStateMarginCnverter}, ConverterParameter=Exit}">
            <fa6:SvgAwesome Icon="Solid_Xmark"/>
        </Button>

        <Button x:Name="btnMaximize" Margin="{Binding Winstate, ElementName=root, Converter={StaticResource WindowStateMarginCnverter}}">
            <fa6:SvgAwesome Icon="Regular_Square"/>
        </Button>

        <Button x:Name="btnMinimize" Margin="{Binding Winstate, ElementName=root, Converter={StaticResource WindowStateMarginCnverter}}">
            <fa6:SvgAwesome Icon="Solid_Minus"/>
        </Button>

    </DockPanel>

 

📌 최대화 했을 시 아이콘 변경

- TitleBar.xaml

    <UserControl.Resources>
        <converters:WindowStateMarginCnverter x:Key="WindowStateMarginCnverter"/>
        <converters:WindowStateIconConverter x:Key="WindowStateIconConverter"/>


		...


        <Button x:Name="btnMaximize" Margin="{Binding Winstate, ElementName=root, Converter={StaticResource WindowStateMarginCnverter}}">
            <fa6:SvgAwesome Icon="{Binding Winstate, ElementName=root, Converter={StaticResource WindowStateIconConverter}}"/>
        </Button>

 

- WindowStateIconConverter.cs 

namespace seungjjangTalk.Converters
{
    class WindowStateIconConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            WindowState state = (WindowState)value;
            if (state == WindowState.Normal) { return EFontAwesomeIcon.Regular_Square; } else return EFontAwesomeIcon.Solid_DownLeftAndUpRightToCenter;
        }

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

 

확대 전 / 후 아이콘 비교 

 

728x90