From 7ffeba837db9ce467f55b2039150b4d8fb77dfe5 Mon Sep 17 00:00:00 2001
From: Ivan Dmitriev <42055372+IvanDmitriev1@users.noreply.github.com>
Date: Sun, 22 Jan 2023 16:19:41 +0600
Subject: [PATCH 1/8] added TitleBarButton
---
src/Wpf.Ui/Controls/TitleBar.cs | 55 ++---
src/Wpf.Ui/Controls/TitleBarButton.cs | 204 ++++++++++++++++++
src/Wpf.Ui/Styles/Controls/TitleBar.xaml | 195 ++++++-----------
...itleBarButton.cs => TitleBarButtonType.cs} | 2 +-
4 files changed, 291 insertions(+), 165 deletions(-)
create mode 100644 src/Wpf.Ui/Controls/TitleBarButton.cs
rename src/Wpf.Ui/TitleBar/{TitleBarButton.cs => TitleBarButtonType.cs} (96%)
diff --git a/src/Wpf.Ui/Controls/TitleBar.cs b/src/Wpf.Ui/Controls/TitleBar.cs
index f95153e60..5d87358fc 100644
--- a/src/Wpf.Ui/Controls/TitleBar.cs
+++ b/src/Wpf.Ui/Controls/TitleBar.cs
@@ -17,8 +17,8 @@ namespace Wpf.Ui.Controls;
/// Custom navigation buttons for the window.
///
[TemplatePart(Name = "PART_MainGrid", Type = typeof(System.Windows.Controls.Grid))]
-[TemplatePart(Name = "PART_MaximizeButton", Type = typeof(Wpf.Ui.Controls.Button))]
-[TemplatePart(Name = "PART_RestoreButton", Type = typeof(Wpf.Ui.Controls.Button))]
+[TemplatePart(Name = "PART_MaximizeButton", Type = typeof(TitleBarButton))]
+[TemplatePart(Name = "PART_RestoreButton", Type = typeof(TitleBarButton))]
public class TitleBar : System.Windows.Controls.Control, IThemeControl
{
private const string ElementMainGrid = "PART_MainGrid";
@@ -31,7 +31,7 @@ public class TitleBar : System.Windows.Controls.Control, IThemeControl
internal Interop.WinDef.POINT _doubleClickPoint;
- internal SnapLayout _snapLayout;
+ internal SnapLayout? _snapLayout;
///
/// Property for .
@@ -379,7 +379,7 @@ public event RoutedEventHandler HelpClicked
///
public TitleBar()
{
- SetValue(TemplateButtonCommandProperty, new Common.RelayCommand(o => OnTemplateButtonClick(o ?? String.Empty)));
+ SetValue(TemplateButtonCommandProperty, new Common.RelayCommand(OnTemplateButtonClick));
Loaded += OnLoaded;
}
@@ -408,8 +408,8 @@ public override void OnApplyTemplate()
base.OnApplyTemplate();
var mainGrid = GetTemplateChild(ElementMainGrid) as System.Windows.Controls.Grid;
- var maximizeButton = GetTemplateChild(ElementMaximizeButton) as Wpf.Ui.Controls.Button;
- var restoreButton = GetTemplateChild(ElementRestoreButton) as Wpf.Ui.Controls.Button;
+ var maximizeButton = GetTemplateChild(ElementMaximizeButton) as TitleBarButton;
+ var restoreButton = GetTemplateChild(ElementRestoreButton) as TitleBarButton;
if (mainGrid != null)
{
@@ -417,8 +417,8 @@ public override void OnApplyTemplate()
mainGrid.MouseMove += OnMainGridMouseMove;
}
- if (ShowMaximize && UseSnapLayout && maximizeButton != null && restoreButton != null)
- InitializeSnapLayout(maximizeButton, restoreButton);
+ maximizeButton?.OnThemeChanged(Theme);
+ restoreButton?.OnThemeChanged(Theme);
}
///
@@ -527,31 +527,6 @@ private bool MinimizeWindowToTray()
return true;
}
- private void InitializeSnapLayout(Wpf.Ui.Controls.Button maximizeButton, Wpf.Ui.Controls.Button restoreButton)
- {
- if (!SnapLayout.IsSupported())
- return;
-
- _snapLayout = SnapLayout.Register(ParentWindow, maximizeButton, restoreButton);
-
- // Can be taken it from the Template, but honestly - a classic - TODO:
- // ButtonsBackground, but
- _snapLayout.HoverColorLight = new SolidColorBrush(Color.FromArgb(
- (byte)0x1A,
- (byte)0x00,
- (byte)0x00,
- (byte)0x00)
- );
- _snapLayout.HoverColorDark = new SolidColorBrush(Color.FromArgb(
- (byte)0x17,
- (byte)0xFF,
- (byte)0xFF,
- (byte)0xFF)
- );
-
- _snapLayout.Theme = Theme;
- }
-
private void OnMainGridMouseMove(object sender, MouseEventArgs e)
{
if (e.LeftButton != MouseButtonState.Pressed || ParentWindow == null)
@@ -612,31 +587,31 @@ private void OnMainGridMouseLeftButtonDown(object sender, MouseButtonEventArgs e
MaximizeWindow();
}
- private void OnTemplateButtonClick(string parameter)
+ private void OnTemplateButtonClick(TitleBarButtonType buttonType)
{
- switch (parameter)
+ switch (buttonType)
{
- case "maximize":
+ case TitleBarButtonType.Maximize:
RaiseEvent(new RoutedEventArgs(MaximizeClickedEvent, this));
MaximizeWindow();
break;
- case "restore":
+ case TitleBarButtonType.Restore:
RaiseEvent(new RoutedEventArgs(MaximizeClickedEvent, this));
RestoreWindow();
break;
- case "close":
+ case TitleBarButtonType.Close:
RaiseEvent(new RoutedEventArgs(CloseClickedEvent, this));
CloseWindow();
break;
- case "minimize":
+ case TitleBarButtonType.Minimize:
RaiseEvent(new RoutedEventArgs(MinimizeClickedEvent, this));
MinimizeWindow();
break;
- case "help":
+ case TitleBarButtonType.Help:
RaiseEvent(new RoutedEventArgs(HelpClickedEvent, this));
break;
}
diff --git a/src/Wpf.Ui/Controls/TitleBarButton.cs b/src/Wpf.Ui/Controls/TitleBarButton.cs
new file mode 100644
index 000000000..35f0f2b4e
--- /dev/null
+++ b/src/Wpf.Ui/Controls/TitleBarButton.cs
@@ -0,0 +1,204 @@
+using System;
+using System.Diagnostics;
+using System.Windows;
+using System.Windows.Automation.Peers;
+using System.Windows.Automation.Provider;
+using System.Windows.Interop;
+using System.Windows.Media;
+using Wpf.Ui.Appearance;
+using Wpf.Ui.Interop;
+using Wpf.Ui.TitleBar;
+
+namespace Wpf.Ui.Controls;
+
+public class TitleBarButton : Wpf.Ui.Controls.Button
+{
+ public static readonly DependencyProperty ButtonTypeProperty = DependencyProperty.Register(nameof(ButtonType),
+ typeof(TitleBarButtonType), typeof(TitleBarButton), new PropertyMetadata(TitleBarButtonType.Unknown));
+
+ ///
+ /// Property for .
+ ///
+ public static readonly DependencyProperty ButtonsForegroundProperty = DependencyProperty.Register(
+ nameof(ButtonsForeground),
+ typeof(Brush), typeof(TitleBarButton), new FrameworkPropertyMetadata(SystemColors.ControlTextBrush,
+ FrameworkPropertyMetadataOptions.Inherits));
+
+ public TitleBarButtonType ButtonType
+ {
+ get => (TitleBarButtonType)GetValue(ButtonTypeProperty);
+ set => SetValue(ButtonTypeProperty, value);
+ }
+
+ ///
+ /// Foreground of the navigation buttons.
+ ///
+ public Brush ButtonsForeground
+ {
+ get => (Brush)GetValue(ButtonsForegroundProperty);
+ set => SetValue(ButtonsForegroundProperty, value);
+ }
+
+ public bool IsHovered { get; private set; }
+
+ /*private static SolidColorBrush _hoverColorLight = new SolidColorBrush(Color.FromArgb(
+ (byte)0x1A,
+ (byte)0x00,
+ (byte)0x00,
+ (byte)0x00));
+
+ private static SolidColorBrush _hoverColorDark = new SolidColorBrush(Color.FromArgb(
+ (byte)0x17,
+ (byte)0xFF,
+ (byte)0xFF,
+ (byte)0xFF));*/
+
+ private HwndSource? _hwndSource;
+ private User32.WM_NCHITTEST _returnValue;
+ private Brush _defaultBackgroundBrush = null!;
+
+ private bool _isClickedDown;
+
+ public TitleBarButton()
+ {
+ Loaded += (_, _) =>
+ {
+ _defaultBackgroundBrush = Background;
+
+ if (ButtonType == TitleBarButtonType.Unknown)
+ return;
+
+ _hwndSource = PresentationSource.FromVisual(this) as HwndSource ??
+ throw new ArgumentNullException($"HwndSource is null");
+
+ _hwndSource.AddHook(Hook);
+
+ _returnValue = ButtonType switch
+ {
+ TitleBarButtonType.Help => User32.WM_NCHITTEST.HTHELP,
+ TitleBarButtonType.Minimize => User32.WM_NCHITTEST.HTMINBUTTON,
+ TitleBarButtonType.Close => User32.WM_NCHITTEST.HTCLOSE,
+ TitleBarButtonType.Restore => User32.WM_NCHITTEST.HTMAXBUTTON,
+ TitleBarButtonType.Maximize => User32.WM_NCHITTEST.HTMAXBUTTON,
+ _ => throw new ArgumentOutOfRangeException()
+ };
+ };
+
+ Unloaded += (_, _) => _hwndSource?.RemoveHook(Hook);
+ }
+
+ public void OnThemeChanged(ThemeType themeType)
+ {
+ /*_hoverBrush = themeType == ThemeType.Light ? _hoverColorLight : _hoverColorDark;*/
+ }
+
+ private IntPtr Hook(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
+ {
+ switch ((User32.WM)msg)
+ {
+ case User32.WM.MOVE:
+ // Adjust [Size] of the buttons if the DPI is changed
+ break;
+
+ // Hit test, for determining whether the mouse cursor is over one of the buttons
+ case User32.WM.NCHITTEST:
+ if (IsMouseOverElement(lParam))
+ {
+ Hover();
+ handled = true;
+ return (IntPtr)_returnValue;
+ }
+
+ RemoveHover();
+ break;
+
+ // Mouse leaves the window
+ case User32.WM.NCMOUSELEAVE:
+ RemoveHover();
+ break;
+
+ // Left button clicked down
+ case User32.WM.NCLBUTTONDOWN:
+ if (IsMouseOverElement(lParam))
+ {
+ _isClickedDown = true;
+ handled = true;
+ }
+ break;
+
+ // Left button clicked up
+ case User32.WM.NCLBUTTONUP:
+ if (_isClickedDown && IsMouseOverElement(lParam))
+ {
+ InvokeClick();
+ handled = true;
+ }
+ break;
+ }
+
+ return IntPtr.Zero;
+ }
+
+ ///
+ /// Do not call it outside of NCHITTEST message!
+ ///
+ private bool IsMouseOverElement(IntPtr lParam)
+ {
+ // This method will be invoked very often and must be as simple as possible.
+
+ if (lParam == IntPtr.Zero)
+ return false;
+
+ var mousePosScreen = new Point(Get_X_LParam(lParam), Get_Y_LParam(lParam));
+ var bounds = new Rect(new Point(), RenderSize);
+ var mousePosRelative = PointFromScreen(mousePosScreen);
+ return bounds.Contains(mousePosRelative);
+ }
+
+ ///
+ /// Invokes click on the button.
+ ///
+ private void InvokeClick()
+ {
+ if (new ButtonAutomationPeer(this).GetPattern(PatternInterface.Invoke) is IInvokeProvider invokeProvider)
+ invokeProvider.Invoke();
+
+ _isClickedDown = false;
+ }
+
+ ///
+ /// Forces button background to change.
+ ///
+ private void Hover()
+ {
+ if (IsHovered)
+ return;
+
+ Background = MouseOverBackground;
+ IsHovered = true;
+ }
+
+ ///
+ /// Forces button background to change.
+ ///
+ public void RemoveHover()
+ {
+ if (!IsHovered)
+ return;
+
+ Background = _defaultBackgroundBrush;
+
+ IsHovered = false;
+ _isClickedDown = false;
+ }
+
+ private static int Get_X_LParam(IntPtr lParam)
+ {
+ return (short)(lParam.ToInt32() & 0xFFFF);
+ }
+
+ private static int Get_Y_LParam(IntPtr lParam)
+ {
+ return (short)(lParam.ToInt32() >> 16);
+ }
+}
diff --git a/src/Wpf.Ui/Styles/Controls/TitleBar.xaml b/src/Wpf.Ui/Styles/Controls/TitleBar.xaml
index e0d1ee4b8..6fcc69108 100644
--- a/src/Wpf.Ui/Styles/Controls/TitleBar.xaml
+++ b/src/Wpf.Ui/Styles/Controls/TitleBar.xaml
@@ -8,62 +8,82 @@
+ xmlns:controls="clr-namespace:Wpf.Ui.Controls"
+ xmlns:titleBar="clr-namespace:Wpf.Ui.TitleBar">
-
-
-