【问题标题】:How to wait for WebViewBrush.Redraw() to finish (UWP printing)?如何等待 WebViewBrush.Redraw() 完成(UWP 打印)?
【发布时间】:2018-03-21 16:02:36
【问题描述】:

我有一个带有嵌入式 WebView 的基本 UWP 应用程序,该应用程序显示一个相当大的 HTML 文档(最多 500 个字母大小的打印页面)。

我想添加对打印该 HTML 文档的支持。这是我的方法:

  1. 为了支持分页,我生成了第二个 HTML 文档,通过为每个“页面”使用 <div style="height:100vh"> 将其分成多个“页面”,最多 500 个。
  2. 我将该“分页”HTML 加载到第二个隐藏的 WebView XAML 页面上,我根据用户选择的页面大小调整大小以恰好适合一个页面。
  3. 我正在等待 WebView 完成加载...
  4. 现在对于每个“页面”:
    1. 我将 WebView 的 scrollY 设置为使用 JavaScript 仅显示当前页面:window.scrollTo(0, -pageOffset)
    2. 然后我使用WebViewBrushWebView 中捕获当前页面的快照
    3. 对所有剩余页面重复此操作...

问题:

我可以生成所有 500 页的打印预览,但有时预览会丢失页面,而其他页面显示多次。

我怀疑这是因为我使用 WebViewBrush.Redraw() 来捕获滚动 WebView 的快照,但文档中说 Redraw() 异步发生。我可能会在 WebViewBrush 有机会重绘之前滚动过去当前页面,从而意外捕获下一页。

如何确保 WebViewBrush 已捕获 WebView 以便我可以滚动到下一页?

我生成单个页面的代码:

    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;
    }

注意:如果有使用 WebViewBrush 打印 500 页的替代方法,我愿意提供建议。我尝试为每个页面使用单独的 WebView,但应用程序在 200 个页面后内存不足。

BOUNTY:我发起了一项赏金活动,向任何能弄清楚如何打印 500 页的人提供 100 分。

【问题讨论】:

    标签: printing webview uwp windows-10 uwp-xaml


    【解决方案1】:

    根据WebViewBrush remarks的重要部分:

    WebView 控件具有固有的异步行为,该行为会在其内容完全加载时重绘控件。但是,一旦解析了 XAML(可能在 WebView 加载 URI 内容之前),关联的 WebViewBrush 就会呈现。或者,您可以等待在 WebViewBrush 上调用 SetSource,直到源内容完全加载(例如,通过在 WebView.LoadCompleted 事件的处理程序中调用 SetSource。

    这样你就可以在WebView.LoadCompleted之后调用WebViewBrushSetSource方法。

    【讨论】:

    • 感谢您的回答。在我创建画笔时,WebView 确实完成了加载。我在问题中添加了示例代码。问题是Redraw() 是异步的,但我不能用“等待”。
    • @Mark,你的意思是上面的代码 sn-p 在 WebView.LoadCompleted 事件中吗?您何时以及如何进行打印操作?在看到 UI 中填充的 rectangle 之前,您打印了吗?
    • 是的,我在WebView.LoadCompleted 中致电PrintManager.ShowPrintUIAsync();。在我打开打印 UI 之前,内容肯定会显示在 WebView 中。然后我使用屏幕外的Rectangle 将 WebViewBrush 应用到。 (见上面的新代码)
    • 我改写了我的问题以准确显示问题。你介意再看一遍吗?谢谢!
    • HTML 页面是动态生成的。我实际上可以看到它正确呈现,因为它加载到的 WebView 在我的页面上可见。当我使用 WebViewBrush 捕获它时,我可以看到它从一个页面移动到另一个页面,所以我认为这不是 HTML 问题。
    【解决方案2】:

    我从您的问题中了解到,您只有一个 HTML 文档。就内容而言,这非常大,您可以将其分成 500 页。您需要打印整个 HTML 页面。在这里,我认为您的 HTML 页面中没有您不想打印的内容、空间或部分。

    我用谷歌搜索了你的问题并得到了一些东西。我修改了该代码并在我的 UWP 应用上运行它,以便为您提供强大的基础。

    我确信这不是您想要的确切解决方案,因为我不知道您要打印的页面大小和其他一些参数,但以下代码对您有用 90% 以上。

    此代码根据您提供给方法的大小生成页面。它将整个网页分为no。页数。它没有任何打印逻辑。我相信你有足够的能力(基于你的声誉点:))来使用 C# 和这段代码。您可以根据需要进行修改。

    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;
    }
    

    【讨论】:

    • 您好,感谢您的回答。我知道stackoverflow.com/a/17222629/901334。这就是我最初解决方案的基础。但是,该解决方案存在相同的问题:WebVieBrush.Redraw() 是异步的,并且没有 API 来等待调用,因此我的某些页面出现空白或重复。
    • 我运行了上面的代码,还删除了不推荐使用的方法等等。你可以试试这个。我拿到了这些页面,它们不是黑色的。
    • 我确实尝试了上述方法,同样的问题。见我之前的评论。这很可能是时间问题。
    • 如果不是机密内容,能否分享一下你的html页面链接?
    【解决方案3】:

    按承诺重访。最后保留原始答案。

    结果比我想象的要方式困难。

    这是一个完全不同的解决方案,可以完全避免这个问题:https://github.com/ArthurHub/HTML-Renderer/

    该项目已被废弃,但状况良好且仍具有相关性。有一个核心渲染器和一堆针对各种 .NET 平台的包装器项目

    • WinForms
    • WPF
    • 单声道
    • dotnet 核心

    UWP 未列出,但核心渲染器是一个纯 C# 解决方案,其中有意分解出与平台相关的内容,并复制了几个完全工作的 PAL(平台适配器层)。

    我使用它从 MVC5 Web API 进行打印,为 SPA 客户端提供浏览器内网络打印服务。这个库速度非常快,并且比 MSHTML 或 EdgeHTML 小很多。它不打印,它呈现。示例呈现位图,打印示例在 WinForms PrintDocument.PrintPage 事件提供的 Graphics 对象上绘制该位图。这会产生可怕的上采样像素化,(完全未记录的)解决方案是渲染 Windows 图元文件。仍然存在分页问题,​​但有一个 PDF 的 PAL,您可以从中借鉴该方法。

    著名的遗言:不应该将 WPF 解决方案转换为 UWP。

    如果您有兴趣,我们可以分叉存储库并添加缺少的 UWP PAL。这样做的风险在于获得支持 - 它可能对小部件猴子非常有用。

    对于预览方面,您可能会在 UI 上分页和绘制元文件。


    我的第一个想法是一个应该工作的丑陋黑客:让线程休眠。重绘已经在另一个线程上发生,这就是你处于竞争状态的原因。如果您保证其他线程有更多时间,您的机会将大大提高。

    新问题:Thread.Sleep 在 UWP 中不可用,因为一切都支持延续,除非它不支持。

    幸运的是,还有其他方法可以给这只猫剥皮。

    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
    

    下一个问题是渲染时间是不可预测的,而且长时间的睡眠会损害性能,因此您需要一种方法来测试是否成功。

    你说失败会产生一个全白的页面。我会为全白写一个测试,当我快速运行时回来重写这个答案的结尾。

    【讨论】:

    • 嗨,彼得,感谢您的回答。我尝试让线程休眠,但有时打印文档中的页面会变成纯白色,因为 WebView 没有及时完成绘制。添加更长的等待时间会减慢渲染速度,但仍不会阻止用户对空白页面不满意...
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多