Set Canvas Background, Width and Height with MVVM - c#

I have a struggle, I don't know how to set the background and size of a canvas with respect to MVVM, currently, I handle this only in the View part, and work ok, over this image (canvas) I want to draw some rectangle, but I want to be able to do everything according to MVVM pattern.
var bmp = new BitmapImage(new Uri(filename, UriKind.Relative));
ImageBrush brush = new ImageBrush();
brush.ImageSource = bmp;
canvas.Width = bmp.PixelWidth;
canvas.Height = bmp.PixelHeight;
canvas.Background = brush;
canvas.SnapsToDevicePixels = true;

The view model could expose a property with e.g. the image file path
public string ImagePath { get; set; }
to which you would bind like this
<Canvas Width="{Binding Background.ImageSource.PixelWidth,
RelativeSource={RelativeSource Self}}"
Height="{Binding Background.ImageSource.PixelHeight,
RelativeSource={RelativeSource Self}}"
SnapsToDevicePixels="True">
<Canvas.Background>
<ImageBrush ImageSource="{Binding ImagePath}"/>
</Canvas.Background>
</Canvas>
The conversion from string to ImageSource would automatically be performed by an ImageSourceConverter instance in WPF.
The Bindings would be simpler when the view model exposes a property of type BitmapSource:
public BitmapSource Image { get; set; }
XAML:
<Canvas Width="{Binding Image.PixelWidth}"
Height="{Binding Image.PixelHeight}"
SnapsToDevicePixels="True">
<Canvas.Background>
<ImageBrush ImageSource="{Binding Image}"/>
</Canvas.Background>
</Canvas>

Related

Crop/copy stretched image in WPF

