Skip to content

Commit 8218cb4

Browse files
pratikonekarkarl
authored andcommitted
Clickable Interactive control sample for custom titlebar (#1360)
With 1.4 changes, WinUI 3 custom titlebar now uses appwindow titlebar + nonclientinputpointersource apis under the hood. This opens up new possibilities like allowing clickable interactive controls like textbox, button in the titlebar area, surrounded by draggable region on both left and the right sides. This code adds a sample to titlebar page which shows users how to create interactive controls in winui 3 titlebar. It also demonstrates the power of mixing and matching high level winui 3 custom titlebar apis and low level nonclient apis.
1 parent db9ff21 commit 8218cb4

File tree

9 files changed

+176
-40
lines changed

9 files changed

+176
-40
lines changed

WinUIGallery/ContentIncludes.props

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -372,6 +372,7 @@
372372
<Content Include="ControlPagesSampleCode\Window\CreateWindowSample1.txt" />
373373
<Content Include="ControlPagesSampleCode\Window\TitleBar\TitleBarSample1.txt" />
374374
<Content Include="ControlPagesSampleCode\Window\TitleBar\TitleBarSample2.txt" />
375+
<Content Include="ControlPagesSampleCode\Window\TitleBar\TitleBarSample3.txt" />
375376
<Content Include="Common\ReadMe.txt" />
376377
<Content Include="DataModel\ControlInfoData.json" />
377378
</ItemGroup>

WinUIGallery/ControlPages/TitleBarPage.xaml

Lines changed: 43 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,35 @@
2626
</Page.Resources>
2727

2828
<StackPanel>
29+
<local:ControlExample HeaderText="Default titlebar (when no user defined titlebar is set)"
30+
CSharpSource="Window\TitleBar\TitleBarSample2.txt">
31+
<local:ControlExample.Example>
32+
<StackPanel Orientation="Vertical" Spacing="10">
33+
<TextBlock TextWrapping="WrapWholeWords">
34+
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.
35+
In this default case, entire non client region (titlebar region) get system titlebar behaviors like drag regions, system menu on context click etc.
36+
<LineBreak></LineBreak>
37+
This is the recommended way of using TitleBar apis and covers most common scenarios.
38+
<LineBreak></LineBreak>
39+
It can be applied by just calling ExtendsContentIntoTitleBar api. This internally calls SetTitleBar api with null argument and provides the default case.
40+
<LineBreak></LineBreak>
41+
Use the button below to toggle between system titlebar and default custom titlebar.
42+
</TextBlock>
43+
<StackPanel Orientation="Horizontal" HorizontalAlignment="Stretch" VerticalAlignment="Top" Spacing="20">
44+
<Button x:Name="defaultTitleBar" Click="defaultTitleBar_Click"></Button>
45+
</StackPanel>
46+
</StackPanel>
47+
</local:ControlExample.Example>
48+
</local:ControlExample>
2949
<local:ControlExample HeaderText="User defined UIElement as custom titlebar for the window"
3050
CSharpSource="Window/TitleBar/TitleBarSample1.txt">
3151
<local:ControlExample.Example>
3252
<StackPanel Orientation="Vertical" Spacing="10">
3353
<TextBlock TextWrapping="WrapWholeWords">
34-
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>
54+
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>
3555
The Background and Foreground Color dropdowns set the foreground and background of titlebar and caption buttons respectively.
56+
<LineBreak></LineBreak>
57+
Use the button below to toggle between system titlebar and custom WinUI titlebar.
3658
</TextBlock>
3759
<StackPanel Orientation="Horizontal" HorizontalAlignment="Stretch" VerticalAlignment="Top" Spacing="10">
3860
<Button x:Name="customTitleBar" Click="customTitleBar_Click"></Button>
@@ -64,7 +86,8 @@
6486
<Rectangle Fill="Green" AutomationProperties.Name="Green"/>
6587
<Rectangle Fill="Blue" AutomationProperties.Name="Blue"/>
6688
<Rectangle Fill="White" AutomationProperties.Name="White"/>
67-
<Rectangle Fill="Black" AutomationProperties.Name="Black"/> </GridView.Items>
89+
<Rectangle Fill="Black" AutomationProperties.Name="Black"/>
90+
</GridView.Items>
6891
</GridView>
6992

7093
</Flyout>
@@ -111,23 +134,33 @@
111134
</local:ControlExample.Example>
112135

113136
</local:ControlExample>
114-
<local:ControlExample HeaderText="Fallback titlebar when no user defined titlebar is set"
115-
CSharpSource="Window/TitleBar/TitleBarSample2.txt">
137+
<local:ControlExample HeaderText="Titlebar Customizations : Interactive controls in Titlebar (non client) area"
138+
CSharpSource="Window\TitleBar\TitleBarSample3.txt">
116139
<local:ControlExample.Example>
117140
<StackPanel Orientation="Vertical" Spacing="10">
118141
<TextBlock TextWrapping="WrapWholeWords">
119-
WinUI provides a fallback titlebar in case where user doesn't want to provide a uielement for setting the titlebar.
120-
A small horizontal section next to min/max/close caption buttons is chosen as the fallback titlebar.
121-
<LineBreak></LineBreak>
122-
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.
142+
WinUI custom titlebar now hosting interactive clickable controls within non client region of the window, when using custom titlebar.
123143
<LineBreak></LineBreak>
124-
Use the Color dropdown controls in the section above to change color of the fallback titlebar.
144+
This is achieved by using lower level
145+
<Hyperlink NavigateUri="https://learn.microsoft.com/windows/windows-app-sdk/api/winrt/microsoft.ui.windowing.appwindowtitlebar">
146+
Microsoft.UI.AppWindowTitlebar
147+
</Hyperlink>
148+
and
149+
<Italic>Microsoft.UI.NonClientInputPointerSource apis</Italic>
150+
<LineBreak></LineBreak>
151+
<LineBreak></LineBreak>
152+
WinUI allows <Bold> mix and match </Bold> of higher level WinUI custom titlebar apis with lower level AppWindow and NonClientInputPointerSource apis for most cases.
153+
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.
154+
If needed, set <Italic> Window.SetTitlebar </Italic> to null (default case) and proceed to use lower level apis for drag functionality.
155+
<LineBreak></LineBreak>
156+
Use the button below to toggle between system titlebar and default custom titlebar.
125157
</TextBlock>
126158
<StackPanel Orientation="Horizontal" HorizontalAlignment="Stretch" VerticalAlignment="Top" Spacing="20">
127-
<Button x:Name="defaultTitleBar" Click="defaultTitleBar_Click"></Button>
159+
<Button x:Name="addInteractiveElements" Click="AddInteractiveElements_Click">Add interactive control to titlebar</Button>
128160
</StackPanel>
129161
</StackPanel>
130162
</local:ControlExample.Example>
131163
</local:ControlExample>
164+
132165
</StackPanel>
133166
</Page>

WinUIGallery/ControlPages/TitleBarPage.xaml.cs

Lines changed: 87 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,12 @@
1515
using WinUIGallery.DesktopWap.Helper;
1616
using Microsoft.UI.Xaml.Shapes;
1717
using System.Threading.Tasks;
18+
using Microsoft.UI.Windowing;
19+
using Microsoft.UI.Xaml.Navigation;
20+
using Microsoft.UI.Input;
21+
using System.IO;
22+
using Windows.Foundation;
23+
using System;
1824

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

3441
public TitleBarPage()
3542
{
@@ -41,11 +48,17 @@ public TitleBarPage()
4148
};
4249
}
4350

