WPF DataGrid GridLines not visible when saved as PDF - wpf

I'm using a DataGrid to represent some data in a WPF application. In a feature where I'm saving a particular WPF Window which has the DataGrid into a PDF using PDFSharp, I'm facing an issue that the DataGrid GridLines are not visible when the saved PDF is viewed in smaller viewing percentages.
(Refer attached images, only when the PDF view is set at 139%, the GridLines are visible. However, in smaller viewing %, some grid lines get omitted.)
Here's the PDF Saving Code:-
MemoryStream lMemoryStream = new MemoryStream();
Package package = Package.Open(lMemoryStream, FileMode.Create);
var doc = new System.Windows.Xps.Packaging.XpsDocument(package);
XpsDocumentWriter writer = System.Windows.Xps.Packaging.XpsDocument.CreateXpsDocumentWriter(doc);
VisualBrush sourceBrush = new VisualBrush(this);
DrawingVisual drawingVisual = new DrawingVisual();
using (var drawingContext = drawingVisual.RenderOpen())
{
drawingContext.DrawRectangle(sourceBrush, null, new Rect(new Point(0, 0), new Point(this.ActualWidth, this.ActualHeight)));
}
writer.Write(drawingVisual);
doc.Close();
package.Close();
var pdfXpsDoc = PdfSharp.Xps.XpsModel.XpsDocument.Open(lMemoryStream);
XpsConverter.Convert(pdfXpsDoc, sFileName, 0);
I believe it has to do with the quality with which the visual is drawn. Then I tried this snippet where I'm using DrawImage to make the visual at a higher resolution. Here's the snippet:-
MemoryStream lMemoryStream = new MemoryStream();
Package package = Package.Open(lMemoryStream, FileMode.Create);
var doc = new System.Windows.Xps.Packaging.XpsDocument(package);
XpsDocumentWriter writer = System.Windows.Xps.Packaging.XpsDocument.CreateXpsDocumentWriter(doc);
double dpiScale = 600.0 / 96.0;
var renderBitmap = new RenderTargetBitmap(Convert.ToInt32(this.Width * dpiScale),
Convert.ToInt32(this.Height * dpiScale),
600.0,
600.0,
PixelFormats.Pbgra32);
renderBitmap.Render(this);
var visual = new DrawingVisual();
using (var dc = visual.RenderOpen())
{
dc.DrawImage(renderBitmap, new Rect(0, 0, this.Width, this.Height));
}
writer.Write(visual);
doc.Close();
package.Close();
var pdfXpsDoc = PdfSharp.Xps.XpsModel.XpsDocument.Open(lMemoryStream);
XpsConverter.Convert(pdfXpsDoc, _pdfFileName, 0);
This snippet is working as in the grid lines are visible even in smaller viewing percentages but it makes my application stuck at the PDF save operation and also it throws System.OutofMemoryException with message "Insufficient memory to continue the execution of the program." However, the application doesn't crash.
To check the behavior of PDF viewer, I generated a table with multiple rows and columns in MS Word and saved it as a PDF. In that case, the table grid lines are clearly visible even at small viewing percentages.
Can anyone help me with this?

I assume the first code snippet creates a table in vector format (you do not supply a PDF that allows to verify this).
The second code snippet attempts to create a bitmap image (raster format).
Either way: with both vector and raster images it depends on the PDF viewer whether thin lines are visible. Adobe Reader has many options (like "Enhance thin lines", "Smooth line art", "Smooth images") that will have an effect on the actual display - to be set on the client computer, nothing to be set in the PDF.
I assume your test with MS Word also created a table in vector format, but maybe with thicker lines. So this test proofs nothing.

