prosource

자동으로 종료되는 WPF 목록 상자 스크롤

probook 2023. 4. 13. 20:56
반응형

자동으로 종료되는 WPF 목록 상자 스크롤

내 어플리케이션에는ListBox아이템이 포함되어 있습니다.애플리케이션은 WPF로 기술되어 있습니다.

마지막으로 추가한 항목으로 자동 스크롤하려면 어떻게 해야 합니까?나는 그 것을 원한다.ScrollViewer새 항목이 추가되면 목록 끝으로 이동합니다.

이런 이벤트가 있나요?ItemsChanged? (사용하고 싶지 않습니다.SelectionChanged이벤트)

이것을 시험해 보세요.

lstBox.SelectedIndex = lstBox.Items.Count -1;
lstBox.ScrollIntoView(lstBox.SelectedItem) ;

Main Window에서 목록의 마지막 항목을 선택하고 포커스를 맞춥니다!

가장 쉬운 방법은 다음과 같습니다.

if (VisualTreeHelper.GetChildrenCount(listView) > 0)
{
    Border border = (Border)VisualTreeHelper.GetChild(listView, 0);
    ScrollViewer scrollViewer = (ScrollViewer)VisualTreeHelper.GetChild(border, 0);
    scrollViewer.ScrollToBottom();
}

ListView 및 ListBox 컨트롤에서는 항상 작동합니다.이 코드를 에 부가합니다.listView.Items.SourceCollection.CollectionChanged이벤트 및 완전 자동 자동 검출 동작이 있습니다.

주의해 주세요listBox.ScrollIntoView(listBox.Items[listBox.Items.Count - 1]);는 중복 항목이 없는 경우에만 작동합니다.같은 내용의 아이템이 있으면 첫 번째 검색까지 스크롤됩니다.

다음은 제가 찾은 해결책입니다.

ListBoxAutomationPeer svAutomation = 
    ListBoxAutomationPeer)ScrollViewerAutomationPeer.
        CreatePeerForElement(myListBox);

IScrollProvider scrollInterface =
    (IScrollProvider)svAutomation.GetPattern(PatternInterface.Scroll);

System.Windows.Automation.ScrollAmount scrollVertical = 
    System.Windows.Automation.ScrollAmount.LargeIncrement;

System.Windows.Automation.ScrollAmount scrollHorizontal = 
    System.Windows.Automation.ScrollAmount.NoAmount;

// If the vertical scroller is not available, 
// the operation cannot be performed, which will raise an exception. 
if (scrollInterface.VerticallyScrollable)
    scrollInterface.Scroll(scrollHorizontal, scrollVertical);

가장 좋은 해결책은 ListBox 컨트롤 내에서 ItemCollection 개체를 사용하는 것입니다. 이 컬렉션은 콘텐츠 뷰어를 위해 특별히 설계되었습니다.마지막 항목을 선택하고 커서 위치 참조를 유지하는 사전 정의된 방법이 있습니다.

myListBox.Items.MoveCurrentToLast();
myListBox.ScrollIntoView(myListBox.Items.CurrentItem);

지금까지 제시된 접근법과는 약간 다르다.

를 사용할 수 있습니다.ScrollViewer ScrollChanged이벤트와 감시 내용ScrollViewer점점 커지고 있습니다.

private void ListBox_OnLoaded(object sender, RoutedEventArgs e)
{
    var listBox = (ListBox) sender;

    var scrollViewer = FindScrollViewer(listBox);

    if (scrollViewer != null)
    {
        scrollViewer.ScrollChanged += (o, args) =>
        {
            if (args.ExtentHeightChange > 0)
                scrollViewer.ScrollToBottom();
        };
    }
}

이것에 의해, 에의 바인딩에 관한 몇개의 문제가 회피됩니다.ListBox ItemsSource변화하는.

ScrollViewer또, 그 전제를 하지 않아도 알 수 있다.ListBox는 기본 제어 템플릿을 사용하고 있습니다.

// Search for ScrollViewer, breadth-first
private static ScrollViewer FindScrollViewer(DependencyObject root)
{
    var queue = new Queue<DependencyObject>(new[] {root});

    do
    {
        var item = queue.Dequeue();

        if (item is ScrollViewer)
            return (ScrollViewer) item;

        for (var i = 0; i < VisualTreeHelper.GetChildrenCount(item); i++)
            queue.Enqueue(VisualTreeHelper.GetChild(item, i));
    } while (queue.Count > 0);

    return null;
}

그런 다음 이것을ListBox Loaded이벤트:

<ListBox Loaded="ListBox_OnLoaded" />

