ラベル WPF の投稿を表示しています。 すべての投稿を表示
ラベル WPF の投稿を表示しています。 すべての投稿を表示

2014年9月11日木曜日

[WPF][Silverlight][C#] ScrollViewer でマウスでの横スクロールと慣性スクロール

将来的に横スクロールにしたいという話があって、事前調査してみました。
やりたいことは下記3つ。
  1. マウスホイールで横スクロール
  2. ドラッグで横スクロール
  3. 慣性スクロール
あんまり情報ないですねー。とりあえずざくっとと作ってみたので、コードだけ紹介します。WPF版では ScrollViewer のカスタムコントロールで、Silverlight では ScrollViewer  を継承できないので Behavior で作りました。ほぼ同じようなコードになってます。加速度までは考慮していないのであしからず。

WPF版(Custom Control)
    /// <summary>
    /// 慣性スクロールビューア
    /// </summary>
    public class MomentumScrollViewer : ScrollViewer
    {
        #region インスタンスフィールド
        /// <summary>
        /// マウスオフセット位置
        /// </summary>
        private double mouseOffset;
        /// <summary>
        /// 開始位置
        /// </summary>
        private double startOffset;
        /// <summary>
        /// プレス有無
        /// </summary>
        private bool isPressed = false;
        /// <summary>
        /// 最終位置
        /// </summary>
        private double lastPosition;
        /// <summary>
        /// 移動量
        /// </summary>
        private double momentumValue;
        #endregion
        #region 依存プロパティ
        /// <summary>
        /// 運動量を取得または設定します。
        /// </summary>
        public double MomentumValue
        {
            get { return (double)GetValue(MomentumValueProperty); }
            set { SetValue(MomentumValueProperty, value); }
        }
        /// <summary>
        /// 運動量依存関係プロパティを識別します。
        /// </summary>
        public static readonly DependencyProperty MomentumValueProperty =
            DependencyProperty.Register("MomentumValue", typeof(double), typeof(MomentumScrollViewer), new PropertyMetadata(0.5));
        /// <summary>
        /// 水平位置を取得または設定します。
        /// </summary>
        public double HorizontalPosition
        {
            get { return (double)GetValue(HorizontalPositionProperty); }
            set { SetValue(HorizontalPositionProperty, value); }
        }
        /// <summary>
        /// 水平位置依存関係プロパティを識別します。
        /// </summary>
        public static readonly DependencyProperty HorizontalPositionProperty =
            DependencyProperty.Register("HorizontalPosition", typeof(double), typeof(MomentumScrollViewer), new FrameworkPropertyMetadata(0.0, new PropertyChangedCallback(OnHorizontalPositionChanged)));
        #endregion
        #region コンストラクタ
        /// <summary>
        /// コンストラクタ
        /// </summary>
        public MomentumScrollViewer()
        {
            // 各イベント登録
            this.PreviewMouseWheel += MomentumScrollViewer_PreviewMouseWheel;
            this.Loaded += (sender, e) =>
                {
                    var content = this.GetValue(ScrollViewer.ContentProperty) as FrameworkElement;
                    if (content != null)
                    {
                        content.MouseLeftButtonDown += content_MouseLeftButtonDown;
                        content.MouseLeftButtonUp += content_MouseLeftButtonUp;
                        content.MouseMove += content_MouseMove;
                        content.MouseLeave += content_MouseLeave;
                    }
                };
        }
        #endregion
        #region イベント
        /// <summary>
        /// 水平スクロール位置変更イベント
        /// </summary>
        /// <param name="obj"></param>
        /// <param name="e"></param>
        private static void OnHorizontalPositionChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
        {
            var scrollViewer = obj as MomentumScrollViewer;
            if (scrollViewer != null)
            {
                scrollViewer.ScrollToHorizontalOffset((double)e.NewValue);
            }
        }
        #region ホイールでのスクロール
        /// <summary>
        /// マウスホイールイベント
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void MomentumScrollViewer_PreviewMouseWheel(object sender, MouseWheelEventArgs e)
        {
            beginScroll(e.Delta);
            e.Handled = true;
        }
        #endregion
        #region ドラッグによるスクロール
        /// <summary>
        /// 左ボタンダウンイベント
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        void content_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
        {
            var position = e.GetPosition(this);
            this.mouseOffset = position.X;
            this.startOffset = this.HorizontalOffset;
            this.isPressed = true;
        }
        /// <summary>
        /// マウス移動イベント
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void content_MouseMove(object sender, MouseEventArgs e)
        {
            if (this.isPressed)
            {
                var position = e.GetPosition(this);
                // 前回値からの運動量を保持し、前回値を更新
                momentumValue = lastPosition - position.X;
                lastPosition = position.X;
                // 変化量を算出し、スクロール
                var delta = (position.X > mouseOffset) ? -(position.X - mouseOffset) : mouseOffset - position.X;
                this.ScrollToHorizontalOffset(startOffset + delta);
            }
        }
        /// <summary>
        /// マウスリーブイベント
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void content_MouseLeave(object sender, MouseEventArgs e)
        {
            dragEnd();
        }
        /// <summary>
        /// 左ボタンアップイベント
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        void content_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
        {
            dragEnd();
        }
        #endregion
        #endregion
        #region プライベートメソッド
        /// <summary>
        /// ドラッグ終了処理
        /// </summary>
        private void dragEnd()
        {
            if (this.isPressed)
            {
                this.isPressed = false;
                // 運動量がある場合は慣性スクロール
                if (momentumValue != 0)
                {
                    beginScroll(momentumValue * (-1));
                }
            }
        }

        /// <summary>
        /// 慣性スクロール開始
        /// </summary>
        /// <param name="delta">変化量</param>
        private void beginScroll(double delta)
        {
            // 目的地設定
            var to = HorizontalOffset - delta * MomentumValue;
            if (to < 0)
                to = 0;
            if (to > ExtentWidth)
                to = ExtentWidth;
            // 慣性スクロールアニメーション開始
            var animation = new DoubleAnimation(to, new Duration(TimeSpan.FromMilliseconds(1000)));
            animation.EasingFunction = new CircleEase() { EasingMode = EasingMode.EaseOut };
            this.BeginAnimation(MomentumScrollViewer.HorizontalPositionProperty, animation);
        }
        #endregion
    }


Sivlerlight版(Behavior)
    /// <summary>
    /// 慣性スクロールビヘイビア
    /// </summary>
    public class MomentumScrollingBehavior : Behavior<ScrollViewer>
    {
        #region インスタンスフィールド
        /// <summary>
        /// マウスオフセット位置
        /// </summary>
        private double mouseOffset;
        /// <summary>
        /// 開始位置
        /// </summary>
        private double startOffset;
        /// <summary>
        /// プレス有無
        /// </summary>
        private bool isPressed = false;
        /// <summary>
        /// 最終位置
        /// </summary>
        private double lastPosition;
        /// <summary>
        /// 移動量
        /// </summary>
        private double momentumValue;
        #endregion
        #region 依存プロパティ
        /// <summary>
        /// 運動量を取得または設定します。
        /// </summary>
        public double MomentumValue
        {
            get { return (double)GetValue(MomentumValueProperty); }
            set { SetValue(MomentumValueProperty, value); }
        }
        /// <summary>
        /// 運動量依存関係プロパティを識別します。
        /// </summary>
        public static readonly DependencyProperty MomentumValueProperty =
            DependencyProperty.Register("MomentumValue", typeof(double), typeof(MomentumScrollingBehavior), new PropertyMetadata(0.5));
        /// <summary>
        /// 水平位置を取得または設定します。
        /// </summary>
        public double HorizontalPosition
        {
            get { return (double)GetValue(HorizontalPositionProperty); }
            set { SetValue(HorizontalPositionProperty, value); }
        }
        /// <summary>
        /// 水平位置依存関係プロパティを識別します。
        /// </summary>
        public static readonly DependencyProperty HorizontalPositionProperty =
            DependencyProperty.Register("HorizontalPosition", typeof(double), typeof(MomentumScrollingBehavior), new PropertyMetadata(0.0, new PropertyChangedCallback(OnHorizontalPositionChanged)));
        #endregion

        #region イベント
        /// <summary>
        /// アタッチ完了
        /// </summary>
        protected override void OnAttached()
        {
            base.OnAttached();
            this.AssociatedObject.MouseWheel += scrollViewer_MouseWheel;
            this.AssociatedObject.Loaded += (sender, e) =>
            {
                var content = this.AssociatedObject.GetValue(ScrollViewer.ContentProperty) as FrameworkElement;
                if (content != null)
                {
                    content.MouseLeftButtonDown += content_MouseLeftButtonDown;
                    content.MouseLeftButtonUp += content_MouseLeftButtonUp;
                    content.MouseMove += content_MouseMove;
                    content.MouseLeave += content_MouseLeave;
                }
            };
        }
        /// <summary>
        /// デタッチ
        /// </summary>
        protected override void OnDetaching()
        {
            this.AssociatedObject.MouseWheel -= scrollViewer_MouseWheel;
            var content = this.AssociatedObject.GetValue(ScrollViewer.ContentProperty) as FrameworkElement;
            if (content != null)
            {
                content.MouseLeftButtonDown -= content_MouseLeftButtonDown;
                content.MouseLeftButtonUp -= content_MouseLeftButtonUp;
                content.MouseMove -= content_MouseMove;
                content.MouseLeave -= content_MouseLeave;
            }
            base.OnDetaching();
        }
        /// <summary>
        /// 水平スクロール位置変更イベント
        /// </summary>
        /// <param name="obj"></param>
        /// <param name="e"></param>
        private static void OnHorizontalPositionChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
        {
            var behavior = obj as MomentumScrollingBehavior;
            if (behavior != null)
            {
                behavior.AssociatedObject.ScrollToHorizontalOffset((double)e.NewValue);
            }
        }
        /// <summary>
        /// マウスホイールイベント
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void scrollViewer_MouseWheel(object sender, MouseWheelEventArgs e)
        {
            beginScroll(e.Delta);
            e.Handled = true;
        }
        #region ドラッグによるスクロール
        /// <summary>
        /// 左ボタンダウンイベント
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        void content_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
        {
            var position = e.GetPosition(this.AssociatedObject);
            this.mouseOffset = position.X;
            this.startOffset = this.AssociatedObject.HorizontalOffset;
            this.isPressed = true;
        }
        /// <summary>
        /// マウス移動イベント
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void content_MouseMove(object sender, MouseEventArgs e)
        {
            if (this.isPressed)
            {
                var position = e.GetPosition(this.AssociatedObject);
                // 前回値からの運動量を保持し、前回値を更新
                momentumValue = lastPosition - position.X;
                lastPosition = position.X;
                // 変化量を算出し、スクロール
                var delta = (position.X > mouseOffset) ? -(position.X - mouseOffset) : mouseOffset - position.X;
                this.AssociatedObject.ScrollToHorizontalOffset(startOffset + delta);
            }
        }
        /// <summary>
        /// マウスリーブイベント
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void content_MouseLeave(object sender, MouseEventArgs e)
        {
            dragEnd();
        }
        /// <summary>
        /// 左ボタンアップイベント
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        void content_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
        {
            dragEnd();
        }
        #endregion
        #endregion
        #region プライベートメソッド
        /// <summary>
        /// ドラッグ終了処理
        /// </summary>
        private void dragEnd()
        {
            if (this.isPressed)
            {
                this.isPressed = false;
                // 運動量がある場合は慣性スクロール
                if (momentumValue != 0)
                {
                    beginScroll(momentumValue * (-1));
                }
            }
        }

        /// <summary>
        /// 慣性スクロール開始
        /// </summary>
        /// <param name="delta">変化量</param>
        private void beginScroll(double delta)
        {
            // 目的地設定
            var to = this.AssociatedObject.HorizontalOffset - delta * MomentumValue;
            if (to < 0)
                to = 0;
            if (to > this.AssociatedObject.ExtentWidth)
                to = this.AssociatedObject.ExtentWidth;
            // 慣性スクロールアニメーション開始
            var storyboard = new Storyboard();
            storyboard.Children.Add(new DoubleAnimation()
            {
                From = this.AssociatedObject.HorizontalOffset,
                To = to,
                Duration = new Duration(TimeSpan.FromMilliseconds(1000)),
                EasingFunction = new CircleEase() { EasingMode = EasingMode.EaseOut }
            });
            Storyboard.SetTarget(storyboard, this);
            Storyboard.SetTargetProperty(storyboard, new PropertyPath("HorizontalPosition"));
            storyboard.Begin();
        }
        #endregion
    }










2014年7月31日木曜日

[WPF][Silverlight][C#]MVVM パターンで TextBox をフォーカス

FocusManager を使えばフォーカス設定できますが、TextBox の IsEnabled が false だったり、複数のFocusManager の設定があると、意図した動きをしてくれなくなります。

如何にすればシンプルに出来るか考えてみましたが、Behavior 使うのが一番シンプルに収まりそうです。フォーカス設定のみだと寂しいので、文字列全選択も書きました。



    public class TextBoxBehavior : Behavior<TextBox>
    {
        /// <summary>
        /// フォーカス設定
        /// </summary>
        public void Focus()
        {
            this.AssociatedObject.Focus();
        }

        /// <summary>
        /// 全選択
        /// </summary>
        public void SelectAll()
        {
            this.AssociatedObject.SelectAll();
        }
    }

後は、XAML側でボタンクリックされた時などに、CallMethodAction を書くだけでOKです。

<Window x:Class="Sample.TestWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:ei="http://schemas.microsoft.com/expression/2010/interactions"
        xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity" 
        xmlns:local="clr-namespace:Sample">
    <StackPanel>
        <Button Content="Button" HorizontalAlignment="Left" Width="75">
            <i:Interaction.Triggers>
                <i:EventTrigger EventName="Click">
                    <ei:CallMethodAction TargetObject="{Binding ElementName=textBoxBehavior}" MethodName="Focus" />
                    <ei:CallMethodAction TargetObject="{Binding ElementName=textBoxBehavior}" MethodName="SelectAll" />
                </i:EventTrigger>
            </i:Interaction.Triggers>
        </Button>

        <TextBox HorizontalAlignment="Left" Height="23" Text="TextBox" Width="120">
            <i:Interaction.Behaviors>
                <local:TextBoxBehavior x:Name="textBoxBehavior" />
            </i:Interaction.Behaviors>
        </TextBox>
    </StackPanel>
</Window>





[WPF]MVVM パターンで DialogResult を変更する

View のみで変更するパターンと、ViewModel からView に通知して閉じるパターンを書きます。後者だと ViewModel の処理結果で閉じないようなケースにも対応出来ますねー。


<Window x:Class="Sample.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:ei="http://schemas.microsoft.com/expression/2010/interactions"
        xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity" 
        xmlns:local="clr-namespace:Sample"
        Height="350" Width="525"
        Name="dialog"
        >
    <Window.DataContext>
        <local:ViewModel x:Name="viewModel" />
    </Window.DataContext>
    <Grid>

        <StackPanel Orientation="Horizontal" HorizontalAlignment="Right" VerticalAlignment="Bottom" >
            <Button Content="From View" Margin="5" >
                <i:Interaction.Triggers>
                    <i:EventTrigger EventName="Click">
                        <ei:ChangePropertyAction TargetObject="{Binding ElementName=dialog}" PropertyName="DialogResult" Value="True" />
                    </i:EventTrigger>
                </i:Interaction.Triggers>
            </Button>

            <Button Content="From ViewModel" Margin="5" >
                <i:Interaction.Triggers>
                    <i:EventTrigger EventName="Click">
                        <ei:CallMethodAction TargetObject="{Binding ElementName=viewModel}" MethodName="Execute" />
                    </i:EventTrigger>
                    <ei:DataTrigger Binding="{Binding Path=CloseFlag}" Value="True">
                        <ei:ChangePropertyAction TargetObject="{Binding ElementName=dialog}" PropertyName="DialogResult" Value="True" />
                    </ei:DataTrigger>
                </i:Interaction.Triggers>
            </Button>
        </StackPanel>
    </Grid>
</Window>

    public class ViewModel : INotifyPropertyChanged
    {
        /// <summary>
        /// プロパティ変更イベントハンドラ
        /// </summary>
        public event PropertyChangedEventHandler PropertyChanged;

        /// <summary>
        /// プロパティ変更処理
        /// </summary>
        /// <param name="name">プロパティ名</param>
        public void OnPropertyChanged(string name)
        {
            if (PropertyChanged != null)
            {
                PropertyChanged(this, new PropertyChangedEventArgs(name));
            }
        }

        /// <summary>
        /// 終了フラグ
        /// </summary>
        private bool closeFlag = false;
        /// <summary>
        /// 終了フラグを取得または設定します。
        /// </summary>
        public bool CloseFlag
        {
            get { return closeFlag; }
            set { closeFlag = value; OnPropertyChanged("CloseFlag"); }
        }

        /// <summary>
        /// 実行
        /// </summary>
        public void Execute()
        {
            // execute process

            // Close
            CloseFlag = true;
        }
    }









2014年4月24日木曜日

[WPF][C#]XMLデータをTreeViewで編集

XML データを TreeView に表示できるところまでやりました。次は編集ですね。

編集画面追加
TreeView 上で編集するのは難しそうなので、編集エリアを設けて選択した要素を操作するのがよさそうです(MVVMパターンとかバインドとか面倒だったので、そちらは割愛させて頂きます。)。そして、ごめんなさい、色々と面倒くさくなってきたので、一気に貼り付けます。


[XAML]
    <Window.Resources>
        <Style TargetType="{x:Type TreeViewItem}">
            <Setter Property="IsExpanded" Value="True" />
        </Style>
        <HierarchicalDataTemplate x:Key="XmlTemplate" ItemsSource="{Binding Path=Elements}">
            <StackPanel Orientation="Horizontal">
                <TextBlock Name="nameTextBlock" Text="{Binding Path=Name.LocalName}"/>
                <TextBlock Name="valueTextBlock" FontWeight="Bold" Margin="5,0" />
            </StackPanel>
            <HierarchicalDataTemplate.Triggers>
                <DataTrigger Binding="{Binding Path=HasElements}" Value="False">
                    <Setter TargetName="valueTextBlock" Property="Text" Value="{Binding Path=Value}" />
                </DataTrigger>
            </HierarchicalDataTemplate.Triggers>
        </HierarchicalDataTemplate>
    </Window.Resources>
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="300"/>
            <ColumnDefinition/>
        </Grid.ColumnDefinitions>

        <!--表示エリア-->
        <TreeView Name="treeView" Margin="5"
                  ItemTemplate= "{StaticResource XmlTemplate}"
                  SelectedItemChanged="treeView_SelectedItemChanged" 
                  >
        </TreeView>
        
        <!--編集エリア-->
        <StackPanel Grid.Column="1" >
            <GroupBox Header="要素名" Margin="5">
                <StackPanel Orientation="Horizontal" Margin="0,5">
                    <TextBox Name="nameTextBox" Text="{Binding Path=Name.LocalName}" Width="80" />
                    <Button Content="追加" Name="addElementButton" Click="addElementButton_Click"  Width="50" Margin="5,0,0,0"/>
                    <Button Content="更新" Name="updateNameButton" Click="updateNameButton_Click"  Width="50" Margin="5,0,0,0"/>
                    <Button Content="削除" Name="delElementButton" Click="delElementButton_Click"  Width="50" Margin="5,0,0,0"/>
                </StackPanel>
            </GroupBox>

            <GroupBox Header="" Margin="5" Name="valueGroup">
                <StackPanel Orientation="Horizontal" Margin="0,5">
                    <TextBox Name="valueTextBox" Text="{Binding Path=Value}" Width="80" />
                    <Button Content="更新" Name="updateValueButton" Click="updateValueButton_Click"  Width="50" Margin="5,0,0,0"/>
                </StackPanel>
            </GroupBox>
        </StackPanel>

    </Grid>


[Code]
        /// <summary>
        /// 要素選択変更イベント
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void treeView_SelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
        {
            var element = e.NewValue as XElement;
            if (element != null)
            {
                nameTextBox.Text = element.Name.LocalName;
                if (!element.HasElements)
                {
                    // 子要素が無ければ値を編集できるようにする
                    valueTextBox.Text = element.Value;
                    valueGroup.Header = element.Name.LocalName;
                    valueGroup.IsEnabled = true;
                }
                else
                {
                    valueTextBox.Text = "";
                    valueGroup.Header = "";
                    valueGroup.IsEnabled = false;
                }
            }
        }

        /// <summary>
        /// 要素追加ボタン実行
        /// </summary>
        /// <remarks>
        /// 選択中要素に子要素を追加する
        /// </remarks>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void addElementButton_Click(object sender, RoutedEventArgs e)
        {
            var element = treeView.SelectedItem as XElement;
            if (!string.IsNullOrEmpty(nameTextBox.Text) && element != null)
            {
                XNamespace x = element.Name.Namespace;
                var newElement = new XElement(x + nameTextBox.Text) { Value = valueTextBox.Text };
                element.Add(newElement);
            }
        }

        /// <summary>
        /// 要素更新ボタン実行
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void updateNameButton_Click(object sender, RoutedEventArgs e)
        {
            var element = treeView.SelectedItem as XElement;
            if (!string.IsNullOrEmpty(nameTextBox.Text) && element != null)
            {
                XNamespace x = element.Name.Namespace;
                element.Name = x + nameTextBox.Text;

                // ①子要素の有無をチェックしないと消えてしまう
                element.Value = valueTextBox.Text;

                // ②選択している要素が解除されてしまう
                treeView.Items.Refresh();
            }
        }

        /// <summary>
        /// 要素削除ボタン実行
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void delElementButton_Click(object sender, RoutedEventArgs e)
        {
            var element = treeView.SelectedItem as XElement;
            if (element != null)
            {
                element.Remove();
            }
        }

        /// <summary>
        /// 値更新ボタン実行
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void updateValueButton_Click(object sender, RoutedEventArgs e)
        {
            var element = treeView.SelectedItem as XElement;
            if (!string.IsNullOrEmpty(nameTextBox.Text) && element != null)
            {
                element.Value = valueTextBox.Text;
            }
        }


残件
残件とか課題としては、MVVM化とかコメントの①②があります。あと属性の編集もですかね。これは要素選択変更イベントで、属性があれば、名称と値のエンティティのリストを作成し、DataGridとかにバインドさせて編集後、更新ボタンとかで要素の属性情報を全て更新すればいいかな。