새 항목이 추가될 때 ListBox 자동 스크롤을 사용하려면 어떻게 해야 합니까?
WPF ListBox는 수평으로 스크롤되도록 설정되어 있습니다.아이템소스가 내 ViewModel 클래스의 관찰 가능한 컬렉션에 바인딩되었습니다.새 항목이 추가될 때마다 ListBox가 오른쪽으로 스크롤하여 새 항목을 볼 수 있도록 합니다.
ListBox가 DataTemplate에 정의되어 있으므로 코드 뒤에 있는 파일의 ListBox에 이름으로 액세스할 수 없습니다.
ListBox를 항상 스크롤하여 최신 추가 항목을 표시하려면 어떻게 해야 합니까?
ListBox에 새 항목이 추가되었을 때 알 수 있는 방법을 알고 싶지만 이를 수행하는 이벤트가 보이지 않습니다.
연결된 속성을 사용하여 ListBox의 동작을 확장할 수 있습니다., 저는 당의경우같, 다음첨은부속것정다입니의할성라고 하는 첨부된 입니다.ScrollOnNewItem
하기로 되어 있는 경우true
에 INotifyCollectionChanged
목록 상자 항목의 이벤트 소스 및 새 항목을 검색하면 목록 상자가 해당 항목으로 스크롤됩니다.
예:
class ListBoxBehavior
{
static readonly Dictionary<ListBox, Capture> Associations =
new Dictionary<ListBox, Capture>();
public static bool GetScrollOnNewItem(DependencyObject obj)
{
return (bool)obj.GetValue(ScrollOnNewItemProperty);
}
public static void SetScrollOnNewItem(DependencyObject obj, bool value)
{
obj.SetValue(ScrollOnNewItemProperty, value);
}
public static readonly DependencyProperty ScrollOnNewItemProperty =
DependencyProperty.RegisterAttached(
"ScrollOnNewItem",
typeof(bool),
typeof(ListBoxBehavior),
new UIPropertyMetadata(false, OnScrollOnNewItemChanged));
public static void OnScrollOnNewItemChanged(
DependencyObject d,
DependencyPropertyChangedEventArgs e)
{
var listBox = d as ListBox;
if (listBox == null) return;
bool oldValue = (bool)e.OldValue, newValue = (bool)e.NewValue;
if (newValue == oldValue) return;
if (newValue)
{
listBox.Loaded += ListBox_Loaded;
listBox.Unloaded += ListBox_Unloaded;
var itemsSourcePropertyDescriptor = TypeDescriptor.GetProperties(listBox)["ItemsSource"];
itemsSourcePropertyDescriptor.AddValueChanged(listBox, ListBox_ItemsSourceChanged);
}
else
{
listBox.Loaded -= ListBox_Loaded;
listBox.Unloaded -= ListBox_Unloaded;
if (Associations.ContainsKey(listBox))
Associations[listBox].Dispose();
var itemsSourcePropertyDescriptor = TypeDescriptor.GetProperties(listBox)["ItemsSource"];
itemsSourcePropertyDescriptor.RemoveValueChanged(listBox, ListBox_ItemsSourceChanged);
}
}
private static void ListBox_ItemsSourceChanged(object sender, EventArgs e)
{
var listBox = (ListBox)sender;
if (Associations.ContainsKey(listBox))
Associations[listBox].Dispose();
Associations[listBox] = new Capture(listBox);
}
static void ListBox_Unloaded(object sender, RoutedEventArgs e)
{
var listBox = (ListBox)sender;
if (Associations.ContainsKey(listBox))
Associations[listBox].Dispose();
listBox.Unloaded -= ListBox_Unloaded;
}
static void ListBox_Loaded(object sender, RoutedEventArgs e)
{
var listBox = (ListBox)sender;
var incc = listBox.Items as INotifyCollectionChanged;
if (incc == null) return;
listBox.Loaded -= ListBox_Loaded;
Associations[listBox] = new Capture(listBox);
}
class Capture : IDisposable
{
private readonly ListBox listBox;
private readonly INotifyCollectionChanged incc;
public Capture(ListBox listBox)
{
this.listBox = listBox;
incc = listBox.ItemsSource as INotifyCollectionChanged;
if (incc != null)
{
incc.CollectionChanged += incc_CollectionChanged;
}
}
void incc_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (e.Action == NotifyCollectionChangedAction.Add)
{
listBox.ScrollIntoView(e.NewItems[0]);
listBox.SelectedItem = e.NewItems[0];
}
}
public void Dispose()
{
if (incc != null)
incc.CollectionChanged -= incc_CollectionChanged;
}
}
}
용도:
<ListBox ItemsSource="{Binding SourceCollection}"
lb:ListBoxBehavior.ScrollOnNewItem="true"/>
업데이트 아래 댓글에서 Andrej의 제안에 따라, 나는 변화를 감지하기 위해 후크를 추가했습니다.ItemsSource
의 시대의ListBox
.
<ItemsControl ItemsSource="{Binding SourceCollection}">
<i:Interaction.Behaviors>
<Behaviors:ScrollOnNewItem/>
</i:Interaction.Behaviors>
</ItemsControl>
public class ScrollOnNewItem : Behavior<ItemsControl>
{
protected override void OnAttached()
{
AssociatedObject.Loaded += OnLoaded;
AssociatedObject.Unloaded += OnUnLoaded;
}
protected override void OnDetaching()
{
AssociatedObject.Loaded -= OnLoaded;
AssociatedObject.Unloaded -= OnUnLoaded;
}
private void OnLoaded(object sender, RoutedEventArgs e)
{
var incc = AssociatedObject.ItemsSource as INotifyCollectionChanged;
if (incc == null) return;
incc.CollectionChanged += OnCollectionChanged;
}
private void OnUnLoaded(object sender, RoutedEventArgs e)
{
var incc = AssociatedObject.ItemsSource as INotifyCollectionChanged;
if (incc == null) return;
incc.CollectionChanged -= OnCollectionChanged;
}
private void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if(e.Action == NotifyCollectionChangedAction.Add)
{
int count = AssociatedObject.Items.Count;
if (count == 0)
return;
var item = AssociatedObject.Items[count - 1];
var frameworkElement = AssociatedObject.ItemContainerGenerator.ContainerFromItem(item) as FrameworkElement;
if (frameworkElement == null) return;
frameworkElement.BringIntoView();
}
}
목록 상자 스크롤 뷰어를 업데이트하고 위치를 맨 아래로 설정하는 아주 교묘한 방법을 찾았습니다.예를 들어 SelectionChanged와 같은 ListBox Events 중 하나에서 이 함수를 호출합니다.
private void UpdateScrollBar(ListBox listBox)
{
if (listBox != null)
{
var border = (Border)VisualTreeHelper.GetChild(listBox, 0);
var scrollViewer = (ScrollViewer)VisualTreeHelper.GetChild(border, 0);
scrollViewer.ScrollToBottom();
}
}
저는 이 솔루션을 사용합니다: http://michlg.wordpress.com/2010/01/16/listbox-automatically-scroll-currentitem-into-view/ .
목록 상자의 항목을 바인딩해도 작동합니다.UI가 아닌 스레드에서 조작되는 관찰 가능한 컬렉션의 소스입니다.
MVVM 스타일 연결 동작
이 연결된 동작은 새 항목이 추가될 때 목록 상자를 자동으로 맨 아래로 스크롤합니다.
<ListBox ItemsSource="{Binding LoggingStream}">
<i:Interaction.Behaviors>
<behaviors:ScrollOnNewItemBehavior
IsActiveScrollOnNewItem="{Binding IfFollowTail, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
</i:Interaction.Behaviors>
</ListBox>
의 신의에서.ViewModel
boolean 부에바수있다습니할에 수 .IfFollowTail { get; set; }
자동 스크롤의 활성화 여부를 제어합니다.
행동은 모든 옳은 일을 합니다.
- 한다면
IfFollowTail=false
이 View Model(보기 모델)에 설정되어 있으면 ListBox가 더 이상 새 항목의 맨 아래로 스크롤되지 않습니다. - 하자마자
IfFollowTail=true
이 View Model에 설정되면 ListBox가 즉시 맨 아래로 스크롤되고 계속 스크롤됩니다. - 빠르네요.몇 백 밀리초 동안 아무것도 하지 않은 후에만 스크롤됩니다.새로운 항목이 추가될 때마다 스크롤되기 때문에 단순한 구현은 매우 느립니다.
- 중복된 ListBox 항목과 함께 작동합니다(다른 많은 구현은 중복과 함께 작동하지 않습니다. 첫 번째 항목으로 스크롤한 다음 중지됩니다).
- 지속적으로 들어오는 항목을 처리하는 로깅 콘솔에 적합합니다.
동작 C# 코드
public class ScrollOnNewItemBehavior : Behavior<ListBox>
{
public static readonly DependencyProperty IsActiveScrollOnNewItemProperty = DependencyProperty.Register(
name: "IsActiveScrollOnNewItem",
propertyType: typeof(bool),
ownerType: typeof(ScrollOnNewItemBehavior),
typeMetadata: new PropertyMetadata(defaultValue: true, propertyChangedCallback:PropertyChangedCallback));
private static void PropertyChangedCallback(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs dependencyPropertyChangedEventArgs)
{
// Intent: immediately scroll to the bottom if our dependency property changes.
ScrollOnNewItemBehavior behavior = dependencyObject as ScrollOnNewItemBehavior;
if (behavior == null)
{
return;
}
behavior.IsActiveScrollOnNewItemMirror = (bool)dependencyPropertyChangedEventArgs.NewValue;
if (behavior.IsActiveScrollOnNewItemMirror == false)
{
return;
}
ListboxScrollToBottom(behavior.ListBox);
}
public bool IsActiveScrollOnNewItem
{
get { return (bool)this.GetValue(IsActiveScrollOnNewItemProperty); }
set { this.SetValue(IsActiveScrollOnNewItemProperty, value); }
}
public bool IsActiveScrollOnNewItemMirror { get; set; } = true;
protected override void OnAttached()
{
this.AssociatedObject.Loaded += this.OnLoaded;
this.AssociatedObject.Unloaded += this.OnUnLoaded;
}
protected override void OnDetaching()
{
this.AssociatedObject.Loaded -= this.OnLoaded;
this.AssociatedObject.Unloaded -= this.OnUnLoaded;
}
private IDisposable rxScrollIntoView;
private void OnLoaded(object sender, RoutedEventArgs e)
{
var changed = this.AssociatedObject.ItemsSource as INotifyCollectionChanged;
if (changed == null)
{
return;
}
// Intent: If we scroll into view on every single item added, it slows down to a crawl.
this.rxScrollIntoView = changed
.ToObservable()
.ObserveOn(new EventLoopScheduler(ts => new Thread(ts) { IsBackground = true}))
.Where(o => this.IsActiveScrollOnNewItemMirror == true)
.Where(o => o.NewItems?.Count > 0)
.Sample(TimeSpan.FromMilliseconds(180))
.Subscribe(o =>
{
this.Dispatcher.BeginInvoke((Action)(() =>
{
ListboxScrollToBottom(this.ListBox);
}));
});
}
ListBox ListBox => this.AssociatedObject;
private void OnUnLoaded(object sender, RoutedEventArgs e)
{
this.rxScrollIntoView?.Dispose();
}
/// <summary>
/// Scrolls to the bottom. Unlike other methods, this works even if there are duplicate items in the listbox.
/// </summary>
private static void ListboxScrollToBottom(ListBox listBox)
{
if (VisualTreeHelper.GetChildrenCount(listBox) > 0)
{
Border border = (Border)VisualTreeHelper.GetChild(listBox, 0);
ScrollViewer scrollViewer = (ScrollViewer)VisualTreeHelper.GetChild(border, 0);
scrollViewer.ScrollToBottom();
}
}
}
이벤트에서 반응형 확장으로 연결
마지막으로, 모든 RX 선량을 사용할 수 있도록 이 확장 방법을 추가합니다.
public static class ListBoxEventToObservableExtensions
{
/// <summary>Converts CollectionChanged to an observable sequence.</summary>
public static IObservable<NotifyCollectionChangedEventArgs> ToObservable<T>(this T source)
where T : INotifyCollectionChanged
{
return Observable.FromEvent<NotifyCollectionChangedEventHandler, NotifyCollectionChangedEventArgs>(
h => (sender, e) => h(e),
h => source.CollectionChanged += h,
h => source.CollectionChanged -= h);
}
}
반응형 확장 추가
은 해야 합니다.Reactive Extensions
당신의 프로젝트에.를 합니다.NuGet
.
Datagrid용 솔루션(ListBox와 동일, DataGrid를 ListBox 클래스로만 대체)
private void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (e.Action == NotifyCollectionChangedAction.Add)
{
int count = AssociatedObject.Items.Count;
if (count == 0)
return;
var item = AssociatedObject.Items[count - 1];
if (AssociatedObject is DataGrid)
{
DataGrid grid = (AssociatedObject as DataGrid);
grid.Dispatcher.BeginInvoke((Action)(() =>
{
grid.UpdateLayout();
grid.ScrollIntoView(item, null);
}));
}
}
}
저는 제안된 해결책이 마음에 들지 않았습니다.
- 저는 "누출된" 속성 설명자를 사용하고 싶지 않았습니다.
- 겉보기에는 사소한 작업에 대해 Rx 종속성과 8줄 쿼리를 추가하고 싶지 않았습니다.저도 계속 작동하는 타이머를 원하지 않았습니다.
- 하지만 저는 shawnfiore의 아이디어가 마음에 들어서 그 위에 애착이 가는 행동을 만들었습니다. 지금까지 제 경우에는 잘 작동합니다.
이것이 제가 결국 가지고 온 것입니다.아마도 누군가를 시간을 절약할 수 있을 것입니다.
public class AutoScroll : Behavior<ItemsControl>
{
public static readonly DependencyProperty ModeProperty = DependencyProperty.Register(
"Mode", typeof(AutoScrollMode), typeof(AutoScroll), new PropertyMetadata(AutoScrollMode.VerticalWhenInactive));
public AutoScrollMode Mode
{
get => (AutoScrollMode) GetValue(ModeProperty);
set => SetValue(ModeProperty, value);
}
protected override void OnAttached()
{
base.OnAttached();
AssociatedObject.Loaded += OnLoaded;
AssociatedObject.Unloaded += OnUnloaded;
}
protected override void OnDetaching()
{
Clear();
AssociatedObject.Loaded -= OnLoaded;
AssociatedObject.Unloaded -= OnUnloaded;
base.OnDetaching();
}
private static readonly DependencyProperty ItemsCountProperty = DependencyProperty.Register(
"ItemsCount", typeof(int), typeof(AutoScroll), new PropertyMetadata(0, (s, e) => ((AutoScroll)s).OnCountChanged()));
private ScrollViewer _scroll;
private void OnLoaded(object sender, RoutedEventArgs e)
{
var binding = new Binding("ItemsSource.Count")
{
Source = AssociatedObject,
Mode = BindingMode.OneWay
};
BindingOperations.SetBinding(this, ItemsCountProperty, binding);
_scroll = AssociatedObject.FindVisualChild<ScrollViewer>() ?? throw new NotSupportedException("ScrollViewer was not found!");
}
private void OnUnloaded(object sender, RoutedEventArgs e)
{
Clear();
}
private void Clear()
{
BindingOperations.ClearBinding(this, ItemsCountProperty);
}
private void OnCountChanged()
{
var mode = Mode;
if (mode == AutoScrollMode.Vertical)
{
_scroll.ScrollToBottom();
}
else if (mode == AutoScrollMode.Horizontal)
{
_scroll.ScrollToRightEnd();
}
else if (mode == AutoScrollMode.VerticalWhenInactive)
{
if (_scroll.IsKeyboardFocusWithin) return;
_scroll.ScrollToBottom();
}
else if (mode == AutoScrollMode.HorizontalWhenInactive)
{
if (_scroll.IsKeyboardFocusWithin) return;
_scroll.ScrollToRightEnd();
}
}
}
public enum AutoScrollMode
{
/// <summary>
/// No auto scroll
/// </summary>
Disabled,
/// <summary>
/// Automatically scrolls horizontally, but only if items control has no keyboard focus
/// </summary>
HorizontalWhenInactive,
/// <summary>
/// Automatically scrolls vertically, but only if itmes control has no keyboard focus
/// </summary>
VerticalWhenInactive,
/// <summary>
/// Automatically scrolls horizontally regardless of where the focus is
/// </summary>
Horizontal,
/// <summary>
/// Automatically scrolls vertically regardless of where the focus is
/// </summary>
Vertical
}
데이터 원본에 바인딩된 목록 상자(또는 목록 보기)에 대해 가장 직접적인 방법은 컬렉션 변경 이벤트와 연결하는 것입니다.목록 상자의 DataContextChanged 이벤트에서 이 작업을 매우 쉽게 수행할 수 있습니다.
//in xaml <ListView x:Name="LogView" DataContextChanged="LogView_DataContextChanged">
private void LogView_DataContextChanged(object sender, DependencyPropertyChangedEventArgs e)
{
var src = LogView.Items.SourceCollection as INotifyCollectionChanged;
src.CollectionChanged += (obj, args) => { LogView.Items.MoveCurrentToLast(); LogView.ScrollIntoView(LogView.Items.CurrentItem); };
}
이것은 사실 제가 찾은 다른 모든 답들의 조합일 뿐입니다.저는 이것이 너무 사소한 기능이기 때문에 우리가 많은 시간(및 코드 라인)을 들일 필요가 없다고 생각합니다.
Autoscroll = 실제 속성만 있으면 됩니다.한숨.
저는 비슷한 문제를 해결하는 데 도움이 되는 훨씬 간단한 방법을 찾았습니다. 코드 뒤에 몇 줄만 있으면 사용자 지정 행동을 만들 필요가 없습니다.이 질문에 대한 나의 답변을 확인합니다(그리고 그 안에 있는 링크를 따릅니다).
wpf(C#) DataGrid ScrollInView - 표시되지 않은 첫 번째 행으로 스크롤하는 방법은 무엇입니까?
ListBox, ListView 및 DataGrid에 대해 작동합니다.
그래서 이 topcs에서 제가 읽은 것은 간단한 행동을 하기에는 조금 복잡합니다.
그래서 저는 스크롤 변경 이벤트를 구독하고 다음 코드를 사용했습니다.
private void TelnetListBox_OnScrollChanged(object sender, ScrollChangedEventArgs e)
{
var scrollViewer = ((ScrollViewer)e.OriginalSource);
scrollViewer.ScrollToEnd();
}
보너스:
그 후 자동 스크롤 기능을 사용하고 싶을 때 설정할 수 있는 확인란을 만들었고 가끔 흥미로운 정보가 보이면 목록 확인란을 선택 해제하는 것을 잊었다는 것을 다시 깨달았습니다.그래서 저는 제 마우스 동작에 반응하는 지능형 자동 스크롤 목록 상자를 만들기로 결정했습니다.
private void TelnetListBox_OnScrollChanged(object sender, ScrollChangedEventArgs e)
{
var scrollViewer = ((ScrollViewer)e.OriginalSource);
scrollViewer.ScrollToEnd();
if (AutoScrollCheckBox.IsChecked != null && (bool)AutoScrollCheckBox.IsChecked)
scrollViewer.ScrollToEnd();
if (_isDownMouseMovement)
{
var verticalOffsetValue = scrollViewer.VerticalOffset;
var maxVerticalOffsetValue = scrollViewer.ExtentHeight - scrollViewer.ViewportHeight;
if (maxVerticalOffsetValue < 0 || verticalOffsetValue == maxVerticalOffsetValue)
{
// Scrolled to bottom
AutoScrollCheckBox.IsChecked = true;
_isDownMouseMovement = false;
}
else if (verticalOffsetValue == 0)
{
}
}
}
private bool _isDownMouseMovement = false;
private void TelnetListBox_OnPreviewMouseWheel(object sender, MouseWheelEventArgs e)
{
if (e.Delta > 0)
{
_isDownMouseMovement = false;
AutoScrollCheckBox.IsChecked = false;
}
if (e.Delta < 0)
{
_isDownMouseMovement = true;
}
}
내가 아래쪽으로 스크롤했을 때 확인란이 true로 선택되고 마우스 휠로 스크롤하면 확인란이 선택 해제되고 목록 상자를 탐색할 수 있습니다.
이것이 제가 사용하는 해결책입니다. 다른 사람에게 도움이 될 수도 있습니다.
statusWindow.SelectedIndex = statusWindow.Items.Count - 1;
statusWindow.UpdateLayout();
statusWindow.ScrollIntoView(statusWindow.SelectedItem);
statusWindow.UpdateLayout();
이것은 나에게 도움이 됩니다.
DirectoryInfo di = new DirectoryInfo(folderBrowserDialog1.SelectedPath);
foreach (var fi in di.GetFiles("*", SearchOption.AllDirectories))
{
int count = Convert.ToInt32(listBox1.Items.Count); // counts every listbox entry
listBox1.Items.Add(count + " - " + fi.Name); // display entrys
listBox1.TopIndex = count; // scroll to the last entry
}
언급URL : https://stackoverflow.com/questions/2006729/how-can-i-have-a-listbox-auto-scroll-when-a-new-item-is-added
'sourcecode' 카테고리의 다른 글
C# - Excel의 모든 행을 반복하는 방법._워크시트? (0) | 2023.04.28 |
---|---|
Angular와 함께 jQuery를 사용하는 방법은 무엇입니까? (0) | 2023.04.28 |
윈도우즈용 명령줄을 사용하여 상승된 cmd를 여는 방법은 무엇입니까? (0) | 2023.04.28 |
명령줄을 통해 Git 커밋 메시지에 느낌표 사용 (0) | 2023.04.28 |
Bash의 경로 문자열에서 파일 접미사와 경로 부분을 제거하려면 어떻게 해야 합니까? (0) | 2023.04.28 |