레이블이 wpf인 게시물을 표시합니다. 모든 게시물 표시
레이블이 wpf인 게시물을 표시합니다. 모든 게시물 표시

2023년 11월 6일 월요일

2가지 예제를 통한 간단한 WPF 바인딩 원리


바인딩의 기본 개념은 위 그림과 같다. 
바인딩 타겟은 항상 DependencyProperty 여야하고 바인딩 소스는 DependencyProperty 나 일반 프로퍼티(.NET 프로퍼티)나 상관없다.
여기서

OneWay 바인딩은 소스 -> 타겟 
OneWayToSource 바인딩은 소스 <- 타겟
TwoWay 바인딩은 소스 <-> 타겟

으로 바인딩 된다.


1. TextBox 바인딩

화면을 위아래로 나눠서 상단에 텍스트 박스를 하단에 버튼 1개을 나타낸 간단한 UI를 만든다.

 <Window x:Class="TextBoxBind.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:TextBoxBind"  
     mc:Ignorable="d"  
     Title="MainWindow" Height="450" Width="800">  
   <Grid>  
     <Grid.RowDefinitions>  
       <RowDefinition Height="50*"/>  
       <RowDefinition Height="50*"/>  
     </Grid.RowDefinitions>  
     <TextBox Grid.Row="0" Text="{Binding InputText, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />  
     <Button x:Name="button" Content="Add Text" HorizontalAlignment="Left" Margin="42,29,0,0" Grid.Row="1" VerticalAlignment="Top" Click="button_Click"/>  
   </Grid>  
 </Window>  

위 xaml 코드에서 TextBox는 InputText 프로퍼티와 바인딩을 하며 Mode는 TwoWay, UpdateSourceTrigger는 PropertyChanged 이다.
즉 양방향 바인딩이고 데이터 변환시 즉시 반영되는 바인딩이다.
code behind 는 아래와 같다.

 using System;  
 using System.Collections.Generic;  
 using System.ComponentModel;  
 using System.Linq;  
 using System.Runtime.CompilerServices;  
 using System.Text;  
 using System.Threading;  
 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;  
   
 namespace TextBoxBind  
 {  
   /// <summary>  
   /// MainWindow.xaml에 대한 상호 작용 논리  
   /// </summary>  
   public partial class MainWindow : Window, INotifyPropertyChanged  
   {  
     private string inputText;  
   
     public string InputText  
     {  
       get { return inputText; }  
       set  
       {  
         if (inputText != value)  
         {  
           Console.WriteLine($"InputText : {value}");  
           inputText = value;  
           OnPropertyChanged("InputText");
         }  
       }  
     }  
   
     public MainWindow()  
     {  
       InitializeComponent();  
       DataContext = this;  
     }  
   
     public event PropertyChangedEventHandler PropertyChanged;  
   
     protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)  
     {  
       PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));  
     }  
   
     private void button_Click(object sender, RoutedEventArgs e)  
     {  
       Thread thread = new Thread(new ThreadStart(() => InputText += "AAA"));  
       thread.IsBackground = true;  
       thread.Start();  
     }  
   }  
 }  
   

Add Text 버튼클릭시 쓰레드로 InputText 프로퍼티의 값을 추가한다.
양방향 바인딩이므로 TextBox에 문자를 입력하면 InputText 값이 바뀌고 버튼을 클릭하면 TextBox 출력문자가 변경된다.('AAA'추가)

여기서 InputText의 setter 부분에 있는 OnPropertyChanged("InputText") 
를 주석처리하면 버튼을 클릭해도 TextBox에 수정내용 즉 InputText 값이 반영되지 않는다.

이유는 바인딩을 하는 순간 TextBox는 
public event PropertyChangedEventHandler PropertyChanged; 
델러게이트 이벤트 핸들러를 구독하게되고 이벤트 발생시 InputText의 getter를 통해 현재 값을 가져와서 UI에 반영(Text 프로퍼티에)하기 때문이다.

Mode=OneWayToSource 를 하면 바인딩 타겟(TextBox) -> 바인딩 소스(InputText) 로만 데이터 전달이 되는데 이 경우는 TextBox가 바로 InputText의 setter 를 호출하게 되는데 이때는 OnPropertyChanged("InputText") 호출이 필요없다.(다른 이유에서 필요할 수 있으므로 호출하는것이 좋다)


2. DataGrid 바인딩