I had the same problem with disappearing grid lines when zooming out a PDF created with WPF.
The problem was that the TextBox objects in the Grid cells had a default background color (white) and a border color (black), and both were painted in the same place when zooming out. The solution was to not have a background at all, by setting the background to Transparent.
TextBox tx = new TextBox();
tx.Text = "X";
tx.SetValue(Grid.RowProperty, row);
tx.SetValue(Grid.ColumnProperty, col);
tx.BorderThickness = new Thickness(0.3, 0.3, 0, 0);
tx.BorderBrush = System.Windows.Media.Brushes.Black;
tx.Background = Brushes.Transparent;
grid.Children.Add(tx);
But what if you want to have some background in the grid cell? Then the solution is to add a separate Border object to the same Grid cell, and use Zindex to make sure that the Border object is painted in front of the other content.
TextBox tx = new TextBox();
tx.Text = "X";
tx.SetValue(Grid.RowProperty, row);
tx.SetValue(Grid.ColumnProperty, col);
tx.BorderThickness = new Thickness(0);
tx.Background = Brushes.LightPink;
grid.Children.Add(tx);
Border ct = new Border();
ct.SetValue(Grid.RowProperty, row);
ct.SetValue(Grid.ColumnProperty, col);
ct.BorderThickness = new Thickness(0.3, 0.3, 0, 0);
ct.BorderBrush = System.Windows.Media.Brushes.Black;
ct.Background = Brushes.Transparent;
ct.HorizontalAlignment = HorizontalAlignment.Stretch;
ct.VerticalAlignment = VerticalAlignment.Stretch;
Grid.SetZIndex(ct, 100);
grid.Children.Add(ct);
Also, UseLayoutRounding must be set to false (false is default). Otherwise lines with Thickness 0.5 or lower will disappear completely.

Related

How can I draw a Polyline onto an Image (WPF)

I've tried a few different approaches to this, but can't seem to get a combination that works.
Creating WPF app in C#, Visual Studio.
System.Windows.Shapes.Polyline works really nicely to draw into a Canvas in real-time, but I want to be able to draw in higher resolution onto a non-visual component that I can then render onto an Image.
If I create a Polyline on a Canvas that's visible in the UI, this works fine:
// Make rendertarget size of full page
RenderTargetBitmap rtb = new RenderTargetBitmap((int)wPage, (int)hPage, 96, 96, PixelFormats.Default);
// Render the polyline
rtb.Render(lineVirt);
// Apply to background image
imgBG.Source = rtb;
But if I create a Polyline on a Canvas that's not visible in the UI, then nothing renders to the image. This is probably fair enough. My guess is that the component recognises that it's not visible and therefore doesn't bother to render.
I've considered putting the Canvas somewhere in the UI buried under other controls, but that seems like a horrible kind of hack.
Essentially, all I need is a clean and fast way to draw a multi-point line of a specified width and color onto an Image. I thought that Polyline would work well, but only seems to work in a visible container.
What are my options?
You do not need a rendered Canvas or any other visible Panel at all.
Just use basic drawing primitives available at the Visual layer.
The DrawGeometry method below draws a Geometry onto a BitmapSource, using the bitmap's rendered size, i.e. the size that takes its DPI into account, and returns the resulting BitmapSource.
public static BitmapSource DrawGeometry(
BitmapSource source, Pen pen, Geometry geometry)
{
var visual = new DrawingVisual();
var rect = new Rect(0, 0, source.Width, source.Height);
using (var dc = visual.RenderOpen())
{
dc.DrawImage(source, rect);
dc.DrawGeometry(null, pen, geometry);
}
var target = new RenderTargetBitmap(
(int)rect.Width, (int)rect.Height, 96, 96, PixelFormats.Default);
target.Render(visual);
return target;
}
In order to draw in the bitmap's pixel units and hence ignore its DPI, modify the method like this:
var rect = new Rect(0, 0, source.PixelWidth, source.PixelHeight);
using (var dc = visual.RenderOpen())
{
dc.DrawRectangle(new ImageBrush(source), null, rect);
dc.DrawGeometry(null, pen, geometry);
}
The following method uses the above to draw a polyline as an IEnumerable<Point>.
public static BitmapSource DrawPolyline(
BitmapSource source, Pen pen, IEnumerable<Point> points)
{
var geometry = new PathGeometry();
if (points.Count() >= 2)
{
var figure = new PathFigure { StartPoint = points.First() };
figure.Segments.Add(new PolyLineSegment(points.Skip(1), true));
geometry.Figures.Add(figure);
}
return DrawGeometry(source, pen, geometry);
}
It would be used like
var source = new BitmapImage(new Uri(...));
var pen = new Pen
{
Brush = Brushes.Blue,
Thickness = 2,
};
var points = new List<Point>
{
new Point(100, 100),
new Point(1000, 100),
new Point(1000, 1000),
new Point(100, 1000),
new Point(100, 100),
};
image.Source = DrawPolyline(source, pen, points);
Your canvas needs a size, so someone or something has to Arrange it. That might already be enough to get it to render, but the only reliable way of rendering arbitrary visuals to a bitmap is to actually place them in the visual tree of a window that's displayed and thus laid out by WPF. You can then render to the bitmap in a deferred task at ContextIdle priority to ensure that layout is complete.

