在Windows Store应用程序中从WebView打印静态内容

4

更新3

现在这个问题有赏金了。示例项目和问题简介位于页面底部的TL;DR;下方,所以节省一些阅读时间并跳到结尾吧!

...

我一直在尝试使用NavigateToString通过静态加载页面来运行JerryNixon的这个高度详细、得票最多的答案。我不明白为什么这不应该起作用,但必须承认代码中有些部分让我感到困惑。
原始答案在此处,但为了完整起见,我将在此复制代码。Jerry为那些想要从WebView打印的人提供了以下示例/解决方案。
<Grid Background="Blue">
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="995" />
        <ColumnDefinition Width="300" />
        <ColumnDefinition />
    </Grid.ColumnDefinitions>
    <WebView Grid.Column="0" x:Name="MyWebView" Source="http://www.stackoverflow.com" HorizontalAlignment="Right" />
    <Rectangle Grid.Column="1" x:Name="MyWebViewRectangle" Fill="Red" />
    <ScrollViewer Grid.Column="2" HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto">
        <ItemsControl x:Name="MyPrintPages" VerticalAlignment="Top" HorizontalAlignment="Left">
            <Rectangle Height="150" Width="100" Fill="White" Margin="5" />
            <Rectangle Height="150" Width="100" Fill="White" Margin="5" />
            <Rectangle Height="150" Width="100" Fill="White" Margin="5" />
            <Rectangle Height="150" Width="100" Fill="White" Margin="5" />
            <Rectangle Height="150" Width="100" Fill="White" Margin="5" />
        </ItemsControl>
    </ScrollViewer>
</Grid>

public MainPage()
{
    this.InitializeComponent();
    MyWebView.LoadCompleted += MyWebView_LoadCompleted;
}

void MyWebView_LoadCompleted(object sender, NavigationEventArgs e)
{
    MyWebViewRectangle.Fill = GetWebViewBrush(MyWebView);
    MyPrintPages.ItemsSource = GetWebPages(MyWebView, new Windows.Foundation.Size(100d, 150d));
    MyWebView.Visibility = Windows.UI.Xaml.Visibility.Visible;
}

WebViewBrush GetWebViewBrush(WebView webView)
{
    // resize width to content
    var _OriginalWidth = webView.Width;
    var _WidthString = webView.InvokeScript("eval",
        new[] { "document.body.scrollWidth.toString()" });
    int _ContentWidth;
    if (!int.TryParse(_WidthString, out _ContentWidth))
        throw new Exception(string.Format("failure/width:{0}", _WidthString));
    webView.Width = _ContentWidth;

    // resize height to content
    var _OriginalHeight = webView.Height;
    var _HeightString = webView.InvokeScript("eval",
        new[] { "document.body.scrollHeight.toString()" });
    int _ContentHeight;
    if (!int.TryParse(_HeightString, out _ContentHeight))
        throw new Exception(string.Format("failure/height:{0}", _HeightString));
    webView.Height = _ContentHeight;

    // create brush
    var _OriginalVisibilty = webView.Visibility;
    webView.Visibility = Windows.UI.Xaml.Visibility.Visible;
    var _Brush = new WebViewBrush
    {
        SourceName = webView.Name,
        Stretch = Stretch.Uniform
    };
    _Brush.Redraw();

    // reset, return
    webView.Width = _OriginalWidth;
    webView.Height = _OriginalHeight;
    webView.Visibility = _OriginalVisibilty;
    return _Brush;
}

