Is there an easy/built-in way to get an exact copy

2019-01-15 13:03发布

问题:

I need to make areas of XAML printable and so have make this button handler:

private void Button_Click_Print(object sender, RoutedEventArgs e)
{
    Customer.PrintReport(PrintableArea);
}

And in PrintReport I pack the frameworkelement into other elements in order to print it in a slightly different way than it is on the screen, like this:

public void PrintReport(FrameworkElement fwe)
{
    StackPanel sp = new StackPanel();
    sp.Children.Add(fwe);
    TextBlock tb = new TextBlock();
    tb.Text = "hello";
    sp.Children.Add(tb);

    PrintDialog dialog = new PrintDialog();
    if (dialog.ShowDialog() == true)
    { 
        dialog.PrintVisual(sp, "Print job"); 
    }
}

But the above gives me the following error:

Specified element is already the logical child of another element. Disconnect it first.

Is there an easy way to clone the FrameworkElement so that I can manipulate the copy, print it, and then forget about it, leaving the original element in the XAML being displayed on the screen intact?

Something like this I would imagine:

FrameworkElement fwe2 = FrameworkElement.Clone(fwe); //pseudo-code

回答1:

I had a similar problem in my current project and solved it with this code.

public static class ExtensionMethods
{
    public static T XamlClone<T>(this T original)
        where T : class
    {
        if (original == null)
            return null;

        object clone;
        using (var stream = new MemoryStream())
        {
            XamlWriter.Save(original, stream);
            stream.Seek(0, SeekOrigin.Begin);
            clone = XamlReader.Load(stream);
        }

        if (clone is T)
            return (T)clone;
        else
            return null;
    }
}

This way it simply appears as a method on all objects in your WPF project, you do not need to give any parameters to the method, and it returns an object of the same class as the original.



回答2:

In WPF, copying (or "cloning") elements is almost never correct. This effectively makes this an XY Problem question. I.e. you only think that you need to literally clone the elements in your visual tree. But you don't.

The idiomatic and correct approach here is to declare a DataTemplate that represents the data you want to print. Of course, that also means that the data you want to print is in turn being represented by a view model class, for which the DataTemplate has been declared (i.e. through the DataType property).

For example:

<DataTemplate DataType={x:Type PrintableViewModel}>
  <!-- template contents go here -->
</DataTemplate>

The PrintableViewModel class being, of course, a view model class containing the data you want to use to populate the visual tree that will be printed.

In the XAML for your UI, you'd then use it something like this:

<ContentControl Content={Binding PrintableViewModelProperty}/>

I.e. bind the Content property to a property in the current DataContext object that returns an instance of your PrintableViewModel, and let the ContentControl display the data appropriately.

WPF will look up the appropriate data template and apply it for display in the ContentControl. When you want to print the data, you then just do something like this:

PrintDialog printDialog = new PrintDialog();

if (printDialog.ShowDialog() == true)
{
    ContentControl contentControl = new ContentControl { Content = ((ViewModelClass)DataContext)PrintableViewModelProperty};

    // This part with the margins is not strictly relevant to your question per se,
    // but it's useful enough to be worth including here for future reference
    PageImageableArea area = printDialog.PrintQueue.GetPrintCapabilities(printDialog.PrintTicket).PageImageableArea;

    contentControl.Margin = new Thickness(area.OriginWidth, area.OriginHeight,
        printDialog.PrintableAreaWidth - area.ExtentWidth - area.OriginWidth,
        printDialog.PrintableAreaHeight - area.ExtentHeight - area.OriginHeight);

    // This shows retrieving the data template which is declared using the DataType
    // property. Of course, if you simply declare a key and reference it explicitly
    // in XAML, you can just use the key itself here.
    DataTemplateKey key = new DataTemplateKey(typeof(MazeViewModel));

    contentControl.ContentTemplate = (DataTemplate)FindResource(key);
    printDialog.PrintVisual(contentControl, "MazeGenerator");
}

This will cause WPF to automatically reuse the template you've already described for the PrintableViewModel class, populating the ContentControl's visual sub-tree according to that template, duplicating the visual you're displaying on the screen, but without having to do any sort of explicit cloning of UI elements.


The above illustrates how to reuse the visual representation exactly. But of course if you have a desire to customize the output for the purpose of printing, it's as simple as declaring a different DataTemplate to be used when you print.



回答3:

I'm sure there should be easy way to do the copy (other than detaching from parent, printing and attaching back). For example you could try XamlWriter to write xaml, and then read it back via XamlReader. But I suspect there may be some binding and layout errors this way.

Instead I would try to use WriteableBitmap to take a snapshot of printable area and print it. This way you create raster and loose vector, but I'm not good enough in printing to say if it's good or bad. Anyway you could try and check :).

Cheers, Anvaka.



标签: c# wpf xaml clone