I want to build a small image and text editor.
I have two Images in a UserControl.
On the left side is the editor and looks momentarily (thanks to some try and error) a little bit overstated, but it works nonetheless. (I will clean up the code later)
<Canvas x:Name="EditCanvas" Grid.Column="0"
Height="{Binding AreaHeight}" Width="{Binding AreaWidth}"
MaxWidth="426"
MouseLeftButtonDown="EditCanvas_MouseLeftButtonDown">
<Canvas.Background>
<VisualBrush TileMode="Tile" Viewport="0, 0, 1, 1" Stretch="{Binding SelectedStretch}">
<VisualBrush.Visual>
<Image x:Name="EditorImage"
Height="{Binding AreaHeight}" Width="{Binding AreaWidth}"
Source="{Binding FileImagePath}" Margin="1"/>
</VisualBrush.Visual>
</VisualBrush>
</Canvas.Background>
</Canvas>
Here I bind the Image to a UriSource. The stretching will change the Image itself, not the underlying source. Now I want to crop this image 'as it is'! So, when it is set to Stretch.UniformToFill or Stretch.Fill (for example) I want the cropped image to look exactly as it is shown in the left area. That means, the cropped image can be cropped on the bottom, the right, not at all or something like this. The formula to calculate this by myself would be a little bit greater.
Now I wonder if there isn't a better way. I have to draw text later on to this cropped image, so I cannot simply convert it to a DrawingContext, DrawingVisual, and so on and draw text in it, because that would also stretch the text later on the right side.
And if possible it would be nice to get a fast method, that can be redrawn a few times per second without killing the GPU.
For showing the preview image I use this XAML at the moment:
<Canvas Grid.Column="1"
Height="{Binding AreaHeight}" Width="{Binding AreaWidth}"
MaxWidth="426">
<Canvas.Background>
<VisualBrush TileMode="Tile"
Viewport="0, 0, 1, 1"
Stretch="{Binding SelectedStretch}">
<VisualBrush.Visual>
<Image Height="{Binding AreaHeight}" Width="{Binding AreaWidth}"
Source="{Binding PreviewImage}"
Margin="1" x:Name="TestImage"/>
</VisualBrush.Visual>
</VisualBrush>
</Canvas.Background>
</Canvas>
<Border Grid.Column="1"
BorderThickness="1" BorderBrush="Black"
Height="{Binding AreaHeight}" Width="{Binding AreaWidth}" />
And this code behind to calculate the image and fill it with the desired text (even if it is faulted at this moment):
BitmapSource imageSource = EditorImage.Source as BitmapSource;
DrawingVisual drawingVisual = new DrawingVisual();
using (DrawingContext drawingContext = drawingVisual.RenderOpen())
{
drawingContext.DrawImage(imageSource, new Rect(0, 0, imageSource.PixelWidth, imageSource.PixelHeight));
List<TextPosition> textPositions = GetTextPositions();
if (textPositions.Any())
{
textPositions.ForEach(x =>
drawingContext.DrawText(new FormattedText(x.Text, CultureInfo.CurrentCulture, FlowDirection.LeftToRight,
new Typeface(x.FontFamily, x.Style, x.Weight, FontStretch), x.FontSize, new SolidColorBrush(x.ForegroundColor),
VisualTreeHelper.GetDpi(drawingVisual).PixelsPerDip), x.Position)
);
}
}
PreviewImage = new RenderTargetBitmap(imageSource.PixelWidth, imageSource.PixelHeight, 96, 96, PixelFormats.Pbgra32);
PreviewImage.Render(drawingVisual);
TestImage.InvalidateVisual();
Ok, it doesn't seem to be possible. So I took a detour and do it now like this (if ever someone else does have a similar problem):
// Get image from the editor
BitmapSource bitmapSource = EditorImage.Source as BitmapSource;
// Get bitmap and transform it to the stretched version
Size scaleFactor = ComputeScaleFactor(new Size(AreaWidth, AreaHeight), new Size(bitmapSource.PixelWidth, bitmapSource.PixelHeight), SelectedStretch, StretchDirection.Both);
Size newCompleteSize = new Size(bitmapSource.PixelWidth * scaleFactor.Width, bitmapSource.PixelHeight * scaleFactor.Height);
Size renderSize = new Size
{
Width = newCompleteSize.Width > AreaWidth ? AreaWidth : newCompleteSize.Width,
Height = newCompleteSize.Height > AreaHeight ? AreaHeight : newCompleteSize.Height
};
Point imagePosition = GetImagePosition(renderSize);
BitmapImage bitmapImage = new BitmapImage();
using (MemoryStream memoryStream = new MemoryStream())
{
JpegBitmapEncoder encoder = new JpegBitmapEncoder();
encoder.Frames.Add(BitmapFrame.Create(bitmapSource));
encoder.Save(memoryStream);
memoryStream.Position = 0;
bitmapImage.BeginInit();
bitmapImage.StreamSource = memoryStream;
bitmapImage.CacheOption = BitmapCacheOption.OnLoad;
bitmapImage.DecodePixelWidth = (int)Math.Round(newCompleteSize.Width, 0, MidpointRounding.AwayFromZero);
bitmapImage.DecodePixelHeight = (int)Math.Round(newCompleteSize.Height, 0, MidpointRounding.AwayFromZero);
bitmapImage.EndInit();
}
DrawingVisual drawingVisual = new DrawingVisual();
using (DrawingContext drawingContext = drawingVisual.RenderOpen())
{
// Draw the copy of the image
drawingContext.DrawImage(bitmapImage, new Rect(imagePosition, new Size(bitmapImage.PixelWidth, bitmapImage.PixelHeight)));
// Check if text has to be drawn
List<TextPosition> textPositions = GetTextPositions();
if (textPositions.Any())
{
textPositions.ForEach(x =>
{
FormattedText text = new FormattedText(x.ReplacedText, CultureInfo.CurrentCulture, FlowDirection.LeftToRight,
new Typeface(x.FontFamily, x.Style, x.Weight, FontStretch), x.FontSize, new SolidColorBrush(x.ForegroundColor),
VisualTreeHelper.GetDpi(drawingVisual).PixelsPerDip)
{
TextAlignment = x.Alignment
};
TextOptions.SetTextRenderingMode(this, TextRenderingMode.Aliased);
TextOptions.SetTextFormattingMode(this, TextFormattingMode.Display);
drawingContext.DrawText(text, new Point(x.Position.X, x.Position.Y));
});
}
}
PreviewImage = new RenderTargetBitmap(
(int)Math.Round(AreaWidth, 0, MidpointRounding.AwayFromZero),
(int)Math.Round(AreaHeight, 0, MidpointRounding.AwayFromZero),
96, 96, PixelFormats.Pbgra32);
PreviewImage.Render(drawingVisual);
And in the XAML I use two images. The first one stretched, the other one not:
<Image x:Name="EditorImage"
HorizontalAlignment="{Binding AlignX}" VerticalAlignment="{Binding AlignY}"
MaxHeight="426" MaxWidth="426" Margin="1"
Height="{Binding AreaHeight}" Width="{Binding AreaWidth}"
Source="{Binding FileImagePath}" Stretch="{Binding SelectedStretch}" />
<Image Grid.Column="1"
VerticalAlignment="Center" HorizontalAlignment="Center"
Source="{Binding PreviewImage}"
Height="{Binding AreaHeight}" Width="{Binding AreaWidth}"
Stretch="None"/>

