728x90

 

WPF를 사용하면 전체적인 틀을 잡기 위해 제일 많이 쓰는 태그가 Grid, StackPanel 아닐까요..?

CSS로 따지면 Container.. div.. 같은 느낌..

 

평소 느낌으로,, 아아,, Grid겠지,, 아아,, StackPanel일거야,,하며 사용하고는 했는데, 이번에 ScrollViewer를 사용하면서깨달은 점을 기록할 예정입니다.

 

각각의 특징을 간단히 정리해보고, 특징에 따른 주의사항에 대해 간단히 정리해봅니다!

 

1. Grid

* 행(Row)과 열(Column)로 UI를 구성(Table과 유사)

* RowDefinition과 ColumnDefinition을 활용하여 정확한 크기 조정 가능

* Grid.RowSpan, Grid.ColumnSpan을 사용해 특정 요소를 여러 행 또는 열에 걸쳐 배치 가능

* 정렬이 필요한 UI

<Grid>
    <Grid.RowDefinitions>
        <RowDefinition Height="Auto"/>
        <RowDefinition Height="*"/>
    </Grid.RowDefinitions>

    <TextBlock Grid.Row="0" Text="Title" FontSize="20"/>
</Grid>

 

 

2. StackPanel

* 수직 또는 수평으로 자식 요소들을 한 줄로 배치(Flex와 유사)

* 내부 요소의 크기를 모두 합산하여 자신의 크기를 결정

* 레이아웃이 단순해서 버튼 목록, 메뉴 등 간단한 UI에 적합

<StackPanel Orientation="Vertical">
    <Button Content="Button 1"/>
    <Button Content="Button 2"/>
    <Button Content="Button 3"/>
</StackPanel>

 

 

💡3. StackPanel이 Grid보다 사용하기 좋은 경우

No. 상황 이유
1 단순한 리스트형 UI 한 방향으로 요소를 추가하는 UI에서 Grid보다 더 간결하고 코드가 짧음
2 자식 요소 개수가 동적으로 변할 때 내부 요소가 많아져도 자동으로 크기를 조정하므로 레이아웃 관리가 쉬움
3 자동 크기 확장이 필요한 경우 요소 개수에 따라 부드럽게 늘어나야 하는 레이아웃에 적합

 

 

🚨 4. StackPanel 사용 시, 유의 사항

StackPanel은 자식 요소들의 크기를 모두 합쳐서 자신의 크기를 결정하는 방식이므로,  StackPanel 내부에 ScrollViewer가 있을 경우 자식의 전체 크기를 그대로 반영하려하기 때문에, 스크롤 기능을 무시하게 됨

→ ScrollViewer 기능이 필요할 경우, StackPanel이 아닌 Grid를 사용해야 함

 

 

 

📑

참고 자료

Chat GPT

728x90
728x90

 

예? 저에게 데이터가 전달되었다고요?

를 표현한 잔망루피입니다.

 

예전에는 WPF 개발을 MVVM 구조로 진행해서 ViewModel과 Model을 사용해서 데이터를 전달하곤 했는데, 이번에는 개발 디자인 패턴이 바뀌어서 MVC로 진행하게 되었습니다.

 

데이터 전달을 EventHandler라는 것을 사용하게 되는데 간단히 정리해보도록 하겠습니다.

 

1. EventHander 선언

public event EventHandler<string> TitleSelected;
// string 타입의 데이터를 이벤트 핸들러에 전달할 수 있음

 

 

2. EventHanlder가 사용될 이벤트 등록 및 구현

lv_Presents.SelectionChanged += Lv_Presents_SelectionChanged;
// ListView에서 항목이 변경될 때 실행되는 이벤트(Lv_Presents_SelectionChanged)를 등록
// 선택 항목이 바뀔 때마다 메서드가 호출

private void lv_Presents_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
    if (lv_Presents.SelectedItem is PresentModel model)
    {
        TitleSelected?.Invoke(this, model.title);
        // this: 현재 UserControl을 이벤트 발신자로 지정
        // model.title: 전달할 데이터
        // ?.Invoke: 이벤트 구독자에게 데이터 전달(구독자가 없을 때 예외가 발생하지 않음)
    }
}

 

 

