WPF - FlowDocument - Stretch Table to Entire Width? - wpf

I have a DataTable containing an arbitrary number of columns and rows which I am trying to print out. The best luck I've had so far is by putting the data into a Table and then adding the table to a FlowDocument.
So far so good. The problem I have right now is that the Table only "wants" to take up about half of the document's width. I've already set the appropriate values for the FlowDocument's PageWidth and ColumnWidth properties, but the Table doesn't seem to want to stretch to fill up the allotted space?

In order to set your FlowDocument contents to the full available widh you must first know the width of the page. The property you need to set that takes care of the content length is the ColumnWidth prop on the FlowDocument.
I usualy create a "PrintLayout" helper class to keep known presets for the Page width/hight and Padding. Wou can snif presets from Ms Word and fill more.
The class for PrintLayout
public class PrintLayout
{
public static readonly PrintLayout A4 = new PrintLayout("29.7cm", "42cm", "3.18cm", "2.54cm");
public static readonly PrintLayout A4Narrow = new PrintLayout("29.7cm", "42cm", "1.27cm", "1.27cm");
public static readonly PrintLayout A4Moderate = new PrintLayout("29.7cm", "42cm", "1.91cm", "2.54cm");
private Size _Size;
private Thickness _Margin;
public PrintLayout(string w, string h, string leftright, string topbottom)
: this(w,h,leftright, topbottom, leftright, topbottom) {
}
public PrintLayout(string w, string h, string left, string top, string right, string bottom) {
var converter = new LengthConverter();
var width = (double)converter.ConvertFromInvariantString(w);
var height = (double)converter.ConvertFromInvariantString(h);
var marginLeft = (double)converter.ConvertFromInvariantString(left);
var marginTop = (double)converter.ConvertFromInvariantString(top);
var marginRight = (double)converter.ConvertFromInvariantString(right);
var marginBottom = (double)converter.ConvertFromInvariantString(bottom);
this._Size = new Size(width, height);
this._Margin = new Thickness(marginLeft, marginTop, marginRight, marginBottom);
}
public Thickness Margin {
get { return _Margin; }
set { _Margin = value; }
}
public Size Size {
get { return _Size; }
}
public double ColumnWidth {
get {
var column = 0.0;
column = this.Size.Width - Margin.Left - Margin.Right;
return column;
}
}
}
next on your FlowDocument you can set the presets
On Xaml
<FlowDocument x:Class="WpfApp.MyPrintoutView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:WpfApp"
mc:Ignorable="d"
PageHeight="{Binding Height, Source={x:Static local:PrintLayout.A4}}"
PageWidth="{Binding Width, Source={x:Static local:PrintLayout.A4}}"
PagePadding="{Binding Margin, Source={x:Static local:PrintLayout.A4}}"
ColumnWidth="{Binding ColumnWidth, Source={x:Static local:PrintLayout.A4}}"
FontFamily="Segoe WP"
FontSize="16" ColumnGap="4">
<!-- flow elements -->
</FlowDocument>
By code
FlowDocument result = new WpfApp.MyPrintoutView();
result.PageWidth = PrintLayout.A4.Size.Width;
result.PageHeight = PrintLayout.A4.Size.Height;
result.PagePadding = PrintLayout.A4.Margin;
result.ColumnWidth = PrintLayout.A4.ColumnWidth;

I had some luck with this: How to set the original width of a WPF FlowDocument, although it only took up about 90% of the space.

Related

Convert a Grid element to Image in WPF

