ポイントスナップ機能が正しく動作しない
お世話になっております。
解決したいこと
C# + WPFでベクターグラフィックスドローイングツールを開発しています。
※下にソースコードへの案内を記載しております。よろしければそちらを参照ください。
このツールにポイントスナップ機能という、図形等を描画する時、他の図形のコーナーに吸着して描画できるものです。
発生している問題・エラー
以下のGIF画像をご覧ください。
既にある四角形の左上のコーナーに吸着させて、新たに四角形を描画しようというものです。左上のコーナーに吸着させる時、1つのコーナーに吸着するポイントが3つあるように見えます。
しかし、XAML上では1つのコーナーには1つのResizeThumbしか配置していません。(※DesignerItems.xaml参照)
<ControlTemplate x:Key="ResizeDecoratorTemplate" TargetType="{x:Type Control}">
<Grid>
<Grid Opacity="0.7" SnapsToDevicePixels="true">
<!-- 上の回転ツマミ部分 -->
<control:RotateThumb
Width="7"
Height="7"
Margin="0,-20,0,0"
HorizontalAlignment="Center"
VerticalAlignment="Top"
Cursor="Hand"
Template="{StaticResource RotateThumbTemplate}" />
<!-- 上 -->
<control:ResizeThumb
Height="3"
Margin="0,-4,0,0"
HorizontalAlignment="Stretch"
VerticalAlignment="Top"
Cursor="SizeNS"
Template="{StaticResource HorizontalResizeHandleTemplate}" />
<!-- 左 -->
<control:ResizeThumb
Width="3"
Margin="-4,0,0,0"
HorizontalAlignment="Left"
VerticalAlignment="Stretch"
Cursor="SizeWE"
Template="{StaticResource VerticalResizeHandleTemplate}" />
<!-- 右 -->
<control:ResizeThumb
Width="3"
Margin="0,0,-4,0"
HorizontalAlignment="Right"
VerticalAlignment="Stretch"
Cursor="SizeWE"
Template="{StaticResource VerticalResizeHandleTemplate}" />
<!-- 下 -->
<control:ResizeThumb
Height="3"
Margin="0,0,0,-4"
HorizontalAlignment="Stretch"
VerticalAlignment="Bottom"
Cursor="SizeNS"
Template="{StaticResource HorizontalResizeHandleTemplate}" />
<!-- 左上 -->
<control:ResizeThumb
Width="7"
Height="7"
Margin="-6,-6,0,0"
HorizontalAlignment="Left"
VerticalAlignment="Top"
Cursor="SizeNWSE"
Template="{StaticResource ResizeHandleTemplate}" />
<!-- 右上 -->
<control:ResizeThumb
Width="7"
Height="7"
Margin="0,-6,-6,0"
HorizontalAlignment="Right"
VerticalAlignment="Top"
Cursor="SizeNESW"
Template="{StaticResource ResizeHandleTemplate}" />
<!-- 左下 -->
<control:ResizeThumb
Width="7"
Height="7"
Margin="-6,0,0,-6"
HorizontalAlignment="Left"
VerticalAlignment="Bottom"
Cursor="SizeNESW"
Template="{StaticResource ResizeHandleTemplate}" />
<!-- 右下 -->
<control:ResizeThumb
Width="7"
Height="7"
Margin="0,0,-6,-6"
HorizontalAlignment="Right"
VerticalAlignment="Bottom"
Cursor="SizeNWSE"
Template="{StaticResource ResizeHandleTemplate}" />
</Grid>
<Grid
Margin="-3"
IsHitTestVisible="False"
Opacity="1">
<Line
Margin="0,-11,0,0"
HorizontalAlignment="Center"
VerticalAlignment="Top"
Stroke="#6c809a"
StrokeThickness="1"
X1="0"
X2="0"
Y1="0"
Y2="11" />
</Grid>
</Grid>
</ControlTemplate>
どうして1つのコーナー上に吸着するポイントが3つできるのでしょうか。
参考情報
ポイントスナップ機能は現在のところ、四角形を描画する時にしか実装していません。
四角形を描画する仕組みは以下をご覧ください。
public class ToolBarViewModel
{
private IDialogService dlgService = null;
public ObservableCollection<ToolItemData> ToolItems { get; } = new ObservableCollection<ToolItemData>();
public ToolBarViewModel(IDialogService dialogService)
{
this.dlgService = dialogService;
:
ToolItems.Add(new ToolItemData("rectangle", "pack://application:,,,/Assets/img/rectangle.png", new DelegateCommand(() =>
{
var behavior = new NDrawRectangleBehavior();
var designerCanvas = App.Current.MainWindow.GetChildOfType<DesignerCanvas>();
var behaviors = Interaction.GetBehaviors(designerCanvas);
behaviors.Clear();
if (!behaviors.Contains(behavior))
{
behaviors.Add(behavior);
}
SelectOneToolItem("rectangle");
})));
:
}
private void SelectOneToolItem(string toolName)
{
var toolItem = ToolItems.Where(i => i.Name == toolName).Single();
toolItem.IsChecked = true;
ToolItems.Where(i => i.Name != toolName).ToList().ForEach(i => i.IsChecked = false);
}
}
internal class NDrawRectangleBehavior : Behavior<DesignerCanvas>
{
private Point? _rectangleStartPoint;
protected override void OnAttached()
{
this.AssociatedObject.MouseDown += AssociatedObject_MouseDown;
this.AssociatedObject.MouseMove += AssociatedObject_MouseMove;
base.OnAttached();
}
protected override void OnDetaching()
{
this.AssociatedObject.MouseDown -= AssociatedObject_MouseDown;
this.AssociatedObject.MouseMove -= AssociatedObject_MouseMove;
base.OnDetaching();
}
private void AssociatedObject_MouseDown(object sender, System.Windows.Input.MouseButtonEventArgs e)
{
if (e.LeftButton == MouseButtonState.Pressed)
{
if (e.Source == AssociatedObject)
{
_rectangleStartPoint = e.GetPosition(AssociatedObject);
e.Handled = true;
}
}
}
private void AssociatedObject_MouseMove(object sender, System.Windows.Input.MouseEventArgs e)
{
var canvas = AssociatedObject as DesignerCanvas;
if (canvas.SourceConnector == null)
{
if (e.LeftButton != MouseButtonState.Pressed)
_rectangleStartPoint = null;
if (_rectangleStartPoint.HasValue)
{
(App.Current.MainWindow.DataContext as MainWindowViewModel).CurrentOperation.Value = "描画";
AdornerLayer adornerLayer = AdornerLayer.GetAdornerLayer(canvas);
if (adornerLayer != null)
{
var adorner = new Adorners.RectangleAdorner(canvas, _rectangleStartPoint);
if (adorner != null)
{
adornerLayer.Add(adorner);
}
}
}
}
}
}
internal class RectangleAdorner : Adorner
{
private DesignerCanvas _designerCanvas;
private Point? _startPoint;
private Point? _endPoint;
private Pen _rectanglePen;
private Dictionary<Point, Adorner> _adorners;
public RectangleAdorner(DesignerCanvas designerCanvas, Point? dragStartPoint)
: base(designerCanvas)
{
_designerCanvas = designerCanvas;
_startPoint = dragStartPoint;
var parent = (AdornedElement as DesignerCanvas).DataContext as IDiagramViewModel;
var brush = new SolidColorBrush(parent.EdgeColors.First());
brush.Opacity = 0.5;
_rectanglePen = new Pen(brush, 1);
_adorners = new Dictionary<Point, Adorner>();
}
protected override void OnMouseMove(MouseEventArgs e)
{
if (e.LeftButton == MouseButtonState.Pressed)
{
if (!this.IsMouseCaptured)
this.CaptureMouse();
//ドラッグ終了座標を更新
_endPoint = e.GetPosition(this);
var mainWindowVM = (App.Current.MainWindow.DataContext as MainWindowViewModel);
var designerCanvas = App.Current.MainWindow.GetChildOfType<DesignerCanvas>();
var diagramVM = mainWindowVM.DiagramViewModel;
var snapPoints = diagramVM.SnapPoints;
Point? snapped = null;
foreach (var snapPoint in snapPoints)
{
if (_endPoint.Value.X > snapPoint.X - mainWindowVM.SnapPower.Value
&& _endPoint.Value.X < snapPoint.X + mainWindowVM.SnapPower.Value
&& _endPoint.Value.Y > snapPoint.Y - mainWindowVM.SnapPower.Value
&& _endPoint.Value.Y < snapPoint.Y + mainWindowVM.SnapPower.Value)
{
//スナップする座標を一時変数へ保存
snapped = snapPoint;
}
}
//スナップした場合
if (snapped != null)
{
AdornerLayer adornerLayer = AdornerLayer.GetAdornerLayer(designerCanvas);
RemoveFromAdornerLayerAndDictionary(snapped, adornerLayer);
//ドラッグ終了座標を一時変数で上書きしてスナップ
_endPoint = snapped;
if (adornerLayer != null)
{
Trace.WriteLine($"Snap={snapped.Value}");
if (!_adorners.ContainsKey(snapped.Value))
{
var adorner = new Adorners.SnapPointAdorner(designerCanvas, snapped.Value);
if (adorner != null)
{
adornerLayer.Add(adorner);
//ディクショナリに記憶する
_adorners.Add(snapped.Value, adorner);
}
}
}
}
else //スナップしなかった場合
{
RemoveAllAdornerFromAdornerLayerAndDictionary(designerCanvas);
}
(App.Current.MainWindow.DataContext as MainWindowViewModel).Details.Value = $"({_startPoint.Value.X}, {_startPoint.Value.Y}) - ({_endPoint.Value.X}, {_endPoint.Value.Y}) (w, h) = ({_endPoint.Value.X - _startPoint.Value.X}, {_endPoint.Value.Y - _startPoint.Value.Y})";
this.InvalidateVisual();
}
else
{
if (this.IsMouseCaptured) this.ReleaseMouseCapture();
}
e.Handled = true;
}
private void RemoveAllAdornerFromAdornerLayerAndDictionary(DesignerCanvas designerCanvas)
{
AdornerLayer adornerLayer = AdornerLayer.GetAdornerLayer(designerCanvas);
var removes = _adorners.ToList();
removes.ForEach(x =>
{
if (adornerLayer != null)
{
adornerLayer.Remove(x.Value);
}
_adorners.Remove(x.Key);
});
}
private void RemoveFromAdornerLayerAndDictionary(Point? snapped, AdornerLayer adornerLayer)
{
var removes = _adorners.Where(x => x.Key != snapped)
.ToList();
removes.ForEach(x =>
{
if (adornerLayer != null)
{
adornerLayer.Remove(x.Value);
}
_adorners.Remove(x.Key);
});
}
protected override void OnMouseUp(System.Windows.Input.MouseButtonEventArgs e)
{
// release mouse capture
if (this.IsMouseCaptured) this.ReleaseMouseCapture();
// remove this adorner from adorner layer
AdornerLayer adornerLayer = AdornerLayer.GetAdornerLayer(_designerCanvas);
if (adornerLayer != null)
{
adornerLayer.Remove(this);
foreach (var adorner in _adorners)
adornerLayer.Remove(adorner.Value);
_adorners.Clear();
}
if (_startPoint.HasValue && _endPoint.HasValue)
{
var rand = new Random();
var item = new NRectangleViewModel();
item.Owner = (AdornedElement as DesignerCanvas).DataContext as IDiagramViewModel;
item.Left.Value = Math.Min(_startPoint.Value.X, _endPoint.Value.X);
item.Top.Value = Math.Min(_startPoint.Value.Y, _endPoint.Value.Y);
item.Width.Value = Math.Max(_startPoint.Value.X - _endPoint.Value.X, _endPoint.Value.X - _startPoint.Value.X);
item.Height.Value = Math.Max(_startPoint.Value.Y - _endPoint.Value.Y, _endPoint.Value.Y - _startPoint.Value.Y);
item.EdgeColor = item.Owner.EdgeColors.First();
item.FillColor = item.Owner.FillColors.First();
item.ZIndex.Value = item.Owner.Items.Count;
item.IsSelected = true;
item.Owner.DeselectAll();
((AdornedElement as DesignerCanvas).DataContext as IDiagramViewModel).AddItemCommand.Execute(item);
_startPoint = null;
_endPoint = null;
}
(App.Current.MainWindow.DataContext as MainWindowViewModel).CurrentOperation.Value = "";
(App.Current.MainWindow.DataContext as MainWindowViewModel).Details.Value = "";
e.Handled = true;
}
protected override void OnRender(DrawingContext dc)
{
base.OnRender(dc);
dc.DrawRectangle(Brushes.Transparent, null, new Rect(RenderSize));
if (_startPoint.HasValue && _endPoint.HasValue)
dc.DrawRectangle(Brushes.Transparent, _rectanglePen, new Rect(_startPoint.Value, _endPoint.Value));
}
}
自分で試したこと
吸着可能なポイントをViewから吸い上げる処理を下記のように実装しました。
1つの四角形が描画されている時に、このSnapPointsプロパティをVisual Studioのデバッグ機能で確認したところ、ポイントが8つ生成されていました。コーナーの左上、上、右上、右、右下、下、左下、左の8個なので間違いはなさそうなのですが...。
public class DiagramViewModel : BindableBase, IDiagramViewModel, IDisposable
{
:
public IEnumerable<Point> SnapPoints
{
get {
var designerCanvas = App.Current.MainWindow.GetChildOfType<DesignerCanvas>();
return designerCanvas.EnumerateChildOfType<ResizeThumb>()
.Where(x => !(x is null))
.Select(x => x.TransformToAncestor(designerCanvas).Transform(new Point(0, 0)))
.Distinct(new SnapPointDistincter());
}
}
:
}
class SnapPointDistincter : IEqualityComparer<Point>
{
public bool Equals(Point a, Point b)
{
var mainWindowVM = (App.Current.MainWindow.DataContext as MainWindowViewModel);
var x = Math.Abs(a.X - b.X);
var y = Math.Abs(a.Y - b.Y);
var r = Math.Sqrt(Math.Pow(x, 2) + Math.Pow(y, 2));
return r < mainWindowVM.SnapPower.Value / 2;
}
public int GetHashCode(Point obj)
{
return obj.GetHashCode();
}
}
ソースコード
boiler's Graphics
https://github.com/dhq-boiler/boiler-s-Graphics
gitリポジトリ
https://github.com/dhq-boiler/boiler-s-Graphics.git
ブランチ:feature/PointSnap
コミット:ce42470
何か私の見落とし、致命的な勘違いなど気づいたところがあれば、回答していただけると助かります。よろしくお願いいたします。