3. 이벤트 구독 및 처리로직 구현

uc_Presents.TitleSelected += uc_Presents_TitleSelected;
// uc_Presents TitleSelected 이벤트가 발생하면, uc_Presents_TitleSelected 메서드 실행
// Window가 생성될 때 이벤트 핸들러를 등록하여 UserControl에서 발생한 이벤트를 받을 준비를 함

private void uc_Presents_TitleSelected(object sender, string title)
{
    tblock_SelectedTitle.Text = title;
}

 

 

💡 이벤트 해제

* 객체가 더 이상 사용되지 않거나 이벤트를 더 이상 받을 필요가 없을 때는 -=로 해제하는 것이 좋음

  * 이벤트 핸들러가 해제되지 않으면 구독자가 GC에 의해 수거되지 않기 때문에, 메모리 누수가 발생할 수 있음

  * 여러 번 구독하는 것을 방지

 

 

 

📑

참고 자료

Chat GPT

 

728x90
728x90

미니 프로젝트로 네트워크와 관련된 공부를 하고 있습니다.

네트워크 공부는 처음이기에 이와 관련된 내용을 정리해 보고자 합니다✍️.

 

TCP Server

  * 특정 IP 주소와 포트에서 클라이언트의 연결 요청을 기다리고, 연결이 수립되면 데이터를 주고받는 역할

using System.Net;
using System.Net.Sockets;
using System.Text;

namespace TcpServerTest1
{
    internal class Program
    {
        static void Main(string[] args)
        {
            NetworkStream stream = null;
            TcpListener listener = null;
            Socket clientSocket = null;
            StreamReader reader = null;
            StreamWriter writer = null;

            try
            {
                // 1. 서버 IP 주소 설정
                IPAddress ipAddress = IPAddress.Parse("127.0.0.1");

                // 2. TCPListener 생성 및 시작
                // TCPListener: 클라이언트의 연결을 수신
                listener = new TcpListener(ipAddress, 5001);
                listener.Start(); // 서버가 클라이언트의 연결 요청을 수신하도록 시작

                // 3. 클라이언트의 연결 대기
                // 클라이언트의 연결 요청을 받아들여 Socket 객체를 반환
                // 클라이언트가 연결을 시도할 때까지 블로킹(즉, 이 코드에서 멈추고 기다림)
                clientSocket = listener.AcceptSocket();

                // 4. 클라이언트와의 데이터 통신을 위한 스트림 설정
                // clientSocket과 연결된 NetworkStream 객체를 생성 -> 이를 통해 클라이언트와 데이터를 주고받을 수 있음
                // Encoding: 데이터를 바이트로 변환하거나 바이트를 텍스트로 변환할 때 사용
                stream = new NetworkStream(clientSocket);
                Encoding encoding = Encoding.GetEncoding("utf-8");

                // 5. 데이터 읽기와 쓰기를 위한 스트림 리더와 라이터 생성
                reader = new StreamReader(stream, encoding);
                writer = new StreamWriter(stream, encoding) { AutoFlush = true };

                // 6. 데이터 수신 및 송신 루프
                while (true) 
                {
                    // 클라이언트로부터 데이터 읽기
                    string str = reader.ReadLine();
                    Console.WriteLine(str);

                    // 클라이언트에게 데이터 응답
                    writer.WriteLine(str);
                }

            }
            catch(Exception ex)
            {
                Console.WriteLine(ex.ToString());
            }
            finally
            {
                clientSocket.Close();
            }
        }
    }
}

 

 

TCP Client

using System.Net.Sockets;
using System.Text;

