Drawing Viewbox using a DrawingContext - wpf

I'm writing a WPF application. All icons I use are vector and stored in standalone ResourceDictionary as series of Viewboxes:
<ResourceDictionary>
<ViewBox x:Shared="False" x:Key="IconKey" Stretch="Uniform">
<Canvas Width="16" Height="16">
<Path ... />
</Canvas>
</ViewBox>
<!-- ... -->
</ResourceDictionary>
I'm implementing now a user control, which, due to very specific requirements, is drawn from scratch by me in OnRender method.
I need to render these icons on my control. One solution is to rasterize them and then draw bitmaps, but the problem is, that view in my control can be freely zoomed, and such icons would look ugly when zoomed in (as opposed to vector ones). Is there a way to render a ViewBox using the DrawingContext?

I don't know how you can render ViewBox but you can render Geometry and that will not be ugly when zoomed.
I have created icon in a XAML file. Build Action: Page.
File:Icon.xaml
<?xml version="1.0" encoding="UTF-8"?>
<Viewbox xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" Stretch="Uniform">
<Canvas Width="25" Height="25">
<Path Fill="#FF000000" StrokeThickness="0.4">
<Path.Data>
<PathGeometry Figures="M 4.9586532 4.4020592 12.198751 19.198359 V 0.91721594 H 20.200561 h 2.894515 Z" FillRule="Nonzero"/>
</Path.Data>
</Path>
</Canvas>
</Viewbox>
To draw this with DrawingContext create method like this
void DrawIcon(DrawingContext context, double left, double top, double scale = 1)
{
var transform = new MatrixTransform(scale, 0, 0, scale, left, top);
var viewBox = (Viewbox) Application.LoadComponent(new Uri("/MyProjectNameSpace;component/Icon.xaml", UriKind.Relative));
var canvas = viewBox.Child as Canvas;
if (canvas?.Children == null) return;
foreach (UIElement child in canvas.Children)
if (child is Path path)
{
path.Data.Transform = transform;
context.DrawGeometry(Brushes.Black, new Pen(Brushes.Black, 1), path.Data);
}
}

Related

What would cause WPF DragDelta values to become suddenly cumulative with each call?

