2025-10-10 11:19:58 +08:00
|
|
|
|
using System.Runtime.InteropServices;
|
|
|
|
|
|
using System.Windows.Automation.Peers;
|
|
|
|
|
|
using System.Windows.Automation.Provider;
|
|
|
|
|
|
using System.Windows.Input;
|
|
|
|
|
|
|
2026-01-02 17:30:30 +08:00
|
|
|
|
using Melskin.Assists;
|
2025-10-10 11:19:58 +08:00
|
|
|
|
|
2026-01-02 17:30:30 +08:00
|
|
|
|
namespace Melskin.Controls;
|
2025-10-10 11:19:58 +08:00
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// NeoWindow 类继承自 WPF 的 Window 类,用于创建具有特定样式和功能的窗口。
|
|
|
|
|
|
/// 该类通过设置默认样式资源引用来自定义窗口外观,并提供属性来控制窗口的行为和内容布局。
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public class NeoWindow : Window
|
|
|
|
|
|
{
|
|
|
|
|
|
private const string VbMinimizeButtonName = "minimizeButton";
|
|
|
|
|
|
private const string VbMaximizeRestoreButtonName = "maximizeRestoreButton";
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 用于在XAML模板中标识关闭按钮的名称。
|
|
|
|
|
|
/// 此常量字符串值为 "closeButton",用于在窗口样式应用时通过GetTemplateChild方法查找对应的关闭按钮控件。
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
private const string VbCloseButtonName = "closeButton";
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
///
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public NeoWindow()
|
|
|
|
|
|
{
|
|
|
|
|
|
SetResourceReference(StyleProperty, typeof(NeoWindow)); //设置默认的样式资源引用
|
|
|
|
|
|
}
|
2025-12-23 21:35:54 +08:00
|
|
|
|
|
2025-10-10 11:19:58 +08:00
|
|
|
|
#region 自定义标题栏按钮行为
|
|
|
|
|
|
|
2025-12-23 21:35:54 +08:00
|
|
|
|
/// <inheritdoc />
|
2025-10-10 11:19:58 +08:00
|
|
|
|
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);
|
|
|
|
|
|
|
2025-12-23 21:35:54 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 跟踪鼠标事件的结构体。
|
|
|
|
|
|
/// </summary>
|
2025-10-10 11:19:58 +08:00
|
|
|
|
[StructLayout(LayoutKind.Sequential)]
|
|
|
|
|
|
public struct TRACKMOUSEEVENT
|
|
|
|
|
|
{
|
2025-12-23 21:35:54 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 结构体大小
|
|
|
|
|
|
/// </summary>
|
2025-10-10 11:19:58 +08:00
|
|
|
|
public int cbSize;
|
2025-12-23 21:35:54 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 控制跟踪行为的标志
|
|
|
|
|
|
/// </summary>
|
2025-10-10 11:19:58 +08:00
|
|
|
|
public uint dwFlags;
|
2025-12-23 21:35:54 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 要为其跟踪鼠标事件的窗口句柄
|
|
|
|
|
|
/// </summary>
|
2025-10-10 11:19:58 +08:00
|
|
|
|
public IntPtr hwndTrack;
|
2025-12-23 21:35:54 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 可选的悬停时间(毫秒)
|
|
|
|
|
|
/// </summary>
|
2025-10-10 11:19:58 +08:00
|
|
|
|
public uint dwHoverTime;
|
2025-12-23 21:35:54 +08:00
|
|
|
|
/// <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>
|
2025-10-10 11:19:58 +08:00
|
|
|
|
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 HTMAXBUTTON = 9;
|
|
|
|
|
|
private const uint TME_LEAVE = 0x00000002;
|
|
|
|
|
|
private const uint TME_NONCLIENT = 0x00000010;
|
|
|
|
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
|
|
private bool isMouseOverMaximizeButton = false;
|
|
|
|
|
|
private bool isMaximizeButtonPressed = false;
|
|
|
|
|
|
|
|
|
|
|
|
private IntPtr HwndSourceHook(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
|
|
|
|
|
|
{
|
|
|
|
|
|
switch (msg)
|
|
|
|
|
|
{
|
|
|
|
|
|
case WM_NCHITTEST:
|
|
|
|
|
|
if (IsMouseOverMaximizeButton(lParam))
|
|
|
|
|
|
{
|
|
|
|
|
|
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;
|
|
|
|
|
|
UpdateMaximizeButtonVisualState();
|
|
|
|
|
|
handled = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
|
|
case WM_NCLBUTTONUP:
|
|
|
|
|
|
if (isMaximizeButtonPressed)
|
|
|
|
|
|
{
|
|
|
|
|
|
isMaximizeButtonPressed = false;
|
|
|
|
|
|
UpdateMaximizeButtonVisualState();
|
|
|
|
|
|
|
|
|
|
|
|
if (isMouseOverMaximizeButton)
|
|
|
|
|
|
{
|
2025-12-23 21:35:54 +08:00
|
|
|
|
maximizeRestoreButton?.RaiseEvent(new RoutedEventArgs(Button.ClickEvent));
|
2025-10-10 11:19:58 +08:00
|
|
|
|
}
|
|
|
|
|
|
handled = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return IntPtr.Zero;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private bool IsMouseOverMaximizeButton(IntPtr lParam)
|
|
|
|
|
|
{
|
|
|
|
|
|
var x = (short)(lParam.ToInt32() & 0xFFFF);
|
|
|
|
|
|
var y = (short)(lParam.ToInt32() >> 16);
|
|
|
|
|
|
var mousePos = new Point(x, y);
|
|
|
|
|
|
|
|
|
|
|
|
var windowPos = this.PointFromScreen(mousePos);
|
|
|
|
|
|
|
2025-12-23 21:35:54 +08:00
|
|
|
|
if (maximizeRestoreButton is Visual maximizeButtonVisual)
|
2025-10-10 11:19:58 +08:00
|
|
|
|
{
|
|
|
|
|
|
var bounds = VisualTreeHelper.GetDescendantBounds(maximizeButtonVisual);
|
|
|
|
|
|
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);
|
2026-01-02 16:37:37 +08:00
|
|
|
|
maximizeRestoreButton?.Background = stateName switch
|
2025-10-10 11:19:58 +08:00
|
|
|
|
{
|
2026-01-02 16:37:37 +08:00
|
|
|
|
"Pressed" =>
|
2025-10-10 11:19:58 +08:00
|
|
|
|
//VisualStateManager.GoToState(maximizeRestoreButton, "Pressed", true);
|
2026-01-02 16:37:37 +08:00
|
|
|
|
new SolidColorBrush((Color)ColorConverter.ConvertFromString("#33000000")),
|
|
|
|
|
|
"MouseOver" =>
|
2025-10-10 11:19:58 +08:00
|
|
|
|
//VisualStateManager.GoToState(maximizeRestoreButton, "MouseOver", true);
|
2026-01-02 16:37:37 +08:00
|
|
|
|
new SolidColorBrush((Color)ColorConverter.ConvertFromString("#1A000000")),
|
|
|
|
|
|
"Normal" => Brushes.Transparent,
|
2025-10-10 11:19:58 +08:00
|
|
|
|
//VisualStateManager.GoToState(maximizeRestoreButton, "Normal", true);
|
2026-01-02 16:37:37 +08:00
|
|
|
|
_ => maximizeRestoreButton?.Background
|
|
|
|
|
|
};
|
2025-10-10 11:19:58 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#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 NeoWindow()
|
|
|
|
|
|
{
|
|
|
|
|
|
DefaultStyleKeyProperty.OverrideMetadata(typeof(NeoWindow), new FrameworkPropertyMetadata(typeof(NeoWindow)));
|
|
|
|
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 如果窗口不活动,请让窗口的内容淡出。
|
|
|
|
|
|
/// 默认值为真(已启用)。
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public static readonly DependencyProperty FadeContentIfInactiveProperty = DependencyProperty.Register(
|
|
|
|
|
|
nameof(FadeContentIfInactive), typeof(bool), typeof(NeoWindow), 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(NeoWindow));
|
|
|
|
|
|
/// <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(NeoWindow));
|
|
|
|
|
|
private Button? minimizeButton;
|
|
|
|
|
|
private Button? maximizeRestoreButton;
|
|
|
|
|
|
private Button? closeButton;
|
|
|
|
|
|
|
|
|
|
|
|
/// <inheritdoc />
|
|
|
|
|
|
public override void OnApplyTemplate()
|
|
|
|
|
|
{
|
|
|
|
|
|
if (minimizeButton != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
minimizeButton.Click -= MinimizeButtonClickHandler;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
minimizeButton = GetTemplateChild(VbMinimizeButtonName) as Button;
|
|
|
|
|
|
|
|
|
|
|
|
if (minimizeButton != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
minimizeButton.Click += MinimizeButtonClickHandler;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (maximizeRestoreButton != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
maximizeRestoreButton.Click -= MaximizeRestoreButtonClickHandler;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
maximizeRestoreButton = GetTemplateChild(VbMaximizeRestoreButtonName) as Button;
|
|
|
|
|
|
|
|
|
|
|
|
if (maximizeRestoreButton != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
maximizeRestoreButton.Click += MaximizeRestoreButtonClickHandler;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (closeButton != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
closeButton.Click -= CloseButtonClickHandler;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
closeButton = GetTemplateChild(VbCloseButtonName) as Button;
|
|
|
|
|
|
|
|
|
|
|
|
if (closeButton != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
closeButton.Click += CloseButtonClickHandler;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
base.OnApplyTemplate();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|