namespace TcpClientTest1
{
    internal class Program
    {
        static void Main(string[] args)
        {
            TcpClient client = null;

            try
            {
                // 1. TCPClient 생성
                client = new TcpClient();

                // 2. 서버에 연결 시도
                // 서버가 해당 포트에서 연결 요청을 듣고 있어야 함
                client.Connect("localhost", 5001);

                // 3. 네트워크 스트림 획득
                // 네트워크 상에서 데이터를 스트림 방식으로 송수신하는 기능을 제공
                // 스트림: 데이터를 바이트 단위로 처리하며, 데이터를 차례로 읽거나 쓸 수 있는 방식
                // 예를 들어, 서버가 클라이언트에 메시지를 보낼 때 NetworkStream을 통해 데이터를 읽을 수 있음
                // 클라이언트는 NetworkStream을 통해 서버로 메시지를 전송할 수 있음
                NetworkStream stream = client.GetStream();

                // 4. 인코딩 설정
                Encoding encoding = Encoding.GetEncoding("utf-8");

                // 5. 스트림 리더와 라이터 생성
                // StreamWriter: 데이터를 서버로 보내는 데 사용, AutoFlush: 매번 쓰기 작업 후에 버퍼를 자동으로 비움
                // StreamReader: 서버로부터 데이터를 읽는 데 사용
                StreamWriter writer = new StreamWriter(stream) { AutoFlush=true };
                StreamReader reader = new StreamReader(stream, encoding);

                // 6. 사용자 입력 받기
                string dataToSend = Console.ReadLine();

                while (true)
                {
                    // 7. 서버에 데이터 전송
                    writer.WriteLine(dataToSend);

                    // 8. 종료 조건 체크
                    if (dataToSend.IndexOf("<EOF>") > -1)
                    {
                        break;
                    }

                    // 9. 서버로부터 데이터 읽기 및 출력
                    Console.WriteLine(reader.ReadLine());

                    // 10. 다음 데이터 입력
                    dataToSend = Console.ReadLine();
                }

            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.ToString());
                throw;
            }
            finally
            {
                client.Close();
            }
        }
    }
}

 

 

TCP Server + Multi Threading

  * 기존 TCP Server에서는 Server : Client가 1:1 구조를 이루면서, 클라이언트가 2이상일 때 처리하지 못함

  * 여러 클라이언트가 동시에 서버에 연결하고 통신할 수 있도록 멀티스레딩을 활용

using System.Net;
using System.Net.Sockets;
using System.Text;

namespace TcpServerTest1
{
    // 개별 클라이언트와의 통신을 처리
    // 각 클라이언트는 별도의 ClientHandler 인스턴스를 가지며, 이 인스턴스는 별도의 스레드에서 실행
    internal class ClientHandler
    {
        // 클라이언트와의 연결을 나타내는 소켓
        Socket socket = null;

        // 클라이언트와의 데이터 통신을 위한 스트림
        NetworkStream stream = null;

        // 클라이언트로부터 데이터를 읽는 리더
        StreamReader reader = null;

        // 클라이언트에게 데이터를 쓰는 라이터
        StreamWriter writer = null;

        public ClientHandler(Socket socket)
        {
            this.socket = socket;
        }

        // 클라이언트와 데이터를 주고받는 메서드
        // 무한 루프를 사용하여 지속적으로 클라이언트로부터 데이터를 읽고 응답
        public void Chat()
        {
            stream = new NetworkStream(socket);
            Encoding encoding = Encoding.GetEncoding("utf-8");

            reader = new StreamReader(stream, encoding);
            writer = new StreamWriter(stream, encoding) { AutoFlush = true };

            while (true)
            {
                // 클라이언트로부터 데이터 읽기
                string str = reader.ReadLine();
                Console.WriteLine(str);

                // 클라이언트에게 데이터 응답
                writer.WriteLine(str);
            }
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            TcpListener listener = null;
            Socket clientSocket = null;

            try
            {
                // 1. 서버 IP 주소 설정
                IPAddress ipAddress = IPAddress.Parse("127.0.0.1");

                // 2. TCPListener 생성 및 시작
                // TCPListener: 클라이언트의 연결을 수신
                listener = new TcpListener(ipAddress, 5001);
                listener.Start(); // 서버가 클라이언트의 연결 요청을 수신하도록 시작
                
                while (true)
                {
	                // 프로그램이 클라이언트의 연결 요청을 받을 때마다, 새로운 Thread를 생성하고 그 스레드가 클라이언트와의 통신을 처리
                    
                    // 3. 클라이언트의 연결 대기 및 수락
                    clientSocket = listener.AcceptSocket();

                    // 4. 클라이언트 처리기 생성 및 스레드 시작
                    ClientHandler handler = new ClientHandler(clientSocket);
                    Thread t = new Thread(new ThreadStart(handler.Chat)); // 새 스레드를 시작하여 클라이언트와의 통신을 독립적으로 처리

                    t.Start();
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.ToString());
            }
            finally
            {
                clientSocket.Close();
            }

        }
    }
}

 

 

