💻 NuGet 패키지 추가
✔ CommunityToolkit,Mvvm
✔ FontAwesome6.svg
✔ Microsoft.Extensions.DependencyInjection
✔ Microsoft.Xaml.Behaviors.Wpf
✔ MySql.Data
💻 의존성 주입
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Collections.Generic;
using System.Configuration;
using System.Data;
using System.Linq;
using System.Threading.Tasks;
using System.Windows;
namespace SunjeongTalk
{
/// <summary>
/// App.xaml에 대한 상호 작용 논리
/// </summary>
public partial class App : Application
{
public App()
{
Services = ConfigureServices();
}
public new static App Current => (App)Application.Current;
public IServiceProvider Services { get; }
private static IServiceProvider ConfigureServices()
{
var services = new ServiceCollection();
return services.BuildServiceProvider();
}
}
}
https://learn.microsoft.com/ko-kr/dotnet/communitytoolkit/mvvm/ioc
[Ioc - .NET Community Toolkit
Microsoft.Extensions.DependencyInjection API를 통해 IServiceProvider 형식을 사용하는 방법을 소개합니다.
learn.microsoft.com](https://learn.microsoft.com/ko-kr/dotnet/communitytoolkit/mvvm/ioc)
-> 여기서 코드 복사
💻 Services 생성
✔ TestService.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SunjeongTalk.Services
{
public class TestService : ITestService
{
public string GetString()
{
return "선정톡입니다.";
}
}
}
✔ ITestService.cs (인터페이스)
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SunjeongTalk.Services
{
public interface ITestService
{
string GetString();
}
}
✔ MainWindow.xaml.cs 에서 확인
namespace SunjeongTalk
{
/// <summary>
/// MainWindow.xaml에 대한 상호 작용 논리
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
ITestService service = new TestService();
MessageBox.Show(service.GetString());
}
}
}
👉 위의 경우를 의존성 주입
이라고 한다. 이렇게 매번 service를 주입하고 불러와야 하기 때문에 이 경우 결합도가 높다고 표현한다.
✔ 싱글톤으롤 서비스 등록
싱글톤으로 생성하게 되면 끝날때까지 계속 사용
services.AddSingleton<ITestService, TestService>();
using Microsoft.Extensions.DependencyInjection;
using SunjeongTalk.Services;
using System;
using System.Collections.Generic;
using System.Configuration;
using System.Data;
using System.Linq;
using System.Threading.Tasks;
using System.Windows;
namespace SunjeongTalk
{
/// <summary>
/// App.xaml에 대한 상호 작용 논리
/// </summary>
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<MainWindow>();
mainView.Show();
}
public new static App Current => (App)Application.Current;
public IServiceProvider Services { get; }
private static IServiceProvider ConfigureServices()
{
var services = new ServiceCollection();
// Services
services.AddSingleton<ITestService, TestService>();
// Views
services.AddSingleton<MainWindow>();
return services.BuildServiceProvider();
}
}
}
💻 MainView.xaml / cs
<Window x:Class="TalkTalk.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:TalkTalk.Views" xmlns:controls="clr-namespace:TalkTalk.Controls"
mc:Ignorable="d"
WindowStyle="None"
Title="MainView" Height="550" Width="350">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="auto"/>
<RowDefinition/>
</Grid.RowDefinitions>
<!--TitleBar-->
<controls:TitleBar/>
<!--Body-->
<StackPanel Grid.Row="1" Background="Blue"/>
</Grid>
</Window>
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;
namespace SunjeongTalk.Views
{
/// <summary>
/// MainView.xaml에 대한 상호 작용 논리
/// </summary>
public partial class MainView : Window
{
public MainView()
{
InitializeComponent();
}
}
}
💻 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="#FFEEE500"/>
<SolidColorBrush x:Key="ColorGray" Color="#7F7200"/>
</ResourceDictionary>
💻 TitleBar.xaml / cs
<UserControl x:Class="TalkTalk.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:TalkTalk.Controls" xmlns:fa6="http://schemas.fontawesome.com/icons/svg"
mc:Ignorable="d"
d:DesignHeight="20" d:DesignWidth="200">
<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>
<Style TargetType="{x:Type fa6:SvgAwesome}">
<Setter Property="PrimaryColor" Value="{StaticResource ColorGray}"/>
</Style>
</UserControl.Resources>
<DockPanel Background="{StaticResource ColorPrimary}"
LastChildFill="False"
x:Name="pnlTitle">
<Button x:Name="btnExit">
<fa6:SvgAwesome Icon="Solid_Xmark"/>
</Button>
<Button x:Name="btnMaximize">
<fa6:SvgAwesome Icon="Regular_Square" Width="13" Height="13"/>
</Button>
<Button x:Name="btnMinimize">
<fa6:SvgAwesome Icon="Solid_Minus"/>
</Button>
</DockPanel>
</UserControl>
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using WpfLib.Extensions;
namespace TalkTalk.Controls
{
/// <summary>
/// TitleBar.xaml에 대한 상호 작용 논리
/// </summary>
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)
{
var window = btnMinimize.FindParent<Window>()!;
window.WindowState = window.WindowState == WindowState.Maximized ? WindowState.Normal : WindowState.Maximized;
}
private void BtnExit_Click(object sender, RoutedEventArgs e)
{
btnMaximize.FindParent<Window>()!.Close();
}
}
}
💻 WpfLib (wpf 애플리케이션으로 라이브러리 만들기)
💻 FindParentExtensions.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Media;
using System.Windows;
namespace WpfLib.Extensions
{
public static class FindParentExtensions
{
public static T? FindParent<T>(this DependencyObject child)
where T : DependencyObject
{
return FindParent<T>(child, null);
}
public static T? FindParent<T>(this DependencyObject child, string? parentName)
where T : DependencyObject
{
var parent = VisualTreeHelper.GetParent(child);
if (parent == null)
{
return null;
}
var frameworkElement = (FrameworkElement)parent;
if ((parentName == null || frameworkElement.Name == parentName) && frameworkElement is T)
{
return (T)parent;
}
else
{
return FindParent<T>(parent, parentName);
}
}
}
}
--> 이것을 TalkTalk에 프로젝트 참조 추가 해준다.
✔ 프로젝트 실행 시 타이틀 바가 클릭되지 않는 문제는 WindowChrome으로 해결해주면 된다.
우선 MainView에 WindowChrome을 추가해준다.
<WindowChrome.WindowChrome>
<WindowChrome CaptionHeight="20"
ResizeBorderThickness="2"/>
</WindowChrome.WindowChrome>
그러나 이 상태에서 실행하면 버튼이 클릭되지 않는다.
TitleBar에 들어가서 속성을 true로 바꿔주면 된다.
<Setter Property="WindowChrome.IsHitTestVisibleInChrome" Value="True"/>
✔ 창을 새로 켜주는 것은 버튼을 누를 때 마다 재귀함수가 실행되는 것이라 비효율 적이다.
그러므로 변수를 선언해주고 사용해주는 것이 좋다
private Window _parentWindow;
public Window ParentWindow
{
get { return _parentWindow; }
set { _parentWindow = value; }
}
✔ [ObservableObject] 추가
{
[ObservableObject]
public partial class TitleBar : UserControl
{
private Window? _parentWindow;
private WindowState _winState;
public WindowState WinState
{
get { return _winState; }
set
{
SetProperty(ref _winState, value);
}
}
이 경우 value가 바뀌게 되면 PropertyChanged가 호출된다.
✔ 실행 시 마진 안 맞는 부분은 컨버터를 사용해주어야 한다.
<converters:WindowStateMarginConverter x:Key="WindowStateMarginConverter"/>
<converters:WindowStateIconConverter x:Key="WindowStateIconConverter"/>
💻 WindowStateMarginConverter : 최대화 시 마진 변경 컨버터
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Data;
namespace SunjeongTalk.Converters
{
public class WindowStateMarginConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
WindowState state = (WindowState) value;
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);
}
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}
💻 WindowStateIconConverter : 최대화 시 아이콘 변경
using FontAwesome6;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Data;
namespace SunjeongTalk.Converters
{
public 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();
}
}
}
✔ 각 버튼에 컨버터 추가
<Button x:Name="btnExit"
Margin="{Binding WinState, ElementName=root, Converter={StaticResource WindowStateMarginConverter}, ConverterParameter=Exit}">
<fa6:SvgAwesome Icon="Solid_Xmark"/>
</Button>
<Button x:Name="btnMaximize"
Margin="{Binding WinState, ElementName=root, Converter={StaticResource WindowStateMarginConverter}}">
<fa6:SvgAwesome Icon="{Binding WinState, ElementName=root, Converter={StaticResource WindowStateIconConverter}}" Width="13" Height="13"/>
</Button>
<Button x:Name="btnMinimize"
Margin="{Binding WinState, ElementName=root, Converter={StaticResource WindowStateMarginConverter}}">
<fa6:SvgAwesome Icon="Solid_Minus"/>
</Button>
마진 컨버터에는 컨버터파라미터가 필요하다.
💻 최종 코드
💻 TitleBar.xaml
<UserControl x:Class="SunjeongTalk.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:SunjeongTalk.Controls" xmlns:fa6="http://schemas.fontawesome.com/icons/svg" xmlns:converters="clr-namespace:SunjeongTalk.Converters"
mc:Ignorable="d"
d:DesignHeight="20" d:DesignWidth="200" Name="root">
<UserControl.Resources>
<converters:WindowStateMarginConverter x:Key="WindowStateMarginConverter"/>
<converters:WindowStateIconConverter x:Key="WindowStateIconConverter"/>
<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"/>
<Setter Property="WindowChrome.IsHitTestVisibleInChrome" Value="True"/>
</Style>
<Style TargetType="{x:Type fa6:SvgAwesome}">
<Setter Property="PrimaryColor" Value="{StaticResource ColorGray}"/>
</Style>
</UserControl.Resources>
<DockPanel Background="{StaticResource ColorPrimary}"
LastChildFill="False"
x:Name="pnlTitle">
<Button x:Name="btnExit"
Margin="{Binding WinState, ElementName=root, Converter={StaticResource WindowStateMarginConverter}, ConverterParameter=Exit}">
<fa6:SvgAwesome Icon="Solid_Xmark"/>
</Button>
<Button x:Name="btnMaximize"
Margin="{Binding WinState, ElementName=root, Converter={StaticResource WindowStateMarginConverter}}">
<fa6:SvgAwesome Icon="{Binding WinState, ElementName=root, Converter={StaticResource WindowStateIconConverter}}" Width="13" Height="13"/>
</Button>
<Button x:Name="btnMinimize"
Margin="{Binding WinState, ElementName=root, Converter={StaticResource WindowStateMarginConverter}}">
<fa6:SvgAwesome Icon="Solid_Minus"/>
</Button>
</DockPanel>
</UserControl>
💻 TitleBar.xaml.cs
using CommunityToolkit.Mvvm.ComponentModel;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using WpfLib;
namespace SunjeongTalk.Controls
{
[ObservableObject]
public partial class TitleBar : UserControl
{
private Window? _parentWindow;
private WindowState _winState;
public WindowState WinState
{
get { return _winState; }
set
{
SetProperty(ref _winState, value);
}
}
public Window ParentWindow
{
get
{
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)
{
WinState = WindowState.Minimized;
ParentWindow.WindowState = WinState;
}
private void BtnMaximize_Click(object sender, RoutedEventArgs e)
{
WinState = ParentWindow.WindowState == WindowState.Maximized
? WindowState.Normal
: WindowState.Maximized;
ParentWindow.WindowState = WinState;
}
private void BtnExit_Click(object sender, RoutedEventArgs e)
{
ParentWindow.Close();
}
}
}
💻 WindowStateIconConverter
using FontAwesome6;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Data;
namespace SunjeongTalk.Converters
{
public 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();
}
}
}
💻 WindowStateMarginConverter
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Data;
namespace SunjeongTalk.Converters
{
public class WindowStateMarginConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
WindowState state = (WindowState) value;
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);
}
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}
💻 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="#FFEEE500"/>
<SolidColorBrush x:Key="ColorGray" Color="#7F7200"/>
</ResourceDictionary>
💻 MainView.xaml / cs
<Window x:Class="SunjeongTalk.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:SunjeongTalk.Views" xmlns:controls="clr-namespace:SunjeongTalk.Controls"
mc:Ignorable="d"
WindowStyle="None"
Title="MainView" Height="550" Width="350">
<WindowChrome.WindowChrome>
<WindowChrome CaptionHeight="20"
ResizeBorderThickness="2"/>
</WindowChrome.WindowChrome>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="auto"/>
<RowDefinition/>
</Grid.RowDefinitions>
<!--TitleBar-->
<controls:TitleBar/>
<!--Body-->
<StackPanel Grid.Row="1" Background="Blue"/>
</Grid>
</Window>
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;
namespace SunjeongTalk.Views
{
/// <summary>
/// MainView.xaml에 대한 상호 작용 논리
/// </summary>
public partial class MainView : Window
{
public MainView()
{
InitializeComponent();
}
}
}
💻 App.xaml / cs
<Application x:Class="SunjeongTalk.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:SunjeongTalk">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="/Styles/Colors.xaml"/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
</Application>
using Microsoft.Extensions.DependencyInjection;
using SunjeongTalk.Views;
using System;
using System.Collections.Generic;
using System.Configuration;
using System.Data;
using System.Linq;
using System.Threading.Tasks;
using System.Windows;
namespace SunjeongTalk
{
/// <summary>
/// Interaction logic for App.xaml
/// </summary>
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();
//services.AddSingleton<MainView, MainView>();
//Views
services.AddSingleton<MainView>();
return services.BuildServiceProvider();
}
}
}
💻 FindParentExtensions
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Media;
using System.Windows;
namespace WpfLib
{
public static class FindParentExtensions
{
public static T? FindParent<T>(this DependencyObject child)
where T : DependencyObject
{
return FindParent<T>(child, null);
}
public static T? FindParent<T>(this DependencyObject child, string? parentName)
where T : DependencyObject
{
var parent = VisualTreeHelper.GetParent(child);
if (parent == null)
{
return null;
}
var frameworkElement = (FrameworkElement)parent;
if ((parentName == null || frameworkElement.Name == parentName) && frameworkElement is T)
{
return (T)parent;
}
else
{
return FindParent<T>(parent, parentName);
}
}
}
}
✔ 오류
- 프레임워크 버전이 안 맞아서 WpfLib 참조 불가
- 프로젝트 생성 시 WPF 앱(.NET Framework)가 아니라 WPF 애플리케이션으로 만들어 주어야 한다. (이거 때문에 헤맸다.............)
📖 참고사이트 및 영상
'C# > WPF' 카테고리의 다른 글
[WPF] 카카오톡 구현 3 - 로그인 화면 만들기(Login Control) (0) | 2023.08.20 |
---|---|
[WPF] 카카오톡 구현 2 - DataTemplate로 UserControl 변경하기 (0) | 2023.08.19 |
[WPF] 데이터 바인딩 (0) | 2023.08.07 |
[WPF] 의존 프로퍼티(Dependency Property) / 의존 속성 (0) | 2023.08.06 |
[WPF] ListBox와 LINQ쿼리 (0) | 2023.08.05 |