화면을 좌우로 나눠서 왼쪽에 DataGrid 를 오른쪽에 버튼과 기타 텍스트박스 컨트롤을 배치했다.

 <Window x:Class="ListBindTest.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:ListBindTest"  
     mc:Ignorable="d"  
     Title="MainWindow" Height="450" Width="800">  
   <Grid>  
     <Grid.ColumnDefinitions>  
       <ColumnDefinition Width="*"/>  
       <ColumnDefinition Width="*"/>  
     </Grid.ColumnDefinitions>  
     <DataGrid Grid.Column="0" ItemsSource="{Binding People}" SelectedItem="{Binding SelectedItem}" SelectionMode="Single" />  
     <Canvas Grid.Column="1">  
       <TextBlock x:Name="textBlock" Canvas.Left="41" TextWrapping="Wrap" Text="Name" Canvas.Top="35"/>  
       <TextBlock x:Name="textBlock_Copy" Canvas.Left="41" TextWrapping="Wrap" Text="Age" Canvas.Top="87" HorizontalAlignment="Center" VerticalAlignment="Top"/>  
       <TextBox x:Name="textName" Canvas.Left="101" Text="{Binding SelectedItem.Name, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Canvas.Top="35" Width="120"/>  
       <TextBox x:Name="textAge" Canvas.Left="101" Text="{Binding SelectedItem.Age, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Canvas.Top="87" Width="120" HorizontalAlignment="Center" VerticalAlignment="Top"/>  
       <TextBlock x:Name="textBlock_Copy1" Canvas.Left="41" TextWrapping="Wrap" Text="Name" Canvas.Top="183" HorizontalAlignment="Center" VerticalAlignment="Top"/>  
       <TextBlock x:Name="textBlock_Copy2" Canvas.Left="41" TextWrapping="Wrap" Text="Age" Canvas.Top="235" HorizontalAlignment="Center" VerticalAlignment="Top"/>  
       <Canvas x:Name="canvasTest">  
         <TextBox x:Name="textName_Copy" Canvas.Left="101" Text="{Binding Name, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Canvas.Top="183" Width="120" HorizontalAlignment="Center" VerticalAlignment="Top"/>  
         <TextBox x:Name="textAge_Copy" Canvas.Left="101" Text="{Binding Age, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Canvas.Top="235" Width="120" HorizontalAlignment="Center" VerticalAlignment="Top"/>  
       </Canvas>  
       <Button x:Name="button" Content="button" Canvas.Left="22" Canvas.Top="297" Click="button_Click"/>  
       <Button x:Name="button1" Content="button1" Canvas.Left="78" Canvas.Top="297" Click="button1_Click"/>  
     </Canvas>  
   </Grid>  
 </Window>     

code behind 는 아래와 같다.
 using System;  
 using System.Collections.Generic;  
 using System.Collections.ObjectModel;  
 using System.ComponentModel;  
 using System.Linq;  
 using System.Runtime.InteropServices;  
 using System.Text;  
 using System.Threading;  
 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;  
   
 namespace ListBindTest  
 {  
   /// <summary>  
   /// MainWindow.xaml에 대한 상호 작용 논리  
   /// </summary>  
   public partial class MainWindow : Window  
   {  
     private MainViewModel viewModel = new MainViewModel();  
     private Person personTest = new Person();  
   
     public MainWindow()  
     {  
       InitializeComponent();  
   
       DataContext = viewModel;  
   
       personTest.Name = "Test";  
       personTest.Age = 100;  
       canvasTest.DataContext = personTest;  
     }  
   
     private void button_Click(object sender, RoutedEventArgs e)  
     {  
       Person p = new Person();  
       p.Name = personTest.Name;  
       p.Age = personTest.Age;  
   
       Thread thread = new Thread(new ThreadStart(() =>  
       {  
         this.Dispatcher.BeginInvoke(new Action(() =>  
         {  
           viewModel.People.Add(p);
         }));  
       }));  
       thread.IsBackground = true;  
       thread.Start();  
     }  
   
     private void button1_Click(object sender, RoutedEventArgs e)  
     {  
       Thread thread = new Thread(new ThreadStart(() =>  
       {  
         Person p = viewModel.SelectedItem;  
         if (p != null) p.Age++;  
       }));  
       thread.IsBackground = true;  
       thread.Start();  
     }  
   }  
   
  public class Person : INotifyPropertyChanged  
   {  
     private string _name;  
     private int _age;  
   
     public string Name  
     {  
       get { return _name; }  
       set  
       {  
         _name = value;  
         OnPropertyChanged(nameof(Name));  
       }  
     }  
   
     public int Age  
     {  
       get { return _age; }  
       set  
       {  
         _age = value;  
         OnPropertyChanged(nameof(Age));  
       }  
     }  
   
     public event PropertyChangedEventHandler PropertyChanged;  
   
     protected virtual void OnPropertyChanged(string propertyName)  
     {  
       PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));  
     }  
   }  
   
   public class MainViewModel : INotifyPropertyChanged  
   {  
     private ObservableCollection<Person> _people;  
     private Person _selectedItem;  
   
     public MainViewModel()  
     {  
       _people = new ObservableCollection<Person>();  
       _people.Add(new Person { Name = "Alice", Age = 25 });  
       _people.Add(new Person { Name = "Bob", Age = 30 });  
       _people.Add(new Person { Name = "Charlie", Age = 35 });  
     }  
   
     public ObservableCollection<Person> People  
     {  
       get { return _people; }  
       set  
       {  
         _people = value;  
         OnPropertyChanged(nameof(People));  
       }  
     }  
   
     public Person SelectedItem  
     {  
       get { return _selectedItem; }  
       set  
       {  
         _selectedItem = value;  
         OnPropertyChanged(nameof(SelectedItem));  
       }  
     }  
   
     public event PropertyChangedEventHandler PropertyChanged;  
   
     protected virtual void OnPropertyChanged(string propertyName)  
     {  
       PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));  
     }  
   }  
 }  
   

button을 클릭하면 DataGrid 에 아이템이 추가되는데 여기서 주의해야할 점은 바인딩된 ObservableCollection 에 아이템을 추가할때 반드시 UI 쓰레드에서 추가해야한다는 점이다.

