How can I navigate between views/screens by dragging sideways (swipe gesture)? - c#

So I want the user to change the displayed window by dragging it sideways. It shouldn't matter where exactly on the screen is his cursor (so only the dragging action matter) I draw a little representation of what I have in mind. I want the user to see the 2nd screen coming after the cursor, even retracting when he moves his cursor back to the right (basically following his cursor). This action should be both ways: from main window to 2nd window AND from 2nd window back to main window
How would you approach this?
EDIT:
Picture 1; The user places his cursor on point A and clicks. While holding click he drags it across the green arrow.
Picture2; This picture represents and intermediate state when the "slide" is still in progress (you can see the "Statistics" screen still hasn't fully taken over the initial screen)
Picture3; Represents the final state; after the user got at the end of the (imaginary) green arrow. The Statistics screen is now fully displayed and the user can read the information that is on it.
The opposite of what happened now should be allowed (dragging from left to right in order to go back to the initial screen)

You need to apply a translate transform to your content.
The following example shows how to drag the content (or the image of the content). For simplicity, the example only shows how to swipe from right to left. It also doesn't show how to implement a history to navigate back. You would need a Queue to store the next pages and a Stack for the previous pages (based on the swipe direction).
Although the below example does it, I don't recommend to use controls directly to handle pages and their navigation. Instead create page data models and render the controls using a DataTemplate for each page model.
MainWindow.xaml.cs
public partial class MainWindow : Window
{
public object CurrentPage
{
get => (object)GetValue(CurrentPageProperty);
set => SetValue(CurrentPageProperty, value);
}
public static readonly DependencyProperty CurrentPageProperty = DependencyProperty.Register(
"CurrentPage",
typeof(object),
typeof(MainWindow),
new PropertyMetadata(default));
private StackPanel DraggableContent { get; }
// Accept swipe after 25% swipe delta
// based on the max possible swipe width (a displayed page).
// Note that the 'PageHost' will host two pages (their image)
// during the swipe action.
private double SwipeAcceptDragThreshold
=> this.PageHost.ActualWidth / 2 * 0.25;
private bool IsSwiping { get; set; }
private double HorizontalDragStart { get; set; }
private UserControl PreviousPage { get; set; }
private UserControl NextPage { get; set; }
/* Page controls */
private UserControl GreenPage { get; }
private UserControl OrangePage { get; }
private UserControl BluePage { get; }
public MainWindow()
{
InitializeComponent();
this.GreenPage = new PageControl() { Background = Brushes.Green };
this.OrangePage = new PageControl() { Background = Brushes.Orange };
this.BluePage = new GreenPage() { Background = Brushes.Blue };
this.CurrentPage = this.GreenPage;
this.NextPage = this.OrangePage;
this.DraggableContent = new StackPanel() { Orientation = Orientation.Horizontal };
}
protected override void OnPreviewMouseLeftButtonDown(MouseButtonEventArgs e)
{
base.OnPreviewMouseLeftButtonDown(e);
this.HorizontalDragStart = e.GetPosition(this).X;
}
protected override void OnPreviewMouseMove(MouseEventArgs e)
{
base.OnPreviewMouseMove(e);
if (e.LeftButton == MouseButtonState.Released)
{
return;
}
double dragDelta = e.GetPosition(this).X - this.HorizontalDragStart;
if (!this.IsSwiping)
{
bool isSwipeRightToLeft = dragDelta < HorizontalDragStart;
UserControl swipeInPage = null;
UserControl swipeOutPage = null;
if (isSwipeRightToLeft)
{
if (this.NextPage == null)
{
return;
}
swipeInPage = this.NextPage;
swipeOutPage = this.CurrentPage as UserControl;
this.PreviousPage = this.CurrentPage as UserControl;
}
this.IsSwiping = true;
/* Create an image of the content that will be dragged, using VisualBrush */
swipeInPage.Height = this.PageHost.ActualHeight;
swipeInPage.Width = this.PageHost.ActualWidth;
swipeOutPage.Height = this.PageHost.ActualHeight;
swipeOutPage.Width = this.PageHost.ActualWidth;
this.CurrentPage = null;
// Prepare the snapshot
this.DraggableContent.Children.Add(swipeOutPage);
this.DraggableContent.Children.Add(swipeInPage);
// To improve performance, the user will only drag a snapshot
// of the pages. The snapshot is painted on an empty Grid
// using a VisualBrush.
this.CurrentPage = new Grid()
{
Background = new VisualBrush(DraggableContent),
Width = this.PageHost.ActualWidth * 2 // Host two pages
};
}
this.TranslateTransform.X = dragDelta;
}
protected override void OnMouseLeftButtonUp(MouseButtonEventArgs e)
{
base.OnMouseLeftButtonUp(e);
if (this.IsSwiping)
{
this.IsSwiping = false;
double dragDelta = Math.Abs(e.GetPosition(this).X - this.HorizontalDragStart);
bool isDragAccepted = dragDelta > this.SwipeAcceptDragThreshold;
// Disconnect page controls from the visual tree
this.DraggableContent.Children.Clear();
this.CurrentPage = isDragAccepted
? NextPage
: this.PreviousPage;
this.TranslateTransform.X = 0;
// TODO::Generate and set next page or null if last page reached
this.NextPage = this.BluePage;
}
}
}
MainWindow.xaml
<Window>
<!-- Allow content to exceed the Window -->
<ScrollViewer HorizontalScrollBarVisibility="Hidden"
VerticalScrollBarVisibility="Hidden">
<ContentPresenter x:Name="PageHost"
Content="{Binding RelativeSource={RelativeSource AncestorType=Window}, Path=CurrentPage}">
<ContentPresenter.RenderTransform>
<TranslateTransform x:Name="TranslateTransform" />
</ContentPresenter.RenderTransform>
</ContentPresenter>
</ScrollViewer>
</Window>
PageControl.xaml
<UserControl>
<Grid Background="{Binding RelativeSource={RelativeSource AncestorType=UserControl}, Path=Background}" />
</UserControl>

Related

Why is the TitleBar title gone after adding acrylic effect?

I used the following code in MainPage.xaml.cs to add acrylic effect on the TitleBar
public void AcrylicTitleBar()
{
CoreApplication.GetCurrentView().TitleBar.ExtendViewIntoTitleBar = true;
var title = ApplicationView.GetForCurrentView();
title.TitleBar.ButtonBackgroundColor = Colors.Transparent;
title.TitleBar.ButtonInactiveBackgroundColor = Colors.Transparent;
title.TitleBar.ButtonForegroundColor = (Color)Resources["SystemBaseHighColor"];
}
but I don't know why the TitleBar title is gone.
If I remove the above code the TitleBar title comes back. can someone please help me?
Your code is not applying any "Acrylic" effect to your app Title bar .
It is simply setting the button background color to "Transparent" which is no where close to the acrylic.
In order to have an acrylic title bar you need to do the following :
In your MainPage.xaml.cs , add code for hiding the default title bar and update its layout so that it still works like the default toolbar :
public MainPage()
{
// Hide default title bar.
var coreTitleBar = CoreApplication.GetCurrentView().TitleBar;
coreTitleBar.ExtendViewIntoTitleBar = true;
UpdateTitleBarLayout(coreTitleBar);
}
private void UpdateTitleBarLayout(CoreApplicationViewTitleBar coreTitleBar)
{
// Get the size of the caption controls area and back button
// (returned in logical pixels), and move your content around as necessary.
LeftPaddingColumn.Width = new GridLength(coreTitleBar.SystemOverlayLeftInset);
RightPaddingColumn.Width = new GridLength(coreTitleBar.SystemOverlayRightInset);
TitleBarButton.Margin = new Thickness(0, 0, coreTitleBar.SystemOverlayRightInset, 0);
// Update title bar control size as needed to account for system size changes.
AppTitleBar.Height = coreTitleBar.Height;
}
private void CoreTitleBar_LayoutMetricsChanged(CoreApplicationViewTitleBar sender, object args)
{
UpdateTitleBarLayout(sender);
}
Next , in your MainPage.xaml add AcrylicBrush as the main or parent grid background (Note : that this grid should wrap all the contents in the page ):
<Grid>
<Grid.Background>
<AcrylicBrush BackgroundSource="HostBackdrop"
TintColor="{ThemeResource SystemAltHighColor}"
FallbackColor="{ThemeResource SystemAltHighColor}"
TintOpacity="0.5">
</AcrylicBrush>
</Grid.Background>
</Grid>
Hope this helps!

Is there any way to pre-render a WebView control with Xamarin Forms?

I've created a set of custom html5 audio controls which I'm using in my iOS and Android Xamarin Forms app.
When I navigate to the page in my app where this content is located, the WebView renders as a white box for a fraction of a second before it renders the html content. Is there any way to render this view before it is visible to the user?
public partial class TutorialPage : ContentPage
{
private readonly ITutorialViewModel _dataModel;
public TutorialPage(ITutorialViewModel dataModel)
{
InitializeComponent();
Title = "Tutorial";
var browser = new WebView() { HeightRequest = 150, WidthRequest = 400, HorizontalOptions = LayoutOptions.CenterAndExpand };
browser.Source = DependencyService.Get<IBaseUrl>().Get();
mainStack.Children.Add(browser);
}
}
You can always use the events of the webview for that.
Take a look at them:
Navigated - Event that is raised after navigation completes.
Navigating - Event that is raised when navigation starts.
So what you have to do is set the webview's "IsVisible" property to false at the start, and when the Navigated event fires, set it back to true.
Example:
public partial class TutorialPage : ContentPage
{
private readonly ITutorialViewModel _dataModel;
private WebView browser;
public TutorialPage(ITutorialViewModel dataModel)
{
InitializeComponent();
Title = "Tutorial";
browser = new WebView() { HeightRequest = 150, WidthRequest = 400, HorizontalOptions = LayoutOptions.CenterAndExpand, IsVisible = false };
browser.Source = DependencyService.Get<IBaseUrl>().Get();
browser.Navigated += Browser_Navigated:
mainStack.Children.Add(browser);
}
void Browser_Navigated (object sender, WebNavigatedEventArgs e)
{
browser.IsVisible = true;
}
}
If you don't want to make the WebView as a global class variable, you can get it from the sender parameter of the Navigated event, as shown below:
void Browser_Navigated (object sender, WebNavigatedEventArgs e)
{
((WebView)sender).IsVisible = true;
}
I bypassed my problem by making the background transparent. The controls still pop in but it's better than having a white box on the page in the meantime.

Why do the labels on my custom user control cause the mouseleave event to fire?

I have written a custom WPF UserControl. It's a square with a Grid named Base. To that grid I add an ellipse and two labels (volume and location), which are populated with text pulled from the properties of an object which is given as a parameter upon control instantiation.
Here's the XAML:
<UserControl x:Class="EasyHyb.SampleWellControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
d:DesignHeight="100" d:DesignWidth="100">
<Grid x:Name="Base">
</Grid>
</UserControl>
And the constructor/event functions in the codebehind:
public SampleWellControl(int size, SourceSample sample)
{
InitializeComponent();
this.sample = sample;
this.Width = this.Height = size;
this.selected = SelectionStatus.Unselected;
double spacing = size / 4;
volume = new Label();
location = new Label();
volume.Content = String.Format("{0:0.00}", sample.volume);
location.Content = sample.well.well;
volume.HorizontalAlignment = location.HorizontalAlignment = System.Windows.HorizontalAlignment.Center;
volume.FontFamily = location.FontFamily = new System.Windows.Media.FontFamily("Meiryo UI");
volume.FontWeight = location.FontWeight = FontWeights.Bold;
volume.Background = location.Background = Base.Background = this.Background = Brushes.Transparent;
volume.Margin = new Thickness(0, spacing, 0, 0);
location.Margin = new Thickness(0, spacing * 2, 0, 0);
well = new Ellipse();
well.Width = well.Height = this.Width;
well.StrokeThickness = 3;
Base.Children.Add(well);
Base.Children.Add(volume);
Base.Children.Add(location);
this.MouseEnter += SampleWellControl_MouseEnter;
this.MouseLeave += SampleWellControl_MouseLeave;
this.MouseUp += SampleWellControl_MouseUp;
this.Cursor = Cursors.Hand;
UpdateFillAndStroke();
}
void SampleWellControl_MouseLeave(object sender, MouseEventArgs e)
{
RevertWell();
}
void SampleWellControl_MouseEnter(object sender, MouseEventArgs e)
{
HighlightWell();
}
public void HighlightWell()
{
if (this.selected == SelectionStatus.Pooled)
{
return;
}
if (this.selected == SelectionStatus.Unselected)
{
this.well.Stroke = this.strokes[SelectionStatus.Selected];
}
else
{
this.well.Stroke = this.strokes[SelectionStatus.Unselected];
}
}
public void RevertWell()
{
if (this.selected == SelectionStatus.Pooled)
{
return;
}
if (this.selected == SelectionStatus.Unselected)
{
this.well.Stroke = this.strokes[SelectionStatus.Unselected];
}
else
{
this.well.Stroke = this.strokes[SelectionStatus.Selected];
}
}
Basically, when the mouse enters the control, the stroke of the ellipse should change unless the well has undergone an operation to give it a "Pooled" status.
When the mouse enters the control, it responds exactly as I expect: the MouseEnter event handler fires. However, when a user moves the mouse over one of the labels inside the control, the MouseLeave event fires. So even though the label is ostensibly part of the control The pictures below show what I'm talking about. Print Screen removes the cursors, but I put blue dots to indicate where the cursor is:
Responding properly:
Now it seems to think the mouse has left the control:
I've tried adding MouseEnter and MouseLeave event handlers to the labels, but they don't fire. The cursor also changes from a hand to a pointer when the labels are moused over. I've tried adding MouseEnter and MouseLeave event handlers to the control after it's instantiated within another class. I added transparent backgrounds to the Grid, control, and labels, but that didn't make any difference either.
I also checked in my MouseLeave event handler to see if the mouse was over the control, and it seems that the control is not detecting the cursor as being over the control itself:
if(!this.IsMouseOver)
{
RevertWell();
}
//also tried IsMouseDirectlyOver
I would like MouseLeave to fire only when the cursor exits the square bounds of the control. How can I accomplish this while keeping the labels?
Use a combination of
IsHitTestVisible="False"
on all of your objects added to Base:
volume = new Label();
volume.IsHitTestVisible="False";
and then your container which has the events, give a background
<Grid x:Name="Base" Background="White">
(Also I wanted to comment but reputation is stupid)
Well shucks, after a lot of searching around it turns out the problem was indeed contained within another control. I had another UserControl class, EmptyWellControl, which had a Label. The text position within the label was calculated using the Label's Height property, which resulted in a nonsense value that made the label extend vertically well beyond the dimensions of the window. The label didn't have a background, but nevertheless interfered with the controls whose path it crossed. Since the empty and sample wells were all laid out on the same grid, every SampleWellControl was affected by these labels.

Load two windows on two separate screens

I am trying to create a WPF presentation application where the Primary Window on the Primary Screen will be used to update the content on the Presentation Window, which needs to be on the secondary screen. I am looking for samples or code snippet that can help me do that.
Also I have attempted the following and doesn't work:
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
var screens = System.Windows.Forms.Screen.AllScreens;
if (screens.Length > 1)
{
var window = new MainWindow
{
WindowStartupLocation = WindowStartupLocation.Manual,
WindowState = WindowState.Maximized,
Top = screens[0].WorkingArea.Top,
Left = screens[0].WorkingArea.Left,
Topmost = true
};
window.Show();
var pesentationWindow = new PresentationWindow
{
WindowStartupLocation = WindowStartupLocation.Manual,
ShowInTaskbar = false,
Top = screens[1].WorkingArea.Top,
Left = screens[1].WorkingArea.Left,
Topmost = true
};
pesentationWindow.Show();
}
}
You can use Screen object in this way
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
Window1 w1 = new Window1();
Window2 w2 = new Window2();
Screen s1 = Screen.AllScreens[0];
Screen s2 = Screen.AllScreens[1];
Rectangle r1 = s1.WorkingArea;
Rectangle r2 = s2.WorkingArea;
w1.Top = r1.Top;
w1.Left = r1.Left;
w2.Top = r2.Top;
w2.Left = r2.Left;
w1.Show();
w2.Show();
w2.Owner = w1;
}
Follow this link for more details.
Using the System.Windows.Forms.Screen.AllScreens static property you can figure out how many screens there are and what their resolutions are. Figure out which is the PrimaryScreen and set the secondary Window to the correct size and position. See for more info: Trouble creating a WPF window in a secondary monitor
Then simply create two separate Window classes that bind your DataContext's bindings differently. For Example:
class DataContext
{
public string Header { get; set; }
}
<Window Title="Primary Window">
<TextBox Text="{Binding Header}"/>
</Window>
<Window Title="Presentation Window">
<TextBlock Content="{Binding Header}"/>
</Window>