IEnumerable<FrameworkElement> GetWebPages(WebView webView, Windows.Foundation.Size page)
{
    // ask the content its width
    var _WidthString = webView.InvokeScript("eval",
        new[] { "document.body.scrollWidth.toString()" });
    int _ContentWidth;
    if (!int.TryParse(_WidthString, out _ContentWidth))
        throw new Exception(string.Format("failure/width:{0}", _WidthString));
    webView.Width = _ContentWidth;

    // ask the content its height
    var _HeightString = webView.InvokeScript("eval",
        new[] { "document.body.scrollHeight.toString()" });
    int _ContentHeight;
    if (!int.TryParse(_HeightString, out _ContentHeight))
        throw new Exception(string.Format("failure/height:{0}", _HeightString));
    webView.Height = _ContentHeight;

    // how many pages will there be?
    var _Scale = page.Width / _ContentWidth;
    var _ScaledHeight = (_ContentHeight * _Scale);
    var _PageCount = (double)_ScaledHeight / page.Height;
    _PageCount = _PageCount + ((_PageCount > (int)_PageCount) ? 1 : 0);

    // create the pages
    var _Pages = new List<Windows.UI.Xaml.Shapes.Rectangle>();
    for (int i = 0; i < (int)_PageCount; i++)
    {
        var _TranslateY = -page.Height * i;
        var _Page = new Windows.UI.Xaml.Shapes.Rectangle
        {
            Height = page.Height,
            Width = page.Width,
            Margin = new Thickness(5),
            Tag = new TranslateTransform { Y = _TranslateY },
        };
        _Page.Loaded += (s, e) =>
        {
            var _Rectangle = s as Windows.UI.Xaml.Shapes.Rectangle;
            var _Brush = GetWebViewBrush(webView);
            _Brush.Stretch = Stretch.UniformToFill;
            _Brush.AlignmentY = AlignmentY.Top;
            _Brush.Transform = _Rectangle.Tag as TranslateTransform;
            _Rectangle.Fill = _Brush;
        };
        _Pages.Add(_Page);
    }
    return _Pages;
}

我的唯一更改是从 WebView 中移除“Source”属性,并将此行添加到“MainPage()”方法中。
var html = await Windows.Storage.PathIO.ReadTextAsync("ms-appx:///Assets/sherlock.html");
MyWebView.NavigateToString(html);
是阿瑟·柯南·道尔(Arthur Conan Doyle)小说《红发联盟》的一部分,这个HTML页面的完整代码在这里 - 注意末尾附近的一段重复出现了几次。这是为了测试而做的,我会在下面解释。

这个样本最初对我来说有效,对于一个小文件。(Sherlock.html没有填充章节)

然而,当我处理大于某个文件大小(通常在4000字以上)的文件时,会出现以下行为。

WebView缩小到非常窄的视图

(请参见更新说明,了解为什么将其划掉)

然后屏幕变灰。(我感觉有点傻把一个灰色屏幕的截图发布出来,但我在这里试图尽可能详细)

enter image description here

该应用程序没有崩溃。没有调试消息出现,也没有发生异常。就好像网格被简单地移除了。 这不是闪屏(我已将其设置为蓝色),也不是应用程序背景(较暗的灰色)。就好像加载了其他东西覆盖在页面顶部。

我尝试调整代码的每个部分,但似乎无法确定问题的原因。此外,该代码的某些部分让我困惑不解。例如,在Load_Completed方法中。

MyWebViewRectangle.Fill = GetWebViewBrush(MyWebView);
MyPrintPages.ItemsSource = GetWebPages(MyWebView, new Windows.Foundation.Size(100d, 150d));
MyWebView.Visibility = Windows.UI.Xaml.Visibility.Visible;

Line1: MyWebViewRectangleWebView的截图填充。为什么?这个控件再也没有被引用过。我猜可能是为了使用户更清晰地查看页面,但似乎没有程序目的。
Line3: MyWebView.Visibility被设置为Visible。这在GetWebViewBrush中再次发生。
var _OriginalVisibilty = webView.Visibility;
webView.Visibility = Windows.UI.Xaml.Visibility.Visible;
// and a few lines later... 
webView.Visibility = _OriginalVisibilty;