UI 쓰레드로 추가하지 않으려면 BindingOperations.EnableCollectionSynchronization 메소드를 사용하는 방법이 있다.

 namespace ListBindTest  
 {  
   /// <summary>  
   /// MainWindow.xaml에 대한 상호 작용 논리  
   /// </summary>  
   public partial class MainWindow : Window  
   {  
     private MainViewModel viewModel = new MainViewModel();  
     private Person personTest = new Person();  
   
     public MainWindow()  
     {  
       InitializeComponent();  
   
       DataContext = viewModel;  
   
       personTest.Name = "Test";  
       personTest.Age = 100;  
       canvasTest.DataContext = personTest;  
     }  
   
     private void button_Click(object sender, RoutedEventArgs e)  
     {  
       Person p = new Person();  
       p.Name = personTest.Name;  
       p.Age = personTest.Age;  
   
       Thread thread = new Thread(new ThreadStart(() =>  
       {  
 #if false  
         this.Dispatcher.BeginInvoke(new Action(() =>  
         {  
           viewModel.People.Add(p);  
           //viewModel.People.Add(personTest);  
         }));  
 #else  
         viewModel.People.Add(p);  
 #endif  
       }));  
       thread.IsBackground = true;  
       thread.Start();        
     }  
   
     private void button1_Click(object sender, RoutedEventArgs e)  
     {  
       Thread thread = new Thread(new ThreadStart(() =>  
       {  
         //personTest.Name = personTest.Name + "X";  
         Person p = viewModel.SelectedItem;  
         if (p != null) p.Age++;  
       }));  
       thread.IsBackground = true;  
       thread.Start();  
     }  
   }  
   
  public class Person : INotifyPropertyChanged  
   {  
     private string _name;  
     private int _age;  
   
     public string Name  
     {  
       get { return _name; }  
       set  
       {  
         _name = value;  
         OnPropertyChanged(nameof(Name));  
       }  
     }  
   
     public int Age  
     {  
       get { return _age; }  
       set  
       {  
         _age = value;  
         OnPropertyChanged(nameof(Age));  
       }  
     }  
   
     public event PropertyChangedEventHandler PropertyChanged;  
   
     protected virtual void OnPropertyChanged(string propertyName)  
     {  
       PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));  
     }  
   }  
   
   public class MainViewModel : INotifyPropertyChanged  
   {  
     private ObservableCollection<Person> _people;  
     private Person _selectedItem;  
   
     private object objLock = new object();  
   
     public MainViewModel()  
     {  
       _people = new ObservableCollection<Person>();  
       _people.Add(new Person { Name = "Alice", Age = 25 });  
       _people.Add(new Person { Name = "Bob", Age = 30 });  
       _people.Add(new Person { Name = "Charlie", Age = 35 });  
   
       BindingOperations.EnableCollectionSynchronization(_people, objLock);  
     }  
   
     public ObservableCollection<Person> People  
     {  
       get { return _people; }  
       set  
       {  
         _people = value;  
         OnPropertyChanged(nameof(People));  
       }  
     }  
   
     public Person SelectedItem  
     {  
       get { return _selectedItem; }  
       set  
       {  
         _selectedItem = value;  
         OnPropertyChanged(nameof(SelectedItem));  
       }  
     }  
   
     public event PropertyChangedEventHandler PropertyChanged;  
   
     protected virtual void OnPropertyChanged(string propertyName)  
     {  
       PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));  
     }  
   }  
 }  

그리고 button1을 클릭하면 Age 값이 1증가하는데 이것은 Person 클래스가 INotifyPropertyChanged 를 구현해서 OnPropertyChanged(nameof(Age)) 를 호출하여 역시 1번 예제에서 설명한데로 public event PropertyChangedEventHandler PropertyChanged 를 구독중인 DataGrid 에게 변경여부를 알려주기 때문이다.

2023년 10월 30일 월요일

내가 이해한 Dependency Property

구글링을 하면 WPF의 Dependency Property에 대한 자료가 많이 나온다. 
차근차근 읽어보면 대략적으로 이해가 가는데 이것을 설명을 하자면 조금 답답한 감이 있다보니 개념을 나름데로 정리해봤다.

Dependency Property 를 간단하게 코드로 표현하면 아래와 같다.

public partial class ColorPickerCtrl : UserControl
{
...
public Brush SelectedColor
{
    get { return (Brush)GetValue(SelectedColorProperty); }
    set { SetValue(SelectedColorProperty, value); }
}
public static DependencyProperty SelectedColorProperty =
         DependencyProperty.Register("SelectedColor", typeof(Brush), typeof(ColorPickerCtrl), new UIPropertyMetadata(null));
...  
}

여기서 ColorPickerCtrl은 DependencyObject를 상속받는 클래스이며 Dependency Property를 소유하려면(has-a)
반드시 DependencyObject 를 상속받아야 한다.
찾아보면 Dependency Property 를 사용하는 이유는 아래와 같다.

메모리 절약, 값 상속, 변경값 noti, 기타(데이터 바인딩, 스타일 적용, 리소스 적용, 에니메이션 등등)

여기서부터 본인이 공부하는 입장이 아니라 WPF의 설계자(앤더스 헤일즈버그?)라고 가정하고 어떻게 구현할지 생각해보자.

위 기능들을 구현하려면 일반적인 프로퍼티(CLR Property)로는 구현이 불가능하다. 
프로퍼티이면서 일종의 "동적으로 관리되는" 프로퍼티라는 개념이 필요하다. 
"동적으로 관리된다"는 것의 의미는 컴파일 타임에 프로퍼티가 결정되는 것이 아니라 런타임에 프로퍼티가 결정된다는 것을 의미한다.

프로퍼티를 관리하려면 해당 프로퍼티에 대한 정보, 좀 더 구체적으로 말하면 프로퍼티의 메타 데이터가 필요하다.
그리고 이 프로퍼티 메타 데이터를 저장할 저장공간도 필요하다. 프로퍼티 메타 데이터를 프로퍼티 메타 데이터 저장소에 저장하려면 프로그램 실행 초기이든 실행 중간이든 programmatic하게 등록(저장)하는 과정이 당연히 필요하다.

