How to render (bitmap) only part of a Visual? - wpf

I must print a displayed TreeView.
Rendering the root TreeViewItem to bitmap, gives me an image of the whole (even non visible nodes) tree. Then I split the bitmap in "pages" to be printed. The rendering code:
m_Bitmap = new RenderTargetBitmap((int)l_RootTreeViewItem.ActualHeightDesiredSize.Width,
(int)l_RootTreeViewItem.ActualHeight, 96, 96,
PixelFormats.Pbgra32);
m_Bitmap.Render(l_RootTreeViewItem);
Works well for small size trees. If the tree is large, RenderTargetBitmap results in "Out Of Memory" Exception.
So, the idea is to render only parts of the visual to avoid memory problems. A Render method where I can choose which part of visual to render will be perfect...
m_Bitmap.Render(l_RootTreeViewItem, xOffset, yOffset, width, height);
... but doesn't exist. Is there some way to do that ?

What I'll do :
Create a VisualBrush of your l_RootTreeViewItem
Create a Rectangle and assign the visual brush to the Fill property
Play with VisualBrush.Viewbox and VisualBrush.Viewport to render the part of the tree view I'm interested in
Use RenderTargetBitmap.Render on my rectangle when needed
EDIT
Solution 2
Put l_RootTreeViewItem in a canvas
Set the ClipToBounds property of the canvas to true
Play with Canvas.Width, Canvas.Height properties and Canvas.Left, Canvas.Top attached properties to display only a part of the TreeViewItem
Use PrintDialog.PrintVisual on the canvas as needed.
<Canvas Width="300" Height="300" ClipToBounds="True">
<TreeViewItem Canvas.Left="-200" Canvas.Top="-100">
...
</TreeViewItem>
</Canvas>

Related

How to zoom image inside rectangle WPF

I am trying to zoom in and out image inside Rectangle control. But while doing so my entire Rectangle is getting zoomed in instead of just image inside that. for doing so I am using ScaleTransform and TranslateTranform on Rectangle. I should do the same on image instead of Rectangle but I dont know how? Could anyone please help me out.
XAML :
<Rectangle x:Name="LiveViewWindow" Fill="#FFF4F4F5" HorizontalAlignment="Right"
ClipToBounds="True" />
Code:
InteropBitmap m_LiveViewBitmapSource =Imaging.CreateBitmapSourceFromMemorySection(
section, width, height, PixelFormats.Bgr32, width*4, 0) as InteropBitmap;
ImageBrush m_BackgroundFrame = new ImageBrush(m_LiveViewBitmapSource);
RenderOptions.SetBitmapScalingMode(m_BackgroundFrame, BitmapScalingMode.LowQuality);
LiveViewWindow.Fill = m_BackgroundFrame;
n then for I am using Invalidate() property to render InteropBitmap to Rectangle.
You may set the Transform property of the ImageBrush. Get more information in Brush Transformation Overview.

Saving UserControl as a JPG - result is "squished"

