8864


指定したIDを持つControlをScrollViewerの先頭にピン止めしたいのですが、どのような方法を取れば対象となるUIElementの表示位置を固定できるでしょうか？

動きの例としては、以下の様なイメージです。

  1. `ItemsSource`に`ObservableCollection<int>`をBindingした`ListView`に、1～1000の数値が`TextBlock`で縦方向のリストとして表示されている
  2. 1～1000が表示された`TextBlock`は、一定時間ごとにランダムに順番が入れ替わる
  3. ユーザーは特定の番号を`TextBox`への入力などで指定すると、その番号の表示された`TextBlock`がぴったり`ListView`の一番上に表示され、その番号の順番が入れ替わっても、その状況が保持されつづける(=自動的に適切な位置にスクロールする)

`ListView.ItemContainerGenerator.ContainerFromItem(...)`で取得した`Visual`を`Visual.PointToScreen(...)`で変換して、その位置に`ScrollViewer.ScrollToVerticalOffset(...)`をすれば…  
という事を考えたのですが、順番入れ替えを検知する方法が`INotifyCollectionChanged`しか見つからず、`CollectionChanged`イベントを起点にUIElementを取ろうとすると`VisualTree`の作成途中なのか、上手くコントロールを取得できませんでした。

このような場合に上手く機能を実現するには、どのような方法があるでしょうか？


CC BY-SA 3.0
2015-04-06T14:14:28.560
8831
2015-04-10T03:35:03.070
6119
question
c# .net wpf
ScrollViewer内の特定のControlを画面上に固定する方法


処理対象のイベントは`CollectionChanged`で良いと思います。

イベント発生時に要素が仮想化されていて生成されていないのであれば、一度強制的に要素を生成させてから質問文の方法でスクロールさせるか、位置を`ItemContainerGenerator.ContainerFromItem`に頼らずに計算するかのどちらかではないでしょうか。  
前者の方法としては

  1. `ListView`から`ListBox`や`DataGrid`に変更して[ScrollIntoView](https://msdn.microsoft.com/ja-jp/library/system.windows.controls.listbox.scrollintoview.aspx)メソッドを使用する。
  2. 内部の`VirtualizingPanel`を取得し、[BringIndexIntoViewPublic](https://msdn.microsoft.com/ja-jp/library/system.windows.controls.virtualizingpanel.bringindexintoviewpublic%28v=vs.110%29.aspx)メソッドを使用する。(.NET Framework 4.5以降)
  3. `protected`メソッドである`VirtualizingPanel.BringIndexIntoView`をリフレクションで呼び出す。

などが考えられます。

また`ItemsSource`のイベントを直接拾うのであれば`Loaded`イベントの発生前に処理を実行しないような留意も必要かと思います。


CC BY-SA 3.0
2015-04-06T14:57:22.253
8835
2015-04-06T21:42:40.680
2015-04-06T21:42:40.680
5750
5750
8831
answer


`VirtualizingStackPanel`を取り出して、[SetVerticalOffset](https://msdn.microsoft.com/ja-
jp/library/system.windows.controls.virtualizingstackpanel.setverticaloffset.aspx)を使ってスクロールできます。`StackPanel`の場合でも同名のメソッドがあります  
`VirtualizingPanel.ScrollUnit`をPixelにしている場合は対応できませんが、この質問の目的であればItemで十分でしょう。

また、表示したい項目がリストの下位にあると最上部にスクロールは不可能なので、`CompositeCollection`でダミーの項目を追加しておくことで対処します。

[code]

    <DockPanel>
        <TextBox DockPanel.Dock="Top" x:Name="textBox" TextChanged="textBox_TextChanged"/>
        <ListView   x:Name="listView" ItemsSource="{Binding}">          
            <ListView.View>
                <GridView >
                    <GridViewColumn Header="ID" DisplayMemberBinding="{Binding ID}" Width="50"/>
                </GridView>
            </ListView.View>
        </ListView>          
    </DockPanel>
    
[/code]

* * *
[code]

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Windows;
    using System.Windows.Controls;
    using System.Windows.Data;
    using System.Windows.Documents;
    using System.Windows.Media;
    using System.Collections.ObjectModel;
    namespace WpfApplication1
    {
    
        public partial class MainWindow : Window
        {
            private System.Windows.Threading.DispatcherTimer timer;
            private ObservableCollection<Item> items;
            private VirtualizingStackPanel virtualPanel;
    
            public MainWindow()
            {
                InitializeComponent();
    
                this.Loaded += (s, e) =>
                    {
                        //スクロールさせるパネルを取得
                        virtualPanel = FindChild<VirtualizingStackPanel>(this.listView);
    
                        //一番上にという条件のために下部にダミーの要素を追加する
                        //ダミー追加はCompositeCollectionで合成する
                        CollectionContainer c1 = new CollectionContainer();
                        BindingOperations.SetBinding(c1, CollectionContainer.CollectionProperty, new Binding("DataContext") { Source = listView });
    
                        CollectionContainer c2 = new CollectionContainer();
                        c2.Collection = new Item[100];//適当な数
    
                        CompositeCollection cmp = new CompositeCollection();
                        cmp.Add(c1);
                        cmp.Add(c2);
    
                        this.listView.ItemsSource = cmp;
    
                        timer = new System.Windows.Threading.DispatcherTimer();
                        timer.Interval = TimeSpan.FromSeconds(1);
                        timer.Tick += (x, y) =>
                        {
                            //ランダムソート
                            List<Item> buff = this.items.ToList();
                            Random rnd = new Random();
                            this.items.Clear();
                            foreach (Item i in buff.OrderBy(_ => rnd.Next())) { items.Add(i); }
                        };
                        timer.Start();
    
                        this.textBox.Text = "10";
                    };
    
                items = Item.Create(1000);
                items.CollectionChanged += items_CollectionChanged;
    
                this.DataContext = items;
            }
    
            void items_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
            {
                Scroll();
            }
            private void textBox_TextChanged(object sender, TextChangedEventArgs e)
            {
                Scroll();
            }
            private void Scroll()
            {
                int id; if (!int.TryParse(this.textBox.Text, out id)) { return; }
    
                if (virtualPanel != null && virtualPanel.CanVerticallyScroll)
                {
                    for (int i = 0; i < items.Count; i++)
                    {
                        if (items[i].ID == id)
                        {
                            virtualPanel.SetVerticalOffset(i);
                            break;
                        }
                    }
                }
            }
    
            private T FindChild<T>(DependencyObject d) where T : DependencyObject
            {
                if (d is T) { return (T)d; }
                int count = VisualTreeHelper.GetChildrenCount(d);
                for (int i = 0; i < count; i++)
                {
                    var si = FindChild<T>(VisualTreeHelper.GetChild(d, i));
                    if (si != null) { return si; }
                }
                return null;
            }
        }
    
        class Item
        {
            public int ID { get; set; }
    
            public static ObservableCollection<Item> Create(int count)
            {
                ObservableCollection<Item> retval = new ObservableCollection<Item>();
    
                for (int i = 1; i <= count; i++)
                {
                    Item item = new Item() { ID = i };
                    retval.Add(item);
                }
                return retval;
            }
        }
    }
    
[/code]


CC BY-SA 3.0
2015-04-07T09:38:22.387
8864
2015-04-10T03:35:03.070
2015-04-10T03:35:03.070
4926
4926
8831
answer