위 코드에서 DependencyProperty.Register static 메소드가 이 역할을 하는 것이다.
SelectedColor 라는 객체(클래스를 인스턴스화 한것)의 프로퍼티에 대한 메타정보, 즉 SelectedColorProperty는 따라서 당연히 static이 되어야한다. 왜냐면 프로퍼티 메타정보는 인스턴스화 한 객체의 정보가 아니라 클래스의 정보이기 때문이다.

그리고 SelectedColor 프로퍼티의 get/set 은 일반 프로퍼티처럼 고정된 특정 변수를 엑세스하는것이 아니라 역시 programmatic하게 구현되어야 한다. 왜냐하면 SelectedColor의 메타데이터 즉, SelectedColorProperty가 programmatic하게 구현되었기 때문이다.
따라서 GetValue/SetValue 가 사용되는 것이다.

2021년 7월 20일 화요일

WPF WindowChrome Window Maximized 했을때 화면 잘림 현상 해결

 Window 에 아래 코드 입력


     
     private void Window_Loaded(object sender, RoutedEventArgs e)
     {
       IntPtr handle = (new WindowInteropHelper(this)).Handle;
       HwndSource.FromHwnd(handle).AddHook(new HwndSourceHook(WindowProc));
     }

     private static IntPtr WindowProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)  
     {  
       switch (msg)  
       {  
         case 0x10:  
           //Console.WriteLine("Close reason: Clicking X");  
           break;  
         case 0x11:  
         case 0x16:  
           //Console.WriteLine("Close reason: WindowsShutDown");  
           break;  
         case 0x0024:  
           WmGetMinMaxInfo(hwnd, lParam);  
           handled = true;  
           break;  
         case 0x0046:  
           NativeMethods.WINDOWPOS pos = (NativeMethods.WINDOWPOS)Marshal.PtrToStructure(lParam, typeof(NativeMethods.WINDOWPOS));  
           if ((pos.flags & (int)(NativeMethods.SWP.NOMOVE)) != 0)  
           {  
             return IntPtr.Zero;  
           }  
   
           Window wnd = (Window)HwndSource.FromHwnd(hwnd).RootVisual;  
           if (wnd == null)  
           {  
             return IntPtr.Zero;  
           }  
   
           bool changedPos = false;  
           if (pos.cx < wnd.MinWidth) { pos.cx = (int)wnd.MinWidth; changedPos = true; }  
           if (pos.cy < wnd.MinHeight) { pos.cy = (int)wnd.MinHeight; changedPos = true; }  
           if (!changedPos)  
           {  
             return IntPtr.Zero;  
           }  
   
           Marshal.StructureToPtr(pos, lParam, true);  
           handled = true;  
           break;  
         case 0x112:  
           if (((ushort)wParam & 0xfff0) == 0xf060)  
           {  
             // close (alt + F4)  
             handled = true;  
           }  
           break;  
         default:  
           break;  
       }  
       return IntPtr.Zero;  
     }  
   
     private static void WmGetMinMaxInfo(System.IntPtr hwnd, System.IntPtr lParam)  
     {  
       NativeMethods.MINMAXINFO mmi = (NativeMethods.MINMAXINFO)Marshal.PtrToStructure(lParam, typeof(NativeMethods.MINMAXINFO));  
   
       // Adjust the maximized size and position to fit the work area of the correct monitor  
       int MONITOR_DEFAULTTONEAREST = 0x00000002;  
       IntPtr monitor = NativeMethods.MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST);  
   
       if (monitor != System.IntPtr.Zero)  
       {  
         NativeMethods.MONITORINFO monitorInfo = new NativeMethods.MONITORINFO();  
         NativeMethods.GetMonitorInfo(monitor, monitorInfo);  
         NativeMethods.RECT rcWorkArea = monitorInfo.rcWork;  
         NativeMethods.RECT rcMonitorArea = monitorInfo.rcMonitor;  
         mmi.ptMaxPosition.x = Math.Abs(rcWorkArea.left - rcMonitorArea.left);  
         mmi.ptMaxPosition.y = Math.Abs(rcWorkArea.top - rcMonitorArea.top);  
         mmi.ptMaxSize.x = Math.Abs(rcWorkArea.right - rcWorkArea.left);  
         mmi.ptMaxSize.y = Math.Abs(rcWorkArea.bottom - rcWorkArea.top);  
       }  
       Marshal.StructureToPtr(mmi, lParam, true);  
     }  

 public class NativeMethods  
   {  
     [DllImport("user32.dll", CharSet = CharSet.Auto)]  
     internal static extern IntPtr SendMessage(IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam);  
   
     // Define the Win32 API methods we are going to use  
     [DllImport("user32.dll")]  
     internal static extern IntPtr GetSystemMenu(IntPtr hWnd, bool bRevert);  
   
     [DllImport("user32.dll", CharSet = CharSet.Unicode)]  
     internal static extern bool InsertMenu(IntPtr hMenu, uint wPosition, uint wFlags, UIntPtr wIDNewItem, string lpNewItem);  
   
     [DllImport("user32.dll", CharSet = CharSet.Unicode)]  
     internal static extern int ModifyMenu(IntPtr hMenu, uint uPosition, uint wFlags, UIntPtr wIDNewItem, string text);  
   
     [DllImport("user32.dll")]  
     internal static extern bool DeleteMenu(IntPtr hMenu, uint uPosition, uint uFlags);  
   
     [DllImport("user32")]  
     public static extern bool GetMonitorInfo(IntPtr hMonitor, MONITORINFO lpmi);  
   
     [DllImport("User32")]  
     public static extern IntPtr MonitorFromWindow(IntPtr handle, int flags);  
   
     #region Win32 Imports  
     internal const UInt32 MF_BYCOMMAND = 0x00000000;  
   
     internal const UInt32 SC_SIZE = 0xF000;  
     internal const UInt32 SC_MOVE = 0xF010;  
     internal const UInt32 SC_MINIMIZE = 0xF020;  
     internal const UInt32 SC_MAXIMIZE = 0xF030;  
     internal const UInt32 SC_NEXTWINDOW = 0xF040;  
     internal const UInt32 SC_PREVWINDOW = 0xF050;  
     internal const UInt32 SC_CLOSE = 0xF060;  
     internal const UInt32 SC_VSCROLL = 0xF070;  
     internal const UInt32 SC_HSCROLL = 0xF080;  
     internal const UInt32 SC_MOUSEMENU = 0xF090;  
     internal const UInt32 SC_KEYMENU = 0xF100;  
     internal const UInt32 SC_ARRANGE = 0xF110;  
     internal const UInt32 SC_RESTORE = 0xF120;  
     internal const UInt32 SC_TASKLIST = 0xF130;  
     internal const UInt32 SC_SCREENSAVE = 0xF140;  
     internal const UInt32 SC_HOTKEY = 0xF150;  
     internal const UInt32 SC_DEFAULT = 0xF160;  
     internal const UInt32 SC_MONITORPOWER = 0xF170;  
     internal const UInt32 SC_CONTEXTHELP = 0xF180;  
     internal const UInt32 SC_SEPARATOR = 0xF00F;  
   
     /// Define our Constants we will use  
     public const Int32 WM_SYSCOMMAND = 0x112;  
     public const Int32 MF_SEPARATOR = 0x800;  
     public const Int32 MF_BYPOSITION = 0x400;  
     public const Int32 MF_STRING = 0x0;  
   
     public enum SWP : uint  
     {  
       NOSIZE = 0x0001,  
       NOMOVE = 0x0002,  
       NOZORDER = 0x0004,  
       NOREDRAW = 0x0008,  
       NOACTIVATE = 0x0010,  
       FRAMECHANGED = 0x0020,  
       SHOWWINDOW = 0x0040,  
       HIDEWINDOW = 0x0080,  
       NOCOPYBITS = 0x0100,  
       NOOWNERZORDER = 0x0200,  
       NOSENDCHANGING = 0x0400,  
     }  
   
     /// <summary>  
     /// POINT aka POINTAPI  
     /// </summary>  
     [StructLayout(LayoutKind.Sequential)]  
     public struct POINT  
     {  
       /// <summary>  
       /// x coordinate of point.  
       /// </summary>  
       public int x;  
       /// <summary>  
       /// y coordinate of point.  
       /// </summary>  
       public int y;  
   
       /// <summary>  
       /// Construct a point of coordinates (x,y).  
       /// </summary>  
       public POINT(int x, int y)  
       {  
         this.x = x;  
         this.y = y;  
       }  
     }  
   
     [StructLayout(LayoutKind.Sequential)]  
     public struct MINMAXINFO  
     {  
       public POINT ptReserved;  
       public POINT ptMaxSize;  
       public POINT ptMaxPosition;  
       public POINT ptMinTrackSize;  
       public POINT ptMaxTrackSize;  
     };  
   
     /// <summary>  
     /// </summary>  
     [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]  
     public class MONITORINFO  
     {  
       /// <summary>  
       /// </summary>        
       public int cbSize = Marshal.SizeOf(typeof(MONITORINFO));  
   
       /// <summary>  
       /// </summary>        
       public RECT rcMonitor = new RECT();  
   
       /// <summary>  
       /// </summary>        
       public RECT rcWork = new RECT();  
   
       /// <summary>  
       /// </summary>        
       public int dwFlags = 0;  
     }  
   
     [StructLayout(LayoutKind.Sequential)]  
     public struct WINDOWPOS  
     {  
       public IntPtr hwnd;  
       public IntPtr hwndInsertAfter;  
       public int x;  
       public int y;  
       public int cx;  
       public int cy;  
       public int flags;  
     }  
   
     /// <summary> Win32 </summary>  
     [StructLayout(LayoutKind.Sequential, Pack = 0)]  
     public struct RECT  
     {  
       /// <summary> Win32 </summary>  
       public int left;  
       /// <summary> Win32 </summary>  
       public int top;  
       /// <summary> Win32 </summary>  
       public int right;  
       /// <summary> Win32 </summary>  
       public int bottom;  
   
       /// <summary> Win32 </summary>  
       public static readonly RECT Empty = new RECT();  
   
       /// <summary> Win32 </summary>  
       public int Width  
       {  
         get { return Math.Abs(right - left); } // Abs needed for BIDI OS  
       }  
       /// <summary> Win32 </summary>  
       public int Height  
       {  
         get { return bottom - top; }  
       }  
   
       /// <summary> Win32 </summary>  
       public RECT(int left, int top, int right, int bottom)  
       {  
         this.left = left;  
         this.top = top;  
         this.right = right;  
         this.bottom = bottom;  
       }  
   
   
       /// <summary> Win32 </summary>  
       public RECT(RECT rcSrc)  
       {  
         this.left = rcSrc.left;  
         this.top = rcSrc.top;  
         this.right = rcSrc.right;  
         this.bottom = rcSrc.bottom;  
       }  
   
       /// <summary> Win32 </summary>  
       public bool IsEmpty  
       {  
         get  
         {  
           // BUGBUG : On Bidi OS (hebrew arabic) left > right  
           return left >= right || top >= bottom;  
         }  
       }  
       /// <summary> Return a user friendly representation of this struct </summary>  
       public override string ToString()  
       {  
         if (this == RECT.Empty) { return "RECT {Empty}"; }  
         return "RECT { left : " + left + " / top : " + top + " / right : " + right + " / bottom : " + bottom + " }";  
       }  
   
       /// <summary> Determine if 2 RECT are equal (deep compare) </summary>  
       public override bool Equals(object obj)  
       {  
         if (!(obj is Rect)) { return false; }  
         return (this == (RECT)obj);  
       }  
   
       /// <summary>Return the HashCode for this struct (not garanteed to be unique)</summary>  
       public override int GetHashCode()  
       {  
         return left.GetHashCode() + top.GetHashCode() + right.GetHashCode() + bottom.GetHashCode();  
       }  
   
   
       /// <summary> Determine if 2 RECT are equal (deep compare)</summary>  
       public static bool operator ==(RECT rect1, RECT rect2)  
       {  
         return (rect1.left == rect2.left && rect1.top == rect2.top && rect1.right == rect2.right && rect1.bottom == rect2.bottom);  
       }  
   
       /// <summary> Determine if 2 RECT are different(deep compare)</summary>  
       public static bool operator !=(RECT rect1, RECT rect2)  
       {  
         return !(rect1 == rect2);  
       }  
     }  
     #endregion  
   }  



