feat: make scale/translate controllable.
This commit is contained in:
@@ -10,20 +10,72 @@
|
|||||||
mc:Ignorable="d">
|
mc:Ignorable="d">
|
||||||
<StackPanel>
|
<StackPanel>
|
||||||
<u:ImageViewer
|
<u:ImageViewer
|
||||||
Name="imageViewer"
|
Name="viewer"
|
||||||
Width="600"
|
Width="600"
|
||||||
Height="300"
|
Height="300"
|
||||||
Source="../Assets/WORLD.png">
|
Source="../Assets/WORLD.png">
|
||||||
<u:ImageViewer.Overlayer>
|
<u:ImageViewer.Overlayer>
|
||||||
<Rectangle
|
<Rectangle
|
||||||
Width="100"
|
|
||||||
Height="100"
|
|
||||||
HorizontalAlignment="Stretch"
|
HorizontalAlignment="Stretch"
|
||||||
VerticalAlignment="Stretch"
|
VerticalAlignment="Stretch"
|
||||||
Fill="Red"
|
Fill="Transparent"
|
||||||
IsHitTestVisible="False"
|
IsHitTestVisible="False"
|
||||||
Opacity="0.2" />
|
Opacity="0.2" />
|
||||||
</u:ImageViewer.Overlayer>
|
</u:ImageViewer.Overlayer>
|
||||||
</u:ImageViewer>
|
</u:ImageViewer>
|
||||||
|
|
||||||
|
<Grid ColumnDefinitions="Auto, Auto, *" RowDefinitions="Auto, Auto, Auto">
|
||||||
|
<TextBlock
|
||||||
|
Grid.Row="0"
|
||||||
|
Grid.Column="0"
|
||||||
|
Text="Scale" />
|
||||||
|
<Slider
|
||||||
|
Grid.Row="0"
|
||||||
|
Grid.Column="1"
|
||||||
|
Width="200"
|
||||||
|
Maximum="10"
|
||||||
|
Minimum="0.1"
|
||||||
|
Value="{Binding #viewer.Scale}" />
|
||||||
|
<TextBlock
|
||||||
|
Grid.Row="0"
|
||||||
|
Grid.Column="2"
|
||||||
|
Text="{Binding #viewer.Scale, StringFormat=\{0:0.00000\}}" />
|
||||||
|
|
||||||
|
<TextBlock
|
||||||
|
Grid.Row="1"
|
||||||
|
Grid.Column="0"
|
||||||
|
Text="TranslateX" />
|
||||||
|
<Slider
|
||||||
|
Grid.Row="1"
|
||||||
|
Grid.Column="1"
|
||||||
|
Width="200"
|
||||||
|
IsSnapToTickEnabled="True"
|
||||||
|
Maximum="300"
|
||||||
|
Minimum="-300"
|
||||||
|
TickFrequency="0.1"
|
||||||
|
Value="{Binding #viewer.TranslateX}" />
|
||||||
|
<TextBlock
|
||||||
|
Grid.Row="1"
|
||||||
|
Grid.Column="2"
|
||||||
|
Text="{Binding #viewer.TranslateX, StringFormat=\{0:0.0\}}" />
|
||||||
|
|
||||||
|
<TextBlock
|
||||||
|
Grid.Row="2"
|
||||||
|
Grid.Column="0"
|
||||||
|
Text="TranslateY" />
|
||||||
|
<Slider
|
||||||
|
Grid.Row="2"
|
||||||
|
Grid.Column="1"
|
||||||
|
Width="200"
|
||||||
|
IsSnapToTickEnabled="True"
|
||||||
|
Maximum="300"
|
||||||
|
Minimum="-300"
|
||||||
|
TickFrequency="0.1"
|
||||||
|
Value="{Binding #viewer.TranslateY}" />
|
||||||
|
<TextBlock
|
||||||
|
Grid.Row="2"
|
||||||
|
Grid.Column="2"
|
||||||
|
Text="{Binding #viewer.TranslateY, StringFormat=\{0:0.0\}}" />
|
||||||
|
</Grid>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</UserControl>
|
</UserControl>
|
||||||
|
|||||||
@@ -4,12 +4,15 @@
|
|||||||
xmlns:u="https://irihi.tech/ursa">
|
xmlns:u="https://irihi.tech/ursa">
|
||||||
<!-- Add Resources Here -->
|
<!-- Add Resources Here -->
|
||||||
<ControlTheme x:Key="{x:Type u:ImageViewer}" TargetType="u:ImageViewer">
|
<ControlTheme x:Key="{x:Type u:ImageViewer}" TargetType="u:ImageViewer">
|
||||||
|
<Setter Property="Background" Value="LightGray" />
|
||||||
<Setter Property="Template">
|
<Setter Property="Template">
|
||||||
<ControlTemplate TargetType="{x:Type u:ImageViewer}">
|
<ControlTemplate TargetType="{x:Type u:ImageViewer}">
|
||||||
<VisualLayerManager Name="{x:Static u:ImageViewer.PART_Layer}">
|
<VisualLayerManager Name="{x:Static u:ImageViewer.PART_Layer}">
|
||||||
<Border Background="Transparent" ClipToBounds="True">
|
<Border Background="{TemplateBinding Background}" ClipToBounds="True">
|
||||||
<Image Name="{x:Static u:ImageViewer.PART_Image}" Source="{TemplateBinding Source}">
|
<Image
|
||||||
|
Name="{x:Static u:ImageViewer.PART_Image}"
|
||||||
|
Source="{TemplateBinding Source}"
|
||||||
|
Stretch="Uniform">
|
||||||
<Image.RenderTransform>
|
<Image.RenderTransform>
|
||||||
<TransformGroup>
|
<TransformGroup>
|
||||||
<ScaleTransform ScaleX="{Binding Scale, RelativeSource={RelativeSource TemplatedParent}}" ScaleY="{Binding Scale, RelativeSource={RelativeSource TemplatedParent}}" />
|
<ScaleTransform ScaleX="{Binding Scale, RelativeSource={RelativeSource TemplatedParent}}" ScaleY="{Binding Scale, RelativeSource={RelativeSource TemplatedParent}}" />
|
||||||
@@ -22,5 +25,8 @@
|
|||||||
</VisualLayerManager>
|
</VisualLayerManager>
|
||||||
</ControlTemplate>
|
</ControlTemplate>
|
||||||
</Setter>
|
</Setter>
|
||||||
|
<Style Selector="^:moving">
|
||||||
|
<Setter Property="Cursor" Value="Hand" />
|
||||||
|
</Style>
|
||||||
</ControlTheme>
|
</ControlTheme>
|
||||||
</ResourceDictionary>
|
</ResourceDictionary>
|
||||||
|
|||||||
@@ -4,25 +4,28 @@ using Avalonia.Controls.Metadata;
|
|||||||
using Avalonia.Controls.Primitives;
|
using Avalonia.Controls.Primitives;
|
||||||
using Avalonia.Input;
|
using Avalonia.Input;
|
||||||
using Avalonia.Media;
|
using Avalonia.Media;
|
||||||
|
using Avalonia.Media.Imaging;
|
||||||
|
|
||||||
namespace Ursa.Controls;
|
namespace Ursa.Controls;
|
||||||
|
|
||||||
[TemplatePart(PART_Image, typeof(Image))]
|
[TemplatePart(PART_Image, typeof(Image))]
|
||||||
[TemplatePart(PART_Layer, typeof(VisualLayerManager))]
|
[TemplatePart(PART_Layer, typeof(VisualLayerManager))]
|
||||||
|
[PseudoClasses(PC_Moving)]
|
||||||
public class ImageViewer: TemplatedControl
|
public class ImageViewer: TemplatedControl
|
||||||
{
|
{
|
||||||
public const string PART_Image = "PART_Image";
|
public const string PART_Image = "PART_Image";
|
||||||
public const string PART_Layer = "PART_Layer";
|
public const string PART_Layer = "PART_Layer";
|
||||||
|
public const string PC_Moving = ":moving";
|
||||||
|
|
||||||
private Image _image = null!;
|
private Image? _image = null!;
|
||||||
private VisualLayerManager? _layer;
|
private VisualLayerManager? _layer;
|
||||||
private Point? _lastClickPoint;
|
private Point? _lastClickPoint;
|
||||||
private Point? _lastReleasePoint;
|
private Point? _lastlocation;
|
||||||
|
|
||||||
public static readonly StyledProperty<object?> OverlayerProperty = AvaloniaProperty.Register<ImageViewer, object?>(
|
public static readonly StyledProperty<Control?> OverlayerProperty = AvaloniaProperty.Register<ImageViewer, Control?>(
|
||||||
nameof(Overlayer));
|
nameof(Overlayer));
|
||||||
|
|
||||||
public object? Overlayer
|
public Control? Overlayer
|
||||||
{
|
{
|
||||||
get => GetValue(OverlayerProperty);
|
get => GetValue(OverlayerProperty);
|
||||||
set => SetValue(OverlayerProperty, value);
|
set => SetValue(OverlayerProperty, value);
|
||||||
@@ -38,34 +41,35 @@ public class ImageViewer: TemplatedControl
|
|||||||
private double _scale = 1;
|
private double _scale = 1;
|
||||||
|
|
||||||
public static readonly DirectProperty<ImageViewer, double> ScaleProperty = AvaloniaProperty.RegisterDirect<ImageViewer, double>(
|
public static readonly DirectProperty<ImageViewer, double> ScaleProperty = AvaloniaProperty.RegisterDirect<ImageViewer, double>(
|
||||||
nameof(Scale), o => o.Scale, unsetValue: 1);
|
nameof(Scale), o => o.Scale, (o,v)=> o.Scale = v, unsetValue: 1);
|
||||||
|
|
||||||
public double Scale
|
public double Scale
|
||||||
{
|
{
|
||||||
get => _scale;
|
get => _scale;
|
||||||
private set => SetAndRaise(ScaleProperty, ref _scale, value);
|
set => SetAndRaise(ScaleProperty, ref _scale, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
private double _translateX;
|
private double _translateX;
|
||||||
|
|
||||||
public static readonly DirectProperty<ImageViewer, double> TranslateXProperty = AvaloniaProperty.RegisterDirect<ImageViewer, double>(
|
public static readonly DirectProperty<ImageViewer, double> TranslateXProperty = AvaloniaProperty.RegisterDirect<ImageViewer, double>(
|
||||||
nameof(TranslateX), o => o.TranslateX, unsetValue: 0);
|
nameof(TranslateX), o => o.TranslateX, (o,v)=>o.TranslateX = v, unsetValue: 0);
|
||||||
|
|
||||||
public double TranslateX
|
public double TranslateX
|
||||||
{
|
{
|
||||||
get => _translateX;
|
get => _translateX;
|
||||||
private set => SetAndRaise(TranslateXProperty, ref _translateX, value);
|
set => SetAndRaise(TranslateXProperty, ref _translateX, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
private double _translateY;
|
private double _translateY;
|
||||||
|
|
||||||
public static readonly DirectProperty<ImageViewer, double> TranslateYProperty = AvaloniaProperty.RegisterDirect<ImageViewer, double>(
|
public static readonly DirectProperty<ImageViewer, double> TranslateYProperty =
|
||||||
nameof(TranslateY), o => o.TranslateY);
|
AvaloniaProperty.RegisterDirect<ImageViewer, double>(
|
||||||
|
nameof(TranslateY), o => o.TranslateY, (o, v) => o.TranslateY = v, unsetValue: 0);
|
||||||
|
|
||||||
public double TranslateY
|
public double TranslateY
|
||||||
{
|
{
|
||||||
get => _translateY;
|
get => _translateY;
|
||||||
private set => SetAndRaise(TranslateYProperty, ref _translateY, value);
|
set => SetAndRaise(TranslateYProperty, ref _translateY, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -73,12 +77,42 @@ public class ImageViewer: TemplatedControl
|
|||||||
{
|
{
|
||||||
OverlayerProperty.Changed.AddClassHandler<ImageViewer>((o, e) => o.OnOverlayerChanged(e));
|
OverlayerProperty.Changed.AddClassHandler<ImageViewer>((o, e) => o.OnOverlayerChanged(e));
|
||||||
SourceProperty.Changed.AddClassHandler<ImageViewer>((o, e) => o.OnSourceChanged(e));
|
SourceProperty.Changed.AddClassHandler<ImageViewer>((o, e) => o.OnSourceChanged(e));
|
||||||
|
TranslateXProperty.Changed.AddClassHandler<ImageViewer>((o,e)=>o.OnTranslateXChanged(e));
|
||||||
|
TranslateYProperty.Changed.AddClassHandler<ImageViewer>((o, e) => o.OnTranslateYChanged(e));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void OnTranslateYChanged(AvaloniaPropertyChangedEventArgs args)
|
||||||
|
{
|
||||||
|
if (PseudoClasses.Contains(PC_Moving)) return;
|
||||||
|
var newValue = args.GetNewValue<double>();
|
||||||
|
if (_lastlocation is not null)
|
||||||
|
{
|
||||||
|
_lastlocation = _lastlocation.Value.WithY(newValue);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_lastlocation = new Point(0, newValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnTranslateXChanged(AvaloniaPropertyChangedEventArgs args)
|
||||||
|
{
|
||||||
|
if (PseudoClasses.Contains(PC_Moving)) return;
|
||||||
|
var newValue = args.GetNewValue<double>();
|
||||||
|
if (_lastlocation is not null)
|
||||||
|
{
|
||||||
|
_lastlocation = _lastlocation.Value.WithX(newValue);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_lastlocation = new Point(newValue, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void OnOverlayerChanged(AvaloniaPropertyChangedEventArgs args)
|
private void OnOverlayerChanged(AvaloniaPropertyChangedEventArgs args)
|
||||||
{
|
{
|
||||||
var control = args.GetNewValue<object?>();
|
var control = args.GetNewValue<Control?>();
|
||||||
if (control is Control c)
|
if (control is { } c)
|
||||||
{
|
{
|
||||||
AdornerLayer.SetAdorner(this, c);
|
AdornerLayer.SetAdorner(this, c);
|
||||||
}
|
}
|
||||||
@@ -90,7 +124,12 @@ public class ImageViewer: TemplatedControl
|
|||||||
Size size = image.Size;
|
Size size = image.Size;
|
||||||
double width = this.Width;
|
double width = this.Width;
|
||||||
double height = this.Height;
|
double height = this.Height;
|
||||||
Scale = Math.Min(width/size.Width, height/size.Height);
|
if (_image is not null)
|
||||||
|
{
|
||||||
|
_image.Width = size.Width;
|
||||||
|
_image.Height = size.Height;
|
||||||
|
}
|
||||||
|
Scale = Math.Max(width/size.Width, height/size.Height);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
|
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
|
||||||
@@ -98,8 +137,16 @@ public class ImageViewer: TemplatedControl
|
|||||||
base.OnApplyTemplate(e);
|
base.OnApplyTemplate(e);
|
||||||
_image = e.NameScope.Get<Image>(PART_Image);
|
_image = e.NameScope.Get<Image>(PART_Image);
|
||||||
_layer = e.NameScope.Get<VisualLayerManager>(PART_Layer);
|
_layer = e.NameScope.Get<VisualLayerManager>(PART_Layer);
|
||||||
Scale = 1;
|
if (Source is { } i)
|
||||||
if (Overlayer is Control c)
|
{
|
||||||
|
Size size = i.Size;
|
||||||
|
double width = this.Width;
|
||||||
|
double height = this.Height;
|
||||||
|
_image.Width = size.Width;
|
||||||
|
_image.Height = size.Height;
|
||||||
|
Scale = Math.Max(width/size.Width, height/size.Height);
|
||||||
|
}
|
||||||
|
if (Overlayer is { } c)
|
||||||
{
|
{
|
||||||
AdornerLayer.SetAdorner(this, c);
|
AdornerLayer.SetAdorner(this, c);
|
||||||
}
|
}
|
||||||
@@ -128,13 +175,13 @@ public class ImageViewer: TemplatedControl
|
|||||||
base.OnPointerMoved(e);
|
base.OnPointerMoved(e);
|
||||||
if (e.Pointer.Captured == this && _lastClickPoint != null)
|
if (e.Pointer.Captured == this && _lastClickPoint != null)
|
||||||
{
|
{
|
||||||
|
PseudoClasses.Set(PC_Moving, true);
|
||||||
Point p = e.GetPosition(this);
|
Point p = e.GetPosition(this);
|
||||||
double deltaX = p.X - _lastClickPoint.Value.X;
|
double deltaX = p.X - _lastClickPoint.Value.X;
|
||||||
double deltaY = p.Y - _lastClickPoint.Value.Y;
|
double deltaY = p.Y - _lastClickPoint.Value.Y;
|
||||||
TranslateX = deltaX + (_lastReleasePoint?.X ?? 0);
|
TranslateX = deltaX + (_lastlocation?.X ?? 0);
|
||||||
TranslateY = deltaY + (_lastReleasePoint?.Y ?? 0);
|
TranslateY = deltaY + (_lastlocation?.Y ?? 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void OnPointerPressed(PointerPressedEventArgs e)
|
protected override void OnPointerPressed(PointerPressedEventArgs e)
|
||||||
@@ -148,6 +195,7 @@ public class ImageViewer: TemplatedControl
|
|||||||
{
|
{
|
||||||
base.OnPointerReleased(e);
|
base.OnPointerReleased(e);
|
||||||
e.Pointer.Capture(null);
|
e.Pointer.Capture(null);
|
||||||
_lastReleasePoint = new Point(TranslateX, TranslateY);
|
_lastlocation = new Point(TranslateX, TranslateY);
|
||||||
|
PseudoClasses.Set(PC_Moving, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user