I have UserControl wrapping a zoomable canvas with shapes that the user can drag.
Recently I tried to make changes to follow the advice I got in this question; Instead applying one transform to the the whole canvas with shapes, I tried to apply it individually to each shape's Geometry. The idea was to eliminate zooming effects on font sizes and line thicknesses.
Unfortunately, although I have most of it working (shapes show up where they should) I seem to have broken the dragging of individual shapes across the canvas. I cannot explain why.
The symptom is that the DragDelta event handler is reporting cumulative values for each HorizontalChange and VerticalChange. So my shapes now zoom off the edge of the screen with even small movements. They use to not be that way.
(Setting the "Handled" boolean on the event args has no effect)
I've posted some code below. While I am grateful to anyone who bothers to read through it, I'm actually more interested in the general concept of how someone can make DragDelta report cumulative values at all. The documentation states that they're only supposed to be the values since the last call to DragDelta.
I cannot be the first person to make this mistake. Is this a common problem with a common cause?
Anyway if you are still reading, here's the detail. The shapes are represented by an interface, IShape.
public interface IShape
{
Geometry RelativeGeometry { get; } // Geometry relative to shape origin
Geometry AbsoluteGeometry { get; } // Geometry relative to canvas origin
}
The IShape objects are drawn in an ItemsControl on a Canvas inside my UserControl
<Canvas x:Name="Scene">
<ItemsControl x:Name="ShapesLayer" ItemsSource="{Binding Shapes}"
ItemTemplate={StaticResource ShapeTemplate}"/>
</Canvas>
This is the ItemTemplate the items control uses to draw an IShape
<DataTemplate x:Key="ShapeTemplate" DataType="{x:Type gci:IShape}">
<Canvas>
<!-- Visible shape (not selectable) -->
<Path Data="{Binding AbsoluteGeometry, {StaticResource TransformGeometry}, ConverterParameter={StaticResource Trans}}"
IsHitTestVisible="False"/>
<!-- Invisible but selectable thumb control for dragging shape -->
<Thumb">
<Thumb.Style>
<EventSetter Event="DragDelta" Handler="Shape_DragDelta"/>
</Thumb.Style>
<Thumb.Template>
<ControlTemplate TargetType="{x:Type Thumb}">
<Path Fill="Transparent" Stroke="Transparent">
<Path.Data>
<RectangleGeometry Rect="{Binding AbsoluteGeometry.Bounds}"
Transform="{StaticResource Trans}"/>
</Path.Data>
</Path>
</ControlTemplate>
</Thumb.Template>
</Thumb>
</Canvas>
</DataTemplate>
This is the DragDelta handler for the shape thumb control in code-behind
private void Shape_DragDelta(object sender, DragDeltaEventArgs e)
{
if (!(sender is FrameworkElement fe) || !(fe.DataContext is IShape shape))
return;
// Move each selected shape by the delta.
var v = new Vector {X = e.HorizontalChange, Y = e.VerticalChange};
foreach (var s in Shapes.Where(shp => shp.IsSelected))
{
if (s.Offset(v) && !(_movedShapes.Contains(s)))
_movedShapes.Add(s);
}
e.Handled = true;
}
Finally, this is the transform, declared in my control's resources and used by all shapes. It builds off of custom Scale and OffsetX/OffsetY dependency properties in the control. My control sets those values in code-behind with mouse/touch handlers. They are propagated to this resource and thus to the shapes.
<TransformGroup x:Key="Trans">
<!-- "Root" is the name of the control. It exposes custom dependency
properties "ZoomScale, "OffsetX" and "OffsetY" which apply to the entire
control and therefore are not changed when a shape is dragged -->
<ScaleTransform CenterX="0" CenterY="0"
ScaleX="{Binding ElementName=Root, Path=ZoomScale}"
ScaleY="{Binding ElementName=Root, Path=ZoomScale}"
/>
<TranslateTransform X="{Binding ElementName=Root, Path=OffsetX}"
Y="{Binding ElementName=Root, Path=OffsetY}"/>
</TransformGroup>
<!-- Value Converter that applies a transform to a Geometry -->
<gcc:TransformGeometryConverter x:Key="TransformGeometry"/>

How to adjust a canvas that child with position (-1,-1) is displayed completely