2013년 2월 28일 목요일

WPF Custom Toggle Image Button

http://www.syntaxstudio.co.uk/2012/12/creating-a-toggle-image-button-control-in-wpf/

사이트의 소스를 수정하여 ImageButton 을 구현하였다. 기존 소스를 확장하여
Check/Uncheck 토글뿐 아니라 일반 버튼으로도 사용할 수 있다.

< ImageButton.xaml >
 <ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"  
           xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"  
           xmlns:local="clr-namespace:SyntaxStudio.ToggleImageButton">  
   
   <Style x:Key="ImageButton" TargetType="{x:Type local:ImageButton}">  
     <Setter Property="Foreground" Value="#FFB8B8B8" />  
     <Setter Property="FontWeight" Value="Bold" />  
     <Setter Property="FontSize" Value="14" />  
     <Setter Property="Template">  
       <Setter.Value>  
         <ControlTemplate TargetType="{x:Type local:ImageButton}">  
           <Border x:Name="PART_Border"  
               Background="{TemplateBinding Background}"  
               BorderBrush="{TemplateBinding BorderBrush}"  
               BorderThickness="{TemplateBinding BorderThickness}">  
   
             <StackPanel>  
               <Grid>  
                 <Image x:Name="PART_Icon"  
                     Source="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=CurrentImage}"   
                     Width="{TemplateBinding Width}"   
                     Height="{TemplateBinding Height}" />  
                 <ContentPresenter Content="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=Content}"   
                     Margin="0,0,0,12"  
                     HorizontalAlignment="Center"  
                     VerticalAlignment="Bottom">  
                 </ContentPresenter>  
               </Grid>  
             </StackPanel>  
           </Border>  
           <!--  
           <ControlTemplate.Triggers>  
             <Trigger Property="IsChecked" Value="True">  
               <Setter TargetName="PART_Icon" Property="Source" Value="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=ActiveIcon}" />  
             </Trigger>  
             <Trigger Property="IsChecked" Value="False">  
               <Setter TargetName="PART_Icon" Property="Source" Value="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=InActiveIcon}" />  
             </Trigger>  
           </ControlTemplate.Triggers>  
           -->  
         </ControlTemplate>  
       </Setter.Value>  
     </Setter>  
   </Style>  
   
   <Style x:Key="ImageButton1" TargetType="{x:Type local:ImageButton}">  
     <Setter Property="Foreground" Value="#FFB8B8B8" />  
     <Setter Property="Template">  
       <Setter.Value>  
         <ControlTemplate TargetType="{x:Type local:ImageButton}">  
           <Border x:Name="PART_Border"  
               Background="{TemplateBinding Background}"  
               BorderBrush="{TemplateBinding BorderBrush}"  
               BorderThickness="{TemplateBinding BorderThickness}">  
   
             <StackPanel>  
               <Grid>  
                 <Image x:Name="PART_Icon"  
                     Source="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=CurrentImage}"   
                     Width="{TemplateBinding Width}"   
                     Height="{TemplateBinding Height}" />  
                 <ContentPresenter Content="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=Content}"   
                     HorizontalAlignment="Center"  
                     VerticalAlignment="Center">  
                 </ContentPresenter>  
               </Grid>  
             </StackPanel>  
           </Border>  
           <!--  
           <ControlTemplate.Triggers>  
             <Trigger Property="IsChecked" Value="True">  
               <Setter TargetName="PART_Icon" Property="Source" Value="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=ActiveIcon}" />  
             </Trigger>  
             <Trigger Property="IsChecked" Value="False">  
               <Setter TargetName="PART_Icon" Property="Source" Value="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=InActiveIcon}" />  
             </Trigger>  
           </ControlTemplate.Triggers>  
           -->  
         </ControlTemplate>  
       </Setter.Value>  
     </Setter>  
   </Style>  
 </ResourceDictionary>  