据我所知,Webview从未被“Collapsed”,但它被设置为“Visible”三次。
如果有人能向我解释其中任何一点(甚至是JerryNixon自己?)我会非常感激。我已经花了几个月的时间来开发应用程序,这是最后一个难题。我不能没有打印功能就发布!
更新
今天早上做了一些发现。我之前一直在XAML中设置WebView的宽度,但即使是最小的文件也会失败。基于这个发现,我在html文件中将宽度设置为800像素,然后它就可以工作了。太好了,问题解决了,对吗?不幸的是,不是这样的。我再次尝试使用更长的文件(《红发联盟》的完整版本,here),同样的问题再次发生了。现在情况变得越来越奇怪了。
我在GetWebPages方法中的webView.Height = _ContentHeight;处插入了一个断点。这向我展示了_ContentHeight确切地是“13241”。
然而,插入断点可使代码正常工作。一旦我点击中断并继续,它似乎全部加载完毕。虽然奇怪的是,直到我将鼠标悬停在滚动条上之前,WebView才会变得不可见。

如果我删除断点,我会再次看到灰色屏幕。

如果我手动将_ContentHeight设置为13230,页面就会加载。这似乎是一个神奇的数字。任何比这更高的数字都会导致页面失败。但是,如果我将html的宽度改为小于800px,它也会失败。因此,逻辑上讲,存在一种800:13230像素(或1:16.5375)的神奇比例。如果我超过这个比例,我就会触发灰色死屏。我可以禁用红色矩形并将GetWebPages限制为3页,灰色屏幕仍然存在。这告诉我问题出在WebView本身。

如果我设置一个断点,这个问题就消失了。

还要注意,在它正常工作时MyWebViewRectangle中的额外空格和MyPrintPages中的额外空白页面:

Working result, but with extra whitespace and blank pages appended

注意:我还添加了NateDiamond的建议,包括事件LongRunningScriptDetectedUnsafeContentWarningDisplayingUnviewableContentIdentified 这是我的示例项目的完整源代码: https://onedrive.live.com/redir?resid=EE99EF9560A6740E!110509&authkey=!ACrZgm6uq5k5yck&ithint=file%2czip 更新2
有些进展。
我在示例文件中放置了一些标头(通过试错找到每个计算页面的“底部”),并增加了页面大小,以便能够看到标记。虽然有9个页面,但只有5个和一小部分第6页被呈现。 GetWebPages创建了9个页面对象,但只有5个左右具有呈现的HTML内容。其余为空。

nothing past page 5

(注意,我改变了背景颜色,因为红色在蓝色上给了我偏头痛)
我已经检查了代码并进行了一些修改,现在我根本不再遇到灰屏问题。然而,每3次中大约有2次只渲染第一页,并且它被裁剪了。这似乎是完全随机的。当渲染9页时,并非所有页面都有内容,正如所述。
以下是我的更改:
添加新方法Resize_WebView()
public void ResizeWebView()
{
    OriginalHeight = MyWebView.Height;

    var _WidthString = MyWebView.InvokeScript("eval", new[] { "document.body.scrollWidth.toString()" });
    int _ContentWidth;
    if (!int.TryParse(_WidthString, out _ContentWidth))
        throw new Exception(string.Format("failure/width:{0}", _WidthString));
    MyWebView.Width = _ContentWidth;

    // ask the content its height
    var _HeightString = MyWebView.InvokeScript("eval", new[] { "document.body.scrollHeight.toString()" });
    int _ContentHeight;
    if (!int.TryParse(_HeightString, out _ContentHeight))
        throw new Exception(string.Format("failure/height:{0}", _HeightString));
    MyWebView.Height = _ContentHeight;
}

MyWebView_LoadCompleted中注释掉了矩形填充,并添加了调用ResizeWebView()的语句:
void MyWebView_LoadCompleted(object sender, NavigationEventArgs e)
{
    ResizeWebView();

    //MyWebViewRectangle.Fill = GetWebViewBrush(MyWebView);
    MyPrintPages.ItemsSource = GetWebPages(MyWebView, new Windows.Foundation.Size(300d, 450d));
}