51+
protected override void OnNavigatedFrom(NavigationEventArgs e)
52+
{
53+
ResetTitlebarSettings();
54+
}
55+
56+
4457

45-
private void SetTitleBar(UIElement titlebar)
58+
private void SetTitleBar(UIElement titlebar, bool forceCustomTitlebar = false)
4659
{
4760
var window = WindowHelper.GetWindowForElement(this as UIElement);
48-
if (!window.ExtendsContentIntoTitleBar)
61+
if (forceCustomTitlebar || !window.ExtendsContentIntoTitleBar)
4962
{
5063
window.ExtendsContentIntoTitleBar = true;
5164
window.SetTitleBar(titlebar);
@@ -60,18 +73,43 @@ private void SetTitleBar(UIElement titlebar)
6073
UpdateTitleBarColor();
6174
}
6275

76+
private void ResetTitlebarSettings()
77+
{
78+
var window = WindowHelper.GetWindowForElement(this as UIElement);
79+
UIElement titleBarElement = UIHelper.FindElementByName(this as UIElement, "AppTitleBar");
80+
SetTitleBar(titleBarElement, forceCustomTitlebar: true);
81+
ClearClickThruRegions();
82+
var txtBoxNonClientArea = UIHelper.FindElementByName(this as UIElement, "AppTitleBarTextBox") as FrameworkElement;
83+
txtBoxNonClientArea.Visibility = Visibility.Collapsed;
84+
addInteractiveElements.Content = "Add interactive control to titlebar";
85+
}
86+
87+
private void SetClickThruRegions(Windows.Graphics.RectInt32[] rects)
88+
{
89+
var window = WindowHelper.GetWindowForElement(this as UIElement);
90+
var nonClientInputSrc = InputNonClientPointerSource.GetForWindowId(window.AppWindow.Id);
91+
nonClientInputSrc.SetRegionRects(NonClientRegionKind.Passthrough, rects);
92+
}
93+
94+
private void ClearClickThruRegions()
95+
{
96+
var window = WindowHelper.GetWindowForElement(this as UIElement);
97+
var noninputsrc = InputNonClientPointerSource.GetForWindowId(window.AppWindow.Id);
98+
noninputsrc.ClearRegionRects(NonClientRegionKind.Passthrough);
99+
}
100+
63101
public void UpdateButtonText()
64102
{
65103
var window = WindowHelper.GetWindowForElement(this as UIElement);
66104
if (window.ExtendsContentIntoTitleBar)
67105
{
68-
customTitleBar.Content = "Reset to system TitleBar";
69-
defaultTitleBar.Content = "Reset to system TitleBar";
106+
customTitleBar.Content = "Reset to System TitleBar";
107+
defaultTitleBar.Content = "Reset to System TitleBar";
70108
}
71109
else
72110
{
73111
customTitleBar.Content = "Set Custom TitleBar";
74-
defaultTitleBar.Content = "Set Fallback Custom TitleBar";
112+
defaultTitleBar.Content = "Set Default Custom TitleBar";
75113
}
76114

77115
}
@@ -117,7 +155,6 @@ private void customTitleBar_Click(object sender, RoutedEventArgs e)
117155
{
118156
UIElement titleBarElement = UIHelper.FindElementByName(sender as UIElement, "AppTitleBar");
119157
SetTitleBar(titleBarElement);
120-
121158
// announce visual change to automation
122159
UIHelper.AnnounceActionForAccessibility(sender as UIElement, "TitleBar size and width changed", "TitleBarChangedNotificationActivityId");
123160
}
@@ -128,5 +165,48 @@ private void defaultTitleBar_Click(object sender, RoutedEventArgs e)
128165
// announce visual change to automation
129166
UIHelper.AnnounceActionForAccessibility(sender as UIElement, "TitleBar size and width changed", "TitleBarChangedNotificationActivityId");
130167
}
168+
169+
private void AddInteractiveElements_Click(object sender, RoutedEventArgs e)
170+
{
171+
var txtBoxNonClientArea = UIHelper.FindElementByName(sender as UIElement, "AppTitleBarTextBox") as FrameworkElement;
172+
173+
if (txtBoxNonClientArea.Visibility == Visibility.Visible)
174+
{
175+
ResetTitlebarSettings();
176+
}
177+
else
178+
{
179+
addInteractiveElements.Content = "Remove interactive control from titlebar";
180+
txtBoxNonClientArea.Visibility = Visibility.Visible;
181+
if (!sizeChangedEventHandlerAdded)
182+
{
183+
sizeChangedEventHandlerAdded = true;
184+
// run this code when textbox has been made visible and its actual width and height has been calculated
185+
txtBoxNonClientArea.SizeChanged += (object sender, SizeChangedEventArgs e) =>
186+
{
187+
if (txtBoxNonClientArea.Visibility != Visibility.Collapsed)
188+
{
189+
GeneralTransform transformTxtBox = txtBoxNonClientArea.TransformToVisual(null);
190+
Rect bounds = transformTxtBox.TransformBounds(new Rect(0, 0, txtBoxNonClientArea.ActualWidth, txtBoxNonClientArea.ActualHeight));
191+
192+
var scale = WindowHelper.GetRasterizationScaleForElement(this);
193+
194+
var transparentRect = new Windows.Graphics.RectInt32(
195+
_X: (int)Math.Round(bounds.X * scale),
196+
_Y: (int)Math.Round(bounds.Y * scale),
197+
_Width: (int)Math.Round(bounds.Width * scale),
198+
_Height: (int)Math.Round(bounds.Height * scale)
199+
);
200+
var rectArr = new Windows.Graphics.RectInt32[] { transparentRect };
201+
SetClickThruRegions(rectArr);
202+
}
203+
};
204+
}
205+
txtBoxNonClientArea.Width += 1; //to trigger size changed event
206+
}
207+
// announce visual change to automation
208+
UIHelper.AnnounceActionForAccessibility(sender as UIElement, "TitleBar size and width changed", "TitleBarChangedNotificationActivityId");
209+
}
210+
131211
}
132212
}
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// no UIElement is set for titlebar, fallback titlebar is created
1+
// no UIElement is set for titlebar, default titlebar is created which extends to entire non client area
22
Window window = App.MainWindow;
33
window.ExtendsContentIntoTitleBar = true;
4-
window.SetTitleBar(null); // this line is optional as by it is null by default
4+
// window.SetTitleBar(null); // optional line as not setting any UIElement as titlebar is same as setting null as titlebar
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
Window window = App.MainWindow;
2+
window.ExtendsContentIntoTitleBar = true;
3+
window.SetTitleBar(AppTitleBar);
4+
var nonClientInputSrc = InputNonClientPointerSource.GetForWindowId(window.AppWindow.Id);
5+
6+
// textbox on titlebar area
7+
var txtBoxNonClientArea = UIHelper.FindElementByName(sender as UIElement, "AppTitleBarTextBox") as FrameworkElement;
8+
GeneralTransform transformTxtBox = txtBoxNonClientArea.TransformToVisual(null);
9+
Rect bounds = transformTxtBox.TransformBounds(new Rect(0, 0, txtBoxNonClientArea.ActualWidth, txtBoxNonClientArea.ActualHeight));
10+
11+
// Windows.Graphics.RectInt32[] rects defines the area which allows click throughs in custom titlebar
12+
// it is non dpi-aware client coordinates. Hence, we convert dpi aware coordinates to non-dpi coordinates
13+
var scale = WindowHelper.GetRasterizationScaleForElement(this);
14+
var transparentRect = new Windows.Graphics.RectInt32(
15+
_X: (int)Math.Round(bounds.X * scale),
16+
_Y: (int)Math.Round(bounds.Y * scale),
17+
_Width: (int)Math.Round(bounds.Width * scale),
18+
_Height: (int)Math.Round(bounds.Height * scale)
19+
);
20+
var rects = new Windows.Graphics.RectInt32[] { transparentRect };
21+
22+
nonClientInputSrc.SetRegionRects(NonClientRegionKind.Passthrough, rects); // areas defined will be click through and can host button and textboxes

