午後わてんのブログ

ベランダ菜園とWindows用アプリ作成(WPFとC#)

WPF、マス目に敷き詰めたThumb、マウスドラッグ移動で入れ替え

2021/03/02追記
コードを書き直した
gogowaten.hatenablog.com

追記ここまで

こういうやつ作りたい

f:id:gogowaten:20210301114146g:plain
こういうの
n x mのマスに左上から右下へ敷き詰めたドラッグ移動できる要素、ドラッグ移動中に他の要素に重なったら入れ替え、入れ替えといっても左上から右下へ並べているので挿入する感じ、うまく表現できんからアニメGIF見たほうが早い
こういうので画像を並べてドラッグ移動で並べ替えできるのを作りたいんだよねえ
今回はそのテストなので、表示する要素はThumbのみ、サイズも100x100固定、並べる個数も3x3の9個に固定


コード

github.com


作成動作環境

動作に必要なのは.NET 5がインストール済みのWindowsで、.NET Frameworkだけでは動かないはず


MainWindow.xaml

<Window x:Class="_20210227_thumb移動入れ替え.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:_20210227_thumb移動入れ替え"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="400">
  <Grid>
    <DockPanel>
      <StatusBar DockPanel.Dock="Bottom">
        <StatusBarItem x:Name="MyStatus" Content="status"/>
      </StatusBar>
      <Canvas x:Name="MyCanvas" UseLayoutRounding="True"/>
    </DockPanel>
  </Grid>
</Window>




MainWindow.xaml.cs

using System.Collections.Generic;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Controls.Primitives;
using System.Collections.ObjectModel;


//左上から右下へ敷き詰められた3x3個のThumb
//マウスドラッグ移動で移動先のThumbと入れ替える感じの動き
//移動先にあるThumbから後ろのThumbすべてを1個づつ右下へ移動させる

namespace _20210227_thumb移動入れ替え
{
    public partial class MainWindow : Window
    {
        private ObservableCollection<FlatThumb> MyThumbs;
        //ドラッグ移動中のThumbのRectと、MyThumbsのIndexに対応するRectとの重なり合う面積保持用
        private double moto重なり面積;
        public MainWindow()
        {
            InitializeComponent();
            MyInitialize();

        }

        //Thumbを9個作成、表示
        private void MyInitialize()
        {
            MyThumbs = new();
            for (int y = 0; y < 3; y++)
            {
                for (int x = 0; x < 3; x++)
                {
                    FlatThumb t = new($"{(y * 3) + x}")
                    {
                        Width = 100,
                        Height = 100,
                        Opacity = 0.7,
                        FontSize = 40
                    };
                    MyCanvas.Children.Add(t);
                    Canvas.SetLeft(t, x * 100);
                    Canvas.SetTop(t, y * 100);
                    t.DragDelta += Thumb_DragDelta;
                    t.DragCompleted += Thumb_DragCompleted;
                    MyThumbs.Add(t);
                }
            }
        }

        //ドラッグ移動終了イベント時、
        //最寄りというか空いている(あるべき)場所に移動させる
        private void Thumb_DragCompleted(object sender, DragCompletedEventArgs e)
        {
            FlatThumb t = sender as FlatThumb;
            int imaIndex = MyThumbs.IndexOf(t);
            int x = imaIndex % 3 * 100;//元のx座標
            int y = imaIndex / 3 * 100;
            Canvas.SetLeft(t, x);
            Canvas.SetTop(t, y);

            MyStatus.Content = "";

        }

        /// <summary>
        /// ドラッグ移動中のThumbとその他のThumbとの重なり合う部分の面積を計算、
        /// 一定以上の面積があった場合、場所を入れ替えて整列
        /// </summary>
        /// <param name="t">ドラッグ移動中のThumb</param>
        private void Idou移動中処理(FlatThumb t)
        {
            t.Opacity = 0.7;
            var p = MyCanvas.PointToScreen(new Point());
            RectangleGeometry geometry = GetThumbGeometry(t, p);
            int imaIndex = MyThumbs.IndexOf(t);//ドラッグ移動中ThumbのIndex
            MyStatus.Content = imaIndex.ToString();

            //重なり面積のリスト作成
            List<double> kasanai面積 = MakeList(geometry, p);

            //面積最大のindex取得、これはIndexになる
            int sakiIndex = 0;
            double max = 0;
            for (int i = 0; i < kasanai面積.Count; i++)
            {
                if (max < kasanai面積[i] && i != imaIndex)
                {
                    max = kasanai面積[i];
                    sakiIndex = i;
                }
            }

            //移動中ThumbのIndexに対応するRectと、移動中ThumbのRect重なり面積
            int x = imaIndex % 3 * 100;//元のx座標
            int y = imaIndex / 3 * 100;
            var idxGeo = new RectangleGeometry(new Rect(new Point(x, y), new Size(100, 100)));
            var ima重なりbound = Geometry.Combine(idxGeo, geometry, GeometryCombineMode.Intersect, null).Bounds;
            var ima重なり面積 = ima重なりbound.Width * ima重なりbound.Height;

            //重なり面積が4000(4割)以上のThumbがある
            //and indexRectとの重なり面積が直前より減っていたら
            //移動中Thumbと入れ替える
            if (max > 4000 && moto重なり面積 > ima重なり面積)
            {
                //Thumbのindexと場所並べ替え
                SortIndexPlace(sakiIndex, imaIndex, t, geometry);
            }
            else
            {
                moto重なり面積 = ima重なり面積;
            }
        }


        /// <summary>
        /// 移動中Thumbとの重なり面積のリスト作成
        /// </summary>
        /// <param name="geometry">移動中ThumbのRectangleGeometry</param>
        /// <param name="p">基準になる座標</param>
        /// <returns></returns>
        private List<double> MakeList(RectangleGeometry geometry, Point p)
        {
            List<double> kasanai面積 = new();
            for (int i = 0; i < MyThumbs.Count; i++)
            {
                Rect bound = Geometry.Combine(
                    geometry,
                    GetThumbGeometry(MyThumbs[i], p),
                    GeometryCombineMode.Intersect, null).Bounds;

                if (bound == Rect.Empty)
                    kasanai面積.Add(0);
                else
                    kasanai面積.Add(bound.Width * bound.Height);
            }
            return kasanai面積;
        }

        /// <summary>
        /// Thumbのindexと場所並べ替え
        /// </summary>
        /// <param name="saki">移動中Thumbの新しいindex</param>
        /// <param name="tID">移動中Thumbのindex</param>
        /// <param name="t">移動中Thumb</param>
        /// <param name="tg">移動中ThumbのRectangleGeometry</param>
        private void SortIndexPlace(int saki, int tID, FlatThumb t, RectangleGeometry tg)
        {
            //今のindexより大きくなる場合は、そのindexより後ろのThumbを並べ替える
            if (saki > tID)
            {
                for (int tt = tID + 1; tt <= saki; tt++)
                {
                    FlatThumb target = MyThumbs[tt];
                    double left = Canvas.GetLeft(target);
                    double top = Canvas.GetTop(target);
                    //左端以外なら
                    if (left != 0)
                    {
                        //左へ移動
                        Canvas.SetLeft(target, left - 100);
                    }
                    else
                    {
                        //一段上の右端へ移動
                        if (top >= 100)
                        {
                            Canvas.SetTop(target, top - 100);
                            Canvas.SetLeft(target, 200);
                        }
                    }
                }

            }
            else
            {
                for (int tt = saki; tt < tID; tt++)
                {
                    FlatThumb target = MyThumbs[tt];
                    double left = Canvas.GetLeft(target);
                    double top = Canvas.GetTop(target);
                    if (left + 100 <= 200)
                    {
                        Canvas.SetLeft(target, left + 100);
                    }
                    else
                    {
                        if (top + 100 <= 200)
                        {
                            Canvas.SetTop(target, top + 100);
                            Canvas.SetLeft(target, 0);
                        }
                    }
                }

            }
            //index変更
            MyThumbs.RemoveAt(tID);
            MyThumbs.Insert(saki, t);

            //移動中Thumbのindexが変更されたので、対応するRectと今のThumbのRectとの重なり面積を再計算
            int x = saki % 3 * 100;
            int y = saki / 3 * 100;
            var imaRect = new RectangleGeometry(new Rect(new Point(x, y), new Size(100, 100)));
            var ima重なりbound = Geometry.Combine(imaRect, tg, GeometryCombineMode.Intersect, null).Bounds;
            moto重なり面積 = ima重なりbound.Width * ima重なりbound.Height;

        }

        //
        /// <summary>
        /// ThumbのRectanleGeometry作成
        /// </summary>
        /// <param name="t">Thumb</param>
        /// <param name="p">Thumbの親パネルの座標</param>
        /// <returns></returns>
        private RectangleGeometry GetThumbGeometry(FlatThumb t, Point p)
        {
            //親パネル上での座標なので、Thumbと親パネル座標で引き算Subtract
            return new(new Rect((Point)Point.Subtract(t.PointToScreen(new Point()), p), new Size(100, 100)));
        }

        //ドラッグ移動イベント時
        private void Thumb_DragDelta(object sender, DragDeltaEventArgs e)
        {
            //移動
            FlatThumb t = sender as FlatThumb;
            Canvas.SetLeft(t, Canvas.GetLeft(t) + e.HorizontalChange);
            Canvas.SetTop(t, Canvas.GetTop(t) + e.VerticalChange);
            t.Opacity = 0.5;

            Idou移動中処理(t);
        }
    }

    public class FlatThumb : Thumb
    {
        private Grid MyPanel;

        public FlatThumb(string text)
        {
            ControlTemplate template = new(typeof(Thumb));
            template.VisualTree = new FrameworkElementFactory(typeof(Grid), "panel");
            this.Template = template;
            this.ApplyTemplate();
            MyPanel = (Grid)template.FindName("panel", this);
            MyPanel.Background = Brushes.Aqua;

            MyPanel.Children.Add(new TextBlock()
            {
                Text = text,
                TextAlignment = TextAlignment.Center,
                VerticalAlignment = VerticalAlignment.Center
            });
            MyPanel.Children.Add(new Border()
            {
                BorderBrush = Brushes.MediumBlue,
                BorderThickness = new Thickness(1)
            });
        }
    }
}




並べる順番、入れ替えルール

並べる順番は左上から右下へなので
0 ,1, 2
3, 4, 5
6, 7, 8
こうで、4を1に持っていった場合は
0 ,4, 1
2, 3, 5
6, 7, 8
こう4を1の場所に挿入するので 2, 3の場所も変化する


どれくらい重なったら入れ替えするのか

f:id:gogowaten:20210301134330p:plain
ここから4番をドラッグ移動して

f:id:gogowaten:20210301134353p:plain
これくらい移動したところで5番と入れ替えたい

入れ替え対象を決める

  • 4番との重なっている部分の面積が大きいもの
  • その面積が一定(4割)以上になったら入れ替える

って今回は決めた、面積じゃなくて距離にすればもっと良かったかもしれないけど、今回は面積
で、その閾値は4割って決めたのは雰囲気
Thumbのサイズは100x100って決めてあるので、その4割は100x100x0.4=4000

f:id:gogowaten:20210301135302p:plain
図では4箇所だけの計算だけど、実際のコードではすべてのThumbとの重なり面積を計算している

f:id:gogowaten:20210301135531p:plain
5番との重なり面積bが4000を超えたら入れ替え処理してこうなる


入れ替え直後の問題

f:id:gogowaten:20210301135854p:plain
場合によっては入れ替え処理直後に、入れ替え先でも重なり面積が4割を超えることがある、そうすると入れ替え処理の無限ループみたいになるので

f:id:gogowaten:20210301140331p:plain
移動中は常に元の位置のRectとの重なり面積を計算して、直前より減っていた場合だけ入れ替え処理することにした
そうすると上の図の場合はマウスは右に動いているはずだから、xよりx'のほうが面積が大きいはずなので、たとえaの面積が4割を超えても入れ替えは発生しないので、無限ループにはならなくて済む

xの面積は移動中のThumbのRectと元の位置のRectが必要で、元の位置Rectは

  • Thumbはリスト(Collection)に入れて管理
  • そのindexを表示場所を決定することにしておく
  • 入れ替え処理時にリストの中での位置(index)も入れ替えする

これでindexから計算できるけど、もっとマシな方法がありそう
重なり面積は直前との比較が必要なので、直前の値を保持する変数を用意しておいて
f:id:gogowaten:20210301142324p:plain
19行目がそれで、Thumbリストは17行目

ドラッグ移動中に入れ替えするかの判定処理部分
f:id:gogowaten:20210301142640p:plain
なんかなあ

感想

期待通りの動きができたときは、できたー!ってなるけど、こう今になってブログに書いて見直してみるとなんかいまいちだなあって部分があって、書き直したくなるけど、そうしてると終わんなくなるんだよねえ、諦めが大切、動けば正義




関連記事
次回は明日

gogowaten.hatenablog.com 今回のを書き直した

前回のWPF記事は2日前

gogowaten.hatenablog.com


gogowaten.hatenablog.com