Printing In Silverlight. Some pages missing

I am maintaining an old application that prints checks from silverlight.
The checks are in a grid and the user selects them and presses the print button.
I verified that all the checks selected in the grid do get sent to the printer but I noticed that sometimes some are missing in the actual printout. I check the EndPrint even for errors and there is none.
How can I make sure all the data gets actually printed?
Here is the code for the printpage event
StackPanel stackPanel = new StackPanel();
CheckInfo check = selectedChecks[printItemIndex];
PrintCheck printCheck = BuildPrintCheck(check);
stackPanel.Children.Add(printCheck);
stackPanel.Measure(new Size(args.PrintableArea.Width, double.PositiveInfinity));
if (++printItemIndex < selectedChecks.Count)
args.HasMorePages = true;
args.PageVisual = stackPanel;
I found a workaround to this issue posted here
http://www.thomasclaudiushuber.com/blog/2009/11/25/how-to-print-dynamically-created-images-in-silverlight-4-beta/
Basically instead of putting the Image straight into the page, put a Rectangle and and at run time load the image dynamically, set it as the image source for an image brush and then set the fill property of the rectangle to the image brush.
OK it turned out that SilverLight5 (runtime) has an issue with printing images. The problem does not exist on clients running SilverLight4.
Here is how I fixed it in my code
private void PlaceImages()
{
var logoStreamResourceInfo = Application.GetResourceStream(new Uri("myApp;/Images/logo.png", UriKind.Relative));
var logo = new BitmapImage();
logo.SetSource(logoStreamResourceInfo.Stream);
var logoImageBrush = new ImageBrush();
logoImageBrush.ImageSource = logo;
upperLogo.Fill = logoImageBrush;
lowerLogo.Fill = logoImageBrush;
}

take a screenshot of WPF datagrid with scrollviewer

I'm trying to take a screenshot of a datagrid which has to many rows to be displayed. So there is a scrollviewer.
So when I just put the datagrid into the Render Method of RenderTargetBitmap I obviously just get the viewable part of the datagrid.
I read that one can take a screenshot of a content when actually rendering the ItemsPresenter of the ScrollViewer of that control, as the ItemsPresenter would have the "real" Width and Height of the content.
Unfortunatly my ScrollViewer doesnt have any different Height, ActualHeight or RenderSize.Height than the dataGrid.
So I always just get the visible part of the Content.
Anyone know how to do this the right way, that it actually takes the whole content ?
Code:
var scroll = GetTemplateChildByName(dataGridInOut);
if (scroll != null)
{
var item = scroll.Content as ItemsPresenter;
var width = item.RenderSize.Width;
var height = item.RenderSize.Height;
var rtb = new RenderTargetBitmap((int) Math.Round(width), (int)Math.Round(height), 96, 96,
PixelFormats.Pbgra32);
var drawingVisual = new DrawingVisual();
var visualBrush = new VisualBrush(item);
using (var context = drawingVisual.RenderOpen())
{
context.DrawRectangle(visualBrush, null, new Rect(new Point(0,0), new Size(width, height)));
}
rtb.Render(drawingVisual);
Clipboard.SetImage(rtb);
}
Leaf is right. You could instantiate another DataGrid bound to the same source programmatically, put it into a container which gives it infinite space, wait for it to render and then take a screenshot of this. No need to actually show it in the UI.

ContentControl + RenderTargetBitmap + empty image