이것은 첨부 속성으로 쉽게 수정할 수 있으며, 이를 보다 일반적인 용도로 사용할 수 있습니다.


또는 야릭의 제안:

<ListBox ScrollViewer.ScrollChanged="ScrollViewer_OnScrollChanged" />

그리고 뒤에 있는 코드:

private void ScrollViewer_OnScrollChanged(object sender, ScrollChangedEventArgs e)
{
    if (e.OriginalSource is ScrollViewer scrollViewer &&
        Math.Abs(e.ExtentHeightChange) > 0.0)
    {
        scrollViewer.ScrollToBottom();
    }
}

listBox.ScrollIntoView(listBox).아이템[리스트 박스]Items. Count - 1 ] ;

여기 있는 어떤 대답도 내가 원하는 것을 해 주지 않았다.그래서 아이템 컨트롤을 자동으로 스크롤하고 사용자가 위로 스크롤하면 자동 스크롤을 일시 중지하고 사용자가 아래로 스크롤하면 자동 스크롤을 재개하는 나만의 동작을 썼습니다.

/// <summary>
/// This will auto scroll a list view to the bottom as items are added.
/// Automatically suspends if the user scrolls up, and recommences when
/// the user scrolls to the end.
/// </summary>
/// <example>
///     <ListView sf:AutoScrollToBottomBehavior="{Binding viewModelAutoScrollFlag}" />
/// </example>
public class AutoScrollToBottomBehavior
{
  /// <summary>
  /// Enumerated type to keep track of the current auto scroll status
  /// </summary>
  public enum StatusType
  {
    NotAutoScrollingToBottom,
    AutoScrollingToBottom,
    AutoScrollingToBottomButSuppressed
  }

  public static StatusType GetAutoScrollToBottomStatus(DependencyObject obj)
  {
    return (StatusType)obj.GetValue(AutoScrollToBottomStatusProperty);
  }

  public static void SetAutoScrollToBottomStatus(DependencyObject obj, StatusType value)
  {
    obj.SetValue(AutoScrollToBottomStatusProperty, value);
  }

  // Using a DependencyProperty as the backing store for AutoScrollToBottomStatus.  This enables animation, styling, binding, etc...
  public static readonly DependencyProperty AutoScrollToBottomStatusProperty =
      DependencyProperty.RegisterAttached(
        "AutoScrollToBottomStatus",
        typeof(StatusType),
        typeof(AutoScrollToBottomBehavior),
        new PropertyMetadata(StatusType.NotAutoScrollingToBottom, (s, e) =>
        {
          if (s is DependencyObject viewer && e.NewValue is StatusType autoScrollToBottomStatus)
          {
            // Set the AutoScrollToBottom property to mirror this one

            bool? autoScrollToBottom = autoScrollToBottomStatus switch
            {
              StatusType.AutoScrollingToBottom => true,
              StatusType.NotAutoScrollingToBottom => false,
              StatusType.AutoScrollingToBottomButSuppressed => false,
              _ => null
            };

            if (autoScrollToBottom.HasValue)
            {
              SetAutoScrollToBottom(viewer, autoScrollToBottom.Value);
            }

            // Only hook/unhook for cases below, not when suspended
            switch(autoScrollToBottomStatus)
            {
              case StatusType.AutoScrollingToBottom:
                HookViewer(viewer);
                break;
              case StatusType.NotAutoScrollingToBottom:
                UnhookViewer(viewer);
                break;
            }
          }
        }));


  public static bool GetAutoScrollToBottom(DependencyObject obj)
  {
    return (bool)obj.GetValue(AutoScrollToBottomProperty);
  }

  public static void SetAutoScrollToBottom(DependencyObject obj, bool value)
  {
    obj.SetValue(AutoScrollToBottomProperty, value);
  }

  // Using a DependencyProperty as the backing store for AutoScrollToBottom.  This enables animation, styling, binding, etc...
  public static readonly DependencyProperty AutoScrollToBottomProperty =
      DependencyProperty.RegisterAttached(
        "AutoScrollToBottom",
        typeof(bool),
        typeof(AutoScrollToBottomBehavior),
        new FrameworkPropertyMetadata(false,  FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, (s, e) =>
        {
          if (s is DependencyObject viewer && e.NewValue is bool autoScrollToBottom)
          {
            // Set the AutoScrollToBottomStatus property to mirror this one
            if (autoScrollToBottom)
            {
              SetAutoScrollToBottomStatus(viewer, StatusType.AutoScrollingToBottom);
            }
            else if (GetAutoScrollToBottomStatus(viewer) == StatusType.AutoScrollingToBottom)
            {
              SetAutoScrollToBottomStatus(viewer, StatusType.NotAutoScrollingToBottom);
            }

            // No change if autoScrollToBottom = false && viewer.AutoScrollToBottomStatus = AutoScrollToBottomStatusType.AutoScrollingToBottomButSuppressed;
          }
        }));