좌: 단일 Thread 통신 / 우: 멀티 Thread 통신

 

 

 

📚 참고 자료

 

728x90
728x90

xaml에서 ⭐나만의 property⭐를 만들 수 있다는 걸 알고 계셨나요?

전 알고 있었습니다. 하지만, ⭐나만의 property⭐가 없어도 구현에 문제가 없어서 Dependency Property를 공부하는 걸 미루다가 프로젝트에서 사용되는 걸 보고, 이 참에 정리하고자 글을 작성해 봅니다.

 

 

 

Dependency Property

  * XAML, C# 코드 비하인드(.xaml.cs)에서 사용 가능

  * 의존속성 값이 변경되면 자동으로 어떤 것을 로드되게 하거나 랜더링 되도록 할 수 있음

    * 애니메이션, 스타일링, 데이터바인딩 등에 자주 사용

  * 기본으로 제공되는 UI 컨트롤은 대부분의 속성이 의존 속성으로 되어 있음

 

 

1. MainWindow에서 바로 사용하기

MainWindow.xaml

<Window x:Class="DependencyPropertyPratice.MainWindow"
        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:DependencyPropertyPratice"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800"
        >
    <Window.ContextMenu>
        <ContextMenu MenuItem.Click="ContextMenu_Click">
            <MenuItem Header="#02343F"/>
            <MenuItem Header="#F0EDCC"/>
            <MenuItem Header="#0A174E"/>
            <MenuItem Header="#F5D042"/>
        </ContextMenu>
    </Window.ContextMenu>

    <TextBox Name="colorBox"
             VerticalAlignment="Center" HorizontalAlignment="Center"
             Width="150" Height="50"
             />
</Window>

 

MainWindow.xaml.cs

using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;

namespace DependencyPropertyPratice
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }

        // DependencyProperty(MyColorProperty)를 위한 래퍼 속성의 MyColor
        // 이 래퍼속성에서는 System.Windows.DependencyObject 클래스의 GetValue()와 SetValue()를 이용해서 get, set을 정의해야 함
        public string MyColor
        {
            get { return (string)GetValue(MyColorProperty); }
            set { SetValue(MyColorProperty, value); }
        }

        // DependencyProperty
        // DependencyProperty 클래스에는 public 생성자가 없으므로 static 메서드인 Register()를 사용해서 등록
        // 수정이 불가능 하도록 의존 속성은 readonly 필드로 선언되는데, 일반 UI 컨트롤 등 대부분의 의존속성은 FrameworkElement에 DependencyProperty로 정의되어 있음
        public static readonly DependencyProperty MyColorProperty = DependencyProperty.Register(
            "MyColor" // 의존 속성으로 등록될 속성
            , typeof(string) // 등록할 의존 속성 타입
            , typeof(MainWindow) // 의존 속성을 소유하게 될 Owner
            , new FrameworkPropertyMetadata(new PropertyChangedCallback(OnMyColorPropertyChanged))
            // 속성값 변경 시, 호출될 메서드
            // Property 값 변경에 따른 Callback() 등 새로운 속성을 추가하기 위해 FrameworkPropertyMetadata를 인자 값으로 전달 할 수 있음
            );

        private static void OnMyColorPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            MainWindow window = d as MainWindow;
            SolidColorBrush brush = (SolidColorBrush)new BrushConverter().ConvertFromString(e.NewValue.ToString());
            window.Background = brush;
            window.Title = (e.OldValue == null) ? "이전 배경색 없음" : e.OldValue.ToString();
            window.colorBox.Text = e.NewValue.ToString();
        }

        private void ContextMenu_Click(object sender, RoutedEventArgs e)
        {
            string selectedColor = (e.Source as MenuItem).Header as string;
            MyColor = selectedColor;
        }

    }
}

 

 

