Skip to content
This repository has been archived by the owner on Aug 29, 2023. It is now read-only.

Commit

Permalink
WIP Part 2 : Gone with Wind32 Titlebar
Browse files Browse the repository at this point in the history
- [x] Maximize button is only visible when not already maximized
- [x] Restore button is only visibile when Window is maximized
- [x] WindowRootGrid is now called WindowRoot
- [x] Added a WindowRoot view model
- [x] Added a Window State Enum for Minimized, Maximized and Other

Please see my prior check-in for the first push towards the effort
to create a lookless Window where the Win32 border and Titlebar
are never used, but all features they would provide still exist.  Other
than cleanup and my normal addiction to refactoring, most of the
work is complete for this feature.

With what I have now, there will be some confusion when it comes
to a UIElements child/children/Content etc.  For example, the
WindowRoot inherits from the UWP LayoutTransformControl that
actually inherits from Grid.  The LayoutTransformControl has a
member called Child.  When I inherit from LayoutTransformControl
I set the Child with another Grid that has two rows.  One row is
in fact the Titlebar and the other is the clients content (what we
normally think of as the Window.Content.  I place the client content
into the second row by providing a property called Content.  So,
when you look at the inheritence path, the LayoutTransformControl
has Grid.Children that it provides access to via its single
LayoutTransformControl.Child.  I inherit from this control and
provide the WindowRoot.Content property.   I am not sure which
is more confusing, my explanation of this or the implementation of
it.

The TitleBar control had four event handlers that were dead code
that I removed.

In Program.cs I updated the InitializeServices method to change
the scope of several injected services to be Transients because
that scope causes much less concerns about lifetime and even
though my example is simple, I didn't want it to be copied and
pasted into the wild as a bunch of singletons.

Until this check-in, the Titlebar was part of the MainPage (was
once called the SettingsPage).  However, I pushed that logic
up closer to the actual Window itself because I do not want
a page to have to concern itself with the Window's titlebar.

On a final random check-in note, I never like the fact that XAML
files need to provide a default parameter-less constructor to
please the Visual Studio designer.  When using nullable, it causes
a real pain because the existence of that constructor means that
you have to protect those properties by null checking them or
disable the related analyzer warnings.  Even though WinUI
doesn't work with a designer right now, I figured I would code
this as if it does.
  • Loading branch information
jtbrower committed Jul 22, 2020
1 parent 935ec21 commit 85fc4a4
Show file tree
Hide file tree
Showing 17 changed files with 434 additions and 123 deletions.
16 changes: 3 additions & 13 deletions src/Windows/CopyPasteTemp.txt
Original file line number Diff line number Diff line change
Expand Up @@ -45,16 +45,6 @@ namespace WinUI.Native


//Notes to self
- Figure out or log a potential issue where custom controls that inherit from other controls such as a grid, only work in XAML, not code behind. Code behind throws exceptions. See the WindowRootGrid control.

7/19/2020
On a side note, I hit the bug that is trying to hide from me where
the DPI changed event fires in infinite recursion causing visual
studio to freeze. I found a related cause. I can force the problem
by maximizing the window, then use my SizeToContent feature.
This will reduce the size of the Window, but the Window still
thinks it is maximized, now if you drag the window slowly
between two monitors that cause a DPI change it will lock up.
My guess is that I am not fully refreshing the window, or making
the wrong combo of Win32 calls that leave the Window in an
invalid state. This should be my next challenge.
- Figure out or log a potential issue where custom controls that inherit from other controls such as a grid, only work in XAML, not code behind. Code behind throws exceptions. See the WindowRoot control.
- Determine if I should open a discussion or issue regarding the infinite DPI change events firing. If I do I need to create a small project that can most easily reproduce the problem.
- Change the mouse cursor during a DragMove.
4 changes: 2 additions & 2 deletions src/Windows/WinUI.CustomControls/ExtWindow.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,15 @@
Size is set to stretch so we can use as much space as possible for dragging.
**Bug/Issue/Misunderstanding?**
Note that the WindowRootGrid custom control will not work in code behind for some odd reason. Only
Note that the WindowRoot custom control will not work in code behind for some odd reason. Only
in XAML. I am guessing this is a WinUI Preview Version 1 bug or a Jason Brower bug! However, i just
couldn't instantiate any custom class that inherits from a Panel/Grid. I have not tried other
custom controls to see if the problem extends beyond a Grid/Panel class. To test this out I subclassed
Grid then instantiated an instance and added it as the Window.Content. This led to exceptions, but
when I replaced the custom Grid class with a normal Grid, it worked fine.
-->
<Grid
x:Name="WindowGrid"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Background="Transparent"
Expand All @@ -40,6 +41,5 @@
<interactivity:Interaction.Behaviors>
<behave:DragMoveBehavior />
</interactivity:Interaction.Behaviors>
<local:WindowRootGrid x:Name="RootGrid" />
</Grid>
</Window>
162 changes: 154 additions & 8 deletions src/Windows/WinUI.CustomControls/ExtWindow.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ namespace WinUI.CustomControls
using System.Diagnostics;
using Windows.Graphics.Display;
using Microsoft.UI.Xaml.Controls;
using WinUI.Vm;
using PInvoke;
using Microsoft.Toolkit.Uwp.Helpers;

////////////////////////////////////////////////////////////////////////////////////////////////////
/// <content>
Expand Down Expand Up @@ -65,6 +68,24 @@ public sealed partial class ExtWindow : IExtWindow, IPlatform

private DisplayInformation? _displayInformation;

////////////////////////////////////////////////////////////////////////////////////////////////////
/// <summary> Occurs when Window State Changed. </summary>
////////////////////////////////////////////////////////////////////////////////////////////////////

public event EventHandler<EnumWindowState>? WindowStateChanged;

////////////////////////////////////////////////////////////////////////////////////////////////////
/// <summary>
/// Note that I am going to make a best attempt at trying to keep this in sync with the actual
/// Win32 windows Show State but I am cautious in that this could be a tough challenge to meet in
/// all edge cases. The primary purpose of state track is to help assure that we know whether to
/// show the Maximize or Restore button in the titlebar. So be cautious until this is hardened
/// through use.
/// </summary>
////////////////////////////////////////////////////////////////////////////////////////////////////

public EnumWindowState _currentWindowState = EnumWindowState.Other;

////////////////////////////////////////////////////////////////////////////////////////////////////
/// <summary> Gets the handle. </summary>
///
Expand All @@ -76,16 +97,136 @@ public sealed partial class ExtWindow : IExtWindow, IPlatform
public IntPtr Handle { get; }

////////////////////////////////////////////////////////////////////////////////////////////////////
/// <summary> Initializes a new instance of the WinUI.DemoApp.ExtWindow class. </summary>
/// <summary>
/// Should not be called by production code, this exists only to please the designer.
/// </summary>
////////////////////////////////////////////////////////////////////////////////////////////////////

public ExtWindow()
{
InitializeComponent();
}

////////////////////////////////////////////////////////////////////////////////////////////////////
/// <summary>
/// Initializes a new instance of the WinUI.CustomControls.ExtWindow class.
/// </summary>
///
/// <param name="windowRoot"> The window root. </param>
////////////////////////////////////////////////////////////////////////////////////////////////////

public ExtWindow(WindowRoot windowRoot) : this()
{
InitializeComponent();

if (windowRoot.Vm != null)
{
var tb = windowRoot.Vm.TitleBarVm;
tb.CloseWindowCmd = new DelegateCmd(Close);
tb.MinimizeWindowCmd = new DelegateCmd(Minimize);
tb.MaximizeWindowCmd = new DelegateCmd(Maximize);
tb.RestoreWindowCmd = new DelegateCmd(Restore);
}

RootGrid = windowRoot;
WindowGrid.Children.Add(windowRoot);

Handle = this.GetHandle();
RootGrid.Loaded += RootGrid_Loaded;
RootGrid.Unloaded += RootGrid_Unloaded;
RemoveBorder();
SizeChanged += ExtWindow_SizeChanged;
}

////////////////////////////////////////////////////////////////////////////////////////////////////
/// <summary> Gets the root grid. </summary>
///
/// <value> The root grid. </value>
////////////////////////////////////////////////////////////////////////////////////////////////////

public WindowRoot RootGrid { get; }

////////////////////////////////////////////////////////////////////////////////////////////////////
/// <summary>
/// The faux title bar can be used in place of the Win32 titlebar. This function will change the
/// visibility of that titlebar, not the Win32 titlebar.
/// </summary>
///
/// <param name="isVisible"> True if is visible, false if not. </param>
///
/// <seealso cref="IExtWindow.ChangeFauxTitlebarVisibility(bool)"/>
////////////////////////////////////////////////////////////////////////////////////////////////////

public void ChangeFauxTitlebarVisibility(bool isVisible)
{
if (RootGrid.Vm == null) return;
if (RootGrid.Vm.TitleBarVm.IsVisible == isVisible) return;

RootGrid.Vm.TitleBarVm.IsVisible = isVisible;

base.Content?.UpdateLayout();

DispatcherQueue.ExecuteOnUIThreadAsync(() => SizeToContent());
}

////////////////////////////////////////////////////////////////////////////////////////////////////
/// <summary> Event handler. Called by ExtWindow for size changed events. </summary>
///
/// <param name="sender"> Source of the event. </param>
/// <param name="args"> Window size changed event information. </param>
////////////////////////////////////////////////////////////////////////////////////////////////////

private void ExtWindow_SizeChanged(object sender, WindowSizeChangedEventArgs args)
{
var windowState = GetWindowState();

if (RootGrid.Vm != null)
RootGrid.Vm.TitleBarVm.IsMaximized = windowState == EnumWindowState.Maximized;

if (WindowStateChanged == null) return;
//Note that I am checking again for null on the event handler to assure
// nothing un-subscribed since I requested the state. Unlikely but thats
// how threading issues are born.
if (_currentWindowState != windowState) WindowStateChanged?.Invoke(this, windowState);
}

////////////////////////////////////////////////////////////////////////////////////////////////////
/// <summary>
/// An IntPtr extension method that gets window state representing minimized, maximized or other.
/// </summary>
///
/// <param name="windowHandle"> Handle of the window. </param>
///
/// <returns> The window state. </returns>
////////////////////////////////////////////////////////////////////////////////////////////////////

private EnumWindowState GetWindowState()
{
//This command will throw a Win32 exception if something goes wrong. Prevent the exception and just return
// false, but assure its written to the Debug pipe. I was going to put this entire method in the
// NativeMethods library, but I wanted the EnumWindowStyle to be available in the View Models library
// without coupling the view models to NativeMethods. I also didn't want to couple native methods
// to the view model library. That said, I could either duplicate the type and cause more confusion
// or put the method here.
try
{
var windowPlacement = User32.GetWindowPlacement(Handle);
var showStyle = windowPlacement.showCmd;

if (showStyle == User32.WindowShowStyle.SW_MINIMIZE ||
showStyle == User32.WindowShowStyle.SW_FORCEMINIMIZE ||
showStyle == User32.WindowShowStyle.SW_SHOWMINIMIZED ||
showStyle == User32.WindowShowStyle.SW_SHOWMINNOACTIVE) return EnumWindowState.Minimized;


if (showStyle == User32.WindowShowStyle.SW_MAXIMIZE ||
showStyle == User32.WindowShowStyle.SW_SHOWMAXIMIZED) return EnumWindowState.Maximized;
}
catch (Exception e)
{
Debug.WriteLine(e);
}
return EnumWindowState.Other;
}

////////////////////////////////////////////////////////////////////////////////////////////////////
Expand Down Expand Up @@ -129,7 +270,7 @@ private void RootGrid_Loaded(object sender, RoutedEventArgs e)
Debug.WriteLine($"{nameof(GetDpiScale)} called before {nameof(RootGrid)}.XamlRoot was available.");
return null;
}
return (RootGrid.XamlRoot.RasterizationScale * 96.0) / PInvoke.User32.GetDpiForWindow(Handle);
return (RootGrid.XamlRoot.RasterizationScale * 96.0) / User32.GetDpiForWindow(Handle);
}