I'm having a strange issue with saving a UserControl as a JPG. Basically what I want to do is create a Live Tile that I can update using a Background Agent (as well as from within the app itself.)
I've followed the steps in this blog post which work great for when I create the tile; I have a custom UserControl with some TextBlocks on it which gets saved as a JPG into IsolatedStorage, exactly as the post says.
Creating the tile is no problem - the UserControl is rendered nicely as it should be. However, when I try to update the tile (using the exact same method - saving a new control as a JPG, then using that JPG as the BackgroundImage), things break down.
The resulting image that gets placed on the tile (and saved in IsolatedStorage) looks like this: squished tile (pulled from IsolatedStorage using the Isolated Storage Explorer Tool)
The background is black, and all the text runs down the side of the image (and overlaps each other) - the expected result is the background being the phone's accent colour, and the text appearing horizontal near the top.
The code used to generate and save the image is exactly the same in both instances - I've abstracted it out into a static method that returns StandardTileData. The only difference is where it is called from: in the working case where the tile is created, it's called from a page within the main app; in the non-working case (where the tile is updated), it's called from a page that can only be accessed by deep-linking from the tile itself.
Any thoughts? I'm guessing something's going wrong with the rendering of the Control to a JPG, since the actual image comes out this way.
The snippet of code that generates the image is here:
StandardTileData tileData = new StandardTileData();
// Create the Control that we'll render into an image.
TileImage image = new TileImage(textA, textB);
image.Measure(new Size(173, 173));
image.Arrange(new Rect(0, 0, 173, 173));
// Render and save it as a JPG.
WriteableBitmap bitmap = new WriteableBitmap(173, 173);
bitmap.Render(image, null);
bitmap.Invalidate();
IsolatedStorageFile storage = IsolatedStorageFile.GetUserStoreForApplication();
String imageFileName = "/Shared/ShellContent/tile" + locName + ".jpg";
using (IsolatedStorageFileStream stream = storage.CreateFile(imageFileName))
{
bitmap.SaveJpeg(stream, 173, 173, 0, 100);
}
tileData.BackgroundImage = new Uri("isostore:" + imageFileName, UriKind.Absolute);
return tileData;
The XAML for the control I'm trying to convert is here:
<UserControl x:Class="Fourcast.TileImage"
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"
mc:Ignorable="d"
FontFamily="{StaticResource PhoneFontFamilyNormal}"
FontSize="{StaticResource PhoneFontSizeNormal}"
Foreground="{StaticResource PhoneForegroundBrush}"
d:DesignHeight="173" d:DesignWidth="173" FontStretch="Normal" Height="173" Width="173">
<Border Background="{StaticResource PhoneAccentBrush}">
<StackPanel>
<TextBlock HorizontalAlignment="Stretch"
TextWrapping="Wrap" VerticalAlignment="Stretch" Style="{StaticResource PhoneTextLargeStyle}" x:Name="Temperature"></TextBlock>
<TextBlock x:Name="Condition" HorizontalAlignment="Stretch"
TextWrapping="Wrap" VerticalAlignment="Stretch" Style="{StaticResource PhoneTextNormalStyle}">
</TextBlock>
</StackPanel>
</Border>
</UserControl>
Update: after some investigation with the debugger, it appears the calls to Measure and Arrange don't seem to do anything when the method is called from the class that's updating the tile. When the tile is being created, however, these calls function as expected (the ActualWidth and ActualHeight of the control get changed to 173).
Not sure what exactly is going on here, but here's what I ended up doing to workaround the issue.
I switched from trying to render a Control to defining a StackPanel in code, adding elements to it, then rendering that to an image. Oddly enough, though, the Arrange and Measure methods don't do anything unless another element has had them applied to it, and UpdateLayout called.
The full resulting code (minus the contents of the TextBlocks and writing to an image) is below.
StandardTileData tileData = new
// None of this actually does anything, but for some reason the StackPanel
// won't render properly without it.
Border image = new Border();
image.Measure(new Size(173, 173));
image.Arrange(new Rect(0, 0, 173, 173));
image.UpdateLayout();
// End of function-less code.
StackPanel stpContent = new StackPanel();
TextBlock txbTemperature = new TextBlock();
txbTemperature.Text = temperature;
txbTemperature.Style = (Style)Application.Current.Resources["PhoneTextLargeStyle"];
stpContent.Children.Add(txbTemperature);
TextBlock txbCondition = new TextBlock();
txbCondition.Text = condition;
txbCondition.Style = (Style)Application.Current.Resources["PhoneTextNormalStyle"];
stpContent.Children.Add(txbCondition);
stpContent.Measure(new Size(173, 173));
stpContent.Arrange(new Rect(0, 0, 173, 173));
The way to get the background of the image to use the phone's accent color is to make it transparent. This will require that you save a PNG rather than a JPG.
The ouput image looks like everything is wrapping more thna it needs to. You probably need to adjust the widths of the elements you're including in your control. Without the XAML it's hard to be more specific.
I also get this strange behaviour - squished tile (using Ree7 Tile toolkit),
Try call update tile into Dispatcher:
Deployment.Current.Dispatcher.BeginInvoke(() =>
{
CustomTile tile = GetTile(card);
StandardTileData tileData = tile.GetShellTileData();
shellTile.Update(tileData);
});
Working for me

How to retrieve Canvas dimensions in the viewmodel