GetMyWebPages 中更改了刷子加载的位置:
IEnumerable<FrameworkElement> GetWebPages(WebView webView, Windows.Foundation.Size page) 
{
    // ask the content its width
    /*var _WidthString = webView.InvokeScript("eval",
        new[] { "document.body.scrollWidth.toString()" });
    int _ContentWidth;
    if (!int.TryParse(_WidthString, out _ContentWidth))
        throw new Exception(string.Format("failure/width:{0}", _WidthString));
    webView.Width = _ContentWidth;

    // ask the content its height
    var _HeightString = webView.InvokeScript("eval",
        new[] { "document.body.scrollHeight.toString()" });
    int _ContentHeight;
    if (!int.TryParse(_HeightString, out _ContentHeight))
        throw new Exception(string.Format("failure/height:{0}", _HeightString));
    webView.Height = _ContentHeight;

    // Manually set _ContentHeight: comment out the above block and uncomment this to test. 
    // int _ContentHeight = 13230;
    // webView.Height = _ContentHeight;*/

    // how many pages will there be?
    //var _Scale = page.Width / _ContentWidth;
    var _Scale = page.Width / webView.Width; // now sourced directly from webview
    //var _ScaledHeight = (_ContentHeight * _Scale); 
    var _ScaledHeight = (webView.Height * _Scale); // now sourced directly from webview
    var _PageCount = (double)_ScaledHeight / page.Height;
    _PageCount = _PageCount + ((_PageCount > (int)_PageCount) ? 1 : 0);

    // create the pages
    var _Pages = new List<Windows.UI.Xaml.Shapes.Rectangle>();
    for (int i = 0; i < (int)_PageCount; i++)
    {
        var _TranslateY = -page.Height * i;
        var _Page = new Windows.UI.Xaml.Shapes.Rectangle
        {
            Height = page.Height,
            Width = page.Width,
            Margin = new Thickness(5),
            Tag = new TranslateTransform { Y = _TranslateY },
        };
        var _Brush = GetWebViewBrush(webView);
        _Brush.Stretch = Stretch.UniformToFill;
        _Brush.AlignmentY = AlignmentY.Top;
        _Brush.Transform = _Page.Tag as TranslateTransform;
        _Page.Fill = _Brush;
        /*_Page.Loaded += async (s, e) =>
        {
            var _Rectangle = s as Windows.UI.Xaml.Shapes.Rectangle;
            var _Brush = await GetMyWebViewBrush(webView);
            _Brush.Stretch = Stretch.UniformToFill;
            _Brush.AlignmentY = AlignmentY.Top;
            _Brush.Transform = _Rectangle.Tag as TranslateTransform;
            _Rectangle.Fill = _Brush;
        };*/
        _Pages.Add(_Page);
    }
    return _Pages;
}

GetWebView中移除了现在不必要的调整大小代码。

WebViewBrush GetWebViewBrush(WebView webView)
{
    /*// resize width to content
    var _OriginalWidth = webView.Width;
    var _WidthString = webView.InvokeScript("eval",
        new[] { "document.body.scrollWidth.toString()" });
    int _ContentWidth;
    if (!int.TryParse(_WidthString, out _ContentWidth))
        throw new Exception(string.Format("failure/width:{0}", _WidthString));
    webView.Width = _ContentWidth;

    // resize height to content
    var _OriginalHeight = webView.Height;
    var _HeightString = webView.InvokeScript("eval",
        new[] { "document.body.scrollHeight.toString()" });
    int _ContentHeight;
    if (!int.TryParse(_HeightString, out _ContentHeight))
        throw new Exception(string.Format("failure/height:{0}", _HeightString));
    webView.Height = _ContentHeight;

    // create brush
    var _OriginalVisibilty = webView.Visibility;
    webView.Visibility = Windows.UI.Xaml.Visibility.Visible;*/
    var _Brush = new WebViewBrush
    {
        SourceName = webView.Name,
        Stretch = Stretch.Uniform
    };
    _Brush.Redraw();

    // reset, return
    /*webView.Width = _OriginalWidth;
    webView.Height = _OriginalHeight;
    webView.Visibility = _OriginalVisibilty;*/
    return _Brush;
}