NEW INFORMATION!!!
Meanwhile i have found a solution but there is a new problem.
The solution is to set the margin of the canvas in code-behind to a new object of type Thickness with top and left 1 or 2.
But the canvas is lying on a tabcontrol.
When i switch between tabs or make a mousedown on the canvas the margin is lost.
I'm working with VS2015 on a WPF-application and have a very curious problem.
I got in one of my WPF windows a canvas as parent for some child elements.
One of these elements is a rectangle which shall show the user the size of a DIN-A4 page.
It is added in code-behind to the children collection of the canvas.
Normally i would place that rectangle at position (0,0).
But because of some problems i have to trick and set the position to (-1,-1) like that:
public static System.Windows.Shapes.Rectangle GetRectangle(double top, double left, double width, double height)
{
var rectangle = new System.Windows.Shapes.Rectangle();
System.Windows.Controls.Canvas.SetLeft(rectangle, -1);
System.Windows.Controls.Canvas.SetTop(rectangle, -1);
rectangle.Width = Math.Round(GetSizeInPoint(width)) + 2;
rectangle.Height = Math.Round(GetSizeInPoint(height));
rectangle.StrokeThickness = 1;
rectangle.Stroke = System.Windows.Media.Brushes.Red;
return rectangle;
}
But the result of it is that just a small part of the top and left border of the rectangle can be seen.
Do i have a chance to "move" the canvas so that the rectangle is displayed completely?
Hereby another important problematic point is that the Grid named "grdProtocolDesigner" can be serialized to XML and saved in the database.
So a complete restructuring would be a big problem.
Here the relevant part of my XAML including the canvas:
<ContentPresenter x:Name="protocolContainer"
Grid.Row="1"
Grid.Column="0">
<ContentPresenter.Content>
<Grid x:Name="grdProtocolDesigner"
Grid.Row="1"
Grid.Column="0">
<ScrollViewer>
<!--Important! The background of this panel has to be "Transparent" so that drag'n'drop-events can work.
Otherwise the events are not fired.-->
<Canvas x:Name="protocolDesignerPanel"
AllowDrop="True"
Visibility="{Binding DesignerPanelsVisibility}"
Width="2200" Height="4000"
MouseEnter="designerpanel_MouseEnter"
MouseLeave="designerpanel_MouseLeave"
MouseDown="designerpanel_MouseDown"
MouseMove="designerpanel_MouseMove"
MouseUp="designerPanel_MouseUp">
<Canvas.RenderTransform>
<ScaleTransform x:Name="scaleProtocolLayout"/>
</Canvas.RenderTransform>
<Canvas.Background>
<ImageBrush ImageSource="../../pictures/CanvasBackground.png"
TileMode="Tile"
Stretch="None"
Viewport="0, 0, 10, 10"
ViewportUnits="Absolute" />
</Canvas.Background>
</Canvas>
</ScrollViewer>
</Grid>
</ContentPresenter.Content>
</ContentPresenter>

In WPF what is the proper method for drawing a line and PNGs on a bitmap that is attached to a canvas?

I don't do that much with WPF anymore and I always seem at a loss for the most basic things. I've tried the Googles but they're not helping.
I've got a canvas (maybe I shouldn't use a canvas?) and I want to attach an image. The only way that I could find to do this was like so:
<Canvas Grid.Column="2" HorizontalAlignment="Right" Height="822" VerticalAlignment="Top" Width="1198" Name="MainCanvas">
<Canvas.Background>
<ImageBrush ImageSource="/MapDesignModule;component/MapFrame.bmp" Stretch="None" AlignmentY="Top" AlignmentX="Right"/>
</Canvas.Background>
</Canvas>
Now, I need to draw lines on the image attached to the canvas (later I will also have to place transparent PNGs, or BMPs with white set to Alpha 0, on the image as well).
In the past I would get a writeablebitmap from the image.source but you apparently can't do that from an ImageBrush?
What is the 'proper way' to put an image on the screen and draw and blit images onto it?
Just put multiple Image and Line elements on top of each other in a common Panel, e.g. a Canvas:
<Canvas>
<Image Source="/MapDesignModule;component/MapFrame.bmp" />
<Image Source="/MapDesignModule;component/Overlay.png" />
<Line X1="100" Y1="100" X2="200" Y2="200" Stroke="Black" />
</Canvas>
You could also assign a name to the Canvas
<Canvas x:Name="canvas">
to access it in code behind:
canvas.Children.Add(new Image
{
Source = new BitmapImage(new Uri(
"pack://application:,,,/MapDesignModule;component/MapFrame.bmp"))
});
canvas.Children.Add(new Line
{
X1 = 100,
Y1 = 100,
X2 = 200,
Y2 = 200
});
In case you later want to add multiple shape overlays, you may consider using an ItemsControl with an appropriate view model, e.g. as shown here: https://stackoverflow.com/a/40190793/1136211

Re-sizing WPF Geometry Path