How can I programmatically scroll a WPF listview?

Is it possible to programmatically scroll a WPF listview? I know winforms doesn't do it, right?
I am talking about say scrolling 50 units up or down, etc. Not scrolling an entire item height at once.
Yes, you'll have to grab the ScrollViwer from the ListView, or but once you have access to that, you can use the methods exposed by it or override the scrolling. You can also scroll by getting the main content area and using it's implementation of the IScrollInfo interface.
Here's a little helper to get the ScrollViwer component of something like a ListBox, ListView, etc.
public static DependencyObject GetScrollViewer(DependencyObject o)
{
// Return the DependencyObject if it is a ScrollViewer
if (o is ScrollViewer)
{ return o; }
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(o); i++)
{
var child = VisualTreeHelper.GetChild(o, i);
var result = GetScrollViewer(child);
if (result == null)
{
continue;
}
else
{
return result;
}
}
return null;
}
And then you can just use .LineUp() and .LineDown() like this:
private void OnScrollUp(object sender, RoutedEventArgs e)
{
var scrollViwer = GetScrollViewer(uiListView) as ScrollViewer;
if (scrollViwer != null)
{
// Logical Scrolling by Item
// scrollViwer.LineUp();
// Physical Scrolling by Offset
scrollViwer.ScrollToVerticalOffset(scrollViwer.VerticalOffset + 3);
}
}
private void OnScrollDown(object sender, RoutedEventArgs e)
{
var scrollViwer = GetScrollViewer(uiListView) as ScrollViewer;
if (scrollViwer != null)
{
// Logical Scrolling by Item
// scrollViwer.LineDown();
// Physical Scrolling by Offset
scrollViwer.ScrollToVerticalOffset(scrollViwer.VerticalOffset + 3);
}
}
<DockPanel>
<Button DockPanel.Dock="Top"
Content="Scroll Up"
Click="OnScrollUp" />
<Button DockPanel.Dock="Bottom"
Content="Scroll Down"
Click="OnScrollDown" />
<ListView x:Name="uiListView">
<!-- Content -->
</ListView>
</DockPanel>
The Logical scrolling exposed by LineUp and LineDown do still scroll by item, if you want to scroll by a set amount you should use the ScrollToHorizontal/VerticalOffset that I've used above. If you want some more complex scrolling too, then take a look at the answer I've provided in this other question.
Have you tried ScrollIntoView?
Alternatively, if it's not a specific item you brought into view, but an offset from the current position, you can use BringIntoView.

Categories

Resources