@dhq_boiler

Are you sure you want to delete the question?

If your question is resolved, you may close it.

Leaving a resolved question undeleted may help others!

We hope you find it useful!

【WPF】クリッピングがうまくいかない

お世話になっております。

解決したいこと

C# + WPFでベクターグラフィックスドローイングツールを開発しています。

2021-07-09.png

※下にソースコードへの案内を記載しております。よろしければそちらを参照ください。

発生している問題・エラー

現在、画像のクリッピング機能を実装しているのですが、うまくいきません。
画像のクリッピング機能は画像に対して任意の図形(例えば矩形)を当てると、その領域が画像からくり抜かれて表示されるというものです。Adobe Illustratorでいうと、クリッピングマスク機能です。
どのように実装されているかというと、PictureDesignerItemViewModelのClipプロパティにPathGeometryインスタンスを設定しています。ClipプロパティはImage.Clipにバインディングされています。

DiagramViewModel.cs
    public class DiagramViewModel : BindableBase, IDiagramViewModel, IDisposable
    {
        :
        public DelegateCommand ClipCommand { get; private set; }
        :
        public DiagramViewModel()
        {
            :
            ClipCommand = new DelegateCommand(() => ExecuteClipCommand(), () => CanExecuteClip());
            :
        }


        private void ExecuteClipCommand()
        {
            var picture = SelectedItems.OfType<PictureDesignerItemViewModel>().First();
            var other = SelectedItems.OfType<DesignerItemViewModelBase>().Last();
            var pathGeometry = other.PathGeometry.Value;
            //pathGeometry.Transform = new TranslateTransform(other.Left.Value - picture.Left.Value, other.Top.Value - picture.Top.Value);
            picture.Clip.Value = pathGeometry;
            Items.Remove(other);
        }

        private bool CanExecuteClip()
        {
            return SelectedItems.Count == 2 &&
                   SelectedItems.First().GetType() == typeof(PictureDesignerItemViewModel);
        }
        :
    }
PictureDesignerItemDataTemplate.xaml
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                    xmlns:viewModel="clr-namespace:boilersGraphics.ViewModels">
    <DataTemplate DataType="{x:Type viewModel:PictureDesignerItemViewModel}">
        <Border BorderBrush="{Binding EdgeColor, Converter={StaticResource solidColorBrushConverter}}"
                BorderThickness="{Binding EdgeThickness}">
            <Image IsHitTestVisible="False"
                   Source="{Binding FileName}"
                   Stretch="Fill"
                   Clip="{Binding Clip.Value}"
                   Tag="picture" />
        </Border>
    </DataTemplate>
</ResourceDictionary>

上記コードで動作確認をしてみると、クリッピングされる領域が大きくずれていることが判明します。詳細は下記GIFアニメーションをご覧ください。(この例だと、いくらを囲っているのに、クリップした結果、いくらが一部しか写っていない)

clipping.gif

該当するソースコード

boiler's Graphics
https://github.com/dhq-boiler/boiler-s-Graphics

gitリポジトリ
https://github.com/dhq-boiler/boiler-s-Graphics.git

コミット:a65cbd2

ブランチ:feature/CombineGeometry

自分で試したこと

右下方向にクリップ領域がずれているので、PathGeometryにTranslateTransformをかけてやれば良いと思って以下のコードを書きましたが、結果はうまくいきませんでした。

DiagramViewModel.cs
        private void ExecuteClipCommand()
        {
            var other = SelectedItems.OfType<DesignerItemViewModelBase>().Last();
            var pathGeometry = other.PathGeometry.Value;
            pathGeometry.Transform = new TranslateTransform(other.Left.Value - picture.Left.Value, other.Top.Value - picture.Top.Value);
            picture.Clip.Value = pathGeometry;
            Items.Remove(other);
        }

clipping_transform.gif

※参考 バイナリ配布

このバグが発生する1つ前のバージョンのv1.2.1は以下からダウンロード可能です。

何か私の見落とし、致命的な勘違いなど気づいたところがあれば、回答していただけると助かります。よろしくお願いいたします。

1 likes

5Answer

ちょっと気になったのですが、「クリッピングができるようになりました」の画像でクリッピング後もハンドル(画像四隅の小さな四角)が元の画像サイズのままなのは意図したものでしょうか?
普通はクリッピング後の小さな画像の四隅にハンドルがつきますよね。
これはジオメトリをどのように設定すればいいかにも関わってきます。