How do I get a Rectangle drawn on an adorner to scale with the Image element its bound to when the window size is changed?

I'm using a rectangle drawn on an adorner to mark a region of interest on an image. The issue is that if I resize the window, the rectangle doesn't change size.
I'm new to WPF, so I've done a bunch of research, googling what I can with multiple different search terms. I actually just learned adorners that way, and I've gotten this far on that, but I've hit a wall on how to finish this last piece. I know that my problem is based in the size of the rectangle, but I don't know what to capture/look for to adjust it, since wpf resizes the actual image object on window resize, so there's no scale factor to look at.
Here's the XAML for the application I'm testing things in.
<Window x:Class="TestingAdorners.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:TestingAdorners"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Grid ClipToBounds="True">
<AdornerDecorator>
<Image Name="Btn" Source="nyan.png" Stretch="Uniform"/>
</AdornerDecorator>
</Grid>
</Window>
The adorner class:
class RoiAdorner : Adorner
{
public Rect rectangle = new Rect();
public RoiAdorner(UIElement adornedElement) : base(adornedElement)
{
rectangle.Height = 30;
rectangle.Width = 100;
IsHitTestVisible = false;
}
protected override void OnRender(DrawingContext drawingContext)
{
Pen pen = new Pen(Brushes.Green, 5);
drawingContext.DrawRectangle(null, pen, rectangle);
}
}
And the Xaml.cs
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
AdornerLayer.GetAdornerLayer(Btn).Add(new RoiAdorner(Btn));
}
private void Window_Loaded(object sender, RoutedEventArgs e)
{
}
}
The desired result is that the rectangle scales with the image object so that it always covers the same region of the image. The problem is I don't know how to capture a scale factor to scale it up and down as the window resizes.
Update: After thinking through Frenchy's suggestion I realized the answer is simply: "Normalize your coordinates"
you just adapt your render method like this:
class RoiAdorner : Adorner
{
public double factorX = 0d;
public double factorY = 0d;
public Rect rectangle = new Rect();
public RoiAdorner(UIElement adornedElement) : base(adornedElement)
{
rectangle.Height = 30;
rectangle.Width = 100;
IsHitTestVisible = false;
}
protected override void OnRender(DrawingContext drawingContext)
{
if (factorY == 0)
factorY = rectangle.Height / AdornedElement.DesiredSize.Height;
if (factorX == 0)
factorX = rectangle.Width / AdornedElement.DesiredSize.Width;
var r = new Rect(new Size(AdornedElement.DesiredSize.Width * factorX, AdornedElement.DesiredSize.Height * factorY));
//Rect adornedElementRect = new Rect(this.AdornedElement.DesiredSize);
drawingContext.DrawRectangle(null, new Pen(Brushes.Red, 5), r);
}
this.AdornedElement.DesiredSize gives you the size of image.
The approach I would use is to render the picture and rectangle to the same thing. Then that one thing is stretched, scaled or whatever.
One way to do this would be to use a DrawingImage. Drawing methods are extremely efficient if rather low level.
<Grid ClipToBounds="True">
<AdornerDecorator>
<Image Name="img" Stretch="Uniform">
<Image.Source>
<DrawingImage PresentationOptions:Freeze="True">
<DrawingImage.Drawing>
<DrawingGroup>
<ImageDrawing Rect="0,0,595,446" ImageSource="DSC00025.jpg"/>
<GeometryDrawing Brush="Green">
<GeometryDrawing.Geometry>
<RectangleGeometry Rect="0,0,100,30" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
</DrawingGroup>
</DrawingImage.Drawing>
</DrawingImage>
</Image.Source>
</Image>
</AdornerDecorator>
</Grid>
Another is with a visualbrush. Controls inherit from visual - this is somewhat higher level coding.
<Grid ClipToBounds="True">
<AdornerDecorator>
<Rectangle Name="rec">
<Rectangle.Fill>
<VisualBrush Stretch="Uniform">
<VisualBrush.Visual>
<Grid Height="446" Width="595">
<Image Source="DSC00025.jpg" Stretch="Fill"/>
<Rectangle Height="30" Width="100" Fill="Green"
VerticalAlignment="Top" HorizontalAlignment="Left"/>
</Grid>
</VisualBrush.Visual>
</VisualBrush>
</Rectangle.Fill>
</Rectangle>
</AdornerDecorator>
</Grid>
Note that both of these are quick and dirty illustrations to give you the idea. The image I picked at random off my hard drive is sized 446 * 595. You could calculate sizes or bind or stretch as suits your requirement best.

Shapes in WPF canvas do not appear

I have encounteres a very weird issue with drawing shapes on a canvas in WPF.
<DockPanel Grid.Row="3">
<Canvas Name="BottomCanvas" Margin="15" Background="Yellow">
<Canvas Name="TransparentCanvas" Background="Transparent"
MouseDown="TransparentCanvas_MouseDown"
MouseUp="TransparentCanvas_MouseUp"
MouseMove="TransparentCanvas_MouseMove"
Width="{Binding RelativeSource={RelativeSource FindAncestor,
AncestorType={x:Type Canvas}},Path=ActualWidth}"
Height="{Binding RelativeSource={RelativeSource FindAncestor,
AncestorType={x:Type Canvas}},Path=ActualHeight}">
</Canvas>
</Canvas>
</DockPanel>
Above there is my canvas defined.
Below there is a simple example of drawing an elipse; I have no idea why this has no effect.
Ellipse myEllipse = new Ellipse();
SolidColorBrush mySolidColorBrush = new SolidColorBrush();
mySolidColorBrush.Color = Color.FromArgb(0, 0, 255, 0);
myEllipse.Fill = mySolidColorBrush;
myEllipse.Width = myEllipse.Height = 100;
Canvas.SetTop(myEllipse, 15);
Canvas.SetLeft(myEllipse, 15); ;
bottomCanvas.Children.Add(myEllipse);
Can anyone see what is wrong?
EDIT:
I'm sorry. As you can see in XAML canvas is named 'BottomCanvas' and in code I've used bottomCanvas which is a property added to a MainWindow I don't know what for. That was the deal.
You mean the Ellipse is not visible? It's just because of the transparent fill of SolidColorBrush. The Alpha value of the Color should not be zero, which means invisible. If so, just change the Alpha value of the Color:
mySolidColorBrush.Color = Color.FromArgb(255, 0, 255, 0);

