How to wait for WebViewBrush.Redraw() to finish (UWP printing)?

亡梦爱人 提交于 2019-12-10 12:55:19

问题


I have a basic UWP app with an embedded WebView presenting a rather large HTML document (up to 500 letter-sized printed pages).

I'd like to add support for printing that HTML document. Here is my approach:

  1. To support pagination, I generate a second HTML document broken into "pages" by using a <div style="height:100vh"> for each "page", up to 500 of these.
  2. I load that "paginated" HTML into a second, hidden WebView on the XAML page that I resize to fit exactly one page based on the user selected page size.
  3. I wait for the WebView to finish loading...
  4. Now for each "page":
    1. I set the WebView's scrollY to show only the current page using JavaScript: window.scrollTo(0, -pageOffset)
    2. I then use WebViewBrush to capture a snapshot of the current page in the WebView
    3. Repeat this for all remaining pages...

The problem:

I can generate the print preview of all 500 pages, but sometimes the preview is missing pages while other pages show up multiple times.

I suspect this is because I use WebViewBrush.Redraw() to capture a snapshot of the scrolled WebView, but the documentation says Redraw() happens asynchronously. I may scroll past the current page before WebViewBrush gets a chance to redraw, hence accidentally capturing the next page.

How can I make sure that WebViewBrush has captured the WebView so that I can scroll to the next page?

My code to generate a single page:

    private async Task<Rectangle> MakePage(WebView webView, 
                        Size pageSize, double pageOffset)
    {
        // Scroll to next page:
        await webView.EvaluateJavaScriptSnippetAsync(
                             $"window.scrollTo(0, {pageOffset})");

        var brush = new WebViewBrush();
        brush.Stretch = Stretch.Uniform;
        brush.SetSource(webView);
        brush.Redraw(); // XXX Need to wait for this, but there's no API

        // Add a delay hoping Redraw() finishes... I think here is the problem.
        await Task.Delay(150);

        var rectangle = new Rectangle()
        {
            Width = pageSize.Width,
            Height = pageSize.Height
        };

        rectangle.Fill = brush;
        brush.Stretch = Stretch.UniformToFill;
        brush.AlignmentY = AlignmentY.Top;

        return rectangle;
    }

Note: If there's an alternative to using WebViewBrush to print 500 pages, I'm open for suggestions. I tried using a separate WebView for each page, but the app runs out of memory after 200 pages.

BOUNTY: I started a bounty offering 100 points to anybody who can figure out how to print 500 pages.


回答1:


According to the Important section of the WebViewBrush remarks:

A WebView control has an inherently asynchronous behavior that redraws the control when its content is completely loaded. But an associated WebViewBrush renders as soon as the XAML is parsed (which might be before the URI content is loaded by the WebView ). Alternatively, you can wait to call SetSource on the WebViewBrush until the source content is fully loaded (for example by calling SetSource in the handler for the WebView.LoadCompleted event.

So that you could calling SetSource method of WebViewBrush after WebView.LoadCompleted.




回答2:


What I understand from your question is that you have a single HTML document. That is very much large in terms of content that you can devide it into 500 pages. You need to print that whole HTML page. Here, I considers that there is no content or space or section in your HTML page which you do not want to print.

I googled your problem and got something. I modified that code and ran it on my UWP app for giving you strong base.

I am sure that this is not the exact solution you want as I do not know the page size you want to print and some other parametes but the following code is more than 90% useful to you.

This code generates pages based on the Size you provide to the method. It will devide the whole webpage into no. of pages. It does not have any printng logic. I am sure you have enough capability (based on your reputation point :) ) to play with C# and this code. You can modify it as per you need.

XAML

<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
    <WebView x:Name="wv" Source="http://www.stackoverflow.com"></WebView>
</Grid>

C#

public MainPage()
{
    this.InitializeComponent();

    wv.LoadCompleted += Wv_LoadCompleted;
}

async private void Wv_LoadCompleted(object sender, NavigationEventArgs e)
{
    var allPages = await GetWebPages(wv, new Windows.Foundation.Size(100d, 150d));
}

