WPF에서 프로그램에서 ListBox를 사용하다보면 ListBox에 항목이 여러 개일 때
특정 항목의 TextBox를 클릭했을 때의 위치를 알아내고 싶은 경우가 발생할 수 있습니다.
이번에는 ListBox에서 클릭한 항목의 위치를 알아내는 방법에 대하여 알아보겠습니다.
ListBox 이벤트 중 GotFocus를 등록하여 이벤트가 발생한 ListBox의 Item 목록을 순회하면서
몇 번째의 Item에 Focus가 활성화 되었는지를 찾아내는 방법입니다.
먼저 테스트를 위한 ListBox를 만들어 보겠습니다.
App.xaml 파일에 다음과 같이 코드를 작성합니다.
<Application.Resources>
<DataTemplate x:Key="ListBoxFormat">
<Border BorderBrush="Black" BorderThickness="1">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBlock Name="number" Grid.Column="0" Text="{Binding Path=Number}" Width="50"/>
<TextBox Name="name" Grid.Column="1" Text="{Binding Path=Name}" Width="200"/>
</Grid>
</Border>
</DataTemplate>
</Application.Resources>
그리고 디자인을 구현하는 곳(프로젝트 처음 생성 시 MainWindow.xaml)에서 Window 블록 안에 다음과 같이 추가해줍니다. 이후의 사진과 똑같이 만들기 위해서 전체 코드를 올렸으나, ListBox가 핵심이므로 기존에 만드신 Grid가 있다면 그 안에 추가해도 무방합니다.
<Grid x:Name="gridMain">
<Grid.RowDefinitions>
<RowDefinition Height="50"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Button Grid.Row="0" Content="Button" HorizontalAlignment="Left" Margin="707,10,0,0" VerticalAlignment="Top" Width="75" Click="Button_Click"/>
<Grid Grid.Row="1" Name="grid3D" Background="#FFFFD6D6">
<ListBox Name="lsbTest" HorizontalAlignment="Left" Height="426" Margin="56,109,0,0" VerticalAlignment="Top" Width="343" ItemTemplate="{StaticResource ListBoxFormat}" GotFocus="listBox_GotFocus"/>
</Grid>
</Grid>
다음은 해당 기능에 대한 코드입니다.
위치를 알아낼 ListBox에 GotFocus 이벤트를 적용해야 합니다.
private void listBox_GotFocus(object sender, RoutedEventArgs e)
{
ListBox list = (ListBox)sender;
for (int i = 0; i < list.Items.Count; i++)
{
object item = list.Items[i];
ListBoxItem lbi = (ListBoxItem)list.ItemContainerGenerator.ContainerFromItem(item);
if (lbi.IsKeyboardFocusWithin)
{
list.SelectedIndex = i;
break;
}
}
}
다음은 GotFocus 이벤트 코드에 대한 설명입니다.
먼저 이벤트가 발생한 ListBox를 추출합니다.
그 다음 반복문을 통하여 다음을 ListBox의 아이템 개수만큼 반복합니다.
- ListBox의 i 위치에 있는 Item을 가져옵니다.
- 가져온 Item을 새로 생성한 ListBoxItem 클래스 인스턴스에 넣습니다.
- TextBox를 클릭했으므로 해당 Item에 키보드의 포커스가 지정되어 있는지를 확인합니다.
포커스가 지정되어있는 Item을 찾으면 해당 위치의 Item을 선택하고 반복문을 빠져나옵니다.
저는 간단하게 class가 담긴 List로 바인딩하여 테스트해 보았으나 정상 동작함을 확인하였습니다.
그리고 반복문으로 Item을 1개씩 추가한 상태에서도 테스트해 보았으나 정상적으로 구해지는것을 확인하였습니다.
다음은 바인딩으로 데이터를 넣고 위와 같이 적용하였을 때의 모습입니다.
list.SelectedIndex = i; 코드를 빼고 테스트해보면 실제로는 리스트의 항목이 선택되지 않았기 때문에
다음과 같이 선택 효과가 나오지 않습니다.
그리고 선택된 부분의 텍스트를 가져오기 위해서는 조금 더 복잡한 방법을 사용해야 합니다.
GotFocus 이벤트 코드를 다음과 같이 수정해줍니다.
private void listBox_GotFocus(object sender, RoutedEventArgs e)
{
ListBox list = (ListBox)sender;
for (int i = 0; i < list.Items.Count; i++)
{
object item = list.Items[i];
ListBoxItem lbi = (ListBoxItem)list.ItemContainerGenerator.ContainerFromItem(item);
if (lbi.IsKeyboardFocusWithin)
{
ContentPresenter itemContentPresenter = FindVisualChild<ContentPresenter>(lbi);
DataTemplate itemDataTemplate = itemContentPresenter.ContentTemplate;
TextBox textBox = (TextBox)itemDataTemplate.FindName("name", itemContentPresenter);
list.SelectedIndex = i;
MessageBox.Show(textBox.Text);
break;
}
}
}
다음은 추가된 부분에 대한 설명입니다.
- 텍스트박스가 클릭된 Item을 찾은 후에 ContentPresenter 클래스로 변수를 생성하여 FindVisualChild 함수에 추출한 ListBoxItem의 ContentPresenter를 구해서 저장합니다.
- 구해진 ContentPresenter의 ContentTemplate 속성을 DataTemplate 클래스로 만든 변수에 저장합니다.
- DataTemplate 변수에 FindName 함수를 사용하여 TextBox를 구합니다.
"name"은 App.xaml에서 생성한 TextBox에 부여한 이름이고, itemContentPresenter 변수는 itemDataTemplate 변수의 부모를 의미합니다.
따라서, 다음과 같이 사용해도 같은 결과를 얻을 수 있습니다.
private void listBox_GotFocus(object sender, RoutedEventArgs e)
{
ListBox list = (ListBox)sender;
for (int i = 0; i < list.Items.Count; i++)
{
object item = list.Items[i];
ListBoxItem lbi = (ListBoxItem)list.ItemContainerGenerator.ContainerFromItem(item);
if (lbi.IsKeyboardFocusWithin)
{
ContentPresenter itemContentPresenter = FindVisualChild<ContentPresenter>(lbi);
TextBox textBox = (TextBox)itemContentPresenter.ContentTemplate.FindName("name", itemContentPresenter);
list.SelectedIndex = i;
MessageBox.Show(textBox.Text);
break;
}
}
}
그리고 ContentPresenter 변수에 데이터를 넣을 때 사용된 FindVisualChild 함수를 추가해줍니다.
private childItem FindVisualChild<childItem>(DependencyObject obj)
where childItem : DependencyObject
{
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(obj); i++)
{
DependencyObject child = VisualTreeHelper.GetChild(obj, i);
if (child != null && child is childItem)
{
return (childItem)child;
}
else
{
childItem childOfChild = FindVisualChild<childItem>(child);
if (childOfChild != null)
return childOfChild;
}
}
return null;
}
다음은 해당 함수의 코드에 대한 설명입니다.
1. 함수를 선언할 때 제네릭 형태로 선언하였고, 보통은 T라고 예시로 많이 들지만 여기서는 childItem이라는 이름으로 사용되었습니다. childItem이라는 명칭은 변경이 가능하고 이 함수에서는 ContentPresentor 클래스로 적용될 것입니다.
그리고 obj 인자로 추출했던 ListBoxItem을 넣어주었기 때문에 이 함수에서 obj는 ListBoxItem이 됩니다.
그 밑에 붙어있는 where 구문은 childItem(ContentPresentor)이 DependencyObject 클래스이거나 DependencyObject에서 파생된 클래스여야 한다는 조건을 부여한 것입니다.
다음은 이해를 돕기 위한 클래스의 상속 순서입니다. 오른쪽으로 갈수록 하위 클래스입니다.
Object → DispatcherObject → DependencyObject → Visual → UIElement
→ FrameworkElement → ContentPresenter
2. 그리고 반복문으로 obj의 하위 객체의 개수만큼 반복을 진행합니다.
3. obj의 하위 객체중 i번째를 추출하여 DependencyObject 변수에 저장합니다. 맨 처음 진행시에는 App.xaml 파일에 DataTemplate 하위 항목들을 보면 Border, Grid, RowDefinition, ColumnDefinition (2), TextBlock, TextBox 들이 존재하므로 이 시점에서 obj의 하위 객체가 총 7개입니다.
4. child 변수가 null이 아니고 childItem(ContentPresentor)으로 변환되는지 확인합니다.
5. 맞다면 childItem(ContentPresentor)로 변환해서 return 해주고, 아니라면 child 변수를 obj에 넣고 다시 재귀적으로 진행합니다.
6. 재귀함수가 끝나고 반환하는 값이 null이 아니면 해당 값을 return 해줍니다.
재귀함수의 개념은 해당 글에서 설명하지 않으며, 추후 포스팅 시 링크를 추가하도록 하겠습니다.
이상으로 C#의 WPF에서 ListBox의 TextBox중 하나에 Focus가 걸렸을 때 찾는 방법에 대하여 알아보았습니다.
C# WPF Canvas의 사용 (2) | 2024.05.30 |
---|---|
WPF ''{DependencyProperty.UnsetValue}'은(는) 'Background' 속성의 올바른 값이 아닙니다.' 오류 해결 방법 기록 (0) | 2022.11.24 |
WPF에서 도형의 투명도가 적용이 안될 때 해결 방법 (0) | 2022.05.19 |
WPF에서 Rhino의 3dm 파일 불러오는 방법 (2) | 2022.05.17 |
WPF에 Winform 컨트롤 가져오기 (0) | 2022.02.20 |
댓글 영역