Drawing line on a canvas in WPF MVVM doesn't work

I have this xaml:
<Canvas cal:View.Context="DrawCanvas">
<!--<Line X1="1" Y1="1" X2="400" Y2="400" Stroke="Black" StrokeThickness="20" IsHitTestVisible="False"/>-->
</Canvas>
and in model I have:
public Canvas DrawCanvas { get; set; }
public ImageSourceViewModel()
{
this.PropertyChanged += this.ImageSourceViewModel_PropertyChanged;
this.Scale = 1;
this.TranslateX = 0;
this.TranslateY = 0;
DrawCanvas=new Canvas();
var line = new Line();
line.X1= 1;
line.Y1 = 1;
line.X2 = 100;
line.Y2 = 10;
line.Stroke=new SolidColorBrush(Colors.Green);
line.StrokeThickness = 2;
line.Visibility=Visibility.Visible;
DrawCanvas.Children.Add(line);
}
I'm using Caliburn Micro.
It doesn't draw any line on output.
There could be two reason for this problem:
1- The canvas on view is not bind to DrawCanvas in ViewModel.
2- The drawing code is not correct.
How can I check that the my view canvas is actually bind to DrawCanvas in my ViewModel? Is the syntax for binding correct? I am using Caliburn Micro.
If the binding is correct, what the problem with drawing code that it is not working?
That is the way you can do it in MVVM (I modified the solution from here: https://stackoverflow.com/a/1030191/3047078):
In the view:
<ItemsControl ItemsSource="{Binding Path=Lines}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas Background="White" Width="500" Height="500" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Line X1="{Binding X1}" Y1="{Binding Y1}" X2="{Binding X2}" Y2="{Binding Y2}" Stroke="Black" StrokeThickness="3"></Line>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
In the ViewModel, you need something like this:
public ObservableCollection<MyLine> Lines {get;set;}
In the Model:
public class MyLine
{
public int X1 {get;set;}
public int Y1 {get;set;}
public int X2 {get;set;}
public int Y2 {get;set;}
}

How to resize a polygon in WPF?

I'm working on my first WPF application and I'm testing out a custom control that is essential a circle with a play button drawn into the middle of it. I seem to have hit a bit of a hitch though. When I draw my play button I can't seem to get it to resize alongside the circle. Specifically, when I resize the circle to be wider or taller, the play button polygon remains the same size and in the same absolute position. Any pointer on setting up my XAML or code to correct this?
Existing XAML:
<Window x:Class="WPFTest.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525" xmlns:my="clr-namespace:WPFTest">
<StackPanel>
<StackPanel.Resources>
<Style TargetType="my:GradientButton">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type my:GradientButton}">
<Grid>
<Ellipse Width="{TemplateBinding Width}" Height="{TemplateBinding Height}" Stroke="{TemplateBinding Foreground}" VerticalAlignment="Top" HorizontalAlignment="Left">
<Ellipse.Fill>
<LinearGradientBrush>
<GradientStop Color="{TemplateBinding GradientStart}" Offset="0"></GradientStop>
<GradientStop Color="{TemplateBinding GradientEnd}" Offset="1"></GradientStop>
</LinearGradientBrush>
</Ellipse.Fill>
</Ellipse>
<Polygon Points="{TemplateBinding PlayPoints}" Fill="{TemplateBinding Foreground}" />
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</StackPanel.Resources>
<my:GradientButton Content="Button" Height="50" x:Name="gradientButton1" Width="50" GradientStart="#FFCCCCCC" GradientEnd="#FFAAAAAA" PlayPoints="18,12 35,25 18,38" />
</StackPanel>
</Window>
Code:
public class GradientButton : Button
{
internal static DependencyProperty GradientStartProperty;
internal static DependencyProperty GradientEndProperty;
internal static DependencyProperty PlayPointsProperty;
static GradientButton()
{
GradientStartProperty = DependencyProperty.Register("GradientStart", typeof(Color), typeof(GradientButton));
GradientEndProperty = DependencyProperty.Register("GradientEnd", typeof(Color), typeof(GradientButton));
PlayPointsProperty = DependencyProperty.Register("PlayPoints", typeof(PointCollection), typeof(GradientButton));
}
public Color GradientStart
{
get { return (Color)base.GetValue(GradientStartProperty); }
set { SetValue(GradientStartProperty, value); }
}
public Color GradientEnd
{
get { return (Color)base.GetValue(GradientEndProperty); }
set { SetValue(GradientEndProperty, value); }
}
public PointCollection PlayPoints
{
get
{
//this is where I'm trying to make the resizing dynamic, but this property never seems to get hit when I put in a breakpoint?
PointCollection result = new PointCollection();
double top = this.Width / 2.778;
double left = this.Width / 4.167;
double middle = this.Height / 2.00;
double right = this.Width / 1.429;
double bottom = this.Height / 1.316;
result.Add(new Point(left, top));
result.Add(new Point(right, middle));
result.Add(new Point(left, bottom));
return result;
}
set { SetValue(PlayPointsProperty, value); }
}
}
You need to set the Stretch property of the Polygon to Uniform

Categories