あと、画像表示領域の矩形はキャンバス(画面上の白い領域)内の座標系でキャンバスの左上が原点になりますが、画像表示領域内の画像は座標系が違い、画像表示領域の左上が原点となります。
クリップすれば画像表示領域座標系の原点を、クリップ後領域の左上に変えなきゃいけないのではないでしょうか。

1Like

Comments

  1. @dhq_boiler

    Questioner

    >ちょっと気になったのですが、「クリッピングができるようになりました」の画像でクリッピング後もハンドル(画像四隅の小さな四角)が元の画像サイズのままなのは意図したものでしょうか?
    >普通はクリッピング後の小さな画像の四隅にハンドルがつきますよね。
    >これはジオメトリをどのように設定すればいいかにも関わってきます。
    これは意図したものではありません。WPFのUIElement.ClipにGeometryを設定すると、そのGeometryの形にくり抜かれるわけですが、UIElementのサイズがクリップ後のサイズに更新はされず、元のUIElementのサイズが維持されます。
    その後は任意のコードによって、Marginにマイナスの値を代入してUIElementのサイズをクリップ後のサイズに変更して、Canvas.Left, Canvas.Topの値を調整する必要があるのだと思います。
    そのコードをQiitaには載せてなかったので、試した結果を追記します。
  2. UIElement.Clipは「画像の一部を切り取る」わけではなく「画像の一部を表示する」であり、元の画像は維持しているのでこのツールが意図しているクリップとは機能が違うということみたいですね。
    ImageがビットマップのみならImageSourceを複製してクリップ済みのビットマップに差し替えればいいと思いますが、ベクタ画像だとそんなことはできないようで…

    表示用UIElementの子要素としてクリップ用UIElementを用意して、表示用では位置とサイズを指定、クリップ用は画像内のクリップ領域を指定とするしかないのかなぁ

その後は任意のコードによって、Marginにマイナスの値を代入してUIElementのサイズをクリップ後のサイズに変更して、Canvas.Left, Canvas.Topの値を調整する必要があるのだと思います。
そのコードをQiitaには載せてなかったので、試した結果を追記します。

上記を試そうとしたのですが、不意にもToReactiveProperty()の戻り値が{null}になってしまう問題が発生したので足踏みします。

DiagramViewModel.cs
        :
        private void ExecuteClipCommand()
        {
            var picture = SelectedItems.OfType<PictureDesignerItemViewModel>().First();
            var pictureRP = picture.ToReactiveProperty(mode: ReactivePropertyMode.RaiseLatestValueOnSubscribe); //戻り値が何故か {null} になる
            var other = SelectedItems.OfType<DesignerItemViewModelBase>().Last();
            var pathGeometry = GeometryCreator.CreateRectangle(other as NRectangleViewModel, picture.Left.Value, picture.Top.Value);
            (pictureRP.Value.Sender as PictureDesignerItemViewModel).Clip.Value = pathGeometry;
            (pictureRP.Value.Sender as PictureDesignerItemViewModel).ClipObject.Value = other;
            pictureRP.Zip(pictureRP.Skip(1), (Old, New) => new { OldItem = Old, NewItem = New }).Subscribe(x =>
            {
                var _other = picture.ClipObject.Value;
                var _pathGeometry = GeometryCreator.CreateRectangle(_other as NRectangleViewModel, picture.Left.Value, picture.Top.Value, x.OldItem, x.NewItem);
                picture.Clip.Value = _pathGeometry;
            })
            .AddTo(_CompositeDisposable);
            Items.Remove(other);
        }
        :
1Like

Comments

クリッピングができるようになりました。

can_clip_but sizing_wrong.gif

しかし、クリッピングを実行した後、画像をリサイズすると、クリップ領域がずれてしまいます。

修正したコードは以下の通りです。

DiagramViewModel.cs

        private void ExecuteClipCommand()
        {
            var picture = SelectedItems.OfType<PictureDesignerItemViewModel>().First();
            var other = SelectedItems.OfType<DesignerItemViewModelBase>().Last();
            var pathGeometry = GeometryCreator.CreateRectangle(other as NRectangleViewModel, picture.Left.Value, picture.Top.Value);
            picture.Clip.Value = pathGeometry;
            Items.Remove(other);
        }