////////////////////////////////////////////////////////////////////////////////////////////////////
Expand All @@ -144,6 +285,9 @@ private void ScaleContentForDpi()
ScaleWindowContent(scale.Value);
}

//It's annoying when this warning is applied to event handler parameters.
#pragma warning disable IDE0060 // Remove unused parameter

////////////////////////////////////////////////////////////////////////////////////////////////////
/// <summary> Displays an information DPI changed. </summary>
///
Expand Down Expand Up @@ -171,7 +315,7 @@ private void DisplayInfo_DpiChanged(DisplayInformation sender, object args)
/// <param name="e"> Double tapped routed event information. </param>
////////////////////////////////////////////////////////////////////////////////////////////////////

private void RootGrid_DoubleTapped(object sender, Microsoft.UI.Xaml.Input.DoubleTappedRoutedEventArgs e)
private void RootGrid_DoubleTapped(object sender, Microsoft.UI.Xaml.Input.DoubleTappedRoutedEventArgs args)
{
if (Handle.IsMaximized())
{
Expand All @@ -183,6 +327,8 @@ private void RootGrid_DoubleTapped(object sender, Microsoft.UI.Xaml.Input.Double
Handle.MaximizeWindow();
}
}
//Ignoring this because they are applying it to event handlers too!
#pragma warning restore IDE0060 // Remove unused parameter

////////////////////////////////////////////////////////////////////////////////////////////////////
/// <summary> Gets the XAML root. </summary>
Expand All @@ -197,11 +343,11 @@ private void RootGrid_DoubleTapped(object sender, Microsoft.UI.Xaml.Input.Double
////////////////////////////////////////////////////////////////////////////////////////////////////
/// <summary>
/// The Window.Content was redeclared as a new property so we could change the setter to prevent
/// an unsuspecting victim from blowing over the top of the WindowRootGrid that this class
/// an unsuspecting victim from blowing over the top of the WindowRoot that this class
/// depends on. I will admit, it is slightly wonky because the XAML for the ExtWindow is setting
/// the content to our WindowRootGrid type. BTW, I actually didn't want or need the XAML for this
/// the content to our WindowRoot type. BTW, I actually didn't want or need the XAML for this
/// class but I stumbled upon an odd bug that I need to report. I was unable to use the custom
/// WindowRootGrid type in this code behind, it through access violations in native code, but if
/// WindowRoot type in this code behind, it through access violations in native code, but if
/// you use the same custom type in XAML it works fine. I have not encountered anything like that
/// before.
///
Expand All @@ -220,7 +366,7 @@ private void RootGrid_DoubleTapped(object sender, Microsoft.UI.Xaml.Input.Double
// event handlers for drag move can be attached to that. If the end user blows away the
// ExtWindow content it will affect the drag move handlers. So replacing the Window.Content
// with this implementation lets me achieve those goals.
get => RootGrid.Child;
get => RootGrid.Content;
set => SetContent(value);
}

Expand Down Expand Up @@ -248,7 +394,7 @@ private void SetContent(FrameworkElement? clientsContent)
RootGrid.HorizontalContentAlignment = horizontalContentAlignment;
RootGrid.VerticalContentAlignment = verticalContentAlignment;
}
RootGrid.Child = clientsContent;
RootGrid.Content = clientsContent;
}
////////////////////////////////////////////////////////////////////////////////////////////////////
/// <summary>
Expand Down
16 changes: 2 additions & 14 deletions src/Windows/WinUI.CustomControls/MainPage.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -48,26 +48,14 @@
</TransformGroup>
</LinearGradientBrush.Transform>
</LinearGradientBrush>
<FontIcon
x:Key="RestoreIcon"
Margin="5"
FontFamily="{StaticResource SymbolThemeFontFamily}"
Glyph="&#xE923;" />
<Style x:Key="KeyboardBorderStyle" TargetType="Border">
<Setter Property="BorderThickness" Value="1" />
<Setter Property="CornerRadius" Value="10" />
<Setter Property="BorderBrush" Value="DarkGray" />
<Setter Property="Background" Value="{StaticResource KeyboardBackgroundBrush}" />
</Style>
<Color x:Key="TitleBarColor">#0078D4</Color>
</Page.Resources>
<controls:DockPanel>
<local:TitleBar
x:Name="TitleBarInstance"
controls:DockPanel.Dock="Top"
Background="{StaticResource TitleBarColor}"
Visibility="{x:Bind Path=Vm.TitleBarVm.IsVisible, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}"
Vm="{x:Bind Path=Vm.TitleBarVm, Mode=OneWay}" />
<!--
Note that I use an attached property to add a drop shadow to the Border inside of this grid. It is a behavior that
requires the parent of what it is shadowing to be some sort of Panel type. This is needed so that it can insert
Expand Down Expand Up @@ -96,12 +84,12 @@
Grid.Column="0"
Margin="5"
VerticalAlignment="Center">
<Button Click="RemoveWindowBorder_Click" Content="Remove Window Border">
<Button Click="RemoveFauxTitleBar_Click" Content="Remove Window Border">
<Interactivity:Interaction.Behaviors>
<behave:SwallowButtonDoubleTapBehavior />
</Interactivity:Interaction.Behaviors>
</Button>
<Button Click="AddWindowBorder_Click" Content="Add Window Border">
<Button Click="AddFauxTitleBar_Click" Content="Add Window Border">
<Interactivity:Interaction.Behaviors>
<behave:SwallowButtonDoubleTapBehavior />
</Interactivity:Interaction.Behaviors>
Expand Down
19 changes: 6 additions & 13 deletions src/Windows/WinUI.CustomControls/MainPage.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -92,34 +92,27 @@ public MainPage(MainPageVm vm, IExtWindow window)
#pragma warning disable IDE0060 // Remove unused parameter