I have a code snippet which is currently returned as Grid.
private Grid GetImage(PlacemarkList locationDetail)
{
Grid gridPushPin = new Grid();
ImageBrush img = new ImageBrush();
img.ImageSource = locationDetail.preferredCashback.Equals("1") ? new BitmapImage {
UriSource = Constants.CashbackIconUri,
DecodePixelWidth = 36,
DecodePixelHeight = 59
} : new BitmapImage {
UriSource = Constants.ATMIconUri,
DecodePixelWidth = 36, DecodePixelHeight = 59
};
TextBlock IndexText = new TextBlock();
IndexText.TextAlignment = TextAlignment.Center;
IndexText.Text = locationDetail.IndexNum.ToString();
gridPushPin.Background = img;
gridPushPin.Tag = locationDetail.bankAddress;
gridPushPin.Tap += grid_Tap;
return gridPushPin;
}
But I want to return the Grid as a Image(Convert the Grid I am generating to Image). Can anybody please help how to accomplish that.
You can use a VisualBrush to paint a copy of any UIElement onto any other. How about something like this:
<Rectangle Width="150" Height="150">
<Rectangle.Fill>
<VisualBrush Visual="{Binding ElementName=NameOfYourGrid}" />
</Rectangle.Fill>
</Rectangle>
Hey did you check this subject. I think what you need is here.
How do I save all content of a WPF ScrollViewer as an image
public class MyWrapperClass
{
private DataGrid dataGrid;
public MyWrapperClass(DataGrid grid)
{
this.dataGrid = grid;
}
public DataGrid MyGrid
{
get
{
return this.grid;
}
set
{
this.grid = value;
}
}
public ImageBrush MyImageBrush
{
get
{
this.grid.Background as ImageBrush;
}
set
{
this.grid.Background = value;
}
}
}
You are creating a Grid, so you will return a Grid.
Create an image instead. Something like:
Image image = new Image();
image.Source = "myImageURL";
This is from memory so actual code may vary depending on what you need to do exactly.
Basically the Source of the image needs to be set similarly to how you're setting ImageSource in your current code, but you may opt to use pack URLs to use an image stored as a resource instead.
Edit after clarifications in comments: So you want to get the grid control as an image. This answer should help.

Displaying transparent image in a WPF DataGrid

I have been tasked with taking an existing list of transparent .png images (currently housed within an ImageList) and displaying them in a WPF DataGrid based on the ImageID column.
I have set up the DataGridColumn as follows:
_dataTemplateColumn = new DataGridTemplateColumn();
_dataTemplateColumn.Header = "";
FrameworkElementFactory _factory = new FrameworkElementFactory(typeof(Image));
Binding _binding = new Binding("Image");
_binding.Mode = BindingMode.TwoWay;
_factory.SetValue(Image.SourceProperty, _binding);
DataTemplate _cellTemplate = new DataTemplate();
_cellTemplate.VisualTree = _factory;
_dataTemplateColumn.CellTemplate = _cellTemplate;
Style _style = new Style();
_style.Setters.Add(new Setter(BackgroundProperty, Brushes.Transparent));
_dataTemplateColumn.CellStyle = _style;
I then create a Custom Object at runtime which includes the image for me and run the following 2 methods on the Image, the first to resize it and the second to convert it into a Bitmap rather than a BitmapImage (which is the only format I have managed to get it working in WPF with so far):
public static Bitmap ResizeImage(this Bitmap Bitmap, Size size)
{
try
{
Bitmap _bitmap = new Bitmap(size.Width, size.Height);
using (Graphics _graphic = Graphics.FromImage((Image)_bitmap))
{
_graphic.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic;
_graphic.DrawImage(Bitmap, 0, 0, size.Width, size.Height);
}
_bitmap.MakeTransparent(Color.Magenta);
return _bitmap;
}
catch (Exception ex)
{
throw ex;
}
}
public static Bitmap ToBitmap(this BitmapImage BitmapImage)
{
using (MemoryStream _stream = new MemoryStream())
{
BitmapEncoder _encoder = new BmpBitmapEncoder();
_encoder.Frames.Add(BitmapFrame.Create(BitmapImage));
_encoder.Save(_stream);
System.Drawing.Bitmap _bitmap = new System.Drawing.Bitmap(_stream);
_bitmap.MakeTransparent(Color.Magenta);
return new Bitmap(_bitmap);
}
}
The Image is being displayed in the correct size and position in the DataGrid but the transparency is not preserved from the .png format. If anyone knows a better method for me (perhaps it is more correct to take the Image into a resource file first for example?) or a way to get the transparency working within my current code it would be most appreciated!
The following example gives you an idea of how it may look like:
XAML:
<Window ...>
<Window.Resources>
<DataTemplate x:Key="ImageCellTemplate">
<Image Source="{Binding Image}" Width="100"/>
</DataTemplate>
</Window.Resources>
<Grid>
<DataGrid x:Name="dataGrid" AutoGenerateColumns="False"/>
</Grid>
</Window>
Code:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
var col = new DataGridTemplateColumn();
col.CellTemplate = (DataTemplate)Resources["ImageCellTemplate"];
dataGrid.Columns.Add(col);
foreach (var file in Directory.EnumerateFiles(#"C:\Users\Public\Pictures\Sample Pictures", "*.jpg"))
{
dataGrid.Items.Add(new DataItem { Image = file });
}
}
}
public class DataItem
{
public string Image { get; set; }
}

