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"> - - -