WinUIGallery/DataModel/ControlInfoData.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2828,7 +2828,7 @@
28282828
"Subtitle": "An example showing a custom UIElement used as the titlebar for the app's window.",
28292829
"ImagePath": "ms-appx:///Assets/ControlImages/TitleBar.png",
28302830
"ImageIconPath": "ms-appx:///Assets/ControlIcons/DefaultIcon.png",
2831-
"Description": "This sample shows how to use a custom UIElement as titlebar for app's window.",
2831+
"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.",
28322832
"Content": "<p>Look at the <i>TitleBarPage.xaml</i> file in Visual Studio to see the full code for this page.</p>",
28332833
"IsUpdated": true,
28342834
"Docs": [

WinUIGallery/Helper/TitleBarHelper.cs

Lines changed: 4 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -19,27 +19,11 @@
1919

2020
namespace WinUIGallery.DesktopWap.Helper
2121
{
22+
2223
internal class TitleBarHelper
2324
{
24-
25-
private static void triggerTitleBarRepaint(Window window)
26-
{
27-
// to trigger repaint tracking task id 38044406
28-
var hwnd = WinRT.Interop.WindowNative.GetWindowHandle(window);
29-
var activeWindow = AppUIBasics.Win32.GetActiveWindow();
30-
if (hwnd == activeWindow)
31-
{
32-
AppUIBasics.Win32.SendMessage(hwnd, AppUIBasics.Win32.WM_ACTIVATE, AppUIBasics.Win32.WA_INACTIVE, IntPtr.Zero);
33-
AppUIBasics.Win32.SendMessage(hwnd, AppUIBasics.Win32.WM_ACTIVATE, AppUIBasics.Win32.WA_ACTIVE, IntPtr.Zero);
34-
}
35-
else
36-
{
37-
AppUIBasics.Win32.SendMessage(hwnd, AppUIBasics.Win32.WM_ACTIVATE, AppUIBasics.Win32.WA_ACTIVE, IntPtr.Zero);
38-
AppUIBasics.Win32.SendMessage(hwnd, AppUIBasics.Win32.WM_ACTIVATE, AppUIBasics.Win32.WA_INACTIVE, IntPtr.Zero);
39-
}
40-
41-
}
42-
25+
// workaround as Appwindow titlebar doesn't update caption button colors correctly when changed while app is running
26+
// https://task.ms/44172495
4327
public static Windows.UI.Color ApplySystemThemeToCaptionButtons(Window window)
4428
{
4529
var res = Application.Current.Resources;
@@ -61,7 +45,7 @@ public static void SetCaptionButtonColors(Window window, Windows.UI.Color color)
6145
{
6246
var res = Application.Current.Resources;
6347
res["WindowCaptionForeground"] = color;
64-
triggerTitleBarRepaint(window);
48+
window.AppWindow.TitleBar.ButtonForegroundColor = color;
6549
}
6650
}
6751
}

WinUIGallery/Helper/WindowHelper.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,21 @@ static public Window GetWindowForElement(UIElement element)
6666
}
6767
return null;
6868
}
69+
// get dpi for an element
70+
static public double GetRasterizationScaleForElement(UIElement element)
71+
{
72+
if (element.XamlRoot != null)
73+
{
74+
foreach (Window window in _activeWindows)
75+
{
76+
if (element.XamlRoot == window.Content.XamlRoot)
77+
{
78+
return element.XamlRoot.RasterizationScale;
79+
}
80+
}
81+
}
82+
return 0.0;
83+
}
6984

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

WinUIGallery/Navigation/NavigationRootPage.xaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@
6767
VerticalAlignment="Center"
6868
Style="{StaticResource CaptionTextBlockStyle}"
6969
Text="{x:Bind AppTitleText}" />
70+
<TextBox x:Name="AppTitleBarTextBox" MinWidth="300" Height="40" Margin="16,0,0,0" Visibility="Collapsed" PlaceholderText="Enter any text" />
7071
</StackPanel>
7172
</Border>
7273

0 commit comments

Comments
 (0)