Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Clickable Interactive control sample for custom titlebar #1360

Merged
merged 7 commits into from
Oct 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions WinUIGallery/ContentIncludes.props
Original file line number Diff line number Diff line change
Expand Up @@ -372,6 +372,7 @@
<Content Include="ControlPagesSampleCode\Window\CreateWindowSample1.txt" />
<Content Include="ControlPagesSampleCode\Window\TitleBar\TitleBarSample1.txt" />
<Content Include="ControlPagesSampleCode\Window\TitleBar\TitleBarSample2.txt" />
<Content Include="ControlPagesSampleCode\Window\TitleBar\TitleBarSample3.txt" />
<Content Include="Common\ReadMe.txt" />
<Content Include="DataModel\ControlInfoData.json" />
</ItemGroup>
Expand Down
53 changes: 43 additions & 10 deletions WinUIGallery/ControlPages/TitleBarPage.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,35 @@
</Page.Resources>

<StackPanel>
<local:ControlExample HeaderText="Default titlebar (when no user defined titlebar is set)"
CSharpSource="Window\TitleBar\TitleBarSample2.txt">
<local:ControlExample.Example>
<StackPanel Orientation="Vertical" Spacing="10">
<TextBlock TextWrapping="WrapWholeWords">
WinUI provides a default titlebar in such cases where the user doesn't explicitly provide a uielement, for setting the titlebar. The system titlebar (Windows-provided titlebar) disappears and client area content is extended to non client area.
In this default case, entire non client region (titlebar region) get system titlebar behaviors like drag regions, system menu on context click etc.
<LineBreak></LineBreak>
This is the recommended way of using TitleBar apis and covers most common scenarios.
<LineBreak></LineBreak>
It can be applied by just calling ExtendsContentIntoTitleBar api. This internally calls SetTitleBar api with null argument and provides the default case.
<LineBreak></LineBreak>
Use the button below to toggle between system titlebar and default custom titlebar.
</TextBlock>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Stretch" VerticalAlignment="Top" Spacing="20">
<Button x:Name="defaultTitleBar" Click="defaultTitleBar_Click"></Button>
</StackPanel>
</StackPanel>
</local:ControlExample.Example>
</local:ControlExample>
<local:ControlExample HeaderText="User defined UIElement as custom titlebar for the window"
CSharpSource="Window/TitleBar/TitleBarSample1.txt">
<local:ControlExample.Example>
<StackPanel Orientation="Vertical" Spacing="10">
<TextBlock TextWrapping="WrapWholeWords">
User can set a top-level UIElement (defined as appTitleBar here) as titlebar for the window. The system titlebar disappears and the chosen uielement starts acting like the titlebar. <LineBreak></LineBreak>
For finer controls, a user can set a top-level UIElement (defined as appTitleBar here) as titlebar for the window. The system titlebar disappears and the chosen uielement starts acting like the titlebar (gets all system titlebar behavior). <LineBreak></LineBreak>
The Background and Foreground Color dropdowns set the foreground and background of titlebar and caption buttons respectively.
<LineBreak></LineBreak>
Use the button below to toggle between system titlebar and custom WinUI titlebar.
</TextBlock>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Stretch" VerticalAlignment="Top" Spacing="10">
<Button x:Name="customTitleBar" Click="customTitleBar_Click"></Button>
Expand Down Expand Up @@ -64,7 +86,8 @@
<Rectangle Fill="Green" AutomationProperties.Name="Green"/>
<Rectangle Fill="Blue" AutomationProperties.Name="Blue"/>
<Rectangle Fill="White" AutomationProperties.Name="White"/>
<Rectangle Fill="Black" AutomationProperties.Name="Black"/> </GridView.Items>
<Rectangle Fill="Black" AutomationProperties.Name="Black"/>
</GridView.Items>
</GridView>

</Flyout>
Expand Down Expand Up @@ -111,23 +134,33 @@
</local:ControlExample.Example>

