ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • WPF 타이틀 바 바꾸기(Title Bar Custom)
    C#/WPF 2018. 4. 27. 11:30

    WPF로 개발을 하다 보면 상단의 ㅡㅁX가 있는 타이틀 바의 스타일을 바꿔야 할 때가 있습니다.

     

    저도 이와 관련해서 며칠 찾아보았지만, 주로 Mahapps.metro(링크)를 사용하고, 직접 다른분께서 만드신 내용은 많이 없더라구요.

     

    우선은 기본적으로 Window의 Title Bar는 색을 바꾼 수 없습니다. 그래서 바꾸는 법으로 WindowChrome이 있지만,

    .NET Framework 4 이상에서 지원되고, .NET Framework 4.5 이상부터 기본으로 포함되기에 혹 낮은 버전의 .NET을 써야 한다면 사용하실 수 없습니다. 그리고 WindowChrom을 써도 원하는대로 안나오거나 마음에 들지 않을 때가 많더라구요

    ㅡㅁX버튼이 안보인다던지 제가 WindowChrom에 대해서도 찾아 보았는데 좋은 방법을 찾지 못했습니다.

     

    그래서 직접 만들게 되었습니다...

     

    혹시 더 나은 방법이 있다면 알려주시면 감사하겠습니다.

     


     

    Window elements

     (Microsoft Docs)

     

    Window의 Title Bar는 시스템 영역이여서 바꿀 수가 없습니다. 그래서 우리가 바꿀 수 있는 Client Area를 활용하여 Title Bar를 만들어 주어야 합니다.

     

    먼저 Window의 속성을 바꿔주여야 합니다.

     

    WindowStyle="None" 

    ResizeMode="CanMinimize"

     
    WindowStyle을 None으로 하면, Title Bar가 없어지고, 
    ResizeMode를 CanMinimize로 하면, Resize Grip이 없어집니다.

     

    이렇게 해서 실행시키면 아무것도 없는 흰 사각형만 화면에 뜨게 됩니다. 

    그래서 작업 표시줄을 통한 종료 혹은 Alt + F4 등으로 종료하여야 합니다.

     

    윈도우 예시 코드 입니다.

     

    1
    2
    3
    4
    5
    6
    <Window
            WindowStyle="None" ResizeMode="CanMinimize" 
            Title="MainWindow" Height="350" Width="600" 
            StateChanged="Window_StateChanged" LocationChanged="Window_LocationChanged"
            >
    </Window>
    cs

     

    xmlns, class 등의 설정 아래 추가하시면 되는 내용입니다.

     

    Title Bar 역할을 만들기 위해 Event Handler가 추가되어 있습니다.

     

     

    이제 해야할 일은 Title Bar 역할을 하는 것을 만들어 주어야 합니다.

     

    1. 우선 영역을 나누고

    2. 윗부분에 ㅡㅁX 버튼과 Title을 추가 (링크)

    3. 마우스로 잡고 윈도우를 드래그 할 수 있도록 하는 기능

    4. 더블 클릭 시 최대화 최소화 (관련 링크)

    5. 우클릭 시 시스템 메뉴 부르기 (링크)

     

    을 넣어주어야 하는데, 2, 4, 5 번은 세부적으로 설명이 필요한데, 글이 너무 길어져서 따로 게시글을 만들었습니다.

     


     

    1. 먼저 영역을 나누어 줍니다.

     

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    <Border Name="main" BorderThickness="1" BorderBrush="LightGray" Margin="0">
        <DockPanel>
            <Border Name="border" DockPanel.Dock="Top" Height="25">
                <Grid>
                    <!-- Title Bar Area -->
                </Grid>
            </Border>
            <Grid DockPanel.Dock="Bottom">
                <!-- Content Area -->
            </Grid>
        </DockPanel>
    </Border>
    cs

     

    프로그램의 가장 자리 테두리 선을 위해 Border를 가장 큰 영역으로 선언합니다.

     

    그 뒤 DockPanel을 통해 Title Bar, Content 영역을 나누어 줍니다.

     

    Content Area를 감싸는 Grid의 경우 여러분께서 원하시는 Panel로 바꾸어 주셔도 됩니다.

     


     

     

    2. 윗 부분에 ㅡㅁX 버튼과 Title을 추가하는 코드입니다.

     

    Title Bar Area 부분에 넣어주시면 됩니다.

     

    더보기

     

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    <Label VerticalContentAlignment="Center" Margin="10,0,105,0"  PreviewMouseDown="System_MouseDown" PreviewMouseMove="System_MouseMove">
        <TextBlock Text="{Binding Title, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type Window}}}"/>
    </Label>
     
    <Grid HorizontalAlignment="Right" Background="White">
        <Grid.Resources>
            <Style TargetType="{x:Type Button}" x:Key="systemButton">
                <Setter Property="Padding" Value="0"/>
                <Setter Property="Width" Value="35"/>
                <Setter Property="Height" Value="25"/>
                <Setter Property="HorizontalAlignment" Value="Right"/>
                <Setter Property="VerticalAlignment" Value="Top"/>
                <Setter Property="Background" Value="Transparent"/>
                <Setter Property="Template">
                    <Setter.Value>
                        <ControlTemplate TargetType="{x:Type Button}">
                            <Border Background="{TemplateBinding Background}"  BorderThickness="0">
                                <ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
                            </Border>
                        </ControlTemplate>
                    </Setter.Value>
                </Setter>
                <Style.Triggers>
                    <Trigger Property="Button.IsMouseOver" Value="True">
                        <Setter Property="Button.Background" Value="LightGray" />
                    </Trigger>
                </Style.Triggers>
     
            </Style>
        </Grid.Resources>
        <Button Click="Close_Click">
            <Button.Style>
                <Style TargetType="Button" BasedOn="{StaticResource systemButton}">
                    <Style.Triggers>
                        <Trigger Property="Button.IsMouseOver" Value="True">
                            <Setter Property="Button.Background" Value="Red" />
                        </Trigger>
                    </Style.Triggers>
     
                </Style>
            </Button.Style>
            <Canvas Height="25" Width="35">
                <Line    
                    X1="12" Y1="8" 
                    X2="22" Y2="18"    
                    Stroke="Black" StrokeThickness="0.75"/>
     
                <Line    
                    X1="12" Y1="18"    
                    X2="22" Y2="8"  
                    Stroke="Black" StrokeThickness="0.75"/>
            </Canvas>
        </Button>
        <Button Margin="0,0,35,0" Click="Maximize_Click"  Style="{DynamicResource systemButton}">
            <Grid>
                <Rectangle Name="rectMax" Width="11" Height="11"
                        Stroke="Black"
                        StrokeThickness="0.75"/>
                <Canvas Name="rectMin"  Visibility="Hidden">
                    <Polyline Points="2.375,2 2.375,0.375 10.625,0.375 10.625,8.625 9,8.625"
                                StrokeThickness="0.75" Stroke="Black"/>                                    
                    <Rectangle Width="9" Height="9"
                        Stroke="Black"
                        StrokeThickness="0.75" Margin="0,2,2,0"/>
                    
                </Canvas>
            </Grid>
        </Button>
        <Button Margin="0,0,70,0" Click="Mimimize_Click"  Style="{DynamicResource systemButton}">
            <Rectangle Width="11"
                        Stroke="Black"
                        StrokeThickness="0.75"/>
        </Button>
    </Grid>
    cs

     

     

    Label 안에 TextBlock을 넣어서 Title을 표시하게 됩니다.

    그리고 Binding을 통해 Window의 Title 속성을 Text로 표시합니다.

     

     

    추가적으로 한가지 기능이 더 필요한 데, 윈도우가 최대화, 최소화 되었을 때 최대화 버튼의 모양을 바꾸는 것입니다.

     

    이는 Window의 StateChanged Event Handler를 통해 바꾸게 됩니다.

     

    더보기
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    private void Window_StateChanged(object sender, EventArgs e)
    {
        if (this.WindowState == WindowState.Maximized)
        {
            main.BorderThickness = new Thickness(0);
            rectMax.Visibility = Visibility.Hidden;
            rectMin.Visibility = Visibility.Visible;
        }
        else
        {
            main.BorderThickness = new Thickness(1);
            rectMax.Visibility = Visibility.Visible;
            rectMin.Visibility = Visibility.Hidden;
        }
    }
    cs

     

     

     

    ㅡㅁX 버튼에 대한 세부적인 내용은 게시글(링크)에서 설명드리겠습니다.

     

    해당 내용은 여러분께서 원하시는 디자인 대로 마음껏 바꾸시면 됩니다.

     


     

     

    3. 마우스로 잡고 윈도우를 드래그 할 수 있는 기능

     

    위 2번 기능의 1번째 줄을 보시면 Label의 PreviewMouseMove Event Handler를 등록해놓았습니다.

     

    이 Label을 사용자가 마우스로 눌렀을 때 System_MouseMove Handler에서 드래그 기능을 구현하게 됩니다.

     

    코드는 cs 파일에 넣어주시면 됩니다.

     

    더보기

     

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    private Point startPos;
     
    private void System_MouseMove(object sender, MouseEventArgs e)
    {
        if (e.LeftButton == MouseButtonState.Pressed)
        {
            if (this.WindowState == WindowState.Maximized && Math.Abs(startPos.Y - e.GetPosition(null).Y) > 2)
            {
                var point = PointToScreen(e.GetPosition(null));
     
                this.WindowState = WindowState.Normal;
     
                this.Left = point.X - this.ActualWidth / 2;
                this.Top = point.Y - border.ActualHeight / 2;
            }
            DragMove();
        }
    }
    cs

     

     

    startPos는 MouseDown에서 사용하게 되는 변수이지만, Drag의 부가 기능을 위해서는 이 변수가 필요합니다.

    startPos를 사용하는 내용은 다음 4번 코드에 같이 쓰겠습니다.

     

    기본적으로 Window는 DragMove()라는 함수를 제공합니다.

     

    하지만 최대화 되어 있을 때 DragMove는 최대화를 풀어주지 못하고 Drag 되지 않습니다.

    그래서 최대화 되었을 때 최대화를 풀어주고, 마우스 위치를 기준으로 Drag를 시작하도록 해주는 코드 입니다.

     

    처음 마우스를 눌렀을 때 바로 풀리는 것이 아닌, 조금 움직인 뒤 Drag를 시작하도록 합니다(startPos).

     


     

     

    4. 더블 클릭 시 최대화, 최소화

     

    이 기능은 간단한 기능처럼 보이지만, WindowStyle이 None인 경우에는 윈도우가 최대화 되었을 때 

    화면 아래 작업 표시줄까지 가려버리는 문제가 발생합니다. 그래서 이 문제를 막기 위해 추가적인 내용이 필요하게 됩니다.

     

    먼저 기본적인 최대화 최소화 코드 입니다.

     

    3과 마찬가지로 Label에서 받아들이며 PreviewMouseDown Event Handler에서 처리하도록 합니다.

     

    더보기
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    private void System_MouseDown(object sender, MouseButtonEventArgs e)
    {
        if (e.ChangedButton == MouseButton.Left)
        {
            if (e.ClickCount >= 2)
            {
                this.WindowState = (this.WindowState == WindowState.Normal) ? WindowState.Maximized : WindowState.Normal;
            }
            else
            {
                startPos = e.GetPosition(null);
            }
        }
    }
    cs

     

     

    그리고 작업 표시줄을 가리지 않도록 해주어야 하는데, 이를 Window의 MaxHeight를 통해 처리합니다.

     

    그리고 여러개의 모니터를 쓸 경우를 위해 Windows.Forms를 통해 Screen 클래스를 사용하게 됩니다.

     

    우선 어셈블리 참조가 필요합니다.

     

    ※참조 추가 -> 어셈블리 -> System.Drawing , System.Windows.Forms 

     

     

    Window에서 LocationChanged Event Handler를 통해 윈도우가 이동할 때마다 모니터를 확인해서 최대 크기를 조정합니다.

     

    더보기

     

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    System.Windows.Forms.Screen[] screens = System.Windows.Forms.Screen.AllScreens;
     
    private void Window_LocationChanged(object sender, EventArgs e)
    {
        int sum = 0;
        foreach (var item in screens)
        {
            sum += item.WorkingArea.Width;
            if (sum >= this.Left + this.Width / 2)
            {
                this.MaxHeight = item.WorkingArea.Height;
                break;
            }
        }
    }
    cs

     

     

    작업 표시줄을 가리지 않게 하는 세부적인 내용은 게시글(링크)에서 설명드리겠습니다.

     


     

    5. 우클릭 시 시스템 메뉴 부르기

     

    이 기능은 Title Bar의 Label을 우클릭 할 때 구현하게 되어서 MouseDown Event Handler에 추가하게 됩니다.

     

    우선 Win API를 호출하여야 하기 때문에 using을 해주는 것이 편합니다.

     

    using System.Runtime.InteropServices;

     

    코드 입니다.

     

    더보기
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    private void System_MouseDown(object sender, MouseButtonEventArgs e)
    {
        if (e.ChangedButton == MouseButton.Left)
        {
            if (e.ClickCount >= 2)
            {
                this.WindowState = (this.WindowState == WindowState.Normal) ? WindowState.Maximized : WindowState.Normal;
            }
            else
            {
                startPos = e.GetPosition(null);
            }
        }
        else if (e.ChangedButton == MouseButton.Right)
        {
            var pos = PointToScreen(e.GetPosition(this));
            IntPtr hWnd = new System.Windows.Interop.WindowInteropHelper(this).Handle;
            IntPtr hMenu = GetSystemMenu(hWnd, false);
            int cmd = TrackPopupMenu(hMenu, 0x100, (int)pos.X, (int)pos.Y, 0, hWnd, IntPtr.Zero);
            if (cmd > 0) SendMessage(hWnd, 0x112, (IntPtr)cmd, IntPtr.Zero);
        }
    }
     
    [DllImport("user32.dll")]
    static extern IntPtr SendMessage(IntPtr hWnd, int msg, IntPtr wp, IntPtr lp);
    [DllImport("user32.dll")]
    static extern IntPtr GetSystemMenu(IntPtr hWnd, bool bRevert);
    [DllImport("user32.dll")]
    static extern int TrackPopupMenu(IntPtr hMenu, uint uFlags, int x, int y, int nReserved, IntPtr hWnd, IntPtr prcRect);
    cs

     

     

    4번의 MouseDown에서 else if 문을 추가해주고, Dll Import를 추가해주면 됩니다.

     

     

    세부적인 내용은 게시글(링크)에서 설명드리겠습니다.

     


     

     

    위 모든 기능을 합치면 일반적인 윈도우의 Title Bar와 같은 기능을 하게 됩니다.

     

    모든 기능을 구현한 Window 예시를 GitHub(링크)에 올려놓았으니 GitHub에서 확인하셔도 됩니다.

     

     

     

    코드 최적화, 추가로 필요한 기능 등을 개선사항을 찾으시면 댓글로 알려주세요^^

    댓글

GiGong