I am trying to convert my WebView to a WebViewBrush in order to print it, in my UWP (C#/XAML) app.
I've set up my WebView, and the Brush such that when I click a button the WebView is hidden and the WebViewBrush gets displayed.
This is the XAML:
<WebView ext:Extension.HtmlString="{Binding Html}"
x:Name="saveWebView"
Grid.Row="1"
Grid.Column="0" />
<Rectangle Height="400" x:Name="saveWebViewBrush" />
When I click the button to show the Brush, basically it's only taking a snapshot of what was visible in the WebView. What I want is to take a snapshot of the entire WebView (and not the scrollbars either!).
The only other person I've seen attempt this was https://stackoverflow.com/a/17222629/2884981 - but unfortunately that's a few years old, and when I try that solution I get a million errors stemming from InvokeScript being obsolete and InvokeScriptAsync causes breaking changes.
The C# code when I press the button is this:
private async void OnButtonClick(object sender, RoutedEventArgs e)
{
//make the rectangle visible when you want something over the top of the web content
saveWebViewBrush.Visibility = Windows.UI.Xaml.Visibility.Visible;
//if the rectangle is visible, then hit the webview and put the content in the webviewbrush
if (saveWebViewBrush.Visibility == Windows.UI.Xaml.Visibility.Visible)
{
WebViewBrush b = new WebViewBrush();
b.SourceName = "saveWebView";
b.Redraw();
saveWebViewBrush.Fill = b;
saveWebView.Visibility = Windows.UI.Xaml.Visibility.Collapsed;
}
}
If anyone knows how to convert this whole WebView to a WebView brush I'd be most grateful.
EDIT
To explain the "why", I am trying to print the contents of a WebView. It seems from what I have read that this is not possible, unless I convert it to a WebViewBrush. But if anyone has any alternative ideas I am all ears!
If anyone knows how to convert this whole WebView to a WebView brush I'd be most grateful.
To achieve this, you can try with following method:
private async void OnButtonClick(object sender, RoutedEventArgs e)
{
int width;
int height;
// get the total width and height
var widthString = await saveWebView.InvokeScriptAsync("eval", new[] { "document.body.scrollWidth.toString()" });
var heightString = await saveWebView.InvokeScriptAsync("eval", new[] { "document.body.scrollHeight.toString()" });
if (!int.TryParse(widthString, out width))
{
throw new Exception("Unable to get page width");
}
if (!int.TryParse(heightString, out height))
{
throw new Exception("Unable to get page height");
}
// resize the webview to the content
saveWebView.Width = width;
saveWebView.Height = height;
WebViewBrush b = new WebViewBrush();
b.SourceName = "saveWebView";
b.Redraw();
saveWebViewBrush.Fill = b;
}
Please note that WebViewBrush.Redraw method happens asynchronously. So to make sure we can get the complete snapshot, we'd better not hide the WebView or we can add some delay before hide the WebView like:
await Task.Delay(500);
saveWebView.Visibility = Windows.UI.Xaml.Visibility.Collapsed;
Once we have the "saveWebViewBrush" Rectangle, we can refer to the Printing sample or this answer to print it.
Related
Friends!
There is a main window, it contains a frame with which I switch between pages.
I have a page that has a canvas. Canvas in the background stream receives data in the form of images in a mosaic view.
foreach (var item in CoreData.fillWallArray.GetConsumingEnumerable())
{
if (File.Exists(item.PathFile))
{
Application.Current.Dispatcher.Invoke(new Action(() =>
{
Image image = new Image();
image.Source = BitmapImageFromFile(item.PathFile);
image.Width = (int)Math.Truncate((CoreData.settings.CellWidth * 30) / 2.54);
image.Height = (int)Math.Truncate((CoreData.settings.CellHeight * 30) / 2.54);
Canvas.SetLeft(image, item.Column * (int)Math.Truncate((CoreData.settings.CellWidth * 30) / 2.54));
Canvas.SetTop(image, item.Row * (int)Math.Truncate((CoreData.settings.CellHeight * 30) / 2.54));
can.Children.Add(image);
}));
Thread.Sleep(100);
}
}
My task is to bring this canvas to the second screen. To do this, I create a second window and, as a context, pass the canvas that I need.
var _BroadcastWindow = new BroadcastWindow();
_BroadcastWindow.DataContext = this.can;
_BroadcastWindow.Show();
And in the second window, I link the data.
<Grid>
<Grid.Background>
<VisualBrush Visual="{Binding}"/>
</Grid.Background>
</Grid>
Everything works fine, data from the canvas synchronously displayed in the second window. But as soon as I switch to another page, the Visualbrush is no longer updated. As soon as I switch back to the page with the canvas I see in the second window, it is updated.
What could be the problem?
I also tried to call Measure, Arrange, UpdateLayout when adding data to the canvas in the background thread, but this did not produce results.
I assume when you say "go to another page" you mean something along the lines of:
frame.Navigate(new System.Uri("Page2.xaml", UriKind.RelativeOrAbsolute));
Every time you do this, your app loads a new Page from a given source. If the current page happens to be the Page that has your Canvas on it, navigation will create a new Canvas instance. If not, and there is no JournalEntry.KeepAlive="true" set for the Page with your Canvas, then contents of the Frame will just get recreated from the Source file every time it is displayed, and a new Canvas will be created with it. Something will get disconnected or prematurely destroyed at some point. Even with KeepAlive set to True, you'll probably just end up with multiple instances of Canvas loaded in memory. Which one do you want to bind to...?
Some alternative approaches off the top of my head:
Cache the Image itself in your View Model and bind both your Canvas on the Page and the VisualBrush to that.
Cache the whole Canvas in your View Model, then switch its contents as needed.
The second approach required only minimal changes to your code, so I could throw in a working example (although I don't know if it's the most optimal):
In Page1.xaml (the page that displays the Canvas):
<Grid>
<ContentControl Content="{Binding canvas, Source={x:Static local:CanvasViewModel.Instance}}" />
</Grid>
In BroadcastWindow.xaml:
<Grid>
<Grid.Background>
<VisualBrush Visual="{Binding}"/>
</Grid.Background>
</Grid>
Example singleton View Model to hold the canvas:
public class CanvasViewModel
{
Rectangle r = new Rectangle
{
Fill = Brushes.Orange,
Width = 200,
Height = 100
};
Ellipse e = new Ellipse
{
Fill = Brushes.DodgerBlue,
Width = 100,
Height = 100
};
public Canvas canvas { get; set; }
public void Initialize()
{
canvas = new Canvas();
Switch(1);
}
// Here the contents of the canvas are switched
// I called it from Click events of two Buttons outside of Frame
// In your case, I imagine it will be something like this:
// public void LoadImage(string path) {...}
public void Switch(int imageNr)
{
switch (imageNr)
{
case 1:
canvas.Children.Clear();
canvas.Children.Add(r);
break;
case 2:
{
canvas.Children.Clear();
canvas.Children.Add(e);
}
break;
default:
break;
}
}
#region CONSTRUCTOR
static CanvasViewModel() { }
private CanvasViewModel() { }
private static CanvasViewModel GetAppViewModelHolder()
{
CanvasViewModel vh = new CanvasViewModel();
vh.Initialize();
return vh;
}
#endregion
#region SINGLETON Instance
private static readonly object _syncRoot = new object();
private static volatile CanvasViewModel instance;
public static CanvasViewModel Instance
{
get
{
var result = instance;
if (result == null)
{
lock (_syncRoot)
{
if (instance == null)
{
result = instance = GetAppViewModelHolder();
}
}
}
return result;
}
}
#endregion
}
Switching between images in the Click event of a Button outside of Frame:
private void Button_Click(object sender, RoutedEventArgs e)
{
CanvasViewModel.Instance.Switch(2);
}
I'm working on Xamarin.Forms based project, trying to print the webview content with pagination.
I've referred the following link already:
How do I print WebView content in a Windows Store App?
But unfortunately this way is not working with Xamarin.Forms because the way demonstrated in the above link is by filling the rectangle(windows shape) using webviewbrush (both the rectangle and webviewbrush are platform dependent controls to UWP). The problem is we can't use webviewbrush to draw rectangle(Xamarin Forms shape).
As a workaround I did the following:
created a boxview in xamarin forms PCL project
created a renderer for this boxview in UWP project(this will give us the windows rectangle shape) and then I did put this rectangle shape into one of the public static properties in PCL project's App.cs class to make it available for Webviewrenderer.cs class for filling this rectangle with webviewbrush.
I can able to access it from webviewrenderer.cs class from UWP project but the problem is the printer dialog shows an empty page.
Pagination works just fine as demonstrated in the above stack overflow link, but all pages are being empty.
Apparently the problem would be with rectangle. Because the same logic is just works fine if I create a native UWP project and the printer dialog shows the webpage as well.
Any help would be much appreciated.
Thanks in advance!
Pagination works just fine as demonstrated in the above stack overflow link, but all pages are being empty.
I have realized the basic printing feature according to your process. I placed the GetWebViewBrush method in the webviewrenderer. Unfortunately, the same issue occur in my side.
async Task<WebViewBrush> GetWebViewBrush(Windows.UI.Xaml.Controls.WebView webView)
{
// resize width to content
var _OriginalWidth = webView.Width;
var _WidthString = await webView.InvokeScriptAsync("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 = await webView.InvokeScriptAsync("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
{
Stretch = Stretch.Uniform,
SourceName = webView.Name
};
// _Brush.SetSource(webView);
_Brush.Redraw();
// reset, return
webView.Width = _OriginalWidth;
webView.Height = _OriginalHeight;
webView.Visibility = _OriginalVisibilty;
return _Brush;
}
When I was debuging, I found webView.Name has never been set value, So I make a new Name property for the customWebView.
public static readonly BindableProperty NameProperty = BindableProperty.Create(
propertyName: "Name",
returnType: typeof(string),
declaringType: typeof(MyWebView),
defaultValue: default(string));
public string Name
{
get { return (string)GetValue(NameProperty); }
set { SetValue(NameProperty, value); }
}
}
And set the Name property of native control(Windows.UI.Xaml.Controls.WebView) with the following code in OnElementChanged method.
if (e.NewElement != null)
{
Control.Name = (Element as MyWebView).Name;
Control.NavigationCompleted += Control_NavigationCompleted;
}
Surprisingly, the page is no longer empty.
I am using a telerik SizeDrawer control for UWP. and in it I want the drawer content to cover full page when it opens. So I tried to bind to Actual-Width of the page but that didn't work and then I tried to register it with sizeChanged event but width of drawer content still remains 0. unless I explicitly define it to some value like 300 then it remains 300 but I want it to cover full screen.
CODE
<telerik:RadSideDrawer Name="MainDrawer" SizeChanged="MainDrawer_SizeChanged"
Loaded="MainDrawer_Loaded">
<telerik:RadSideDrawer.MainContent>
<Grid />
</telerik:RadSideDrawer.MainContent>
<telerik:RadSideDrawer.DrawerContent>
<Grid Name="DrawerGrid" Background="Yellow">
</Grid>
</telerik:RadSideDrawer.DrawerContent>
</telerik:RadSideDrawer>
C#
private void MainDrawer_SizeChanged(object sender, SizeChangedEventArgs e)
{
DrawerGrid.Width = e.NewSize.Width;
}
private void MainDrawer_Loaded(object sender, RoutedEventArgs e)
{
DrawerGrid.Width = MainDrawer.Width;
}
Note that height of drawer grid is alright and stretches to full screen but width remains 0 and if width is untouched totally then it is 240 fixed.
This is how you do it -
First, define a double property that gets the width of the current Window.
public double WindowWidth => Window.Current.Bounds.Width;
Then use it as a one-time binding source for the DrawerLength of your RadSideDrawer.
<primitives:RadSideDrawer x:Name="MainDrawer"
DrawerLength="{x:Bind WindowWidth}" ... />
Finally, update the DrawerLength whenever the size of the MainDrawer gets updated.
private void MainDrawer_SizeChanged(object sender, SizeChangedEventArgs e) =>
MainDrawer.DrawerLength = e.NewSize.Width;
Also note if you don't feel like doing bindings, you can replace the first two steps with one line of code, right after InitializeComponent();
MainDrawer.DrawerLength = Window.Current.Bounds.Width;
Update
Looks like when the drawer is collapsed, the control itself resets its Opacity back to 1, and then when you constantly update the DrawerLength, the edge of the drawer could come out and give a bad user experience.
You can try to throttle page size changes with Reactive Extensions, or you can simply set the Opacity of the drawer back to 0 when it's collapsed.
To do this, you just need to monitor the DrawerState property and when it's Closed, set the DrawerGrid.Opacity to 0.
But remember you will also need to set it back to 1 before it gets expanded. Unfortunately there isn't an Opening state and Opened fires too late, so you have to locate the menu button and do it inside its Click event.
public MainPage()
{
InitializeComponent();
MainDrawer.DrawerLength = Window.Current.Bounds.Width;
Loaded += (s, e) =>
{
// GetChildByName: https://github.com/JustinXinLiu/Continuity/blob/master/Continuity/Extensions/UtilExtensions.cs#L44
var menuButton = MainDrawer.GetChildByName<Button>("PART_SideDrawerButton");
menuButton.Click += (s1, e1) => DrawerGrid.Opacity = 1;
};
MainDrawer.RegisterPropertyChangedCallback(RadSideDrawer.DrawerStateProperty, (s, e) =>
{
var state = MainDrawer.DrawerState;
if (state == DrawerState.Closed)
{
DrawerGrid.Opacity = 0;
}
});
}
scrollTo in WebView UWP may be achieved through:
private string ScrollToTopString = #"window.scrollTo(0,0);";
private async void ButtonClick(object sender, RoutedEventArgs e)
{
await WebViewTest.InvokeScriptAsync("eval", new string[] { ScrollToTopString });
}
But what about animated/Smooth scrolling in WebView UWP? In Android that is achieved either officially or through variations (for example, using android.animation.ObjectAnimator), whereas in UWP only ScrollViewer seems to support it as far as I know. Example 1 Example 2
Any ideas?
You can't animate the WebView because the WebView itself isn't actually scrolling, just like your browser doesn't actually scroll; it's the "window" element that's scrolling.
However, if I understand what you're wanting correctly, just replace your ScrollToTopString with this:
var ScrollToTopString = #"var int = setInterval(function() {
window.scrollBy(0, -5);
if( window.pageYOffset === 0 )
clearInterval(int);
}, 1);";
This will scroll that "window" element I mentioned. You can raise the speed of the interval (I have it set to 1) to slow down the animation, or lower the value of scrollBy to make it go faster.
I have a webBrowser control named webBrowser1 that is added and docked as DockStyle.Full on a custom user control. The web-browser accepts some HTML text dynamically and displays it. I disabled the scroll bars of the webBrowser control. My problem is that whenever the content is somewhat lengthy, the webBrowser hides it from below. But the requirement of my project objective is that the webBrowser must not show either scroll bars or it should not hide some of the content. The content must be completely shown as it is without scrolling. That means the user control on which the webBrowser is docked must resize itself according to webBrowser's content. So, can anyone please suggest me how to achieve this? I searched all over the internet and SO but found nothing.
You can get the current size of HTML window via WebBrowser.Document.Window.Size and resize the container control accordingly. Depending on how your WebBrowser control content receives dynamic updates, you'd probably need to do this after each update. You could also try WebBrowser.Document.Body.ScrollRectangle if Document.Window.Size doesn't grow in the expected way.
[EDITED] The following code works for me (IE10):
private void Form1_Load(object sender, EventArgs e)
{
this.BackColor = System.Drawing.Color.DarkGray;
this.webBrowser.ScrollBarsEnabled = false;
this.webBrowser.Dock = DockStyle.None;
this.webBrowser.Location = new System.Drawing.Point(0, 0);
this.webBrowser.Size = new System.Drawing.Size(320, 200);
DownloadAsync("http://www.example.com").ContinueWith((task) =>
{
var html = task.Result;
MessageBox.Show(String.Format(
"WebBrowser.Size: {0}, Document.Window.Size: {1}, Document.Body.ScrollRectangle: {2}\n\n{3}",
this.webBrowser.Size,
this.webBrowser.Document.Window.Size,
this.webBrowser.Document.Body.ScrollRectangle.Size,
html));
this.webBrowser.Size = this.webBrowser.Document.Body.ScrollRectangle.Size;
}, TaskScheduler.FromCurrentSynchronizationContext());
}
async Task<string> DownloadAsync(string url)
{
TaskCompletionSource<bool> onloadTcs = new TaskCompletionSource<bool>();
WebBrowserDocumentCompletedEventHandler handler = null;
handler = delegate
{
this.webBrowser.DocumentCompleted -= handler;
// attach to subscribe to DOM onload event
this.webBrowser.Document.Window.AttachEventHandler("onload", delegate
{
// each navigation has its own TaskCompletionSource
if (onloadTcs.Task.IsCompleted)
return; // this should not be happening
// signal the completion of the page loading
onloadTcs.SetResult(true);
});
};
// register DocumentCompleted handler
this.webBrowser.DocumentCompleted += handler;
// Navigate to url
this.webBrowser.Navigate(url);
// continue upon onload
await onloadTcs.Task;
// the document has been fully loaded, can access DOM here
// return the current HTML snapshot
return ((dynamic)this.webBrowser.Document.DomDocument).documentElement.outerHTML.ToString();
}
To resize your usercontrol you first need to get the size needed by the content. This can be achived with TextRender.MeasureText, like so:
public static int GetContentHeight(string content, Control contentHolder, Font contentFont)
{
Font font = (contentFont != null) ? contentFont : contentHolder.Font;
Size sz = new Size(contentHolder.Width, int.MaxValue);
int padding = 3;
int borders = contentHolder.Height - contentHolder.ClientSize.Height;
TextFormatFlags flags = TextFormatFlags.WordBreak;
sz = TextRenderer.MeasureText(content, contentHolder.Font, sz, flags);
int cHeight = sz.Height + borders + padding;
return cHeight;
}
In your case it's a bit more tricky, as the text contains HTML-tags wich needs to be filtered away, to get the correct height.. I belive this can be achived with RegEx or a simple algo wich removes all content between < and > from a string.. You may also have to create special handlig for some HTML-tags (I.E Lists)