这是一个非常重要的问题。

简短概述:

这里是更新后的项目。 https://onedrive.live.com/redir?resid=EE99EF9560A6740E!110545&authkey=!AMcnB_0xKRhYHIc&ithint=file%2czip

加载并观察。文档中有9页。

  • GetWebPages偶尔只加载一页,被裁剪了
  • 其余时间,GetWebPages加载页面1-5、部分6和3个空白页面

这几乎是你需要知道的所有内容来回答这个问题。

我在这个问题上设置了赏金。我正在寻找解决这个问题的方法,并且如果可能的话,为每个页面添加填充以使文本不会溢出到边缘。


在Windows 8.1中,有许多成功和失败状态的事件,例如LongRunningScriptDetected、UnsafeContentWarningDisplaying和UnviewableContentIdentified。尝试添加这些事件并查看是否触发了任何事件。还可以尝试使用新的NavigateToLocalStreamUri方法。 - Nate Diamond
谢谢Nate。我刚刚添加了所有这些事件,并在每个事件中加入了throw new Exception...。没有异常。再次,只有一个灰色屏幕。我验证了这不是网格被移除的问题 - 页面本身的背景颜色是较深的灰色。这个灰色是“应用程序启动”灰色。 - roryok
你尝试在不同大小的屏幕上测试了吗?尝试在模拟器中加载,并使用不同大小的屏幕查看是否会改变数字。 - Nate Diamond
2个回答

0

[更新]

这个新代码在完全相同的位置截断非常奇怪。这表明 WebViewBrush 中不存在数据。如果您将整个 brush 渲染到控件中,它是否会渲染所有内容?如果我的理论是正确的,您只能看到第 6 页的截止点。这开始像 Surface Pro 上的视频驱动程序错误。Surface Pro 使用英特尔视频芯片组并使用共享即系统内存。您应该有足够的资源来渲染 brush 的单个实例(所有九页)。请记住,即使在更好的硬件上,这种方法仍然可能在大页面计数时导致资源问题。

如果您有机会,请尝试在另一台具有不同制造商的显卡的机器上进行侧面加载,以确认它确实可以工作。我将尝试找到 Surface Pro 1 并查看是否可以重现您的发现。

在你开始使用XAML打印之前,我需要提醒你已知的资源陷阱。XAML打印子系统要求您在呈现之前将所有页面传递给它。然后将整个文档作为一个整体发送到打印机。这要求每一页都被缓存为XAML,然后每一页都被呈现为位图。这全部发生在主内存中。您可能可以使用大约20页左右,但一旦超过这个数量,您很快就会耗尽系统资源。50到100页可能会因内存压力而导致运行时关闭。在低资源设备上,您甚至只能获得更少的页面。

最初我以为你的目标是打印HTML页面。下面的解决方案非常适合这种情况。然而,如果您的最终目标是允许用户从应用程序中打印大型文档,则真正的答案(也是Windows Store应用程序的唯一解决方案)是使用Direct2D和DirectWrite。这些技术允许您将页面“流式传输”到打印机缓存中。打印机缓存由磁盘支持,不会引起同样的内存压力问题(尽管理论上仍可能耗尽磁盘空间)。不用说,这是理想的解决方案,但确实需要很多专业知识。您需要了解一些关于C++ / CX以及Direct2D的知识,才能正确地分页页面。

Direct2D应用程序打印示例

希望这可以帮到您,

詹姆斯


今天我仔细研究了一下,我觉得你遇到的问题是资源限制。在我的工作站级别的机器上,我无法复现这个问题,我的同事也无法在他的笔记本电脑上复现这个问题。我在你的代码中看到的是你为每页创建了一个新的画刷。即使你对画刷进行了变换,整个 WebView 仍会被转换成一个位图,一个非常大的位图,每一页都要这样做。这给 XAML 富合成器施加了资源限制。由于合成器基于 D3D,你很可能在显卡的纹理内存上耗尽了。