async Task<IEnumerable<FrameworkElement>> GetWebPages(WebView webView, Windows.Foundation.Size pageSize)
{
    // GETTING WIDTH FROM WEVIEW CONTENT
    var widthFromView = await webView.InvokeScriptAsync("eval", new[] { "document.body.scrollWidth.toString()" });

    int contentWidth;
    if (!int.TryParse(widthFromView, out contentWidth))
        throw new Exception(string.Format("failure/width:{0}", widthFromView));
    webView.Width = contentWidth;

    // GETTING HEIGHT FROM WEBVIEW CONTENT
    var heightFromView = await webView.InvokeScriptAsync("eval", new[] { "document.body.scrollHeight.toString()" });

    int contentHeight;
    if (!int.TryParse(heightFromView, out contentHeight))
        throw new Exception(string.Format("failure/height:{0}", heightFromView));

    webView.Height = contentHeight;

    // CALCULATING NO OF PAGES
    var scale = pageSize.Width / contentWidth;
    var scaledHeight = (contentHeight * scale);
    var pageCount = (double)scaledHeight / pageSize.Height;
    pageCount = pageCount + ((pageCount > (int)pageCount) ? 1 : 0);

    // CREATE PAGES
    var pages = new List<Windows.UI.Xaml.Shapes.Rectangle>();
    for (int i = 0; i < (int)pageCount; i++)
    {
        var translateY = -pageSize.Height * i;
        var page = new Windows.UI.Xaml.Shapes.Rectangle
        {
            Height = pageSize.Height,
            Width = pageSize.Width,
            Margin = new Thickness(5),
            Tag = new TranslateTransform { Y = translateY },
        };

        page.Loaded += async (s, e) =>
        {
            var rectangle = s as Windows.UI.Xaml.Shapes.Rectangle;
            var wvBrush = await GetWebViewBrush(webView);
            wvBrush.Stretch = Stretch.UniformToFill;
            wvBrush.AlignmentY = AlignmentY.Top;
            wvBrush.Transform = rectangle.Tag as TranslateTransform;
            rectangle.Fill = wvBrush;
        };

        pages.Add(page);
    }
    return pages;
}

async Task<WebViewBrush> GetWebViewBrush(WebView webView)
{
    // ASSING ORIGINAL CONTENT WIDTH
    var originalWidth = webView.Width;

    var widthFromView = await webView.InvokeScriptAsync("eval", new[] { "document.body.scrollWidth.toString()" });

    int contentWidth;
    if (!int.TryParse(widthFromView, out contentWidth))
        throw new Exception(string.Format("failure/width:{0}", widthFromView));
    webView.Width = contentWidth;

    // ASSINGING ORIGINAL CONTENT HEIGHT
    var originalHeight = webView.Height;

    var heightFromView = await webView.InvokeScriptAsync("eval", new[] { "document.body.scrollHeight.toString()" });

    int contentHeight;
    if (!int.TryParse(heightFromView, out contentHeight))
        throw new Exception(string.Format("failure/height:{0}", heightFromView));
    webView.Height = contentHeight;

    // CREATING BRUSH
    var originalVisibilty = webView.Visibility;
    webView.Visibility = Windows.UI.Xaml.Visibility.Visible;

    var wvBrush = new WebViewBrush
    {
        SourceName = webView.Name,
        Stretch = Stretch.Uniform
    };
    wvBrush.Redraw();

    webView.Width = originalWidth;
    webView.Height = originalHeight;
    webView.Visibility = originalVisibilty;

    return wvBrush;
}



回答3:


Revisited as promised. Original answer preserved at end.

Turned out to be way harder than I thought.

Here's a totally different solution that avoids the problem entirely: https://github.com/ArthurHub/HTML-Renderer/

This project is abandoned but is in good condition and remains relevant. There is a core renderer and a bunch of wrapper projects targeting various .NET platforms

  • WinForms
  • WPF
  • Mono
  • dotnet core

UWP is not listed, but the core renderer is a pure C# solution with platform dependent stuff deliberately factored out and several fully worked PALs (platform adapter layers) to copy.

I have used it to print from an MVC5 Web API providing in-browser network printing services to SPA clients. This library is blazingly fast and much smaller than MSHTML or EdgeHTML. It doesn't print, it renders. The samples render a bitmap, which the printing sample draws on the Graphics object provided by a WinForms PrintDocument.PrintPage event. This produces horrible up-sampling pixilation for which the (totally undocumented) solution is to render a Windows Metafile instead. There is still the question of pagination, but there is a PAL for PDF from which you can crib the approach.

Famous last words: It shouldn't be hard to convert the WPF solution to UWP.

If you're interested we could fork the repo and add the missing UWP PAL. The risk with doing so is being badgered for support - it's likely to be very useful to widget monkeys.

For the preview aspect you would probably paginate and paint metafiles on the UI.


My first thought is an ugly hack that ought to work: sleep the thread. The redraw is already happening on another thread, that's why you're in a race condition. If you guarantee the other thread more time your chances improve greatly.

New problem: Thread.Sleep is not available in UWP because everything supports continuations except when it doesn't.

Fortunately there are other ways to skin this cat.

wvBrush.Redraw();

// System.Threading.Tasks.Task.Delay(30).Wait() is more elegant but less portable than...
using (var waitHandle = new System.Threading.ManualResetEventSlim(initialState: false))
  waitHandle.Wait(TimeSpan.FromMilliseconds(30)); // exploit timeout

The next problem is that render time is unpredictable and a long sleep hurts performance, so you need a way to test for success.

You say that failure produces an all white page. I will write a test for all-white and come back and rewrite the end of this answer when I get it working fast.



来源:https://stackoverflow.com/questions/49411176/how-to-wait-for-webviewbrush-redraw-to-finish-uwp-printing

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!