Im trying to create some chart images without ever displaying those charts on the screen. I'v been at this for quite a while and tried a lot of different things but nothing seems to work. The code works perfectly if I display the chart in a window first, but if I don't display it in a window, the bitmap is just white with a black border (no idea why).
I have tried adding the chart to a border before rendering and giving the border a green borderBrush. In the bitmap, I see the green borderBrush then the black border and white background but no chart. The Chart is not contained in a black boarder so I don't know where that is coming from.
I have tried adding the chart to a window without calling window.Show() and again I just get the black boarder and white background. However if I call window.Show() the bitmap contains the chart.
I have tried using a drawingVisual as explained here, same result.
Here is the code (not including adding the element to a border or window):
private static BitmapSource CreateElementScreenshot(FrameworkElement element, int dpi)
{
if (!element.IsMeasureValid)
{
Size size = new Size(element.Width, element.Height);
element.Measure(size);
element.Arrange(new Rect(size));
}
element.UpdateLayout();
var scale = dpi/96.0;
var renderTargetBitmap = new RenderTargetBitmap
(
(int)(scale * element.RenderSize.Width),(int)(scale * element.RenderSize.Height),dpi,dpi,PixelFormats.Default
);
// this is waiting for dispatcher to perform measure, arrange and render passes
element.Dispatcher.Invoke(((Action)(() => renderTargetBitmap.Render(element))), DispatcherPriority.Render);
return renderTargetBitmap;
}
Note: The chart is a ContentControl.
Is there anyway I can get the chart to render without displaying it in a window first?
Calling element.ApplyTemplate() did the trick.
If someone has similar problems with rendering RenderTargetBitmap (getting white / empty image) items that are in StackPanel you can temporary move them to Grid, then render and put it back in StackPanel
Grid grid = new System.Windows.Controls.Grid() { Background = Brushes.White, Width = iWidth, Height = iHeight };
Panel panel = plot.Parent as Panel;
if (panel != null)
{
panel.Children.Remove(plot);
grid.Children.Add(plot);
grid.Measure(new Size(iWidth, iHeight));
grid.Arrange(new Rect(new Size(iWidth, iHeight)));
}
plot.Measure(new Size(iWidth, iHeight));
plot.Arrange(new Rect(new Size(iWidth, iHeight)));
plot.ApplyTemplate();
plot.UpdateLayout();
grid.ApplyTemplate();
grid.UpdateLayout();
RenderTargetBitmap renderTargetBitmap = new RenderTargetBitmap(
iWidth,
iHeight,
96, 96, PixelFormats.Pbgra32);
renderTargetBitmap.Render(grid);
PngBitmapEncoder encoder = new PngBitmapEncoder();
encoder.Frames.Add(BitmapFrame.Create(renderTargetBitmap));
MemoryStream memoryStream = new MemoryStream();
encoder.Save(memoryStream);
bitmap = new System.Drawing.Bitmap(memoryStream);
if (panel != null)
{
grid.Children.Remove(plot);
panel.Children.Add(plot);
}
plot.Measure(new Size(iWidthBefore, iHeightBefore));
plot.Arrange(new Rect(new Size(iWidthBefore, iHeightBefore)));
plot.UpdateLayout();
For me, calling element.Arrange() was the missing piece.

Missing images in FlowDocument saved as XPS document