我想我有一个解决方案给你。我不会撒谎,它有点丑陋,但对我来说有效,希望对你也有效。基本上,我通过画刷将页面部分渲染到 XAML 中声明的隐藏矩形中。然后,我使用 RenderTargetBitmap 将隐藏矩形渲染出来。这只会渲染我们需要的 WebView 部分。然后,我将 RenderTargetBitmap 设置为 Image 的源。我将这些图像添加到页面集合中。页面集合设置为您的 ItemsControl,就完成了。

大部分的更改都在 GetWebPages 函数中:

async Task<bool> GetWebPages(WebView webView, Windows.Foundation.Size page)
{
    // how many pages will there be?
    var _Scale = page.Width / webView.Width; // now sourced directly from webview
    var _ScaledHeight = (webView.Height * _Scale); // now sourced directly from webview
    var _PageCount = (double)_ScaledHeight / page.Height;
    _PageCount = _PageCount + ((_PageCount > (int)_PageCount) ? 1 : 0);

    var _Brush = GetWebViewBrush(webView);

    for (int i = 0; i < (int)_PageCount; i++)
    {
        var _TranslateY = -page.Height * i;
        // You can probably optimize this bit
        _RenderSource.Height = page.Height;
        _RenderSource.Width = page.Width;
        Margin = new Thickness(5);

        _Brush.Stretch = Stretch.UniformToFill;
        _Brush.AlignmentY = AlignmentY.Top;
        _Brush.Transform = new TranslateTransform { Y = _TranslateY };
        _RenderSource.Fill = _Brush;

        RenderTargetBitmap rtb = new RenderTargetBitmap();

        await rtb.RenderAsync(_RenderSource);

        var _iPge = new Image
        {
            Source = rtb,
            Margin = new Thickness(10)
        };

        _Pages.Add(_iPge);
    }
    return true;
}

整个修改后的项目可以在这里找到:http://1drv.ms/1y91iv7 请试一试,让我知道它是否适用于您。
PS:有一些布局问题和分页问题,我没有尝试修复。
希望这可以帮助您,
詹姆斯

我非常感激你抽出的时间,也很想说这个修复可以解决问题,但是对于我来说它仍然在完全相同的位置截断。你可能是对的,关于资源方面——我的开发机器是一台4GB的 Surface Pro。我认为我将不得不尝试一个完全不同的方法——将HTML转换为XAML,在RichTextBlock中呈现并从那里打印。这不是我想要的,但它应该能够解决内存问题。 - roryok

0

虽然这不是一个解决方案,但由于我自己也在苦苦挣扎UWP打印问题,所以想分享我的发现:

1. 灰色屏幕:

你使用WebViewBrush.Redraw()来捕获WebView的“页面”。然而,Redraw()是异步进行的。不幸的是,我们不能使用await等待WebViewBrush捕获WebView。

我怀疑您正在处理下一页,但WebViewBrush尚未捕获上一页。这就是为什么在添加断点时可以正常工作的原因。

2. WebViewBrush的大小限制:

我遇到了类似的问题。对我而言,如果我的WebView高度超过8000px,WebVewBrush就无法正常工作。我认为这也取决于视图的宽度,因此取决于像素的总数。

我的尝试:将WebView调整为页面大小,并使用JavaScript从页面滚动到页面。但这并不能解决以上的灰页问题。

3. 内存限制

是的,这是个问题。我尝试每页使用一个单独的WebView,但在240页后应用程序会耗尽内存。

然而,我成功地使用了500个以矩形渲染为基础的"普通"页面。

我还尝试使用MS Edge加载和打印一个有500页的PDF文件,这是可行的,因此可以打印大型文档。

供参考,这是我自己的问题(如果我找到解决方案,我会发布更新):

如何等待WebViewBrush.Redraw()完成(UWP打印)?


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接