2. SubWindow에서 상속받아서 사용하기

SubWindow.xaml

<local:MainWindow x:Class="DependencyPropertyPratice.WindowSub"
        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:DependencyPropertyPratice"
        mc:Ignorable="d"
        Title="WindowSub" Height="450" Width="800"
        MyColor="#F95700"
        >
    <Grid>
        <Button Content="Go To the Main Window"
                Width="150" Height="30"
                Click="Button_Click"
                />
    </Grid>
</local:MainWindow>

 

☠️ 이렇게 못씁니다.

오류납니다.

'DependencyPropertyPratice.MainWindow' cannot be the root of a XAML file because it was defined using XAML.

 

WPF에서는 XAML 파일을 통해 이미 정의된 클래스를 다시 XAML의 루트 요소로 사용하는 것을 허용하지 않습니다.

 

상속을 받고자 한다면, MainWindow를 XAML이 아닌 코드 비하인드 파일(C# 파일)에서만 정의하여 SubWindow에서 이를 상속할 수 있도록 할 수 있습니다.

 

 

MainWindowOnlyCS.cs

using System.Windows;
using System.Windows.Media;

namespace DependencyPropertyPratice
{
    public class MainWindowOnlyCS : Window
    {
        public string MyColor
        {
            get { return (string)GetValue(MyColorProperty); }
            set { SetValue(MyColorProperty, value); }
        }

        public static readonly DependencyProperty MyColorProperty = DependencyProperty.Register(
            "MyColor" // 의존 속성으로 등록될 속성
            , typeof(string) // 등록할 의존 속성 타입
            , typeof(MainWindowOnlyCS) // 의존 속성을 소유하게 될 Owner
            , new FrameworkPropertyMetadata(new PropertyChangedCallback(OnMyColorPropertyChanged))
            // 속성값 변경 시, 호출될 메서드
            );

        private static void OnMyColorPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            MainWindowOnlyCS window = d as MainWindowOnlyCS;
            SolidColorBrush brush = (SolidColorBrush)new BrushConverter().ConvertFromString(e.NewValue.ToString());
            window.Background = brush;
            window.Title = (e.OldValue == null) ? "이전 배경색 없음" : e.OldValue.ToString();
        }
    }
}

 

WindowSub.xaml

<local:MainWindowOnlyCS x:Class="DependencyPropertyPratice.WindowSub"
        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:DependencyPropertyPratice"
        mc:Ignorable="d"
        Title="WindowSub" Height="450" Width="800"
        MyColor="#F95700"
        >
    <Grid>
        <Button Content="Go To the Main Window"
                Width="150" Height="30"
                Click="Button_Click"
                />
    </Grid>
</local:MainWindowOnlyCS>

 

.cs로 이뤄진 Window 파일을 상속받아서 xaml 태그에 MyColor를 전달하면 DependencyProperty에 따라 WindowSub의 배경색이 오랜지 색으로 변경됩니다.

 

 

 

 

📚 참고 자료

 

Reference Source

 

referencesource.microsoft.com

 

 

728x90
728x90

xaml에서 비슷한 두 개의 코드에 style을 적용시켰는데, 하나의 코드에서는 동작하고 하나의 코드에서는 동작을 하지 않았습니다. 이유를 간단하게 정리해 보고자 합니다.


저에겐 MouseOver 시, 글씨를 굵게 만들어주는 Style이 하나 있습니다.

 