I've got a Canvas element in a resizable window; on this canvas are a number of Image and ArcElements that are connected together. I'm trying to get the position of the images to adjust relatively in response to a resize of the window/canvas, but for some reason I can't read the dimensions of the canvas.
The main window is defined as:
<Page>
<DockPanel LastChildFill="True">
<TextBox DockPanel.Dock="Top">Message</TextBox>
<Canvas></Canvas>
</DockPanel>
</Page>
I've hooked up MvvMLight's EventToCommand so that I can route the Canvas's LayoutUpdated or
SizeChanged events to my viewmodel; I tried databinding the Canvas's Width and Height properties, but the dimensions always came out as zero, which meant that all the images on the canvas would appear dead centre rather than positioned as desired.
It turns out I was heading in the right direction by using MvvmLight's EventToCommand; there is an attribute PassEventArgsToCommand that when set to True sends the event args to the appropriate RelayCommand in the viewmodel. So in the viewmodel, I initialised the command thusly:
Commands.ResizeCommand = new RelayCommand<SizeChangedEventArgs>(action => RecalculateObjectPositions(action));
and in the RecalculateObjectPositions method, I can access e.NewSize to find the new size of the canvas.

Why ActualWidth and ActualHeight start from 0,0?

I want to add WPF Path to InkCanvas and use selection to select WPF Path.
So, I use this code.
System.Windows.Shapes.Path path = drawCanvas.Children[i] as System.Windows.Shapes.Path;
drawCanvas.Children.RemoveAt(i);
inkCanvas.Children.Add(path);
This is the output. I have to select WPF Path from 0,0 because Actualwidth and ActualHeight start from 0,0.
How do I select absolute WPF Path?
Thanks
Edit:
Now, I can select it absolutely by using this code.
System.Windows.Shapes.Path path = drawCanvas.Children[i] as System.Windows.Shapes.Path;
drawCanvas.Children.RemoveAt(i);
path.Margin = new Thickness(-getMinX(path), -getMinY(path), 0, 0);
containPath.Children.Add(path);
containPath.Width = getMaxX(path) - getMinX(path);
containPath.Height = getMaxY(path) - getMinY(path);
containPath.Margin = new Thickness(getMinX(path), getMinY(path), 0, 0);
inkCanvas.Children.Add(containPath);
You can use the UIElement.UpdateLayout Method on the InkCanvas to update the FrameworkElement.ActualWidth Property and ActualHeight. See the ActualWidth link for background information on why this is needed.
Edit:
I misunderstood the question. It wasn't that ActualWidth and ActualHeight were zero but that their values were relative to (0, 0) on the InkCanvas. A pretty good solution to the problem is to wrap the Path in a Canvas and position it using Margin like this:
<Canvas Width="50" Height="50" Margin="200,200,0,0">
<Line
X1="0" Y1="0"
X2="50" Y2="50"
Stroke="Black"
StrokeThickness="4" />
</Canvas>
which behaves like:
The disadvantage of this approach is the user has to lasso the Canvas which is a rectangle, not an arbitrary shape. Nevertheless, it's a lot better than having to lasso the object and the origin.
For elements that can only be defined via control points (eg Line, Path etc as against Rectangle, Ellipse etc) when you add it to an items control (which I assume the InkCanvas is since it supports selection), first a canvas is added to the panel at 0,0. The width and the Height of the canvas are determined from the maximum X and Y coordinates of the control points. After this the element is added as a child of this canvas.
You will also see that whenever you see this kind of behaviors with an element, the element wont support Layout Properties like Horizontal/Vertical alignment etc.
The only way I see around this is to find the ActualWidth and ActualHeight manually from the coordinates in the path.
Edit: This is from personal experience and not from any documentation.

need to add textBlocks to a group box and clear when data is loaded in a graph control