  private static Action GetUnhookAction(DependencyObject obj)
  {
    return (Action)obj.GetValue(UnhookActionProperty);
  }

  private static void SetUnhookAction(DependencyObject obj, Action value)
  {
    obj.SetValue(UnhookActionProperty, value);
  }

  // Using a DependencyProperty as the backing store for MyProperty.  This enables animation, styling, binding, etc...
  private static readonly DependencyProperty UnhookActionProperty =
      DependencyProperty.RegisterAttached("UnhookAction", typeof(Action), typeof(AutoScrollToBottomBehavior), new PropertyMetadata(null));

  private static void ItemsControl_Loaded(object sender, RoutedEventArgs e)
  {
    if (sender is ItemsControl itemsControl)
    {
      itemsControl.Loaded -= ItemsControl_Loaded;
      HookViewer(itemsControl);
    }
  }

  private static void HookViewer(DependencyObject viewer)
  {
    if (viewer is ItemsControl itemsControl)
    {
      // If this is triggered the xaml setup then the control won't be loaded yet,
      // and so won't have a visual tree which we need to get the scrollviewer,
      // so defer this hooking until the items control is loaded.
      if (!itemsControl.IsLoaded)
      {
        itemsControl.Loaded += ItemsControl_Loaded;
        return;
      }

      if (FindScrollViewer(viewer) is ScrollViewer scrollViewer)
      {
        scrollViewer.ScrollToBottom();

        // Scroll to bottom when the item count changes
        NotifyCollectionChangedEventHandler itemsCollectionChangedHandler = (s, e) =>
        {
          if (GetAutoScrollToBottom(viewer))
          {
            scrollViewer.ScrollToBottom();
          }
        };
        ((INotifyCollectionChanged)itemsControl.Items).CollectionChanged += itemsCollectionChangedHandler;


        ScrollChangedEventHandler scrollChangedEventHandler = (s, e) =>
        {
          bool userScrolledToBottom = (e.VerticalOffset + e.ViewportHeight) > (e.ExtentHeight - 1.0);
          bool userScrolledUp = e.VerticalChange < 0;

          // Check if auto scrolling should be suppressed
          if (userScrolledUp && !userScrolledToBottom)
          {
            if (GetAutoScrollToBottomStatus(viewer) == StatusType.AutoScrollingToBottom)
            {
              SetAutoScrollToBottomStatus(viewer, StatusType.AutoScrollingToBottomButSuppressed);
            }
          }

          // Check if auto scrolling should be unsuppressed
          if (userScrolledToBottom)
          {
            if (GetAutoScrollToBottomStatus(viewer) == StatusType.AutoScrollingToBottomButSuppressed)
            {
              SetAutoScrollToBottomStatus(viewer, StatusType.AutoScrollingToBottom);
            }
          }
        };

        scrollViewer.ScrollChanged += scrollChangedEventHandler;

        Action unhookAction = () =>
        {
          ((INotifyCollectionChanged)itemsControl.Items).CollectionChanged -= itemsCollectionChangedHandler;
          scrollViewer.ScrollChanged -= scrollChangedEventHandler;
        };

        SetUnhookAction(viewer, unhookAction);
      }
    }
  }

  /// <summary>
  /// Unsubscribes the event listeners on the ItemsControl and ScrollViewer
  /// </summary>
  /// <param name="viewer"></param>
  private static void UnhookViewer(DependencyObject viewer)
  {
    var unhookAction = GetUnhookAction(viewer);
    SetUnhookAction(viewer, null);
    unhookAction?.Invoke();
  }

  /// <summary>
  /// A recursive function that drills down a visual tree until a ScrollViewer is found.
  /// </summary>
  /// <param name="viewer"></param>
  /// <returns></returns>
  private static ScrollViewer FindScrollViewer(DependencyObject viewer)
  {
    if (viewer is ScrollViewer scrollViewer)
      return scrollViewer;

    return Enumerable.Range(0, VisualTreeHelper.GetChildrenCount(viewer))
      .Select(i => FindScrollViewer(VisualTreeHelper.GetChild(viewer, i)))
      .Where(child => child != null)
      .FirstOrDefault();
  }
}