< ImageButton.cs >
 // --------------------------------  
 // <copyright file="ToggleImageButton.cs" company="SyntaxStudio">  
 //   Copyright 2012, www.syntaxstudio.co.uk  
 // </copyright>  
 // <author>Ryan Haworth</author>  
 // ---------------------------------  
   
 using System;  
 using System.Collections.Generic;  
 using System.Linq;  
 using System.Text;  
 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 System.Windows.Controls.Primitives;  
   
 namespace SyntaxStudio.ToggleImageButton  
 {  
   /// <summary>  
   /// The is is a custom control class for the toggle button. This toggle button will  
   /// accept two images. As the button is toggled between an inactive state to an  
   /// active state the button will change its image.  
   /// </summary>  
   public class ImageButton : ButtonBase  
   {  
     private static readonly DependencyProperty CurrentImageProperty = DependencyProperty.Register("CurrentImage", typeof(ImageSource), typeof(ImageButton));  
     private static readonly DependencyProperty UnCheckedImageProperty = DependencyProperty.Register("UnCheckedImage", typeof(ImageSource), typeof(ImageButton));  
     private static readonly DependencyProperty UnCheckedHoverImageProperty = DependencyProperty.Register("UnCheckedHoverImage", typeof(ImageSource), typeof(ImageButton));  
     private static readonly DependencyProperty CheckedImageProperty = DependencyProperty.Register("CheckedImage", typeof(ImageSource), typeof(ImageButton));  
     private static readonly DependencyProperty CheckedHoverImageProperty = DependencyProperty.Register("CheckedHoverImage", typeof(ImageSource), typeof(ImageButton));  
     private static readonly DependencyProperty PressedImageProperty = DependencyProperty.Register("PressedImage", typeof(ImageSource), typeof(ImageButton));  
     private static readonly DependencyProperty DisabledImageProperty = DependencyProperty.Register("DisabledImage", typeof(ImageSource), typeof(ImageButton));  
   
     private bool isHovering = false;  
   
     private bool isChecked = false;  
     public bool IsChecked  
     {  
       get { return isChecked; }  
       set  
       {  
         if (value == false)  
         {  
           if (isHovering == false)  
             CurrentImage = UnCheckedImage;  
           else  
             CurrentImage = UnCheckedHoverImage;  
         }  
         else  
         {  
           if (isHovering == false)  
             CurrentImage = CheckedImage;  
           else  
             CurrentImage = CheckedHoverImage;  
         }  
         isChecked = value;  
       }  
     }  
   
     private bool isDisabled = false;  
     public bool IsDisabled  
     {  
       get { return isDisabled; }  
       set  
       {  
         if (value == false)  
         {  
           CurrentImage = UnCheckedImage;  
         }  
         else  
         {  
           CurrentImage = DisabledImage;  
         }  
       }  
     }  
   
     public ImageSource CurrentImage  
     {  
       get { return (ImageSource)GetValue(CurrentImageProperty); }  
       set { SetValue(CurrentImageProperty, value); }  
     }  
   
     public ImageSource UnCheckedImage  
     {  
       get { return (ImageSource)GetValue(UnCheckedImageProperty); }  
       set { SetValue(UnCheckedImageProperty, value); }  
     }  
   
     public ImageSource UnCheckedHoverImage  
     {  
       get { return (ImageSource)GetValue(UnCheckedHoverImageProperty); }  
       set { SetValue(UnCheckedHoverImageProperty, value); }  
     }  
   
     public ImageSource CheckedImage  
     {  
       get { return (ImageSource)GetValue(CheckedImageProperty); }  
       set { SetValue(CheckedImageProperty, value); }  
     }  
   
     public ImageSource CheckedHoverImage  
     {  
       get { return (ImageSource)GetValue(CheckedHoverImageProperty); }  
       set { SetValue(CheckedHoverImageProperty, value); }  
     }  
   
     public ImageSource PressedImage  
     {  
       get { return (ImageSource)GetValue(PressedImageProperty); }  
       set { SetValue(PressedImageProperty, value); }  
     }  
   
     public ImageSource DisabledImage  
     {  
       get { return (ImageSource)GetValue(DisabledImageProperty); }  
       set { SetValue(DisabledImageProperty, value); }  
     }  
   
     public ImageButton()  
     {  
       CurrentImage = UnCheckedImage;  
     }  
   
     static ImageButton()  
     {  
       DefaultStyleKeyProperty.OverrideMetadata(typeof(ImageButton), new FrameworkPropertyMetadata(typeof(ImageButton)));  
     }  
   
     protected override void OnMouseEnter(MouseEventArgs e)  
     {  
       base.OnMouseEnter(e);  
   
       isHovering = true;  
   
       if (IsChecked == false)  
       {  
         if (UnCheckedHoverImage != null)  
           CurrentImage = UnCheckedHoverImage;  
       }  
       else  
       {  
         if (CheckedHoverImage != null)  
           CurrentImage = CheckedHoverImage;  
       }  
     }  
   
     protected override void OnMouseLeave(MouseEventArgs e)  
     {  
       base.OnMouseLeave(e);  
   
       isHovering = false;  
   
       if (IsChecked == false)  
       {  
         if (UnCheckedImage != null)  
           CurrentImage = UnCheckedImage;  
       }  
       else  
       {  
         if (CheckedImage != null)  
           CurrentImage = CheckedImage;  
       }  
     }  
   
     protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e)  
     {  
       base.OnMouseLeftButtonDown(e);  
       if (PressedImage != null)  
         CurrentImage = PressedImage;  
     }  
   
     protected override void OnMouseLeftButtonUp(MouseButtonEventArgs e)  
     {  
       base.OnMouseLeftButtonUp(e);  
       if (PressedImage != null)  
       {  
         if (IsChecked == true)  
           CurrentImage = CheckedImage;  
         else  
           CurrentImage = UnCheckedImage;  
       }  
     }  
   }  
 }  
   