I'm working on a WPF application. Given a geometry string path, such as:
F1 M 27,18L 23,26L 33,30L 24,38L 33,46L 23,50L 27,58L 45,58L 55,38L 45,18L 27,18 Z
Is it possible to scale the drawing to a width and height (no matter how small/large the original was) while keeping the figure as a whole, and then finally return the string path representation of the new scaled figure?
There is no need to scale the values in a path geometry string. Just put it in the Data property of a Path control and set its Width, Height and Stretch properties as needed:
<Path Data="F1 M27,18 L23,26 33,30 24,38 33,46 23,50 27,58 45,58 55,38 45,18 27,18 Z"
Width="100" Height="100" Stretch="Uniform" Fill="Black"/>
Yes you can do it! The only thing you need to do, is to use a Viewbox for wrapping the item. This is a sample with a code that I had done, in this case I used the geometry as a DrawingBrush
...
<UserControl.Resources>
<DrawingBrush x:Key="Field" Stretch="Uniform">
<DrawingBrush.Drawing>
<DrawingGroup>
<GeometryDrawing Brush="{StaticResource FieldGrassBrush}" Geometry="F1 M 91.733,119.946C 92.8352,122.738 93.9374,125.529 92.9241,129.209C 91.9107,132.889 88.7819,137.458 84.4263,139.271C 80.0707,141.084 74.4885,140.142 70.8885,137.982C 67.2885,135.822 65.6707,132.444 65.1819,129.182C 64.693,125.92 65.333,122.773 65.973,119.626L 0.16,53.9203C 0.444319,53.4758 0.728638,53.0312 3.48413,48.7023C 6.23962,44.3733 11.4663,36.16 18.5596,28C 25.653,19.84 34.613,11.7333
........
</DrawingGroup>
</DrawingBrush.Drawing>
</DrawingBrush>
</UserControl.Resources>
...
Then the View Box (Note that the Grid has fixed Height and Width, but it will be stretched to the Viewbox's size, in this case, in an uniform way):
...
<Viewbox Stretch="Uniform" Grid.Row="2" Grid.ColumnSpan="2">
<Grid Height="300" Width="390">
<Rectangle Fill="{DynamicResource Field}" StrokeThickness="0"/>
...

In WPF, view a portion of an image

We have an image where we create view box coordinates that are topleft/bottom right points within the image that are setup to allow for viewing portions of an image at different times in our application. In WPF, how do we load an image, and with topleft/bottom right points within that image, only show the portion of the image within that view box?
You can do this with a CroppedBitmap:
<Image>
<Image.Source>
<CroppedBitmap Source="<path to source image>" SourceRect="20,20,50,50"/>
</Image.Source>
</Image>
This will display the 50x50 region of the image starting at position (20,20)
Using a RenderTransform with a Clip works even better, because the CroppedBitmap is kinda immutable:
<Image x:Name="MyImage">
<Image.RenderTransform>
<TranslateTransform X="-100" Y="-100" />
</Image.RenderTransform>
<Image.Clip>
<RectangleGeometry Rect="0 0 250 250" />
</Image.Clip>
</Image>
This will display the image at offset (100, 100) with a size of (150, 150), so don't forget the rect must include the startoffsets.
Here's a method to calculate it in code:
public static void ClipImage(System.Windows.Controls.Image image, Rect visibleRect)
{
image.RenderTransform = new TranslateTransform(-visibleRect.X, -visibleRect.Y);
image.Clip = new RectangleGeometry
{
Rect = new Rect(
0,
0,
visibleRect.X + visibleRect.Width,
visibleRect.Y + visibleRect.Height)
};
}
It seems to me that you can make the image control a part of the viewbox as shown below:
<Viewbox Name="vBox" Stretch="None" HorizontalAlignment="Left"
VerticalAlignment="Top" Height="50" Width="50">
<Image Name="ClippedImage"
Source="{Binding NotifyOnSourceUpdated=True, NotifyOnTargetUpdated=True}"
Stretch="None" />
</Viewbox>
This will give you a view box 50x50. obviously you can change the height and width to suit your needs. I use a scrollviewer to pan around the smaller viewbox.

Resources