0Like

すいません、GeometryCreatorのコードを貼り付け忘れてました。

GeometryCreator.cs
    class GeometryCreator
    {
        :

        public static PathGeometry CreateRectangle(NRectangleViewModel item, double offsetX, double offsetY)
        {
            var geometry = new StreamGeometry();
            geometry.FillRule = FillRule.EvenOdd;
            using (var ctx = geometry.Open())
            {
                ctx.BeginFigure(new Point(item.Left.Value - offsetX, item.Top.Value - offsetY), true, true);
                ctx.LineTo(new Point(item.Left.Value - offsetX + item.Width.Value, item.Top.Value - offsetY), true, false);
                ctx.LineTo(new Point(item.Left.Value - offsetX + item.Width.Value, item.Top.Value - offsetY + item.Height.Value), true, false);
                ctx.LineTo(new Point(item.Left.Value - offsetX, item.Top.Value - offsetY + item.Height.Value), true, false);
            }
            geometry.Freeze();
            return PathGeometry.CreateFromGeometry(geometry);
        }
    }
0Like

ToReactiveProperty() returns {null} を回避する方法がわかったので、修正しました。ですが、クリッピング後リサイズするとクリップ領域がずれる問題はまだ解決していません。

ReactivePropertyの更新前・更新後の値を取得する機能を使って、あわせてクリップ領域を拡大縮小するコードを書きました。

DesignerItemViewModelBase.cs
public abstract class DesignerItemViewModelBase : SelectableDesignerItemViewModelBase, IObservable<TransformNotification>, ICloneable
    {
        :
        public ReactiveProperty<double> Width { get; } = new ReactiveProperty<double>(mode: ReactivePropertyMode.RaiseLatestValueOnSubscribe | ReactivePropertyMode.DistinctUntilChanged);

        public ReactiveProperty<double> Height { get; } = new ReactiveProperty<double>(mode: ReactivePropertyMode.RaiseLatestValueOnSubscribe | ReactivePropertyMode.DistinctUntilChanged);

        :

        public ReactiveProperty<double> Left { get; } = new ReactiveProperty<double>(mode: ReactivePropertyMode.RaiseLatestValueOnSubscribe | ReactivePropertyMode.DistinctUntilChanged);

        public ReactiveProperty<double> Top { get; } = new ReactiveProperty<double>(mode: ReactivePropertyMode.RaiseLatestValueOnSubscribe | ReactivePropertyMode.DistinctUntilChanged);
        :

        private void Init()
        {
            MinWidth = 0;
            MinHeight = 0;

            Left
                .Zip(Left.Skip(1), (Old, New) => new { OldItem = Old, NewItem = New})
                .Subscribe(x => UpdateTransform(nameof(Left), x.OldItem, x.NewItem))
                .AddTo(_CompositeDisposable);
            Top
                .Zip(Top.Skip(1), (Old, New) => new { OldItem = Old, NewItem = New })
                .Subscribe(x => UpdateTransform(nameof(Top), x.OldItem, x.NewItem))
                .AddTo(_CompositeDisposable);
            Width
                .Zip(Width.Skip(1), (Old, New) => new { OldItem = Old, NewItem = New })
                .Subscribe(x => UpdateTransform(nameof(Width), x.OldItem, x.NewItem))
                .AddTo(_CompositeDisposable);
            Height
                .Zip(Height.Skip(1), (Old, New) => new { OldItem = Old, NewItem = New })
                .Subscribe(x => UpdateTransform(nameof(Height), x.OldItem, x.NewItem))
                .AddTo(_CompositeDisposable);
            RotationAngle
                .Zip(RotationAngle.Skip(1), (Old, New) => new { OldItem = Old, NewItem = New })
                .Subscribe(x => UpdateTransform(nameof(RotationAngle), x.OldItem, x.NewItem))
                .AddTo(_CompositeDisposable);
            Matrix
                .Zip(Matrix.Skip(1), (Old, New) => new { OldItem = Old, NewItem = New })
                .Subscribe(x => UpdateTransform(nameof(Matrix), x.OldItem, x.NewItem))
                .AddTo(_CompositeDisposable);
            Right = Left.Select(x => x + Width.Value)
                        .ToReadOnlyReactiveProperty();
            Bottom = Top.Select(x => x + Height.Value)
                        .ToReadOnlyReactiveProperty();

            Matrix.Value = new Matrix();
        }

        :

        public void UpdateTransform(string propertyName, object oldValue, object newValue)
        {
            UpdateCenterPoint();
            TransformObserversOnNext(propertyName, oldValue, newValue);
            UpdatePathGeometryIfEnable();
        }

        public void UpdatePathGeometryIfEnable()
        {
            if (EnablePathGeometryUpdate.Value)
            {
                if (RotationAngle.Value == 0)
                {
                    PathGeometry.Value = CreateGeometry();
                }
                else
                {
                    RotatePathGeometry.Value = CreateGeometry(RotationAngle.Value);
                }
            }
        }

        :

        public void TransformObserversOnNext(string propertyName, object oldValue, object newValue)
        {
            var tn = new TransformNotification()
            {
                Sender = this,
                PropertyName = propertyName,
                OldValue = oldValue,
                NewValue = newValue
            };
            this.TransformNortification.Value = tn;
            _observers.ForEach(x => x.OnNext(tn));
        }
        :
    }