I'm creating a graph control. what i'm doing for adding x and y axis tally labels is i'm
adding a text block to each tally mark and show the value related to that tally mark.
but when i need to load data form the database and redraw the textbolcks again and refresh the graph area i can't remove the older textblocks they are still on the graph pane.
to overcome this problem i thought to put the text blocks in side a group box and when graph pane is redrawn to delete the group box elements and put them again..
is this approach correct?
please tell me how to put elements to groupbox in code behind class?
and please tell me if their is any other solution to my problem.
regards,
rangana.
In WPF there are many solutions to most problems. I will discuss three possible solutions to your problem - the one you describe and two others. You can decide which will work best for you.
Solution 1: Using TextBlock objects to disply the labels
It sounds like you have a Canvas and you're adding a TextBlock to it for each tick mark. This is a viable solution if performance isn't too critical and you can't use data binding.
There are two ways to remove the TextBlocks in this case:
You can keep a List<TextBlock> containing all the Textblocks list of the TextBlocks you created the last time you created the labels. Whenever you recreate the labels, run through this list and remove each TextBlock on the list from the containing panel (the Canvas)
You can create a new Canvas and put the TextBlocks on it, then delete the whole Canvas when you relabel.
Here is an example of the second technique, since it is slightly more efficient:
class MyGraphBuilder
{
Canvas _labelCanvas;
...
void AddLabels()
{
// Remove old label canvas, if any
if(_labelCanvas!=null)
_graphCanvas.Children.Remove(_labelCanvas);
// Add new label canvas
_labelCanvas = new Canvas();
_graphCanvas.Children.Add(_labelCanvas);
// Create labels
foreach(...)
{
...
_labelCanvas.Add(new TextBlock ...
}
...
}
}
Solution 2: Using data binding
In WPF you can create many graphs without writing a single line of code! WPF's built in data binding is sufficient to create relatively complex bar charts, etc.
Here is an example of using data binding to create a simple bar chart:
<ItemsControl ItemsSource="{Binding myData}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<DockPanel>
<TextBlock Width="50" Text="{Binding Label}"/>
<Rectangle VerticalAlignment="{Stretch}" Width="{Binding Value}">
<Rectangle.LayoutTransform>
<ScaleTransform ScaleX="10" /> <!-- Scale factor here, can be binding too -->
</Rectangle.LayoutTransform>
</Rectangle>
<TextBlock Text="{Binding Value}" FontSize="8"/>
</DockPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
Numeric labels can be added to the horizontal axis by using a second ItemsControl laid out horizontally, and with its data template a fixed width and showing numbers and tick marks.
Solution 3: Using low level Drawing classes
Build your graph by constructing a DrawingGroup object and adding GeometryDrawing and GlyphRunDrawing objects to it, then putting the DrawingGroup inside DrawingVisual and add that to your main Panel.
You should use one GeometryDrawing or GlyphRunDrawing for each set of items sharing a given brush and pen. For example if your axes and tick marks are all the same color and width, create a single GeometryDrawing for all of them, but if each tick mark is a differnet color, create multiple GeometryDrawing objects.
You will create a Geometry object for each GeometryDrawing. For the best efficiency it should be a StreamGeometry, but the other Geometry classes also work well, may be easier to use, and may be initialized in XAML. Creating a PathGeometry or EllipseGeometry is probably already familar to you so I'll focus on creating a StreamGeometry. You do this by calling the Open method in a using() statement, then writing to the returned context. Here is an example:
Geometry BuildAxesAndTicksGeometry()
{
// First create geometry
var geometry = new StreamGeometry();
using(var context = geometry.Open())
{
// Horizontal axis
context.BeginFigure(new Point(0,0), false, false);
context.LineTo(new Point(_width, 0), true, false);
// Vertical axis
context.BeginFigure(new Point(0,0), false, false);
context.LineTo(new Point(0, _height), true, false);
// Horizontal ticks
for(int i=0; i<_nTicksHorizontal; i++)
{
context.BeginFiture(new Point(i * _tickSpacing, -10), false, false);
context.LineTo(new Point(i * _tickSpacing, 10), true, false);
}
// Do same for vertical ticks
}
// Now add it to a drawing
return new GeometryDrawing { Geometry = geometry, Stroke = _axisPen };
}
Drawing BuildDrawing()
{
var mainDrawing = new DrawingGroup();
mainDrawing.Add(BuildAxesAndTicksGeometry());
... // Add other drawings, including one or more for the data
return mainDrawing;
}
void UpdateDrawing()
{
myDrawingVisual.Drawing = BuildDrawing(); // where myDrawingVisual is defined in the XAML
}
Comparison of solutions
For most cases I would recommend solution 2 or 3, for these reasons:
If the graph is simple enough to use data binding it will save you a lot of time. Go with solution 2.
If the graph cannot be done with data binding, using Drawing objects is approximately as simple as any other technique, and can perform better. Go with solution 3.
In your case if you've already invested significant work into your Solution 1, you may want to stick with it even though it probably isn't the best.

Resources