</local:ControlExample>
<local:ControlExample HeaderText="Fallback titlebar when no user defined titlebar is set"
CSharpSource="Window/TitleBar/TitleBarSample2.txt">
<local:ControlExample HeaderText="Titlebar Customizations : Interactive controls in Titlebar (non client) area"
CSharpSource="Window\TitleBar\TitleBarSample3.txt">
<local:ControlExample.Example>
<StackPanel Orientation="Vertical" Spacing="10">
<TextBlock TextWrapping="WrapWholeWords">
WinUI provides a fallback titlebar in case where user doesn't want to provide a uielement for setting the titlebar.
A small horizontal section next to min/max/close caption buttons is chosen as the fallback titlebar.
<LineBreak></LineBreak>
It can be applied by just calling ExtendsContentIntoTitleBar api only and not calling SetTitleBar afterwards. It can also be manually triggered by calling SetTitleBar api with null arument.
WinUI custom titlebar now hosting interactive clickable controls within non client region of the window, when using custom titlebar.
<LineBreak></LineBreak>
Use the Color dropdown controls in the section above to change color of the fallback titlebar.
This is achieved by using lower level
<Hyperlink NavigateUri="https://learn.microsoft.com/windows/windows-app-sdk/api/winrt/microsoft.ui.windowing.appwindowtitlebar">
Microsoft.UI.AppWindowTitlebar
</Hyperlink>
and
<Italic>Microsoft.UI.NonClientInputPointerSource apis</Italic>
<LineBreak></LineBreak>
<LineBreak></LineBreak>
WinUI allows <Bold> mix and match </Bold> of higher level WinUI custom titlebar apis with lower level AppWindow and NonClientInputPointerSource apis for most cases.
One exception is one should not use <Italic> Window.SetTitlebar </Italic> api along with any lower level api which also sets drag regions as it can result in unexpected behavior.
If needed, set <Italic> Window.SetTitlebar </Italic> to null (default case) and proceed to use lower level apis for drag functionality.
<LineBreak></LineBreak>
Use the button below to toggle between system titlebar and default custom titlebar.
</TextBlock>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Stretch" VerticalAlignment="Top" Spacing="20">
<Button x:Name="defaultTitleBar" Click="defaultTitleBar_Click"></Button>
<Button x:Name="addInteractiveElements" Click="AddInteractiveElements_Click">Add interactive control to titlebar</Button>
</StackPanel>
</StackPanel>
</local:ControlExample.Example>
</local:ControlExample>

</StackPanel>
</Page>
94 changes: 87 additions & 7 deletions WinUIGallery/ControlPages/TitleBarPage.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@
using WinUIGallery.DesktopWap.Helper;
using Microsoft.UI.Xaml.Shapes;
using System.Threading.Tasks;
using Microsoft.UI.Windowing;
using Microsoft.UI.Xaml.Navigation;
using Microsoft.UI.Input;
using System.IO;
using Windows.Foundation;
using System;

// To learn more about WinUI, the WinUI project structure,
// and more about our project templates, see: http://aka.ms/winui-project-info.
Expand All @@ -29,7 +35,8 @@ namespace AppUIBasics.ControlPages
public sealed partial class TitleBarPage : Page
{
private Windows.UI.Color currentBgColor = Colors.Transparent;
private Windows.UI.Color currentFgColor = Colors.Black;
private Windows.UI.Color currentFgColor = ThemeHelper.ActualTheme == ElementTheme.Dark ? Colors.White : Colors.Black;
private bool sizeChangedEventHandlerAdded = false;

public TitleBarPage()
{
Expand All @@ -41,11 +48,17 @@ public TitleBarPage()
};
}

protected override void OnNavigatedFrom(NavigationEventArgs e)
{
ResetTitlebarSettings();
}