DiagramViewModel.cs
        private void ExecuteClipCommand()
        {
            var picture = SelectedItems.OfType<PictureDesignerItemViewModel>().First();
            var other = SelectedItems.OfType<DesignerItemViewModelBase>().Last();
            var pathGeometry = GeometryCreator.CreateRectangle(other as NRectangleViewModel, picture.Left.Value, picture.Top.Value);
            (picture.TransformNortification.Value.Sender as PictureDesignerItemViewModel).Clip.Value = pathGeometry;
            (picture.TransformNortification.Value.Sender as PictureDesignerItemViewModel).ClipObject.Value = other;
            picture.TransformNortification.Zip(picture.TransformNortification.Skip(1), (Old, New) => new { OldItem = Old, NewItem = New })
            .Where(x => x.NewItem.PropertyName == "Width" || x.NewItem.PropertyName == "Height")
            .Subscribe(x =>
            {
                var _other = picture.ClipObject.Value;
                var _pathGeometry = GeometryCreator.CreateRectangle(_other as NRectangleViewModel, picture.Left.Value, picture.Top.Value, x.NewItem.PropertyName, (double)x.NewItem.OldValue, (double)x.NewItem.NewValue);
                picture.Clip.Value = _pathGeometry;
            })
            .AddTo(_CompositeDisposable);
            Items.Remove(other);
        }
GeometryCreator.cs
    class GeometryCreator
    {
       :

        public static PathGeometry CreateRectangle(NRectangleViewModel item, double offsetX, double offsetY, string propertyName, double oldItem, double newItem)
        {
            double widthRatio = 1;
            double heightRatio = 1;
            if (propertyName == "Width")
            {
                widthRatio = newItem / oldItem;
            }
            else if (propertyName == "Height")
            {
                heightRatio = newItem / oldItem;
            }
            
            //TODO Rectangleを構成する4点に widthRatio と heightRatio を掛ける
            var geometry = new StreamGeometry();
            geometry.FillRule = FillRule.EvenOdd;
            using (var ctx = geometry.Open())
            {
                ctx.BeginFigure(new Point(widthRatio * (item.Left.Value - offsetX), heightRatio * (item.Top.Value - offsetY)), true, true);
                ctx.LineTo(new Point(widthRatio * (item.Left.Value - offsetX + item.Width.Value), heightRatio * (item.Top.Value - offsetY)), true, false);
                ctx.LineTo(new Point(widthRatio * (item.Left.Value - offsetX + item.Width.Value), heightRatio * (item.Top.Value - offsetY + item.Height.Value)), true, false);
                ctx.LineTo(new Point(widthRatio * (item.Left.Value - offsetX), heightRatio * (item.Top.Value - offsetY + item.Height.Value)), true, false);
            }
            geometry.Freeze();
            return PathGeometry.CreateFromGeometry(geometry);
        }

        :
    }

しかし、下記のGIFアニメのように、クリップ後リサイズをすると、わずかに幅高さに変動がある程度で、不自然な挙動になっています。

clipping_area_cant_resize.gif

0Like

Comments

Your answer might help someone💌