< 사용예 >
...
xmlns:ToggleImageButton="clr-namespace:SyntaxStudio.ToggleImageButton" 
...
                <ToggleImageButton:ImageButton x:Name="btnToggleBorder" Width="65" Height="35" 
                                               CurrentImage="/images/uncheck.png"
                                               UnCheckedImage="/images/uncheck.png" UnCheckedHoverImage="/images/uncheck_hover.png" 
                                               CheckedImage="/images/check.png" CheckedHoverImage="check_hover.png" 
                                               Click="btnToggleBorder_Click"/>
...
btnToggleBorder.IsChecked = true/false 를 줘서 토글상태 변경




WPF Grid Sliding Animation

http://www.codeproject.com/Articles/18379/WPF-Tutorial-Part-2-Writing-a-custom-animation-cla
에 있는 클래스를 조금 수정하여 Grid 패널을 숨기고 보여주는 기능을 애니메이션으로 구현.
기존 클래스를 사용하면 Grid Column/Row 의 GridLength 속성이 Pixel 인 경우 제대로 동작하지 않는 문제와 애니메이션후 동작하지않는 문제가 있었고 위 주소를 참조하여 이를 해결.

< GridLengthAnimation.cs >

namespace GridAnimationDemo
{
    internal class GridLengthAnimation : AnimationTimeline
    {
        static GridLengthAnimation()
        {
            FromProperty = DependencyProperty.Register("From", typeof(GridLength),
                typeof(GridLengthAnimation));

            ToProperty = DependencyProperty.Register("To", typeof(GridLength),
                typeof(GridLengthAnimation));
        }