private void SetTitleBar(UIElement titlebar)
private void SetTitleBar(UIElement titlebar, bool forceCustomTitlebar = false)
{
var window = WindowHelper.GetWindowForElement(this as UIElement);
if (!window.ExtendsContentIntoTitleBar)
if (forceCustomTitlebar || !window.ExtendsContentIntoTitleBar)
{
window.ExtendsContentIntoTitleBar = true;
window.SetTitleBar(titlebar);
Expand All @@ -60,18 +73,43 @@ private void SetTitleBar(UIElement titlebar)
UpdateTitleBarColor();
}

private void ResetTitlebarSettings()
{
var window = WindowHelper.GetWindowForElement(this as UIElement);
UIElement titleBarElement = UIHelper.FindElementByName(this as UIElement, "AppTitleBar");
SetTitleBar(titleBarElement, forceCustomTitlebar: true);
ClearClickThruRegions();
var txtBoxNonClientArea = UIHelper.FindElementByName(this as UIElement, "AppTitleBarTextBox") as FrameworkElement;
txtBoxNonClientArea.Visibility = Visibility.Collapsed;
addInteractiveElements.Content = "Add interactive control to titlebar";
}

private void SetClickThruRegions(Windows.Graphics.RectInt32[] rects)
{
var window = WindowHelper.GetWindowForElement(this as UIElement);
var nonClientInputSrc = InputNonClientPointerSource.GetForWindowId(window.AppWindow.Id);
nonClientInputSrc.SetRegionRects(NonClientRegionKind.Passthrough, rects);
}

private void ClearClickThruRegions()
{
var window = WindowHelper.GetWindowForElement(this as UIElement);
var noninputsrc = InputNonClientPointerSource.GetForWindowId(window.AppWindow.Id);
noninputsrc.ClearRegionRects(NonClientRegionKind.Passthrough);
}

public void UpdateButtonText()
{
var window = WindowHelper.GetWindowForElement(this as UIElement);
if (window.ExtendsContentIntoTitleBar)
{
customTitleBar.Content = "Reset to system TitleBar";
defaultTitleBar.Content = "Reset to system TitleBar";
customTitleBar.Content = "Reset to System TitleBar";
defaultTitleBar.Content = "Reset to System TitleBar";
}
else
{
customTitleBar.Content = "Set Custom TitleBar";
defaultTitleBar.Content = "Set Fallback Custom TitleBar";
defaultTitleBar.Content = "Set Default Custom TitleBar";
}

}
Expand Down Expand Up @@ -117,7 +155,6 @@ private void customTitleBar_Click(object sender, RoutedEventArgs e)
{
UIElement titleBarElement = UIHelper.FindElementByName(sender as UIElement, "AppTitleBar");
SetTitleBar(titleBarElement);

// announce visual change to automation
UIHelper.AnnounceActionForAccessibility(sender as UIElement, "TitleBar size and width changed", "TitleBarChangedNotificationActivityId");
}
Expand All @@ -128,5 +165,48 @@ private void defaultTitleBar_Click(object sender, RoutedEventArgs e)
// announce visual change to automation
UIHelper.AnnounceActionForAccessibility(sender as UIElement, "TitleBar size and width changed", "TitleBarChangedNotificationActivityId");
}

private void AddInteractiveElements_Click(object sender, RoutedEventArgs e)
{
var txtBoxNonClientArea = UIHelper.FindElementByName(sender as UIElement, "AppTitleBarTextBox") as FrameworkElement;

if (txtBoxNonClientArea.Visibility == Visibility.Visible)
{
ResetTitlebarSettings();
}
else
{
addInteractiveElements.Content = "Remove interactive control from titlebar";
txtBoxNonClientArea.Visibility = Visibility.Visible;
if (!sizeChangedEventHandlerAdded)
{
sizeChangedEventHandlerAdded = true;
// run this code when textbox has been made visible and its actual width and height has been calculated
txtBoxNonClientArea.SizeChanged += (object sender, SizeChangedEventArgs e) =>
{
if (txtBoxNonClientArea.Visibility != Visibility.Collapsed)
{
GeneralTransform transformTxtBox = txtBoxNonClientArea.TransformToVisual(null);
Rect bounds = transformTxtBox.TransformBounds(new Rect(0, 0, txtBoxNonClientArea.ActualWidth, txtBoxNonClientArea.ActualHeight));

var scale = WindowHelper.GetRasterizationScaleForElement(this);

var transparentRect = new Windows.Graphics.RectInt32(
_X: (int)Math.Round(bounds.X * scale),
_Y: (int)Math.Round(bounds.Y * scale),
_Width: (int)Math.Round(bounds.Width * scale),
_Height: (int)Math.Round(bounds.Height * scale)
);
var rectArr = new Windows.Graphics.RectInt32[] { transparentRect };
SetClickThruRegions(rectArr);
}
};
pratikone marked this conversation as resolved.
Show resolved Hide resolved
}
txtBoxNonClientArea.Width += 1; //to trigger size changed event
}
// announce visual change to automation
UIHelper.AnnounceActionForAccessibility(sender as UIElement, "TitleBar size and width changed", "TitleBarChangedNotificationActivityId");
}

}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// no UIElement is set for titlebar, fallback titlebar is created
// no UIElement is set for titlebar, default titlebar is created which extends to entire non client area
Window window = App.MainWindow;
window.ExtendsContentIntoTitleBar = true;
window.SetTitleBar(null); // this line is optional as by it is null by default
// window.SetTitleBar(null); // optional line as not setting any UIElement as titlebar is same as setting null as titlebar
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
Window window = App.MainWindow;
window.ExtendsContentIntoTitleBar = true;
window.SetTitleBar(AppTitleBar);
var nonClientInputSrc = InputNonClientPointerSource.GetForWindowId(window.AppWindow.Id);

// textbox on titlebar area
var txtBoxNonClientArea = UIHelper.FindElementByName(sender as UIElement, "AppTitleBarTextBox") as FrameworkElement;
GeneralTransform transformTxtBox = txtBoxNonClientArea.TransformToVisual(null);
Rect bounds = transformTxtBox.TransformBounds(new Rect(0, 0, txtBoxNonClientArea.ActualWidth, txtBoxNonClientArea.ActualHeight));