저에게 가장 간단한 작업 방법은 다음과 같습니다. (바인딩 없음)

 private void WriteMessage(string message, Brush color, ListView lv)
        {

            Dispatcher.BeginInvoke(new Action(delegate
            {
                ListViewItem ls = new ListViewItem
                {
                    Foreground = color,
                    Content = message
                };
                lv.Items.Add(ls);
                lv.ScrollIntoView(lv.Items[lv.Items.Count - 1]);
            }));
        }

클래스를 만들거나 xaml을 변경할 필요가 없습니다.이 방법으로 메시지를 작성하기만 하면 자동으로 스크롤됩니다.

그냥 전화하는 거야

myLv.Items.Add(ls);
myLv.ScrollIntoView(lv.Items[lv.Items.Count - 1]);

예를 들면, 날 위해 일하지 마.

자동 롤링을 실현하는 가장 쉬운 방법은 Collection Changed 이벤트에 후크하는 것입니다.ListBox 컨트롤에서 파생된 커스텀클래스에 이 기능을 추가합니다.

using System.Collections.Specialized;
using System.Windows.Controls;
using System.Windows.Media;

namespace YourProgram.CustomControls
{
  public class AutoScrollListBox : ListBox
  {
      public AutoScrollListBox()
      {
          if (Items != null)
          {
              // Hook to the CollectionChanged event of your ObservableCollection
              ((INotifyCollectionChanged)Items).CollectionChanged += CollectionChange;
          }
      }

      // Is called whenever the item collection changes
      private void CollectionChange(object sender, NotifyCollectionChangedEventArgs e)
      {
          if (Items.Count > 0)
          {
              // Get the ScrollViewer object from the ListBox control
              Border border = (Border)VisualTreeHelper.GetChild(this, 0);
              ScrollViewer SV = (ScrollViewer)VisualTreeHelper.GetChild(border, 0);

              // Scroll to bottom
              SV.ScrollToBottom();
          }
      }
  }
}

커스텀 컨트롤의 네임스페이스를 WPF 창에 추가하고 커스텀 ListBox 컨트롤을 사용합니다.

<Window x:Class="MainWindow"
         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:YourProgram"
         xmlns:cc="clr-namespace:YourProgram.CustomControls"
         mc:Ignorable="d" 
         d:DesignHeight="450" d:DesignWidth="800">
         
    <cc:AutoScrollListBox ItemsSource="{Binding YourObservableCollection}"/>
        
</Window>

ListBox를 사용해 보세요.ScrollIntoView() 메서드입니다만, 경우에 따라서는 문제가 있습니다.

다음은 Tamir Khason의 예입니다.WPF의 ListBox 자동 스크롤

이것은 나에게 100% 효과가 있었던 방법이다.

초기화 부분:

private ObservableCollection<ActionLogData> LogListBind = new ObservableCollection<ActionLogData>();

LogList.ItemsSource = LogListBind;
LogListBind.CollectionChanged += this.OnCollectionChanged;

ListView의 항목 소스로 사용되는 My ObservableCollection의 CollectionChanged에 바인딩된 위임자:

private void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
      if (VisualTreeHelper.GetChildrenCount(LogList) > 0)
      {
           Decorator border = VisualTreeHelper.GetChild(LogList, 0) as Decorator;
           ScrollViewer scrollViewer = border.Child as ScrollViewer;
           scrollViewer.ScrollToBottom();
      }
}

이 솔루션은 @mateusz-my-lak 솔루션을 기반으로 합니다만, 몇개의 수정과 심플화를 실시했습니다.

.NET 5에서 이 답변과 모두의 답변을 조합하여 생각해낸 가장 깔끔한 방법은 다음과 같습니다.

View 생성자에서 이벤트를 구독합니다(뒤 코드).

var listViewItemsSource = (INotifyCollectionChanged)MyListView.Items.SourceCollection;
listViewItemsSource.CollectionChanged += MyListViewCollectionChanged;

★★★★★★★★★★★★★★★★★★★★★★★★에MyListViewCollectionChanged「」, 「」를 합니다.ScrollViewer.

private void MyListViewCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
    var border = (Decorator)VisualTreeHelper.GetChild(LoggerListView, 0);
    var scrollViewer = (ScrollViewer)border.Child;
    scrollViewer.ScrollToEnd();
}

메모: 구성 요소가 초기화되지 않았기 때문에 생성자에서 스크롤 뷰어를 가져올 수 없습니다.

마지막으로 추가된 항목이 목록의 마지막 항목인 경우에만 맨 아래로 스크롤하십시오.

if (lstBox.SelectedIndex == lstBox.Items.Count - 1)
{
    // Scroll to bottom
    lstBox.ScrollIntoView(lstBox.SelectedItem);
}

언급URL : https://stackoverflow.com/questions/2337822/wpf-listbox-scroll-to-end-automatically

반응형