        public override Type TargetPropertyType
        {
            get
            {
                return typeof(GridLength);
            }
        }

        protected override System.Windows.Freezable CreateInstanceCore()
        {
            return new GridLengthAnimation();
        }

        public static readonly DependencyProperty FromProperty;
        public GridLength From
        {
            get
            {
                return (GridLength)GetValue(GridLengthAnimation.FromProperty);
            }
            set
            {
                SetValue(GridLengthAnimation.FromProperty, value);
            }
        }

        public static readonly DependencyProperty ToProperty;
        public GridLength To
        {
            get
            {
                return (GridLength)GetValue(GridLengthAnimation.ToProperty);
            }
            set
            {
                SetValue(GridLengthAnimation.ToProperty, value);
            }
        }

        public override object GetCurrentValue(object defaultOriginValue,
            object defaultDestinationValue, AnimationClock animationClock)
        {
            double fromVal = ((GridLength)GetValue(GridLengthAnimation.FromProperty)).Value;
            double toVal = ((GridLength)GetValue(GridLengthAnimation.ToProperty)).Value;

            if (fromVal > toVal)
            {
                return new GridLength((1 - animationClock.CurrentProgress.Value) * (fromVal - toVal) + toVal,
                ((GridLength)GetValue(GridLengthAnimation.FromProperty)).GridUnitType);
            }

            return new GridLength(animationClock.CurrentProgress.Value * (toVal - fromVal) + fromVal,
            ((GridLength)GetValue(GridLengthAnimation.ToProperty)).GridUnitType);
        }
    }
}

< Show/Hide 애니메이션 >

GridLength orgGridWidth = gridMain.ColumnDefinitions[0].Width;
int minPanelWidth = 30;
        private void btnSlidePanel_Click(object sender, RoutedEventArgs e)
        {
            if (gridMain.ColumnDefinitions[0].Width.Value > minPanelWidth)  // hide grid
            {
                orgGridWidth = gridMain.ColumnDefinitions[0].Width;                
                GridLengthAnimation gla = new GridLengthAnimation();
                gla.From = gridMain.ColumnDefinitions[0].Width;
                gla.To = new GridLength(minPanelWidth, GridUnitType.Pixel);
                gla.Duration = new TimeSpan(0, 0, 0, 0, 300);
                gla.FillBehavior = FillBehavior.HoldEnd;
                gla.Completed += delegate(object o, EventArgs evt)
                {
                    gridMain.ColumnDefinitions[0].Width = new GridLength(minPanelWidth, GridUnitType.Pixel);
                    gridMain.ColumnDefinitions[0].BeginAnimation(ColumnDefinition.WidthProperty, null);
                    gla.FillBehavior = FillBehavior.Stop;
                };
                gridMain.ColumnDefinitions[0].BeginAnimation(ColumnDefinition.WidthProperty, gla);
            }
            else // show grid
            {
                GridLengthAnimation gla = new GridLengthAnimation();
                gla.From = gridMain.ColumnDefinitions[0].Width;
                gla.To = orgGridWidth;
                gla.Duration = new TimeSpan(0, 0, 0, 0, 300);
                gla.FillBehavior = FillBehavior.HoldEnd;
                gla.Completed += delegate(object o, EventArgs evt)
                {
                    gridMain.ColumnDefinitions[0].Width = orgGridWidth;
                    gridMain.ColumnDefinitions[0].BeginAnimation(ColumnDefinition.WidthProperty, null);
                    gla.FillBehavior = FillBehavior.Stop;
                };
                gridMain.ColumnDefinitions[0].BeginAnimation(ColumnDefinition.WidthProperty, gla);
            }
        }