// Windows.Graphics.RectInt32[] rects defines the area which allows click throughs in custom titlebar
// it is non dpi-aware client coordinates. Hence, we convert dpi aware coordinates to non-dpi coordinates
var scale = WindowHelper.GetRasterizationScaleForElement(this);
var transparentRect = new Windows.Graphics.RectInt32(
_X: (int)Math.Round(bounds.X * scale),
_Y: (int)Math.Round(bounds.Y * scale),
_Width: (int)Math.Round(bounds.Width * scale),
_Height: (int)Math.Round(bounds.Height * scale)
);
var rects = new Windows.Graphics.RectInt32[] { transparentRect };

nonClientInputSrc.SetRegionRects(NonClientRegionKind.Passthrough, rects); // areas defined will be click through and can host button and textboxes
2 changes: 1 addition & 1 deletion WinUIGallery/DataModel/ControlInfoData.json
Original file line number Diff line number Diff line change
Expand Up @@ -2828,7 +2828,7 @@
"Subtitle": "An example showing a custom UIElement used as the titlebar for the app's window.",
"ImagePath": "ms-appx:///Assets/ControlImages/TitleBar.png",
"ImageIconPath": "ms-appx:///Assets/ControlIcons/DefaultIcon.png",
"Description": "This sample shows how to use a custom UIElement as titlebar for app's window.",
"Description": "This sample shows how to use a custom titlebar for the app's window. There are 2 ways of doing it: using default titlebar and setting an UIElement as a custom titlebar.",
"Content": "<p>Look at the <i>TitleBarPage.xaml</i> file in Visual Studio to see the full code for this page.</p>",
"IsUpdated": true,
"Docs": [
Expand Down
24 changes: 4 additions & 20 deletions WinUIGallery/Helper/TitleBarHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,27 +19,11 @@

namespace WinUIGallery.DesktopWap.Helper
{

internal class TitleBarHelper
{

private static void triggerTitleBarRepaint(Window window)
{
// to trigger repaint tracking task id 38044406
var hwnd = WinRT.Interop.WindowNative.GetWindowHandle(window);
var activeWindow = AppUIBasics.Win32.GetActiveWindow();
if (hwnd == activeWindow)
{
AppUIBasics.Win32.SendMessage(hwnd, AppUIBasics.Win32.WM_ACTIVATE, AppUIBasics.Win32.WA_INACTIVE, IntPtr.Zero);
AppUIBasics.Win32.SendMessage(hwnd, AppUIBasics.Win32.WM_ACTIVATE, AppUIBasics.Win32.WA_ACTIVE, IntPtr.Zero);
}
else
{
AppUIBasics.Win32.SendMessage(hwnd, AppUIBasics.Win32.WM_ACTIVATE, AppUIBasics.Win32.WA_ACTIVE, IntPtr.Zero);
AppUIBasics.Win32.SendMessage(hwnd, AppUIBasics.Win32.WM_ACTIVATE, AppUIBasics.Win32.WA_INACTIVE, IntPtr.Zero);
}

}

// workaround as Appwindow titlebar doesn't update caption button colors correctly when changed while app is running
// https://task.ms/44172495
public static Windows.UI.Color ApplySystemThemeToCaptionButtons(Window window)
{
var res = Application.Current.Resources;
Expand All @@ -61,7 +45,7 @@ public static void SetCaptionButtonColors(Window window, Windows.UI.Color color)
{
var res = Application.Current.Resources;
res["WindowCaptionForeground"] = color;
triggerTitleBarRepaint(window);
window.AppWindow.TitleBar.ButtonForegroundColor = color;
}
}
}
15 changes: 15 additions & 0 deletions WinUIGallery/Helper/WindowHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,21 @@ static public Window GetWindowForElement(UIElement element)
}
return null;
}
// get dpi for an element
static public double GetRasterizationScaleForElement(UIElement element)
{
if (element.XamlRoot != null)
{
foreach (Window window in _activeWindows)
{
if (element.XamlRoot == window.Content.XamlRoot)
{
return element.XamlRoot.RasterizationScale;
}
}
}
return 0.0;
}

static public List<Window> ActiveWindows { get { return _activeWindows; }}

Expand Down
1 change: 1 addition & 0 deletions WinUIGallery/Navigation/NavigationRootPage.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@
VerticalAlignment="Center"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{x:Bind AppTitleText}" />
<TextBox x:Name="AppTitleBarTextBox" MinWidth="300" Height="40" Margin="16,0,0,0" Visibility="Collapsed" PlaceholderText="Enter any text" />
</StackPanel>
</Border>

Expand Down