////////////////////////////////////////////////////////////////////////////////////////////////////
/// <summary> Event handler. Called by RemoveWindowBorder for click events. </summary>
/// <summary> Event handler. Called by RemoveFauxTitleBar for click events. </summary>
///
/// <param name="sender"> Source of the event. </param>
/// <param name="e"> Routed event information. </param>
////////////////////////////////////////////////////////////////////////////////////////////////////

public void RemoveWindowBorder_Click(object sender, RoutedEventArgs e)
public void RemoveFauxTitleBar_Click(object sender, RoutedEventArgs e)
{
Vm.TitleBarVm.IsVisible = false;
UpdateLayout();
DispatcherQueue.ExecuteOnUIThreadAsync(() => _mainWindow.SizeToContent());
_mainWindow.ChangeFauxTitlebarVisibility(false);
}

////////////////////////////////////////////////////////////////////////////////////////////////////
/// <summary> Event handler. Called by AddWindowBorder for click events. </summary>
///
/// <exception cref="InvalidOperationException"> Thrown when the requested operation is
/// invalid. </exception>
/// <summary> Event handler. Called by AddFauxTitleBar for click events. </summary>
///
/// <param name="sender"> Source of the event. </param>
/// <param name="e"> Routed event information. </param>
////////////////////////////////////////////////////////////////////////////////////////////////////

public void AddWindowBorder_Click(object sender, RoutedEventArgs e)
public void AddFauxTitleBar_Click(object sender, RoutedEventArgs e)
{
Vm.TitleBarVm.IsVisible = true;
UpdateLayout();
DispatcherQueue.ExecuteOnUIThreadAsync(() => _mainWindow.SizeToContent());
_mainWindow.ChangeFauxTitlebarVisibility(true);
}

////////////////////////////////////////////////////////////////////////////////////////////////////
Expand Down
Loading

0 comments on commit 85fc4a4

Please sign in to comment.