361 lines
13 KiB
C#
361 lines
13 KiB
C#
using System.Runtime.InteropServices;
|
||
using System.Windows.Input;
|
||
|
||
namespace Melskin.Controls;
|
||
|
||
/// <summary>
|
||
/// MelWindow 类继承自 WPF 的 Window 类,用于创建具有特定样式和功能的窗口。
|
||
/// 该类通过设置默认样式资源引用来自定义窗口外观,并提供属性来控制窗口的行为和内容布局。
|
||
/// </summary>
|
||
public class MelWindow : Window
|
||
{
|
||
private const string VbMinimizeButtonName = "minimizeButton";
|
||
private const string VbMaximizeRestoreButtonName = "maximizeRestoreButton";
|
||
/// <summary>
|
||
/// 用于在XAML模板中标识关闭按钮的名称。
|
||
/// 此常量字符串值为 "closeButton",用于在窗口样式应用时通过GetTemplateChild方法查找对应的关闭按钮控件。
|
||
/// </summary>
|
||
private const string VbCloseButtonName = "closeButton";
|
||
/// <summary>
|
||
///
|
||
/// </summary>
|
||
public MelWindow()
|
||
{
|
||
SetResourceReference(StyleProperty, typeof(MelWindow)); //设置默认的样式资源引用
|
||
}
|
||
|
||
#region 自定义标题栏按钮行为
|
||
|
||
/// <inheritdoc />
|
||
protected override void OnSourceInitialized(EventArgs e)
|
||
{
|
||
base.OnSourceInitialized(e);
|
||
var helper = new WindowInteropHelper(this);
|
||
var hwndSource = HwndSource.FromHwnd(helper.Handle);
|
||
hwndSource?.AddHook(HwndSourceHook);
|
||
}
|
||
|
||
[DllImport("user32.dll")]
|
||
[return: MarshalAs(UnmanagedType.Bool)]
|
||
private static extern bool TrackMouseEvent(ref TRACKMOUSEEVENT lpEventTrack);
|
||
|
||
/// <summary>
|
||
/// 跟踪鼠标事件的结构体。
|
||
/// </summary>
|
||
[StructLayout(LayoutKind.Sequential)]
|
||
public struct TRACKMOUSEEVENT
|
||
{
|
||
/// <summary>
|
||
/// 结构体大小
|
||
/// </summary>
|
||
public int cbSize;
|
||
/// <summary>
|
||
/// 控制跟踪行为的标志
|
||
/// </summary>
|
||
public uint dwFlags;
|
||
/// <summary>
|
||
/// 要为其跟踪鼠标事件的窗口句柄
|
||
/// </summary>
|
||
public IntPtr hwndTrack;
|
||
/// <summary>
|
||
/// 可选的悬停时间(毫秒)
|
||
/// </summary>
|
||
public uint dwHoverTime;
|
||
/// <summary>
|
||
/// 初始化 TRACKMOUSEEVENT 结构体的新实例。
|
||
/// </summary>
|
||
/// <param name="dwFlags">A combination of flags that specify the services requested. The value should correspond to the TME_*
|
||
/// constants defined by the Windows API.</param>
|
||
/// <param name="hwndTrack">窗口句柄</param>
|
||
public TRACKMOUSEEVENT(uint dwFlags, IntPtr hwndTrack)
|
||
{
|
||
this.cbSize = Marshal.SizeOf(typeof(TRACKMOUSEEVENT));
|
||
this.dwFlags = dwFlags;
|
||
this.hwndTrack = hwndTrack;
|
||
this.dwHoverTime = 0;
|
||
}
|
||
}
|
||
|
||
private const int WM_NCHITTEST = 0x0084;
|
||
private const int WM_NCMOUSEMOVE = 0x00A0;
|
||
private const int WM_NCMOUSELEAVE = 0x02A2;
|
||
private const int WM_NCLBUTTONDOWN = 0x00A1;
|
||
private const int WM_NCLBUTTONUP = 0x00A2;
|
||
private const int WM_SYSCOMMAND = 0x0112;
|
||
private const int HTMAXBUTTON = 9;
|
||
private const int SC_MAXIMIZE = 0xF030;
|
||
private const int SC_RESTORE = 0xF120;
|
||
private const uint TME_LEAVE = 0x00000002;
|
||
private const uint TME_NONCLIENT = 0x00000010;
|
||
|
||
#endregion
|
||
|
||
private bool isMouseOverMaximizeButton = false;
|
||
private bool isMaximizeButtonPressed = false;
|
||
/// <summary>
|
||
/// 记录点击前的窗口状态,用于判断系统是否已通过 Snap Layout 处理了窗口状态变更。
|
||
/// </summary>
|
||
private WindowState windowStateBeforeClick;
|
||
|
||
private IntPtr HwndSourceHook(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
|
||
{
|
||
switch (msg)
|
||
{
|
||
case WM_NCHITTEST:
|
||
if (IsMouseOverMaximizeButton(lParam))
|
||
{
|
||
// 返回 HTMAXBUTTON 让 Windows 11 Shell 知道鼠标在最大化按钮上,
|
||
// 从而触发 Snap Layout 弹出菜单。
|
||
handled = true;
|
||
return new IntPtr(HTMAXBUTTON);
|
||
}
|
||
else if (isMouseOverMaximizeButton || isMaximizeButtonPressed)
|
||
{
|
||
// 如果鼠标移出按钮区域,重置状态
|
||
isMouseOverMaximizeButton = false;
|
||
isMaximizeButtonPressed = false;
|
||
UpdateMaximizeButtonVisualState();
|
||
}
|
||
break;
|
||
|
||
case WM_NCMOUSEMOVE:
|
||
bool isCurrentlyOver = IsMouseOverMaximizeButton(lParam);
|
||
if (isCurrentlyOver != isMouseOverMaximizeButton)
|
||
{
|
||
isMouseOverMaximizeButton = isCurrentlyOver;
|
||
UpdateMaximizeButtonVisualState();
|
||
|
||
if (isMouseOverMaximizeButton)
|
||
{
|
||
var tme = new TRACKMOUSEEVENT(TME_LEAVE | TME_NONCLIENT, hwnd);
|
||
TrackMouseEvent(ref tme);
|
||
}
|
||
}
|
||
break;
|
||
|
||
case WM_NCMOUSELEAVE:
|
||
isMouseOverMaximizeButton = false;
|
||
isMaximizeButtonPressed = false;
|
||
UpdateMaximizeButtonVisualState();
|
||
break;
|
||
|
||
case WM_NCLBUTTONDOWN:
|
||
if (isMouseOverMaximizeButton)
|
||
{
|
||
isMaximizeButtonPressed = true;
|
||
windowStateBeforeClick = this.WindowState;
|
||
UpdateMaximizeButtonVisualState();
|
||
handled = true;
|
||
}
|
||
break;
|
||
|
||
case WM_NCLBUTTONUP:
|
||
if (isMaximizeButtonPressed)
|
||
{
|
||
isMaximizeButtonPressed = false;
|
||
isMouseOverMaximizeButton = false;
|
||
UpdateMaximizeButtonVisualState();
|
||
|
||
// 检查系统是否已经通过 Snap Layout 处理了窗口状态变更
|
||
// 如果窗口状态未被系统改变,说明用户只是普通点击最大化按钮
|
||
if (this.WindowState == windowStateBeforeClick)
|
||
{
|
||
ToggleWindowState();
|
||
}
|
||
handled = true;
|
||
}
|
||
break;
|
||
|
||
case WM_SYSCOMMAND:
|
||
var command = wParam.ToInt32() & 0xFFF0;
|
||
if (command == SC_MAXIMIZE || command == SC_RESTORE)
|
||
{
|
||
// 让系统正常处理 Snap Layout 发出的最大化/还原命令
|
||
// 不要阻止这些消息,以确保 Snap Layout 选择的布局能正确应用
|
||
}
|
||
break;
|
||
}
|
||
|
||
return IntPtr.Zero;
|
||
}
|
||
|
||
private bool IsMouseOverMaximizeButton(IntPtr lParam)
|
||
{
|
||
// 使用 ToInt64() 确保在 64 位系统和多显示器环境下坐标解析正确
|
||
var val = lParam.ToInt64();
|
||
var x = (short)(val & 0xFFFF);
|
||
var y = (short)((val >> 16) & 0xFFFF);
|
||
var mousePos = new Point(x, y);
|
||
|
||
var windowPos = this.PointFromScreen(mousePos);
|
||
|
||
if (maximizeRestoreButton is Visual maximizeButtonVisual)
|
||
{
|
||
var bounds = VisualTreeHelper.GetDescendantBounds(maximizeButtonVisual);
|
||
//Debug.WriteLine(bounds.ToString());
|
||
var buttonTransform = maximizeButtonVisual.TransformToAncestor(this);
|
||
var buttonRect = buttonTransform.TransformBounds(bounds);
|
||
|
||
return buttonRect.Contains(windowPos);
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
private void UpdateMaximizeButtonVisualState()
|
||
{
|
||
string stateName;
|
||
if (isMaximizeButtonPressed && isMouseOverMaximizeButton)
|
||
{
|
||
stateName = "Pressed";
|
||
}
|
||
else if (isMouseOverMaximizeButton)
|
||
{
|
||
stateName = "MouseOver";
|
||
}
|
||
else
|
||
{
|
||
stateName = "Normal";
|
||
}
|
||
//VisualStateManager.GoToState(maximizeRestoreButton, stateName, true);
|
||
maximizeRestoreButton?.Background = stateName switch
|
||
{
|
||
"Pressed" =>
|
||
//VisualStateManager.GoToState(maximizeRestoreButton, "Pressed", true);
|
||
new SolidColorBrush((Color)ColorConverter.ConvertFromString("#33000000")),
|
||
"MouseOver" =>
|
||
//VisualStateManager.GoToState(maximizeRestoreButton, "MouseOver", true);
|
||
new SolidColorBrush((Color)ColorConverter.ConvertFromString("#1A000000")),
|
||
"Normal" => Brushes.Transparent,
|
||
//VisualStateManager.GoToState(maximizeRestoreButton, "Normal", true);
|
||
_ => maximizeRestoreButton?.Background
|
||
};
|
||
}
|
||
|
||
#region Window Control Methods
|
||
|
||
private void TitleBar_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
|
||
{
|
||
if (e.ClickCount == 2)
|
||
{
|
||
ToggleWindowState();
|
||
}
|
||
else
|
||
{
|
||
this.DragMove();
|
||
}
|
||
}
|
||
|
||
private void maximizeRestoreButton_Click(object sender, RoutedEventArgs e)
|
||
{
|
||
ToggleWindowState();
|
||
}
|
||
|
||
private void ToggleWindowState()
|
||
{
|
||
this.WindowState = (this.WindowState == WindowState.Normal) ? WindowState.Maximized : WindowState.Normal;
|
||
}
|
||
|
||
private void CloseButton_Click(object sender, RoutedEventArgs e)
|
||
{
|
||
this.Close();
|
||
}
|
||
|
||
#endregion
|
||
static MelWindow()
|
||
{
|
||
DefaultStyleKeyProperty.OverrideMetadata(typeof(MelWindow), new FrameworkPropertyMetadata(typeof(MelWindow)));
|
||
}
|
||
/// <summary>
|
||
/// 如果窗口不活动,请让窗口的内容淡出。
|
||
/// 默认值为真(已启用)。
|
||
/// </summary>
|
||
public static readonly DependencyProperty FadeContentIfInactiveProperty = DependencyProperty.Register(
|
||
nameof(FadeContentIfInactive), typeof(bool), typeof(MelWindow), new FrameworkPropertyMetadata(true));
|
||
|
||
/// <summary>
|
||
/// 如果窗口不活动,请让窗口的内容淡出。
|
||
/// 默认值为真(已启用)。
|
||
/// </summary>
|
||
public bool FadeContentIfInactive
|
||
{
|
||
get => (bool)GetValue(FadeContentIfInactiveProperty);
|
||
|
||
set => SetValue(FadeContentIfInactiveProperty, value);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 标题栏左侧内容模板
|
||
/// </summary>
|
||
public object TitleBarLeftContent
|
||
{
|
||
get => (object)GetValue(TitleBarLeftContentProperty);
|
||
set => SetValue(TitleBarLeftContentProperty, value);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 用于定义窗口左侧内容区域的数据模板。
|
||
/// 此依赖属性允许开发者自定义左侧内容的布局和显示方式,通过设置不同的DataTemplate来改变该区域的内容呈现形式。
|
||
/// </summary>
|
||
public static readonly DependencyProperty TitleBarLeftContentProperty =
|
||
DependencyProperty.Register(nameof(TitleBarLeftContent), typeof(object), typeof(MelWindow));
|
||
/// <summary>
|
||
/// 标题栏右侧内容模板
|
||
/// </summary>
|
||
public object TitleBarRightContent
|
||
{
|
||
get => (object)GetValue(TitleBarRightContentProperty);
|
||
set => SetValue(TitleBarRightContentProperty, value);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 用于定义窗口右侧内容区域的数据模板。
|
||
/// 此依赖属性允许开发者自定义右侧内容的布局和显示方式,通过设置不同的DataTemplate来改变该区域的内容呈现形式。
|
||
/// </summary>
|
||
public static readonly DependencyProperty TitleBarRightContentProperty =
|
||
DependencyProperty.Register(nameof(TitleBarRightContent), typeof(object), typeof(MelWindow));
|
||
private Button? minimizeButton;
|
||
private Button? maximizeRestoreButton;
|
||
private Button? closeButton;
|
||
|
||
/// <inheritdoc />
|
||
public override void OnApplyTemplate()
|
||
{
|
||
base.OnApplyTemplate();
|
||
minimizeButton?.Click -= MinimizeButtonClickHandler;
|
||
|
||
minimizeButton = GetTemplateChild(VbMinimizeButtonName) as Button;
|
||
|
||
minimizeButton?.Click += MinimizeButtonClickHandler;
|
||
|
||
maximizeRestoreButton?.Click -= MaximizeRestoreButtonClickHandler;
|
||
|
||
maximizeRestoreButton = GetTemplateChild(VbMaximizeRestoreButtonName) as Button;
|
||
|
||
maximizeRestoreButton?.Click += MaximizeRestoreButtonClickHandler;
|
||
|
||
closeButton?.Click -= CloseButtonClickHandler;
|
||
|
||
closeButton = GetTemplateChild(VbCloseButtonName) as Button;
|
||
|
||
closeButton?.Click += CloseButtonClickHandler;
|
||
|
||
|
||
}
|
||
|
||
private void CloseButtonClickHandler(object sender, RoutedEventArgs args)
|
||
{
|
||
Close();
|
||
}
|
||
|
||
private void MaximizeRestoreButtonClickHandler(object sender, RoutedEventArgs args)
|
||
{
|
||
WindowState = (WindowState == WindowState.Normal) ? WindowState.Maximized : WindowState.Normal;
|
||
}
|
||
|
||
private void MinimizeButtonClickHandler(object sender, RoutedEventArgs args)
|
||
{
|
||
WindowState = WindowState.Minimized;
|
||
}
|
||
}
|