Use of ItemsControl ActualHeight to paginate a printed XAML form?

I need to generate a printed form using XAML that has a header grid and a variable number of rows that can result in multiple pages as the number of rows increases. The header must appear on each page and each row may vary in height due to text wrapping within the row. I am currently trying to use ActualHeight of the ItemsControl (rows container) to determine when to generate a new page, but ActualHeight always has a value of zero.
My "XAML_Form" has the following structure. A grid is used in the ItemTemplate to allow aligning columns in the rows with columns in the header grid.
<Grid Width="980" Height="757">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid Name="_headerControl" Grid.Row="0"/>
<ItemsControl Name=_rowsControl ItemsSource={Binding Rows} Grid.Row="1">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
</Grid>
Our architecture has a report class that handles adding footers, page numbers, and aggregating the pages into a PDF. This report class has a nested ViewModel for each XAML View page. The ViewModel for each page uses a backing List of row objects:
List<RowClass> RowsList;
The ViewModel also has an ICollectionView that is used to bind as the ItemsSource:
ICollectionView Rows = new ListCollectionView(RowsList);
My report class has a CreatePages method which contains code like this:
IList<XAML_Form> pages = new List<XAML_Form>();
var vm = new PageViewModelClass();
var page = new XAML_Form { DataContext = vm };
page.InitializeComponent();
page.Measure(new Size(Double.PositiveInfinity, Double.PositiveInfinity);
page.Arrange(new Rect(new Point(0,0), page.DesiredSize));
var maxRowsHeight = page.DesiredSize.Height - page._headerControl.ActualHeight;
pages.Add(page);
var rowsOnPage = 0;
foreach (var row in sourceRowsObjectList)
{
rowsOnPage++;
vm.RowsList.Add(row);
vm.Rows.Refresh();
page.Measure(new Size(Double.PositiveInfinity, Double.PositiveInfinity);
page.Arrange(new Rect(new Point(0,0), page.DesiredSize));
if (page._rowsControl.ActualHeight <= maxRowsHeight)
continue;
// The rows exceed the available space; the row needs to go on the next page.
vm.RowsList.RemoveAt(--rowsOnPage);
vm = new PageViewModelClass();
vm.RowsList.Add(row);
rowsOnPage = 1;
page = new XAML_Form { DataContext = vm };
page.InitializeComponent();
pages.Add(page);
}
return pages;
The initial Measure/Arrange does provide me with the expected value for maxRowsHeight. And the generated form looks fine for a single page with a few rows. My specific problem is: Why is page._rowsControl.ActualHeight always zero? And generally, is there a better approach to this problem?
Here is a solution. We are trying to separate the view concerns from the view model concerns, so there are still improvements to be made.
The CreatePages method in the report class is now:
private static IEnumerable<XAML_Form> CreatePages()
{
IList<XAML_Form> pages = new List<XAML_Form>();
int rowCount = sourceRowsObjectList.Count;
int remainingRowCount = rowCount;
do
{
var pageVm = new PageViewModelClass();
var page = new XAML_Form(pageVm);
pages.Add(page);
int numberOfRowsToAdd = Math.Min(remainingRowCount, XAML_Form.MaxNumberOfRows);
pageVm.AddRows(sourceRowsObjectList.Skip(rowCount - remainingRowCount).Take(numberOfRowsToAdd));
remainingRowCount -= numberOfRowsToAdd;
while (page.AreRowsOverflowing())
{
pageVm.RemoveLastRow();
remainingRowCount++;
}
} while (remainingRowCount > 0);
return pages;
}
The pertinent XAML_Form code behind is as follows:
private static int _maxNumberOfRows = -1;
public XAML_Form(PageViewModelClass viewModel)
{
InitializeComponent();
Measure(new Size(Double.PositiveInfinity, Double.PositiveInfinity));
Arrange(new Rect(new Point(0, 0), DesiredSize);
ViewModel = viewModel;
}
public PageViewModelClass ViewModel
{
get { return (PageViewModelClass)DataContext; }
private set { DataContext = value; }
}
public static int MaxNumberOfRows
{
get // Compute this only once, the first time it is called.
{
if (_maxNumberOfRows < 0) return _maxNumberOfRows;
var page = new XAML_Form();
var singleRowCollection = new object[] { null; }
page._rowsControl.ItemsSource = singleItemCollection;
page._rowsControl.UpdateLayout();
var rowHeight = page._rowsControl.ActualHeight;
_maxNumberOfRows = (int)((page.DesiredSize.Height - page._headerControl.ActualHeight) / rowHeight);
page._rowsControl.ItemsSource = null;
return _maxNumberOfRows;
}
}
// Call this method as rarely as possible. UpdateLayout is EXPENSIVE!
public bool AreRowsOverflowing()
{
_rowsControl.UpdateLayout();
return _rowsControl.ActualHeight > DesiredSize.Height - _headerControl.ActualHeight;
}

Silverlight - DataGrid in Scrollviewer, Column.Width="*" makes datagrid take several screen-widths

When I have the following setup, the last column having a width of * causes the datagrid to create huge horizontal scrollbars (extends grid to several widths of the screen). I'm not really sure why this is, but I really need a way to avoid that. I don't want to have to "simulate" column's with * lengths.
edit: Apparently I'm not the only one who noticed this.
http://connect.microsoft.com/VisualStudio/feedback/details/559644/silverlight-4-datagrid-star-column-width
Xaml:
<ScrollViewer Padding="0" HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto" >
<sdk:DataGrid AutoGenerateColumns="False" x:Name="dg"/>
</ScrollViewer>
Code:
private void UserControl_Loaded(object sender, RoutedEventArgs e)
{
dg.Columns.Add(new DataGridTextColumn { Binding = new Binding("A"), Header = "A" });
dg.Columns.Add(new DataGridTextColumn { Binding = new Binding("B"), Header = "B" });
dg.Columns.Add(new DataGridTextColumn { Binding = new Binding("C"), Header = "C" });
dg.Columns[2].Width = new DataGridLength(1, DataGridLengthUnitType.Star);
dg.ItemsSource = new[]
{
new I { A = "SAF", B = "SAF", C = "SAF" },
new I { A = "SAF", B = "SAF", C = "SAF" },
new I { A = "SAF", B = "SAF", C = "SAF" }
};
}
public class I
{
public string A { get; set; }
public string B { get; set; }
public string C { get; set; }
}
you need to put a maxwidth on the scrollviewer? otherwise width is defaulted to auto and maxwidth is infinity
If you remove
dg.Columns[2].Width = new DataGridLength(1, DataGridLengthUnitType.Star);
the problem should go away. Can I ask why you want the last column to take the rest of the space? The column actually knows to resize itself properly to fit its cell size and column header size.
Also, if you don't specify the column's width, then you don't really need a scrollviewer to scroll and see all the columns. The datagrid has a scrollviewer built in and when there are columns out of the screen the horizontal scrollbar will apear.
I hope this helps. :)

Silverlight 2 ListBox with a custom panel won't respect size from ArrangeOverride

If I use a custom panel to layout my ListBoxItems, the ListBox won't respect their combined Height (it does respect their combined Width though) - even when my ArrangeOverride returns a size that surrounds all the items.
Setting the ListBox's Height explicitly makes everything work, but I want it to work that out for itself!
Has anyone seen this before?
Thanks
Update: In the example below, the ListBox uses a custom panel that stacks the Articles vertically according to the Row property and returns a size big enough to surround all of them. But unless I set the Height for the ListBox it collapses!
<UserControl x:Class="SilverlightApplication1.Page"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:SilverlightApplication1">
<UserControl.Resources>
<DataTemplate x:Key="ArticleTemplate">
<TextBlock Text="{Binding Title}" />
</DataTemplate>
</UserControl.Resources>
<ListBox Height="200"
Background="AntiqueWhite"
ItemTemplate="{StaticResource ArticleTemplate}"
ItemsSource="{Binding}" VerticalAlignment="Top"
Margin="0,0,0,0">
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<local:MyPanel />
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
</ListBox>
</UserControl>
Here is the panel:
public class MyPanel : Panel
{
protected override Size ArrangeOverride(Size arrangeSize)
{
double width = 0, height = 0;
foreach (UIElement child in this.Children)
{
var article = (Article)((ContentControl)child).DataContext;
var y = child.DesiredSize.Height * article.Row;
var location = new Point(0, y);
var rect = new Rect(location, child.DesiredSize);
child.Arrange(rect);
width = Math.Max(width, child.DesiredSize.Width);
height = Math.Max(height, y + child.DesiredSize.Height);
}
return new Size(width, height);
}
protected override Size MeasureOverride(Size availableSize)
{
foreach (UIElement child in this.Children)
{
if (child != null)
{
child.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
}
}
return new Size();
}
}
And the domain class:
public class Article
{
private readonly int row;
private readonly string title;
public Article(string title, int row)
{
this.title = title;
this.row = row;
}
public int Row { get { return this.row; } }
public string Title { get { return this.title; } }
}
I was not able to reproduce the problem that you described. Could you provide some example code/xaml so that I can take a look?
UPDATE:
I believe the problem here is that you are returning (0,0) as the Panel's DesiredSize from MeasureOverride. You probably want to do something like:
protected override Size MeasureOverride(Size availableSize)
{
double width = 0;
double height = 0;
foreach (UIElement child in this.Children)
{
if (child != null)
{
child.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
width = Math.Max(width, child.DesiredSize.Width);
height = Math.Max(height, child.DesiredSize.Height);
}
}
return new Size(width, height);
}
The actual sizing logic is likely to be different depending on your requirements.
What is happening in your code is that by returning (0,0) from MeasureOverride, your Panel is essentially "asking" its parent for this amount of space to be reserved for it. So when it comes to the Arrange phase of the layout cycle the finalSize passed to ArrangeOverride is very small (0 in at least one dimension). You can verify this by setting a breakpoint in ArrangeOverride and then examining the finalSize parameter. To get your code to work correctly with the layout system, you need to return from MeasureOverride the minimum amount of space that your Panel needs to contain its children.
Dont forget that ListBoxes contain ScrollViewers. The ScrollViewer is probably seeing the height of its children and thinking "sweet as, I can handle that", then setting its own size.
Try setting
ScrollViewer.VerticalScrollBarVisibility = "Hidden"
or
ScrollViewer.VerticalScrollBarVisibility = "Disabled"
in your ListBox and see what happens.

Resources