I am having some difficulties getting images contained in a FlowDocument to show when the FlowDocument is saved as an XPS document.
Here is what I do:
Create an image using the Image control of WPF. I set the image source bracketed by calls to BeginInit/EndInit.
Add the image to the FlowDocument wrapping it in a BlockUIContainer.
Save the FlowDocument object to an XPS file using a modified version of this code.
If I then view the saved file in the XPS viewer, the image is not shown. The problem is that the images are not loaded until actually shown on the screen by WPF so they are not saved to the XPS file. Hence, there is a workaround: If I first show the document on screen using the FlowDocumentPageViewer and then save the XPS file afterwards, the image is loaded and shows up in the XPS file. This works even if the FlowDocumentPageViewer is hidden. But that gives me another challenge. Here is what I wish to do (in pseudocode):
void SaveDocument()
{
AddFlowDocumentToFlowDocumentPageViewer();
SaveFlowDocumentToXpsFile();
}
This of course does not work since the FlowDocumentPageViewer never gets a chance to show its contents before the document is saved to the XPS file. I tried wrapping SaveFlowDocumentToXpsFile in a call to Dispatcher.BeginInvoke but it did not help.
My questions are:
Can I somehow force the images to load before saving the XPS file without actually showing the document on screen? (I tried fiddling with BitmapImage.CreateOptions with no luck).
If there is no solution to question #1, is there a way to tell when FlowDocumentPageViewer has finished loading its contents so that I know when it is save to create the XPS file?
The eventual solution was the same as you came to, which is to put the document in a viewer and briefly show it on screen. Below is the helper method that I wrote to do this for me.
private static string ForceRenderFlowDocumentXaml =
#"<Window xmlns=""http://schemas.microsoft.com/netfx/2007/xaml/presentation""
xmlns:x=""http://schemas.microsoft.com/winfx/2006/xaml"">
<FlowDocumentScrollViewer Name=""viewer""/>
</Window>";
public static void ForceRenderFlowDocument(FlowDocument document)
{
using (var reader = new XmlTextReader(new StringReader(ForceRenderFlowDocumentXaml)))
{
Window window = XamlReader.Load(reader) as Window;
FlowDocumentScrollViewer viewer = LogicalTreeHelper.FindLogicalNode(window, "viewer") as FlowDocumentScrollViewer;
viewer.Document = document;
// Show the window way off-screen
window.WindowStartupLocation = WindowStartupLocation.Manual;
window.Top = Int32.MaxValue;
window.Left = Int32.MaxValue;
window.ShowInTaskbar = false;
window.Show();
// Ensure that dispatcher has done the layout and render passes
Dispatcher.CurrentDispatcher.Invoke(DispatcherPriority.Loaded, new Action(() => {}));
viewer.Document = null;
window.Close();
}
}
Edit: I just added window.ShowInTaskbar = false to the method as if you were quick you could see the window appear in the taskbar.
The user will never "see" the window as it is positioned way off-screen at Int32.MaxValue - a trick that was common back in the day with early multimedia authoring (e.g. Macromedia/Adobe Director).
For people searching and finding this question, I can tell you that there is no other way to force the document to render.
HTH,
Couple things...
You sure the image is sized before its written? Usually you have to call Measure on the control so that it may size itself accordingly (infinity lets the control expand to its Width and Height)
image.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
Also, sometimes you have to bump the UI thread so that everything gets updated in the control
Dispatcher.Invoke(DispatcherPriority.Render, new Action(() =>{}));
You do not have to display the document in order to have images saved into the xps. Are you calling commit on the XpsSerializationManager?
FlowDocument fd = new FlowDocument();
fd.Blocks.Add(new Paragraph(new Run("This is a test")));
string image = #"STRING_PATH";
BitmapImage bi = new BitmapImage();
bi.BeginInit();
bi.UriSource = new Uri(image, UriKind.RelativeOrAbsolute);
bi.CacheOption = BitmapCacheOption.OnLoad;
bi.EndInit();
MemoryStream ms = new MemoryStream();
Package pkg = Package.Open(ms, FileMode.Create, FileAccess.ReadWrite);
Uri pkgUri = bi.UriSource;
PackageStore.AddPackage(pkgUri, pkg);
Image img = new Image();
img.Source = bi;
BlockUIContainer blkContainer = new BlockUIContainer(img);
fd.Blocks.Add(blkContainer);
DocumentPaginator paginator = ((IDocumentPaginatorSource)fd).DocumentPaginator;
using (XpsDocument xps = new XpsDocument(#"STRING PATH WHERE TO SAVE FILE", FileAccess.ReadWrite, CompressionOption.Maximum))
{
using (XpsSerializationManager serializer = new XpsSerializationManager(new XpsPackagingPolicy(xps), false))
{
serializer.SaveAsXaml(paginator);
serializer.Commit();
}
}
I was able to address this by throwing the flowdocument into a viewer, and then do a measure/arrange.
FlowDocumentScrollViewer flowDocumentScrollViewer = new FlowDocumentScrollViewer();
flowDocumentScrollViewer.Document = flowDocument;
flowDocumentScrollViewer.Measure(new Size(Double.PositiveInfinity, Double.PositiveInfinity));
flowDocumentScrollViewer.Arrange(new Rect(new Point(0, 0), new Point(Double.MaxValue, Double.MaxValue)));

Resources