Today I'm working with WPF! My app is almost finished, except for the last feature. I have created a graphic element which represents a label like the ones you can see under the goods in a supermarket shelf. Print one of them is easy, using PrintDialog.PrintVisual(myVisualItem,"description");.
Now, I have to print a series of these labels, using PintVisual() in a loop, prints every element in a separate document! There's no overload of PrintVisual() which accepts an IEnumerable of visual item. I've seen online that i should paginate these elements, but I cannot find a way to do it What can I do? Thanks!
You could combine all canvas you got into one big canvas and then print this one.
Here is some sample code how to concat multiple canvas into one big one:
public Canvas CombineCanvases(params Canvas[] canvases)
{
// Force each canvas to update its desired size
foreach (var canvas in canvases) { canvas.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity)); }
var result = new Canvas
{
Height = canvases.Sum(x => x.DesiredSize.Height), // add all heights to get needed height
Width = canvases.Max(x => x.DesiredSize.Width) // find required width
};
var pos = 0d;
foreach (var canvas in canvases)
{
Canvas.SetLeft(canvas, 0);
Canvas.SetTop(canvas, pos); // position element at this pixel count
pos += canvas.DesiredSize.Height; // and increment it
result.Children.Add(canvas);
}
return result;
}
You need to pass all canvas's you want to print and print the result.
var c1 = new Canvas
{
Width = 100,
Height = 100,
Children = { new TextBlock { Text = "Canvas 1" } }
};
var c2 = new Canvas
{
Width = 100,
Height = 100,
Children = { new TextBlock { Text = "Canvas 2" } }
};
var canvas = CombineCanvases(c1, c2);
// print canvas here
Pagination would be if you check how many labels fit on one page and then not concat all canvas into one huge on, but just take as many as are needed to fill one page.
Then print the page and repeat this until all Labels were printed. This is called pagination.
If you believe they will all fit on one page, you could put them in a StackPanel with Orientation = Vertical. Then print the stack panel.
var panel = new StackPanel() { Orientation = Orientation.Vertical };
panel.Children.Add(canvas1);
panel.Children.Add(canvas2);
panel.UpdateLayout();
var dlg = new PrintDialog();
dlg.PrintVisual(panel, "description");
Related
I have a WPF window containing a Canvas which is populated with rotated Rectangles in code. The rectangles each have a MouseDown event and their positions will be distributed according to coordinates provided by the user. Often two or more will overlap, partially obstructing the rectangle beneath it.
I need the MouseDown event to fire for each rectangle that is under the mouse when it is pressed, even if that rectangle is obstructed by another rectangle, but I am only getting the MouseDown event for the topmost rectangle.
I have tried setting e.Handled for the clicked rectangle, and routing the events through the Canvas with no luck, and even gone as far as trying to locate the objects beneath the mouse based on their coordinates, but the rotation of the rectangles make that difficult to calculate.
public MainWindow()
{
InitializeComponent();
Rectangle r1 = new Rectangle() {Width = 80, Height = 120, Fill = Brushes.Blue };
r1.MouseDown += r_MouseDown;
RotateTransform rt1 = new RotateTransform(60);
r1.RenderTransform = rt1;
Canvas.SetLeft(r1, 150);
Canvas.SetTop(r1, 50);
canvas1.Children.Add(r1);
Rectangle r2 = new Rectangle() { Width = 150, Height = 50, Fill = Brushes.Green };
r2.MouseDown += r_MouseDown;
RotateTransform rt2 = new RotateTransform(15);
r2.RenderTransform = rt2;
Canvas.SetLeft(r2, 100);
Canvas.SetTop(r2, 100);
canvas1.Children.Add(r2);
}
private void r_MouseDown(object sender, MouseButtonEventArgs e)
{
Console.WriteLine("Rectangle Clicked");
}
}
There is another question that is similar to this, but it has no accepted answer and it is quite unclear as to what the final solution should be to resolve this issue. Let's see if we can be a little more clear.
First off, the solution outlined below will use the VisualTreeHelper.HitTest method in order to identify if the mouse has clicked your rectangles. The VisualTreeHelper allows us to find the rectangles even if they have moved around due to things like Canvas.SetTop and various .RenderTransform operations.
Secondly, we are going to be capturing the click event on your canvas element rather than on the individual rectangles. This allows us to handle things at the canvas level and check all the rectangles at once, as it were.
public MainWindow()
{
InitializeComponent();
//Additional rectangle for testing.
Rectangle r3 = new Rectangle() { Width = 175, Height = 80, Fill = Brushes.Goldenrod };
Canvas.SetLeft(r3, 80);
Canvas.SetTop(r3, 80);
canvas1.Children.Add(r3);
Rectangle r1 = new Rectangle() { Width = 80, Height = 120, Fill = Brushes.Blue };
RotateTransform rt1 = new RotateTransform(60);
r1.RenderTransform = rt1;
Canvas.SetLeft(r1, 100);
Canvas.SetTop(r1, 100);
canvas1.Children.Add(r1);
Rectangle r2 = new Rectangle() { Width = 150, Height = 50, Fill = Brushes.Green };
RotateTransform rt2 = new RotateTransform(15);
r2.LayoutTransform = rt2;
Canvas.SetLeft(r2, 100);
Canvas.SetTop(r2, 100);
canvas1.Children.Add(r2);
//Mouse 'click' event.
canvas1.PreviewMouseDown += canvasMouseDown;
}
//list to store the hit test results
private List<HitTestResult> hitResultsList = new List<HitTestResult>();
The HitTest method being used is the more complicated one, because the simplest version of that method only returns "the topmost" item. And by topmost, they mean the first item drawn, so it's actually visually the one on the bottom of the stack of rectangles. In order to get all of the rectangles, we need to use the complicated version of the HitTest method shown below.
private void canvasMouseDown(object sender, MouseButtonEventArgs e)
{
if (canvas1.Children.Count > 0)
{
// Retrieve the coordinates of the mouse position.
Point pt = e.GetPosition((UIElement)sender);
// Clear the contents of the list used for hit test results.
hitResultsList.Clear();
// Set up a callback to receive the hit test result enumeration.
VisualTreeHelper.HitTest(canvas1,
new HitTestFilterCallback(MyHitTestFilter),
new HitTestResultCallback(MyHitTestResult),
new PointHitTestParameters(pt));
// Perform actions on the hit test results list.
if (hitResultsList.Count > 0)
{
string msg = null;
foreach (HitTestResult htr in hitResultsList)
{
Rectangle r = (Rectangle)htr.VisualHit;
msg += r.Fill.ToString() + "\n";
}
//Message displaying the fill colors of all the rectangles
//under the mouse when it was clicked.
MessageBox.Show(msg);
}
}
}
// Filter the hit test values for each object in the enumeration.
private HitTestFilterBehavior MyHitTestFilter(DependencyObject o)
{
// Test for the object value you want to filter.
if (o.GetType() == typeof(Label))
{
// Visual object and descendants are NOT part of hit test results enumeration.
return HitTestFilterBehavior.ContinueSkipSelfAndChildren;
}
else
{
// Visual object is part of hit test results enumeration.
return HitTestFilterBehavior.Continue;
}
}
// Add the hit test result to the list of results.
private HitTestResultBehavior MyHitTestResult(HitTestResult result)
{
//Filter out the canvas object.
if (!result.VisualHit.ToString().Contains("Canvas"))
{
hitResultsList.Add(result);
}
// Set the behavior to return visuals at all z-order levels.
return HitTestResultBehavior.Continue;
}
The test example above just displays a message box showing the fill colors of all rectangles under the mouse pointer when it was clicked; verifying that VisualTreeHelper did in fact retrieve all the rectangles in the stack.
I use this example ( https://siderite.dev/blog/how-to-draw-outlined-text-in-wpf-and.html ) for add outline to a text in a TextBlock. However this example don't support Inlines.
I try to add this ability modifying the OnRender and I iterate the Inlines collection to build a Geometry for each Inline block. The problem is that I need to get the position and size of the inline block in the TextBlock instead of those of the whole text in the TextBlock.
This is the source code that I have modified.
protected override void OnRender(DrawingContext drawingContext)
{
ensureTextBlock();
base.OnRender(drawingContext);
foreach (Inline inline in _textBlock.Inlines)
{
var textRange = new TextRange(inline.ContentStart, inline.ContentEnd);
var formattedText = new FormattedText(
textRange.Text,
CultureInfo.CurrentUICulture,
inline.FlowDirection,
new Typeface(inline.FontFamily, inline.FontStyle, inline.FontWeight, inline.FontStretch),
inline.FontSize, System.Windows.Media.Brushes.Black // This brush does not matter since we use the geometry of the text.
);
formattedText.SetTextDecorations(inline.TextDecorations);
//*****************************************************************
//This part get the size and position of the TextBlock
// Instead I need to find a way to get the size and position of the Inline block
//formattedText.TextAlignment = _textBlock.TextAlignment;
//formattedText.Trimming = _textBlock.TextTrimming;
formattedText.LineHeight = _textBlock.LineHeight;
formattedText.MaxTextWidth = _textBlock.ActualWidth - _textBlock.Padding.Left - _textBlock.Padding.Right;
formattedText.MaxTextHeight = _textBlock.ActualHeight - _textBlock.Padding.Top;// - _textBlock.Padding.Bottom;
while (formattedText.Extent == double.NegativeInfinity)
{
formattedText.MaxTextHeight++;
}
// Build the geometry object that represents the text.
var _textGeometry = formattedText.BuildGeometry(new System.Windows.Point(_textBlock.Padding.Left, _textBlock.Padding.Top));
//*****************************************************************
var textPen = new System.Windows.Media.Pen(Stroke, StrokeThickness * 2)
{
DashCap = PenLineCap.Round,
EndLineCap = PenLineCap.Round,
LineJoin = PenLineJoin.Round,
StartLineCap = PenLineCap.Round
};
var boundsGeo = new RectangleGeometry(new Rect(0, 0, ActualWidth, ActualHeight));
_clipGeometry = Geometry.Combine(boundsGeo, _textGeometry, GeometryCombineMode.Exclude, null);
drawingContext.PushClip(_clipGeometry);
drawingContext.DrawGeometry(System.Windows.Media.Brushes.Transparent, textPen, _textGeometry);
drawingContext.Pop();
}
}
I need to change the part that get the size and position from the TextBlock in order to get the size and position of the Inline block, but I don't see any property of Inline that can get that information. Any idea?
I have found a way to get the position with:
inline.ElementStart.GetCharacterRect(LogicalDirection.Forward).Left
inline.ElementStart.GetCharacterRect(LogicalDirection.Forward).Top
Then I changed the line that build the geometry in this way:
double posLeft = inline.ElementStart.GetCharacterRect(LogicalDirection.Forward).Left;
double posTop = inline.ElementStart.GetCharacterRect(LogicalDirection.Forward).Top;
var _textGeometry = formattedText.BuildGeometry(new System.Windows.Point(posLeft, posTop));
I still have some problem with wrap because when the text is wrapped the Geometry is built only in the first line.
Try this
Rect rect = Rect.Union(inline.ElementStart.GetCharacterRect(LogicalDirection.Forward),
inline.ElementEnd.GetCharacterRect(LogicalDirection.Backward));
Thanx
Use the LineHeight property of the TextBlock:
formattedText.LineHeight = _textBlock.LineHeight;
So you can modify the LineHeight value programmatically.
I found the Run or Paragraph in FlowDocument and now I need to know the HEIGHT of it.
i.e.
while (navigator.CompareTo(flowDocViewer.Document.ContentEnd) < 0)
{
TextPointerContext context = navigator.GetPointerContext(LogicalDirection.Backward);
Run run = navigator.Parent as Run;
// I need to get HEIGHT of Run in pixels somehow
Is it possible to do in fact?
Thank you!
A little function i am using. The input is a string containing a Section. You can easily render other blockelements like Paragraph.
You also can omit the second parameter of the Parse method.
The trick is not to measure the Paragraph, but the ViewBox which contains a RichTextBox. This is needed to actually render the Flowdocument. The ViewBox dynamically gets the size of the rtb. Maybe you even can do this without the ViewBox. I spent some time to figure this out and it works for me.
Note that Width of the RichTextBox is set to double.MaxValue. This means when you want to measure a single paragraph it has to be very long or everything is in one line. So this only makes sense when you know the Width of your output device. As this is a FlowDocument there is no Width, it flows ;)
I use this to paginate a FlowDocument where i know the paper size.
The returned Height is device independent units.
private double GetHeaderFooterHeight(string headerFooter)
{
var section = (Section)XamlReader.Parse(headerFooter, _pd.ParserContext);
var flowDoc = new FlowDocument();
flowDoc.Blocks.Add(section);
var richtextbox = new RichTextBox { Width = double.MaxValue, Document = flowDoc };
var viewbox = new Viewbox { Child = richtextbox };
viewbox.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
viewbox.Arrange(new Rect(viewbox.DesiredSize));
var size = new Size() { Height = viewbox.ActualHeight, Width = viewbox.ActualWidth };
return size.Height;
}
I have written the following chunk of code that prints my ListBox perfectly when being sent to a physical printer, however when trying to send it to the XPS printer driver or using the XpsDocumentWriter class (I assume they use the same code under the hood) I receive the following exception:
System.ArgumentException was unhandled by user code
Message=Width and Height must be non-negative.
Source=ReachFramework
StackTrace:
at System.Windows.Xps.Serialization.VisualSerializer.WriteTileBrush(String element, TileBrush brush, Rect bounds)
The exception obviously points to an item not having a correct width/height however I have debugged the code when sending it to the different printers (physical and XPS driver) and I haven't been able to find any differences.
Below is how I create the visual to send to the printer:
private ScrollViewer GeneratePrintableView()
{
ScrollViewer scrollView = new ScrollViewer();
Grid grid = new Grid { Background = Brushes.White, Width = this.myListBox.ActualWidth, Height = this.myListBox.ActualHeight };
grid.RowDefinitions.Add(new RowDefinition());
grid.RowDefinitions[0].Height = new GridLength(0, GridUnitType.Auto);
grid.RowDefinitions.Add(new RowDefinition());
grid.RowDefinitions[1].Height = new GridLength(0, GridUnitType.Auto);
// Add the title and icon to the top
VisualBrush titleClone = new VisualBrush(this.TitleBar);
var titleRectangle = new Rectangle { Fill = titleClone, Width = this.TitleBar.ActualWidth, Height = this.TitleBar.ActualHeight };
grid.Children.Add(titleRectangle);
Grid.SetRow(titleRectangle, 0);
this.myListBox.Width = this.myListBox.ActualWidth;
this.myListBox.Height = this.myListBox.ActualHeight;
VisualBrush clone = new VisualBrush(this.myListBox) { Stretch = Stretch.None, AutoLayoutContent = true };
var rectangle = new Rectangle { Fill = clone, Width = this.myListBox.ActualWidth, Height = this.myListBox.ActualHeight };
Border border = new Border { Background = Brushes.White, Width = this.myListBox.ActualWidth, Height = this.myListBox.ActualHeight };
border.Child = rectangle;
grid.Children.Add(border);
Grid.SetRow(border, 1);
scrollView.Width = this.myListBox.ActualWidth;
scrollView.Height = this.myListBox.ActualHeight;
scrollView.Content = grid;
scrollView.VerticalScrollBarVisibility = ScrollBarVisibility.Hidden;
return scrollView;
}
Here is the GetPage override in my DocumentPaginator implementation:
public override DocumentPage GetPage(int pageNumber)
{
Page page = new Page();
double z = 0.0;
this.grid = new Grid();
this.grid.RowDefinitions.Add(new RowDefinition());
this.grid.RowDefinitions[0].Height = new GridLength(0, GridUnitType.Auto);
this.grid.Children.Add(this.printViewer);
Grid.SetRow(this.printViewer, 0);
//Adjusting the vertical scroll offset depending on the page number
if (pageNumber + 1 == 1) //if First Page
{
this.printViewer.ScrollToVerticalOffset(0);
this.printViewer.UpdateLayout();
}
else if (pageNumber + 1 == _verticalPageCount) //if Last Page
{
if (this.printViewer.ScrollableHeight == 0) //If printing only single page and the contents fits only on one page
{
this.printViewer.ScrollToVerticalOffset(0);
this.printViewer.UpdateLayout();
}
else if (this.printViewer.ScrollableHeight <= this.printViewer.Height) //If scrollconentheight is less or equal than scrollheight
{
this.printViewer.Height = this.printViewer.ScrollableHeight;
this.printViewer.ScrollToEnd();
this.printViewer.UpdateLayout();
}
else //if the scrollcontentheight is greater than scrollheight then set the scrollviewer height to be the remainder between scrollcontentheight and scrollheight
{
this.printViewer.Height = (this.printViewer.ScrollableHeight % this.printViewer.Height) + 5;
this.printViewer.ScrollToEnd();
this.printViewer.UpdateLayout();
}
}
else //Other Pages
{
z = z + this.printViewer.Height;
this.printViewer.ScrollToVerticalOffset(z);
this.printViewer.UpdateLayout();
}
page.Content = this.grid; //put the grid into the page
page.Arrange(new Rect(this.originalMargin.Left, this.originalMargin.Top, this.ContentSize.Width, this.ContentSize.Height));
page.UpdateLayout();
return new DocumentPage(page);
}
Interestingly if I change the Fill of rectangle to a Brush instead of clone then I do not receive the exception and the outputted file is the correct size.
I have spent over a day trying to debug why this isn't working and I am hoping that someone out there has either seen a similar issue or is able to point out any mistakes I am making.
Thanks for any responses.
I had to give up finding a solution with VisualBrush. If there is a GroupBox in the Visual for the brush I could never get it to produce a XPS file. It always fails with
System.ArgumentException was unhandled by user code Message=Width and Height must be non-negative. Source=ReachFramework StackTrace: at System.Windows.Xps.Serialization.VisualSerializer.WriteTileBrush(String element, TileBrush brush, Rect bounds)
The workaround was to clone the content that should go in the VisualBrush (Is there an easy/built-in way to get an exact copy (clone) of a XAML element?) and use that directly in a Grid instead of an VisualBrush
Have you checked the value of ActualWidth and ActualHeight of myListBox when the VisualBrush is being created? I don't know from where myListBox comes, but if it is not rendered by the time you are generating your xps document you may run into problems. You can try to manually force the control to render and see if it makes any difference.
I was unable to rectify the problem however using this link Paginated printing of WPF visuals I was able to find a suitable solution to allow printing of complicated visuals within my WPF application.
It's 2016 now and it's still not fixed. The problem is using TileBrush or any descendant type (VisualBrush in your case). If you use absolute mapping, it works, it's the relative mapping that causes the problem. Calculate the final size yourself and set Viewport to this size, ViewportUnits to Absolute. Also make sure you don't use Stretch.
I'm trying to create Labels that are centered around different columns on a Canvas. This code looks plausible:
string[] titles = { "Acorn", "Banana", "Chrysanthemum" };
double col = 20.0;
foreach (string s in titles)
{
var lbl = new Label() { Content = s };
lbl.SetValue(Canvas.LeftProperty, col - (lbl.Width / 2.0));
myCanvas.Children.Add(lbl);
col += 150.0;
}
But it does not work because the lbl.Width (and lbl.ActualWidth) aren't calculated until rendering, which is long after the Labels are being created.
Is there a way to get the accurate Label.Width prior to a layout operation? On a Canvas there isn't all the control layout and flow you get with Grids or StackPanels.
The trick is to ask the component what size it wants to be with the Measure method. If you specify unlimited available space with the double.PositiveInfinity value, you can then use the control's DesiredSize property to get its ideal, unclipped, unflowed size.
This code shows the working solution:
string[] titles = { "Acorn", "Banana", "Chrysanthemum" };
double col = 20.0;
foreach (string s in titles) {
var lbl = new Label() { Content = s };
lbl.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
lbl.SetValue(Canvas.LeftProperty, col - (lbl.DesiredSize.Width / 2.0));
myCanvas.Children.Add(lbl);
col += 150.0;
}
You cant get the width before a layout pass... But what you could try is setting the Label to Visibility of Hidden (not Collapsed), then attach a Loaded event, and set its position then change back the visibility?
Have you tried:
double pos = Canvas.GetLeft(lbl);
I'm not sure if this works before the layout pass, but worth a shot