<Style x:Key="DefaultButton" TargetType="Button">
    <Setter Property="Background" Value="DodgerBlue"/>
    <Setter Property="Foreground" Value="#FFFFFF"/>
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="Button">
                <Border x:Name="Border" 
                        Background="{TemplateBinding Background}"
                        BorderBrush="{TemplateBinding BorderBrush}"
                        CornerRadius="5">
                    <ContentPresenter HorizontalAlignment="Center" 
                                      VerticalAlignment="Center"/>
                </Border>
                <ControlTemplate.Triggers>
                    <Trigger Property="IsMouseOver" Value="True">
                        <Setter Property="FontWeight" Value="Bold" />
                    </Trigger>
                </ControlTemplate.Triggers>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

 

스타일을 한 번 정의해두면, 여러 UI 요소에 동일한 스타일을 적용할 수 있어 코드의 재사용성이 높아집니다.

 

DodgerBlue 색상의 호버 효과가 있는 버튼은 이제 저에게 식은죽 먹기가 됩니다.

 

<Button Style="{StaticResource DefaultButton}">
    <TextBlock Text="Hello"/>
</Button>
<Button Style="{StaticResource DefaultButton}"
        Content="Hello"
        >

 

 

하지만, 아래 코드에서는 Button안의 TextBlock에 FontWeight를 직접적으로 명시했다가는 DodgerBlue 색상만 가진 버튼이 됩니다.

 

<Button Style="{StaticResource DefaultButton}">
    <TextBlock Text="Hello"
               FontWeight="Light"
               />
</Button>
<Button Style="{StaticResource DefaultButton}"
        Content="Hello"
        FontWeight="Light"
        >

 

그 차이를 간단히 정리해 보고자 합니다.

 

먼저, Sytle에 설정된 내용을 간력히 정리해보면, 다음과 같습니다.

 

 

1. ControlTemplate

Control Template , 즉 <> 태그의 외형을 정의합니다.

ControlTemplate을 사용할 경우, 기본 Template을 덮어씁니다.

 

2. ContentPresenter

새로운 Template을 만들어 사용할 경우, 내용(Content)을 표시하는 기능인 ContentPresenter도 함께 선언해야 합니다.

 

3. TemplateBinding

ControlTemplate 안에서 외부의 속성 값을 바인딩할 때 사용합니다.

여기서는 DefaultButton을 사용한 Button의 Background인 DodgerBlue를 참조하게 됩니다.

 

4. Trigger

마지막으로 trigger는 특정 조건이 만족될 때(여기서는 마우스가 버튼 위에 올라왔을 때) 스타일이나 속성을 변경합니다. 이를 통해 ControlTemplate에서 설정된 속성들을 변경할 수 있습니다.

 

 

1번 코드와 2번 코드는 Button 안의 TextBlock에 FontWeight를 직접 설정했느냐의 차이가 있습니다.

 

1번은

  Complate.Trigger → Button 속성 변경 → ContentPresenter: 변경된 속성을 Button의 자식 요소인 TextBlock에 전달합니다.

2번은

  Complate.Trigger → Button 속성 변경 → ContentPresenter: 변경된 속성을 Button의 자식 요소인 TextBlock에 전달합니다. 하지만, 자식 요소에서 직접 설정된 속성은 ControlTemplate의 외부에서 설정된 것으로 간주되어, ControlTemplate의 트리거에서 영향을 받지 않습니다. 

 

간단하게 정리하면, ControlTemplate은 전체적인 Template를 control할 수 있지만, <> 안에서 직접 설정된 요소는 변경할 수 없습니다.

 

 

 

📚 참고 자료

 

[c# wpf] Control Template 란? / 사용방법 (with Style)

이 글을 읽기 전 선수 지식 포스팅 [c# wpf] Style 태그 사용 방법 [c# wpf] Style 상속 방법(with BasedOn) [c# wpf] StaticResource란? (DynamicResource 비교) 왼쪽 그림은 우리가 아는 기본적인 버튼에 빨간 배경색을

yeko90.tistory.com

 

728x90