FancyInput

This commit is contained in:
RL-Xiang
2026-09-02 20:08:33 +08:00
commit d80276d867
145 changed files with 27045 additions and 0 deletions
+39
View File
@@ -0,0 +1,39 @@
# Visual Studio user and workspace files
.vs/
*.suo
*.user
*.userosscache
*.sln.docstates
# Visual Studio Code workspace files
.vscode/
# .NET build outputs
[Bb]in/
[Oo]bj/
# Test and coverage output
TestResults/
coverage/
*.coverage
*.coveragexml
# NuGet and publish output
packages/
*.nupkg
*.snupkg
publish/
# Generated logs and temporary files
*.log
*.tmp
*.temp
# OS metadata
.DS_Store
Thumbs.db
# folders
FancyInput/Docs
FancyInput/Properties
FancyInput/Resources/Licenses
+28
View File
@@ -0,0 +1,28 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.12.36129.13 d17.12
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FancyInput", "FancyInput\FancyInput.csproj", "{CE7F092D-2694-47BC-A610-BC915DDF7970}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Debug|x64 = Debug|x64
Release|Any CPU = Release|Any CPU
Release|x64 = Release|x64
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{CE7F092D-2694-47BC-A610-BC915DDF7970}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{CE7F092D-2694-47BC-A610-BC915DDF7970}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CE7F092D-2694-47BC-A610-BC915DDF7970}.Debug|x64.ActiveCfg = Debug|x64
{CE7F092D-2694-47BC-A610-BC915DDF7970}.Debug|x64.Build.0 = Debug|x64
{CE7F092D-2694-47BC-A610-BC915DDF7970}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CE7F092D-2694-47BC-A610-BC915DDF7970}.Release|Any CPU.Build.0 = Release|Any CPU
{CE7F092D-2694-47BC-A610-BC915DDF7970}.Release|x64.ActiveCfg = Release|x64
{CE7F092D-2694-47BC-A610-BC915DDF7970}.Release|x64.Build.0 = Release|x64
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
EndGlobal
+24
View File
@@ -0,0 +1,24 @@
<Application x:Class="FancyInput.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:FancyInput"
xmlns:md="clr-namespace:FancyInput.Models"
StartupUri="MainWindow.xaml">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="Resources/Styles/ScrollViewerStyles.xaml"/>
<ResourceDictionary Source="Resources/Styles/ControlStyles.xaml"/>
<ResourceDictionary Source="Resources/Styles/TextBoxStyles.xaml"/>
<ResourceDictionary Source="Resources/Styles/ButtonStyles.xaml"/>
<ResourceDictionary Source="Resources/Styles/CheckBoxStyles.xaml"/>
<ResourceDictionary Source="Resources/Styles/SliderStyles.xaml"/>
<ResourceDictionary Source="Resources/Styles/ComboBoxStyles.xaml"/>
<ResourceDictionary Source="Resources/Styles/LabelStyles.xaml"/>
<ResourceDictionary Source="Resources/Styles/MenuStyles.xaml"/>
<ResourceDictionary Source="Resources/Styles/BorderStyles.xaml"/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
</Application>
+217
View File
@@ -0,0 +1,217 @@
using System;
using System.Diagnostics;
using System.IO;
using System.IO.Pipes;
using System.Threading;
using System.Windows;
using System.Security.Principal;
using System.Security.AccessControl;
using FancyInput.Models;
using System.Windows.Interop;
namespace FancyInput
{
public static class WindowHelper
{
public static void TryAddRawInputHook(Window window)
{
try
{
var hwndSource = HwndSource.FromHwnd(new WindowInteropHelper(window).Handle);
hwndSource.AddHook(RawInputParser.WndDragProc);
}
catch { }
}
public static void TryRemoveRawInputHook(Window window)
{
try
{
var hwndSource = HwndSource.FromHwnd(new WindowInteropHelper(window).Handle);
hwndSource.RemoveHook(RawInputParser.WndDragProc);
}
catch { }
}
}
public partial class App : Application
{
private static Mutex? _mutex;
private const string MutexName = "FancyInput_SingleInstance_Mutex";
private const string PipeName = "FancyInputPipe";
private volatile bool _pipeServerRunning = true;
private Thread? _pipeThread;
protected override void OnStartup(StartupEventArgs e)
{
if (e.Args.Length > 0 && e.Args[0] == "--restart-as-admin")
{
if (Application.Current.MainWindow is FancyInputMainWindow mainWindow)
{
mainWindow.AllowExit();
}
try
{
using var pipe = new NamedPipeClientStream(".", PipeName, PipeDirection.Out);
pipe.Connect(1000);
using var writer = new StreamWriter(pipe);
string allArgs = string.Join("|", e.Args);
writer.WriteLine(allArgs);
writer.Flush();
}
catch
{
// 主进程未启动或管道不可用,忽略
}
Application.Current.Shutdown();
return;
}
bool createdNew;
_mutex = new Mutex(true, MutexName, out createdNew);
if (!createdNew)
{
if (Application.Current.MainWindow is FancyInputMainWindow mainWindow)
mainWindow.AllowExit();
try
{
if (e.Args.Length > 0)
{
using var pipe = new NamedPipeClientStream(".", PipeName, PipeDirection.Out);
pipe.Connect(1000);
using var writer = new StreamWriter(pipe);
string allArgs = string.Join("|", e.Args);
writer.WriteLine(allArgs);
writer.Flush();
}
else
{
using var pipe = new NamedPipeClientStream(".", PipeName, PipeDirection.Out);
pipe.Connect(1000);
using var writer = new StreamWriter(pipe);
writer.WriteLine("--show");
writer.Flush();
}
}
catch
{
}
Application.Current.Shutdown();
return;
}
// 主进程启动管道监听
StartPipeServer();
base.OnStartup(e);
}
private void StartPipeServer()
{
_pipeServerRunning = true;
_pipeThread = new Thread(() =>
{
while (_pipeServerRunning)
{
var identity = WindowsIdentity.GetCurrent();
var userSid = identity.User;
var pipeSecurity = new PipeSecurity();
pipeSecurity.AddAccessRule(new PipeAccessRule(
userSid!,
PipeAccessRights.FullControl,
AccessControlType.Allow));
using var pipe = NamedPipeServerStreamAcl.Create(
PipeName,
PipeDirection.In,
1,
PipeTransmissionMode.Byte,
PipeOptions.None,
0, 0,
pipeSecurity);
try
{
pipe.WaitForConnection();
using var reader = new StreamReader(pipe);
string? argLine = reader.ReadLine();
if (!string.IsNullOrEmpty(argLine))
{
string[] args = argLine.Split('|');
if (File.Exists(args[0]))
{
Application.Current.Dispatcher.Invoke(() =>
{
if (Application.Current.MainWindow is FancyInputMainWindow mainWindow)
{
mainWindow.OpenProjectFile(args[0]);
}
});
}
if (args[0] == "--restart-as-admin")
{
_pipeServerRunning = false;
Application.Current.Dispatcher.Invoke(() =>
{
if (Application.Current.MainWindow is FancyInputMainWindow mainWindow)
{
mainWindow.AllowExit();
mainWindow.Close();
}
string[] extraArgs = args.Skip(1).ToArray();
string arguments = string.Join(" ", extraArgs.Select(a => $"\"{a}\""));
var processInfo = new ProcessStartInfo
{
FileName = Process.GetCurrentProcess().MainModule!.FileName,
Arguments = arguments,
UseShellExecute = true,
Verb = "runas"
};
// 主动关闭管道
pipe.Dispose();
_mutex?.ReleaseMutex();
_mutex?.Dispose();
_mutex = null;
try
{
Process.Start(processInfo);
}
catch
{
}
});
}
if (args[0] == "--show")
{
Application.Current.Dispatcher.Invoke(() =>
{
if (Application.Current.MainWindow is FancyInputMainWindow mainWindow)
{
mainWindow.Show();
if (mainWindow.WindowState == WindowState.Minimized)
mainWindow.WindowState = WindowState.Normal;
mainWindow.Activate();
}
});
}
}
}
catch
{
// 忽略异常,继续循环
}
}
});
_pipeThread.IsBackground = true;
_pipeThread.Start();
}
}
}
+50
View File
@@ -0,0 +1,50 @@
using System.Linq;
using System.Windows;
namespace FancyInput
{
public static class AppMessageBox
{
public static MessageBoxResult Show(string messageBoxText)
=> Show(messageBoxText, "提示", MessageBoxButton.OK, MessageBoxImage.None);
public static MessageBoxResult Show(string messageBoxText, string caption)
=> Show(messageBoxText, caption, MessageBoxButton.OK, MessageBoxImage.None);
public static MessageBoxResult Show(string messageBoxText, string caption, MessageBoxButton button)
=> Show(messageBoxText, caption, button, MessageBoxImage.None);
public static MessageBoxResult Show(string messageBoxText, string caption, MessageBoxButton button, MessageBoxImage icon)
{
if (Application.Current == null)
{
return MessageBox.Show(messageBoxText, caption, button, icon);
}
if (!Application.Current.Dispatcher.CheckAccess())
{
return Application.Current.Dispatcher.Invoke(() => Show(messageBoxText, caption, button, icon));
}
var windows = Application.Current.Windows.OfType<Window>().Where(w => w.IsVisible).ToList();
Window? mainWindow = Application.Current.MainWindow is { IsVisible: true } mw ? mw : null;
Window? owner = windows.FirstOrDefault(w => w.IsActive)
?? mainWindow
?? windows.FirstOrDefault();
var dialog = new AppMessageBoxWindow(messageBoxText, caption, button, icon);
if (owner != null)
{
dialog.Owner = owner;
dialog.WindowStartupLocation = WindowStartupLocation.CenterOwner;
}
else
{
dialog.WindowStartupLocation = WindowStartupLocation.CenterScreen;
}
dialog.ShowDialog();
return dialog.Result;
}
}
}
+114
View File
@@ -0,0 +1,114 @@
<Window x:Class="FancyInput.AppMessageBoxWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="提示"
Width="360"
MinWidth="360"
MinHeight="170"
SizeToContent="Height"
ResizeMode="NoResize"
WindowStyle="SingleBorderWindow"
ShowInTaskbar="False"
Background="#FFF8F9FB">
<Grid Margin="12">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="8"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="10"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid Grid.Row="0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="40"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Border x:Name="IconCircle"
Width="30"
Height="30"
CornerRadius="15"
Background="#FF4E8DD8"
VerticalAlignment="Top"
HorizontalAlignment="Left">
<TextBlock x:Name="IconGlyph"
Text="i"
Foreground="White"
FontWeight="SemiBold"
FontSize="16"
HorizontalAlignment="Center"
VerticalAlignment="Center"/>
</Border>
<TextBlock x:Name="CaptionText"
Grid.Column="1"
Text="提示"
Foreground="#FF2A2A2A"
FontSize="20"
FontWeight="SemiBold"
VerticalAlignment="Center"/>
</Grid>
<Border Grid.Row="2"
Background="White"
BorderBrush="#FFDDE3EA"
BorderThickness="1"
CornerRadius="10"
Padding="12">
<ScrollViewer x:Name="MessageScrollViewer"
VerticalScrollBarVisibility="Auto"
HorizontalScrollBarVisibility="Disabled"
MaxHeight="160"
Style="{StaticResource SimpleScrollViewerStyle}">
<TextBlock x:Name="MessageText"
TextWrapping="Wrap"
LineHeight="22"
Foreground="#FF2F3136"
FontSize="14"/>
</ScrollViewer>
</Border>
<Border Grid.Row="4"
Background="White"
BorderBrush="#FFE3E7EE"
BorderThickness="1"
CornerRadius="10"
Padding="8">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
<Button x:Name="CopyDetailsBottomButton"
Content="复制详情"
Height="28"
MinWidth="64"
Margin="0,0,6,0"
FontSize="12"
Style="{StaticResource RoundCornerButton}"
Click="CopyDetailsButton_Click"
Visibility="Collapsed"/>
<Button x:Name="Button1"
Width="78"
Height="30"
Margin="0,0,6,0"
FontSize="13"
Style="{StaticResource RoundCornerButton}"
Click="Button_Click"
Visibility="Collapsed"/>
<Button x:Name="Button2"
Width="78"
Height="30"
Margin="0,0,6,0"
FontSize="13"
Style="{StaticResource RoundCornerButton}"
Click="Button_Click"
Visibility="Collapsed"/>
<Button x:Name="Button3"
Width="78"
Height="30"
FontSize="13"
Style="{StaticResource RoundCornerButton}"
Click="Button_Click"
Visibility="Collapsed"/>
</StackPanel>
</Border>
</Grid>
</Window>
+242
View File
@@ -0,0 +1,242 @@
using System.ComponentModel;
using System.Runtime.InteropServices;
using System.Threading;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
namespace FancyInput
{
public partial class AppMessageBoxWindow : Window
{
private const uint CfuNicodeText = 13;
private const uint GmemMoveable = 0x0002;
private readonly MessageBoxButton _buttons;
private readonly string _message;
private readonly bool _isErrorDetails;
private bool _hasExplicitResult;
public MessageBoxResult Result { get; private set; } = MessageBoxResult.None;
public AppMessageBoxWindow(string message, string caption, MessageBoxButton buttons, MessageBoxImage icon)
{
InitializeComponent();
_buttons = buttons;
_message = message;
_isErrorDetails = icon == MessageBoxImage.Error || icon == MessageBoxImage.Stop || icon == MessageBoxImage.Hand;
Title = string.IsNullOrWhiteSpace(caption) ? "提示" : caption;
CaptionText.Text = Title;
MessageText.Text = message;
ApplyIcon(icon);
ConfigureMessagePanel();
BuildButtons(buttons);
}
private void ConfigureMessagePanel()
{
CopyDetailsBottomButton.Visibility = _isErrorDetails ? Visibility.Visible : Visibility.Collapsed;
}
private void ApplyIcon(MessageBoxImage icon)
{
if (icon == MessageBoxImage.Error || icon == MessageBoxImage.Stop || icon == MessageBoxImage.Hand)
{
IconCircle.Background = new SolidColorBrush(Color.FromRgb(198, 74, 66));
IconGlyph.Text = "x";
return;
}
if (icon == MessageBoxImage.Warning || icon == MessageBoxImage.Exclamation)
{
IconCircle.Background = new SolidColorBrush(Color.FromRgb(224, 153, 58));
IconGlyph.Text = "!";
return;
}
if (icon == MessageBoxImage.Information || icon == MessageBoxImage.Asterisk)
{
IconCircle.Background = new SolidColorBrush(Color.FromRgb(78, 141, 216));
IconGlyph.Text = "i";
return;
}
if (icon == MessageBoxImage.Question)
{
IconCircle.Background = new SolidColorBrush(Color.FromRgb(101, 31, 101));
IconGlyph.Text = "?";
return;
}
IconCircle.Background = new SolidColorBrush(Color.FromRgb(122, 130, 140));
IconGlyph.Text = "i";
}
private void BuildButtons(MessageBoxButton buttons)
{
ShowButton(Button1, Visibility.Collapsed, string.Empty, MessageBoxResult.None);
ShowButton(Button2, Visibility.Collapsed, string.Empty, MessageBoxResult.None);
ShowButton(Button3, Visibility.Collapsed, string.Empty, MessageBoxResult.None);
switch (buttons)
{
case MessageBoxButton.OK:
ShowButton(Button3, Visibility.Visible, "确定", MessageBoxResult.OK);
Button3.IsDefault = true;
Button3.IsCancel = true;
break;
case MessageBoxButton.OKCancel:
ShowButton(Button2, Visibility.Visible, "取消", MessageBoxResult.Cancel);
ShowButton(Button3, Visibility.Visible, "确定", MessageBoxResult.OK);
Button3.IsDefault = true;
Button2.IsCancel = true;
break;
case MessageBoxButton.YesNo:
ShowButton(Button2, Visibility.Visible, "否", MessageBoxResult.No);
ShowButton(Button3, Visibility.Visible, "是", MessageBoxResult.Yes);
Button3.IsDefault = true;
break;
case MessageBoxButton.YesNoCancel:
ShowButton(Button1, Visibility.Visible, "取消", MessageBoxResult.Cancel);
ShowButton(Button2, Visibility.Visible, "否", MessageBoxResult.No);
ShowButton(Button3, Visibility.Visible, "是", MessageBoxResult.Yes);
Button3.IsDefault = true;
Button1.IsCancel = true;
break;
}
}
private static void ShowButton(Button button, Visibility visibility, string text, MessageBoxResult result)
{
button.Visibility = visibility;
button.Content = text;
button.Tag = result;
button.IsDefault = false;
button.IsCancel = false;
}
private void Button_Click(object sender, RoutedEventArgs e)
{
if (sender is Button button && button.Tag is MessageBoxResult result)
{
Result = result;
_hasExplicitResult = true;
}
DialogResult = true;
Close();
}
private void CopyDetailsButton_Click(object sender, RoutedEventArgs e)
{
if (TrySetClipboardTextWin32(_message))
{
CopyDetailsBottomButton.Content = "已复制";
}
else
{
CopyDetailsBottomButton.Content = "复制失败";
}
}
private static bool TrySetClipboardTextWin32(string text)
{
for (int attempt = 0; attempt < 2; attempt++)
{
if (!OpenClipboard(IntPtr.Zero))
{
Thread.Sleep(20);
continue;
}
try
{
if (!EmptyClipboard())
{
continue;
}
int byteCount = (text.Length + 1) * 2;
IntPtr hGlobal = GlobalAlloc(GmemMoveable, (UIntPtr)byteCount);
if (hGlobal == IntPtr.Zero)
{
continue;
}
IntPtr pGlobal = GlobalLock(hGlobal);
if (pGlobal == IntPtr.Zero)
{
GlobalFree(hGlobal);
continue;
}
try
{
Marshal.Copy(text.ToCharArray(), 0, pGlobal, text.Length);
Marshal.WriteInt16(pGlobal, text.Length * 2, 0);
}
finally
{
GlobalUnlock(hGlobal);
}
if (SetClipboardData(CfuNicodeText, hGlobal) == IntPtr.Zero)
{
GlobalFree(hGlobal);
continue;
}
return true;
}
finally
{
CloseClipboard();
}
}
return false;
}
[DllImport("user32.dll", SetLastError = true)]
private static extern bool OpenClipboard(IntPtr hWndNewOwner);
[DllImport("user32.dll", SetLastError = true)]
private static extern bool CloseClipboard();
[DllImport("user32.dll", SetLastError = true)]
private static extern bool EmptyClipboard();
[DllImport("user32.dll", SetLastError = true)]
private static extern IntPtr SetClipboardData(uint uFormat, IntPtr hMem);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern IntPtr GlobalAlloc(uint uFlags, UIntPtr dwBytes);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern IntPtr GlobalLock(IntPtr hMem);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool GlobalUnlock(IntPtr hMem);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern IntPtr GlobalFree(IntPtr hMem);
protected override void OnClosing(CancelEventArgs e)
{
if (!_hasExplicitResult)
{
Result = _buttons switch
{
MessageBoxButton.OK => MessageBoxResult.OK,
MessageBoxButton.OKCancel => MessageBoxResult.Cancel,
MessageBoxButton.YesNo => MessageBoxResult.No,
MessageBoxButton.YesNoCancel => MessageBoxResult.Cancel,
_ => MessageBoxResult.None,
};
}
base.OnClosing(e);
}
}
}
+10
View File
@@ -0,0 +1,10 @@
using System.Windows;
[assembly: ThemeInfo(
ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located
//(used if a resource is not found in the page,
// or application resource dictionaries)
ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located
//(used if a resource is not found in the page,
// app, or any theme specific resource dictionaries)
)]
+26
View File
@@ -0,0 +1,26 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
namespace FancyInput.Models
{
public static class ButtonHelper
{
public static readonly DependencyProperty CornerRadiusProperty =
DependencyProperty.RegisterAttached(
"CornerRadius",
typeof(CornerRadius),
typeof(ButtonHelper),
new PropertyMetadata(new CornerRadius(4)));
public static void SetCornerRadius(UIElement element, CornerRadius value)
=> element.SetValue(CornerRadiusProperty, value);
public static CornerRadius GetCornerRadius(UIElement element)
=> (CornerRadius)element.GetValue(CornerRadiusProperty);
}
}
+48
View File
@@ -0,0 +1,48 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Data;
using System.Windows.Media;
namespace FancyInput.Models
{
public class ScaleToTransformConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
double scale = 1.0;
if (value is double d && !double.IsNaN(d) && !double.IsInfinity(d) && d > 0)
{
scale = d;
}
return new ScaleTransform(scale, scale);
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
return Binding.DoNothing;
}
}
public class AngleToRotateTransformConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
double angle = 0;
if (value is double d)
angle = d;
return new RotateTransform(angle);
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is RotateTransform rt)
return rt.Angle;
return 0.0;
}
}
}
+320
View File
@@ -0,0 +1,320 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace FancyInput.Models
{
public static class EnumExtensions
{
public static string GetDescription(this Enum value)
{
var field = value.GetType().GetField(value.ToString());
if (field != null && Attribute.GetCustomAttribute(field, typeof(DescriptionAttribute)) is DescriptionAttribute attr)
{
return attr.Description;
}
return value.ToString();
}
}
public enum GamepadBackend
{
SDL = 0,
XInput = 1
}
public enum MousMoveMode
{
[Description("绝对坐标")]
Absolute,
[Description("相对坐标")]
Relative
}
public enum MouseWheelDirection
{
[Description("向上")]
Up,
[Description("向下")]
Down
}
public enum MacroAddDirection
{
[Description("右侧")]
Right,
[Description("左侧")]
Left
}
public enum InputDevice
{
[Description("键盘")]
Keyboard,
[Description("鼠标按键")]
MouseButton,
[Description("鼠标滚轮")]
MouseWheel,
[Description("鼠标移动")]
MouseMove,
[Description("XInput游戏手柄按键")]
XInputButton,
[Description("XInput游戏手柄扳机")]
XInputTrigger,
[Description("XInput游戏手柄摇杆")]
XInputStick,
[Description("SDL游戏手柄按键")]
SDLButton,
[Description("SDL游戏手柄扳机")]
SDLTrigger,
[Description("SDL游戏手柄摇杆")]
SDLStick,
[Description("SDL游戏手柄Home")]
SDLGuide,
}
public enum MoveState
{
StartMove,
Moving,
StopMove
}
[Flags]
public enum FIPGamepadButtonflags : ushort
{
None = 0,
Up = 1,
Down = 2,
Left = 4,
Right = 8,
Start = 0x10,
Select = 0x20,
LS = 0x40,
RS = 0x80,
LB = 0x100,
RB = 0x200,
A = 0x1000,
B = 0x2000,
X = 0x4000,
Y = 0x8000
}
public enum ElementType
{
Texture = 0,
KeyboardButton = 1,
GamepadButton = 2,
MouseButton = 3,
MouseWheel = 4,
AnalogStick = 5,
GamepadTrigger = 6,
GamepadPlayerId = 7,
DPad = 8,
MouseMovement = 9,
}
public enum GamepadCodeType
{
A = 0,
B = 1,
X = 2,
Y = 3,
Select = 4,
Start = 6,
LB = 9,
RB = 10,
Up = 11,
Down = 12,
Left = 13,
Right = 14,
// hitbox legacy codes (0xEC00 + n)
HitBox_B = 60416, // id: B
HitBox_A = 60417, // id: A
HitBox_Y = 60418, // id: Y
HitBox_X = 60419, // id: X
HitBox_LB = 60420, // id: LS
HitBox_RB = 60421, // id: RS
HitBox_Select = 60422, // id: SELECT
HitBox_Start = 60423, // id: START
HitBox_LS = 60425, // id: LS
HitBox_RS = 60426, // id: RS
HitBox_Left = 60427, // id: LEFT
HitBox_Right = 60428, // id: RIGHT
HitBox_Up = 60429, // id: UP
HitBox_Down = 60430, // id: DOWN
}
public enum GamepadTriggerType
{
Left = 0,
Right = 1,
}
public enum GamepadThumbType
{
LeftThumb = 0,
RightThumb = 1,
}
public enum GamepadUserIdx
{
One = 0,
Two = 1,
Three = 2,
Four = 3
}
public enum MouseCodeType
{
None = 0,
Left = 1,
Right = 2,
Middle = 3,
XButton1 = 4,
XButton2 = 5,
}
public enum Direction
{
Up = 1,
Down = 2,
Left = 3,
Right = 4
}
public enum Side
{
Left = 0,
Right = 1
}
public enum ExportType
{
PngAndJson,
ProjectFile
}
public enum MainWindowPanelType
{
Read,
Create,
Manage,
Workshop,
Settings
}
public enum MouseMoveType
{
Dot = 0,
Arrow = 1
}
public enum WindowCloseType
{
Save = 0,
Discard = 1
}
public enum ButtonState
{
Released = 0,
Pressed = 1
}
public enum KeyBoardMappingType
{
Windows,
BIOS
}
public enum KeyBoardCodeType
{
None = 0, Escape = 1, D1 = 2, D2 = 3, D3 = 4, D4 = 5, D5 = 6, D6 = 7, D7 = 8, D8 = 9, D9 = 10, D0 = 11,
OemMinus = 12, Oemplus = 13, Back = 14, Tab = 15, Q = 16, W = 17, E = 18, R = 19, T = 20, Y = 21, U = 22, I = 23, O = 24, P = 25,
OemOpenBrackets = 26, OemCloseBrackets = 27, Enter = 28, LControlKey = 29, A = 30, S = 31, D = 32, F = 33, G = 34, H = 35,
J = 36, K = 37, L = 38, Oem1 = 39, Oem7 = 40, Oem3 = 41, LShiftKey = 42, Oem5 = 43, Z = 44, X = 45, C = 46, V = 47, B = 48,
N = 49, M = 50, Oemcomma = 51, OemPeriod = 52, Oem2 = 53, RShiftKey = 54, Divide = 3637, Multiply = 55, LMenu = 56, Space = 57, CapsLock = 58,
F1 = 59, F2 = 60, F3 = 61, F4 = 62, F5 = 63, F6 = 64, F7 = 65, F8 = 66, F9 = 67, F10 = 68, NumLock = 69, ScrollLock = 70,
NumPad7 = 71, NumPad8 = 72, NumPad9 = 73, Subtract = 74, NumPad4 = 75, NumPad5 = 76, NumPad6 = 77, Add = 78, NumPad1 = 79, NumPad2 = 80,
NumPad3 = 81, NumPad0 = 82, Decimal = 83, NumpadEquals = 3597, F11 = 87, F12 = 88, F13 = 91, F14 = 92, F15 = 93,
F16 = 99, F17 = 100, F18 = 101, F19 = 102, F20 = 103, F21 = 104, F22 = 105, F23 = 106, F24 = 107,
PrintScreen = 3639, Oem102 = 3654, RMenu = 3640, NumpadEnter = 3612, RControlKey = 3613,
Pause = 3653, Home = 3655, PageUp = 3657, End = 3663, PageDown = 3665, Insert = 3666, Delete = 3667, Up = 57416, Left = 57419,
Right = 57421, Down = 57424, LWin = 3675, RWin = 3676, Apps = 3677,
Power = 57438, Sleep = 57439, Wake = 57443,
MediaPlay = 57378, MediaStop = 57380, MediaPrevious = 57360, MediaNext = 57369, MediaSelect = 57453, MediaEject = 57388,
VolumeMute = 57376, VolumeUp = 57392, VolumeDown = 57390,
AppMail = 57452, AppCalculator = 57377, AppMusic = 57404, AppPictures = 57444,
BrowserSearch = 57445, BrowserHome = 57394, BrowserBack = 57450, BrowserForward = 57449, BrowserStop = 57448,
BrowserRefresh = 57447, BrowserFavorites = 57446,
Katakana = 112, Underscore = 115, Furigana = 119, Kanji = 121, Hiragana = 123, Yen = 125, NumpadComma = 126,
SunHelp = 65397, SunStop = 65400, SunProps = 65398, SunFront = 65399, SunOpen = 65396, SunFind = 65406,
SunAgain = 65401, SunUndo = 65402, SunCopy = 65404, SunInsert = 65405, SunCut = 65403
}
public enum FipKeys : ushort
{
None = 0x00, Escape = 0x1B, D1 = 0x31, D2 = 0x32, D3 = 0x33, D4 = 0x34, D5 = 0x35, D6 = 0x36, D7 = 0x37, D8 = 0x38, D9 = 0x39, D0 = 0x30,
OemMinus = 0xBD, Oemplus = 0xBB, Back = 0x08, Tab = 0x09, Q = 0x51, W = 0x57, E = 0x45, R = 0x52, T = 0x54, Y = 0x59, U = 0x55, I = 0x49, O = 0x4F, P = 0x50,
OemOpenBrackets = 0xDB, OemCloseBrackets = 0xDD, Enter = 0x1000, LControlKey = 0xA2, A = 0x41, S = 0x53, D = 0x44, F = 0x46, G = 0x47, H = 0x48,
J = 0x4A, K = 0x4B, L = 0x4C, Oem1 = 0xBA, Oem7 = 0xDE, Oem3 = 0xC0, Oem102 = 0xE2, LShiftKey = 0xA0, Oem5 = 0xDC, Z = 0x5A, X = 0x58, C = 0x43, V = 0x56, B = 0x42,
N = 0x4E, M = 0x4D, Oemcomma = 0xBC, OemPeriod = 0xBE, Oem2 = 0xBF, RShiftKey = 0xA1, Divide = 0x6F, Multiply = 0x6A, LMenu = 0xA4, Space = 0x20, CapsLock = 0x14,
F1 = 0x70, F2 = 0x71, F3 = 0x72, F4 = 0x73, F5 = 0x74, F6 = 0x75, F7 = 0x76, F8 = 0x77, F9 = 0x78, F10 = 0x79, NumLock = 0x90, ScrollLock = 0x91,
NumPad7 = 0x67, NumPad8 = 0x68, NumPad9 = 0x69, Subtract = 0x6D, NumPad4 = 0x64, NumPad5 = 0x65, NumPad6 = 0x66, Add = 0x6B, NumPad1 = 0x61, NumPad2 = 0x62,
NumPad3 = 0x63, NumPad0 = 0x60, Decimal = 0x6E, NumpadEquals = 0x92, F11 = 0x7A, F12 = 0x7B,
F13 = 0x7C, F14 = 0x7D, F15 = 0x7E, F16 = 0x7F, F17 = 0x80, F18 = 0x81, F19 = 0x82, F20 = 0x83, F21 = 0x84, F22 = 0x85, F23 = 0x86, F24 = 0x87,
PrintScreen = 0x2C, RMenu = 0xA5, NumpadEnter = 0x1001, RControlKey = 0xA3,
Pause = 0x13, Home = 0x24, PageUp = 0x21, End = 0x23, PageDown = 0x22, Insert = 0x2D, Delete = 0x2E, Up = 0x26, Left = 0x25,
Right = 0x27, Down = 0x28, LWin = 0x5B, RWin = 0x5C, Apps = 0x5D, Sleep = 0x5F, Wake = 0xE3,
MediaNext = 0xB0, MediaPrevious = 0xB1, MediaStop = 0xB2, MediaPlay = 0xB3, MediaSelect = 0xB5,
VolumeMute = 0xAD, VolumeUp = 0xAF, VolumeDown = 0xAE,
AppMail = 0xB4, AppMusic = 0xB6, AppPictures = 0xB7, AppCalculator = 0xB7,
BrowserBack = 0xA6, BrowserForward = 0xA7, BrowserRefresh = 0xA8, BrowserStop = 0xA9, BrowserSearch = 0xAA, BrowserFavorites = 0xAB, BrowserHome = 0xAC
}
public enum FipRawKeys : uint
{
None = 0x0000, Escape = 0x01, D1 = 0x02, D2 = 0x03, D3 = 0x04, D4 = 0x05, D5 = 0x06, D6 = 0x07, D7 = 0x08, D8 = 0x09, D9 = 0x0A, D0 = 0x0B,
OemMinus = 0x0C, Oemplus = 0x0D, Back = 0x0E, Tab = 0x0F, Q = 0x10, W = 0x11, E = 0x12, R = 0x13, T = 0x14, Y = 0x15, U = 0x16, I = 0x17, O = 0x18, P = 0x19,
OemOpenBrackets = 0x1A, OemCloseBrackets = 0x1B, Enter = 0x1C, LControlKey = 0x1D, A = 0x1E, S = 0x1F, D = 0x20, F = 0x21, G = 0x22, H = 0x23,
J = 0x24, K = 0x25, L = 0x26, Oem1 = 0x27, Oem7 = 0x28, Oem3 = 0x29, Oem102 = 0x56, LShiftKey = 0x2A, Oem5 = 0x2B, Z = 0x2C, X = 0x2D, C = 0x2E, V = 0x2F, B = 0x30,
N = 0x31, M = 0x32, Oemcomma = 0x33, OemPeriod = 0x34, Oem2 = 0x35, RShiftKey = 0x36, Divide = 0xE035, Multiply = 0x37, LMenu = 0x38, Space = 0x39, CapsLock = 0x3A,
F1 = 0x3B, F2 = 0x3C, F3 = 0x3D, F4 = 0x3E, F5 = 0x3F, F6 = 0x40, F7 = 0x41, F8 = 0x42, F9 = 0x43, F10 = 0x44, NumLock = 0x45, ScrollLock = 0x46,
NumPad7 = 0x47, NumPad8 = 0x48, NumPad9 = 0x49, Subtract = 0x4A, NumPad4 = 0x4B, NumPad5 = 0x4C, NumPad6 = 0x4D, Add = 0x4E, NumPad1 = 0x4F, NumPad2 = 0x50,
NumPad3 = 0x51, NumPad0 = 0x52, Decimal = 0x53, NumpadEquals = 0xE00D, F11 = 0x57, F12 = 0x58,
F13 = 0x5B, F14 = 0x5C, F15 = 0x5D, F16 = 0x63, F17 = 0x64, F18 = 0x65, F19 = 0x66, F20 = 0x67, F21 = 0x68, F22 = 0x69, F23 = 0x6A, F24 = 0x6B,
PrintScreen = 0xE037, RMenu = 0xE038, NumpadEnter = 0xE01C, RControlKey = 0xE01D,
Pause = 0xE11D45, Home = 0xE047, PageUp = 0xE049, End = 0xE04F, PageDown = 0xE051, Insert = 0xE052, Delete = 0xE053, Up = 0xE048, Left = 0xE04B,
Right = 0xE04D, Down = 0xE050, LWin = 0xE05B, RWin = 0xE05C, Apps = 0xE05D,
Power = 0xE05E, Sleep = 0xE05F, Wake = 0xE063,
MediaPlay = 0xE022, MediaStop = 0xE024, MediaPrevious = 0xE010, MediaNext = 0xE019, MediaSelect = 0xE06D, MediaEject = 0xE02C,
VolumeMute = 0xE020, VolumeUp = 0xE030, VolumeDown = 0xE02E,
AppMail = 0xE06C, AppCalculator = 0xE021, AppMusic = 0xE03C, AppPictures = 0xE064,
BrowserSearch = 0xE065, BrowserHome = 0xE032, BrowserBack = 0xE06A, BrowserForward = 0xE069, BrowserStop = 0xE068, BrowserRefresh = 0xE067, BrowserFavorites = 0xE066,
Katakana = 0x70, Underscore = 0x73, Furigana = 0x77, Kanji = 0x79, Hiragana = 0x7B, Yen = 0x7D, NumpadComma = 0x7E,
SunHelp = 0xFF75, SunStop = 0xFF78, SunProps = 0xFF76, SunFront = 0xFF77, SunOpen = 0xFF74, SunFind = 0xFF7E,
SunAgain = 0xFF79, SunUndo = 0xFF7A, SunCopy = 0xFF7C, SunInsert = 0xFF7D, SunCut = 0xFF7B
}
}
+130
View File
@@ -0,0 +1,130 @@
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
namespace FancyInput.Common
{
/// <summary>
/// 附加行为:让 TextBox 随鼠标滚轮增减数值。
/// 无需 code-behind,在 XAML 中一行启用。
/// </summary>
public static class IntegerTextBoxBehavior
{
public static readonly DependencyProperty IsEnabledProperty =
DependencyProperty.RegisterAttached("IsEnabled", typeof(bool),
typeof(IntegerTextBoxBehavior),
new PropertyMetadata(false, OnIsEnabledChanged));
public static bool GetIsEnabled(DependencyObject obj) =>
(bool)obj.GetValue(IsEnabledProperty);
public static void SetIsEnabled(DependencyObject obj, bool value) =>
obj.SetValue(IsEnabledProperty, value);
public static readonly DependencyProperty AllowNegativeProperty =
DependencyProperty.RegisterAttached("AllowNegative", typeof(bool),
typeof(IntegerTextBoxBehavior),
new PropertyMetadata(false));
public static bool GetAllowNegative(DependencyObject obj) =>
(bool)obj.GetValue(AllowNegativeProperty);
public static void SetAllowNegative(DependencyObject obj, bool value) =>
obj.SetValue(AllowNegativeProperty, value);
/// 允许输入零,默认允许(暂不启用)
public static readonly DependencyProperty AllowZroProperty =
DependencyProperty.RegisterAttached("AllowZero", typeof(bool),
typeof(IntegerTextBoxBehavior),
new PropertyMetadata(true));
public static bool GetAllowZero(DependencyObject obj) =>
(bool)obj.GetValue(AllowZroProperty);
public static void SetAllowZero(DependencyObject obj, bool value) =>
obj.SetValue(AllowZroProperty, value);
public static readonly DependencyProperty SpinIncrementProperty =
DependencyProperty.RegisterAttached("SpinIncrement", typeof(int),
typeof(IntegerTextBoxBehavior),
new PropertyMetadata(1));
public static int GetSpinIncrement(DependencyObject obj) =>
(int)obj.GetValue(SpinIncrementProperty);
public static void SetSpinIncrement(DependencyObject obj, int value) =>
obj.SetValue(SpinIncrementProperty, value);
public static readonly DependencyProperty MinValueProperty =
DependencyProperty.RegisterAttached("MinValue", typeof(int?),
typeof(IntegerTextBoxBehavior),
new PropertyMetadata(null));
private static void OnIsEnabledChanged(DependencyObject d,
DependencyPropertyChangedEventArgs e)
{
if (d is not TextBox tb) return;
if ((bool)e.NewValue)
{
InputMethod.SetIsInputMethodEnabled(tb, false);
tb.PreviewTextInput += OnpPreviewTextInput;
tb.PreviewMouseWheel += OnPreviewMouseWheel;
DataObject.AddPastingHandler(tb, OnPasting);
}
else
{
InputMethod.SetIsInputMethodEnabled(tb, true);
tb.PreviewTextInput -= OnpPreviewTextInput;
tb.PreviewMouseWheel -= OnPreviewMouseWheel;
DataObject.RemovePastingHandler(tb, OnPasting);
}
}
private static void OnpPreviewTextInput(object sender, TextCompositionEventArgs e)
{
if (sender is TextBox tb)
{
string oldText = tb.Text;
oldText = oldText.Remove(tb.SelectionStart, tb.SelectionLength);
string newText = oldText.Insert(tb.SelectionStart, e.Text);
bool allowNegative = GetAllowNegative(tb);
// return 表示输入合法,放行;否则 e.Handled = true,阻止输入
if (allowNegative && oldText.Length == 0 && e.Text == "-") return;
if (int.TryParse(newText, out int value) && (allowNegative || value >= 0)) return;
e.Handled = true;
}
}
private static void OnPasting(object sender, DataObjectPastingEventArgs e)
{
if (e.DataObject.GetDataPresent(typeof(string)))
{
if (sender is TextBox tb)
{
string pastedText = (string)e.DataObject.GetData(typeof(string));
string oldText = tb.Text;
oldText = oldText.Remove(tb.SelectionStart, tb.SelectionLength);
string newText = oldText.Insert(tb.SelectionStart, pastedText);
bool allowNegative = GetAllowNegative(tb);
// return 表示输入合法,放行;否则 e.CancelCommand(),取消粘贴
if (allowNegative && oldText.Length == 0 && pastedText == "-") return;
if (int.TryParse(newText, out int value) && (allowNegative || value >= 0)) return;
}
}
e.CancelCommand();
}
private static void OnPreviewMouseWheel(object sender, MouseWheelEventArgs e)
{
if (sender is not TextBox tb) return;
if (!int.TryParse(tb.Text, out int cur)) return;
int inc = GetSpinIncrement(tb);
int next = cur + (e.Delta > 0 ? inc : -inc);
if (next < 0 && !GetAllowNegative(tb)) next = 0;
if (next == cur) return;
tb.Text = next.ToString();
e.Handled = true; // 阻止 ScrollViewer 消费事件
}
}
}
File diff suppressed because one or more lines are too long
+133
View File
@@ -0,0 +1,133 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
namespace FancyInput.Common
{
public class Utility
{
public static bool TrySetClipboardText(string text, out string errorDetail)
{
if (TrySetClipboardTextViaWin32(text, out var win32Error))
{
errorDetail = string.Empty;
return true;
}
errorDetail = win32Error == 0 ? "Win32 未知错误" : $"Win32 0x{win32Error:X8}";
return false;
}
private static bool TrySetClipboardTextViaWin32(string text, out int win32Error)
{
const int maxOpenRetries = 12;
const int openRetryDelayMs = 10;
const uint cfUnicodeText = 13;
const uint gmemMoveable = 0x0002;
const uint gmemZeroInit = 0x0040;
win32Error = 0;
var opened = false;
var hGlobal = IntPtr.Zero;
try
{
for (var i = 0; i < maxOpenRetries; i++)
{
if (OpenClipboard(IntPtr.Zero))
{
opened = true;
break;
}
win32Error = Marshal.GetLastWin32Error();
Thread.Sleep(openRetryDelayMs);
}
if (!opened)
{
return false;
}
if (!EmptyClipboard())
{
win32Error = Marshal.GetLastWin32Error();
return false;
}
var bytes = Encoding.Unicode.GetBytes(text + '\0');
hGlobal = GlobalAlloc(gmemMoveable | gmemZeroInit, (UIntPtr)bytes.Length);
if (hGlobal == IntPtr.Zero)
{
win32Error = Marshal.GetLastWin32Error();
return false;
}
var target = GlobalLock(hGlobal);
if (target == IntPtr.Zero)
{
win32Error = Marshal.GetLastWin32Error();
return false;
}
try
{
Marshal.Copy(bytes, 0, target, bytes.Length);
}
finally
{
GlobalUnlock(hGlobal);
}
if (SetClipboardData(cfUnicodeText, hGlobal) == IntPtr.Zero)
{
win32Error = Marshal.GetLastWin32Error();
return false;
}
// Ownership has transferred to the clipboard.
hGlobal = IntPtr.Zero;
return true;
}
finally
{
if (opened)
{
CloseClipboard();
}
if (hGlobal != IntPtr.Zero)
{
GlobalFree(hGlobal);
}
}
}
[DllImport("user32.dll", SetLastError = true)]
private static extern bool OpenClipboard(IntPtr hWndNewOwner);
[DllImport("user32.dll", SetLastError = true)]
private static extern bool CloseClipboard();
[DllImport("user32.dll", SetLastError = true)]
private static extern bool EmptyClipboard();
[DllImport("user32.dll", SetLastError = true)]
private static extern IntPtr SetClipboardData(uint uFormat, IntPtr hMem);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern IntPtr GlobalAlloc(uint uFlags, UIntPtr dwBytes);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern IntPtr GlobalLock(IntPtr hMem);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool GlobalUnlock(IntPtr hMem);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern IntPtr GlobalFree(IntPtr hMem);
}
}
+47
View File
@@ -0,0 +1,47 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net8.0-windows</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<UseWPF>true</UseWPF>
<Platforms>AnyCPU;x64</Platforms>
<ApplicationIcon>Resources\Icons\Icon.ico</ApplicationIcon>
<Version>1.2.0</Version>
<AssemblyTitle>FancyInput</AssemblyTitle>
<Product>FancyInput</Product>
<Copyright>Copyright (c) 2026 XLworkspace.</Copyright>
</PropertyGroup>
<ItemGroup>
<None Remove="Resources\QRCodes\bilibili.jpg" />
<None Remove="Resources\QRCodes\wechat.jpg" />
<None Remove="Resources\QRCodes\xlworkspace.jpg" />
</ItemGroup>
<ItemGroup>
<Content Include="Resources\Icons\Icon.ico" />
</ItemGroup>
<ItemGroup>
<Resource Include="Resources\Icons\Icon.png" />
<Resource Include="Resources\QRCodes\bilibili.jpg" />
<Resource Include="Resources\QRCodes\wechat.jpg" />
<Resource Include="Resources\QRCodes\xlworkspace.jpg" />
</ItemGroup>
<ItemGroup>
<Folder Include="Images\" />
<Folder Include="Views\Pages\" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Hardcodet.NotifyIcon.Wpf" Version="2.0.1" />
<PackageReference Include="MouseKeyHook" Version="5.7.1" />
<PackageReference Include="SDL3-CS" Version="3.3.7" />
<PackageReference Include="SDL3-CS.Native" Version="3.3.7" />
<PackageReference Include="SharpDX.XInput" Version="4.2.0" />
<PackageReference Include="WpfAnimatedGif" Version="2.0.2" />
</ItemGroup>
</Project>
+132
View File
@@ -0,0 +1,132 @@
<Window x:Class="FancyInput.FancyInputMainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:FancyInput"
xmlns:V="clr-namespace:FancyInput.Views"
xmlns:vm ="clr-namespace:FancyInput.ViewModels"
xmlns:md="clr-namespace:FancyInput.Models"
xmlns:tb="http://www.hardcodet.net/taskbar"
xmlns:uc="clr-namespace:FancyInput.Views.Controls"
xmlns:c="clr-namespace:FancyInput.Common"
mc:Ignorable="d"
d:DataContext="{d:DesignInstance Type=vm:MainWindowViewModel}"
xmlns:v="clr-namespace:FancyInput.Views"
d:Height="500"
Title="FancyInput" Height="480" Width="600" InputMethod.IsInputMethodEnabled="False"
Closed="Window_Closed" WindowStartupLocation="CenterScreen"
MinHeight="450" MinWidth="600"
>
<Grid x:Name="MainGrid" Background="AliceBlue">
<Grid.RowDefinitions>
<RowDefinition Height="180"/>
<RowDefinition Height="800*"/>
<RowDefinition Height="30"/>
</Grid.RowDefinitions>
<tb:TaskbarIcon x:Name="NotifyIcon" IconSource="/Resources/Icons/Icon.png" ToolTipText="FancyInput"
Visibility="{Binding TrayIconVisibility}" TrayLeftMouseDown="ShowMainWindow">
<tb:TaskbarIcon.ContextMenu>
<ContextMenu>
<MenuItem Style="{StaticResource ContextSimpleMenuItem}" Header="退出" Click="Exit_Click"/>
</ContextMenu>
</tb:TaskbarIcon.ContextMenu>
</tb:TaskbarIcon>
<StackPanel Orientation="Vertical" Grid.Row="0" >
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Margin="10,10">
<Label Content="Fancy" FontSize="80" FontWeight="Bold" Foreground="#FF651F65" FontFamily="Cascadia Mono" Padding="0"/>
<Label Content="Input" FontSize="80" FontFamily="Cascadia Mono" Padding="0"/>
</StackPanel>
<Grid Margin="20,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="300*"/>
<ColumnDefinition Width="300*"/>
<ColumnDefinition Width="300*"/>
<ColumnDefinition Width="300*"/>
<ColumnDefinition Width="300*"/>
</Grid.ColumnDefinitions>
<Button x:Name="ReadSelectButton" Margin="10" FontFamily="Cascadia Mono" FontSize="30" Grid.Column="0"
Style="{Binding ReadButtonStyle}" Tag="{x:Static md:MainWindowPanelType.Read}" Click="ChangePanel">
<StackPanel Orientation="Horizontal">
<uc:IconButton PathData="{x:Static c:PathDataGeometry.OpenBox}"
PathFill="White" PathStroke="White" PathStrokeThickness="10"
Width="25" Height="25" IsHitTestVisible="False"/>
<Label Content="读取" Foreground="White" FontSize="20"/>
</StackPanel>
</Button>
<Button x:Name="CreateSelectButton" Margin="10" FontFamily="Cascadia Mono" FontSize="30" Click="ChangePanel" Grid.Column="1"
Style="{Binding CreateButtonStyle}" Tag="{x:Static md:MainWindowPanelType.Create}" >
<StackPanel Orientation="Horizontal">
<uc:IconButton PathData="{x:Static c:PathDataGeometry.FolderAdd}"
PathFill="White" PathStroke="White" PathStrokeThickness="1"
Width="25" Height="25" IsHitTestVisible="False"/>
<Label Content="创建" Foreground="White" FontSize="20"/>
</StackPanel>
</Button>
<Button x:Name="ManageSelectButton" Margin="10" FontFamily="Cascadia Mono" FontSize="30" Click="ChangePanel" Grid.Column="2"
Style="{Binding ManageButtonStyle}" Tag="{x:Static md:MainWindowPanelType.Manage}" >
<StackPanel Orientation="Horizontal">
<uc:IconButton PathData="{x:Static c:PathDataGeometry.Database}"
PathFill="White" PathStroke="White" PathStrokeThickness="0.5"
Width="25" Height="25" IsHitTestVisible="False"/>
<Label Content="管理" Foreground="White" FontSize="20"/>
</StackPanel>
</Button>
<Button Margin="10" FontFamily="Cascadia Mono" FontSize="30" Click="ChangePanel" Grid.Column="3"
Style="{Binding WorkshopButtonStyle}" Tag="{x:Static md:MainWindowPanelType.Workshop}" >
<StackPanel Orientation="Horizontal">
<uc:IconButton PathData="{x:Static c:PathDataGeometry.Bulb}"
PathFill="White" PathStroke="White" PathStrokeThickness="1"
Width="25" Height="25" IsHitTestVisible="False"/>
<Label Content="工坊" Foreground="White" FontSize="20"/>
</StackPanel>
</Button>
<Button Margin="10" FontFamily="Cascadia Mono" FontSize="30" Click="ChangePanel" Grid.Column="4"
Style="{Binding SettingsButtonStyle}" Tag="{x:Static md:MainWindowPanelType.Settings}" >
<StackPanel Orientation="Horizontal">
<uc:IconButton PathData="{x:Static c:PathDataGeometry.Gear}"
PathFill="White" PathStroke="White" PathStrokeThickness="1"
Width="25" Height="25" IsHitTestVisible="False"/>
<Label Content="设置" Foreground="White" FontSize="20"/>
</StackPanel>
</Button>
</Grid>
</StackPanel>
<TabControl Grid.Row="1" SelectedIndex="{Binding SelectedPanelIndex,Mode=OneWay}" BorderThickness="0" Background="Transparent">
<TabControl.Resources>
<Style TargetType="TabPanel">
<Setter Property="Visibility" Value="Collapsed"/>
</Style>
</TabControl.Resources>
<TabItem>
<uc:ReadPage PngFilePath="{Binding PngFilePath,Mode=TwoWay}" JsonFilePath="{Binding JsonFilePath,Mode=TwoWay}"
ProjectFilePath="{Binding ProjectFilePath,Mode=TwoWay}"
LoadMode="{Binding LoadMode,Mode=TwoWay}"
ConfigLoadClick="ReadPage_ConfigLoadClick" ConfigEditClick="ReadPage_ConfigEditClick"/>
</TabItem>
<TabItem>
<uc:CreatePage CreateFromEmptyClick="CreatePage_CreateFromEmptyClick"/>
</TabItem>
<TabItem>
<uc:ManagePage OverlayWindowViewModels="{Binding OverlayWindowViewModels,Mode=TwoWay}"
SaveGroupClick="ManagePage_SaveGroupClick" LoadGroupClick="ManagePage_LoadGroupClick" Margin="20,5"
UpMoveClick="ManagePage_UpMoveClick" DownMoveClick="ManagePage_DownMoveClick"/>
</TabItem>
<TabItem>
<uc:WorkshopPanel Margin="20,5" MacroWindowOpenClick="OpenMacroWindow"/>
</TabItem>
<TabItem>
<uc:SettingsPage DataContext="{Binding}" AboutClick="About" Margin="20,5"/>
</TabItem>
</TabControl>
<StackPanel Orientation="Horizontal" Grid.Row="2">
<Viewbox Height="15" Width="15">
<Ellipse Width="100" Height="100" Fill="{Binding DebugCircleFill}"/>
</Viewbox>
<Label Content="{Binding DebugLabelString}" FontFamily="Cascadia Mono" VerticalAlignment="Center"/>
</StackPanel>
</Grid>
</Window>
+729
View File
@@ -0,0 +1,729 @@
using FancyInput.Models;
using FancyInput.ViewModels;
using FancyInput.Views;
using System.Diagnostics;
using System.Net.Http;
using System.Text.Json;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Interop;
using System.Windows.Media;
using System.Reflection;
using System.IO;
using WpfAnimatedGif;
using MessageBox = System.Windows.MessageBox;
using FancyInput.Views.Controls;
using System.Globalization;
using FancyInput.Views.Windows;
using System.Numerics;
namespace FancyInput
{
public static class VersionInfo
{
public static string CurrentVersion
{
get
{
var v = Assembly.GetExecutingAssembly().GetName().Version;
if (v == null) return "1.0.0";
return $"{v.Major}.{v.Minor}.{v.Build}";
}
}
public const string Date = "2026-6-27";
public const string Mail = "contact@xlworkspace.com";
public const string HealthAPI = "http://xlworkspace.com/api/fancyinput/health";
public const string LatestVersionAPI = "http://xlworkspace.com/api/fancyinput/latest-version";
public const string LatestDownloadUrl = "http://xlworkspace.com/api/fancyinput/latest-download-url";
public const string UpdateInfoUrlPrefix = "http://xlworkspace.com/api/fancyinput/update-info/";
public const string DownloadCountAllAPI = "http://xlworkspace.com/api/fancyinput/download-count/all";
}
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class FancyInputMainWindow : Window
{
private bool _isExit = false;
//private bool _hidHideInitialized = false;
//private bool _hidHideEnabled = false;
public bool IsConfigLoaded= false;
public MainWindowViewModel ViewModel { get; set; }
//public ConsoleWindow ConsoleWindow { get; set; } = new ConsoleWindow();
public FancyInputMainWindow()
{
InitializeComponent();
ViewModel = new MainWindowViewModel(this);
ViewModel.LoadConfig();
if (!string.IsNullOrEmpty(ViewModel.MacroSavePath) && Directory.Exists(ViewModel.MacroSavePath)) //自动加载宏文件夹
ViewModel.MacroWindowViewModel.LoadMacroFromFolder(ViewModel.MacroSavePath);
ViewModel.CurrentVersion = VersionInfo.CurrentVersion;
this.Title = $"FancyInput v{VersionInfo.CurrentVersion}";
ViewModel.PanelType = MainWindowPanelType.Read;
ViewModel.InputParser.GetInput += DebubTest;
ParseArgs();
this.DataContext = ViewModel;
this.Loaded += FancyInputMainWindow_Loaded;
}
private void DebubTest(object? sender, InputArgs e)
{
if (ViewModel.DetectInput)
{
//if (e.Device == Models.InputDevice.MouseMove && e.MoveState == MoveState.Moving) return;
//if (e.Device == Models.InputDevice.MouseMove || e.Device == Models.InputDevice.MouseButton || e.Device == Models.InputDevice.MouseWheel) return;
//if (e.Device is Models.InputDevice.XInputButton or Models.InputDevice.XInputTrigger or Models.InputDevice.XInputStick
// or Models.InputDevice.SDLButton or Models.InputDevice.SDLTrigger or Models.InputDevice.SDLStick
// && e.GamepadUserIdx == GamepadUserIdx.One)
//{
// if (TryMirrorRealGamepadToVirtual(e))
// {
// //ViewModel.InputDetectString = $"MirrorGamepad: {e}";
// return;
// }
//}
//if (e.Device == Models.InputDevice.Keyboard && e.RawKey.HasValue)
//{
// if (TrySendVirtualGamepadTestInput(e.RawKey.Value, e.State ?? ButtonState.Released))
// {
// ViewModel.InputDetectString = $"GamepadTest: {e.RawKey} {e.State}";
// return;
// }
//}
//else
ViewModel.InputDetectString = $"{e}";
}
}
private static bool TryMirrorRealGamepadToVirtual(InputArgs e)
{
switch (e.Device)
{
case Models.InputDevice.XInputButton:
case Models.InputDevice.SDLButton:
if (e.Flag.HasValue)
{
if (e.Flag.Value == FIPGamepadButtonflags.RS)
{
var macroState = e.State ?? ButtonState.Released;
InputGenerator.SendXInputButton(FIPGamepadButtonflags.A, macroState);
InputGenerator.SendXInputButton(FIPGamepadButtonflags.X, macroState);
return true;
}
InputGenerator.SendXInputButton(e.Flag.Value, e.State ?? ButtonState.Released);
return true;
}
return false;
case Models.InputDevice.XInputTrigger:
case Models.InputDevice.SDLTrigger:
if (e.Side.HasValue && e.Value.HasValue)
{
InputGenerator.SendXInpuTrigger(e.Value.Value, e.Side.Value);
return true;
}
return false;
case Models.InputDevice.XInputStick:
case Models.InputDevice.SDLStick:
if (e.Side.HasValue && e.ValueVector is { Length: >= 2 })
{
InputGenerator.SendXInputStick(e.ValueVector[0], e.ValueVector[1], e.Side.Value);
return true;
}
return false;
default:
return false;
}
}
private void ShowMainWindow(object sender, RoutedEventArgs e)
{
Show();
if (WindowState == WindowState.Minimized)
WindowState = WindowState.Normal;
this.Activate();
}
private void Exit_Click(object sender, RoutedEventArgs e)
{
AllowExit();
Close();
}
public void AllowExit() => _isExit = true;
protected override void OnClosing(System.ComponentModel.CancelEventArgs e)
{
if (!_isExit && ViewModel.MinimizeToTray)
{
e.Cancel = true;
Hide();
}
else
{
ViewModel.TrayIconVisibility = Visibility.Collapsed;
NotifyIcon.Dispose();
ViewModel.DetectInput = false;
// Keep physical pad hidden until virtual pad is detached.
// Use finally so hide cleanup still runs even if detach path changes in future.
//VirtualControllerManager.Instance.Unplug();
//TryDisableRealGamepadHideMode();
}
base.OnClosing(e);
}
private async void FancyInputMainWindow_Loaded(object sender, RoutedEventArgs e)
{
if (ViewModel.RunAsAdmin && !MainWindowViewModel.IsRunAsAdmin())
{
string adminArg = ViewModel.AdminRestartArg;
string[] args = Environment.GetCommandLineArgs();
string[] effectiveArgs = args.Skip(1).ToArray();
string arguments = string.Join(" ", effectiveArgs.Select(a => $"\"{a}\""));
arguments = $"{adminArg} {arguments}".Trim();
var processInfo = new ProcessStartInfo
{
FileName = Process.GetCurrentProcess().MainModule!.FileName,
UseShellExecute = true,
Arguments = arguments
};
Process.Start(processInfo);
}
if (!ViewModel.RunAsAdmin && MainWindowViewModel.IsRunAsAdmin())
{
ViewModel.RunAsAdmin = true;
}
IsConfigLoaded = true;
InputLanguageManager.Current.CurrentInputLanguage = new CultureInfo("en-US");
await CheckForUpdateAsync(latestRemind:false, isLaunch:true);
WindowHelper.TryAddRawInputHook(this);
//TryEnableRealGamepadHideMode();
}
private void OpenMacroWindow(object sender, RoutedEventArgs e)
{
var macroWindowViewModel = ViewModel.MacroWindowViewModel;
MacroWindow macroWindow = new MacroWindow(macroWindowViewModel);
macroWindow.Owner = this;
macroWindow.WindowStartupLocation = WindowStartupLocation.CenterOwner;
macroWindow.ShowDialog();
}
//private void TryEnableRealGamepadHideMode()
//{
// if (_hidHideInitialized) return;
// _hidHideInitialized = true;
// var result = HidHideManager.TryEnableForCurrentApp(out string detail);
// Debug.WriteLine($"HidHide init: {detail}");
// _hidHideEnabled = result;
// if (result) return;
// // 提示一次即可:需要管理员权限 + 已安装 HidHide,且已在 HidHide 中勾选要隐藏的真实手柄。
// AppMessageBox.Show(
// "未能启用“隐藏手柄”。\n\n" +
// "请确认:\n" +
// "1) 已安装 HidHide\n" +
// "2) 以管理员权限运行 FancyInput\n" +
// "3) 在 HidHide 配置中把真实手柄加入隐藏设备列表。\n\n" +
// $"详情:{detail}",
// "HidHide 提示",
// MessageBoxButton.OK,
// MessageBoxImage.Information);
//}
//private void TryDisableRealGamepadHideMode()
//{
// if (!_hidHideEnabled) return;
// bool result = HidHideManager.TryDisableForCurrentApp(out string detail);
// Debug.WriteLine($"HidHide shutdown: {detail}");
// if (result)
// {
// _hidHideEnabled = false;
// }
//}
private async Task CheckForUpdateAsync(bool latestRemind,bool isLaunch=false)
{
try
{
using var httpClient = new HttpClient();
// 假设服务器返回纯文本版本号,如 "1.0.1"
var versionInfo = await httpClient.GetAsync(VersionInfo.LatestDownloadUrl);
versionInfo.EnsureSuccessStatusCode();
using var jsonStream = await versionInfo.Content.ReadAsStreamAsync();
var payload = await JsonSerializer.DeserializeAsync<JsonElement>(jsonStream);
var latestVersion = payload.GetProperty("version").GetString();
var downloadUrl = payload.GetProperty("url").GetString();
var updateInfo = payload.GetProperty("update_info").GetString();
latestVersion = latestVersion!.Trim();
if (IsNewerVersion(latestVersion, VersionInfo.CurrentVersion))
{
if (isLaunch)
{
bool ignore = false;
foreach (string version in ViewModel.LaunchUpdateIgnoreVersions)
{
if (latestVersion == version)
{
ignore = true;
break;
}
}
if (ignore) return;
}
var prompt = new UpdatePromptWindow(
UpdatePromptMode.UpdateAvailable,
VersionInfo.CurrentVersion,
latestVersion,
NormalizeUpdateInfo(updateInfo),
isLaunch)
{
Owner = this,
WindowStartupLocation = WindowStartupLocation.CenterOwner,
};
prompt.ShowDialog();
if (prompt.Action == UpdatePromptAction.Download)
{
this.AllowExit();
this.Close();
Process.Start(new ProcessStartInfo
{
FileName = downloadUrl,
UseShellExecute = true
});
//var installerBytes = await httpClient.GetByteArrayAsync(downloadUrl);
//var outputPath = Path.Combine(Environment.CurrentDirectory, $"installer-{latestVersion}.exe");
//await File.WriteAllBytesAsync(outputPath, installerBytes);
//Process.Start(new ProcessStartInfo
//{
// FileName = outputPath,
// UseShellExecute = true
//});
}
else if (isLaunch && prompt.IgnoreThisVersionOnLaunch)
{
ViewModel.LaunchUpdateIgnoreVersions = ViewModel.LaunchUpdateIgnoreVersions.Append(latestVersion).ToArray();
ViewModel.SaveConfig();
}
}
else
{
if (latestRemind)
{
var prompt = new UpdatePromptWindow(
UpdatePromptMode.Latest,
VersionInfo.CurrentVersion,
latestVersion,
string.Empty,
false)
{
Owner = this,
WindowStartupLocation = WindowStartupLocation.CenterOwner,
};
prompt.ShowDialog();
}
}
}
catch (Exception ex)
{
if (latestRemind)
{
var prompt = new UpdatePromptWindow(
UpdatePromptMode.Error,
VersionInfo.CurrentVersion,
"未知",
ex.Message,
false)
{
Owner = this,
WindowStartupLocation = WindowStartupLocation.CenterOwner,
};
prompt.ShowDialog();
}
}
}
private static string NormalizeUpdateInfo(string? updateInfo)
{
if (string.IsNullOrWhiteSpace(updateInfo))
{
return "暂无更新说明。";
}
string text = updateInfo
.Replace("\\r\\n", "\n")
.Replace("\\n", "\n")
.Replace("\r\n", "\n")
.Trim();
if (!text.Contains('\n'))
{
text = text.Replace("", "\n").Replace(";", ";\n");
}
return text;
}
private bool IsNewerVersion(string latest, string current)
{
Version? vLatest, vCurrent;
if (Version.TryParse(latest, out vLatest) && Version.TryParse(current, out vCurrent))
return vLatest > vCurrent;
return false;
}
public void ParseArgs()
{
string[] args = Environment.GetCommandLineArgs();
if (args.Length > 1)
{
string filePath = args[1];
OpenProjectFile(filePath);
}
}
public void OpenProjectFile(string filePath)
{
if (System.IO.File.Exists(filePath))
{
if (filePath.EndsWith(".fip", StringComparison.OrdinalIgnoreCase))
{
ViewModel.LoadMode = LoadMode.ProjectFile;
ViewModel.ProjectFilePath = filePath;
LoadConfig();
}
else if (filePath.EndsWith(".fips", StringComparison.OrdinalIgnoreCase))
{
ViewModel.OpenFips(filePath);
}
}
this.Activate(); // 激活窗口
}
private void ReadPage_ConfigLoadClick(object sender, RoutedEventArgs e) => LoadConfig();
private void ReadPage_ConfigEditClick(object sender, RoutedEventArgs e)
{
try
{
ElementTreeViewModel elementViewModel = (ViewModel.LoadMode == LoadMode.PngJson) ?
new ElementTreeViewModel(ViewModel.PngFilePath, ViewModel.JsonFilePath) : new ElementTreeViewModel(ViewModel.ProjectFilePath);
ElementTreeWindow elementTreeWindow = new ElementTreeWindow(elementViewModel);
elementTreeWindow.ElementTreeViewModel.InputParser = ViewModel.InputParser;
elementTreeWindow.Owner = this;
elementTreeWindow.ShowDialog();
}
catch (Exception ex)
{
FancyInput.AppMessageBox.Show($"发生错误:{ex.Message}\n\n详细信息:\n{ex}",
"加载失败",
MessageBoxButton.OK,
MessageBoxImage.Error
);
}
}
public OverlayWindowViewModel? LoadConfig()
{
try
{
LoadMode loadMode = ViewModel.LoadMode;
ElementTreeViewModel vm;
switch (loadMode)
{
case LoadMode.PngJson:
XImage inputImage = new XImage(ViewModel.PngFilePath);
OverlayRoot overlayRoot = OverlayParser.Parse(ViewModel.JsonFilePath);
vm = new ElementTreeViewModel(overlayRoot, inputImage);
break;
case LoadMode.ProjectFile:
vm = new ElementTreeViewModel(ViewModel.ProjectFilePath);
break;
default:
throw new Exception($"无法识别的加载类型:{loadMode}");
}
vm.LoadMode = loadMode;
vm.PngFilePath = ViewModel.PngFilePath;
vm.JsonFilePath = ViewModel.JsonFilePath;
vm.ProjectFilePath = ViewModel.ProjectFilePath;
OverlayWindow overlayWindow = new OverlayWindow(vm);
overlayWindow.Show();
ViewModel.InputParser.GetInput += overlayWindow.ElementTreeViewModel.InputParser_GetInput;
overlayWindow.Closed += (s, e) =>
{
overlayWindow.Dispose();
ViewModel.OverlayWindowViewModels.RemoveAt(overlayWindow.OverlayWindowViewModel.Idx);
ViewModel.InputParser.GetInput -= overlayWindow.ElementTreeViewModel.InputParser_GetInput;
ViewModel.RefreshOverlayIndices();
};
ViewModel.OverlayWindowViewModels.Add(overlayWindow.OverlayWindowViewModel);
ViewModel.RefreshOverlayIndices();
string pngName = System.IO.Path.GetFileName(ViewModel.PngFilePath);
string jsonName = System.IO.Path.GetFileName(ViewModel.JsonFilePath);
string projectFileName = System.IO.Path.GetFileName(ViewModel.ProjectFilePath);
ViewModel.DebugCircleFill = new SolidColorBrush(Colors.Green);
ViewModel.DebugLabelString = (loadMode == LoadMode.PngJson) ? $"已加载:{pngName},{jsonName}" : $"已加载:{projectFileName}";
return overlayWindow.OverlayWindowViewModel;
}
catch (Exception ex)
{
ViewModel.DebugCircleFill = new SolidColorBrush(Colors.Red);
ViewModel.DebugLabelString = $"加载出错:{ex.Message}";
FancyInput.AppMessageBox.Show($"发生错误:{ex.Message}\n\n详细信息:\n{ex}",
"加载失败",
MessageBoxButton.OK,
MessageBoxImage.Error
);
return null;
}
}
public OverlayWindowViewModel? LoadElementTreeViewModel(ElementTreeViewModel vm)
{
OverlayWindow overlayWindow = new OverlayWindow(vm);
overlayWindow.Show();
ViewModel.InputParser.GetInput += overlayWindow.ElementTreeViewModel.InputParser_GetInput;
overlayWindow.Closed += (s, e) =>
{
overlayWindow.Dispose();
ViewModel.OverlayWindowViewModels.RemoveAt(overlayWindow.OverlayWindowViewModel.Idx);
ViewModel.InputParser.GetInput -= overlayWindow.ElementTreeViewModel.InputParser_GetInput;
ViewModel.RefreshOverlayIndices();
};
ViewModel.OverlayWindowViewModels.Add(overlayWindow.OverlayWindowViewModel);
ViewModel.RefreshOverlayIndices();
return overlayWindow.OverlayWindowViewModel;
}
private void Window_Closed(object sender, EventArgs e)
{
if (ViewModel == null) return;
int count = ViewModel.OverlayWindowViewModels.Count;
for (int i = count-1; i >=0; i--)
{
ViewModel.OverlayWindowViewModels[i].OverlayWindow.Close();
}
ViewModel.InputParser.Dispose();
//VirtualControllerManager.Instance.Unplug();
}
private void ChangePanel(object sender, RoutedEventArgs e)
{
Button? btn = sender as Button;
if (btn == null) return;
MainWindowPanelType panelType = btn.Tag is MainWindowPanelType pnl ? pnl : MainWindowPanelType.Read;
ViewModel.PanelType = panelType;
}
private void CreatePage_CreateFromEmptyClick(object sender, RoutedEventArgs e)
{
ElementTreeViewModel elementTreeViewModel = new ElementTreeViewModel();
ElementTreeWindow elementTreeWindow = new ElementTreeWindow(elementTreeViewModel);
elementTreeWindow.ElementTreeViewModel.InputParser = ViewModel.InputParser;
elementTreeWindow.DataContext = elementTreeViewModel;
elementTreeWindow.Owner = this;
elementTreeWindow.ShowDialog();
}
private void About(object sender, RoutedEventArgs e)
{
AboutWindow aboutWindow = new AboutWindow(VersionInfo.CurrentVersion, VersionInfo.Date,VersionInfo.Mail);
aboutWindow.Owner = this;
aboutWindow.WindowStartupLocation = WindowStartupLocation.CenterOwner;
aboutWindow.CheckUpdateRequested += async () =>
{
await CheckForUpdateAsync(latestRemind:true);
};
aboutWindow.UpdateTimelineRequested += async () =>
{
await ShowUpdateTimelineWindowAsync();
};
aboutWindow.ShowDialog();
}
private async Task ShowUpdateTimelineWindowAsync()
{
IReadOnlyList<VersionTimelineEntry> entries = await FetchTimelineEntriesAsync();
var timelineWindow = new UpdateTimelineWindow(VersionInfo.CurrentVersion, VersionInfo.Date, entries)
{
Owner = this,
WindowStartupLocation = WindowStartupLocation.CenterOwner,
};
timelineWindow.ShowDialog();
}
private async Task<IReadOnlyList<VersionTimelineEntry>> FetchTimelineEntriesAsync()
{
try
{
using var httpClient = new HttpClient();
var response = await httpClient.GetAsync(VersionInfo.DownloadCountAllAPI);
response.EnsureSuccessStatusCode();
using var stream = await response.Content.ReadAsStreamAsync();
var payload = await JsonSerializer.DeserializeAsync<JsonElement>(stream);
var entries = new List<VersionTimelineEntry>();
if (payload.TryGetProperty("versions", out var versions) && versions.ValueKind == JsonValueKind.Array)
{
foreach (var item in versions.EnumerateArray())
{
if (!item.TryGetProperty("version", out var versionElement))
{
continue;
}
string version = (versionElement.GetString() ?? string.Empty).Trim();
if (string.IsNullOrWhiteSpace(version))
{
continue;
}
int downloads = 0;
if (item.TryGetProperty("downloads", out var downloadElement) && downloadElement.TryGetInt32(out var parsedDownloads))
{
downloads = parsedDownloads;
}
string updateInfo = await TryFetchUpdateInfoAsync(httpClient, version);
entries.Add(new VersionTimelineEntry
{
Version = version,
DownloadCount = downloads,
UpdateInfo = NormalizeUpdateInfo(updateInfo),
IsCurrent = version == VersionInfo.CurrentVersion,
IsExpanded = version == VersionInfo.CurrentVersion,
});
}
}
if (entries.Count == 0)
{
return BuildFallbackTimelineEntries();
}
if (!entries.Any(e => e.IsCurrent))
{
entries.Insert(0, new VersionTimelineEntry
{
Version = VersionInfo.CurrentVersion,
DownloadCount = 0,
UpdateInfo = NormalizeUpdateInfo(ReadLocalUpdateInfo()),
IsCurrent = true,
IsExpanded = true,
});
}
return entries;
}
catch
{
return BuildFallbackTimelineEntries();
}
}
private async Task<string> TryFetchUpdateInfoAsync(HttpClient httpClient, string version)
{
try
{
string url = VersionInfo.UpdateInfoUrlPrefix + Uri.EscapeDataString(version);
var response = await httpClient.GetAsync(url);
if (!response.IsSuccessStatusCode)
{
return "暂无更新说明。";
}
using var stream = await response.Content.ReadAsStreamAsync();
var payload = await JsonSerializer.DeserializeAsync<JsonElement>(stream);
if (payload.TryGetProperty("info", out var infoElement))
{
return infoElement.GetString() ?? "暂无更新说明。";
}
return "暂无更新说明。";
}
catch
{
return "暂无更新说明。";
}
}
private IReadOnlyList<VersionTimelineEntry> BuildFallbackTimelineEntries()
{
return new List<VersionTimelineEntry>
{
new VersionTimelineEntry
{
Version = VersionInfo.CurrentVersion,
DownloadCount = 0,
UpdateInfo = NormalizeUpdateInfo(ReadLocalUpdateInfo()),
IsCurrent = true,
IsExpanded = true,
}
};
}
private static string ReadLocalUpdateInfo()
{
string[] candidates =
{
Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Update.txt"),
Path.Combine(Environment.CurrentDirectory, "Update.txt"),
};
foreach (var path in candidates)
{
if (File.Exists(path))
{
return File.ReadAllText(path);
}
}
return "暂无本地更新说明。";
}
private void ManagePage_SaveGroupClick(object sender, RoutedEventArgs e)=> ViewModel.SaveLoadedGroup();
private void ManagePage_LoadGroupClick(object sender, RoutedEventArgs e)=> ViewModel.AddLoadedGroup();
private void ManagePage_UpMoveClick(object sender, ElementMoveActionEventArgs e) => ViewModel.UpMoveAt(e.Index);
private void ManagePage_DownMoveClick(object sender, ElementMoveActionEventArgs e) => ViewModel.DownMoveAt(e.Index);
}
}
@@ -0,0 +1,21 @@
using System;
using System.Globalization;
using System.Windows.Data;
namespace FancyInput.Models
{
public class EnumDescriptionConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is Enum e)
return e.GetDescription();
return value?.ToString() ?? "";
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}
+350
View File
@@ -0,0 +1,350 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text.Json;
using FancyInput.ViewModels;
namespace FancyInput.Models
{
internal static class HidHideManager
{
private static readonly HashSet<string> HiddenByFancyInput = new(StringComparer.OrdinalIgnoreCase);
public static bool TryEnableForCurrentApp(out string detail)
{
detail = string.Empty;
if (!MainWindowViewModel.IsRunAsAdmin())
{
detail = "当前进程不是管理员权限。";
return false;
}
if (!TryFindCliPath(out string? cliPath))
{
detail = "未找到 HidHideCLI.exe(请先安装 HidHide)。";
return false;
}
string? appPath = Process.GetCurrentProcess().MainModule?.FileName;
if (string.IsNullOrWhiteSpace(appPath) || !File.Exists(appPath))
{
detail = "无法获取当前程序路径。";
return false;
}
if (!RunCli(cliPath!, $"--app-reg \"{appPath}\"", out string appRegErr))
{
detail = $"注册白名单失败:{appRegErr}";
return false;
}
if (!RunCli(cliPath!, "--cloak-on", out string cloakErr))
{
detail = $"开启 HidHide cloak 失败:{cloakErr}";
return false;
}
if (!TryHidePresentGamingDevices(cliPath!, out int hiddenCount, out string hideErr))
{
detail = $"已开启 cloak,但自动隐藏真实手柄失败:{hideErr}";
return false;
}
detail = $"已注册白名单、开启 cloak,并自动隐藏 {hiddenCount} 个真实手柄设备。";
return true;
}
public static bool TryDisableForCurrentApp(out string detail)
{
detail = string.Empty;
if (!MainWindowViewModel.IsRunAsAdmin())
{
detail = "当前进程不是管理员权限。";
return false;
}
if (!TryFindCliPath(out string? cliPath))
{
detail = "未找到 HidHideCLI.exe(请先安装 HidHide)。";
return false;
}
string? appPath = Process.GetCurrentProcess().MainModule?.FileName;
if (string.IsNullOrWhiteSpace(appPath) || !File.Exists(appPath))
{
detail = "无法获取当前程序路径。";
return false;
}
bool unhideOk = TryUnhideDevicesAddedByFancyInput(cliPath!, out string unhideErr);
bool cloakOk = RunCli(cliPath!, "--cloak-off", out string cloakErr);
bool unregOk = RunCli(cliPath!, $"--app-unreg \"{appPath}\"", out string unregErr);
if (unhideOk && cloakOk && unregOk)
{
detail = "已恢复被隐藏设备、关闭 HidHide cloak 并移除白名单。";
return true;
}
var errors = new List<string>();
if (!unhideOk) errors.Add($"恢复设备失败:{unhideErr}");
if (!cloakOk) errors.Add($"关闭 cloak 失败:{cloakErr}");
if (!unregOk) errors.Add($"移除白名单失败:{unregErr}");
if (errors.Count > 0)
{
detail = string.Join("", errors);
return false;
}
detail = "已执行 HidHide 退出清理。";
return true;
}
private static bool TryHidePresentGamingDevices(string cliPath, out int hiddenCount, out string error)
{
hiddenCount = 0;
error = string.Empty;
if (!RunCliWithOutput(cliPath, "--dev-gaming", out string output, out string listErr))
{
error = $"读取游戏设备列表失败:{listErr}";
return false;
}
var paths = ParsePresentGamingDevicePaths(output);
if (paths.Count == 0)
{
return true;
}
foreach (string path in paths)
{
if (!RunCli(cliPath, $"--dev-hide \"{path}\"", out string hideErr))
{
error = $"隐藏设备失败:{path}{hideErr}";
return false;
}
HiddenByFancyInput.Add(path);
hiddenCount++;
}
return true;
}
private static bool TryUnhideDevicesAddedByFancyInput(string cliPath, out string error)
{
error = string.Empty;
if (HiddenByFancyInput.Count == 0)
{
return true;
}
foreach (string path in HiddenByFancyInput.ToArray())
{
if (!RunCli(cliPath, $"--dev-unhide \"{path}\"", out string unhideErr))
{
error = $"{path} -> {unhideErr}";
return false;
}
HiddenByFancyInput.Remove(path);
}
return true;
}
private static List<string> ParsePresentGamingDevicePaths(string output)
{
var result = new List<string>();
if (string.IsNullOrWhiteSpace(output))
{
return result;
}
try
{
using JsonDocument document = JsonDocument.Parse(output);
if (document.RootElement.ValueKind != JsonValueKind.Array)
{
return result;
}
foreach (JsonElement container in document.RootElement.EnumerateArray())
{
if (!container.TryGetProperty("devices", out JsonElement devices) || devices.ValueKind != JsonValueKind.Array)
{
continue;
}
foreach (JsonElement device in devices.EnumerateArray())
{
if (!TryGetBoolean(device, "present", out bool present) || !present) continue;
if (!TryGetBoolean(device, "gamingDevice", out bool gaming) || !gaming) continue;
string instancePath = TryGetString(device, "deviceInstancePath");
if (string.IsNullOrWhiteSpace(instancePath)) continue;
if (IsVirtualDevice(device, instancePath)) continue;
result.Add(instancePath);
}
}
}
catch
{
// Ignore parse issues and return empty so caller can continue safely.
}
return result;
}
private static bool IsVirtualDevice(JsonElement device, string instancePath)
{
string[] virtualHints =
{
"VIGEM",
"NEFARIUS",
"VIRTUAL",
"ROOT\\"
};
string baseContainer = TryGetString(device, "baseContainerDeviceInstancePath");
string friendly = TryGetString(device, "friendlyName");
string product = TryGetString(device, "product");
string merged = string.Join("|", new[] { instancePath, baseContainer, friendly, product });
return virtualHints.Any(h => merged.Contains(h, StringComparison.OrdinalIgnoreCase));
}
private static bool TryGetBoolean(JsonElement element, string propertyName, out bool value)
{
value = false;
if (!element.TryGetProperty(propertyName, out JsonElement property)) return false;
if (property.ValueKind != JsonValueKind.True && property.ValueKind != JsonValueKind.False) return false;
value = property.GetBoolean();
return true;
}
private static string TryGetString(JsonElement element, string propertyName)
{
if (!element.TryGetProperty(propertyName, out JsonElement property)) return string.Empty;
if (property.ValueKind != JsonValueKind.String) return string.Empty;
return property.GetString() ?? string.Empty;
}
private static bool TryFindCliPath(out string? cliPath)
{
string programFiles = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles);
string[] candidates =
{
Path.Combine(programFiles, "Nefarius Software Solutions", "HidHide", "x64", "HidHideCLI.exe"),
Path.Combine(programFiles, "Nefarius Software Solutions", "HidHide", "HidHideCLI.exe"),
Path.Combine(programFiles, "Nefarius Software Solutions e.U", "HidHide", "x64", "HidHideCLI.exe"),
Path.Combine(programFiles, "Nefarius Software Solutions e.U", "HidHide", "HidHideCLI.exe")
};
foreach (string path in candidates)
{
if (File.Exists(path))
{
cliPath = path;
return true;
}
}
cliPath = null;
return false;
}
private static bool RunCli(string cliPath, string args, out string error)
{
error = string.Empty;
try
{
var psi = new ProcessStartInfo
{
FileName = cliPath,
Arguments = args,
UseShellExecute = false,
CreateNoWindow = true,
RedirectStandardOutput = true,
RedirectStandardError = true,
WorkingDirectory = Path.GetDirectoryName(cliPath) ?? AppContext.BaseDirectory
};
using var p = Process.Start(psi);
if (p == null)
{
error = "无法启动 HidHideCLI。";
return false;
}
p.WaitForExit(5000);
string stderr = p.StandardError.ReadToEnd().Trim();
if (p.ExitCode != 0)
{
error = string.IsNullOrEmpty(stderr) ? $"退出码 {p.ExitCode}" : stderr;
return false;
}
return true;
}
catch (Exception ex)
{
error = ex.Message;
return false;
}
}
private static bool RunCliWithOutput(string cliPath, string args, out string output, out string error)
{
output = string.Empty;
error = string.Empty;
try
{
var psi = new ProcessStartInfo
{
FileName = cliPath,
Arguments = args,
UseShellExecute = false,
CreateNoWindow = true,
RedirectStandardOutput = true,
RedirectStandardError = true,
WorkingDirectory = Path.GetDirectoryName(cliPath) ?? AppContext.BaseDirectory
};
using var p = Process.Start(psi);
if (p == null)
{
error = "无法启动 HidHideCLI。";
return false;
}
output = p.StandardOutput.ReadToEnd();
string stderr = p.StandardError.ReadToEnd().Trim();
p.WaitForExit(5000);
if (p.ExitCode != 0)
{
error = string.IsNullOrEmpty(stderr) ? $"退出码 {p.ExitCode}" : stderr;
return false;
}
return true;
}
catch (Exception ex)
{
error = ex.Message;
return false;
}
}
}
}
+326
View File
@@ -0,0 +1,326 @@
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Windows.Forms;
namespace FancyInput.Models
{
public class InputGenerator
{
public static bool PlugVirtualGamepad() => VirtualControllerManager.Instance.PlugIn();
public static void UnplugVirtualGamepad() => VirtualControllerManager.Instance.Unplug();
public static bool IsVirtualGamepadPlugged => VirtualControllerManager.Instance.IsAvailable;
public static int VirtualGamepadIndex => VirtualControllerManager.Instance.Index;
public static void SendKey(FipRawKeys rawKey, ButtonState state = ButtonState.Pressed)
{
bool keyUp = state == ButtonState.Released;
var keyInfo = MapRawKey(rawKey);
if (keyInfo is null) return;
SendInputs( new[] { CreateKeyboardInput(keyInfo.Value.VirtualKey, keyInfo.Value.ScanCode, keyInfo.Value.IsExtended, keyUp) });
}
public static void SendMouseButton(MouseButtons button, ButtonState state = ButtonState.Pressed)
=>SendInputs(new[] { CreateMouseButtonInput(button, state == ButtonState.Pressed) });
public static void SendMouseMove(int x, int y, bool absolute = true) => SendInputs(new[] { CreateMouseMoveInput(x, y, absolute) });
public static void SendMouseWheel(int delta)=>SendInputs(new[] { CreateMouseWheelInput(delta) });
public static void SendXInputButton(FIPGamepadButtonflags button, ButtonState state = ButtonState.Pressed)
{
if (!VirtualControllerManager.Instance.IsAvailable)
{
if (!VirtualControllerManager.Instance.Initialize())
throw new NotSupportedException("Virtual XInput controller is not available. Ensure ViGEm client DLL is present and ViGEmBus is installed.");
}
bool pressed = state == ButtonState.Pressed;
VirtualControllerManager.Instance.SetButton(button, pressed);
}
public static void SendXInpuTrigger(int triggerValue,Side side)
{
if (!VirtualControllerManager.Instance.IsAvailable)
{
if (!VirtualControllerManager.Instance.Initialize())
throw new NotSupportedException("Virtual XInput controller is not available. Ensure ViGEm client DLL is present and ViGEmBus is installed.");
}
VirtualControllerManager.Instance.SetTrigger(triggerValue, side);
}
public static void SendXInputStick(int x, int y, Side side)
{
if (!VirtualControllerManager.Instance.IsAvailable)
{
if (!VirtualControllerManager.Instance.Initialize())
throw new NotSupportedException("Virtual XInput controller is not available. Ensure ViGEm client DLL is present and ViGEmBus is installed.");
}
VirtualControllerManager.Instance.SetStick(x, y, side);
}
private static void SendInputs(IEnumerable<INPUT> inputs)
{
var inputList = inputs as INPUT[] ?? new List<INPUT>(inputs).ToArray();
if (inputList.Length == 0) return;
if (SendInput((uint)inputList.Length, inputList, Marshal.SizeOf(typeof(INPUT))) == 0)
{
int error = Marshal.GetLastWin32Error();
throw new InvalidOperationException($"SendInput failed with error code {error}.");
}
}
private static INPUT CreateKeyboardInput(ushort virtualKey, ushort scanCode, bool isExtended, bool keyUp)
{
return new INPUT
{
type = INPUT_KEYBOARD,
U = new InputUnion
{
ki = new KEYBDINPUT
{
wVk = virtualKey,
wScan = scanCode,
dwFlags = KEYEVENTF_SCANCODE | (isExtended ? KEYEVENTF_EXTENDEDKEY : 0) | (keyUp ? KEYEVENTF_KEYUP : 0)
}
}
};
}
private static INPUT CreateMouseButtonInput(MouseButtons button, bool keyDown)
{
return new INPUT
{
type = INPUT_MOUSE,
U = new InputUnion
{
mi = new MOUSEINPUT
{
dwFlags = button switch
{
MouseButtons.Left => keyDown ? MOUSEEVENTF_LEFTDOWN : MOUSEEVENTF_LEFTUP,
MouseButtons.Right => keyDown ? MOUSEEVENTF_RIGHTDOWN : MOUSEEVENTF_RIGHTUP,
MouseButtons.Middle => keyDown ? MOUSEEVENTF_MIDDLEDOWN : MOUSEEVENTF_MIDDLEUP,
MouseButtons.XButton1 => keyDown ? MOUSEEVENTF_XDOWN : MOUSEEVENTF_XUP,
MouseButtons.XButton2 => keyDown ? MOUSEEVENTF_XDOWN : MOUSEEVENTF_XUP,
_ => throw new NotSupportedException($"Mouse button {button} is not supported.")
},
mouseData = button switch
{
MouseButtons.XButton1 => XBUTTON1,
MouseButtons.XButton2 => XBUTTON2,
_ => 0
}
}
}
};
}
private static INPUT CreateMouseMoveInput(int x, int y, bool absolute)
{
if (absolute)
{
int normalizedX = NormalizeAbsoluteCoordinate(x, Screen.PrimaryScreen?.Bounds.Width ?? 1);
int normalizedY = NormalizeAbsoluteCoordinate(y, Screen.PrimaryScreen?.Bounds.Height ?? 1);
return new INPUT
{
type = INPUT_MOUSE,
U = new InputUnion
{
mi = new MOUSEINPUT
{
dx = normalizedX,
dy = normalizedY,
dwFlags = MOUSEEVENTF_MOVE | MOUSEEVENTF_ABSOLUTE
}
}
};
}
// Relative move: pass deltas directly and do not set ABSOLUTE
return new INPUT
{
type = INPUT_MOUSE,
U = new InputUnion
{
mi = new MOUSEINPUT
{
dx = x,
dy = y,
dwFlags = MOUSEEVENTF_MOVE
}
}
};
}
private static INPUT CreateMouseWheelInput(int delta)
{
return new INPUT
{
type = INPUT_MOUSE,
U = new InputUnion
{
mi = new MOUSEINPUT
{
mouseData = unchecked((uint)delta),
dwFlags = MOUSEEVENTF_WHEEL
}
}
};
}
private static KeyInfo? MapRawKey(FipRawKeys rawKey)
{
uint code = (uint)rawKey;
if (code == 0) return null;
// Detect E0/E1 prefix by high byte(s). Common pattern: 0xE0xx or 0xE1xxxx
bool isExtended = (code & 0xFF00u) == 0xE000u || (code & 0xFF0000u) == 0xE10000u;
ushort scan = (ushort)(code & 0xFFu);
// Determine a sensible virtual-key (vKey). For numpad keys this depends on NumLock state.
ushort vKey = 0;
try
{
bool numLockOn = System.Windows.Forms.Control.IsKeyLocked(Keys.NumLock);
// Handle numpad-specific behavior when NumLock is toggled
if (RawInputParser.NumLockAffectedFipRawKeys.Contains(rawKey))
{
vKey = rawKey switch
{
FipRawKeys.NumPad0 => (ushort)(numLockOn ? Keys.NumPad0 : Keys.Insert),
FipRawKeys.NumPad1 => (ushort)(numLockOn ? Keys.NumPad1 : Keys.End),
FipRawKeys.NumPad2 => (ushort)(numLockOn ? Keys.NumPad2 : Keys.Down),
FipRawKeys.NumPad3 => (ushort)(numLockOn ? Keys.NumPad3 : Keys.PageDown),
FipRawKeys.NumPad4 => (ushort)(numLockOn ? Keys.NumPad4 : Keys.Left),
FipRawKeys.NumPad5 => (ushort)(numLockOn ? Keys.NumPad5 : Keys.Clear),
FipRawKeys.NumPad6 => (ushort)(numLockOn ? Keys.NumPad6 : Keys.Right),
FipRawKeys.NumPad7 => (ushort)(numLockOn ? Keys.NumPad7 : Keys.Home),
FipRawKeys.NumPad8 => (ushort)(numLockOn ? Keys.NumPad8 : Keys.Up),
FipRawKeys.NumPad9 => (ushort)(numLockOn ? Keys.NumPad9 : Keys.PageUp),
FipRawKeys.Decimal => (ushort)(numLockOn ? Keys.Decimal : Keys.Delete),
FipRawKeys.Multiply => (ushort)Keys.Multiply,
FipRawKeys.Add => (ushort)Keys.Add,
FipRawKeys.Subtract => (ushort)Keys.Subtract,
FipRawKeys.Divide => (ushort)Keys.Divide,
_ => 0
};
}
// Special-case NumpadEnter
if (rawKey == FipRawKeys.NumpadEnter)
{
vKey = (ushort)Keys.Return;
}
// Fallback: try to parse enum name into Keys, else use MapVirtualKey from scan code
if (vKey == 0)
{
if (Enum.TryParse(rawKey.ToString(), true, out Keys parsed))
{
vKey = (ushort)parsed;
}
else
{
uint mapped = MapVirtualKey(scan, 1); // MAPVK_VSC_TO_VK
vKey = (ushort)mapped;
}
}
}
catch
{
// On any failure, fall back to mapping via scan code only
vKey = (ushort)MapVirtualKey(scan, 1);
}
return new KeyInfo(vKey, scan, isExtended);
}
private readonly record struct KeyInfo(ushort VirtualKey, ushort ScanCode, bool IsExtended);
private const uint INPUT_MOUSE = 0;
private const uint INPUT_KEYBOARD = 1;
private const uint KEYEVENTF_EXTENDEDKEY = 0x0001;
private const uint KEYEVENTF_KEYUP = 0x0002;
private const uint KEYEVENTF_SCANCODE = 0x0008;
private const uint MOUSEEVENTF_LEFTDOWN = 0x0002;
private const uint MOUSEEVENTF_LEFTUP = 0x0004;
private const uint MOUSEEVENTF_MOVE = 0x0001;
private const uint MOUSEEVENTF_RIGHTDOWN = 0x0008;
private const uint MOUSEEVENTF_RIGHTUP = 0x0010;
private const uint MOUSEEVENTF_MIDDLEDOWN = 0x0020;
private const uint MOUSEEVENTF_MIDDLEUP = 0x0040;
private const uint MOUSEEVENTF_XDOWN = 0x0080;
private const uint MOUSEEVENTF_XUP = 0x0100;
private const uint MOUSEEVENTF_WHEEL = 0x0800;
private const uint MOUSEEVENTF_ABSOLUTE = 0x8000;
private const uint XBUTTON1 = 0x0001;
private const uint XBUTTON2 = 0x0002;
private const uint MAPVK_VK_TO_VSC = 0;
[StructLayout(LayoutKind.Sequential)]
private struct INPUT
{
public uint type;
public InputUnion U;
}
[StructLayout(LayoutKind.Explicit)]
private struct InputUnion
{
[FieldOffset(0)]
public MOUSEINPUT mi;
[FieldOffset(0)]
public KEYBDINPUT ki;
}
[StructLayout(LayoutKind.Sequential)]
private struct MOUSEINPUT
{
public int dx;
public int dy;
public uint mouseData;
public uint dwFlags;
public uint time;
public IntPtr dwExtraInfo;
}
[StructLayout(LayoutKind.Sequential)]
private struct KEYBDINPUT
{
public ushort wVk;
public ushort wScan;
public uint dwFlags;
public uint time;
public IntPtr dwExtraInfo;
}
[DllImport("user32.dll", SetLastError = true)]
private static extern uint SendInput(uint nInputs, INPUT[] pInputs, int cbSize);
[DllImport("user32.dll", SetLastError = true)]
private static extern uint MapVirtualKey(uint uCode, uint uMapType);
private static int NormalizeAbsoluteCoordinate(int coordinate, int length)
{
if (length <= 1) return 0;
coordinate = Math.Max(0, Math.Min(length - 1, coordinate));
return (int)Math.Round((coordinate * 65535.0) / (length - 1));
}
}
}
+549
View File
@@ -0,0 +1,549 @@
using FancyInput.ViewModels;
using SharpDX.XInput;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Linq;
using System.Reflection.Emit;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Controls;
using System.Windows.Forms;
using System.Windows.Media;
using System.Windows.Threading;
using System.Xml.Linq;
using static System.Windows.Forms.AxHost;
using Timer = System.Threading.Timer;
namespace FancyInput.Models
{
public class InputHandler : IDisposable
{
public static bool USE_CENTER_MOUSEMOVE = true;
public static int MOUSE_SENSE = 20;
public const int TRIGGER_CACHE_RANGE = 9;
public const int BUTTON_TIMER_INTERVAL = 25;
// 用于滑动平均的Shift
private Queue<(double X, double Y)> _mouseMoveHistory = new Queue<(double X, double Y)>();
private ElementTreeViewModel? _elementTreeViewModel;
public ElementTreeViewModel? ElementTreeViewModel => _elementTreeViewModel;
private ObservableCollection<ElementViewModel> _elementViewModels;
public ObservableCollection<ElementViewModel > ElementViewModels => _elementViewModels;
private Dictionary<InputDevice, List<int>> _respondersIdx;
private DispatcherTimer _buttonTimer;
private HashSet<ElementViewModel> _buttonRuning = new HashSet<ElementViewModel>();
private Dictionary<FipKeys, List<int>> _keyboardDictionary = new();
private Dictionary<FipRawKeys, List<int>> _rawKeyboardDictionary = new();
private Dictionary<FIPGamepadButtonflags, List<int>> _gamepadButtonDictionary = new();
private Dictionary<MouseButtons, List<int>> _mouseDictionary = new();
public const int MOUSEMOVE_DEADZONE= 10;
public InputHandler(ElementTreeViewModel elementTreeViewModel)
{
_elementTreeViewModel = elementTreeViewModel;
_elementViewModels = _elementTreeViewModel.ElementViewModels;
_respondersIdx = new Dictionary<InputDevice, List<int>>();
ResetRespondersAndDictionaries();
_buttonTimer = new DispatcherTimer(DispatcherPriority.Send) { Interval = TimeSpan.FromMilliseconds(BUTTON_TIMER_INTERVAL) };
_buttonTimer.Tick += ButtonTimer_Tick;
_buttonTimer.Start();
}
private void ButtonTimer_Tick(object? sender, EventArgs e)
{
List<ElementViewModel> vms = _buttonRuning.ToList();
foreach (var vm in vms)
{
if (vm.Stopwatch.ElapsedMilliseconds >= vm.StopwatchMilliseconds)
{
vm.CurrentSourceIdx = 0;
vm.Stopwatch.Stop();
vm.Stopwatch.Reset();
_buttonRuning.Remove(vm);
}
}
}
public InputHandler(ElementViewModel elementViewModel)
{
_elementTreeViewModel = null;
_elementViewModels = new ObservableCollection<ElementViewModel> { elementViewModel };
_respondersIdx = new Dictionary<InputDevice, List<int>>();
ResetRespondersAndDictionaries();
_buttonTimer = new DispatcherTimer(DispatcherPriority.Send) { Interval = TimeSpan.FromMilliseconds(BUTTON_TIMER_INTERVAL) };
_buttonTimer.Tick += ButtonTimer_Tick;
_buttonTimer.Start();
}
public InputHandler(OverlayRoot overlayRoot, XImage image)
{
_elementTreeViewModel = new ElementTreeViewModel(overlayRoot, image);
_elementViewModels = _elementTreeViewModel.ElementViewModels;
_respondersIdx = new Dictionary<InputDevice, List<int>>();
ResetRespondersAndDictionaries();
_buttonTimer = new DispatcherTimer(DispatcherPriority.Send) { Interval = TimeSpan.FromMilliseconds(BUTTON_TIMER_INTERVAL) };
_buttonTimer.Tick += ButtonTimer_Tick;
_buttonTimer.Start();
}
public InputHandler(string pngPath, string jsonPath)
{
_elementTreeViewModel = new ElementTreeViewModel(pngPath, jsonPath);
_elementViewModels = _elementTreeViewModel.ElementViewModels;
_respondersIdx = new Dictionary<InputDevice, List<int>>();
ResetRespondersAndDictionaries();
_buttonTimer = new DispatcherTimer(DispatcherPriority.Send) { Interval = TimeSpan.FromMilliseconds(BUTTON_TIMER_INTERVAL) };
_buttonTimer.Tick += ButtonTimer_Tick;
_buttonTimer.Start();
}
public void Dispose()
{
_buttonTimer.Stop();
}
public void ResetRespondersAndDictionaries()
{
_respondersIdx.Clear();
_keyboardDictionary.Clear();
_rawKeyboardDictionary.Clear();
_mouseDictionary.Clear();
_gamepadButtonDictionary.Clear();
foreach (InputDevice device in Enum.GetValues(typeof(InputDevice)))
{
_respondersIdx[device] = new List<int>();
}
for (int i = 0; i < _elementViewModels.Count; i++)
{
ElementViewModel elementViewModel = _elementViewModels[i];
switch (elementViewModel.ElementType)
{
case ElementType.GamepadPlayerId:
_respondersIdx[InputDevice.SDLGuide].Add(i);
break;
case ElementType.Texture:
break;
case ElementType.MouseMovement:
_respondersIdx[InputDevice.MouseMove].Add(i);
break;
case ElementType.KeyboardButton:
_respondersIdx[InputDevice.Keyboard].Add(i);
KeyBoardCodeType keyBoardCode = elementViewModel.SelectedKeyBoardButton;
if (Enum.TryParse<FipKeys>(keyBoardCode.ToString(), out FipKeys key))
{
if (!_keyboardDictionary.ContainsKey(key))
{
List<int> idx = new List<int>() { i };
_keyboardDictionary[key] = idx;
}
else
_keyboardDictionary[key].Add(i);
}
else
{
throw new Exception($"{keyBoardCode.ToString()} is not a valid enum name for Type: FipKeys");
}
if (Enum.TryParse<FipRawKeys>(keyBoardCode.ToString(), out FipRawKeys rawKey))
{
if (!_rawKeyboardDictionary.ContainsKey(rawKey))
{
List<int> idx = new List<int>() { i };
_rawKeyboardDictionary[rawKey] = idx;
}
else
_rawKeyboardDictionary[rawKey].Add(i);
}
break;
case ElementType.MouseButton:
_respondersIdx[InputDevice.MouseButton].Add(i);
MouseCodeType mouseButtonCode = elementViewModel.SelectedMouseButton;
if (Enum.TryParse<MouseButtons>(mouseButtonCode.ToString(), out MouseButtons button))
{
if (!_mouseDictionary.ContainsKey(button))
{
List<int> idx = new List<int>() { i };
_mouseDictionary[button] = idx;
}
else
_mouseDictionary[button].Add(i);
}
break;
case ElementType.MouseWheel:
_respondersIdx[InputDevice.MouseButton].Add(i);
_respondersIdx[InputDevice.MouseWheel].Add(i);
break;
case ElementType.GamepadButton:
_respondersIdx[InputDevice.XInputButton].Add(i);
_respondersIdx[InputDevice.SDLButton].Add(i);
GamepadCodeType gamepadCode = elementViewModel.SelectedGamepadButton;
string gamepadButtonName = gamepadCode.ToString();
string prefix = "HitBox_";
if (gamepadButtonName.StartsWith(prefix))
{
gamepadButtonName = gamepadButtonName.Substring(prefix.Length);
}
if (Enum.TryParse<FIPGamepadButtonflags>(gamepadButtonName, out FIPGamepadButtonflags flag))
{
if (!_gamepadButtonDictionary.ContainsKey(flag))
{
List<int> idx = new List<int>() { i };
_gamepadButtonDictionary[flag] = idx;
}
else
_gamepadButtonDictionary[flag].Add(i);
}
else
{
throw new Exception($"{gamepadCode.ToString()} is not a valid enum name for Type: GamepadButtonFlags");
}
break;
case ElementType.DPad:
_respondersIdx[InputDevice.XInputButton].Add(i);
_respondersIdx[InputDevice.SDLButton].Add(i);
break;
case ElementType.AnalogStick:
_respondersIdx[InputDevice.XInputButton].Add(i);
_respondersIdx[InputDevice.XInputStick].Add(i);
_respondersIdx[InputDevice.SDLButton].Add(i);
_respondersIdx[InputDevice.SDLStick].Add(i);
break;
case ElementType.GamepadTrigger:
_respondersIdx[InputDevice.XInputTrigger].Add(i);
_respondersIdx[InputDevice.SDLTrigger].Add(i);
elementViewModel.MakeCache(TRIGGER_CACHE_RANGE);
break;
default:
throw new Exception("Unknown Element Type");
}
}
}
public int DPadShift(FIPGamepadButtonflags flags)
{
bool isUpPressed = (flags & FIPGamepadButtonflags.Up) == FIPGamepadButtonflags.Up;
bool isDownPressed = (flags & FIPGamepadButtonflags.Down) == FIPGamepadButtonflags.Down;
bool isLeftPressed = (flags & FIPGamepadButtonflags.Left) == FIPGamepadButtonflags.Left;
bool isRightPressed = (flags & FIPGamepadButtonflags.Right) == FIPGamepadButtonflags.Right;
if (isLeftPressed)
{
if (!isUpPressed && !isDownPressed) return 1;
else if (isUpPressed) return 5;
else if (isDownPressed) return 7;
else return 0;
}
else if (isRightPressed)
{
if (!isUpPressed && !isDownPressed) return 2;
else if (isUpPressed) return 6;
else if (isDownPressed) return 8;
else return 0;
}
else if (isUpPressed) return 3;
else if (isDownPressed) return 4;
else return 0;
}
public void Handle(InputArgs e)
{
var respondersIdx = _respondersIdx[e.Device];
if (respondersIdx.Count ==0) return;
for (int i = 0; i < respondersIdx.Count; i++)
{
int responderIdx = respondersIdx[i];
ElementViewModel elementViewModel = ElementViewModels[responderIdx];
var type = elementViewModel.ElementType;
if (type == ElementType.GamepadButton || type == ElementType.KeyboardButton || type == ElementType.MouseButton) continue;
switch (e.Device)
{
case InputDevice.Keyboard:
break;
case InputDevice.MouseWheel:
if (e.Value.HasValue)
{
int wheelValue = e.Value.Value;
elementViewModel.CurrentSourceIdx = wheelValue > 0 ? 2 : 3;
}
else if (e.MoveState.HasValue && e.MoveState == MoveState.StopMove)
elementViewModel.CurrentSourceIdx = 0;
break;
case InputDevice.MouseButton:
if (!e.MouseButton.HasValue)
throw new Exception("e.button should not be null for MouseButton events");
if (!e.State.HasValue)
throw new Exception("e.State should not be null for button events");
if (e.MouseButton.Value == MouseButtons.Middle && type == ElementType.MouseWheel)
{
elementViewModel.CurrentSourceIdx = (int)e.State.Value;
}
break;
case InputDevice.MouseMove:
if (e.MoveState == MoveState.StopMove)
{
//MouseMove Stopped
if ( !USE_CENTER_MOUSEMOVE || e.PreventMouseCentering)
{
elementViewModel.OffsetX = 0;
elementViewModel.OffsetY = 0;
//ElementTreeViewModel.ElementViewModels[responderIdx].RotateAngle = 0;
}
break;
}
if (e.ValueVector == null || e.ValueVector.Length != 4)
throw new Exception("e.ValueVector should not be null and have length of 4 for MouseMove events");
double moveX = (double)e.ValueVector[0];
double moveY = (double)e.ValueVector[1];
double mouseX = (double)e.ValueVector[2];
double mouseY = (double)e.ValueVector[3];
double avgMouseMoveX = 0;
double avgMouseMoveY = 0;
double relativeMouseX = mouseX / RawInputParser.ScreenCenterX - 1;
double relativeMouseY = mouseY / RawInputParser.ScreenCenterY - 1;
_mouseMoveHistory.Enqueue((moveX, moveY));
while (_mouseMoveHistory.Count > 12)
{
_mouseMoveHistory.Dequeue();
}
var historyList = _mouseMoveHistory.ToList();
if (historyList.Count > 2)
{
var xList = historyList.Select(item => item.X).OrderBy(x => x).ToList();
var yList = historyList.Select(item => item.Y).OrderBy(y => y).ToList();
xList.RemoveAt(0);
xList.RemoveAt(xList.Count - 1);
yList.RemoveAt(0);
yList.RemoveAt(yList.Count - 1);
avgMouseMoveX = xList.Average();
avgMouseMoveY = yList.Average();
}
else
{
avgMouseMoveX = historyList.Average(item => item.X);
avgMouseMoveY = historyList.Average(item => item.Y);
}
MouseMoveType mouseMoveType = elementViewModel.SelectedMouseMoveType;
if (mouseMoveType == MouseMoveType.Arrow)
{
double angleRadius = USE_CENTER_MOUSEMOVE ? Math.Atan2(relativeMouseY, relativeMouseX) : Math.Atan2(avgMouseMoveY, avgMouseMoveX);
double angle = angleRadius * 180 / Math.PI + 90;
elementViewModel.RotateAngle = angle;
break;
}
else if (mouseMoveType == MouseMoveType.Dot)
{
double radius = (double)elementViewModel.MouseRadius;
double offsetX = USE_CENTER_MOUSEMOVE ?
Math.Clamp(relativeMouseX / (MOUSE_SENSE / 20.0), -1, 1) * radius :
Math.Clamp((avgMouseMoveX / MOUSE_SENSE), -1, 1) * radius;
double offsetY = USE_CENTER_MOUSEMOVE ?
Math.Clamp(relativeMouseY / (MOUSE_SENSE / 20.0), -1, 1) * radius :
Math.Clamp((avgMouseMoveY / MOUSE_SENSE), -1, 1) * radius;
elementViewModel.OffsetX = (int)(offsetX * elementViewModel.Scale);
elementViewModel.OffsetY = (int)(offsetY * elementViewModel.Scale);
}
break;
case InputDevice.XInputButton:
case InputDevice.SDLButton:
if (e.State == null) throw new Exception("e.State should not be null for button events");
if (!e.GamepadUserIdx.HasValue)
throw new Exception("e.GamepadUserIdx should not be null for GamepadButton events");
if (e.GamepadUserIdx.Value != elementViewModel.GamepadUserIndex) break;
switch (type)
{
case ElementType.AnalogStick:
if (!e.Flag.HasValue)
throw new Exception("e.flag should not be null for AnalogStick events");
if (!e.State.HasValue)
throw new Exception("e.State should not be null for AnalogStick events");
if ((e.Flag.Value == FIPGamepadButtonflags.LS && elementViewModel.SelectedSide == Side.Left)||
(e.Flag.Value == FIPGamepadButtonflags.RS && elementViewModel.SelectedSide == Side.Right))
elementViewModel.CurrentSourceIdx = (int)e.State.Value;
break;
case ElementType.DPad:
if (e.Flags.HasValue)
{
int shift = DPadShift(e.Flags.Value);
elementViewModel.CurrentSourceIdx = shift;
}
break;
}
break;
case InputDevice.XInputStick:
case InputDevice.SDLStick:
if (!e.GamepadUserIdx.HasValue)
throw new Exception("e.GamepadUserIdx should not be null for GamepadButton events");
if (e.GamepadUserIdx.Value != elementViewModel.GamepadUserIndex) break;
if (e.ValueVector == null || e.ValueVector.Length != 2)
throw new Exception("e.ValueVector should not be null and have length of 2 for AnalogStick events");
int vX = e.ValueVector[0];
int vY = e.ValueVector[1];
switch (type)
{
case ElementType.AnalogStick:
if ((e.Side == Side.Left && elementViewModel.SelectedSide == Side.Left)||
(e.Side == Side.Right && elementViewModel.SelectedSide == Side.Right))
{
int radius = elementViewModel.Radius;
int shiftX = (int)(((double)vX / (65535.0 / 2)) * (double)radius);
int shiftY = -(int)(((double)vY / (65535.0 / 2)) * (double)radius);
elementViewModel.OffsetX = shiftX;
elementViewModel.OffsetY = shiftY;
}
break;
}
break;
case InputDevice.XInputTrigger:
case InputDevice.SDLTrigger:
if (!e.GamepadUserIdx.HasValue)
throw new Exception("e.GamepadUserIdx should not be null for GamepadButton events");
if (e.GamepadUserIdx.Value != elementViewModel.GamepadUserIndex) break;
if (!e.Value.HasValue)
throw new Exception("e.Value should not be null for GamepadTrigger events");
int value = e.Value.Value;
switch (type)
{
case ElementType.GamepadTrigger:
if ((e.Side == Side.Left && elementViewModel.SelectedSide == Side.Left) ||
(e.Side == Side.Right && elementViewModel.SelectedSide == Side.Right))
{
int frameRange = TRIGGER_CACHE_RANGE; //0-9, total 10 frames
int threshold = (int)(0.1 * 255);
bool triggerMode = elementViewModel.IsTriggerMode;
int triggerFrame = (value >= threshold) ? frameRange : 0;
int direction = (int)elementViewModel.SelectedDirection;
int index = triggerMode? triggerFrame:Math.Min((int)((value / 255.0) * frameRange), frameRange);
//Debug.WriteLine($"value:{value}, index:{index}");
elementViewModel.CurrentSourceIdx = (frameRange + 1) * (direction - 1) + index;
}
break;
}
break;
case InputDevice.SDLGuide:
if (!e.GamepadUserIdx.HasValue)
throw new Exception("e.GamepadUserIdx should not be null for GamepadButton events");
if (e.GamepadUserIdx.Value != elementViewModel.GamepadUserIndex) break;
if (!e.State.HasValue)
throw new Exception("e.State should not be null for GamepadGuide events");
if (e.State.Value == ButtonState.Pressed)
elementViewModel.CurrentSourceIdx = 4;
else
elementViewModel.CurrentSourceIdx = (int)elementViewModel.GamepadUserIndex;
break;
default:
throw new Exception("Unknown Input Device");
}
}
//单独处理 Button 类型事件
switch (e.Device)
{
case InputDevice.MouseButton:
if (!e.MouseButton.HasValue)
throw new Exception("e.button should not be null for MouseButton events");
if (!e.State.HasValue)
throw new Exception("e.State should not be null for MouseButton events");
if (_mouseDictionary.ContainsKey(e.MouseButton.Value))
{
for (int i = 0; i < _mouseDictionary[e.MouseButton.Value].Count; i++)
{
int idx = _mouseDictionary[e.MouseButton.Value][i];
var elementViewModel = ElementViewModels[idx];
SetButtonState(elementViewModel, e.State.Value);
}
}
break;
case InputDevice.MouseWheel:
case InputDevice.MouseMove:
case InputDevice.XInputTrigger:
case InputDevice.XInputStick:
case InputDevice.SDLStick:
case InputDevice.SDLTrigger:
case InputDevice.SDLGuide:
break;
case InputDevice.XInputButton:
case InputDevice.SDLButton:
if (!e.GamepadUserIdx.HasValue)
throw new Exception("e.GamepadUserIdx should not be null for GamepadButton events");
if (!e.Flag.HasValue)
throw new Exception("e.flag should not be null for GamepadButton events");
if (!e.State.HasValue)
throw new Exception("e.State should not be null for GamepadButton events");
if (_gamepadButtonDictionary.ContainsKey(e.Flag.Value))
{
for (int i = 0; i < _gamepadButtonDictionary[e.Flag.Value].Count; i++)
{
int idx = _gamepadButtonDictionary[e.Flag.Value][i];
var elementViewModel = ElementViewModels[idx];
if (e.GamepadUserIdx.Value != elementViewModel.GamepadUserIndex) break;
SetButtonState(elementViewModel, e.State.Value);
}
}
break;
case InputDevice.Keyboard:
if (!e.State.HasValue)
throw new Exception("e.State should not be null for Keyboard events");
if (!e.KeyBoardMappingType.HasValue)
throw new Exception("e.KeyBoardMappingType should not be null for Keyboard events");
switch (e.KeyBoardMappingType.Value)
{
case KeyBoardMappingType.Windows:
if (!e.Key.HasValue)
throw new Exception("e.Key should not be null for Keyboard events");
if (_keyboardDictionary.ContainsKey(e.Key.Value))
{
for (int i = 0; i < _keyboardDictionary[e.Key.Value].Count; i++)
{
int idx = _keyboardDictionary[e.Key.Value][i];
var elementViewModel = ElementViewModels[idx];
SetButtonState(elementViewModel, e.State.Value);
}
}
break;
case KeyBoardMappingType.BIOS:
if (!e.RawKey.HasValue)
throw new Exception("e.RawKey should not be null for Keyboard events");
if (_rawKeyboardDictionary.ContainsKey(e.RawKey.Value))
{
for (int i = 0; i < _rawKeyboardDictionary[e.RawKey.Value].Count; i++)
{
int idx = _rawKeyboardDictionary[e.RawKey.Value][i];
var elementViewModel = ElementViewModels[idx];
SetButtonState(elementViewModel, e.State.Value);
}
}
break;
default:
throw new Exception("Unknown KeyBoardMappingType");
}
break;
default:
throw new Exception("Unknown Input Device");
}
}
private void SetButtonState(ElementViewModel elementViewModel, ButtonState state)
{
if (!elementViewModel.UseStopwatch)
{
elementViewModel.CurrentSourceIdx = (int)state;
}
else if (state == ButtonState.Pressed)
{
elementViewModel.CurrentSourceIdx = 1;
_buttonRuning.Add(elementViewModel);
elementViewModel.Stopwatch.Restart();
}
}
}
}
+95
View File
@@ -0,0 +1,95 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using System.Text;
using System.Threading.Tasks;
namespace FancyInput.Models
{
public static class InputMacro
{
public static void MouseMoveAnimation(Vector2 from, Vector2 to, int steps, int duration)
{
Vector2 step = (to - from) / steps;
int stepDuration = Math.Max(1, duration / steps);
Task.Run(() =>
{
InputGenerator.SendMouseMove((int)from.X, (int)from.Y);
for (int i = 1; i < steps + 1; i++)
{
Thread.Sleep(stepDuration);
Vector2 currentPosition = from + step * i;
InputGenerator.SendMouseMove((int)currentPosition.X, (int)currentPosition.Y);
}
});
}
public static void MouseWheelAnimation(bool isUp, int steps, int duration)
{
int stepDuration = Math.Max(1, duration / steps);
Task.Run(() =>
{
for (int i = 0; i < steps; i++)
{
Thread.Sleep(stepDuration);
InputGenerator.SendMouseWheel(isUp? 120 : -120);
}
});
}
public static void GamepadTriggerAnimation(Side side,int from, int to, int steps, int duration)
{
int step = (to - from) / steps;
int stepDuration = Math.Max(1, duration / steps);
Task.Run(() =>
{
InputGenerator.SendXInpuTrigger(from, side);
for (int i = 1; i < steps + 1; i++)
{
Thread.Sleep(stepDuration);
int currentValue = from + step * i;
InputGenerator.SendXInpuTrigger(currentValue, side);
}
});
}
public static void GamepadstickAnimation(Side side, Vector2 from, Vector2 to, int steps, int duration)
{
Vector2 step = (to - from) / steps;
int stepDuration = Math.Max(1, duration / steps);
Task.Run(() =>
{
InputGenerator.SendXInputStick((int)from.X, (int)from.Y, side);
for (int i = 1; i < steps + 1; i++)
{
Thread.Sleep(stepDuration);
Vector2 currentPosition = from + step * i;
InputGenerator.SendXInputStick((int)currentPosition.X, (int)currentPosition.Y, side);
}
});
}
public static void GamepadButtonPress(FIPGamepadButtonflags button, int Duration)
{
Task.Run(() =>
{
InputGenerator.SendXInputButton(button,ButtonState.Pressed);
Thread.Sleep(Duration);
InputGenerator.SendXInputButton(button, ButtonState.Released);
});
}
public static void KeyPress(FipRawKeys key, int Duration)
{
Task.Run(() =>
{
InputGenerator.SendKey(key,ButtonState.Pressed);
Thread.Sleep(Duration);
InputGenerator.SendKey(key, ButtonState.Released);
});
}
}
}
+803
View File
@@ -0,0 +1,803 @@
using SharpDX.XInput;
using System.Diagnostics;
using System.Windows.Forms;
using System.Windows.Threading;
using SDL3;
using Window = System.Windows.Window;
namespace FancyInput.Models
{
public class InputArgs : EventArgs
{
public InputDevice Device { get; set; }
public string Tag { get; set; } = "";
public ButtonState? State { get; set; }
public MoveState? MoveState { get; set; }
public int? Value { get; set; }
public int[]? ValueVector { get; set; }
public bool PreventMouseCentering { get; set; } = false;
public FipKeys? Key { get; set; }
public FipRawKeys? RawKey { get; set; }
public KeyBoardMappingType? KeyBoardMappingType { get; set; }
public Side? Side { get; set; }
public Message? Message { get; set; }
// 保留兼容字段
public FIPGamepadButtonflags? Flag { get; set; }
public FIPGamepadButtonflags? Flags { get; set; }
public MouseButtons? MouseButton { get; set; }
public GamepadUserIdx? GamepadUserIdx { get; set; }
public override string ToString()
{
var sb = new System.Text.StringBuilder();
sb.Append($"Device: {Device}");
if (Flags.HasValue) sb.Append($", Flags: {Flags}");
if (State.HasValue) sb.Append($", State: {State}");
if (MoveState.HasValue) sb.Append($", MoveState: {MoveState}");
if (Value.HasValue) sb.Append($", Value: {Value}");
if (ValueVector != null) sb.Append($", ValueVector: [{string.Join(", ", ValueVector)}]");
if (Key.HasValue) sb.Append($", Key: {Key}");
if (RawKey.HasValue) sb.Append($", RawKey: {RawKey}");
if (KeyBoardMappingType.HasValue) sb.Append($", KeyBoardMappingType: {KeyBoardMappingType}");
if (Side.HasValue) sb.Append($", Side: {Side}");
if (MouseButton.HasValue) sb.Append($", MouseButton: {MouseButton}");
if (GamepadUserIdx.HasValue) sb.Append($", GamepadUserIdx: {GamepadUserIdx}");
if (Message.HasValue) sb.Append($", Message: {Message}");
return sb.ToString();
}
}
public sealed class InputParser : IDisposable
{
private Window? _hookWindow;
private RawInputParser? _globalHook;
public bool NumPadAsArrow { get; set; } = false;
public KeyBoardMappingType KeyBoardMappingType { get; set; } = KeyBoardMappingType.BIOS;
private readonly DispatcherTimer _timer;
private readonly Stopwatch _stopwatch;
private readonly Stopwatch _mouseMoveStopwatch;
public event EventHandler<InputArgs>? GetInput;
private int _mousePosX = 0;
private int _mousePosY = 0;
private int _lastMouseX = 0;
private int _lastMouseY = 0;
private bool _preventMouseCentering = false;
public bool PreventMouseCentering
{
get => _preventMouseCentering;
set
{
_preventMouseCentering = value;
if (_globalHook == null) return;
switch (value)
{
case true:
_globalHook.MouseMove -= GlobalHook_MouseMove;
_globalHook.RawMouseMove -= GlobalHook_RawMouseMove;
_globalHook.RawMouseMove += GlobalHook_RawMouseMove;
break;
case false:
_globalHook.MouseMove -= GlobalHook_MouseMove;
_globalHook.RawMouseMove -= GlobalHook_RawMouseMove;
_globalHook.MouseMove += GlobalHook_MouseMove;
break;
}
}
}
private bool _isWheeling = false;
private bool _isMoving = false;
private bool _isDisposed = false;
public bool IsDisposed => _isDisposed;
private double _timerInterval = 16.67;
public double TimerInterval
{
get => _timerInterval;
set
{
if (value < 5) value = 5;
if (value > 1000) value = 1000;
_timerInterval = value;
if (_isDisposed) return;
_timer.Interval = TimeSpan.FromMilliseconds(_timerInterval);
}
}
public GamepadBackend Backend { get; private set; }
// ==================== XInput State ====================
private readonly List<Controller> _xControllers = new();
private readonly List<int> _xLastLT = new();
private readonly List<int> _xLastRT = new();
private readonly List<int> _xLastLX = new();
private readonly List<int> _xLastLY = new();
private readonly List<int> _xLastRX = new();
private readonly List<int> _xLastRY = new();
private readonly List<FIPGamepadButtonflags> _xLastButtons = new();
// ==================== SDL State ====================
private readonly Dictionary<uint, IntPtr> _sdlGamepads = new();
private readonly Dictionary<uint, SdlPadState> _sdlLastState = new();
private bool _sdlInitialized = false;
private const int XINPUT_STICK_DELTA_THRESHOLD = 20;
private const int SDL_STICK_DELTA_THRESHOLD = 2000;
private const int SDL_TRIGGER_DELTA_THRESHOLD = 5;
private sealed class SdlPadState
{
public readonly bool[] Buttons = new bool[(int)SDL.GamepadButton.Count];
public int LeftX;
public int LeftY;
public int RightX;
public int RightY;
public int LeftTrigger;
public int RightTrigger;
public FIPGamepadButtonflags XFlags = FIPGamepadButtonflags.None;
}
public InputParser(GamepadBackend backend = GamepadBackend.SDL)
{
Backend = backend;
InitKeyboardAndMouse();
// Polling gamepad state at Send priority reduces input latency under heavy render load (e.g., animated GIF overlays).
_timer = new DispatcherTimer(DispatcherPriority.Send) { Interval = TimeSpan.FromMilliseconds(_timerInterval) };
_timer.Tick += Timer_Tick;
_timer.Start();
_stopwatch = new Stopwatch();
_mouseMoveStopwatch = new Stopwatch();
if (Backend == GamepadBackend.XInput)
{
InitXInput();
}
}
public void Dispose()
{
if (_isDisposed) return;
StopTicker();
UnhookKeyboardAndMouse();
ShutdownGamepadBackend();
_hookWindow?.Close();
_isDisposed = true;
GC.SuppressFinalize(this);
}
public void SetGamepadBackend(GamepadBackend backend)
{
if (_isDisposed) return;
if (Backend == backend) return;
ShutdownGamepadBackend();
Backend = backend;
if (Backend == GamepadBackend.XInput)
{
InitXInput();
}
}
public void StartTicker()
{
if (_isDisposed) return;
_timer.Tick -= Timer_Tick;
_timer.Tick += Timer_Tick;
_timer.Start();
}
public void StopTicker()
{
if (_isDisposed) return;
_timer.Tick -= Timer_Tick;
_timer.Stop();
}
private void Timer_Tick(object? sender, EventArgs e)
{
if (_isDisposed) return;
// wheel stop
if (_isWheeling && _stopwatch.ElapsedMilliseconds > 200)
{
_isWheeling = false;
_stopwatch.Stop();
//_stopwatch.Reset();
GetInput?.Invoke(this, new InputArgs
{
Device = InputDevice.MouseWheel,
Tag = "MouseWheelStopped",
MoveState = MoveState.StopMove,
});
}
// mouse move stop
if (_isMoving && _mouseMoveStopwatch.ElapsedMilliseconds > 100)
{
_isMoving = false;
_mouseMoveStopwatch.Stop();
//_stopwatch.Reset();
GetInput?.Invoke(this, new InputArgs
{
Device = InputDevice.MouseMove,
Tag = "MouseMoveStopped",
MoveState = MoveState.StopMove,
PreventMouseCentering = _preventMouseCentering
});
if (_globalHook!=null)
{
_globalHook.MouseX = RawInputParser.ScreenCenterX;
_globalHook.MouseY = RawInputParser.ScreenCenterY;
}
}
if (Backend == GamepadBackend.XInput)
{
PollXInput();
}
else
{
PollSdlInput();
}
}
// ==================== Keyboard / Mouse ====================
private void InitKeyboardAndMouse()
{
_hookWindow = new Window
{
Visibility = System.Windows.Visibility.Hidden,
Width = 0,
Height = 0
};
_hookWindow.Show();
_mousePosX = Cursor.Position.X;
_mousePosY = Cursor.Position.Y;
_lastMouseX = _mousePosX;
_lastMouseY = _mousePosY;
IntPtr handle = new System.Windows.Interop.WindowInteropHelper(_hookWindow).Handle;
_globalHook = new RawInputParser(handle);
_globalHook.KeyDown += GlobalHook_KeyDown;
_globalHook.KeyUp += GlobalHook_KeyUp;
_globalHook.MouseDown += GlobalHook_MouseDown;
_globalHook.MouseUp += GlobalHook_MouseUp;
_globalHook.MouseWheel += GlobalHook_MouseWheel;
PreventMouseCentering = false;
}
private void UnhookKeyboardAndMouse()
{
if (_globalHook == null) return;
_globalHook.KeyDown -= GlobalHook_KeyDown;
_globalHook.KeyUp -= GlobalHook_KeyUp;
_globalHook.MouseDown -= GlobalHook_MouseDown;
_globalHook.MouseUp -= GlobalHook_MouseUp;
_globalHook.MouseWheel -= GlobalHook_MouseWheel;
_globalHook.MouseMove -= GlobalHook_MouseMove;
_globalHook.RawMouseMove -= GlobalHook_RawMouseMove;
_globalHook.Dispose();
_globalHook = null;
}
private void GlobalHook_KeyDown(object? sender, RawInputKeyEventArgs e)
{
if (_isDisposed) return;
FipRawKeys rawKey = e.RawKey;
if ( KeyBoardMappingType == KeyBoardMappingType.Windows && !e.IsNumLockOn &&
RawInputParser.NumLockAffectedFipRawKeys.Contains(rawKey) && !NumPadAsArrow ) return;
GetInput?.Invoke(this, new InputArgs
{
Device = InputDevice.Keyboard,
Tag = e.Key.ToString(),
State = ButtonState.Pressed,
Key = e.Key,
RawKey = rawKey,
KeyBoardMappingType = KeyBoardMappingType
});
}
private void GlobalHook_KeyUp(object? sender, RawInputKeyEventArgs e)
{
if (_isDisposed) return;
FipRawKeys rawKey = e.RawKey;
if (!e.IsNumLockOn &&
RawInputParser.NumLockAffectedFipRawKeys.Contains(rawKey) &&
!NumPadAsArrow &&
KeyBoardMappingType == KeyBoardMappingType.Windows) return;
GetInput?.Invoke(this, new InputArgs
{
Device = InputDevice.Keyboard,
Tag = e.Key.ToString(),
State = 0,
Key = e.Key,
RawKey = rawKey,
KeyBoardMappingType = KeyBoardMappingType
});
}
private void GlobalHook_MouseDown(object? sender, MouseEventArgs e)
{
if (_isDisposed) return;
GetInput?.Invoke(this, new InputArgs
{
Device = InputDevice.MouseButton,
Tag = e.Button.ToString(),
State = ButtonState.Pressed,
MouseButton = e.Button
});
}
private void GlobalHook_MouseUp(object? sender, MouseEventArgs e)
{
if (_isDisposed) return;
GetInput?.Invoke(this, new InputArgs
{
Device = InputDevice.MouseButton,
Tag = e.Button.ToString(),
State = 0,
MouseButton = e.Button
});
}
private void GlobalHook_MouseWheel(object? sender, MouseEventArgs e)
{
if (_isDisposed) return;
MoveState moveState = _isWheeling ? MoveState.Moving : MoveState.StartMove;
_stopwatch.Restart();
_isWheeling = true;
GetInput?.Invoke(this, new InputArgs
{
Device = InputDevice.MouseWheel,
Tag = "MouseWheel",
Value = e.Delta,
MoveState = moveState
});
}
private void GlobalHook_MouseMove(object? sender, MouseEventArgs e)
{
if (_isDisposed || _globalHook == null) return;
_mousePosX = e.X;
_mousePosY = e.Y;
int deltaX = _mousePosX - _lastMouseX;
int deltaY = _mousePosY - _lastMouseY;
if (deltaX != 0 || deltaY != 0)
{
MoveState moveState = _isMoving ? MoveState.Moving : MoveState.StartMove;
_isMoving = true;
_mouseMoveStopwatch.Restart();
GetInput?.Invoke(this, new InputArgs
{
Device = InputDevice.MouseMove,
Tag = "MouseMove",
MoveState = moveState,
ValueVector = new[] { deltaX, deltaY, e.X, e.Y }
});
}
_lastMouseX = _mousePosX;
_lastMouseY = _mousePosY;
}
private void GlobalHook_RawMouseMove(object? sender, RawInputMouseMoveEventArgs e)
{
if (_isDisposed || _globalHook == null) return;
_mousePosX = e.X;
_mousePosY = e.Y;
if (e.DeltaX != 0 || e.DeltaY != 0)
{
MoveState moveState = _isMoving ? MoveState.Moving : MoveState.StartMove;
_isMoving = true;
_mouseMoveStopwatch.Restart();
GetInput?.Invoke(this, new InputArgs
{
Device = InputDevice.MouseMove,
Tag = "MouseMove",
MoveState = moveState,
ValueVector = new[] { e.DeltaX, e.DeltaY, e.X, e.Y }
});
}
_lastMouseX = _mousePosX;
_lastMouseY = _mousePosY;
}
// ==================== XInput Backend ====================
private void InitXInput()
{
_xControllers.Clear();
_xLastLT.Clear();
_xLastRT.Clear();
_xLastLX.Clear();
_xLastLY.Clear();
_xLastRX.Clear();
_xLastRY.Clear();
_xLastButtons.Clear();
for (int i = 0; i < 4; i++)
{
_xControllers.Add(new Controller((UserIndex)i));
_xLastLT.Add(0);
_xLastRT.Add(0);
_xLastLX.Add(0);
_xLastLY.Add(0);
_xLastRX.Add(0);
_xLastRY.Add(0);
_xLastButtons.Add(FIPGamepadButtonflags.None);
}
}
private void PollXInput()
{
for (int i = 0; i < _xControllers.Count; i++)
{
Controller controller = _xControllers[i];
if (!controller.IsConnected) continue;
GamepadUserIdx userIdx = (GamepadUserIdx)i;
var state = controller.GetState();
var buttons = state.Gamepad.Buttons;
FIPGamepadButtonflags btns = (FIPGamepadButtonflags)buttons;
if (btns != _xLastButtons[i])
{
foreach (FIPGamepadButtonflags flag in Enum.GetValues(typeof(FIPGamepadButtonflags)))
{
if (flag == FIPGamepadButtonflags.None) continue;
ButtonState oldPressed = (_xLastButtons[i] & flag) == flag ? ButtonState.Pressed : ButtonState.Released;
ButtonState nowPressed = (btns & flag) == flag ? ButtonState.Pressed : ButtonState.Released;
//FIPGamepadButtonflags
if (oldPressed != nowPressed)
{
GetInput?.Invoke(this, new InputArgs
{
Device = InputDevice.XInputButton,
GamepadUserIdx = userIdx,
Tag = flag.ToString(),
State = nowPressed,
Flag = flag,
Flags = btns
});
}
}
_xLastButtons[i] = btns;
}
byte lt = state.Gamepad.LeftTrigger;
byte rt = state.Gamepad.RightTrigger;
if (lt != _xLastLT[i])
{
GetInput?.Invoke(this, new InputArgs
{
Device = InputDevice.XInputTrigger,
GamepadUserIdx = userIdx,
Tag = "LeftTrigger",
Side = Side.Left,
Value = lt
});
_xLastLT[i] = lt;
}
if (rt != _xLastRT[i])
{
GetInput?.Invoke(this, new InputArgs
{
Device = InputDevice.XInputTrigger,
GamepadUserIdx = userIdx,
Tag = "RightTrigger",
Side = Side.Right,
Value = rt
});
_xLastRT[i] = rt;
}
short lx = state.Gamepad.LeftThumbX;
short ly = state.Gamepad.LeftThumbY;
short rx = state.Gamepad.RightThumbX;
short ry = state.Gamepad.RightThumbY;
if (Math.Abs(lx - _xLastLX[i]) > XINPUT_STICK_DELTA_THRESHOLD ||
Math.Abs(ly - _xLastLY[i]) > XINPUT_STICK_DELTA_THRESHOLD)
{
GetInput?.Invoke(this, new InputArgs
{
Device = InputDevice.XInputStick,
GamepadUserIdx = userIdx,
Tag = "LeftThumb",
Side = Side.Left,
ValueVector = [lx, ly]
});
_xLastLX[i] = lx;
_xLastLY[i] = ly;
}
if (Math.Abs(rx - _xLastRX[i]) > XINPUT_STICK_DELTA_THRESHOLD ||
Math.Abs(ry - _xLastRY[i]) > XINPUT_STICK_DELTA_THRESHOLD)
{
GetInput?.Invoke(this, new InputArgs
{
Device = InputDevice.XInputStick,
GamepadUserIdx = userIdx,
Tag = "RightThumb",
Side = Side.Right,
ValueVector = [rx, ry]
});
_xLastRX[i] = rx;
_xLastRY[i] = ry;
}
}
}
// ==================== SDL Backend ====================
private bool EnsureSdl3()
{
if (_sdlInitialized) return true;
if (!SDL.Init(SDL.InitFlags.Gamepad))
return false;
_sdlInitialized = true;
return true;
}
private void PollSdlInput()
{
if (!EnsureSdl3()) return;
SDL.UpdateGamepads();
var ids = SDL.GetGamepads(out int count);
if (ids == null || count <= 0) return;
HashSet<uint> activeIds = new();
foreach (uint id in ids) activeIds.Add(id);
foreach (uint id in ids)
{
if (!_sdlGamepads.TryGetValue(id, out IntPtr pad) || pad == IntPtr.Zero)
{
pad = SDL.OpenGamepad(id);
if (pad == IntPtr.Zero) continue;
_sdlGamepads[id] = pad;
}
if (!_sdlLastState.TryGetValue(id, out SdlPadState? last))
{
last = new SdlPadState();
_sdlLastState[id] = last;
}
int playerIndex = SDL.GetGamepadPlayerIndex(pad);
GamepadUserIdx? userIdx = (playerIndex >= 0 && playerIndex <= 3)
? (GamepadUserIdx)playerIndex
: (GamepadUserIdx?)null;
// buttons
for (int bi = 0; bi < (int)SDL.GamepadButton.Count; bi++)
{
SDL.GamepadButton btn = (SDL.GamepadButton)bi;
bool nowPressed = SDL.GetGamepadButton(pad, btn);
bool oldPressed = last.Buttons[bi];
if (nowPressed == oldPressed) continue;
last.Buttons[bi] = nowPressed;
if (TryMapSdlButtonToSDipButton(btn, out FIPGamepadButtonflags mappedFlag))
{
if (nowPressed) last.XFlags |= mappedFlag;
else last.XFlags &= ~mappedFlag;
}
if (btn == SDL.GamepadButton.Guide)
{
GetInput?.Invoke(this, new InputArgs
{
Device = InputDevice.SDLGuide,
GamepadUserIdx = userIdx,
Tag = "Guide",
State = nowPressed ? ButtonState.Pressed : ButtonState.Released
});
}
else
{
GetInput?.Invoke(this, new InputArgs
{
Device = InputDevice.SDLButton,
GamepadUserIdx = userIdx,
Tag = btn.ToString(),
State = nowPressed ? ButtonState.Pressed : ButtonState.Released,
Flag = TryMapSdlButtonToSDipButton(btn, out var flag) ? flag : null,
Flags = last.XFlags
});
}
}
const int SDL_AXIS_MAX = 32767;
const int SDL_AXIS_MIN = -32768;
const int SDL_TRIGGER_MAX = 32767;
const int SDL_TRIGGER_MIN = 0;
// left stick
int lx_raw = SDL.GetGamepadAxis(pad, SDL.GamepadAxis.LeftX);
int ly_raw = SDL.GetGamepadAxis(pad, SDL.GamepadAxis.LeftY);
int lx = Math.Clamp(lx_raw, SDL_AXIS_MIN, SDL_AXIS_MAX);
int ly = -Math.Clamp(ly_raw, SDL_AXIS_MIN, SDL_AXIS_MAX); // 反转Y轴
if (Math.Abs(lx - last.LeftX) > SDL_STICK_DELTA_THRESHOLD ||
Math.Abs(ly - last.LeftY) > SDL_STICK_DELTA_THRESHOLD)
{
GetInput?.Invoke(this, new InputArgs
{
Device = InputDevice.SDLStick,
GamepadUserIdx = userIdx,
Tag = "LeftThumb",
Side = Side.Left,
ValueVector = [lx, ly]
});
last.LeftX = lx;
last.LeftY = ly;
}
// right stick
int rx_raw = SDL.GetGamepadAxis(pad, SDL.GamepadAxis.RightX);
int ry_raw = SDL.GetGamepadAxis(pad, SDL.GamepadAxis.RightY);
int rx = Math.Clamp(rx_raw, SDL_AXIS_MIN, SDL_AXIS_MAX);
int ry = -Math.Clamp(ry_raw, SDL_AXIS_MIN, SDL_AXIS_MAX); // 反转Y轴
if (Math.Abs(rx - last.RightX) > SDL_STICK_DELTA_THRESHOLD ||
Math.Abs(ry - last.RightY) > SDL_STICK_DELTA_THRESHOLD)
{
GetInput?.Invoke(this, new InputArgs
{
Device = InputDevice.SDLStick,
GamepadUserIdx = userIdx,
Tag = "RightThumb",
Side = Side.Right,
ValueVector = [rx, ry]
});
last.RightX = rx;
last.RightY = ry;
}
// triggers
int lt_raw = SDL.GetGamepadAxis(pad, SDL.GamepadAxis.LeftTrigger);
int rt_raw = SDL.GetGamepadAxis(pad, SDL.GamepadAxis.RightTrigger);
byte lt = (byte)(Math.Clamp(lt_raw, SDL_TRIGGER_MIN, SDL_TRIGGER_MAX) * 255 / SDL_TRIGGER_MAX);
byte rt = (byte)(Math.Clamp(rt_raw, SDL_TRIGGER_MIN, SDL_TRIGGER_MAX) * 255 / SDL_TRIGGER_MAX);
if (Math.Abs(lt - last.LeftTrigger) > SDL_TRIGGER_DELTA_THRESHOLD)
{
GetInput?.Invoke(this, new InputArgs
{
Device = InputDevice.SDLTrigger,
GamepadUserIdx = userIdx,
Tag = "LeftTrigger",
Side = Side.Left,
Value = lt
});
last.LeftTrigger = lt;
}
if (Math.Abs(rt - last.RightTrigger) > SDL_TRIGGER_DELTA_THRESHOLD)
{
GetInput?.Invoke(this, new InputArgs
{
Device = InputDevice.SDLTrigger,
GamepadUserIdx = userIdx,
Tag = "RightTrigger",
Side = Side.Right,
Value = rt
});
last.RightTrigger = rt;
}
}
// remove disconnected
List<uint> toRemove = new();
foreach (var kv in _sdlGamepads)
{
if (!activeIds.Contains(kv.Key))
{
SDL.CloseGamepad(kv.Value);
toRemove.Add(kv.Key);
}
}
foreach (uint id in toRemove)
{
_sdlGamepads.Remove(id);
_sdlLastState.Remove(id);
}
}
private static bool TryMapSdlButtonToSDipButton(SDL.GamepadButton btn, out FIPGamepadButtonflags flag)
{
flag = FIPGamepadButtonflags.None;
switch (btn)
{
case SDL.GamepadButton.South: flag = FIPGamepadButtonflags.A; return true;
case SDL.GamepadButton.East: flag = FIPGamepadButtonflags.B; return true;
case SDL.GamepadButton.West: flag = FIPGamepadButtonflags.X; return true;
case SDL.GamepadButton.North: flag = FIPGamepadButtonflags.Y; return true;
case SDL.GamepadButton.Back: flag = FIPGamepadButtonflags.Select; return true;
case SDL.GamepadButton.Start: flag = FIPGamepadButtonflags.Start; return true;
case SDL.GamepadButton.LeftShoulder: flag = FIPGamepadButtonflags.LB; return true;
case SDL.GamepadButton.RightShoulder: flag = FIPGamepadButtonflags.RB; return true;
case SDL.GamepadButton.LeftStick: flag = FIPGamepadButtonflags.LS; return true;
case SDL.GamepadButton.RightStick: flag = FIPGamepadButtonflags.RS; return true;
case SDL.GamepadButton.DPadUp: flag = FIPGamepadButtonflags.Up; return true;
case SDL.GamepadButton.DPadDown: flag = FIPGamepadButtonflags.Down; return true;
case SDL.GamepadButton.DPadLeft: flag = FIPGamepadButtonflags.Left; return true;
case SDL.GamepadButton.DPadRight: flag = FIPGamepadButtonflags.Right; return true;
default: return false;
}
}
private void ShutdownSdl3()
{
foreach (var kv in _sdlGamepads)
{
SDL.CloseGamepad(kv.Value);
}
_sdlGamepads.Clear();
_sdlLastState.Clear();
if (_sdlInitialized)
{
SDL.Quit();
_sdlInitialized = false;
}
}
private void ShutdownGamepadBackend()
{
if (Backend == GamepadBackend.SDL)
{
ShutdownSdl3();
}
else
{
_xControllers.Clear();
_xLastLT.Clear();
_xLastRT.Clear();
_xLastLX.Clear();
_xLastLY.Clear();
_xLastRX.Clear();
_xLastRY.Clear();
_xLastButtons.Clear();
}
}
}
}
+362
View File
@@ -0,0 +1,362 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using FancyInput.ViewModels;
namespace FancyInput.Models
{
public enum CommandBackEnd
{
Cmd,
PowerShell
}
public static class MacroActionPlayer
{
public static async Task<int> Run(this MacroAction action,
Action<string> stdoutCallback,
Action<string> stderrCallback,
string? workingDirectory = null,
CancellationToken cancellationToken = default,
CommandBackEnd backEnd = CommandBackEnd.Cmd)
{
switch (action)
{
case MacroActionCommand cmdAction:
var (exitCode, _) = await RunCommandAction(cmdAction, stdoutCallback, stderrCallback, workingDirectory, cancellationToken, backEnd);
return exitCode;
case MacroActionKeyboardButton keyboardAction:
return await RunKeyboardButtonAction(keyboardAction, stderrCallback, cancellationToken);
case MacroActionMouseButton mouseButtonAction:
return await RunMouseButtonAction(mouseButtonAction, stderrCallback, cancellationToken);
case MacroActionMouseMove mouseMoveAction:
return await RunMouseMoveAction(mouseMoveAction, stderrCallback, cancellationToken);
case MacroActionMouseWheel mouseWheelAction:
return await RunMouseWheelAction(mouseWheelAction, stderrCallback, cancellationToken);
case MacroActionDelay delayAction:
return await RunDelayAction(delayAction, stderrCallback, cancellationToken);
default:
stderrCallback?.Invoke($"未知的动作类型: {action.GetType().Name}");
return -1;
}
}
public static async Task<(int ExitCode, System.Diagnostics.Process? Process)> RunCommandAction(
MacroActionCommand action,
Action<string> stdoutCallback,
Action<string> stderrCallback,
string? workingDirectory = null,
CancellationToken cancellationToken = default,
CommandBackEnd backEnd = CommandBackEnd.Cmd)
{
string command = action.Command;
bool waitUntilExit = action.WaitUntilExit;
System.Diagnostics.Process? process = null;
bool shouldDispose = false;
try
{
if (action.PreWait > 0)
await Task.Delay(action.PreWait, cancellationToken);
process = new System.Diagnostics.Process();
if (backEnd == CommandBackEnd.PowerShell)
{
process.StartInfo.FileName = "powershell.exe";
process.StartInfo.Arguments = $"-NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass -Command \"{command}\"";
}
else
{
process.StartInfo.FileName = "cmd.exe";
process.StartInfo.Arguments = $"/C {command}";
}
process.StartInfo.RedirectStandardOutput = true;
process.StartInfo.RedirectStandardError = true;
process.StartInfo.UseShellExecute = false;
process.StartInfo.CreateNoWindow = true;
if (!string.IsNullOrEmpty(workingDirectory))
{
process.StartInfo.WorkingDirectory = workingDirectory;
}
process.OutputDataReceived += (sender, e) =>
{
if (e.Data != null) stdoutCallback?.Invoke(e.Data);
};
process.ErrorDataReceived += (sender, e) =>
{
if (e.Data != null) stderrCallback?.Invoke(e.Data);
};
process.Start();
process.BeginOutputReadLine();
process.BeginErrorReadLine();
int exitCode = 0;
if (waitUntilExit)
{
shouldDispose = true;
await process.WaitForExitAsync(cancellationToken);
exitCode = process.ExitCode;
}
if (action.PostWait > 0)
await Task.Delay(action.PostWait, cancellationToken);
return (exitCode, shouldDispose ? null : process);
}
catch (OperationCanceledException)
{
if (process != null && !process.HasExited)
{
try { process.Kill(); } catch { }
}
shouldDispose = true;
return (-2, null);
}
catch (Exception ex)
{
stderrCallback?.Invoke($"执行命令出错: {ex.Message}");
shouldDispose = true;
return (-1, null);
}
finally
{
if (shouldDispose)
process?.Dispose();
}
}
public static async Task<int> RunKeyboardButtonAction(
MacroActionKeyboardButton action,
Action<string> stderrCallback,
CancellationToken cancellationToken = default)
{
FipRawKeys key = action.Key;
bool downSent = false;
bool upSent = false;
bool down = action.Down;
bool up = action.Up;
int duration = action.Duration;
int repeat = action.RepeatCount>1&&down&&up ? action.RepeatCount : 1; // 如果没有 Down 或 Up,则不重复
try
{
for (int i = 0; i < repeat; i++)
{
downSent = false;
upSent = false;
if (action.PreWait > 0)
await Task.Delay(action.PreWait, cancellationToken);
if (down)
{
InputGenerator.SendKey(key, ButtonState.Pressed);
downSent = true;
}
await Task.Delay(duration, cancellationToken);
if (up)
{
InputGenerator.SendKey(key, ButtonState.Released);
upSent = true;
}
if (action.PostWait > 0)
await Task.Delay(action.PostWait, cancellationToken);
}
return 0; // Success
}
catch (OperationCanceledException)
{
// Down 已发送、Up 尚未发送、且原本计划了 Up → 补发 Release
if (downSent && !upSent && action.Up)
{
try { InputGenerator.SendKey(action.Key, ButtonState.Released); } catch { }
}
return -2;
}
catch (Exception ex)
{
stderrCallback?.Invoke($"执行键盘动作出错: {ex.Message}");
return -1;
}
}
public static async Task<int> RunMouseButtonAction(
MacroActionMouseButton action,
Action<string> stderrCallback,
CancellationToken cancellationToken = default)
{
MouseButtons button = action.Button;
bool down = action.Down;
bool up = action.Up;
int duration = action.Duration;
bool downSent = false;
bool upSent = false;
int repeat = action.RepeatCount > 1 && action.Down && action.Up ? action.RepeatCount : 1; // 如果没有 Down 或 Up,则不重复
try
{
for ( int i = 0; i<repeat;i++)
{
downSent = false;
upSent = false;
if (action.PreWait > 0)
await Task.Delay(action.PreWait, cancellationToken);
if (down)
{
InputGenerator.SendMouseButton(button, ButtonState.Pressed);
downSent = true;
}
await Task.Delay(duration, cancellationToken);
if (up)
{
InputGenerator.SendMouseButton(button, ButtonState.Released);
upSent = true;
}
if (action.PostWait > 0)
await Task.Delay(action.PostWait, cancellationToken);
}
return 0; // Success
}
catch (OperationCanceledException)
{
// Down 已发送、Up 尚未发送、且原本计划了 Up → 补发 Release
if (downSent && !upSent && action.Up)
{
try { InputGenerator.SendMouseButton(action.Button, ButtonState.Released); } catch { }
}
return -2;
}
catch (Exception ex)
{
stderrCallback?.Invoke($"执行鼠标动作出错: {ex.Message}");
return -1;
}
}
public static async Task<int> RunMouseMoveAction(
MacroActionMouseMove action,
Action<string> stderrCallback,
CancellationToken cancellationToken = default)
{
try
{
if (action.PreWait > 0)
await Task.Delay(action.PreWait, cancellationToken);
int FromX = action.FromX;
int FromY = action.FromY;
int ToX = action.ToX;
int ToY = action.ToY;
int steps = Math.Max(1, action.Steps);
int duration = Math.Max(0, action.Duration);
int stepDelay = (int)Math.Max(1, (double)duration / steps);
MousMoveMode mouseMoveMode = action.MoveMode;
if (steps == 1 && mouseMoveMode == MousMoveMode.Absolute)
InputGenerator.SendMouseMove(ToX, ToY, absolute:true);
else if (steps == 1 && mouseMoveMode == MousMoveMode.Relative)
InputGenerator.SendMouseMove(ToX - FromX, ToY - FromY, absolute: false);
else if (mouseMoveMode == MousMoveMode.Absolute)
{
InputGenerator.SendMouseMove(FromX, FromY, absolute: true);
for (int i = 1; i < steps+1; i++)
{
double ratio = (double)i / steps;
int x = (int)(FromX * (1 - ratio) + ToX * ratio);
int y = (int)(FromY * (1 - ratio) + ToY * ratio);
await Task.Delay(stepDelay, cancellationToken);
InputGenerator.SendMouseMove(x, y, absolute: true);
}
}
else if (mouseMoveMode == MousMoveMode.Relative)
{
int Dx = ToX - FromX;
int Dy = ToY - FromY;
int dx = (int)(Dx / (double)steps);
int dy = (int)(Dy / (double)steps);
for (int i = 1; i < steps + 1; i++)
{
await Task.Delay(stepDelay, cancellationToken);
// 最后一步补偿整数截断的累积误差
if (i == steps)
InputGenerator.SendMouseMove(Dx - dx * (steps - 1), Dy - dy * (steps - 1), absolute: false);
else
InputGenerator.SendMouseMove(dx, dy, absolute: false);
}
}
if (action.PostWait > 0)
await Task.Delay(action.PostWait, cancellationToken);
return 0; // Success
}
catch (OperationCanceledException)
{
return -2;
}
catch (Exception ex)
{
stderrCallback?.Invoke($"执行鼠标移动动作出错: {ex.Message}");
return -1;
}
}
public static async Task<int> RunMouseWheelAction(
MacroActionMouseWheel action,
Action<string> stderrCallback,
CancellationToken cancellationToken = default)
{
try
{
if (action.PreWait > 0)
await Task.Delay(action.PreWait, cancellationToken);
MouseWheelDirection direction = action.WheelDirection;
int delta = direction == MouseWheelDirection.Up ? 120 : -120;
int steps = action.Steps;
int duration = Math.Max(0, action.Duration);
int stepDelay = (int)Math.Max(1, (double)duration / steps);
if (steps == 1)
{
InputGenerator.SendMouseWheel(delta);
}
else
{
for (int i = 0; i < steps; i++)
{
await Task.Delay(stepDelay, cancellationToken);
InputGenerator.SendMouseWheel(delta);
}
}
if (action.PostWait > 0)
await Task.Delay(action.PostWait, cancellationToken);
return 0;
}
catch (OperationCanceledException)
{
return -2;
}
catch (Exception ex)
{
stderrCallback?.Invoke($"执行鼠标滚轮动作出错: {ex.Message}");
return -1;
}
}
public static async Task<int> RunDelayAction(
MacroActionDelay action,
Action<string> stderrCallback,
CancellationToken cancellationToken = default)
{
try
{
if (action.PreWait > 0)
await Task.Delay(action.PreWait, cancellationToken);
int duration = Math.Max(0, action.Duration);
await Task.Delay(duration, cancellationToken);
if (action.PostWait > 0)
await Task.Delay(action.PostWait, cancellationToken);
return 0; // Success
}
catch (OperationCanceledException)
{
return -2;
}
catch (Exception ex)
{
stderrCallback?.Invoke($"执行延迟动作出错: {ex.Message}");
return -1;
}
}
}
}
+27
View File
@@ -0,0 +1,27 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace FancyInput.Models
{
public enum TriggerMode
{
[Description("按下时触发一次")]
Once,
[Description("按住时触发并循环")]
Hold,
[Description("按下时触发并循环,再次按下将其关闭")]
Toggle
}
public enum TriggerTiming
{
[Description("按下时")]
Press,
[Description("抬起时")]
Release
}
}
+96
View File
@@ -0,0 +1,96 @@
using System.IO;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace FancyInput.Models
{
public class JsonIntOrStringConverter : JsonConverter<int?>
{
public override int? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
return reader.TokenType switch
{
JsonTokenType.Number => reader.GetInt32(),
JsonTokenType.String => int.TryParse(reader.GetString(), out var v) ? v : throw new JsonException(),
JsonTokenType.Null => null,
_ => throw new JsonException()
};
}
public override void Write(Utf8JsonWriter writer, int? value, JsonSerializerOptions options)
{
if (value.HasValue)
writer.WriteNumberValue(value.Value);
else
writer.WriteNullValue();
}
}
public class OverlayElement
{
public ElementType type { get; set; }
public string id { get; set; } = "";
public int[] pos { get; set; } = Array.Empty<int>();
public int[] mapping { get; set; } = Array.Empty<int>();
[JsonConverter(typeof(JsonIntOrStringConverter))]
public int? z_level { get; set; }
public int? code { get; set; }
public bool? trigger_mode { get; set; }
public Side? side { get; set; }
public Direction? direction { get; set; }
public int? stick_radius { get; set; }
public MouseMoveType? mouse_type { get; set; }
public int? mouse_radius { get; set; }
public override string ToString()
{
return $"Type: {type}, ID: {id}, code:{code}";
}
public string ToJSON()
{
var options = new JsonSerializerOptions
{
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
WriteIndented = true
};
return JsonSerializer.Serialize(this, options);
}
}
public class OverlayRoot
{
public int default_width { get; set; }
public int default_height { get; set; }
public int? space_h { get; set; }
public int? space_v { get; set; }
public int? flags { get; set; }
public int overlay_width { get; set; }
public int overlay_height { get; set; }
public List<OverlayElement> elements { get; set; } = new();
public string ToJSON()
{
var options = new JsonSerializerOptions
{
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
WriteIndented = true
};
return JsonSerializer.Serialize(this, options);
}
}
internal class OverlayParser
{
public static OverlayRoot Parse(string path)
{
var json = File.ReadAllText(path);
var result = JsonSerializer.Deserialize<OverlayRoot>(json);
if (result != null)
return result;
else
throw new Exception("Failed to parse overlay JSON.");
}
}
}
+653
View File
@@ -0,0 +1,653 @@
using System;
using System.Diagnostics;
using System.Numerics;
using System.Runtime.InteropServices;
using System.Threading;
using System.Windows.Forms;
using System.Windows.Interop;
using System.Windows.Threading;
using Gma.System.MouseKeyHook;
using Application = System.Windows.Application;
using Timer = System.Threading.Timer;
namespace FancyInput.Models
{
public class RawInputKeyEventArgs : EventArgs
{
public ushort VKeyCode { get; }
public ushort ScanCode { get; }
public FipKeys Key { get; private set; } = FipKeys.None;
public FipRawKeys RawKey { get; private set; } = FipRawKeys.None;
public bool IsE0 { get; }
public bool IsKeyDown { get; }
// 新增修饰键状态
public bool IsCtrlDown { get; }
public bool IsShiftDown { get; }
public bool IsAltDown { get; }
public bool IsWinDown { get; }
public bool IsFnDown { get; } // Fn键无法直接检测,通常为false
// 新增锁定键状态
public bool IsNumLockOn { get; }
public bool IsCapsLockOn { get; }
public bool IsScrollLockOn { get; }
public RawInputKeyEventArgs(
ushort vkey, ushort scancode, bool isE0, bool isKeyDown,
bool isCtrlDown, bool isShiftDown, bool isAltDown, bool isWinDown, bool isFnDown = false)
{
VKeyCode = vkey;
ScanCode = scancode;
IsE0 = isE0;
IsKeyDown = isKeyDown;
IsCtrlDown = isCtrlDown;
IsShiftDown = isShiftDown;
IsAltDown = isAltDown;
IsWinDown = isWinDown;
IsFnDown = isFnDown;
Key = RawInputParser.MapRawInputToCustomKeys(vkey, scancode, isE0);
RawKey = RawInputParser.MapRawInputToCustomRawKeys(vkey, scancode, isE0);
// 检测Lock状态
IsNumLockOn = Control.IsKeyLocked(Keys.NumLock);
IsCapsLockOn = Control.IsKeyLocked(Keys.CapsLock);
IsScrollLockOn = Control.IsKeyLocked(Keys.Scroll);
}
}
public class RawInputMouseMoveEventArgs : EventArgs
{
public int DeltaX { get; }
public int DeltaY { get; }
public int X { get; }
public int Y { get; }
public RawInputMouseMoveEventArgs(int deltaX, int deltaY, int x, int y)
{
DeltaX = deltaX;
DeltaY = deltaY;
X = x;
Y = y;
}
}
public class RawInputParser : IDisposable
{
public static readonly int ScreenH = Screen.PrimaryScreen!.Bounds.Height;
public static readonly int ScreenW = Screen.PrimaryScreen!.Bounds.Width;
public static readonly int ScreenCenterX = ScreenW / 2;
public static readonly int ScreenCenterY = ScreenH / 2;
private static IntPtr _hwnd;
private HwndSource? _source;
private IKeyboardMouseEvents? _globalHook;
private IntPtr _rawBuffer = IntPtr.Zero;
private int _rawBufferSize = 0;
private int _mouseDeltaX = 0;
private int _mouseDeltaY = 0;
private int _pendingMouseDeltaX = 0;
private int _pendingMouseDeltaY = 0;
private int _mouseDispatchQueued = 0;
public int MouseX;
public int MouseY;
private Timer? _mouseMoveTimer;
private int _isDisposedFlag = 0;
public event EventHandler<RawInputKeyEventArgs>? KeyDown;
public event EventHandler<RawInputKeyEventArgs>? KeyUp;
public event MouseEventHandler? MouseDown;
public event MouseEventHandler? MouseUp;
public event MouseEventHandler? MouseWheel;
public event MouseEventHandler? MouseMove;
public event EventHandler<RawInputMouseMoveEventArgs>? RawMouseMove;
public static readonly HashSet<FipRawKeys> NumLockAffectedFipRawKeys = new()
{
FipRawKeys.NumPad0,
FipRawKeys.NumPad1,
FipRawKeys.NumPad2,
FipRawKeys.NumPad3,
FipRawKeys.NumPad4,
FipRawKeys.NumPad5,
FipRawKeys.NumPad6,
FipRawKeys.NumPad7,
FipRawKeys.NumPad8,
FipRawKeys.NumPad9,
};
public RawInputParser(IntPtr hwnd, int moseMoveTickMiniSecond = 16)
{
_hwnd = hwnd;
_rawBuffer = Marshal.AllocHGlobal(RAWINPUT_INITIAL_BUFFER_SIZE);
_rawBufferSize = RAWINPUT_INITIAL_BUFFER_SIZE;
_source = HwndSource.FromHwnd(_hwnd);
_source.AddHook(WndProc);
RegisterRawInpuKeyboard();
RegisterRawInputMouse();
// 鼠标事件使用GlobalMouseKeyHook
_globalHook = Hook.GlobalEvents();
_globalHook.MouseDownExt += (s, e) => MouseDown?.Invoke(s, e);
_globalHook.MouseUpExt += (s, e) => MouseUp?.Invoke(s, e);
_globalHook.MouseWheel += (s, e) => MouseWheel?.Invoke(s, e);
_globalHook.MouseMove += (s, e) => MouseMove?.Invoke(s, e);
int intervalMs = Math.Max(1, moseMoveTickMiniSecond);
_mouseMoveTimer = new Timer(MouseMoveTimer_Tick, null, intervalMs, intervalMs);
MouseX = Cursor.Position.X;
MouseY = Cursor.Position.Y;
}
private void MouseMoveTimer_Tick(object? state)
{
if (Volatile.Read(ref _isDisposedFlag) != 0)
return;
int deltaX = Interlocked.Exchange(ref _mouseDeltaX, 0);
int deltaY = Interlocked.Exchange(ref _mouseDeltaY, 0);
if (deltaX == 0 && deltaY == 0) return;
int targetX = MouseX + deltaX;
int targetY = MouseY + deltaY;
targetX = Math.Max(0, Math.Min(ScreenW - 1, targetX));
targetY = Math.Max(0, Math.Min(ScreenH - 1, targetY));
MouseX = targetX;
MouseY = targetY;
// Coalesce timer ticks into at most one queued UI callback to avoid dispatcher backlog.
Interlocked.Add(ref _pendingMouseDeltaX, deltaX);
Interlocked.Add(ref _pendingMouseDeltaY, deltaY);
QueueRawMouseDispatch();
}
private void QueueRawMouseDispatch()
{
if (Interlocked.CompareExchange(ref _mouseDispatchQueued, 1, 0) != 0)
return;
var dispatcher = Application.Current?.Dispatcher;
if (dispatcher == null)
{
Interlocked.Exchange(ref _mouseDispatchQueued, 0);
return;
}
dispatcher.BeginInvoke(DispatcherPriority.Render, new Action(FlushPendingRawMouseMove));
}
private void FlushPendingRawMouseMove()
{
if (Volatile.Read(ref _isDisposedFlag) != 0)
{
Interlocked.Exchange(ref _mouseDispatchQueued, 0);
return;
}
int deltaX = Interlocked.Exchange(ref _pendingMouseDeltaX, 0);
int deltaY = Interlocked.Exchange(ref _pendingMouseDeltaY, 0);
if (deltaX != 0 || deltaY != 0)
{
var args = new RawInputMouseMoveEventArgs(deltaX, deltaY, MouseX, MouseY);
RawMouseMove?.Invoke(this, args);
}
Interlocked.Exchange(ref _mouseDispatchQueued, 0);
// Handle race: new deltas may arrive between exchange and reset.
if (Volatile.Read(ref _pendingMouseDeltaX) != 0 || Volatile.Read(ref _pendingMouseDeltaY) != 0)
{
QueueRawMouseDispatch();
}
}
public static void RegisterRawInpuKeyboard()
{
RAWINPUTDEVICE[] rid = new RAWINPUTDEVICE[1];
rid[0].usUsagePage = 0x01;
rid[0].usUsage = 0x06; // Keyboard
rid[0].dwFlags = RIDEV_INPUTSINK;
rid[0].hwndTarget = _hwnd;
bool success = RegisterRawInputDevices(rid, (uint)rid.Length, (uint)Marshal.SizeOf(typeof(RAWINPUTDEVICE)));
if (!success)
{
int error = Marshal.GetLastWin32Error();
AppMessageBox.Show($"Failed to register raw input devices. Error code: {error}");
}
}
public static void RegisterRawInputMouse()
{
RAWINPUTDEVICE[] rid = new RAWINPUTDEVICE[1];
rid[0].usUsagePage = 0x01;
rid[0].usUsage = 0x02; // Mouse
rid[0].dwFlags = RIDEV_INPUTSINK;
rid[0].hwndTarget = _hwnd;
bool success = RegisterRawInputDevices(rid, (uint)rid.Length, (uint)Marshal.SizeOf(typeof(RAWINPUTDEVICE)));
if (!success)
{
int error = Marshal.GetLastWin32Error();
AppMessageBox.Show($"Failed to register raw input mouse. Error code: {error}");
}
}
public static void UnregisterRawInputKeyboard()
{
RAWINPUTDEVICE[] rid = new RAWINPUTDEVICE[1];
rid[0].usUsagePage = 0x01;
rid[0].usUsage = 0x06; // Keyboard
rid[0].dwFlags = 0x00000001; // RIDEV_REMOVE
rid[0].hwndTarget = IntPtr.Zero; // 必须为0
bool success = RegisterRawInputDevices(rid, (uint)rid.Length, (uint)Marshal.SizeOf(typeof(RAWINPUTDEVICE)));
if (!success)
{
int error = Marshal.GetLastWin32Error();
AppMessageBox.Show($"Failed to unregister raw input keyboard. Error code: {error}");
}
}
public static void UnregisterRawInputMouse()
{
RAWINPUTDEVICE[] rid = new RAWINPUTDEVICE[1];
rid[0].usUsagePage = 0x01;
rid[0].usUsage = 0x02; // Mouse
rid[0].dwFlags = 0x00000001; // RIDEV_REMOVE
rid[0].hwndTarget = IntPtr.Zero; // 必须为0
bool success = RegisterRawInputDevices(rid, (uint)rid.Length, (uint)Marshal.SizeOf(typeof(RAWINPUTDEVICE)));
if (!success)
{
int error = Marshal.GetLastWin32Error();
AppMessageBox.Show($"Failed to unregister raw input mouse. Error code: {error}");
}
}
private readonly HashSet<ushort> _pressedKeys = new();
private void HandleKeyEvent(RAWKEYBOARD kb)
{
bool isKeyDown = kb.Message == WM_KEYDOWN || kb.Message == WM_SYSKEYDOWN;
bool isKeyUp = kb.Message == WM_KEYUP || kb.Message == WM_SYSKEYUP;
bool isE0 = (kb.Flags & E0_FLAG) != 0;
ushort key = kb.VKey;
// 检查修饰键
bool ctrl = IsKeyDown(Keys.LControlKey) || IsKeyDown(Keys.RControlKey);
bool shift = IsKeyDown(Keys.LShiftKey) || IsKeyDown(Keys.RShiftKey);
bool alt = IsKeyDown(Keys.LMenu) || IsKeyDown(Keys.RMenu);
bool win = IsKeyDown(Keys.LWin) || IsKeyDown(Keys.RWin);
var args = new RawInputKeyEventArgs(
kb.VKey, kb.MakeCode, isE0, isKeyDown,
ctrl, shift, alt, win, false);
if (isKeyDown)
{
if (_pressedKeys.Add(key)) // 只在第一次按下时触发
{
OnKeyDown(args);
}
}
else if (isKeyUp)
{
_pressedKeys.Remove(key);
OnKeyUp(args);
}
}
private void InvokeOnDispatcher(Action action)
{
var dispatcher = Application.Current?.Dispatcher;
if (dispatcher == null || dispatcher.CheckAccess())
{
action();
return;
}
dispatcher.BeginInvoke(DispatcherPriority.Input, action);
}
private void OnKeyDown(RawInputKeyEventArgs args) =>
InvokeOnDispatcher(() => KeyDown?.Invoke(this, args));
private void OnKeyUp(RawInputKeyEventArgs args) =>
InvokeOnDispatcher(() => KeyUp?.Invoke(this, args));
int _lastRawMouseX = 0;
int _lastRawMouseY = 0;
int _currentRawMouseX = 0;
int _currentRawMouseY = 0;
private IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
{
const int WM_INPUT = 0x00FF;
if (msg == WM_INPUT)
{
if (KeyDown == null && KeyUp == null && MouseDown == null && MouseUp == null && MouseWheel == null && MouseMove == null && RawMouseMove == null)
return IntPtr.Zero;
uint dwSize = (uint)_rawBufferSize;
uint result = GetRawInputData(lParam, RID_INPUT, _rawBuffer, ref dwSize, (uint)Marshal.SizeOf(typeof(RAWINPUTHEADER))); ;
if (result == uint.MaxValue)
return IntPtr.Zero;
// 如果缓冲区不够大,扩容并重试(极少发生)
if (result > dwSize)
{
Marshal.FreeHGlobal(_rawBuffer);
_rawBuffer = Marshal.AllocHGlobal((int)result);
_rawBufferSize = (int)result;
dwSize = result;
if (GetRawInputData(lParam, RID_INPUT, _rawBuffer, ref dwSize, (uint)Marshal.SizeOf(typeof(RAWINPUTHEADER))) != dwSize)
return IntPtr.Zero;
}
RAWINPUTHEADER header = Marshal.PtrToStructure<RAWINPUTHEADER>(_rawBuffer);
IntPtr dataPtr = _rawBuffer + Marshal.SizeOf<RAWINPUTHEADER>();
if (header.dwType == RIM_TYPEMOUSE)
{
RAWMOUSE mouse = Marshal.PtrToStructure<RAWMOUSE>(dataPtr);
// 绝对坐标模式(如某些游戏或远程桌面)需要特殊处理
if ((mouse.usFlags & RI_MOUSE_MOVE_ABSOLUTE) == RI_MOUSE_MOVE_ABSOLUTE)
{
// 归一化到 0~65535 区间
int absX = Math.Clamp(mouse.lLastX, 0, 65535);
int absY = Math.Clamp(mouse.lLastY, 0, 65535);
// 转换为像素坐标
int pixelX = (int)((absX / 65535.0) * (ScreenW - 1));
int pixelY = (int)((absY / 65535.0) * (ScreenH - 1));
_currentRawMouseX = pixelX;
_currentRawMouseY = pixelY;
Interlocked.Add(ref _mouseDeltaX, _currentRawMouseX - _lastRawMouseX);
Interlocked.Add(ref _mouseDeltaY, _currentRawMouseY - _lastRawMouseY);
_lastRawMouseX = _currentRawMouseX;
_lastRawMouseY = _currentRawMouseY;
return IntPtr.Zero;
}
Interlocked.Add(ref _mouseDeltaX, mouse.lLastX);
Interlocked.Add(ref _mouseDeltaY, mouse.lLastY);
return IntPtr.Zero;
}
if (header.dwType == RIM_TYPEKEYBOARD)
{
if (KeyDown == null && KeyUp == null)
return IntPtr.Zero;
RAWKEYBOARD kb = Marshal.PtrToStructure<RAWKEYBOARD>(dataPtr);
HandleKeyEvent(kb);
return IntPtr.Zero;
}
}
return IntPtr.Zero;
}
public static FipKeys MapRawInputToCustomKeys(ushort vkey, ushort scancode, bool isE0)
{
// Pause/Break (E1)
if (vkey == 0x13) // VK_PAUSE
return FipKeys.Pause;
// PrintScreen (有些驱动不设置 isE0)
if (vkey == 0x2C) // VK_SNAPSHOT
return FipKeys.PrintScreen;
// Enter
if (vkey == 0x0D && scancode == 0x1C)
return isE0 ? FipKeys.NumpadEnter : FipKeys.Enter;
// Ctrl
if (vkey == 0x11 && scancode == 0x1D)
return isE0 ? FipKeys.RControlKey : FipKeys.LControlKey;
// Alt
if (vkey == 0x12 && scancode == 0x38)
return isE0 ? FipKeys.RMenu : FipKeys.LMenu;
// Shift
if (vkey == 0x10)
{
if (scancode == 0x2A) return FipKeys.LShiftKey;
if (scancode == 0x36) return FipKeys.RShiftKey;
}
// Win / Apps (避免 VK_UNKNOWN)
if (vkey == 0x5B) return FipKeys.LWin;
if (vkey == 0x5C) return FipKeys.RWin;
if (vkey == 0x5D) return FipKeys.Apps;
// 其它键直接映射 (VK)
if (Enum.IsDefined(typeof(FipKeys), vkey))
return (FipKeys)vkey;
// 兜底:使用 E0 扫描码识别方向/编辑区/扩展键
if (isE0)
{
switch (scancode)
{
case 0x48: return FipKeys.Up;
case 0x4B: return FipKeys.Left;
case 0x4D: return FipKeys.Right;
case 0x50: return FipKeys.Down;
case 0x47: return FipKeys.Home;
case 0x4F: return FipKeys.End;
case 0x49: return FipKeys.PageUp;
case 0x51: return FipKeys.PageDown;
case 0x52: return FipKeys.Insert;
case 0x53: return FipKeys.Delete;
case 0x35: return FipKeys.Divide;
case 0x5B: return FipKeys.LWin;
case 0x5C: return FipKeys.RWin;
case 0x5D: return FipKeys.Apps;
}
}
return FipKeys.None;
}
public static FipRawKeys MapRawInputToCustomRawKeys(ushort vkey, ushort scancode, bool isE0)
{
uint code = isE0 ? (0xE000u | scancode) : scancode;
// Pause/Break (E1)
if (vkey == 0x13) // VK_PAUSE
return FipRawKeys.Pause;
// PrintScreen (只看 vkey 更稳)
if (vkey == 0x2C) // VK_SNAPSHOT
return FipRawKeys.PrintScreen;
// 右Ctrl / 左Ctrl
if (vkey == 0x11 && scancode == 0x1D)
return isE0 ? FipRawKeys.RControlKey : FipRawKeys.LControlKey;
// 右Alt / 左Alt
if (vkey == 0x12 && scancode == 0x38)
return isE0 ? FipRawKeys.RMenu : FipRawKeys.LMenu;
// 小键盘Enter / 主键盘Enter
if (vkey == 0x0D && scancode == 0x1C)
return isE0 ? FipRawKeys.NumpadEnter : FipRawKeys.Enter;
// Win键 (若 vkey 可用则直接返回)
if (vkey == 0x5B) return FipRawKeys.LWin;
if (vkey == 0x5C) return FipRawKeys.RWin;
if (vkey == 0x5D) return FipRawKeys.Apps;
// E0 前缀 / 普通扫描码兜底
if (Enum.IsDefined(typeof(FipRawKeys), code))
return (FipRawKeys)code;
return FipRawKeys.None;
}
public static Vector2 GetCursorPosition()
{
GetCursorPos(out POINT point);
return new Vector2(point.X, point.Y);
}
private const uint RIDEV_INPUTSINK = 0x00000100;
private const int RAWINPUT_INITIAL_BUFFER_SIZE = 48;
private const int RID_INPUT = 0x10000003;
private const int RIM_TYPEKEYBOARD = 1;
private const int RIM_TYPEMOUSE = 0;
private const int WM_KEYDOWN = 0x0100;
private const int WM_SYSKEYDOWN = 0x0104;
private const int WM_KEYUP = 0x0101;
private const int WM_SYSKEYUP = 0x0105;
private const int E0_FLAG = 0x02;
private const ushort RI_MOUSE_LEFT_BUTTON_DOWN = 0x0001;
private const ushort RI_MOUSE_LEFT_BUTTON_UP = 0x0002;
private const ushort RI_MOUSE_RIGHT_BUTTON_DOWN = 0x0004;
private const ushort RI_MOUSE_RIGHT_BUTTON_UP = 0x0008;
private const ushort RI_MOUSE_MIDDLE_BUTTON_DOWN = 0x0010;
private const ushort RI_MOUSE_MIDDLE_BUTTON_UP = 0x0020;
private const ushort RI_MOUSE_XBUTTON1_DOWN = 0x0040;
private const ushort RI_MOUSE_XBUTTON1_UP = 0x0080;
private const ushort RI_MOUSE_XBUTTON2_DOWN = 0x0100;
private const ushort RI_MOUSE_XBUTTON2_UP = 0x0200;
private const ushort RI_MOUSE_WHEEL = 0x0400;
private const ushort RI_MOUSE_MOVE_ABSOLUTE = 0x0001;
[StructLayout(LayoutKind.Sequential)]
struct RAWINPUTDEVICE
{
public ushort usUsagePage;
public ushort usUsage;
public uint dwFlags;
public IntPtr hwndTarget;
}
[StructLayout(LayoutKind.Sequential)]
struct RAWINPUTHEADER
{
public uint dwType;
public uint dwSize;
public IntPtr hDevice;
public IntPtr wParam;
}
[StructLayout(LayoutKind.Sequential)]
struct RAWKEYBOARD
{
public ushort MakeCode;
public ushort Flags;
public ushort Reserved;
public ushort VKey;
public uint Message;
public uint ExtraInformation;
}
[StructLayout(LayoutKind.Explicit)]
struct RAWMOUSE
{
[FieldOffset(0)]
public ushort usFlags;
[FieldOffset(2)]
public ushort reserved; // 填充对齐
[FieldOffset(4)]
public ushort usButtonFlags;
[FieldOffset(6)]
public ushort usButtonData;
[FieldOffset(8)]
public uint ulRawButtons;
[FieldOffset(12)]
public int lLastX;
[FieldOffset(16)]
public int lLastY;
[FieldOffset(20)]
public uint ulExtraInformation;
}
[StructLayout(LayoutKind.Sequential)]
struct POINT
{
public int X;
public int Y;
}
[DllImport("user32.dll")]
static extern bool GetCursorPos(out POINT lpPoint);
[DllImport("user32.dll")]
static extern short GetKeyState(int nVirtKey);
[DllImport("User32.dll", SetLastError = true)]
static extern bool RegisterRawInputDevices(
[In] RAWINPUTDEVICE[] pRawInputDevices,
uint uiNumDevices,
uint cbSize);
[DllImport("User32.dll", SetLastError = true)]
static extern uint GetRawInputData(
IntPtr hRawInput,
uint uiCommand,
IntPtr pData,
ref uint pcbSize,
uint cbSizeHeader);
private static bool IsKeyDown(Keys key)
{
return (GetKeyState((int)key) & 0x8000) != 0;
}
public void Dispose()
{
if (Interlocked.Exchange(ref _isDisposedFlag, 1) != 0)
return;
_mouseMoveTimer?.Change(Timeout.Infinite, Timeout.Infinite);
_mouseMoveTimer?.Dispose();
_mouseMoveTimer = null;
if (_source != null)
{
_source.RemoveHook(WndProc);
_source = null;
}
if (_rawBuffer != IntPtr.Zero)
{
Marshal.FreeHGlobal(_rawBuffer);
_rawBuffer = IntPtr.Zero;
_rawBufferSize = 0;
}
//Device.MouseInput -= OnMouseInput;
_globalHook?.Dispose();
_globalHook = null;
GC.SuppressFinalize(this);
}
private const int WM_ENTERSIZEMOVE = 0x0231;
private const int WM_EXITSIZEMOVE = 0x0232;
public static IntPtr WndDragProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
{
switch (msg)
{
case WM_ENTERSIZEMOVE:
// 开始移动或缩放
RawInputParser.UnregisterRawInputMouse();
break;
case WM_EXITSIZEMOVE:
// 结束移动或缩放
RawInputParser.RegisterRawInputMouse();
break;
}
return IntPtr.Zero;
}
}
}
@@ -0,0 +1,56 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Data;
namespace FancyInput.Models
{
/// <summary>
/// 通用的Slider进度转换器
/// 根据Slider的Minimum、Maximum和Value计算进度宽度
/// </summary>
public class SliderProgressConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
try
{
// values[0] = Value
// values[1] = Minimum
// values[2] = Maximum
// values[3] = TrackBackground.ActualWidth
if (values.Length >= 4 &&
values[0] is double value &&
values[1] is double minimum &&
values[2] is double maximum &&
values[3] is double trackWidth)
{
// 防止除以0
if (Math.Abs(maximum - minimum) < 0.0001)
return 0.0;
// 计算百分比
double percentage = (value - minimum) / (maximum - minimum);
// 返回进度条宽度
return Math.Max(0, Math.Min(trackWidth, percentage * trackWidth));
}
return 0.0;
}
catch
{
return 0.0;
}
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}
@@ -0,0 +1,311 @@
using System;
using System.Linq;
using System.Reflection;
using System.Diagnostics;
using System.IO;
namespace FancyInput.Models
{
/// <summary>
/// Lightweight runtime wrapper for Nefarius.ViGEm.Client to manage a single virtual Xbox360 controller.
/// Uses reflection so the project can compile even if the DLL is not present at build time.
/// </summary>
public sealed class VirtualControllerManager : IDisposable
{
private static readonly Lazy<VirtualControllerManager> _instance = new(() => new VirtualControllerManager());
public static VirtualControllerManager Instance => _instance.Value;
private object? _client;
private object? _controller;
private MethodInfo? _connectMethod;
private MethodInfo? _disconnectMethod;
private MethodInfo? _setButtonStateMethod;
private MethodInfo? _setAxisValueMethod;
private MethodInfo? _setSliderValueMethod;
private MethodInfo? _submitReportMethod;
private PropertyInfo? _userIndexProperty;
private Type? _xboxButtonEnumType;
private Type? _xboxAxisEnumType;
private Type? _xboxSliderEnumType;
private bool _initialized = false;
public bool IsAvailable => _initialized;
public int Index { get; private set; } = -1;
private VirtualControllerManager() { }
public bool PlugIn() => Initialize();
public void Unplug() => Disconnect();
public bool Initialize()
{
if (_initialized) return true;
try
{
// find/load the ViGEm client type
var clientType = ResolveViGEmClientType();
if (clientType == null) return false;
_client = Activator.CreateInstance(clientType);
var createMethod = FindMethod(clientType, "CreateXbox360Controller", 0);
if (createMethod == null) return false;
_controller = createMethod.Invoke(_client, null);
if (_controller == null) return false;
var controllerType = _controller.GetType();
// choose parameterless Connect/Disconnect to avoid AmbiguousMatchException
_connectMethod = controllerType.GetMethods(BindingFlags.Public | BindingFlags.Instance)
.FirstOrDefault(m => m.Name == "Connect" && m.GetParameters().Length == 0);
_disconnectMethod = controllerType.GetMethods(BindingFlags.Public | BindingFlags.Instance)
.FirstOrDefault(m => m.Name == "Disconnect" && m.GetParameters().Length == 0);
_xboxButtonEnumType = controllerType.Assembly.GetType("Nefarius.ViGEm.Client.Targets.Xbox360.Xbox360Button");
_xboxAxisEnumType = controllerType.Assembly.GetType("Nefarius.ViGEm.Client.Targets.Xbox360.Xbox360Axis");
_xboxSliderEnumType = controllerType.Assembly.GetType("Nefarius.ViGEm.Client.Targets.Xbox360.Xbox360Slider");
_setButtonStateMethod = controllerType.GetMethods(BindingFlags.Public | BindingFlags.Instance)
.FirstOrDefault(m => m.Name == "SetButtonState" && m.GetParameters().Length == 2 &&
_xboxButtonEnumType != null && m.GetParameters()[0].ParameterType == _xboxButtonEnumType &&
m.GetParameters()[1].ParameterType == typeof(bool));
_setAxisValueMethod = controllerType.GetMethods(BindingFlags.Public | BindingFlags.Instance)
.FirstOrDefault(m => m.Name == "SetAxisValue" && m.GetParameters().Length == 2 &&
_xboxAxisEnumType != null && m.GetParameters()[0].ParameterType == _xboxAxisEnumType &&
m.GetParameters()[1].ParameterType == typeof(short));
_setSliderValueMethod = controllerType.GetMethods(BindingFlags.Public | BindingFlags.Instance)
.FirstOrDefault(m => m.Name == "SetSliderValue" && m.GetParameters().Length == 2 &&
_xboxSliderEnumType != null && m.GetParameters()[0].ParameterType == _xboxSliderEnumType &&
m.GetParameters()[1].ParameterType == typeof(byte));
_submitReportMethod = FindMethod(controllerType, "SubmitReport", 0);
_userIndexProperty = controllerType.GetProperty("UserIndex", BindingFlags.Public | BindingFlags.Instance);
if (_setButtonStateMethod == null || _setAxisValueMethod == null || _setSliderValueMethod == null || _submitReportMethod == null)
{
Debug.WriteLine("VirtualControllerManager.Initialize failed: required Xbox360 methods not found on controller type.");
return false;
}
// connect
_connectMethod?.Invoke(_controller, null);
UpdateControllerIndex();
_initialized = true;
return true;
}
catch (Exception ex)
{
Debug.WriteLine("VirtualControllerManager.Initialize failed: " + ex);
_initialized = false;
return false;
}
}
public void Disconnect()
{
if (!_initialized) return;
try
{
_disconnectMethod?.Invoke(_controller, null);
}
catch { }
_initialized = false;
Index = -1;
}
public void SetButton(FIPGamepadButtonflags flag, bool pressed)
{
EnsureInitialized();
if (!_initialized) throw new NotSupportedException("Virtual controller not available.");
var mappings = new[]
{
(FIPGamepadButtonflags.Up, "Up"),
(FIPGamepadButtonflags.Down, "Down"),
(FIPGamepadButtonflags.Left, "Left"),
(FIPGamepadButtonflags.Right, "Right"),
(FIPGamepadButtonflags.Start, "Start"),
(FIPGamepadButtonflags.Select, "Back"),
(FIPGamepadButtonflags.LS, "LeftThumb"),
(FIPGamepadButtonflags.RS, "RightThumb"),
(FIPGamepadButtonflags.LB, "LeftShoulder"),
(FIPGamepadButtonflags.RB, "RightShoulder"),
(FIPGamepadButtonflags.A, "A"),
(FIPGamepadButtonflags.B, "B"),
(FIPGamepadButtonflags.X, "X"),
(FIPGamepadButtonflags.Y, "Y"),
};
foreach (var (f, name) in mappings)
{
if (!flag.HasFlag(f)) continue;
var enumVal = ParseEnum(_xboxButtonEnumType, name);
_setButtonStateMethod!.Invoke(_controller, new[] { enumVal, (object)pressed });
}
_submitReportMethod!.Invoke(_controller, null);
}
public void SetTrigger(int triggerValue, Side side)
{
EnsureInitialized();
if (!_initialized) throw new NotSupportedException("Virtual controller not available.");
byte v = (byte)Math.Clamp(triggerValue, 0, 255);
string sliderName = side == Side.Left ? "LeftTrigger" : "RightTrigger";
object slider = ParseEnum(_xboxSliderEnumType, sliderName);
_setSliderValueMethod!.Invoke(_controller, new object[] { slider, v });
_submitReportMethod!.Invoke(_controller, null);
}
public void SetStick(int x, int y, Side side)
{
EnsureInitialized();
if (!_initialized) throw new NotSupportedException("Virtual controller not available.");
short sx = (short)Math.Clamp(x, short.MinValue, short.MaxValue);
short sy = (short)Math.Clamp(y, short.MinValue, short.MaxValue);
if (side == Side.Left)
{
object axisX = ParseEnum(_xboxAxisEnumType, "LeftThumbX");
object axisY = ParseEnum(_xboxAxisEnumType, "LeftThumbY");
_setAxisValueMethod!.Invoke(_controller, new object[] { axisX, sx });
_setAxisValueMethod!.Invoke(_controller, new object[] { axisY, sy });
}
else
{
object axisX = ParseEnum(_xboxAxisEnumType, "RightThumbX");
object axisY = ParseEnum(_xboxAxisEnumType, "RightThumbY");
_setAxisValueMethod!.Invoke(_controller, new object[] { axisX, sx });
_setAxisValueMethod!.Invoke(_controller, new object[] { axisY, sy });
}
_submitReportMethod!.Invoke(_controller, null);
}
private void EnsureInitialized()
{
if (_initialized) return;
Initialize();
}
private void UpdateControllerIndex()
{
try
{
if (_controller == null || _userIndexProperty == null)
{
Index = -1;
return;
}
object? raw = _userIndexProperty.GetValue(_controller);
if (raw is int i)
{
Index = i;
return;
}
Index = -1;
}
catch
{
Index = -1;
}
}
private static Type? ResolveViGEmClientType()
{
const string clientTypeName = "Nefarius.ViGEm.Client.ViGEmClient";
// 1) already loaded assemblies
var loaded = AppDomain.CurrentDomain.GetAssemblies()
.SelectMany(a => a.GetTypesSafe())
.FirstOrDefault(t => t.FullName == clientTypeName);
if (loaded != null) return loaded;
// 2) try load by assembly name (works when DLL is in probing path)
try
{
var asm = Assembly.Load("Nefarius.ViGEm.Client");
var t = asm.GetType(clientTypeName);
if (t != null) return t;
}
catch (Exception ex)
{
Debug.WriteLine("ResolveViGEmClientType Assembly.Load failed: " + ex.Message);
}
// 3) try load from app base directory
try
{
string dllPath = Path.Combine(AppContext.BaseDirectory, "Nefarius.ViGEm.Client.dll");
if (File.Exists(dllPath))
{
var asm = Assembly.LoadFrom(dllPath);
var t = asm.GetType(clientTypeName);
if (t != null) return t;
}
}
catch (Exception ex)
{
Debug.WriteLine("ResolveViGEmClientType Assembly.LoadFrom failed: " + ex.Message);
}
return null;
}
private static object ParseEnum(Type? enumType, string name)
{
if (enumType == null)
throw new NotSupportedException("Virtual controller enum type is unavailable.");
// ViGEm's Xbox360Button/Axis/Slider in this version are class types with static fields,
// not CLR enums. Resolve by static field first.
var field = enumType
.GetFields(BindingFlags.Public | BindingFlags.Static)
.FirstOrDefault(f => string.Equals(f.Name, name, StringComparison.OrdinalIgnoreCase));
if (field != null)
{
var value = field.GetValue(null);
if (value != null) return value;
}
if (enumType.IsEnum)
{
return Enum.Parse(enumType, name, ignoreCase: true);
}
throw new NotSupportedException($"Type '{enumType.FullName}' does not expose a usable member named '{name}'.");
}
public void Dispose()
{
Disconnect();
_client = null;
_controller = null;
}
private static MethodInfo? FindMethod(Type? type, string name, int parameterCount)
{
if (type == null) return null;
return type.GetMethods(BindingFlags.Public | BindingFlags.Instance)
.FirstOrDefault(m => m.Name == name && m.GetParameters().Length == parameterCount);
}
}
internal static class ReflectionHelpers
{
public static System.Collections.Generic.IEnumerable<Type> GetTypesSafe(this Assembly a)
{
try { return a.GetTypes(); } catch { return System.Array.Empty<Type>(); }
}
}
}
+662
View File
@@ -0,0 +1,662 @@
using System.Drawing;
using System.IO;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using Color = System.Windows.Media.Color;
namespace FancyInput.Models
{
public enum XImageType
{
PNG,
GIF,
Unknown
}
public class XImage
{
public XImageType ImageType { get; set; }
private BitmapSource _bitmapSource;
public BitmapSource BitmapSource => _bitmapSource;
public int Width => _bitmapSource.PixelWidth;
public int Height => _bitmapSource.PixelHeight;
public byte[]? GifRawBytes { get; private set; }
public XImage(string path)
{
ImageType = XImage.GetImageType(path);
switch (ImageType)
{
case XImageType.GIF:
GifRawBytes = File.ReadAllBytes(path);
var bitmap = new BitmapImage();
bitmap.BeginInit();
bitmap.CacheOption = BitmapCacheOption.OnLoad;
bitmap.StreamSource = new MemoryStream(GifRawBytes);
bitmap.EndInit();
_bitmapSource = bitmap;
break;
case XImageType.PNG:
{
using var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read);
var decoder = BitmapDecoder.Create(stream, BitmapCreateOptions.PreservePixelFormat, BitmapCacheOption.OnLoad);
_bitmapSource = decoder.Frames[0];
_bitmapSource = _bitmapSource.Format == PixelFormats.Bgra32 ?
_bitmapSource : new FormatConvertedBitmap(_bitmapSource, PixelFormats.Bgra32, null, 0);
}
break;
default:
throw new NotSupportedException($"Unsupported image format: {Path.GetExtension(path)}");
}
}
public XImage(BitmapSource bitmapSource, XImageType imageType=XImageType.PNG, byte[]? gifRawBytes = null)
{
_bitmapSource = bitmapSource;
ImageType = imageType;
if (imageType == XImageType.GIF && gifRawBytes != null)
GifRawBytes = gifRawBytes.ToArray();
}
public static XImageType GetImageType(string path)
{
var ext = Path.GetExtension(path).ToLower();
return ext switch
{
".gif" => XImageType.GIF,
".png" or ".jpg" or ".jpeg" or ".bmp" => XImageType.PNG,
_ => XImageType.Unknown
};
}
public static List<(BitmapSource Frame, int DelayMs)> LoadGifFramesWithDelay(string path)
{
using var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read);
var decoder = BitmapDecoder.Create(stream, BitmapCreateOptions.PreservePixelFormat, BitmapCacheOption.OnLoad);
var result = new List<(BitmapSource, int)>();
foreach (var frame in decoder.Frames)
{
var metadata = frame.Metadata as BitmapMetadata;
int delay = 100; // 默认100ms
if (metadata != null && metadata.ContainsQuery("/grctlext/Delay"))
{
delay = (ushort)metadata.GetQuery("/grctlext/Delay") * 10;
}
result.Add((frame, delay));
}
return result;
}
private static BitmapSource EnsureBgra32(BitmapSource source)
{
return source.Format == PixelFormats.Bgra32
? source
: new FormatConvertedBitmap(source, PixelFormats.Bgra32, null, 0);
}
private BitmapSource GetBitmapForPixelOperations()
{
if (ImageType == XImageType.GIF && GifRawBytes != null && GifRawBytes.Length > 0)
{
using var stream = new MemoryStream(GifRawBytes, false);
var decoder = BitmapDecoder.Create(stream, BitmapCreateOptions.PreservePixelFormat, BitmapCacheOption.OnLoad);
if (decoder.Frames.Count == 0)
throw new InvalidOperationException("GIF has no decodable frames.");
return EnsureBgra32(decoder.Frames[0]);
}
return EnsureBgra32(_bitmapSource);
}
public XImage Cut(int left, int right, int top, int bottom)
{
if (left < 0 || right < 0 || top < 0 || bottom < 0)
throw new ArgumentException("Cut values must be non-negative");
if (left + right >= Width || top + bottom >= Height)
throw new ArgumentException("Cut values are too large");
int newW = Width - left - right;
int newH = Height - top - bottom;
return Crop(left, top, newW, newH);
}
public XImage Crop(int x, int y, int w, int h)
{
if (w <= 0 || h <= 0)
throw new ArgumentException("Crop width and height must be positive");
var source = GetBitmapForPixelOperations();
var format = PixelFormats.Bgra32;
const int bytesPerPixel = 4;
int srcStride = source.PixelWidth * bytesPerPixel;
byte[] srcPixels = new byte[source.PixelHeight * srcStride];
source.CopyPixels(srcPixels, srcStride, 0);
// Fixed-size output canvas initialized to transparent pixels.
int dstStride = w * bytesPerPixel;
byte[] dstPixels = new byte[h * dstStride];
int srcStartX = Math.Max(0, x);
int srcStartY = Math.Max(0, y);
int srcEndX = Math.Min(source.PixelWidth, x + w);
int srcEndY = Math.Min(source.PixelHeight, y + h);
int copyW = srcEndX - srcStartX;
int copyH = srcEndY - srcStartY;
if (copyW > 0 && copyH > 0)
{
int dstStartX = srcStartX - x;
int dstStartY = srcStartY - y;
for (int row = 0; row < copyH; row++)
{
int srcOffset = (srcStartY + row) * srcStride + srcStartX * bytesPerPixel;
int dstOffset = (dstStartY + row) * dstStride + dstStartX * bytesPerPixel;
Buffer.BlockCopy(srcPixels, srcOffset, dstPixels, dstOffset, copyW * bytesPerPixel);
}
}
var bmp = BitmapSource.Create(w, h, source.DpiX, source.DpiY, format, null, dstPixels, dstStride);
return new XImage(bmp, XImageType.PNG);
}
public static XImage Empty(int H,int W)
{
var format = System.Windows.Media.PixelFormats.Bgra32;
var stride = W * (format.BitsPerPixel / 8);
byte[] pixels = new byte[H * stride];
var bmp = BitmapSource.Create(W, H, 96, 96, format, null, pixels, stride);
return new XImage(bmp,XImageType.PNG);
}
public static XImage FromMask(bool[] mask,int H,int W,Color color)
{
if (mask.Length != H * W)
throw new ArgumentException("Mask length must be equal to H * W");
var format = System.Windows.Media.PixelFormats.Bgra32;
var stride = W * (format.BitsPerPixel / 8);
byte[] pixels = new byte[H * stride];
for (int i = 0; i < mask.Length; i++)
{
if (!mask[i])
{
pixels[i * 4] = 0;
pixels[i * 4 + 1] = 0;
pixels[i * 4 + 2] = 0;
pixels[i * 4 + 3] = 0;
}
else
{
pixels[i * 4] = color.B;
pixels[i * 4 + 1] = color.G;
pixels[i * 4 + 2] = color.R;
pixels[i * 4 + 3] = color.A;
}
}
var bmp = BitmapSource.Create(W, H, 96, 96, format, null, pixels, stride);
return new XImage(bmp, XImageType.PNG);
}
public XImage Copy() => new XImage(_bitmapSource.Clone(),ImageType,GifRawBytes);
public (byte[] R, byte[] G, byte[] B, byte[] A) ExtractChannels()
{
var source = GetBitmapForPixelOperations();
int w = source.PixelWidth;
int h = source.PixelHeight;
var format = source.Format;
var stride = w * (format.BitsPerPixel / 8);
byte[] pixels = new byte[h * stride];
source.CopyPixels(new System.Windows.Int32Rect(0, 0, w, h), pixels, stride, 0);
byte[] r = new byte[w * h];
byte[] g = new byte[w * h];
byte[] b = new byte[w * h];
byte[] a = new byte[w * h];
for (int i = 0, j = 0; i < pixels.Length; i += 4, j++)
{
b[j] = pixels[i];
g[j] = pixels[i + 1];
r[j] = pixels[i + 2];
a[j] = pixels[i + 3];
}
return (r, g, b, a);
}
public static XImage MergeChannels(byte[] r, byte[] g, byte[] b, byte[] a,int h,int w)
{
var format = System.Windows.Media.PixelFormats.Bgra32;
var stride = w * (format.BitsPerPixel / 8);
byte[] pixels = new byte[h * stride];
for (int i = 0, j = 0; j < r.Length; i += 4, j++)
{
pixels[i] = b[j];
pixels[i + 1] = g[j];
pixels[i + 2] = r[j];
pixels[i + 3] = a[j];
}
var bmp = BitmapSource.Create(w, h, 96, 96, format, null, pixels, stride);
return new XImage(bmp, XImageType.PNG);
}
public XImage ReSize(int H, int W)
{
if (H == Height && W == Width)
return this;
if (H <= 0 || W <= 0)
throw new ArgumentException("Width and Height must be positive integers");
var source = GetBitmapForPixelOperations();
//var scaleX = (double)W / source.PixelWidth;
//var scaleY = (double)H / source.PixelHeight;
//var transform = new System.Windows.Media.ScaleTransform(scaleX, scaleY);
//var dv = new System.Windows.Media.DrawingVisual();
//using (var dc = dv.RenderOpen())
//{
// dc.PushTransform(transform);
// dc.DrawImage(source, new System.Windows.Rect(0, 0, source.PixelWidth, source.PixelHeight));
// dc.Pop();
//}
//var rtb = new System.Windows.Media.Imaging.RenderTargetBitmap(
// W, H, source.DpiX, source.DpiY, System.Windows.Media.PixelFormats.Default);
//rtb.Render(dv);
//return new XImage(rtb, XImageType.PNG);
// 直接把原图拉伸到目标像素区域
var dv = new System.Windows.Media.DrawingVisual();
using (var dc = dv.RenderOpen())
{
dc.DrawImage(source, new System.Windows.Rect(0, 0, W, H));
}
// RenderTargetBitmap 只能用 Pbgra32
var rtb = new System.Windows.Media.Imaging.RenderTargetBitmap(
W, H, 96, 96, System.Windows.Media.PixelFormats.Pbgra32);
rtb.Render(dv);
// 转换为 Bgra32
var bgra32 = new FormatConvertedBitmap(rtb, PixelFormats.Bgra32, null, 0);
return new XImage(bgra32, XImageType.PNG);
}
public XImage Scale(double scale)
{
if (scale <= 0)
throw new ArgumentException("Scale must be a positive number");
int newW = (int)(Width * scale);
int newH = (int)(Height * scale);
var resized = ReSize(newH, newW);
return resized;
}
public XImage EraseCircle(double centerX, double centerY, double radius)
{
if (double.IsNaN(centerX) || double.IsInfinity(centerX) ||
double.IsNaN(centerY) || double.IsInfinity(centerY) ||
double.IsNaN(radius) || double.IsInfinity(radius))
{
throw new ArgumentException("Center and radius must be finite numbers.");
}
if (radius <= 0)
{
return Copy();
}
var source = GetBitmapForPixelOperations();
const int bytesPerPixel = 4;
int width = source.PixelWidth;
int height = source.PixelHeight;
int stride = width * bytesPerPixel;
byte[] pixels = new byte[height * stride];
source.CopyPixels(pixels, stride, 0);
int minX = Math.Max(0, (int)Math.Floor(centerX - radius));
int maxX = Math.Min(width - 1, (int)Math.Ceiling(centerX + radius));
int minY = Math.Max(0, (int)Math.Floor(centerY - radius));
int maxY = Math.Min(height - 1, (int)Math.Ceiling(centerY + radius));
double radiusSquared = radius * radius;
for (int y = minY; y <= maxY; y++)
{
for (int x = minX; x <= maxX; x++)
{
double dx = x - centerX;
double dy = y - centerY;
if (dx * dx + dy * dy <= radiusSquared)
{
int offset = y * stride + x * bytesPerPixel;
pixels[offset] = 0;
pixels[offset + 1] = 0;
pixels[offset + 2] = 0;
pixels[offset + 3] = 0;
}
}
}
var bitmap = BitmapSource.Create(
width,
height,
source.DpiX,
source.DpiY,
PixelFormats.Bgra32,
null,
pixels,
stride);
return new XImage(bitmap, XImageType.PNG);
}
public static XImage Merge(XImage image1, XImage image2, Direction direction, double percentage)
{
var source1 = image1.GetBitmapForPixelOperations();
var source2 = image2.GetBitmapForPixelOperations();
if (source1.PixelWidth != source2.PixelWidth || source1.PixelHeight != source2.PixelHeight)
throw new ArgumentException("Images must have same dimensions");
int w = source1.PixelWidth;
int h = source1.PixelHeight;
var format = source1.Format;
var dpiX = source1.DpiX;
var dpiY = source1.DpiY;
int bytesPerPixel = format.BitsPerPixel / 8;
int stride = w * bytesPerPixel;
// 创建结果像素数组
byte[] resultPixels = new byte[h * stride];
// 根据方向进行处理
switch (direction)
{
case Direction.Up: // 上下合并:image1在上,image2在下
{
int splitPos = (int)(h * (1 - percentage));
splitPos = Math.Max(0, Math.Min(splitPos, h));
if (splitPos > 0)
{
source1.CopyPixels(
new System.Windows.Int32Rect(0, 0, w, splitPos),
resultPixels, stride, 0);
}
if (splitPos < h)
{
int startY = Math.Min(splitPos, h - 1);
int height = h - startY;
int offset = startY * stride;
source2.CopyPixels(
new System.Windows.Int32Rect(0, startY, w, height),
resultPixels, stride, offset);
}
break;
}
case Direction.Down: // 下上合并:image2在上,image1在下
{
int splitPos = (int)(h * percentage);
splitPos = Math.Max(0, Math.Min(splitPos, h));
if (splitPos > 0)
{
source2.CopyPixels(
new System.Windows.Int32Rect(0, 0, w, splitPos),
resultPixels, stride, 0);
}
if (splitPos < h)
{
// 复制image1的下半部分
int startY = Math.Min(splitPos, h - 1);
int height = h - startY;
int offset = startY * stride;
source1.CopyPixels(
new System.Windows.Int32Rect(0, startY, w, height),
resultPixels, stride, offset);
}
break;
}
case Direction.Left: // 左右合并:image1在左,image2在右
{
int splitPos = (int)(w * (1 - percentage));
splitPos = Math.Max(0, Math.Min(splitPos, w));
byte[] rowBuffer = new byte[w * bytesPerPixel];
for (int y = 0; y < h; y++)
{
int destOffset = y * stride;
if (splitPos > 0)
{
source1.CopyPixels(
new System.Windows.Int32Rect(0, y, splitPos, 1),
rowBuffer, splitPos * bytesPerPixel, 0);
Buffer.BlockCopy(rowBuffer, 0, resultPixels, destOffset, splitPos * bytesPerPixel);
}
if (splitPos < w)
{
int rightWidth = w - splitPos;
source2.CopyPixels(
new System.Windows.Int32Rect(splitPos, y, rightWidth, 1),
rowBuffer, rightWidth * bytesPerPixel, 0);
Buffer.BlockCopy(rowBuffer, 0, resultPixels, destOffset + splitPos * bytesPerPixel,
rightWidth * bytesPerPixel);
}
}
break;
}
case Direction.Right: // 右左合并:image2在左,image1在右
{
int splitPos = (int)(w * percentage);
splitPos = Math.Max(0, Math.Min(splitPos, w));
byte[] rowBuffer = new byte[w * bytesPerPixel];
for (int y = 0; y < h; y++)
{
int destOffset = y * stride;
if (splitPos > 0)
{
source2.CopyPixels(
new System.Windows.Int32Rect(0, y, splitPos, 1),
rowBuffer, splitPos * bytesPerPixel, 0);
Buffer.BlockCopy(rowBuffer, 0, resultPixels, destOffset, splitPos * bytesPerPixel);
}
if (splitPos < w)
{
int rightWidth = w - splitPos;
source1.CopyPixels(
new System.Windows.Int32Rect(splitPos, y, rightWidth, 1),
rowBuffer, rightWidth * bytesPerPixel, 0);
Buffer.BlockCopy(rowBuffer, 0, resultPixels, destOffset + splitPos * bytesPerPixel,
rightWidth * bytesPerPixel);
}
}
break;
}
default:
throw new ArgumentException("Direction must be 1-4");
}
// 创建结果位图
var resultBitmap = BitmapSource.Create(
w, h, dpiX, dpiY, format, null, resultPixels, stride);
return new XImage(resultBitmap, XImageType.PNG);
}
public void Save(string path)
{
var ext = Path.GetExtension(path).ToLower();
BitmapEncoder encoder;
switch (ext)
{
case ".png":
encoder = new PngBitmapEncoder();
break;
case ".jpg":
case ".jpeg":
encoder = new JpegBitmapEncoder();
break;
case ".bmp":
encoder = new BmpBitmapEncoder();
break;
case ".gif":
if (ImageType == XImageType.GIF)
{
// 只保存当前帧,非动画
encoder = new GifBitmapEncoder();
}
else
{
encoder = new GifBitmapEncoder();
}
break;
default:
throw new NotSupportedException($"Unsupported image format: {ext}");
}
encoder.Frames.Add(BitmapFrame.Create(_bitmapSource));
using var stream = new FileStream(path, FileMode.Create, FileAccess.Write);
encoder.Save(stream);
if (ext == ".gif" && ImageType == XImageType.GIF)
{
// 提示:只保存当前帧,非动画
System.Diagnostics.Debug.WriteLine("Warning: Saving GIF only saves the current frame, not the full animation.");
}
}
public record PlacedImage(int Index, XImage Image, int X, int Y);
public static (XImage atlas, List<PlacedImage> placements) PackImages(List<XImage> images)
{
if (images == null || images.Count == 0)
throw new ArgumentException("images 不能为空");
// 记录原始索引
var indexed = images.Select((img, idx) => (img, idx, source: img.GetBitmapForPixelOperations())).ToList();
// 按高度降序排列,但保留原始索引
var sorted = indexed.OrderByDescending(x => x.source.PixelHeight).ToList();
int maxRowWidth = sorted.Sum(x => x.source.PixelWidth);
int atlasWidth = 0, atlasHeight = 0;
int curX = 0, curY = 0, rowHeight = 0;
List<PlacedImage> placements = new();
foreach (var (img, idx, source) in sorted)
{
if (curX + source.PixelWidth > maxRowWidth && curX > 0)
{
atlasWidth = Math.Max(atlasWidth, curX);
curY += rowHeight;
curX = 0;
rowHeight = 0;
}
placements.Add(new PlacedImage(idx, img, curX, curY));
curX += source.PixelWidth;
rowHeight = Math.Max(rowHeight, source.PixelHeight);
}
atlasWidth = Math.Max(atlasWidth, curX);
atlasHeight = curY + rowHeight;
var wb = new System.Windows.Media.Imaging.WriteableBitmap(
atlasWidth, atlasHeight, 96, 96, System.Windows.Media.PixelFormats.Bgra32, null);
foreach (var p in placements)
{
var src = p.Image.GetBitmapForPixelOperations();
var rect = new System.Windows.Int32Rect(0, 0, src.PixelWidth, src.PixelHeight);
int stride = src.PixelWidth * (src.Format.BitsPerPixel / 8);
byte[] pixels = new byte[src.PixelHeight * stride];
src.CopyPixels(rect, pixels, stride, 0);
wb.Lock();
try
{
IntPtr destPtr = wb.BackBuffer + p.Y * wb.BackBufferStride + p.X * 4;
for (int row = 0; row < src.PixelHeight; row++)
{
System.Runtime.InteropServices.Marshal.Copy(
pixels, row * stride,
destPtr + row * wb.BackBufferStride,
stride);
}
wb.AddDirtyRect(new System.Windows.Int32Rect(p.X, p.Y, src.PixelWidth, src.PixelHeight));
}
finally
{
wb.Unlock();
}
}
// placements 按原始索引排序,方便查找
var placementsByIndex = placements.OrderBy(p => p.Index).ToList();
return (new XImage(wb, XImageType.PNG), placementsByIndex);
}
public static XImage ConcatImages(List<XImage> images, int spacing, int direction)
{
if (images == null || images.Count == 0)
throw new ArgumentException("images 不能为空");
var sources = images.Select(img => img.GetBitmapForPixelOperations()).ToList();
int w = sources[0].PixelWidth;
int h = sources[0].PixelHeight;
foreach (var src in sources)
if (src.PixelWidth != w || src.PixelHeight != h)
throw new ArgumentException("所有图片必须尺寸一致");
int count = images.Count;
int outW, outH;
if (direction == 0) // 向下
{
outW = w;
outH = h * count + spacing * (count - 1);
}
else if (direction == 1) // 向右
{
outW = w * count + spacing * (count - 1);
outH = h;
}
else
{
throw new ArgumentException("direction 只能为0(向下)或1(向右)");
}
var wb = new System.Windows.Media.Imaging.WriteableBitmap(
outW, outH, 96, 96, System.Windows.Media.PixelFormats.Bgra32, null);
for (int i = 0; i < count; i++)
{
int x = direction == 1 ? i * (w + spacing) : 0;
int y = direction == 0 ? i * (h + spacing) : 0;
var src = sources[i];
var rect = new System.Windows.Int32Rect(0, 0, w, h);
int stride = w * (src.Format.BitsPerPixel / 8);
byte[] pixels = new byte[h * stride];
src.CopyPixels(rect, pixels, stride, 0);
wb.Lock();
try
{
IntPtr destPtr = wb.BackBuffer + y * wb.BackBufferStride + x * 4;
for (int row = 0; row < h; row++)
{
System.Runtime.InteropServices.Marshal.Copy(
pixels, row * stride,
destPtr + row * wb.BackBufferStride,
stride);
}
wb.AddDirtyRect(new System.Windows.Int32Rect(x, y, w, h));
}
finally
{
wb.Unlock();
}
}
return new XImage(wb, XImageType.PNG);
}
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

@@ -0,0 +1,25 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Style x:Key="InfoTipBorderStyle" TargetType="Border">
<Setter Property="Height" Value="14"/>
<Setter Property="Width" Value="14"/>
<Setter Property="BorderBrush" Value="Purple"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="CornerRadius" Value="8"/>
<Setter Property="Margin" Value="0,0,10,0"/>
<Setter Property="Cursor" Value="Hand"/>
<Setter Property="Background" Value="#05808080"/>
<Setter Property="Child">
<Setter.Value>
<Label Content="?" FontSize="8" Padding="0"
HorizontalAlignment="Center" VerticalAlignment="Center"
FontWeight="Bold" Foreground="Purple"/>
</Setter.Value>
</Setter>
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="#0B5B06DE"/>
</Trigger>
</Style.Triggers>
</Style>
</ResourceDictionary>
@@ -0,0 +1,254 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:md="clr-namespace:FancyInput.Models">
<Style TargetType="Button" x:Key="FancyButton">
<Setter Property="Cursor" Value="Hand"/>
<Setter Property="FontFamily" Value="Cascadia Mono"/>
<Setter Property="FontSize" Value="20"/>
<Setter Property="Foreground" Value="White"/>
<Setter Property="Background" Value="#FF651F65"/>
<Setter Property="BorderBrush" Value="#FF651F65"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="Padding" Value="8,4"/>
<Setter Property="SnapsToDevicePixels" Value="True"/>
<Setter Property="Effect">
<Setter.Value>
<DropShadowEffect Color="#88651F65" BlurRadius="1" ShadowDepth="1"/>
</Setter.Value>
</Setter>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="8"
SnapsToDevicePixels="True"
Effect="{TemplateBinding Effect}">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="#FF953895"/>
</Trigger>
</Style.Triggers>
</Style>
<Style TargetType="Button" x:Key="RoundCornerButton">
<Setter Property="Cursor" Value="Hand"/>
<Setter Property="FontFamily" Value="Cascadia Mono"/>
<Setter Property="FontSize" Value="20"/>
<Setter Property="Foreground" Value="White"/>
<Setter Property="Background" Value="#FF651F65"/>
<Setter Property="BorderBrush" Value="#FF651F65"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="Padding" Value="8,4"/>
<Setter Property="SnapsToDevicePixels" Value="True"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{Binding Path=(md:ButtonHelper.CornerRadius), RelativeSource={RelativeSource TemplatedParent}}"
SnapsToDevicePixels="True">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="#FF953895"/>
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter Property="Background" Value="#AA953895"/>
</Trigger>
</Style.Triggers>
</Style>
<Style TargetType="Button" x:Key="FancySelectedButton">
<Setter Property="Cursor" Value="Arrow"/>
<Setter Property="FontFamily" Value="Cascadia Mono"/>
<Setter Property="FontSize" Value="20"/>
<Setter Property="Foreground" Value="White"/>
<Setter Property="Background" Value="DarkGray"/>
<Setter Property="BorderBrush" Value="DarkGray"/>
<Setter Property="BorderThickness" Value="2"/>
<Setter Property="Padding" Value="8,4"/>
<Setter Property="SnapsToDevicePixels" Value="True"/>
<Setter Property="Effect">
<Setter.Value>
<DropShadowEffect Color="#4C808080" BlurRadius="2" ShadowDepth="2"/>
</Setter.Value>
</Setter>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="8"
SnapsToDevicePixels="True"
Effect="{TemplateBinding Effect}">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="DarkGray"/>
</Trigger>
</Style.Triggers>
</Style>
<Style x:Key="InfoTipButtonStyle" TargetType="Button">
<Setter Property="Height" Value="14"/>
<Setter Property="Width" Value="14"/>
<Setter Property="BorderBrush" Value="Purple"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="Background" Value="#05808080"/>
<Setter Property="Margin" Value="0,0,10,0"/>
<Setter Property="Cursor" Value="Hand"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="8">
<Label Content="?" FontSize="8" Padding="0"
HorizontalAlignment="Center" VerticalAlignment="Center"
FontWeight="Bold" Foreground="Purple"/>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="#0B5B06DE"/>
</Trigger>
</Style.Triggers>
</Style>
<Style x:Key="CloseButton" TargetType="Button">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border x:Name="Border"
CornerRadius="0,0,0,0" Background="{TemplateBinding Background}">
<ContentPresenter Content="{TemplateBinding Content}"/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="Border" Property="Background" Value="#7FFF0A0A"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="NormalButton" TargetType="Button">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border x:Name="Border"
CornerRadius="0,0,0,0" Background="{TemplateBinding Background}"
Padding="{TemplateBinding Padding}">
<ContentPresenter Content="{TemplateBinding Content}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="Border" Property="Background" Value="#FF969696"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="PushButton" TargetType="Button">
<Setter Property="HorizontalContentAlignment" Value="Center"/>
<Setter Property="VerticalContentAlignment" Value="Center"/>
<Setter Property="Background" Value="#FF6900CC"/>
<Setter Property="Foreground" Value="White"/>
<Setter Property="BorderBrush" Value="#FFD4A5FA"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Width" Value="60"/>
<Setter Property="Height" Value="40"/>
<Setter Property="Padding" Value="2"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border x:Name="Border"
CornerRadius="5" Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
Padding="{TemplateBinding Padding}"
Width="{TemplateBinding Width}"
Height="{TemplateBinding Height}">
<ContentPresenter Content="{TemplateBinding Content}"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
TextBlock.Foreground="{TemplateBinding Foreground}"
/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="False">
<Setter Property="Background" Value="#FF6900CC"/>
</Trigger>
<Trigger Property="IsMouseOver" Value="true">
<Setter Property="Cursor" Value="Hand"/>
<Setter Property="BorderThickness" Value="1"/>
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter Property="Background" Value="#FFD6BDEE"/>
</Trigger>
<EventTrigger RoutedEvent="Button.MouseEnter">
<BeginStoryboard>
<Storyboard>
<ColorAnimation
Storyboard.TargetProperty="(Button.Background).(SolidColorBrush.Color)"
To="#FF782AC3" Duration="0:0:0.2" />
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="RoundedTextBoxStyle" TargetType="{x:Type TextBox}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type TextBox}">
<Border x:Name="Border"
CornerRadius="5"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
HorizontalAlignment="{TemplateBinding HorizontalAlignment}"
VerticalAlignment="{TemplateBinding VerticalAlignment}"
Width="{TemplateBinding Width}"
Height="{TemplateBinding Height}">
<ScrollViewer x:Name="PART_ContentHost"
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"
FontSize="{TemplateBinding FontSize}" />
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsFocused" Value="True">
<Setter TargetName="Border" Property="BorderBrush" Value="#FF805C8F"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
@@ -0,0 +1,224 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<!-- 简单的滑动开关样式 -->
<Style x:Key="SimpleToggleSwitch" TargetType="{x:Type CheckBox}">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="BorderBrush" Value="Transparent"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="HorizontalAlignment" Value="Left"/>
<Setter Property="VerticalAlignment" Value="Center"/>
<Setter Property="Cursor" Value="Hand"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type CheckBox}">
<Grid>
<!-- 开关容器 -->
<Border x:Name="SwitchContainer"
Width="80"
Height="36"
CornerRadius="18"
BorderBrush="#CCCCCC"
BorderThickness="2"
Background="White"
SnapsToDevicePixels="True">
<Grid Margin="0">
<!-- 紫色背景区域 -->
<Border x:Name="PurpleArea"
CornerRadius="18"
Background="#9C27B0"
HorizontalAlignment="Left"
Width="36"/>
<!-- 滑动圆形 -->
<Ellipse x:Name="ThumbCircle"
Width="28"
Height="28"
Fill="White"
HorizontalAlignment="Left"
VerticalAlignment="Center"
Margin="4,0,0,0">
<Ellipse.Effect>
<DropShadowEffect ShadowDepth="1"
Opacity="0.3"
BlurRadius="3"/>
</Ellipse.Effect>
</Ellipse>
</Grid>
</Border>
</Grid>
<ControlTemplate.Triggers>
<!-- 选中状态(开) -->
<Trigger Property="IsChecked" Value="True">
<Trigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<!-- 移动滑块到右边 -->
<ThicknessAnimation Storyboard.TargetName="ThumbCircle"
Storyboard.TargetProperty="Margin"
To="45,0,0,0"
Duration="0:0:0.2"/>
<!-- 展开紫色区域到全宽 -->
<DoubleAnimation Storyboard.TargetName="PurpleArea"
Storyboard.TargetProperty="Width"
To="76"
Duration="0:0:0.2"/>
</Storyboard>
</BeginStoryboard>
</Trigger.EnterActions>
<Trigger.ExitActions>
<BeginStoryboard>
<Storyboard>
<!-- 返回左边 -->
<ThicknessAnimation Storyboard.TargetName="ThumbCircle"
Storyboard.TargetProperty="Margin"
To="4,0,0,0"
Duration="0:0:0.2"/>
<!-- 收缩紫色区域 -->
<DoubleAnimation Storyboard.TargetName="PurpleArea"
Storyboard.TargetProperty="Width"
To="36"
Duration="0:0:0.2"/>
</Storyboard>
</BeginStoryboard>
</Trigger.ExitActions>
</Trigger>
<!-- 鼠标悬停效果 -->
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="SwitchContainer" Property="BorderBrush" Value="#888888"/>
</Trigger>
<!-- 按下效果 -->
<Trigger Property="IsPressed" Value="True">
<Setter TargetName="ThumbCircle" Property="RenderTransformOrigin" Value="0.5,0.5"/>
<Setter TargetName="ThumbCircle" Property="RenderTransform">
<Setter.Value>
<ScaleTransform ScaleX="0.9" ScaleY="0.9"/>
</Setter.Value>
</Setter>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!-- 带标签的开关样式 -->
<Style x:Key="LabeledToggleSwitch" TargetType="{x:Type CheckBox}" BasedOn="{StaticResource SimpleToggleSwitch}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type CheckBox}">
<Viewbox Height="{TemplateBinding Height}" Width="{TemplateBinding Width}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="10"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<!-- 开关部分 -->
<Border x:Name="SwitchContainer"
Width="80"
Height="36"
CornerRadius="18"
BorderBrush="#CCCCCC"
BorderThickness="2"
Background="White"
Grid.Column="0">
<Grid>
<!-- 紫色背景区域 -->
<Border x:Name="PurpleArea"
CornerRadius="18"
Background="#9C27B0"
HorizontalAlignment="Left"
Width="36"/>
<!-- 滑动圆形 -->
<Ellipse x:Name="ThumbCircle"
Width="28"
Height="28"
Fill="White"
HorizontalAlignment="Left"
VerticalAlignment="Center"
Margin="4,0,0,0">
<Ellipse.Effect>
<DropShadowEffect ShadowDepth="1"
Opacity="0.3"
BlurRadius="3"/>
</Ellipse.Effect>
</Ellipse>
</Grid>
</Border>
<!-- 标签文本 -->
<TextBlock x:Name="LabelText"
Grid.Column="2"
VerticalAlignment="Center"
Text="{TemplateBinding Content}"
FontSize="14"/>
</Grid>
</Viewbox>
<ControlTemplate.Triggers>
<!-- 选中状态(开) -->
<Trigger Property="IsChecked" Value="True">
<Trigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<!-- 移动滑块到右边 -->
<ThicknessAnimation Storyboard.TargetName="ThumbCircle"
Storyboard.TargetProperty="Margin"
To="48,0,0,0"
Duration="0:0:0.2"/>
<!-- 展开紫色区域到全宽 -->
<DoubleAnimation Storyboard.TargetName="PurpleArea"
Storyboard.TargetProperty="Width"
To="80"
Duration="0:0:0.2"/>
</Storyboard>
</BeginStoryboard>
</Trigger.EnterActions>
<Trigger.ExitActions>
<BeginStoryboard>
<Storyboard>
<!-- 返回左边 -->
<ThicknessAnimation Storyboard.TargetName="ThumbCircle"
Storyboard.TargetProperty="Margin"
To="4,0,0,0"
Duration="0:0:0.2"/>
<!-- 收缩紫色区域 -->
<DoubleAnimation Storyboard.TargetName="PurpleArea"
Storyboard.TargetProperty="Width"
To="36"
Duration="0:0:0.2"/>
</Storyboard>
</BeginStoryboard>
</Trigger.ExitActions>
</Trigger>
<!-- 鼠标悬停效果 -->
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="SwitchContainer" Property="BorderBrush" Value="#888888"/>
</Trigger>
<!-- 按下效果 -->
<Trigger Property="IsPressed" Value="True">
<Setter TargetName="ThumbCircle" Property="RenderTransformOrigin" Value="0.5,0.5"/>
<Setter TargetName="ThumbCircle" Property="RenderTransform">
<Setter.Value>
<ScaleTransform ScaleX="0.9" ScaleY="0.9"/>
</Setter.Value>
</Setter>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
@@ -0,0 +1,133 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:models="clr-namespace:FancyInput.Models">
<models:EnumDescriptionConverter x:Key="EnumDescriptionConverter"/>
<Style x:Key="RoundCornerComboBox" TargetType="{x:Type ComboBox}">
<Setter Property="IsEditable" Value="False"/>
<Setter Property="Width" Value="85"/>
<Setter Property="Height" Value="25"/>
<Setter Property="ItemTemplate">
<Setter.Value>
<DataTemplate>
<TextBlock Text="{Binding Converter={StaticResource EnumDescriptionConverter}}"/>
</DataTemplate>
</Setter.Value>
</Setter>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ComboBox}">
<Grid>
<ToggleButton x:Name="ToggleButton" Focusable="false"
HorizontalAlignment="Right" VerticalAlignment="Center"
Width="{TemplateBinding Width}" Height="{TemplateBinding Height}"
IsChecked="{Binding Path=IsDropDownOpen,
RelativeSource={RelativeSource TemplatedParent}}"
Background="{TemplateBinding Background}"
Foreground="{TemplateBinding Foreground}"
ClickMode="Press">
<ToggleButton.Template>
<ControlTemplate TargetType="{x:Type ToggleButton}">
<Border Background="{TemplateBinding Background}"
x:Name="Border"
Width="{TemplateBinding Width}" Height="{TemplateBinding Height}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="5">
<Path x:Name="Arrow" Fill="{TemplateBinding Foreground}"
HorizontalAlignment="Right" VerticalAlignment="Center"
Width="15"
Data="M 0 0 L 4 4 L 8 0 Z"
Margin="0,0,0,0"/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsChecked" Value="True">
<Setter TargetName="Arrow" Property="Fill" Value="#FF651F65"/>
<Setter TargetName="Border" Property="BorderBrush" Value="#FF651F65"/>
</Trigger>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="Arrow" Property="Fill" Value="#AA651F65"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</ToggleButton.Template>
</ToggleButton>
<ContentPresenter x:Name="ContentSite"
IsHitTestVisible="False"
VerticalAlignment="Center"
Content="{TemplateBinding SelectionBoxItem}"
Margin="0,2,20,2">
<ContentPresenter.ContentTemplate>
<DataTemplate >
<TextBlock Text="{Binding Converter={StaticResource EnumDescriptionConverter}}"
HorizontalAlignment="Center"
VerticalAlignment="Center" />
</DataTemplate>
</ContentPresenter.ContentTemplate>
</ContentPresenter>
<Popup x:Name="PART_Popup" Placement="Bottom" IsOpen="{TemplateBinding IsDropDownOpen}"
AllowsTransparency="True" Focusable="False" PopupAnimation="Slide">
<Grid MaxHeight="{TemplateBinding MaxDropDownHeight}" MinWidth="{TemplateBinding ActualWidth}"
x:Name="DropDown" SnapsToDevicePixels="True" Background="Transparent">
<Border x:Name="DropDownBorder"
BorderBrush="#FF651F65"
Background="{TemplateBinding Background}"
BorderThickness="1"
CornerRadius="10"
Padding="0"
SnapsToDevicePixels="True">
<Border.Effect>
<DropShadowEffect Color="#44000000" BlurRadius="10" ShadowDepth="2" Opacity="0.5"/>
</Border.Effect>
<ScrollViewer SnapsToDevicePixels="True" Style="{StaticResource SimpleScrollViewerStyle}">
<StackPanel IsItemsHost="True" KeyboardNavigation.DirectionalNavigation="Contained"
Orientation="Vertical"/>
</ScrollViewer>
</Border>
</Grid>
</Popup>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style TargetType="ComboBoxItem" x:Key="CustomComboBoxItemStyle">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="Foreground" Value="Black"/>
<Setter Property="Padding" Value="4,2"/>
<Setter Property="HorizontalContentAlignment" Value="Left"/>
<Setter Property="VerticalContentAlignment" Value="Center"/>
<Setter Property="SnapsToDevicePixels" Value="True"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ComboBoxItem">
<Border x:Name="Border"
Background="{TemplateBinding Background}"
CornerRadius="6"
Padding="{TemplateBinding Padding}">
<ContentPresenter
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsHighlighted" Value="True">
<Setter TargetName="Border" Property="Background" Value="#77651F65"/>
<Setter Property="Foreground" Value="White"/>
</Trigger>
<Trigger Property="IsSelected" Value="True">
<Setter TargetName="Border" Property="Background" Value="#FF651F65"/>
<Setter Property="Foreground" Value="White"/>
</Trigger>
<Trigger Property="IsEnabled" Value="False">
<Setter Property="Foreground" Value="#FFBFBFBF"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
@@ -0,0 +1,42 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Style x:Key="IconButtonDisabledOpacityStyle" TargetType="UserControl">
<Setter Property="Opacity" Value="1"/>
<Style.Triggers>
<Trigger Property="IsEnabled" Value="False">
<Setter Property="Opacity" Value="0.45"/>
</Trigger>
</Style.Triggers>
</Style>
<Style x:Key="ChessPieceHoverOverlayEllipseStyle" TargetType="Ellipse">
<Setter Property="Fill" Value="#02000000"/>
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Fill" Value="#4CFFFFFF"/>
</Trigger>
</Style.Triggers>
</Style>
<Style x:Key="MainMenuGameHoverOverlayBorderStyle" TargetType="Border">
<Setter Property="Background" Value="#01000000"/>
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background">
<Setter.Value>
<LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
<GradientStop Color="#11FFFFFF"/>
<GradientStop Color="#66000000" Offset="1"/>
</LinearGradientBrush>
</Setter.Value>
</Setter>
</Trigger>
</Style.Triggers>
</Style>
<Style x:Key="ChineseChessBoardItemContainerStyle" TargetType="ContentPresenter">
<Setter Property="Canvas.Left" Value="{Binding VisualY}"/>
<Setter Property="Canvas.Top" Value="{Binding VisaulX}"/>
<Setter Property="Panel.ZIndex" Value="{Binding ZIndex}"/>
</Style>
</ResourceDictionary>
@@ -0,0 +1,29 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Style x:Key="RoundedLabel" TargetType="Label">
<Setter Property="Background" Value="#FFF"/>
<Setter Property="Padding" Value="8,2"/>
<Setter Property="HorizontalContentAlignment" Value="Center"/>
<Setter Property="VerticalContentAlignment" Value="Center"/>
<Setter Property="Effect">
<Setter.Value>
<DropShadowEffect Color="#33141014" BlurRadius="1" ShadowDepth="1"/>
</Setter.Value>
</Setter>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Label">
<Border Background="{TemplateBinding Background}"
CornerRadius="3"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}">
<ContentPresenter HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
RecognizesAccessKey="True"
Margin="{TemplateBinding Padding}"/>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
+125
View File
@@ -0,0 +1,125 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Style x:Key="SimpleMenuItem" TargetType="MenuItem">
<Setter Property="Foreground" Value="{Binding Foreground, RelativeSource={RelativeSource AncestorType=Menu}}"/>
<Setter Property="Background" Value="{Binding Background, RelativeSource={RelativeSource AncestorType=Menu}}"/>
<Setter Property="Padding" Value="10,2"/>
<Setter Property="ItemContainerStyle">
<Setter.Value>
<Style TargetType="MenuItem">
<Setter Property="Foreground" Value="{Binding Foreground, RelativeSource={RelativeSource AncestorType=Menu}}"/>
<Setter Property="Background" Value="{Binding Background, RelativeSource={RelativeSource AncestorType=Menu}}"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="MenuItem">
<Border x:Name="Item" Background="{TemplateBinding Background}" Margin="0" VerticalAlignment="Stretch"
Height="25" Width="170">
<Grid HorizontalAlignment="Left" VerticalAlignment="Center">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="35"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<ContentPresenter ContentSource="Icon" Margin="5,0,5,0" VerticalAlignment="Center" Grid.Column="0"/>
<ContentPresenter ContentSource="Header" RecognizesAccessKey="True" Grid.Column="1"/>
<Popup x:Name="PART_Popup"
Placement="Right"
IsOpen="{TemplateBinding IsSubmenuOpen}"
AllowsTransparency="True"
Focusable="False"
PopupAnimation="Fade">
<Border Background="{Binding Background, RelativeSource={RelativeSource AncestorType=Menu}}"
BorderBrush="#7F8E8E8E" BorderThickness="1">
<ItemsPresenter />
</Border>
</Popup>
</Grid>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="Item" Property="Background" Value="#4CDDDDDD"/>
</Trigger>
<Trigger Property="IsSubmenuOpen" Value="True">
<Setter TargetName="Item" Property="Background" Value="#4CDDDDDD"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Setter.Value>
</Setter>
<Setter Property="OverridesDefaultStyle" Value="True"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="MenuItem">
<Border x:Name="Item" Background="{TemplateBinding Background}" Margin="0" VerticalAlignment="Stretch" Height="25" Width="70">
<Grid HorizontalAlignment="Center" VerticalAlignment="Center">
<ContentPresenter ContentSource="Header" RecognizesAccessKey="True" />
<Popup x:Name="PART_Popup"
Placement="Bottom"
IsOpen="{TemplateBinding IsSubmenuOpen}"
AllowsTransparency="True"
Focusable="False"
PopupAnimation="Fade">
<Border Background="{Binding Background, RelativeSource={RelativeSource AncestorType=Menu}}"
BorderBrush="#7F8E8E8E" BorderThickness="1">
<ItemsPresenter />
</Border>
</Popup>
</Grid>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="Item" Property="Background" Value="#4CDDDDDD"/>
</Trigger>
<Trigger Property="IsSubmenuOpen" Value="True">
<Setter TargetName="Item" Property="Background" Value="#4CDDDDDD"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="SimpleMenuSeparator" TargetType="Separator">
<Setter Property="SnapsToDevicePixels" Value="True"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Separator">
<Grid Margin="4,2,4,2" Height="2">
<Rectangle Height="1"
VerticalAlignment="Center"
Fill="#4CDDDDDD"/>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="ContextSimpleMenuItem" TargetType="MenuItem">
<Setter Property="Foreground" Value="{Binding Foreground, RelativeSource={RelativeSource AncestorType=ContextMenu}}"/>
<Setter Property="Background" Value="{Binding Background, RelativeSource={RelativeSource AncestorType=ContextMenu}}"/>
<Setter Property="Margin" Value="0"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="MenuItem">
<Border x:Name="Border" Background="{TemplateBinding Background}" Margin="0" VerticalAlignment="Stretch"
Height="25" Width="100" CornerRadius="5">
<StackPanel x:Name="Item" Orientation="Horizontal">
<ContentPresenter ContentSource="Icon"/>
<TextBlock x:Name="HeaderText" Margin="10,0,0,0" VerticalAlignment="Center"
Text="{TemplateBinding Header}" Foreground="{TemplateBinding Foreground}"/>
</StackPanel>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="Item" Property="Background" Value="#FFBBA1FB"/>
<Setter TargetName="HeaderText" Property="Foreground" Value="Black"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
@@ -0,0 +1,164 @@
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<SolidColorBrush x:Key="ScrollViewerBgBrush" Color="Transparent" />
<SolidColorBrush x:Key="ScrollViewerBorderBrush" Color="Transparent" />
<SolidColorBrush x:Key="ScrollBarTrackBrush" Color="#EEF2F6" />
<SolidColorBrush x:Key="ScrollBarThumbBrush" Color="#A8B3C2" />
<SolidColorBrush x:Key="ScrollBarThumbHoverBrush" Color="#7F8EA3" />
<Style x:Key="SimpleThumbStyle" TargetType="Thumb">
<Setter Property="OverridesDefaultStyle" Value="True" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Thumb">
<Border
x:Name="ThumbBorder"
Margin="1"
CornerRadius="4"
Background="{StaticResource ScrollBarThumbBrush}" />
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="ThumbBorder" Property="Background" Value="{StaticResource ScrollBarThumbHoverBrush}" />
</Trigger>
<Trigger Property="IsDragging" Value="True">
<Setter TargetName="ThumbBorder" Property="Background" Value="{StaticResource ScrollBarThumbHoverBrush}" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="SimpleRepeatButtonStyle" TargetType="RepeatButton">
<Setter Property="Focusable" Value="False" />
<Setter Property="IsTabStop" Value="False" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="RepeatButton">
<Border Background="Transparent" />
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="SimpleVerticalScrollBarStyle" TargetType="ScrollBar">
<Setter Property="Width" Value="8" />
<Setter Property="Background" Value="{StaticResource ScrollBarTrackBrush}" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ScrollBar">
<Border Width="{TemplateBinding Width}"
Margin="2" CornerRadius="5"
Background="{TemplateBinding Background}">
<Track x:Name="PART_Track" IsDirectionReversed="True">
<Track.DecreaseRepeatButton>
<RepeatButton
Command="ScrollBar.PageUpCommand"
Style="{StaticResource SimpleRepeatButtonStyle}" />
</Track.DecreaseRepeatButton>
<Track.Thumb>
<Thumb Style="{StaticResource SimpleThumbStyle}" />
</Track.Thumb>
<Track.IncreaseRepeatButton>
<RepeatButton
Command="ScrollBar.PageDownCommand"
Style="{StaticResource SimpleRepeatButtonStyle}" />
</Track.IncreaseRepeatButton>
</Track>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="SimpleHorizontalScrollBarStyle" TargetType="ScrollBar">
<Setter Property="Height" Value="8" />
<Setter Property="Background" Value="{StaticResource ScrollBarTrackBrush}" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ScrollBar">
<Border
Margin="2"
CornerRadius="5"
Background="{TemplateBinding Background}">
<Track x:Name="PART_Track">
<Track.DecreaseRepeatButton>
<RepeatButton
Command="ScrollBar.PageLeftCommand"
Style="{StaticResource SimpleRepeatButtonStyle}" />
</Track.DecreaseRepeatButton>
<Track.Thumb>
<Thumb Style="{StaticResource SimpleThumbStyle}" />
</Track.Thumb>
<Track.IncreaseRepeatButton>
<RepeatButton
Command="ScrollBar.PageRightCommand"
Style="{StaticResource SimpleRepeatButtonStyle}" />
</Track.IncreaseRepeatButton>
</Track>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="SimpleScrollViewerStyle" TargetType="ScrollViewer">
<Setter Property="Background" Value="{StaticResource ScrollViewerBgBrush}" />
<Setter Property="BorderBrush" Value="{StaticResource ScrollViewerBorderBrush}" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="Padding" Value="6" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ScrollViewer">
<Border
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="2">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<ScrollContentPresenter
Grid.Row="0"
Grid.Column="0"
Margin="{TemplateBinding Padding}"
CanContentScroll="{TemplateBinding CanContentScroll}" />
<ScrollBar
x:Name="PART_VerticalScrollBar"
Grid.Row="0"
Grid.Column="1"
Orientation="Vertical"
Maximum="{TemplateBinding ScrollableHeight}"
ViewportSize="{TemplateBinding ViewportHeight}"
Value="{Binding VerticalOffset, RelativeSource={RelativeSource TemplatedParent}, Mode=OneWay}"
Visibility="{TemplateBinding ComputedVerticalScrollBarVisibility}"
Style="{StaticResource SimpleVerticalScrollBarStyle}" />
<ScrollBar
x:Name="PART_HorizontalScrollBar"
Grid.Row="1"
Grid.Column="0"
Orientation="Horizontal"
Maximum="{TemplateBinding ScrollableWidth}"
ViewportSize="{TemplateBinding ViewportWidth}"
Value="{Binding HorizontalOffset, RelativeSource={RelativeSource TemplatedParent}, Mode=OneWay}"
Visibility="{TemplateBinding ComputedHorizontalScrollBarVisibility}"
Style="{StaticResource SimpleHorizontalScrollBarStyle}" />
</Grid>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
@@ -0,0 +1,266 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:md="clr-namespace:FancyInput.Models">
<!-- 主要Slider样式 - 使用MultiBinding -->
<Style x:Key="ModernSlider" TargetType="{x:Type Slider}">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="BorderBrush" Value="Transparent"/>
<Setter Property="Height" Value="40"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Slider}">
<Grid>
<!-- 轨道背景 -->
<Border x:Name="TrackBackground"
Height="8"
CornerRadius="4"
Background="#E0E0E0"
VerticalAlignment="Center"/>
<!-- 进度轨道 -->
<Border x:Name="ProgressTrack"
Height="8"
CornerRadius="4"
HorizontalAlignment="Left"
VerticalAlignment="Center">
<Border.Background>
<LinearGradientBrush StartPoint="0,0" EndPoint="1,0">
<GradientStop Color="#9C27B0" Offset="0"/>
<GradientStop Color="#BA68C8" Offset="1"/>
</LinearGradientBrush>
</Border.Background>
<!-- 使用MultiBinding计算宽度 -->
<Border.Width>
<MultiBinding >
<Binding Path="Value" RelativeSource="{RelativeSource TemplatedParent}"/>
<Binding Path="Minimum" RelativeSource="{RelativeSource TemplatedParent}"/>
<Binding Path="Maximum" RelativeSource="{RelativeSource TemplatedParent}"/>
<Binding ElementName="TrackBackground" Path="ActualWidth"/>
<MultiBinding.Converter>
<md:SliderProgressConverter/>
</MultiBinding.Converter>
</MultiBinding>
</Border.Width>
</Border>
<!-- 滑块轨道 -->
<Track x:Name="PART_Track">
<Track.Thumb>
<Thumb x:Name="Thumb"
Focusable="False"
Height="32"
Width="32">
<Thumb.Template>
<ControlTemplate TargetType="{x:Type Thumb}">
<Grid>
<!-- 圆形滑块 -->
<Ellipse Width="28" Height="28"
Fill="White"
Stroke="#9C27B0"
StrokeThickness="2">
<Ellipse.Effect>
<DropShadowEffect ShadowDepth="1"
Opacity="0.3"
BlurRadius="4"/>
</Ellipse.Effect>
</Ellipse>
<!-- 内圈指示 -->
<Ellipse Width="12" Height="12"
Fill="#9C27B0"/>
</Grid>
</ControlTemplate>
</Thumb.Template>
</Thumb>
</Track.Thumb>
</Track>
</Grid>
<ControlTemplate.Triggers>
<!-- 禁用状态 -->
<Trigger Property="IsEnabled" Value="False">
<Setter TargetName="TrackBackground" Property="Opacity" Value="0.5"/>
<Setter TargetName="ProgressTrack" Property="Opacity" Value="0.5"/>
<Setter TargetName="Thumb" Property="Opacity" Value="0.5"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!-- 简单版本 - 没有进度条的Slider -->
<Style x:Key="SimpleSlider" TargetType="{x:Type Slider}">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="BorderBrush" Value="Transparent"/>
<Setter Property="Height" Value="40"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Slider}">
<Grid>
<!-- 轨道背景 -->
<Border Height="4" CornerRadius="3" Background="#E0E0E0" VerticalAlignment="Center"/>
<!-- 滑块轨道 -->
<Track x:Name="PART_Track">
<Track.Thumb>
<Thumb x:Name="Thumb" Focusable="False" Height="{TemplateBinding Height}" Width="{TemplateBinding Height}">
<Thumb.Template>
<ControlTemplate TargetType="{x:Type Thumb}">
<Ellipse Width="{TemplateBinding Height}" Height="{TemplateBinding Height}" Fill="White" Stroke="#9C27B0" StrokeThickness="2">
<Ellipse.Effect>
<DropShadowEffect ShadowDepth="1" Opacity="0.2" BlurRadius="3"/>
</Ellipse.Effect>
</Ellipse>
</ControlTemplate>
</Thumb.Template>
</Thumb>
</Track.Thumb>
</Track>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!-- 迷你版Slider - 更细的轨道 -->
<Style x:Key="MiniSlider" TargetType="{x:Type Slider}">
<Setter Property="Background" Value="#E0E0E0"/>
<Setter Property="Foreground" Value="#9C27B0"/>
<Setter Property="Height" Value="30"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Slider}">
<Grid>
<!-- 轨道背景 -->
<Border x:Name="TrackBackground"
Height="4"
CornerRadius="2"
Background="{TemplateBinding Background}"
VerticalAlignment="Center"/>
<!-- 进度轨道 -->
<Border x:Name="ProgressTrack"
Height="4"
CornerRadius="2"
HorizontalAlignment="Left"
VerticalAlignment="Center"
Background="{TemplateBinding Foreground}">
<!-- 使用MultiBinding计算宽度 -->
<Border.Width>
<MultiBinding>
<Binding Path="Value" RelativeSource="{RelativeSource TemplatedParent}"/>
<Binding Path="Minimum" RelativeSource="{RelativeSource TemplatedParent}"/>
<Binding Path="Maximum" RelativeSource="{RelativeSource TemplatedParent}"/>
<Binding ElementName="TrackBackground" Path="ActualWidth"/>
<MultiBinding.Converter>
<md:SliderProgressConverter/>
</MultiBinding.Converter>
</MultiBinding>
</Border.Width>
</Border>
<!-- 滑块轨道 -->
<Track x:Name="PART_Track">
<Track.Thumb>
<Thumb x:Name="Thumb"
Focusable="False"
Background="{TemplateBinding Background}"
Foreground="{TemplateBinding Foreground}"
Height="24"
Width="24">
<Thumb.Template>
<ControlTemplate TargetType="{x:Type Thumb}">
<Grid>
<Ellipse Width="20" Height="20"
Fill="White"
Stroke="{TemplateBinding Foreground}"
StrokeThickness="2"/>
<Ellipse Width="10" Height="10"
HorizontalAlignment="Center" VerticalAlignment="Center"
Fill="{TemplateBinding Foreground}"
StrokeThickness="2"/>
</Grid>
</ControlTemplate>
</Thumb.Template>
</Thumb>
</Track.Thumb>
</Track>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!-- 垂直Slider专用样式 -->
<Style x:Key="VerticalMiniSlider" TargetType="{x:Type Slider}">
<Setter Property="Background" Value="#E0E0E0"/>
<Setter Property="Foreground" Value="#9C27B0"/>
<Setter Property="Width" Value="30"/>
<Setter Property="Orientation" Value="Vertical"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Slider}">
<Grid>
<!-- 轨道背景 (垂直) -->
<Border x:Name="TrackBackground"
Width="4"
CornerRadius="2"
Background="{TemplateBinding Background}"
HorizontalAlignment="Center"/>
<!-- 进度轨道 (垂直,从下往上) -->
<Border x:Name="ProgressTrack"
Width="4"
CornerRadius="2"
HorizontalAlignment="Center"
VerticalAlignment="Bottom"
Background="{TemplateBinding Foreground}">
<!-- 垂直Slider:高度由值决定 -->
<Border.Height>
<MultiBinding >
<Binding Path="Value" RelativeSource="{RelativeSource TemplatedParent}"/>
<Binding Path="Minimum" RelativeSource="{RelativeSource TemplatedParent}"/>
<Binding Path="Maximum" RelativeSource="{RelativeSource TemplatedParent}"/>
<Binding ElementName="TrackBackground" Path="ActualHeight"/>
<MultiBinding.Converter>
<md:SliderProgressConverter/>
</MultiBinding.Converter>
</MultiBinding>
</Border.Height>
</Border>
<!-- 滑块轨道 -->
<Track x:Name="PART_Track">
<Track.Thumb>
<Thumb x:Name="Thumb"
Focusable="False"
Background="{TemplateBinding Background}"
Foreground="{TemplateBinding Foreground}"
Height="24"
Width="24">
<Thumb.Template>
<ControlTemplate TargetType="{x:Type Thumb}">
<Grid>
<Ellipse Width="20" Height="20"
Fill="White"
Stroke="{TemplateBinding Foreground}"
StrokeThickness="2"/>
<Ellipse Width="10" Height="10"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Fill="{TemplateBinding Foreground}"/>
</Grid>
</ControlTemplate>
</Thumb.Template>
</Thumb>
</Track.Thumb>
</Track>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
@@ -0,0 +1,66 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Style TargetType="TextBox" x:Key="FancyTextBox">
<Setter Property="FontFamily" Value="Cascadia Mono"/>
<Setter Property="FontSize" Value="20"/>
<Setter Property="Foreground" Value="#FF651F65"/>
<Setter Property="Background" Value="#FFF8F8FF"/>
<Setter Property="BorderBrush" Value="#FF651F65"/>
<Setter Property="BorderThickness" Value="2"/>
<Setter Property="Padding" Value="8,4"/>
<Setter Property="SnapsToDevicePixels" Value="True"/>
<Setter Property="Effect">
<Setter.Value>
<DropShadowEffect Color="#88651F65" BlurRadius="2" ShadowDepth="2"/>
</Setter.Value>
</Setter>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="TextBox">
<Border x:Name="border"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="8"
SnapsToDevicePixels="True"
Effect="{TemplateBinding Effect}">
<ScrollViewer x:Name="PART_ContentHost"
Margin="0"
VerticalAlignment="Center"/>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style TargetType="TextBox" x:Key="RoundCornerTextBox">
<Setter Property="HorizontalContentAlignment" Value="Center"/>
<Setter Property="VerticalContentAlignment" Value="Center"/>
<Setter Property="FontFamily" Value="Cascadia Mono"/>
<Setter Property="FontSize" Value="20"/>
<Setter Property="Foreground" Value="#FF651F65"/>
<Setter Property="Background" Value="#FFF8F8FF"/>
<Setter Property="BorderBrush" Value="#FF651F65"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="Padding" Value="0"/>
<Setter Property="SnapsToDevicePixels" Value="True"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="TextBox">
<Border x:Name="border"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
Padding="{TemplateBinding Padding}"
CornerRadius="4"
SnapsToDevicePixels="True">
<ScrollViewer x:Name="PART_ContentHost"
Margin="0"
VerticalAlignment="Center"/>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
@@ -0,0 +1,308 @@
using FancyInput.Common;
using FancyInput.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
namespace FancyInput.ViewModels
{
public class ElementSegmentViewModel: ViewModelBase
{
private const double DefaultCanvasSize = 100.0;
private double _canvasWidthForEditor = DefaultCanvasSize;
private double _canvasHeightForEditor = DefaultCanvasSize;
private string _name = string.Empty;
public string Name
{
get => _name;
set=> SetProperty(ref _name, value);
}
private XImage? _image;
public XImage? Image
{
get => _image;
set
{
SetProperty(ref _image, value);
OnPropertyChanged(nameof(ImageSource));
OnPropertyChanged(nameof(Width));
OnPropertyChanged(nameof(Height));
if (!(_image!=null && value !=null && _image.Width == value.Width && _image.Height == value.Height))
RecalculateEditorRanges();
}
}
public ImageSource? ImageSource => Image?.BitmapSource;
public double Width => Image != null ? Image.Width : 0;
public double Height => Image != null ? Image.Height : 0;
private bool _isVisible = true;
public bool IsVisible
{
get => _isVisible;
set
{
SetProperty(ref _isVisible, value);
OnPropertyChanged(nameof(SegmentVisibility));
OnPropertyChanged(nameof(VisibilityIcon));
OnPropertyChanged(nameof(VisibilityToolTip));
OnPropertyChanged(nameof(VisibilityButtonBrush));
}
}
public Visibility SegmentVisibility => IsVisible ? Visibility.Visible : Visibility.Collapsed;
public Geometry VisibilityIcon => _isVisible ? PathDataGeometry.EyeOn : PathDataGeometry.EyeOff;
public string VisibilityToolTip => _isVisible ? "隐藏" : "显示";
public Brush VisibilityButtonBrush => _isVisible ?new SolidColorBrush(Colors.White): new SolidColorBrush( Color.FromArgb(0xFF, 0xF4, 0x8C, 0x8C));
private bool _isLocked = false;
public bool IsLocked
{
get => _isLocked;
set
{
SetProperty(ref _isLocked, value);
OnPropertyChanged(nameof(LockIcon));
OnPropertyChanged(nameof(LockToolTip));
OnPropertyChanged(nameof(LockButtonBrush));
OnPropertyChanged(nameof(IsHitTestVisible));
}
}
public bool IsHitTestVisible => !IsLocked;
public Geometry LockIcon => _isLocked? PathDataGeometry.LockOn : PathDataGeometry.LockOff;
public string LockToolTip => _isLocked ? "解锁" : "锁定";
public Brush LockButtonBrush => _isLocked ? new SolidColorBrush( Color.FromArgb(0xFF, 0xF4, 0x8C, 0x8C)) : new SolidColorBrush(Colors.White);
private bool _isSelected = false;
public bool IsSelected
{
get => _isSelected;
set
{
SetProperty(ref _isSelected, value);
OnPropertyChanged(nameof(SelectColor1));
OnPropertyChanged(nameof(SelectColor2));
}
}
public Brush SelectColor1 => new SolidColorBrush(Colors.Gray);
public Brush SelectColor2 => new SolidColorBrush(_isSelected ? Color.FromRgb(0xFF,0xF2,0xFF) : Colors.White);
private double _localScaleMin = 0.2;
public double LocalScaleMin
{
get => _localScaleMin;
private set => SetProperty(ref _localScaleMin, value);
}
private double _localScaleMax = 4.0;
public double LocalScaleMax
{
get => _localScaleMax;
private set => SetProperty(ref _localScaleMax, value);
}
private double _offsetXMin = -100.0;
public double OffsetXMin
{
get => _offsetXMin;
private set => SetProperty(ref _offsetXMin, value);
}
private double _offsetXMax = 100.0;
public double OffsetXMax
{
get => _offsetXMax;
private set => SetProperty(ref _offsetXMax, value);
}
private double _offsetYMin = -100.0;
public double OffsetYMin
{
get => _offsetYMin;
private set => SetProperty(ref _offsetYMin, value);
}
private double _offsetYMax = 100.0;
public double OffsetYMax
{
get => _offsetYMax;
private set => SetProperty(ref _offsetYMax, value);
}
private double _alphaMin = 0.0;
public double AlphaMin
{
get => _alphaMin;
private set => SetProperty(ref _alphaMin, value);
}
private double _alphaMax = 1.0;
public double AlphaMax
{
get => _alphaMax;
private set => SetProperty(ref _alphaMax, value);
}
private double _localScaleWheelFactor = 1.03;
public double LocalScaleWheelFactor
{
get => _localScaleWheelFactor;
private set => SetProperty(ref _localScaleWheelFactor, value);
}
private double _offsetXWheelStep = 1.0;
public double OffsetXWheelStep
{
get => _offsetXWheelStep;
private set => SetProperty(ref _offsetXWheelStep, value);
}
private double _offsetYWheelStep = 1.0;
public double OffsetYWheelStep
{
get => _offsetYWheelStep;
private set => SetProperty(ref _offsetYWheelStep, value);
}
private double _alphaWheelStep = 0.02;
public double AlphaWheelStep
{
get => _alphaWheelStep;
private set => SetProperty(ref _alphaWheelStep, value);
}
private double _alpha = 0.5;
public double Alpha
{
get => _alpha;
set
{
if (IsLocked)
{
return;
}
SetProperty(ref _alpha, Clamp(value, AlphaMin, AlphaMax));
}
}
public event Action? SegmentChanged;
private void OnSegmentChanged() => SegmentChanged?.Invoke();
private double _localScale = 1.1;
public double LocalScale
{
get => _localScale;
set
{
if (IsLocked)
{
return;
}
if (SetProperty(ref _localScale, Clamp(value, LocalScaleMin, LocalScaleMax)))
{
OnSegmentChanged();
}
}
}
private double offsetX = 0.0;
public double OffsetX
{
get => offsetX;
set
{
if (IsLocked)
{
return;
}
if (SetProperty(ref offsetX, Clamp(value, OffsetXMin, OffsetXMax)))
{
OnSegmentChanged();
}
}
}
private double offsetY = 0.0;
public double OffsetY
{
get => offsetY;
set
{
if (IsLocked)
{
return;
}
if (SetProperty(ref offsetY, Clamp(value, OffsetYMin, OffsetYMax)))
{
OnSegmentChanged();
}
}
}
public void ConfigureEditorRanges(double canvasWidth, double canvasHeight)
{
_canvasWidthForEditor = Math.Max(1.0, canvasWidth);
_canvasHeightForEditor = Math.Max(1.0, canvasHeight);
RecalculateEditorRanges();
}
private void RecalculateEditorRanges()
{
double imageWidth = Math.Max(1.0, Width);
double imageHeight = Math.Max(1.0, Height);
double canvasWidth = Math.Max(1.0, _canvasWidthForEditor);
double canvasHeight = Math.Max(1.0, _canvasHeightForEditor);
double fitScale = Math.Min(canvasWidth / imageWidth, canvasHeight / imageHeight);
if (double.IsNaN(fitScale) || double.IsInfinity(fitScale) || fitScale <= 0)
{
fitScale = 1.0;
}
LocalScaleMin = Clamp(fitScale * 0.25, 0.05, 1.0);
LocalScaleMax = Clamp(fitScale * 8.0, 1.5, 20.0);
if (LocalScaleMax < LocalScaleMin * 1.5)
{
LocalScaleMax = LocalScaleMin * 1.5;
}
double offsetXSpan = Math.Max(canvasWidth, imageWidth) * 1.2;
double offsetYSpan = Math.Max(canvasHeight, imageHeight) * 1.2;
OffsetXMin = -offsetXSpan;
OffsetXMax = offsetXSpan;
OffsetYMin = -offsetYSpan;
OffsetYMax = offsetYSpan;
AlphaMin = 0.0;
AlphaMax = 1.0;
LocalScaleWheelFactor = fitScale < 0.75 ? 1.035 : 1.03;
OffsetXWheelStep = Math.Max(1.0, offsetXSpan / 200.0);
OffsetYWheelStep = Math.Max(1.0, offsetYSpan / 200.0);
AlphaWheelStep = 0.02;
LocalScale = LocalScale;
OffsetX = OffsetX;
OffsetY = OffsetY;
Alpha = Alpha;
}
private static double Clamp(double value, double min, double max)
{
if (value < min) return min;
if (value > max) return max;
return value;
}
}
}
@@ -0,0 +1,756 @@
using FancyInput.Models;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
//using System.Windows.Forms;
using System.Windows.Media;
using System.IO;
using SaveFileDialog = System.Windows.Forms.SaveFileDialog;
using DialogResult = System.Windows.Forms.DialogResult;
using System.Diagnostics;
using System.Collections.Specialized;
namespace FancyInput.ViewModels
{
public class ElementTreeViewModel : INotifyPropertyChanged,IDisposable
{
public event PropertyChangedEventHandler? PropertyChanged;
protected void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
protected virtual bool SetProperty<T>(ref T field, T value, [CallerMemberName] string propertyName = "")
{
if (Equals(field, value)) return false;
field = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
return true;
}
public string Name { get; set; } = string.Empty;
public LoadMode LoadMode { get; set; } = LoadMode.PngJson;
public string PngFilePath { get; set; } = string.Empty;
public string JsonFilePath { get; set; } = string.Empty;
public string ProjectFilePath { get; set; } = string.Empty;
private GamepadUserIdx _gamepadUserIndex = GamepadUserIdx.One;
public GamepadUserIdx GamepadUserIndex
{
get => _gamepadUserIndex;
set
{
for (int i = 0; i < ElementViewModels.Count; i++)
{
var elementViewModel = ElementViewModels[i];
switch (elementViewModel.ElementType)
{
case ElementType.GamepadButton:
case ElementType.GamepadTrigger:
case ElementType.AnalogStick:
case ElementType.DPad:
case ElementType.GamepadPlayerId:
elementViewModel.GamepadUserIndex = value;
break;
case ElementType.Texture:
case ElementType.KeyboardButton:
case ElementType.MouseButton:
case ElementType.MouseWheel:
case ElementType.MouseMovement:
break;
default:
break;
}
}
}
}
public bool IsGamepad
{
get
{
for (int i = 0; i < ElementViewModels.Count; i++)
{
switch (ElementViewModels[i].ElementType)
{
case ElementType.GamepadButton:
case ElementType.GamepadTrigger:
case ElementType.AnalogStick:
case ElementType.DPad:
case ElementType.GamepadPlayerId:
return true;
case ElementType.Texture:
case ElementType.KeyboardButton:
case ElementType.MouseButton:
case ElementType.MouseWheel:
case ElementType.MouseMovement:
break;
default:
break;
}
}
return false;
}
}
//private InputParser? _inputParser;
public InputParser? InputParser { get; set; } = null;
private InputHandler? _inputHandler;
public InputHandler InputHandler => _inputHandler ??= new InputHandler(this);
private bool _isTestEnabled = false;
public bool IsTestEnabled
{
get => _isTestEnabled;
set
{
SetProperty(ref _isTestEnabled, value);
if (InputParser != null)
{
if (value)
{
InputParser.GetInput -= InputParser_GetInput;
InputParser.GetInput += InputParser_GetInput;
}
else
{
InputParser.GetInput -= InputParser_GetInput;
}
}
}
}
public void InputParser_GetInput(object? sender, InputArgs e)
{
InputHandler.Handle(e);
}
public void Dispose()
{
if (InputParser != null)
{
InputParser.GetInput -= InputParser_GetInput;
InputParser = null;
}
if (_inputHandler != null)
{
_inputHandler.Dispose();
}
foreach (var element in ElementViewModels)
{
element.ParentCollection = null;
}
}
private Color _imageBackGroundColor = Colors.Black;
public Color ImageBackGroundColor
{
get => _imageBackGroundColor;
set
{
SetProperty(ref _imageBackGroundColor, value);
OnPropertyChanged(nameof(ImageBackground));
}
}
public SolidColorBrush ImageBackground => new SolidColorBrush(_imageBackGroundColor);
ObservableCollection<ElementViewModel> _elementViewModels = new();
public ObservableCollection<ElementViewModel> ElementViewModels
{
get => _elementViewModels;
set => SetProperty(ref _elementViewModels, value);
}
private ObservableCollection<ElementViewModel> _selectedElementViewModels = new();
public ObservableCollection<ElementViewModel> SelectedElementViewModels
{
get => _selectedElementViewModels;
set => SetProperty(ref _selectedElementViewModels, value);
}
private bool _isSelectedLocked = false;
public bool IsSelectedLocked
{
get => _isSelectedLocked;
set
{
SetProperty(ref _isSelectedLocked, value);
foreach (var element in SelectedElementViewModels.ToList())
{
element.IsLocked = value;
}
if (value)
{
RemoveLockedSelectedElements();
}
}
}
private bool _isAllLocked = false;
public bool IsAllLocked
{
get => _isAllLocked;
set
{
SetProperty(ref _isAllLocked, value);
foreach (var element in ElementViewModels)
{
element.IsLocked = value;
}
if (value)
{
RemoveLockedSelectedElements();
}
}
}
public void RemoveLockedSelectedElements()
{
for (int i = _selectedElementViewModels.Count - 1; i >= 0; i--)
{
if (_selectedElementViewModels[i].IsLocked)
{
_selectedElementViewModels.RemoveAt(i);
}
}
}
private bool _isSelectedHidden = false;
public bool IsSelectedHidden
{
get => _isSelectedHidden;
set
{
SetProperty(ref _isSelectedHidden, value);
foreach (var element in SelectedElementViewModels)
{
element.IsVisible = !value;
}
}
}
public ElementViewModel? SelectedElementViewModel => _selectedElementViewModels.Count==0?null: _selectedElementViewModels[0];
public int? SelectedIdx => SelectedElementViewModel?.Idx;
public bool HasSelectedElement => SelectedElementViewModel != null;
public Visibility HasSelectedVisibility => HasSelectedElement ? Visibility.Visible : Visibility.Collapsed;
public Visibility HasManySelectedVisibility => _selectedElementViewModels.Count > 1 ? Visibility.Visible : Visibility.Collapsed;
private Visibility _selectionRectVisibility = Visibility.Collapsed;
public Visibility SelectionRectVisibility
{
get => _selectionRectVisibility;
set => SetProperty(ref _selectionRectVisibility, value);
}
private double _selectionRectLeft = 0;
public double SelectionRectLeft
{
get => _selectionRectLeft;
set => SetProperty(ref _selectionRectLeft, value);
}
private double _selectionRectTop = 0;
public double SelectionRectTop
{
get => _selectionRectTop;
set => SetProperty(ref _selectionRectTop, value);
}
private double _selectionRectWidth = 0;
public double SelectionRectWidth
{
get => _selectionRectWidth;
set => SetProperty(ref _selectionRectWidth, value);
}
private double _selectionRectHeight = 0;
public double SelectionRectHeight
{
get => _selectionRectHeight;
set => SetProperty(ref _selectionRectHeight, value);
}
//public const double MinScale = 0.1;
//public const double MaxScale = 10.0;
private double _canvasScale = 1.0;
public double CanvasScale
{
get => _canvasScale;
set => SetProperty(ref _canvasScale, value);
}
public const double MIN_CANVAS_WIDTH = 800;
public const double MIN_CANVAS_Height = 600;
public const int MIN_TIGHT_CANVAS = 100;
private const double _defaultCanvasSizeScale = 1.5;
public double MinCanvasWidth => TightCanvasWidth;
public double MinCanvasHeight => TightCanvasHeight;
public double MaxCanvasWidth => TightCanvasWidth * 4;
public double MaxCanvasHeight => TightCanvasHeight * 4;
private double _canvasWidth;
public double CanvasWidth
{
get => _canvasWidth;
set
{
if (value < MinCanvasWidth) value = MinCanvasWidth;
if (value > MaxCanvasWidth) value = MaxCanvasWidth;
SetProperty(ref _canvasWidth, value);
}
}
private double _canvasHeight;
public double CanvasHeight
{
get => _canvasHeight;
set
{
if (value < MinCanvasHeight) value = MinCanvasHeight;
if (value > MaxCanvasHeight) value = MaxCanvasHeight;
SetProperty(ref _canvasHeight, value);
}
}
public double TightCanvasWidth
{
get
{
if (ElementViewModels.Count == 0) return MIN_CANVAS_WIDTH;
double maxX = ElementViewModels.Max(ev => ev.PosX + ev.MaxWidth*ev.Scale);
double maxMouseRadius = ElementViewModels.Max(ev => ev.MouseRadius * ev.Scale);
double maxGamepadRadius = ElementViewModels.Max(ev=>ev.Radius * ev.Scale);
double with = maxX + maxMouseRadius*2 + maxGamepadRadius*2;
return Math.Max(MIN_TIGHT_CANVAS, with);
}
}
public double TightCanvasHeight
{
get
{
if (_elementViewModels.Count == 0) return MIN_CANVAS_Height;
double maxY = ElementViewModels.Max(ev => ev.PosY + ev.MaxHeight * ev.Scale);
double maxMouseRadius = ElementViewModels.Max(ev => ev.MouseRadius * ev.Scale);
double maxGamepadRadius = ElementViewModels.Max(ev => ev.Radius * ev.Scale);
double height = maxY + maxMouseRadius * 2 + maxGamepadRadius * 2;
return Math.Max(MIN_TIGHT_CANVAS, height);
}
}
private XImage? _atlas;
public XImage? Atlas
{
get => _atlas;
set => SetProperty(ref _atlas, value);
}
public event Action<ElementViewModel, string>? ElementWARNingEvent;
public ElementTreeViewModel() { Initialize();}
public ElementTreeViewModel(string fipPath)
{
var bytes = File.ReadAllBytes(fipPath);
var dto = System.Text.Json.JsonSerializer.Deserialize<ElementTreeViewModelDto>(bytes);
if (dto == null)
throw new Exception($"项目文件:{fipPath}解析失败!");
var loadedVm = ElementTreeViewModel.FromDto(dto);
// 拷贝属性
this.Name = loadedVm.Name;
this.ImageBackGroundColor = loadedVm.ImageBackGroundColor;
this.ElementViewModels.Clear();
foreach (var evm in loadedVm.ElementViewModels)
this.ElementViewModels.Add(evm);
Initialize();
}
public ElementTreeViewModel(string pngPath, string jsonPath)
{
XImage inputImage = new XImage(pngPath);
OverlayRoot overlayRoot = OverlayParser.Parse(jsonPath);
for (int i = 0; i < overlayRoot.elements.Count; i++)
{
ElementViewModel elementViewModel = ElementViewModel.FromOverlayElement(overlayRoot.elements[i], inputImage);
_elementViewModels.Add(elementViewModel);
}
Initialize();
}
public ElementTreeViewModel(OverlayRoot overlayRoot, XImage image)
{
Atlas = image;
for (int i = 0; i < overlayRoot.elements.Count; i++)
{
ElementViewModel elementViewModel = ElementViewModel.FromOverlayElement(overlayRoot.elements[i], image);
_elementViewModels.Add(elementViewModel);
}
Initialize();
}
private void Initialize()
{
ReIndexing();
for (int i = 0; i < _elementViewModels.Count; i++)
{
_elementViewModels[i].ParentCollection = _elementViewModels;
}
foreach (ElementViewModel elementViewModel in _elementViewModels)
{
int? anchorIdx = elementViewModel.OffsetAnchorSavedIdx;
if (anchorIdx != null && anchorIdx>=0 && anchorIdx < _elementViewModels.Count)
{
ElementViewModel? anchor = _elementViewModels[(int)anchorIdx];
elementViewModel.OffsetAnchorViewModel = anchor;
}
}
_selectedElementViewModels.CollectionChanged -= SelectedElementViewModels_CollectionChanged;
_selectedElementViewModels.CollectionChanged += SelectedElementViewModels_CollectionChanged;
CanvasWidth = TightCanvasWidth * _defaultCanvasSizeScale;
CanvasHeight = TightCanvasHeight * _defaultCanvasSizeScale;
}
public void RemoveAt(int idx)
{
if (idx < 0 || idx >= _elementViewModels.Count) return;
_elementViewModels[idx].Dispose();
_elementViewModels.RemoveAt(idx);
ReIndexing();
}
public void UpMoveAt(int idx)
{
if (idx <= 0 || idx >= _elementViewModels.Count) return;
var temp = _elementViewModels[idx - 1];
_elementViewModels[idx - 1] = _elementViewModels[idx];
_elementViewModels[idx] = temp;
ReIndexing();
}
public void DownMoveAt(int idx)
{
if (idx < 0 || idx >= _elementViewModels.Count - 1) return;
var temp = _elementViewModels[idx + 1];
_elementViewModels[idx + 1] = _elementViewModels[idx];
_elementViewModels[idx] = temp;
ReIndexing();
}
public void TopMoveAt(int idx)
{
if (idx <= 0 || idx >= _elementViewModels.Count) return;
var temp = _elementViewModels[idx];
_elementViewModels.RemoveAt(idx);
_elementViewModels.Insert(0, temp);
ReIndexing();
}
public void BottomMoveAt(int idx)
{
if (idx < 0 || idx >= _elementViewModels.Count - 1) return;
var temp = _elementViewModels[idx];
_elementViewModels.RemoveAt(idx);
_elementViewModels.Add(temp);
ReIndexing();
}
public void AddElement(ElementViewModel elementViewModel)
{
elementViewModel.Idx = _elementViewModels.Count;
_elementViewModels.Add(elementViewModel);
elementViewModel.ParentCollection = _elementViewModels;
ReIndexing();
}
public void ReIndexing()
{
for (int i = 0; i < _elementViewModels.Count; i++)
{
_elementViewModels[i].Idx = i;
}
OnPropertyChanged(nameof(ElementViewModels));
OnPropertyChanged(nameof(CanvasWidth));
OnPropertyChanged(nameof(CanvasHeight));
OnPropertyChanged(nameof(TightCanvasWidth));
OnPropertyChanged(nameof(TightCanvasHeight));
if (InputHandler != null)
InputHandler.ResetRespondersAndDictionaries();
}
private void SelectedElementViewModels_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
OnPropertyChanged(nameof(SelectedElementViewModel));
OnPropertyChanged(nameof(HasSelectedVisibility));
OnPropertyChanged(nameof(HasManySelectedVisibility));
foreach (var element in ElementViewModels)
{
element.IsSelected = SelectedElementViewModels.Contains(element);
}
}
public bool HasCircleChainIfSelectedBind(ElementViewModel vm)
{
var selectedSnapshot = SelectedElementViewModels.ToList();
var originalAnchors = selectedSnapshot
.Select(s => s.OffsetAnchorViewModel)
.ToList();
try
{
// 临时套用目标绑定
foreach (var selected in selectedSnapshot)
{
selected.SetPrivateOffsetAnchor(vm);
}
// 真正的环检测:访问过同一节点即成环
foreach (var start in ElementViewModels)
{
var visited = new HashSet<ElementViewModel>();
var cur = start.OffsetAnchorViewModel;
while (cur != null)
{
if (!visited.Add(cur))
{
return true;
}
cur = cur.OffsetAnchorViewModel;
}
}
return false;
}
finally
{
// 保证恢复
for (int i = 0; i < selectedSnapshot.Count; i++)
{
selectedSnapshot[i].SetPrivateOffsetAnchor(originalAnchors[i]);
}
}
}
public void Export(ExportType exportType)
{
SaveFileDialog saveFileDialog = new SaveFileDialog();
saveFileDialog.Filter = (exportType == ExportType.PngAndJson) ? "JSON 文件 (*.json)|*.json" : "FancyInput 项目文件 (*.fip)|*.fip";
if (saveFileDialog.ShowDialog() == DialogResult.OK)
{
string fileName = saveFileDialog.FileName;
if (exportType == ExportType.ProjectFile)
{
var dto = ToDto(this);
var bytes = System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(dto);
File.WriteAllBytes(fileName, bytes);
FancyInput.AppMessageBox.Show($"项目文件已成功保存到:{fileName}", "导出成功", MessageBoxButton.OK, MessageBoxImage.Information);
}
else
{
if (Atlas == null)
GenerateAtlas();
if (Atlas == null)
{
FancyInput.AppMessageBox.Show("图集生成失败,无法导出JSON文件!");
return;
}
OverlayRoot overlayRoot = new OverlayRoot();
overlayRoot.elements = new List<OverlayElement>();
foreach (var evm in _elementViewModels)
{
overlayRoot.elements.Add(evm.ToOverlayElement());
}
overlayRoot.overlay_height = (int)TightCanvasHeight;
overlayRoot.overlay_width = (int)TightCanvasWidth;
var options = new System.Text.Json.JsonSerializerOptions
{
DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull,
WriteIndented = true
};
string json = System.Text.Json.JsonSerializer.Serialize(overlayRoot, options);
System.IO.File.WriteAllText(saveFileDialog.FileName, json);
string pngName = System.IO.Path.ChangeExtension(saveFileDialog.FileName, ".png");
Atlas.Save(pngName);
FancyInput.AppMessageBox.Show($"PNG纹理图集和JSON文件已成功保存到:\n{pngName}\n{saveFileDialog.FileName}", "导出成功", MessageBoxButton.OK, MessageBoxImage.Information);
}
}
}
public void GenerateAtlas()
{
List<XImage> concatedImages = new List<XImage>();
for (int i = 0; i < _elementViewModels.Count; i++)
{
_elementViewModels[i].Idx = i;
ElementType elementType = _elementViewModels[i].ElementType;
XImage? concatedImage = null;
List<XImage> elementImages = new List<XImage>();
XImage? xImage0 = _elementViewModels[i][0];
XImage? xImagelocal = null;
if (xImage0 != null)
{
elementImages.Add(xImage0);
}
else
{
FancyInput.AppMessageBox.Show($"元素{i},Id:{_elementViewModels[i].Id}缺少图片,无法生成图集!");
return;
}
switch (elementType)
{
case ElementType.Texture:
concatedImage = xImage0.Copy();
break;
case ElementType.KeyboardButton:
case ElementType.GamepadButton:
case ElementType.MouseButton:
case ElementType.GamepadTrigger:
case ElementType.AnalogStick:
xImagelocal = _elementViewModels[i][1];
if (xImagelocal != null)
elementImages.Add(xImagelocal);
else
{
elementImages.Add(xImage0.Copy());
ElementWARNingEvent?.Invoke(_elementViewModels[i], "缺少按下状态图片,已使用默认图片代替!");
}
concatedImage = XImage.ConcatImages(elementImages, ElementViewModel.SPACE_BETWEEN_IMAGES, 0);
break;
case ElementType.MouseWheel:
for (int j = 1; j < 4; j++)
{
xImagelocal = _elementViewModels[i][j];
if (xImagelocal != null)
elementImages.Add(xImagelocal);
else
{
elementImages.Add(xImage0.Copy());
ElementWARNingEvent?.Invoke(_elementViewModels[i], $"缺少状态{j}图片,已使用默认图片代替!");
}
}
concatedImage = XImage.ConcatImages(elementImages, ElementViewModel.SPACE_BETWEEN_IMAGES, 1);
break;
case ElementType.DPad:
for (int j = 1; j < 9; j++)
{
xImagelocal = _elementViewModels[i][j];
if (xImagelocal != null)
elementImages.Add(xImagelocal);
else
{
elementImages.Add(xImage0.Copy());
ElementWARNingEvent?.Invoke(_elementViewModels[i], $"缺少状态{j}图片,已使用默认图片代替!");
}
}
concatedImage = XImage.ConcatImages(elementImages, ElementViewModel.SPACE_BETWEEN_IMAGES, 1);
break;
case ElementType.GamepadPlayerId:
for (int j = 1; j < 5; j++)
{
xImagelocal = _elementViewModels[i][j];
if (xImagelocal != null)
elementImages.Add(xImagelocal);
else
{
elementImages.Add(xImage0.Copy());
ElementWARNingEvent?.Invoke(_elementViewModels[i], $"缺少状态{j}图片,已使用默认图片代替!");
}
}
concatedImage = XImage.ConcatImages(elementImages, ElementViewModel.SPACE_BETWEEN_IMAGES, 1);
break;
case ElementType.MouseMovement:
concatedImage = xImage0.Copy();
ElementWARNingEvent?.Invoke(_elementViewModels[i], "该元素类型不支持生成图集!");
break;
default:
throw new ArgumentOutOfRangeException($"不支持的元素类型:{elementType}");
}
if (concatedImage == null)
{
FancyInput.AppMessageBox.Show($"元素{i},Id:{_elementViewModels[i].Id}图集生成失败!");
return;
}
concatedImages.Add(concatedImage);
}
var result = XImage.PackImages(concatedImages);
Atlas = result.atlas;
var placements = result.placements;
for (int i = 0; i < placements.Count; i++)
{
var placedImage = placements[i];
if (placedImage == null)
{
ElementWARNingEvent?.Invoke(_elementViewModels[i], "图集打包失败,元素位置未更新!");
continue;
}
XImage image = placedImage.Image;
int Index = placedImage.Index;
int X = placedImage.X;
int Y = placedImage.Y;
if (Index >= 0 && Index < _elementViewModels.Count)
{
_elementViewModels[Index].MappingU = X;
_elementViewModels[Index].MappingV = Y;
}
}
}
public static ElementTreeViewModelDto ToDto(ElementTreeViewModel vm)
{
var ElementViewModelDTOs = vm.ElementViewModels.Select(ElementViewModel.ToDto).ToList();
return new ElementTreeViewModelDto
{
Name = vm.Name,
LoadMode = vm.LoadMode,
PngFilePath = vm.PngFilePath,
JsonFilePath = vm.JsonFilePath,
ProjectFilePath = vm.ProjectFilePath,
ImageBackGroundColor = new byte[] {
vm.ImageBackGroundColor.A,
vm.ImageBackGroundColor.R,
vm.ImageBackGroundColor.G,
vm.ImageBackGroundColor.B
},
Elements = ElementViewModelDTOs
};
}
public static ElementTreeViewModel FromDto(ElementTreeViewModelDto dto)
{
var vm = new ElementTreeViewModel
{
Name = dto.Name,
LoadMode = dto.LoadMode,
PngFilePath = dto.PngFilePath,
JsonFilePath = dto.JsonFilePath,
ProjectFilePath = dto.ProjectFilePath,
ImageBackGroundColor = dto.ImageBackGroundColor.Length == 4
? Color.FromArgb(dto.ImageBackGroundColor[0], dto.ImageBackGroundColor[1], dto.ImageBackGroundColor[2], dto.ImageBackGroundColor[3])
: Colors.Black
};
foreach (var elementDto in dto.Elements)
{
vm.ElementViewModels.Add(ElementViewModel.FromDto(elementDto));
}
vm.ReIndexing();
return vm;
}
}
public class ElementTreeViewModelDto
{
public string Name { get; set; } = string.Empty;
public LoadMode LoadMode { get; set; } = LoadMode.PngJson;
public string PngFilePath { get; set; } = string.Empty;
public string JsonFilePath { get; set; } = string.Empty;
public string ProjectFilePath { get; set; } = string.Empty;
public byte[] ImageBackGroundColor { get; set; } = Array.Empty<byte>(); // ARGB
public List<ElementViewModelDto> Elements { get; set; } = new();
}
}
@@ -0,0 +1,439 @@
using FancyInput.Models;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Text.Json.Serialization;
using System.Windows.Media.Imaging;
using System.Windows.Media;
namespace FancyInput.ViewModels
{
public partial class ElementViewModel
{
public void MakeCache(int frameRange)
{
if (ElementType != ElementType.GamepadTrigger) return;
XImage? startImage = this[0];
XImage? endImage = this[1];
if (startImage == null || endImage == null) return;
if (_triggerCacheImages != null)
{
_triggerCacheImages.Clear();
}
int framesInBatch = frameRange + 1;
_triggerCacheImages = new List<XImage>();
for (int directionIdx = 1; directionIdx <= 4; directionIdx++)
{
Direction direction = (Direction)directionIdx;
for (int frame = 0; frame < framesInBatch; frame++)
{
double percent = (double)frame / frameRange;
XImage Merge = XImage.Merge(startImage, endImage, direction, percent);
_triggerCacheImages.Add(Merge);
}
}
}
public ElementViewModel Copy()
{
ElementViewModel copy = new ElementViewModel()
{
ElementType = this.ElementType,
Id = this.Id,
Idx = this.Idx,
PosX = this.PosX,
PosY = this.PosY,
IsVisible = this.IsVisible,
OffsetAnchorViewModel = this.OffsetAnchorViewModel,
OffsetAnchorSavedIdx = this.OffsetAnchorSavedIdx,
MappingU = this.MappingU,
MappingV = this.MappingV,
MappingW = this.MappingW,
MappingH = this.MappingH,
LogScale = this.LogScale,
Radius = this.Radius,
MouseRadius = this.MouseRadius,
IsTriggerMode = this.IsTriggerMode,
ImageBackGroundColor = this.ImageBackGroundColor,
UseStopwatch = this.UseStopwatch,
StopwatchMilliseconds = this.StopwatchMilliseconds,
SelectedDirection = this.SelectedDirection,
SelectedSide = this.SelectedSide,
SelectedGamepadButton = this.SelectedGamepadButton,
SelectedKeyBoardButton = this.SelectedKeyBoardButton,
SelectedMouseButton = this.SelectedMouseButton,
SelectedMouseMoveType = this.SelectedMouseMoveType,
};
for (int i = 0; i < MAX_IAMGE_NUM; i++)
{
copy[i] = this[i]?.Copy();
copy.SegmentsScales[i] = this.SegmentsScales[i];
copy.SegmentsOffsetX[i] = this.SegmentsOffsetX[i];
copy.SegmentsOffsetY[i] = this.SegmentsOffsetY[i];
}
return copy;
}
public void CopyFrom(ElementViewModel viewModel, bool changeIdx = false)
{
this.ElementType = viewModel.ElementType;
this.Id = viewModel.Id;
this.PosX = viewModel.PosX;
this.PosY = viewModel.PosY;
this.IsVisible = viewModel.IsVisible;
this.OffsetAnchorViewModel = viewModel.OffsetAnchorViewModel;
this.OffsetAnchorSavedIdx = viewModel.OffsetAnchorSavedIdx;
this.MappingU = viewModel.MappingU;
this.MappingV = viewModel.MappingV;
this.MappingW = viewModel.MappingW;
this.MappingH = viewModel.MappingH;
this.LogScale = viewModel.LogScale;
this.Radius = viewModel.Radius;
this.MouseRadius = viewModel.MouseRadius;
this.IsTriggerMode = viewModel.IsTriggerMode;
this.ImageBackGroundColor = viewModel.ImageBackGroundColor;
this.UseStopwatch = viewModel.UseStopwatch;
this.StopwatchMilliseconds = viewModel.StopwatchMilliseconds;
this.SelectedDirection = viewModel.SelectedDirection;
this.SelectedSide = viewModel.SelectedSide;
this.SelectedKeyBoardButton = viewModel.SelectedKeyBoardButton;
this.SelectedGamepadButton = viewModel.SelectedGamepadButton;
this.SelectedMouseButton = viewModel.SelectedMouseButton;
this.SelectedMouseMoveType = viewModel.SelectedMouseMoveType;
for (int i = 0; i < MAX_IAMGE_NUM; i++)
{
this[i] = viewModel[i]?.Copy();
this.SegmentsScales[i] = viewModel.SegmentsScales[i];
this.SegmentsOffsetX[i] = viewModel.SegmentsOffsetX[i];
this.SegmentsOffsetY[i] = viewModel.SegmentsOffsetY[i];
}
if (changeIdx)
{
this.Idx = viewModel.Idx;
}
}
public static ElementViewModel FromOverlayElement(OverlayElement overlayElement, XImage texture)
{
ElementViewModel elementViewModel = new ElementViewModel()
{
ElementType = overlayElement.type,
Id = overlayElement.id,
PosX = overlayElement.pos.Length > 0 ? overlayElement.pos[0] : 0,
PosY = overlayElement.pos.Length > 1 ? overlayElement.pos[1] : 0,
MappingU = overlayElement.mapping.Length > 0 ? overlayElement.mapping[0] : 0,
MappingV = overlayElement.mapping.Length > 1 ? overlayElement.mapping[1] : 0,
MappingW = overlayElement.mapping.Length > 2 ? overlayElement.mapping[2] : texture.Width,
MappingH = overlayElement.mapping.Length > 3 ? overlayElement.mapping[3] : texture.Height,
};
//Additional properties
switch (overlayElement.type)
{
case ElementType.GamepadButton:
if (overlayElement.code.HasValue)
{
elementViewModel.SelectedGamepadButton = (GamepadCodeType)overlayElement.code.Value;
}
break;
case ElementType.KeyboardButton:
if (overlayElement.code.HasValue)
{
elementViewModel.SelectedKeyBoardButton = (KeyBoardCodeType)overlayElement.code.Value;
}
break;
case ElementType.MouseButton:
if (overlayElement.code.HasValue)
{
elementViewModel.SelectedMouseButton = (MouseCodeType)overlayElement.code.Value;
}
break;
case ElementType.GamepadTrigger:
if (overlayElement.trigger_mode.HasValue)
{
elementViewModel.IsTriggerMode = overlayElement.trigger_mode.Value;
}
if (overlayElement.side.HasValue)
{
elementViewModel.SelectedSide = overlayElement.side.Value;
}
if (overlayElement.direction.HasValue)
{
elementViewModel.SelectedDirection = overlayElement.direction.Value;
}
break;
case ElementType.AnalogStick:
if (overlayElement.side.HasValue)
{
elementViewModel.SelectedSide = overlayElement.side.Value;
}
if (overlayElement.stick_radius.HasValue)
{
elementViewModel.Radius = overlayElement.stick_radius.Value;
}
break;
case ElementType.MouseMovement:
if (overlayElement.mouse_type.HasValue)
{
elementViewModel.SelectedMouseMoveType = overlayElement.mouse_type.Value;
}
if (overlayElement.mouse_radius.HasValue)
{
elementViewModel.MouseRadius = overlayElement.mouse_radius.Value;
}
break;
case ElementType.MouseWheel:
case ElementType.GamepadPlayerId:
case ElementType.Texture:
case ElementType.DPad:
// No additional properties
break;
}
//Croped images from texure
switch (overlayElement.type)
{
case ElementType.MouseButton:
case ElementType.KeyboardButton:
case ElementType.GamepadButton:
case ElementType.GamepadTrigger:
case ElementType.AnalogStick:
elementViewModel[0] = texture.Crop(elementViewModel.MappingU, elementViewModel.MappingV, elementViewModel.MappingW, elementViewModel.MappingH);
elementViewModel[1] = texture.Crop(elementViewModel.MappingU, elementViewModel.MappingV + elementViewModel.MappingH + SPACE_BETWEEN_IMAGES,
elementViewModel.MappingW, elementViewModel.MappingH);
break;
case ElementType.MouseWheel:
for (int i = 0; i < 4; i++)
{
elementViewModel[i] = texture.Crop(elementViewModel.MappingU + i * (elementViewModel.MappingW + SPACE_BETWEEN_IMAGES),
elementViewModel.MappingV, elementViewModel.MappingW, elementViewModel.MappingH);
}
break;
case ElementType.DPad:
for (int i = 0; i < MAX_IAMGE_NUM; i++)
{
elementViewModel[i] = texture.Crop(elementViewModel.MappingU + i * (elementViewModel.MappingW + SPACE_BETWEEN_IMAGES),
elementViewModel.MappingV, elementViewModel.MappingW, elementViewModel.MappingH);
}
break;
case ElementType.Texture:
elementViewModel[0] = texture.Crop(elementViewModel.MappingU, elementViewModel.MappingV, elementViewModel.MappingW, elementViewModel.MappingH);
break;
case ElementType.GamepadPlayerId:
for (int i = 0; i < 5; i++)
{
elementViewModel[i] = texture.Crop(elementViewModel.MappingU + i * (elementViewModel.MappingW + SPACE_BETWEEN_IMAGES),
elementViewModel.MappingV, elementViewModel.MappingW, elementViewModel.MappingH);
}
break;
case ElementType.MouseMovement:
elementViewModel[0] = texture.Crop(elementViewModel.MappingU, elementViewModel.MappingV, elementViewModel.MappingW, elementViewModel.MappingH);
break;
}
return elementViewModel;
}
public OverlayElement ToOverlayElement()
{
OverlayElement overlayElement = new OverlayElement()
{
type = this.ElementType,
id = this.Id,
pos = new int[] { this.PosX, this.PosY },
mapping = new int[] { this.MappingU, this.MappingV, this.MappingW, this.MappingH },
};
//Additional properties
switch (this.ElementType)
{
case ElementType.GamepadButton:
overlayElement.code = (int)this.SelectedGamepadButton;
break;
case ElementType.KeyboardButton:
overlayElement.code = (int)this.SelectedKeyBoardButton;
break;
case ElementType.MouseButton:
overlayElement.code = (int)this.SelectedMouseButton;
break;
case ElementType.GamepadTrigger:
overlayElement.trigger_mode = this.IsTriggerMode;
overlayElement.side = this.SelectedSide;
overlayElement.direction = this.SelectedDirection;
break;
case ElementType.AnalogStick:
overlayElement.side = this.SelectedSide;
overlayElement.stick_radius = this.Radius;
break;
case ElementType.MouseMovement:
overlayElement.mouse_type = this.SelectedMouseMoveType;
overlayElement.mouse_radius = this.MouseRadius;
break;
case ElementType.MouseWheel:
case ElementType.GamepadPlayerId:
case ElementType.Texture:
// No additional properties
break;
}
return overlayElement;
}
public static ElementViewModelDto ToDto(ElementViewModel vm)
{
var dto = new ElementViewModelDto
{
ElementType = vm.ElementType,
Idx = vm.Idx,
Id = vm.Id,
PosX = vm.PosX,
PosY = vm.PosY,
IsVisible = vm.IsVisible,
OffsetAnchorSavedIdx = vm.OffsetAnchorIdx,
MappingU = vm.MappingU,
MappingV = vm.MappingV,
MappingW = vm.MappingW,
MappingH = vm.MappingH,
LogScale = vm.LogScale,
Radius = vm.Radius,
MouseRadius = vm.MouseRadius,
IsTriggerMode = vm.IsTriggerMode,
ImageBackGroundColor = new byte[] { vm.ImageBackGroundColor.A, vm.ImageBackGroundColor.R,
vm.ImageBackGroundColor.G, vm.ImageBackGroundColor.B },
UseStopwatch = vm.UseStopwatch,
StopwatchMilliseconds = vm.StopwatchMilliseconds,
SelectedGamepadButton = vm.SelectedGamepadButton,
SelectedKeyBoardButton = vm.SelectedKeyBoardButton,
SelectedMouseButton = vm.SelectedMouseButton,
SelectedSide = vm.SelectedSide,
SelectedDirection = vm.SelectedDirection,
SelectedMouseMoveType = vm.SelectedMouseMoveType,
Images = new List<byte[]?>(),
ImagesGifRaw = new List<byte[]?>()
};
for (int i = 0; i < ElementViewModel.MAX_IAMGE_NUM; i++)
{
var img = vm[i];
if (img == null)
{
dto.Images.Add(null);
dto.ImagesGifRaw.Add(null);
continue;
}
if (img.ImageType == XImageType.GIF)
{
// 保存GIF原始字节流
if (img.BitmapSource is BitmapImage bi && img.GifRawBytes != null)
{
dto.ImagesGifRaw.Add(img.GifRawBytes);
}
else
{
dto.ImagesGifRaw.Add(null);
}
dto.Images.Add(null);
}
else
{
using var ms = new MemoryStream();
var encoder = new PngBitmapEncoder();
encoder.Frames.Add(BitmapFrame.Create(img.BitmapSource));
encoder.Save(ms);
dto.Images.Add(ms.ToArray());
dto.ImagesGifRaw.Add(null);
}
}
return dto;
}
public static ElementViewModel FromDto(ElementViewModelDto dto)
{
var vm = new ElementViewModel
{
ElementType = dto.ElementType,
Idx = dto.Idx,
Id = dto.Id,
PosX = dto.PosX,
PosY = dto.PosY,
IsVisible = dto.IsVisible,
OffsetAnchorSavedIdx = dto.OffsetAnchorSavedIdx,
MappingU = dto.MappingU,
MappingV = dto.MappingV,
MappingW = dto.MappingW,
MappingH = dto.MappingH,
LogScale = dto.LogScale,
Radius = dto.Radius,
MouseRadius = dto.MouseRadius,
IsTriggerMode = dto.IsTriggerMode,
ImageBackGroundColor = dto.ImageBackGroundColor.Length == 4
? Color.FromArgb(dto.ImageBackGroundColor[0], dto.ImageBackGroundColor[1], dto.ImageBackGroundColor[2], dto.ImageBackGroundColor[3])
: Colors.Black,
UseStopwatch = dto.UseStopwatch,
StopwatchMilliseconds = dto.StopwatchMilliseconds,
SelectedGamepadButton = dto.SelectedGamepadButton,
SelectedKeyBoardButton = dto.SelectedKeyBoardButton,
SelectedMouseButton = dto.SelectedMouseButton,
SelectedSide = dto.SelectedSide,
SelectedDirection = dto.SelectedDirection,
SelectedMouseMoveType = dto.SelectedMouseMoveType
};
for (int i = 0; i < ElementViewModel.MAX_IAMGE_NUM; i++)
{
if (dto.ImagesGifRaw.Count > i && dto.ImagesGifRaw[i] != null && dto.ImagesGifRaw[i]!.Length > 0)
{
var ms = new MemoryStream(dto.ImagesGifRaw[i]!);
var bitmap = new BitmapImage();
bitmap.BeginInit();
bitmap.CacheOption = BitmapCacheOption.OnLoad;
bitmap.StreamSource = ms;
bitmap.EndInit();
vm[i] = new XImage(bitmap, XImageType.GIF, dto.ImagesGifRaw[i]);
}
else if (dto.Images.Count > i && dto.Images[i] != null && dto.Images[i]!.Length > 0)
{
using var ms = new MemoryStream(dto.Images[i]!);
var decoder = new PngBitmapDecoder(ms, BitmapCreateOptions.PreservePixelFormat, BitmapCacheOption.OnLoad);
vm[i] = new XImage(decoder.Frames[0], XImageType.PNG);
}
else
{
vm[i] = null;
}
}
return vm;
}
}
public class ElementViewModelDto
{
public ElementType ElementType { get; set; }
public int Idx { get; set; }
public string Id { get; set; } = string.Empty;
public int PosX { get; set; }
public int PosY { get; set; }
public bool IsVisible { get; set; } = true;
public int? OffsetAnchorSavedIdx { get; set; } = null;
public int MappingU { get; set; }
public int MappingV { get; set; }
public int MappingW { get; set; }
public int MappingH { get; set; }
public double LogScale { get; set; } = 0.0;
public int Radius { get; set; }
public int MouseRadius { get; set; }
public bool IsTriggerMode { get; set; }
public byte[] ImageBackGroundColor { get; set; } = Array.Empty<byte>(); // ARGB
public bool UseStopwatch { get; set; } = false;
public int StopwatchMilliseconds { get; set; } = 50;
public GamepadCodeType SelectedGamepadButton { get; set; }
public KeyBoardCodeType SelectedKeyBoardButton { get; set; }
public MouseCodeType SelectedMouseButton { get; set; }
public Side SelectedSide { get; set; }
public Direction SelectedDirection { get; set; }
public MouseMoveType SelectedMouseMoveType { get; set; }
public List<byte[]?> Images { get; set; } = new(); // PNG字节流,最多9张
public List<byte[]?> ImagesGifRaw { get; set; } = new(); // GIF原始字节流,最多9张
}
}
@@ -0,0 +1,721 @@
using FancyInput.Common;
using FancyInput.Models;
using FancyInput.Views.Controls;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
//using System.Windows.Forms;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using Color = System.Windows.Media.Color;
using Cursor = System.Windows.Input.Cursor;
using Cursors = System.Windows.Input.Cursors;
namespace FancyInput.ViewModels
{
public partial class ElementViewModel : INotifyPropertyChanged,IDisposable
{
public event PropertyChangedEventHandler? PropertyChanged;
protected void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
protected virtual bool SetProperty<T>(ref T field, T value, [CallerMemberName] string propertyName = "")
{
if (Equals(field, value)) return false;
field = value;
OnPropertyChanged(propertyName);
return true;
}
public AnimatedImage? AnimatedImageControl { get; set; }
private Color _imageBackGroundColor = Colors.Black;
public Color ImageBackGroundColor
{
get => _imageBackGroundColor;
set
{
SetProperty(ref _imageBackGroundColor, value);
OnPropertyChanged(nameof(ImageBackground));
}
}
public SolidColorBrush ImageBackground => new SolidColorBrush(_imageBackGroundColor);
public ElementType ElementType { get; set; }
private GamepadUserIdx _gamepadUserIndex = GamepadUserIdx.One;
public GamepadUserIdx GamepadUserIndex
{
get => _gamepadUserIndex;
set
{
_gamepadUserIndex = value;
if (ElementType == ElementType.GamepadPlayerId)
CurrentSourceIdx = (int)value;
}
}
private bool _isSelected = false;
public bool IsSelected
{
get => _isSelected;
set
{
SetProperty(ref _isSelected, value);
OnPropertyChanged(nameof(SelectColor1));
OnPropertyChanged(nameof(SelectColor2));
}
}
public SolidColorBrush SelectColor1 => new SolidColorBrush(_isSelected ? Color.FromArgb(0xFF, 0x65, 0x1F, 0x65) : Colors.Gray);
public SolidColorBrush SelectColor2 => new SolidColorBrush(_isSelected ? Color.FromArgb(0xAA, 0x65, 0x1F, 0x65) : Color.FromArgb(0x33, 0x3D, 0x3C, 0x3E));
public const int SPACE_BETWEEN_IMAGES = 3;
public const int MAX_IAMGE_NUM = 9;
private string _id = string.Empty;
public string Id
{
get => _id;
set => SetProperty(ref _id, value);
}
private bool _isVisible = true;
public bool IsVisible
{
get => _isVisible;
set
{
SetProperty(ref _isVisible, value);
OnPropertyChanged(nameof(ElementVisibility));
OnPropertyChanged(nameof(VisibilityIcon));
OnPropertyChanged(nameof(VisibilityToolTip));
OnPropertyChanged(nameof(VisibilityButtonBrush));
}
}
public Visibility ElementVisibility => IsVisible ? Visibility.Visible : Visibility.Collapsed;
public Geometry VisibilityIcon => _isVisible ? PathDataGeometry.EyeOn : PathDataGeometry.EyeOff;
public string VisibilityToolTip => _isVisible ? "隐藏" : "显示";
public Brush VisibilityButtonBrush => _isVisible ?new SolidColorBrush(Colors.White): new SolidColorBrush( Color.FromArgb(0xFF, 0xF4, 0x8C, 0x8C));
private bool _isLocked = false;
public bool IsLocked
{
get => _isLocked;
set
{
SetProperty(ref _isLocked, value);
OnPropertyChanged(nameof(LockIcon));
OnPropertyChanged(nameof(LockToolTip));
OnPropertyChanged(nameof(LockButtonBrush));
OnPropertyChanged(nameof(IsHitTestVisible));
}
}
public bool IsHitTestVisible => !IsLocked;
public Geometry LockIcon => _isLocked? PathDataGeometry.LockOn : PathDataGeometry.LockOff;
public string LockToolTip => _isLocked ? "解锁" : "锁定";
public Brush LockButtonBrush => _isLocked ? new SolidColorBrush( Color.FromArgb(0xFF, 0xF4, 0x8C, 0x8C)) : new SolidColorBrush(Colors.White);
public double Scale => Math.Pow(10, _logScale);
public string ScaleString
{
get => Math.Pow(10, _logScale).ToString("0.##");
set
{
if (double.TryParse(value, out double scale) && scale > 0)
{
double log = Math.Log10(scale);
if (log < -1) log = -1;
else if (log > 1) log = 1;
_logScale = log;
OnPropertyChanged(nameof(LogScale));
OnPropertyChanged(nameof(Scale));
OnPropertyChanged(nameof(ScaleString));
}
}
}
private double _logScale = 0.0;
public double LogScale
{
get => _logScale;
set
{
SetProperty(ref _logScale, value);
OnPropertyChanged(nameof(Scale));
OnPropertyChanged(nameof(ScaleString));
}
}
private int _posX = 0;
public int PosX
{
get => _posX;
set
{
SetProperty(ref _posX, value);
OnPropertyChanged(nameof(VisualPosX));
OnPropertyChanged(nameof(OverlayJSON));
}
}
private int _posY = 0;
public int PosY
{
get => _posY;
set
{
SetProperty(ref _posY, value);
OnPropertyChanged(nameof(VisualPosY));
OnPropertyChanged(nameof(OverlayJSON));
}
}
public event Action<int>? OffsetXChanged;
public event Action<int>? OffsetYChanged;
private void OnOffsetXChanged(int offsetX) => OffsetXChanged?.Invoke(offsetX);
private void OnOffsetYChanged(int offsetY) => OffsetYChanged?.Invoke(offsetY);
private void HandleOffsetXChanged(int offsetX) => OffsetX = offsetX;
private void HandleOffsetYChanged(int offsetY) => OffsetY = offsetY;
private int offsetX = 0;
public int OffsetX
{
get => offsetX;
set
{
if(SetProperty(ref offsetX, value))
{
OnPropertyChanged(nameof(OffsetX));
OnPropertyChanged(nameof(VisualPosX));
OnPropertyChanged(nameof(OverlayJSON));
OnOffsetXChanged(offsetX);
}
}
}
private int offsetY = 0;
public int OffsetY
{
get => offsetY;
set
{
if (SetProperty(ref offsetY, value))
{
OnPropertyChanged(nameof(OffsetY));
OnPropertyChanged(nameof(VisualPosY));
OnPropertyChanged(nameof(OverlayJSON));
OnOffsetYChanged(offsetY);
}
}
}
public int VisualPosX => _posX + offsetX;
public int VisualPosY => _posY + offsetY;
public event Action? IdxChanged;
private void OnIdxChanged() => IdxChanged?.Invoke();
private void HandleOffsetAnchorIdxChanged() => OnPropertyChanged(nameof(OffsetAnchorIdx));
private int idx = 0;
public int Idx
{
get => idx;
set
{
if (SetProperty(ref idx, value))
{
OnIdxChanged();
OnPropertyChanged(nameof(IdxString));
}
}
}
public string IdxString => idx.ToString();
public int? OffsetAnchorIdx=> _offsetAnchorViewModel?.Idx;
public int? OffsetAnchorSavedIdx { get; set; } = null;
private ObservableCollection<ElementViewModel>? _parentCollection;
public ObservableCollection<ElementViewModel>? ParentCollection
{
get => _parentCollection;
set => SetProperty(ref _parentCollection, value);
}
private ElementViewModel? _offsetAnchorViewModel;
public void SetPrivateOffsetAnchor(ElementViewModel? anchor)=> _offsetAnchorViewModel = anchor;
public ElementViewModel? OffsetAnchorViewModel
{
get => _offsetAnchorViewModel;
set
{
if (ReferenceEquals(_offsetAnchorViewModel, value)) return;
if (ReferenceEquals(this, value)) return;
if (value !=null && GetOffsetAnchorChainDepth(value) == -1) return;
if (_offsetAnchorViewModel == null && value != null)
{
value.OffsetXChanged -= HandleOffsetXChanged;
value.OffsetYChanged -= HandleOffsetYChanged;
value.IdxChanged -= HandleOffsetAnchorIdxChanged;
value.OffsetXChanged += HandleOffsetXChanged;
value.OffsetYChanged += HandleOffsetYChanged;
value.IdxChanged += HandleOffsetAnchorIdxChanged;
if (SetProperty(ref _offsetAnchorViewModel, value))
{
OffsetX = value.OffsetX;
OffsetY = value.OffsetY;
HandleOffsetAnchorIdxChanged();
}
}
else if (_offsetAnchorViewModel != null && value == null)
{
_offsetAnchorViewModel.OffsetXChanged -= HandleOffsetXChanged;
_offsetAnchorViewModel.OffsetYChanged -= HandleOffsetYChanged;
_offsetAnchorViewModel.IdxChanged -= HandleOffsetAnchorIdxChanged;
if(SetProperty(ref _offsetAnchorViewModel, value))
{
OffsetX = 0;
OffsetY = 0;
HandleOffsetAnchorIdxChanged();
}
}
else if (_offsetAnchorViewModel != null && value != null)
{
_offsetAnchorViewModel.OffsetXChanged -= HandleOffsetXChanged;
_offsetAnchorViewModel.OffsetYChanged -= HandleOffsetYChanged;
_offsetAnchorViewModel.IdxChanged -= HandleOffsetAnchorIdxChanged;
value.OffsetXChanged -= HandleOffsetXChanged;
value.OffsetYChanged -= HandleOffsetYChanged;
value.IdxChanged -= HandleOffsetAnchorIdxChanged;
value.OffsetXChanged += HandleOffsetXChanged;
value.OffsetYChanged += HandleOffsetYChanged;
value.IdxChanged += HandleOffsetAnchorIdxChanged;
if(SetProperty(ref _offsetAnchorViewModel, value))
{
OffsetX = value.OffsetX;
OffsetY = value.OffsetY;
HandleOffsetAnchorIdxChanged();
}
}
}
}
public const int MAX_OFFSET_ANCHOR_CHAIN_DEPTH = 100;
public int GetOffsetAnchorChainDepth(ElementViewModel vmAnchor)
{
int depth = 1;
ElementViewModel? vm = vmAnchor;
HashSet<ElementViewModel> visited = new HashSet<ElementViewModel>();
while (vm != null)
{
// If the candidate chain reaches this node, binding would form a cycle.
if (ReferenceEquals(vm, this))
{
return -1;
}
// Existing cycles in the chain should also be treated as invalid.
if (!visited.Add(vm))
{
return -1;
}
vm = vm.OffsetAnchorViewModel;
if (vm != null)
{
depth++;
if (depth > MAX_OFFSET_ANCHOR_CHAIN_DEPTH)
{
return -1;
}
}
}
return depth;
}
private double _rotateAngle = 0;
public double RotateAngle
{
get => _rotateAngle;
set => SetProperty(ref _rotateAngle, value);
}
private int _mappingU = 0;
public int MappingU
{
get => _mappingU;
set
{
SetProperty(ref _mappingU, value);
OnPropertyChanged(nameof(OverlayJSON));
}
}
private int _mappingV = 0;
public int MappingV
{
get => _mappingV;
set
{
SetProperty(ref _mappingV, value);
OnPropertyChanged(nameof(OverlayJSON));
}
}
private int _mappingW = 0;
public int MappingW
{
get => _mappingW;
set
{
SetProperty(ref _mappingW, value);
OnPropertyChanged(nameof(MaxRadius));
OnPropertyChanged(nameof(OverlayJSON));
Radius = Math.Min(Radius, MaxRadius);
}
}
private int _mappingH = 0;
public int MappingH
{
get => _mappingH;
set
{
SetProperty(ref _mappingH, value);
OnPropertyChanged(nameof(MaxRadius));
OnPropertyChanged(nameof(OverlayJSON));
Radius = Math.Min(Radius, MaxRadius);
}
}
public int MaxHeight => _images.Take(ImagesCount[ElementType]).Where(img => img != null).Max(img => img?.Height ?? 0);
public int MaxWidth => _images.Take(ImagesCount[ElementType]).Where(img => img != null).Max(img => img?.Width ?? 0);
private int _radius = 0;
public int Radius
{
get => _radius;
set
{
SetProperty(ref _radius, value);
OnPropertyChanged(nameof(RadiusString));
OnPropertyChanged(nameof(OverlayJSON));
}
}
public string RadiusString => _radius.ToString();
public int MaxRadius => Math.Max(MappingW, MappingH) * 2;
private int _mouseRadius = 0;
public int MouseRadius
{
get => _mouseRadius;
set
{
SetProperty(ref _mouseRadius, value);
OnPropertyChanged(nameof(OverlayJSON));
OnPropertyChanged(nameof(MouseRadiusString));
}
}
public string MouseRadiusString => _mouseRadius.ToString();
public static readonly Dictionary<ElementType, int> ImagesCount = new()
{
{ ElementType.Texture, 1 },
{ ElementType.MouseMovement, 1 },
{ ElementType.KeyboardButton, 2 },
{ ElementType.GamepadButton, 2 },
{ ElementType.MouseButton, 2 },
{ ElementType.GamepadTrigger, 2 },
{ ElementType.AnalogStick, 2 },
{ ElementType.MouseWheel, 4 },
{ ElementType.GamepadPlayerId, 5 },
{ ElementType.DPad, 9 },
};
private readonly XImage?[] _images = new XImage?[MAX_IAMGE_NUM];
public XImage?[] Images => _images;
public XImage? this[int index]
{
get => _images[index];
set
{
if (_images[index] != value)
{
_images[index] = value;
OnPropertyChanged($"Image{index}Source");
MappingH = CurrentImageSource?.PixelHeight ?? 1;
MappingW = CurrentImageSource?.PixelWidth ?? 1;
OnPropertyChanged(nameof(MaxHeight));
OnPropertyChanged(nameof(MaxWidth));
OnPropertyChanged(nameof(CurrentImageSource));
}
}
}
public bool IsMainImageLoaded => _images[0] != null;
public bool IsImagesLoaded
{
get
{
for (int i = 0; i < ImagesCount[ElementType]; i++)
{
if (_images[i] == null)
return false;
}
return true;
}
}
public bool IsAllSameShape
{
get
{
var validImages = _images.Take(ImagesCount[ElementType]).Where(img => img != null).ToList();
if (validImages.Count == 0) return true;
int width = validImages[0]!.Width;
int height = validImages[0]!.Height;
return validImages.All(img => img!.Width == width && img.Height == height);
}
}
public bool IsGIFExist => _images.Any(img => img != null && img.ImageType == XImageType.GIF);
public void FillMissingImagesWithMainImage()
{
for (int i = 0; i < ImagesCount[ElementType]; i++)
{
this[i] = this[0];
}
}
public void ResizeAllImagesToMainImage()
{
if (!IsImagesLoaded) return;
int width = this[0]?.Width ?? 2;
int height = this[0]?.Height ?? 2;
for (int i = 1; i < ImagesCount[ElementType]; i++)
{
if (_images[i] != null && _images[i]?.ImageType != XImageType.GIF)
this[i] = this[i]?.ReSize(height, width);
}
}
public void ResizeAllImagesToMaxSize()
{
if (!IsImagesLoaded) return;
int maxWidth = _images.Take(ImagesCount[ElementType]).Max(img => img?.Width ?? 0);
int maxHeight = _images.Take(ImagesCount[ElementType]).Max(img => img?.Height ?? 0);
maxWidth = Math.Max(maxWidth, 2);
maxHeight = Math.Max(maxHeight, 2);
for (int i = 0; i < ImagesCount[ElementType]; i++)
{
if (_images[i] != null && _images[i]?.ImageType != XImageType.GIF)
this[i] = this[i]?.ReSize(maxHeight, maxWidth);
}
}
public void ResizeAllImagesToFirstGIF()
{
var firstGIF = _images.FirstOrDefault(img => img != null && img.ImageType == XImageType.GIF);
if (firstGIF == null) return;
int width = firstGIF.Width;
int height = firstGIF.Height;
for (int i = 0; i < ImagesCount[ElementType]; i++)
{
if (_images[i] != null && _images[i]?.ImageType != XImageType.GIF)
this[i] = this[i]?.ReSize(height, width);
}
}
public BitmapSource? GetImageSource(int index)
{
if (ElementType == ElementType.GamepadTrigger && _triggerCacheImages != null)
{
return _triggerCacheImages[index].BitmapSource;
}
else
return _images[index]?.BitmapSource;
}
private int _currentSourceIdx = 0;
public int CurrentSourceIdx
{
get => _currentSourceIdx;
set
{
if (ElementType == ElementType.GamepadTrigger && _triggerCacheImages != null)
{
if (value < 0 || value >= _triggerCacheImages.Count) return;
}
else if (ElementType != ElementType.GamepadTrigger)
{
if (value < 0 || value >= MAX_IAMGE_NUM) return;
}
SetProperty(ref _currentSourceIdx, value);
MappingH = CurrentImageSource?.PixelHeight ?? 1;
MappingW = CurrentImageSource?.PixelWidth ?? 1;
OnPropertyChanged(nameof(CurrentImageSource));
OnPropertyChanged(nameof(CurrentModifiedImageSource));
OnPropertyChanged(nameof(CurrentModifiedImageWidth));
OnPropertyChanged(nameof(CurrentModifiedImageHeight));
OnPropertyChanged(nameof(ModifiedCanvasWidth));
OnPropertyChanged(nameof(ModifiedCanvasHeight));
}
}
private bool _useStopwatch = false;
public bool UseStopwatch
{
get => _useStopwatch;
set
{
SetProperty(ref _useStopwatch, value);
OnPropertyChanged(nameof(StopwatchVisibility));
}
}
public Visibility StopwatchVisibility => _useStopwatch ? Visibility.Visible : Visibility.Collapsed;
private int _stopwatchMilliseconds = 50;
public int StopwatchMilliseconds
{
get => _stopwatchMilliseconds;
set
{
if (value >= 50 && value <= 5000)
SetProperty(ref _stopwatchMilliseconds, value);
}
}
private Stopwatch? _stopwatch = null;
public Stopwatch Stopwatch => _stopwatch ??= new Stopwatch();
private List<XImage>? _triggerCacheImages = null;
public List<XImage>? TriggerCacheImages => _triggerCacheImages;
public BitmapSource? MainImageSource => GetImageSource(0);
public BitmapSource? CurrentImageSource => GetImageSource(_currentSourceIdx);
public BitmapSource? Image0Source => GetImageSource(0);
public BitmapSource? Image1Source => GetImageSource(1);
public BitmapSource? Image2Source => GetImageSource(2);
public BitmapSource? Image3Source => GetImageSource(3);
public BitmapSource? Image4Source => GetImageSource(4);
public BitmapSource? Image5Source => GetImageSource(5);
public BitmapSource? Image6Source => GetImageSource(6);
public BitmapSource? Image7Source => GetImageSource(7);
public BitmapSource? Image8Source => GetImageSource(8);
private bool _isTriggerMode = false;
public bool IsTriggerMode
{
get => _isTriggerMode;
set
{
SetProperty(ref _isTriggerMode, value);
OnPropertyChanged(nameof(IsNonTriggerMode));
OnPropertyChanged(nameof(TriggerCursor));
}
}
public bool IsNonTriggerMode => !_isTriggerMode;
public Cursor TriggerCursor => _isTriggerMode ? Cursors.No : Cursors.Arrow;
public Array GamepadButtonList => Enum.GetValues(typeof(GamepadCodeType));
private GamepadCodeType _selectedGamepadButton = Models.GamepadCodeType.A;
public GamepadCodeType SelectedGamepadButton
{
get => _selectedGamepadButton;
set
{
SetProperty(ref _selectedGamepadButton, value);
OnPropertyChanged(nameof(OverlayJSON));
}
}
public Array KeyBoardButtonList => Enum.GetValues(typeof(KeyBoardCodeType));
private KeyBoardCodeType _selectedKeyBoardButton = Models.KeyBoardCodeType.None;
public KeyBoardCodeType SelectedKeyBoardButton
{
get => _selectedKeyBoardButton;
set
{
SetProperty(ref _selectedKeyBoardButton, value);
OnPropertyChanged(nameof(OverlayJSON));
}
}
public Array SideList => Enum.GetValues(typeof(Side));
private Side _selectedSide = Models.Side.Left;
public Side SelectedSide
{
get => _selectedSide;
set
{
SetProperty(ref _selectedSide, value);
OnPropertyChanged(nameof(OverlayJSON));
}
}
public Array MouseButtonList => Enum.GetValues(typeof(MouseCodeType));
private MouseCodeType _selectedMouseButton = Models.MouseCodeType.None;
public MouseCodeType SelectedMouseButton
{
get => _selectedMouseButton;
set
{
SetProperty(ref _selectedMouseButton, value);
OnPropertyChanged(nameof(OverlayJSON));
}
}
public Array DirectionList => Enum.GetValues(typeof(Direction));
private Direction _selectedDirection = Models.Direction.Up;
public Direction SelectedDirection
{
get => _selectedDirection;
set
{
SetProperty(ref _selectedDirection, value);
OnPropertyChanged(nameof(OverlayJSON));
}
}
public Array MouseMoveList => Enum.GetValues(typeof(MouseMoveType));
private MouseMoveType _selectedMouseMoveType = MouseMoveType.Dot;
public MouseMoveType SelectedMouseMoveType
{
get => _selectedMouseMoveType;
set
{
SetProperty(ref _selectedMouseMoveType, value);
OnPropertyChanged(nameof(OverlayJSON));
}
}
public ElementViewModel()
{ GetSegments(); }
public override string ToString()
{
return ParentCollection==null?"无": $"编号: {Idx}";
}
public void Dispose()
{
this.OffsetAnchorViewModel = null;
this.AnimatedImageControl = null;
this.PropertyChanged = null;
this.OffsetXChanged = null;
this.OffsetYChanged = null;
this.IdxChanged = null;
this.ParentCollection = null;
}
public string OverlayJSON => ToOverlayElement().ToJSON();
}
}
@@ -0,0 +1,586 @@
using FancyInput.Models;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Imaging;
namespace FancyInput.ViewModels
{
public partial class ElementViewModel
{
private bool _isDetectingInput = false;
public bool IsDetectingInput
{
get => _isDetectingInput;
set
{
SetProperty(ref _isDetectingInput, value);
OnPropertyChanged(nameof(DetectingString));
OnPropertyChanged(nameof(DetectingColor));
}
}
public string DetectingString => _isDetectingInput ? "检测中..." : "检测";
public SolidColorBrush DetectingColor => _isDetectingInput ? new SolidColorBrush(Colors.Red) : new SolidColorBrush(Color.FromArgb(0xFF, 0x65, 0x1F, 0x65));
private double _canvasWidth = 100;
public double CanvasWidth
{
get => _canvasWidth;
set => SetProperty(ref _canvasWidth, value);
}
private double _canvasHeight = 100;
public double CanvasHeight
{
get => _canvasHeight;
set => SetProperty(ref _canvasHeight, value);
}
public void InitCanvasSize()
{
var images = _images.Take(ImagesCount[ElementType]).Where(img => img != null).ToList();
if (images.Count == 0)
{
CanvasWidth = 100;
CanvasHeight = 100;
}
else
{
int maxWidth = images.Max(img => img!.Width) ;
int maxHeight = images.Max(img => img!.Height);
CanvasWidth = maxWidth;
CanvasHeight = maxHeight;
}
}
public double[] SegmentsScales = Enumerable.Repeat(1.0, MAX_IAMGE_NUM).ToArray();
public double[] SegmentsOffsetX = Enumerable.Repeat(0.0, MAX_IAMGE_NUM).ToArray();
public double[] SegmentsOffsetY = Enumerable.Repeat(0.0, MAX_IAMGE_NUM).ToArray();
public enum AlignRegionMode
{
Union,
Intersection,
}
private bool _allowTesting = false;
public bool AllowTesting
{
get => _allowTesting;
set
{
SetProperty(ref _allowTesting, value);
}
}
//private ObservableCollection<ElementSegmentViewModel> _segments = new ObservableCollection<ElementSegmentViewModel>();
public ObservableCollection<ElementSegmentViewModel> Segments { get; } = new ObservableCollection<ElementSegmentViewModel>();
public ObservableCollection<ElementSegmentViewModel> SelectedSegmentViewModels { get; } = new ObservableCollection<ElementSegmentViewModel>();
public bool HasSelectedSegment => SelectedSegmentViewModels.Count > 0;
public bool HasMultipleSelectedSegments => SelectedSegmentViewModels.Count > 1;
public Visibility HasSelectedSegmentVisibility => HasSelectedSegment ? Visibility.Visible : Visibility.Collapsed;
public Visibility HasMultipleSelectedSegmentsVisibility => HasMultipleSelectedSegments ? Visibility.Visible : Visibility.Collapsed;
public ElementSegmentViewModel? SelectedSegmentViewModel => HasSelectedSegment ? SelectedSegmentViewModels[0] : null;
private ElementSegmentViewModel? _erasePreviewOwnerSegment;
private XImage? _erasePreviewImage;
public bool HasPendingErase => _erasePreviewImage != null;
public ImageSource? SelectedSegmentSource => _erasePreviewImage?.BitmapSource ?? SelectedSegmentViewModel?.ImageSource;
public double SelectedSegmentWidth => _erasePreviewImage?.Width ?? SelectedSegmentViewModel?.Width ?? 0;
public double SelectedSegmentHeight => _erasePreviewImage?.Height ?? SelectedSegmentViewModel?.Height ?? 0;
public double MaxEraseBrushRadius => Math.Max(CanvasWidth, CanvasHeight)/2*0.3;
public double MinEraseBrushRadius => 0.5;
public double _eraseBrushRadius = 2;
public double EraseBrushRadius
{
get => _eraseBrushRadius;
set
{
if (SetProperty(ref _eraseBrushRadius, value))
{
OnPropertyChanged(nameof(EraseBrushDiameter));
OnPropertyChanged(nameof(EraseBrushPreviewLeft));
OnPropertyChanged(nameof(EraseBrushPreviewTop));
}
}
}
private double _eraseBrushCenterX = 0;
public double EraseBrushCenterX
{
get => _eraseBrushCenterX;
private set
{
if (SetProperty(ref _eraseBrushCenterX, value))
{
OnPropertyChanged(nameof(EraseBrushPreviewLeft));
}
}
}
private double _eraseBrushCenterY = 0;
public double EraseBrushCenterY
{
get => _eraseBrushCenterY;
private set
{
if (SetProperty(ref _eraseBrushCenterY, value))
{
OnPropertyChanged(nameof(EraseBrushPreviewTop));
}
}
}
private bool _isEraseBrushPreviewVisible = false;
public bool IsEraseBrushPreviewVisible
{
get => _isEraseBrushPreviewVisible;
private set
{
if (SetProperty(ref _isEraseBrushPreviewVisible, value))
{
OnPropertyChanged(nameof(EraseBrushPreviewVisibility));
}
}
}
public double EraseBrushDiameter => EraseBrushRadius * 2;
public double EraseBrushPreviewLeft => EraseBrushCenterX - EraseBrushRadius;
public double EraseBrushPreviewTop => EraseBrushCenterY - EraseBrushRadius;
public Visibility EraseBrushPreviewVisibility => IsEraseBrushPreviewVisible && HasSelectedSegment ? Visibility.Visible : Visibility.Collapsed;
public void SetEraseBrushPreview(double centerX, double centerY)
{
EraseBrushCenterX = centerX;
EraseBrushCenterY = centerY;
IsEraseBrushPreviewVisible = true;
}
public void HideEraseBrushPreview()
{
IsEraseBrushPreviewVisible = false;
}
private double _selectionRectLeft = 0;
public double SelectionRectLeft
{
get => _selectionRectLeft;
set => SetProperty(ref _selectionRectLeft, value);
}
private double _selectionRectTop = 0;
public double SelectionRectTop
{
get => _selectionRectTop;
set => SetProperty(ref _selectionRectTop, value);
}
private double _selectionRectWidth = 0;
public double SelectionRectWidth
{
get => _selectionRectWidth;
set => SetProperty(ref _selectionRectWidth, value);
}
private double _selectionRectHeight = 0;
public double SelectionRectHeight
{
get => _selectionRectHeight;
set => SetProperty(ref _selectionRectHeight, value);
}
private Visibility _selectionRectVisibility = Visibility.Collapsed;
public Visibility SelectionRectVisibility
{
get => _selectionRectVisibility;
set => SetProperty(ref _selectionRectVisibility, value);
}
public void SyncSelectedSegmentState()
{
foreach (var segment in Segments)
{
segment.IsSelected = SelectedSegmentViewModels.Contains(segment);
}
if (_erasePreviewOwnerSegment != null && _erasePreviewOwnerSegment != SelectedSegmentViewModel)
{
DiscardErasePreview();
}
OnPropertyChanged(nameof(HasSelectedSegment));
OnPropertyChanged(nameof(HasMultipleSelectedSegments));
OnPropertyChanged(nameof(HasSelectedSegmentVisibility));
OnPropertyChanged(nameof(HasMultipleSelectedSegmentsVisibility));
OnPropertyChanged(nameof(HasPendingErase));
OnPropertyChanged(nameof(EraseBrushPreviewVisibility));
OnPropertyChanged(nameof(SelectedSegmentViewModel));
OnPropertyChanged(nameof(SelectedSegmentSource));
OnPropertyChanged(nameof(SelectedSegmentWidth));
OnPropertyChanged(nameof(SelectedSegmentHeight));
}
public bool TryStartOrContinueErasePreview(ElementSegmentViewModel segment)
{
if (segment.Image == null)
{
return false;
}
if (_erasePreviewOwnerSegment != segment || _erasePreviewImage == null)
{
_erasePreviewOwnerSegment = segment;
_erasePreviewImage = segment.Image.Copy();
}
OnPropertyChanged(nameof(HasPendingErase));
OnPropertyChanged(nameof(SelectedSegmentSource));
OnPropertyChanged(nameof(SelectedSegmentWidth));
OnPropertyChanged(nameof(SelectedSegmentHeight));
return true;
}
public bool TryApplyEraseToPreview(double centerX, double centerY, double radius)
{
if (_erasePreviewImage == null)
{
return false;
}
_erasePreviewImage = _erasePreviewImage.EraseCircle(centerX, centerY, radius);
OnPropertyChanged(nameof(SelectedSegmentSource));
OnPropertyChanged(nameof(SelectedSegmentWidth));
OnPropertyChanged(nameof(SelectedSegmentHeight));
return true;
}
public bool CommitErasePreview()
{
if (_erasePreviewOwnerSegment == null || _erasePreviewImage == null)
{
return false;
}
_erasePreviewOwnerSegment.Image = _erasePreviewImage.Copy();
DiscardErasePreview();
return true;
}
public void DiscardErasePreview()
{
if (_erasePreviewOwnerSegment == null && _erasePreviewImage == null)
{
return;
}
_erasePreviewOwnerSegment = null;
_erasePreviewImage = null;
OnPropertyChanged(nameof(HasPendingErase));
OnPropertyChanged(nameof(SelectedSegmentSource));
OnPropertyChanged(nameof(SelectedSegmentWidth));
OnPropertyChanged(nameof(SelectedSegmentHeight));
}
public void RemoveLockedSelectedSegments()
{
for (int i = SelectedSegmentViewModels.Count - 1; i >= 0; i--)
{
if (SelectedSegmentViewModels[i].IsLocked)
{
SelectedSegmentViewModels.RemoveAt(i);
}
}
SyncSelectedSegmentState();
}
public void AttachSegmentChangeHandler()
{
foreach (var segment in Segments)
{
segment.SegmentChanged -= ChangeModifiedVisual;
segment.SegmentChanged += ChangeModifiedVisual;
}
}
public void DetachSegmentChangeHandler()
{
foreach (var segment in Segments)
{
segment.SegmentChanged -= ChangeModifiedVisual;
}
}
public void ChangeModifiedVisual()
{
SaveSegments();
ResetModifiedImageSources();
OnPropertyChanged(nameof(CurrentModifiedImageSource));
OnPropertyChanged(nameof(CurrentModifiedImageWidth));
OnPropertyChanged(nameof(CurrentModifiedImageHeight));
OnPropertyChanged(nameof(ModifiedCanvasWidth));
OnPropertyChanged(nameof(ModifiedCanvasHeight));
}
private static readonly string[] DefaultTwoStateTitles = { "闲置", "按下" };
private static readonly string[] DPadTitles = { "闲置", "左", "右", "上", "下", "左上", "右上", "左下", "右下" };
private static readonly string[] MouseWheelTitles = { "闲置", "按下", "向上", "向下" };
private static readonly string[] GamepadPlayerIdTitles = { "玩家1", "玩家2", "玩家3", "玩家4", "GUIDE" };
private static readonly string[] SingleTextureTitle = { "贴图" };
public void GetSegments()
{
DetachSegmentChangeHandler();
DiscardErasePreview();
SelectedSegmentViewModels.Clear();
Segments.Clear();
string[]? titles = ElementType switch
{
ElementType.KeyboardButton => DefaultTwoStateTitles,
ElementType.MouseButton => DefaultTwoStateTitles,
ElementType.GamepadButton => DefaultTwoStateTitles,
ElementType.GamepadTrigger => DefaultTwoStateTitles,
ElementType.AnalogStick => DefaultTwoStateTitles,
ElementType.DPad => DPadTitles,
ElementType.MouseWheel => MouseWheelTitles,
ElementType.GamepadPlayerId => GamepadPlayerIdTitles,
ElementType.MouseMovement => SingleTextureTitle,
_ => null,
};
if (titles == null)
{
return;
}
int count = Math.Min(titles.Length, ImagesCount[ElementType]);
for (int i = 0; i < count; i++)
{
var segment = new ElementSegmentViewModel
{
Name = titles[i],
Image = _images[i],
LocalScale = SegmentsScales[i],
OffsetX = SegmentsOffsetX[i],
OffsetY = SegmentsOffsetY[i]
};
segment.ConfigureEditorRanges(CanvasWidth, CanvasHeight);
Segments.Add(segment);
}
AttachSegmentChangeHandler();
}
public void SaveSegments()
{
for (int i = 0; i < Segments.Count; i++)
{
if (i < ImagesCount[ElementType])
{
SegmentsScales[i] = Segments[i].LocalScale;
SegmentsOffsetX[i] = Segments[i].OffsetX;
SegmentsOffsetY[i] = Segments[i].OffsetY;
}
}
}
private List<XImage>? _modifiedTriggerCacheImages = null;
private XImage?[] _modifiedImages = new XImage?[MAX_IAMGE_NUM];
public XImage?[] ModifiedImages => _modifiedImages;
public void MakeModifiedCache(int frameRange)
{
if (ElementType != ElementType.GamepadTrigger) return;
XImage? startImage = _modifiedImages[0];
XImage? endImage = _modifiedImages[1];
if (startImage == null || endImage == null) return;
if (_modifiedTriggerCacheImages != null)
{
_modifiedTriggerCacheImages.Clear();
}
int framesInBatch = frameRange + 1;
_modifiedTriggerCacheImages = new List<XImage>();
for (int directionIdx = 1; directionIdx <= 4; directionIdx++)
{
Direction direction = (Direction)directionIdx;
for (int frame = 0; frame < framesInBatch; frame++)
{
double percent = (double)frame / frameRange;
XImage Merge = XImage.Merge(startImage, endImage, direction, percent);
_modifiedTriggerCacheImages.Add(Merge);
}
}
}
public BitmapSource? GetModifiedImageSource(int index)
{
if (ElementType == ElementType.GamepadTrigger && _modifiedTriggerCacheImages != null)
{
return _modifiedTriggerCacheImages[index].BitmapSource;
}
else
return _modifiedImages[index]?.BitmapSource;
}
public BitmapSource? CurrentModifiedImageSource => GetModifiedImageSource(_currentSourceIdx);
public double CurrentModifiedImageWidth => CurrentModifiedImageSource?.PixelWidth ?? 0;
public double CurrentModifiedImageHeight => CurrentModifiedImageSource?.PixelHeight ?? 0;
public double ModifiedCanvasWidth => Math.Max(1, _modifiedImages.Where(img => img != null).Select(img => (double)img!.Width).DefaultIfEmpty(0).Max());
public double ModifiedCanvasHeight => Math.Max(1, _modifiedImages.Where(img => img != null).Select(img => (double)img!.Height).DefaultIfEmpty(0).Max());
private bool TryGetUnionValidRectFromModifiedImages(out int left, out int top, out int right, out int bottom)
{
left = 0;
top = 0;
right = 0;
bottom = 0;
bool hasAnyValidRect = false;
foreach (var image in _modifiedImages)
{
if (image == null)
{
continue;
}
int width = image.Width;
int height = image.Height;
var alpha = image.ExtractChannels().A;
int minX = width;
int minY = height;
int maxX = -1;
int maxY = -1;
for (int y = 0; y < height; y++)
{
int rowStart = y * width;
for (int x = 0; x < width; x++)
{
if (alpha[rowStart + x] == 0)
{
continue;
}
if (x < minX) minX = x;
if (y < minY) minY = y;
if (x > maxX) maxX = x;
if (y > maxY) maxY = y;
}
}
if (maxX < minX || maxY < minY)
{
continue;
}
if (!hasAnyValidRect)
{
left = minX;
top = minY;
right = maxX + 1;
bottom = maxY + 1;
hasAnyValidRect = true;
}
else
{
left = Math.Min(left, minX);
top = Math.Min(top, minY);
right = Math.Max(right, maxX + 1);
bottom = Math.Max(bottom, maxY + 1);
}
}
return hasAnyValidRect;
}
public void ResetModifiedImageSources(AlignRegionMode mode = AlignRegionMode.Union)
{
Array.Clear(_modifiedImages, 0, _modifiedImages.Length);
int imageCount = ImagesCount[ElementType];
var entries = new List<(int idx, XImage image, int x, int y, int right, int bottom)>();
for (int i = 0; i < imageCount; i++)
{
if (Segments[i].Image == null)
{
continue;
}
double scale = SegmentsScales[i] > 0 ? SegmentsScales[i] : 1.0;
XImage scaled = Math.Abs(scale - 1.0) < 1e-9 ? Segments[i].Image! : Segments[i].Image!.Scale(scale);
int x = (int)Math.Round(SegmentsOffsetX[i]);
int y = (int)Math.Round(SegmentsOffsetY[i]);
entries.Add((i, scaled, x, y, x + scaled.Width, y + scaled.Height));
}
if (entries.Count == 0)
{
return;
}
int regionLeft;
int regionTop;
int regionRight;
int regionBottom;
if (mode == AlignRegionMode.Intersection)
{
regionLeft = entries.Max(e => e.x);
regionTop = entries.Max(e => e.y);
regionRight = entries.Min(e => e.right);
regionBottom = entries.Min(e => e.bottom);
// No overlap in intersection mode.
if (regionRight <= regionLeft || regionBottom <= regionTop)
{
return;
}
}
else
{
regionLeft = entries.Min(e => e.x);
regionTop = entries.Min(e => e.y);
regionRight = entries.Max(e => e.right);
regionBottom = entries.Max(e => e.bottom);
}
int regionWidth = regionRight - regionLeft;
int regionHeight = regionBottom - regionTop;
foreach (var entry in entries)
{
int cropX = regionLeft - entry.x;
int cropY = regionTop - entry.y;
_modifiedImages[entry.idx] = entry.image.Crop(cropX, cropY, regionWidth, regionHeight);
}
if (TryGetUnionValidRectFromModifiedImages(out int validLeft, out int validTop, out int validRight, out int validBottom))
{
int validWidth = validRight - validLeft;
int validHeight = validBottom - validTop;
for (int i = 0; i < _modifiedImages.Length; i++)
{
if (_modifiedImages[i] == null)
{
continue;
}
_modifiedImages[i] = _modifiedImages[i]!.Crop(validLeft, validTop, validWidth, validHeight);
}
}
MakeModifiedCache(InputHandler.TRIGGER_CACHE_RANGE);
}
}
}
@@ -0,0 +1,713 @@
using FancyInput.Models;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using Cursor = System.Windows.Input.Cursor;
using Cursors = System.Windows.Input.Cursors;
namespace FancyInput.ViewModels
{
public enum ImageSourceType
{
Main,
Original
}
public class ImageEditorViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler? PropertyChanged;
protected void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
protected virtual bool SetProperty<T>(ref T field, T value, [CallerMemberName] string propertyName = "")
{
if (Equals(field, value)) return false;
field = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
return true;
}
public double OriginalCanvasScale { get; set; } = 1.0;
public const double ScaleStep = 0.1;
public const double MinScale = 0.1;
public const double MaxScale = 10.0;
public double ActualMinScale => MinScale * OriginalCanvasScale;
public double ActualMaxScale => MaxScale * OriginalCanvasScale;
public double PowStep => 1 + ScaleStep;
private double _canvasScale = 1.0;
public double CanvasScale
{
get => _canvasScale;
set
{
SetProperty(ref _canvasScale, value);
OnPropertyChanged(nameof(CutLineWidth));
}
}
public double CutLineWidth => 1.0 / CanvasScale;
private ImageSourceType _imageSourceType = ImageSourceType.Main;
public ImageSourceType ImageSourceType
{
get => _imageSourceType;
set
{
SetProperty(ref _imageSourceType, value);
if (_imageSourceType == ImageSourceType.Main)
CurrentImage = MainImage;
else
CurrentImage = OriginalImage;
}
}
private int _originalH, _originalW;
public int OriginalH
{
get => _originalH;
set =>SetProperty(ref _originalH, value);
}
public int OriginalW
{
get => _originalW;
set => SetProperty(ref _originalW, value);
}
public const int MIN_H = 1;
public const int MIN_W = 1;
public int H => Math.Max((int)((_originalH - _topCut - _bottomCut) * _imageScale), MIN_H);
public int W => Math.Max((int)((_originalW - _leftCut - _rightCut) * _imageScale),MIN_W);
public int VirtualH => _originalH - _topCut - _bottomCut;
public int VirtualW => _originalW - _leftCut - _rightCut;
private double _logImageScale = 0;
public double LogImageScale
{
get => _logImageScale;
set
{
SetProperty(ref _logImageScale, value);
_imageScale = Math.Pow(10, _logImageScale);
OnPropertyChanged(nameof(W));
OnPropertyChanged(nameof(H));
OnPropertyChanged(nameof(ImageScaleString));
}
}
private double _minLogImageScale;
public double MinLogImageScale
{
get => _minLogImageScale;
set => SetProperty(ref _minLogImageScale, value);
}
private double _maxLogImageScale;
public double MaxLogImageScale
{
get => _maxLogImageScale;
set => SetProperty(ref _maxLogImageScale, value);
}
private double _imageScale;
public string ImageScaleString
{
get => $"{_imageScale:F2}";
set
{
if (double.TryParse(value, out double result))
{
result = Math.Clamp(result, ActualMinScale, ActualMaxScale);
LogImageScale = Math.Log10(result);
OnPropertyChanged(nameof(ImageScaleString));
}
}
}
private Color _cutBorderBackgroundColor = Colors.Transparent;
public Color CutBorderBackgroundColor
{
get => _cutBorderBackgroundColor;
set
{
SetProperty(ref _cutBorderBackgroundColor, value);
OnPropertyChanged(nameof(CutBorderBackgroundBrush));
}
}
public SolidColorBrush CutBorderBackgroundBrush => new SolidColorBrush(CutBorderBackgroundColor);
private int _maxHCut;
public int MaxHCut
{
get => _maxHCut;
set=>SetProperty(ref _maxHCut, value);
}
private int _maxWCut;
public int MaxWCut
{
get => _maxWCut;
set => SetProperty(ref _maxWCut, value);
}
private int _leftCut;
public int LeftCut
{
get => _leftCut;
set
{
SetProperty(ref _leftCut, Math.Clamp(value, 0, _maxWCut - _rightCut));
OnPropertyChanged(nameof(VirtualW));
OnPropertyChanged(nameof(W));
}
}
private int _rightCut;
public int RightCut
{
get => _rightCut;
set
{
SetProperty(ref _rightCut, Math.Clamp(value, 0, _maxWCut - _leftCut));
OnPropertyChanged(nameof(VirtualW));
OnPropertyChanged(nameof(W));
}
}
private int _topCut;
public int TopCut
{
get => _topCut;
set
{
SetProperty(ref _topCut, Math.Clamp(value, 0, _maxHCut - _bottomCut));
OnPropertyChanged(nameof(VirtualH));
OnPropertyChanged(nameof(H));
}
}
private int _bottomCut;
public int BottomCut
{
get => _bottomCut;
set
{
SetProperty(ref _bottomCut, Math.Clamp(value, 0, _maxHCut - _topCut));
OnPropertyChanged(nameof(VirtualH));
OnPropertyChanged(nameof(H));
}
}
private bool _isCutVisible = false;
public bool IsCutVisible
{
get => _isCutVisible;
set
{
SetProperty(ref _isCutVisible, value);
CutVisibility = value? Visibility.Visible:Visibility.Hidden;
OnPropertyChanged(nameof(CutVisibility));
}
}
public Visibility CutVisibility { get; set; }
private bool _isDetectingInput = false;
public bool IsEditingCutBorder
{
get => _isDetectingInput;
set
{
SetProperty(ref _isDetectingInput, value);
OnPropertyChanged(nameof(DetectingString));
OnPropertyChanged(nameof(DetectingColor));
}
}
public string DetectingString => _isDetectingInput ? "绘制中..." : "手动";
public SolidColorBrush DetectingColor => _isDetectingInput ? new SolidColorBrush(Colors.Red) : new SolidColorBrush(Color.FromArgb(0xFF, 0x65, 0x1F, 0x65));
private bool _isProtectTransparent = true;
public bool IsProtectTransparent
{
get => _isProtectTransparent;
set => SetProperty(ref _isProtectTransparent, value);
}
private byte[] _originalRBytes;
private byte[] _originalGBytes;
private byte[] _originalBBytes;
private byte[] _originalABytes;
public XImage OriginalImage => XImage.MergeChannels(_originalRBytes, _originalGBytes,
_originalBBytes, _originalABytes, _originalH, _originalW);
public BitmapSource OriginalImageSource => OriginalImage.BitmapSource;
private XImage _mainImage;
public XImage MainImage
{
get => _mainImage;
set
{
SetProperty(ref _mainImage, value);
OnPropertyChanged(nameof(MainImageSource));
if (ImageSourceType == ImageSourceType.Main)
CurrentImage = MainImage;
}
}
public BitmapSource MainImageSource => MainImage.BitmapSource;
private XImage? _currentImage;
private XImage? CurrentImage
{
get => _currentImage;
set
{
SetProperty(ref _currentImage, value);
OnPropertyChanged(nameof(CurrentImageSource));
}
}
public BitmapSource? CurrentImageSource => CurrentImage?.BitmapSource;
private byte[] _rBytes;
private byte[] _gBytes;
private byte[] _bBytes;
private byte[] _aBytes;
private double _rShift = 0;
public double RShift
{
get => _rShift;
set
{
SetProperty(ref _rShift, value);
_rBytes = ShiftBytes(_originalRBytes, _rShift, _aBytes);
MainImage = XImage.MergeChannels(_rBytes, _gBytes, _bBytes, _aBytes, _originalH, _originalW);
}
}
private double _gShift = 0;
public double GShift
{
get => _gShift;
set
{
SetProperty(ref _gShift, value);
_gBytes = ShiftBytes(_originalGBytes, _gShift, _aBytes);
MainImage = XImage.MergeChannels(_rBytes, _gBytes, _bBytes, _aBytes, _originalH, _originalW);
}
}
private double _bShift = 0;
public double BShift
{
get => _bShift;
set
{
SetProperty(ref _bShift, value);
_bBytes = ShiftBytes(_originalBBytes, _bShift, _aBytes);
MainImage = XImage.MergeChannels(_rBytes, _gBytes, _bBytes, _aBytes, _originalH, _originalW);
}
}
private double _aShift = 0;
public double AShift
{
get => _aShift;
set
{
SetProperty(ref _aShift, value);
_aBytes = ShiftBytes(_originalABytes, _aShift, _originalABytes, true);
MainImage = XImage.MergeChannels(_rBytes, _gBytes, _bBytes, _aBytes, _originalH, _originalW);
}
}
//蒙版
private bool[] _maskBytes;
public bool[] MaskBytes
{
get => _maskBytes;
set
{
SetProperty(ref _maskBytes, value);
OnPropertyChanged(nameof(MaskBytes));
OnPropertyChanged(nameof(MaskSource));
}
}
private bool _useMask = false;
public bool UseMask
{
get => _useMask;
set => SetProperty(ref _useMask, value);
}
private bool _isMaskVisible = false;
public bool IsMaskVisible
{
get => _isMaskVisible;
set
{
SetProperty(ref _isMaskVisible, value);
OnPropertyChanged(nameof(MaskVisibility));
OnPropertyChanged(nameof(MaskBrushPreviewVisibility));
OnPropertyChanged(nameof(CanvasCursor));
}
}
public Visibility MaskVisibility => _isMaskVisible? Visibility.Visible : Visibility.Collapsed;
private Color _maskColor = Color.FromArgb(150,255,0,0);
public Color MaskColor
{
get => _maskColor;
set
{
SetProperty(ref _maskColor, value);
OnPropertyChanged(nameof(MaskColorBrush));
OnPropertyChanged(nameof(MaskSource));
}
}
public SolidColorBrush MaskColorBrush => new SolidColorBrush(MaskColor);
public BitmapSource MaskSource => XImage.FromMask(_maskBytes, OriginalH, OriginalW, _maskColor).BitmapSource;
//private bool _allowEditingMask = false;
//public bool AllowEditingMask
//{
// get => _allowEditingMask;
// set
// {
// SetProperty(ref _allowEditingMask, value);
// OnPropertyChanged(nameof(CanvasCursor));
// OnPropertyChanged(nameof(IsEditBorderEnabled));
// }
//}
//public bool IsEditBorderEnabled => !AllowEditingMask;
private bool _useMaskBrush = false;
public bool UseMaskBrush
{
get => _useMaskBrush;
set
{
SetProperty(ref _useMaskBrush, value);
OnPropertyChanged(nameof(CanvasCursor));
OnPropertyChanged(nameof(MaskBrushPreviewVisibility));
}
}
public const double MinMaskBrushRadius = 0.4;
public const double MaxMaskBrushRadius = 60;
public const double MaskBrushLogStep = 0.08;
public double MinLogMaskBrushRadius => Math.Log10(MinMaskBrushRadius);
public double MaxLogMaskBrushRadius => Math.Log10(MaxMaskBrushRadius);
private double _maskBrushRadius = 1;
public double MaskBrushRadius
{
get => _maskBrushRadius;
set
{
SetProperty(ref _maskBrushRadius, Math.Clamp(value, MinMaskBrushRadius, MaxMaskBrushRadius));
OnPropertyChanged(nameof(MaskBrushRadiusString));
OnPropertyChanged(nameof(LogMaskBrushRadius));
OnPropertyChanged(nameof(MaskBrushPreviewDiameter));
OnPropertyChanged(nameof(MaskBrushPreviewLeft));
OnPropertyChanged(nameof(MaskBrushPreviewTop));
}
}
public double LogMaskBrushRadius
{
get => Math.Log10(MaskBrushRadius);
set => MaskBrushRadius = Math.Pow(10, Math.Clamp(value, MinLogMaskBrushRadius, MaxLogMaskBrushRadius));
}
private double _maskBrushCenterX;
public double MaskBrushCenterX
{
get => _maskBrushCenterX;
private set
{
if (SetProperty(ref _maskBrushCenterX, value))
{
OnPropertyChanged(nameof(MaskBrushPreviewLeft));
}
}
}
private double _maskBrushCenterY;
public double MaskBrushCenterY
{
get => _maskBrushCenterY;
private set
{
if (SetProperty(ref _maskBrushCenterY, value))
{
OnPropertyChanged(nameof(MaskBrushPreviewTop));
}
}
}
private bool _isMaskBrushPreviewVisible;
public bool IsMaskBrushPreviewVisible
{
get => _isMaskBrushPreviewVisible;
private set
{
if (SetProperty(ref _isMaskBrushPreviewVisible, value))
{
OnPropertyChanged(nameof(MaskBrushPreviewVisibility));
}
}
}
public double MaskBrushPreviewDiameter => MaskBrushRadius * 2;
public double MaskBrushPreviewLeft => MaskBrushCenterX - MaskBrushRadius;
public double MaskBrushPreviewTop => MaskBrushCenterY - MaskBrushRadius;
public Visibility MaskBrushPreviewVisibility => IsMaskVisible && UseMaskBrush && IsMaskBrushPreviewVisible
? Visibility.Visible
: Visibility.Collapsed;
public void SetMaskBrushPreview(double x, double y)
{
MaskBrushCenterX = Math.Clamp(x, 0, Math.Max(0, OriginalW - 1));
MaskBrushCenterY = Math.Clamp(y, 0, Math.Max(0, OriginalH - 1));
IsMaskBrushPreviewVisible = true;
}
public void HideMaskBrushPreview()
{
IsMaskBrushPreviewVisible = false;
}
public void AdjustMaskBrushRadiusByWheel(int wheelDelta)
{
if (wheelDelta == 0)
{
return;
}
LogMaskBrushRadius += wheelDelta > 0 ? MaskBrushLogStep : -MaskBrushLogStep;
}
public string MaskBrushRadiusString => $"{MaskBrushRadius:F1}";
public Cursor CanvasCursor
{
get
{
if (IsMaskVisible)
{
if (UseMaskBrush)
return Cursors.Pen;
else
return Cursors.Cross;
}
else
return Cursors.Arrow;
}
}
private Color _imageBackGroundColor = Colors.Black;
public Color ImageBackGroundColor
{
get => _imageBackGroundColor;
set
{
SetProperty(ref _imageBackGroundColor, value);
OnPropertyChanged(nameof(ImageBackGround));
}
}
public SolidColorBrush ImageBackGround => new SolidColorBrush(_imageBackGroundColor);
private Visibility _maskBorderVisibility = Visibility.Collapsed;
public Visibility MaskBorderVisibility
{
get => _maskBorderVisibility;
set => SetProperty(ref _maskBorderVisibility, value);
}
private int _maskBorderX = 0;
private int _maskBorderY = 0;
private int _maskBorderW = 0;
private int _maskBorderH = 0;
public int MaskBorderX
{
get => _maskBorderX;
set => SetProperty(ref _maskBorderX, value);
}
public int MaskBorderY
{
get => _maskBorderY;
set => SetProperty(ref _maskBorderY, value);
}
public int MaskBorderW
{
get => _maskBorderW;
set => SetProperty(ref _maskBorderW, value);
}
public int MaskBorderH
{
get => _maskBorderH;
set => SetProperty(ref _maskBorderH, value);
}
public ImageEditorViewModel(XImage xImage)
{
_mainImage = xImage.Copy();
var bytes = xImage.ExtractChannels();
_originalRBytes = bytes.R;
_originalGBytes = bytes.G;
_originalBBytes = bytes.B;
_originalABytes = bytes.A;
_rBytes = CopyBytes(_originalRBytes);
_gBytes = CopyBytes(_originalGBytes);
_bBytes = CopyBytes(_originalBBytes);
_aBytes = CopyBytes(_originalABytes);
OriginalH = xImage.Height;
OriginalW = xImage.Width;
MaxHCut = _originalH -1;
MaxWCut = _originalW -1;
LeftCut = 0;
RightCut = 0;
TopCut = 0;
BottomCut = 0;
IsCutVisible = false;
CurrentImage = MainImage;
OriginalCanvasScale = 200.0/Math.Max(OriginalH, OriginalW);
CanvasScale = OriginalCanvasScale;
//计算最小和最大缩放比例
MinLogImageScale = -1;
MaxLogImageScale = 1;
LogImageScale = 0;
//蒙版
_maskBytes = CreateMask(_originalABytes, true);
MaskBorderX = 0;
MaskBorderY = 0;
MaskBorderW = OriginalW;
MaskBorderH = OriginalH;
}
private static byte[] CopyBytes(byte[] bytes)
{
byte[] newBytes = new byte[bytes.Length];
Array.Copy(bytes, newBytes, bytes.Length);
return newBytes;
}
private static bool[] CreateMask(byte[] bytes, bool fill)
{
bool[] newBytes = new bool[bytes.Length];
for (int i = 0; i < bytes.Length; i++)
{
newBytes[i] = fill;
}
return newBytes;
}
public void FillCroppedMask(int X,int Y,int W,int H,bool fill)
{
if (MaskBytes.Length != OriginalH * OriginalW)
throw new ArgumentException("Fatal Error:蒙版尺寸不正确!");
X = Math.Clamp(X, 0, OriginalW-1);
Y = Math.Clamp(Y, 0, OriginalH-1);
W = Math.Clamp(W, 1, OriginalW - X);
H = Math.Clamp(H, 1, OriginalH - Y);
for (int i = Y; i < Y + H; i++)
{
int iStride = i * OriginalW;
for (int j = X; j < X + W; j++)
{
MaskBytes[iStride + j] = fill;
}
}
MaskBytes = MaskBytes;
}
public void AddMask()=> FillCroppedMask(MaskBorderX, MaskBorderY, MaskBorderW, MaskBorderH, true);
public void RemoveMask()=> FillCroppedMask(MaskBorderX, MaskBorderY, MaskBorderW, MaskBorderH, false);
public void FillBrushMask(int X, int Y, double CircleRadius, bool fill)
{
if (MaskBytes.Length != OriginalH * OriginalW)
throw new ArgumentException("Fatal Error:蒙版尺寸不正确!");
X = Math.Clamp(X, 0, OriginalW - 1);
Y = Math.Clamp(Y, 0, OriginalH - 1);
int left = (int)Math.Clamp(X - CircleRadius, 0, OriginalW - 1);
int right = (int)Math.Clamp(X + CircleRadius, 0, OriginalW - 1);
int top = (int)Math.Clamp(Y - CircleRadius, 0, OriginalH - 1);
int bottom = (int)Math.Clamp(Y + CircleRadius, 0, OriginalH - 1);
double radiusSquared = CircleRadius * CircleRadius;
for (int i = top; i <= bottom; i++)
{
int iStride = i * OriginalW;
for (int j = left; j <= right; j++)
{
double dx = j - X;
double dy = i - Y;
if (dx * dx + dy * dy <= radiusSquared)
{
MaskBytes[iStride + j] = fill;
}
}
}
MaskBytes = MaskBytes;
}
private byte[] ShiftBytes(byte[] bytes, double shift, byte[] aBytes, bool isABytes = false)
{
byte[] newBytes = new byte[bytes.Length];
if (IsProtectTransparent)
{
for (int i = 0; i < bytes.Length; i++)
{
if (UseMask)
{
if (MaskBytes[i])
{
if (aBytes[i] == 0)
newBytes[i] = bytes[i]; // keep transparent pixels unchanged
else
{
int newValue = (int)bytes[i] + (int)shift;
if (newValue >= 255) newValue = 255;
if (newValue <= 0) newValue = isABytes ? 1 : 0; // prevent alpha from being 0
newBytes[i] = (byte)newValue;
}
}
else
{
int newValue = (int)bytes[i];
newBytes[i] = (byte)newValue;
}
}
else
{
if (aBytes[i] == 0)
newBytes[i] = bytes[i]; // keep transparent pixels unchanged
else
{
int newValue = (int)bytes[i] + (int)shift;
if (newValue >= 255) newValue = 255;
if (newValue <= 0) newValue = isABytes ? 1 : 0; // prevent alpha from being 0
newBytes[i] = (byte)newValue;
}
}
}
}
else
{
for (int i = 0; i < bytes.Length; i++)
{
if (UseMask)
{
if (MaskBytes[i])
{
int newValue = (int)bytes[i] + (int)shift;
if (newValue >= 255) newValue = 255;
if (newValue <= 0) newValue = 0;
newBytes[i] = (byte)newValue;
}
else
{
int newValue = (int)bytes[i];
newBytes[i] = (byte)newValue;
}
}
else
{
int newValue = (int)bytes[i] + (int)shift;
if (newValue >= 255) newValue = 255;
if (newValue <= 0) newValue = 0;
newBytes[i] = (byte)newValue;
}
}
}
return newBytes;
}
}
}
@@ -0,0 +1,740 @@
using FancyInput.Common;
using FancyInput.Models;
using System.Text.Json;
using System.Windows;
using System.Windows.Forms;
using System.Windows.Media;
namespace FancyInput.ViewModels;
using FancyInput.Views;
using System.Windows.Input;
public enum MacroActionType
{
None,
Command,
Keyboard,
MouseButton,
MouseMove,
MouseWheel,
Delay,
}
public class MacroAction : ViewModelBase
{
private string _name = "动作";
public string Name
{
get => _name;
set => SetProperty(ref _name, value);
}
private Geometry? _icon = null;
public Geometry? Icon
{
get => _icon;
set => SetProperty(ref _icon, value);
}
private int _iconMargin = 1;
public int IconMargin
{
get => _iconMargin;
set => SetProperty(ref _iconMargin, value);
}
private int _iconShiftX = 0;
public int IconShiftX
{
get => _iconShiftX;
set => SetProperty(ref _iconShiftX, value);
}
private int _iconShiftY = 0;
public int IconShiftY
{
get => _iconShiftY;
set => SetProperty(ref _iconShiftY, value);
}
private bool _isIconFilpped = false;
public bool IsIconFilpped
{
get => _isIconFilpped;
set => SetProperty(ref _isIconFilpped, value);
}
private bool _isSelected = false;
public bool IsSelected
{
get => _isSelected;
set
{
SetProperty(ref _isSelected, value);
OnPropertyChanged(nameof(Background));
OnPropertyChanged(nameof(IsSelectedVisibility));
}
}
public Visibility IsSelectedVisibility => IsSelected ? Visibility.Visible : Visibility.Collapsed;
public SolidColorBrush Background => IsSelected ? new SolidColorBrush(Color.FromArgb(0x11, 0x00, 0x00, 0x00))
: new SolidColorBrush(Colors.Transparent);
private MacroActionType _actionType = MacroActionType.None;
public MacroActionType ActionType
{
get => _actionType;
set => SetProperty(ref _actionType, value);
}
private int _preWait = 0;
public int PreWait
{
get => _preWait;
set
{
int wait_time_ms = Math.Min(Math.Max(0, value), 60000);
SetProperty(ref _preWait, wait_time_ms);
OnPropertyChanged(nameof(PreWaitVisibility));
}
}
protected int _duration = 0;
public virtual int Duration
{
get => _duration;
set
{
int duration_ms = Math.Min(Math.Max(0, value), 60000);
SetProperty(ref _duration, duration_ms);
OnPropertyChanged(nameof(DurationVisibility));
}
}
private int _postWait = 0;
public int PostWait
{
get => _postWait;
set
{
int wait_time_ms = Math.Min(Math.Max(0, value), 60000);
SetProperty(ref _postWait, wait_time_ms);
OnPropertyChanged(nameof(PostWaitVisibility));
}
}
private int _repeatCount = 1;
public int RepeatCount
{
get => _repeatCount;
set
{
int count = Math.Min(Math.Max(1, value), 1000);
SetProperty(ref _repeatCount, count);
OnPropertyChanged(nameof(RepeatCountVisibility));
}
}
public int TotalDuration => (PreWait + Duration + PostWait) * RepeatCount;
public int StartTime { get; set; }
public int EndTime => StartTime + Duration;
public Visibility PreWaitVisibility => _preWait > 0 ? Visibility.Visible : Visibility.Collapsed;
public Visibility DurationVisibility => _duration > 0 ? Visibility.Visible : Visibility.Collapsed;
public Visibility PostWaitVisibility => _postWait > 0 ? Visibility.Visible : Visibility.Collapsed;
public Visibility RepeatCountVisibility => _repeatCount > 1 ? Visibility.Visible : Visibility.Collapsed;
public MacroAction Clone() => FromDto(ToDto());
/// <summary>
/// 将当前 MacroAction 序列化为 DTO(仅包含数据属性,不含 UI 状态和计算属性)
/// </summary>
public MacroActionDto ToDto()
{
var dto = new MacroActionDto { ActionType = ActionType };
// 反射遍历当前类型及其基类中所有可读写的公共实例属性
foreach (var prop in GetType().GetProperties(
System.Reflection.BindingFlags.Public |
System.Reflection.BindingFlags.Instance))
{
if (!prop.CanRead || !prop.CanWrite) continue;
if (DtoSkipProperties.Contains(prop.Name)) continue;
dto.Properties[prop.Name] = prop.GetValue(this);
}
return dto;
}
/// <summary>
/// 从 DTO 反序列化创建 MacroAction 实例
/// </summary>
public static MacroAction FromDto(MacroActionDto dto)
{
var action = CreateInstance(dto.ActionType);
action.CopyFrom(dto);
return action;
}
/// <summary>
/// 从 DTO 原地恢复数据到当前实例(保留同一引用,不破坏 UI 绑定)
/// </summary>
public void CopyFrom(MacroActionDto dto)
{
foreach (var kvp in dto.Properties)
{
var prop = GetType().GetProperty(kvp.Key,
System.Reflection.BindingFlags.Public |
System.Reflection.BindingFlags.Instance);
if (prop != null && prop.CanWrite)
{
var value = kvp.Value;
// 处理 JsonElement 反序列化时的类型转换
if (value is System.Text.Json.JsonElement jsonElement)
{
value = System.Text.Json.JsonSerializer.Deserialize(jsonElement.GetRawText(), prop.PropertyType);
}
prop.SetValue(this, value);
}
}
}
/// <summary>
/// 根据 ActionType 创建对应的子类实例
/// </summary>
private static MacroAction CreateInstance(MacroActionType actionType) => actionType switch
{
MacroActionType.Command => new MacroActionCommand(),
MacroActionType.Keyboard => new MacroActionKeyboardButton(),
MacroActionType.MouseButton => new MacroActionMouseButton(),
MacroActionType.MouseMove => new MacroActionMouseMove(),
MacroActionType.MouseWheel => new MacroActionMouseWheel(),
MacroActionType.Delay => new MacroActionDelay(),
_ => throw new ArgumentException($"未知的 MacroActionType: {actionType}")
};
/// <summary>
/// DTO 序列化时跳过的属性名(UI 状态、计算属性、命令等不可序列化成员)
/// </summary>
private static readonly HashSet<string> DtoSkipProperties = new()
{
// UI 状态
nameof(IsSelected),
// WPF 类型(Geometry 有 Transform.Inverse 循环引用,不可序列化)
nameof(Icon),
// 计算属性
nameof(TotalDuration),
nameof(EndTime),
nameof(Background),
nameof(IsSelectedVisibility),
nameof(PreWaitVisibility),
nameof(PostWaitVisibility),
// 子类计算属性
"ShortName",
"DownLabelVisibility",
"UpLabelVisibility",
// 静态/类型信息
"AvailableKeys",
"AvailableButtons",
"AvailableMoveModes",
"AvailableWheelDirections",
// ICommand(不可序列化)
"CaptureFromCommand",
"CaptureToCommand",
};
}
/// <summary>
/// MacroAction 的数据传输对象,用于序列化/反序列化和 Clone
/// </summary>
public class MacroActionDto
{
public MacroActionType ActionType { get; set; }
public Dictionary<string, object?> Properties { get; set; } = new();
}
// 执行命令
public class MacroActionCommand : MacroAction
{
private string _command = "";
public string Command
{
get => _command;
set => SetProperty(ref _command, value);
}
private bool _waitUntilExit = true;
public bool WaitUntilExit
{
get => _waitUntilExit;
set => SetProperty(ref _waitUntilExit, value);
}
public MacroActionCommand()
{
Name = "执行命令";
ActionType = MacroActionType.Command;
Icon = PathDataGeometry.TerminalRound;
}
}
// 键盘
public class MacroActionKeyboardButton : MacroAction
{
public Array AvailableKeys => Enum.GetValues(typeof(FipRawKeys));
private FipRawKeys _key = FipRawKeys.None;
public FipRawKeys Key
{
get => _key;
set
{
SetProperty(ref _key, value);
OnPropertyChanged(nameof(ShortName));
}
}
private bool _down = true;
public bool Down
{
get => _down;
set
{
SetProperty(ref _down, value);
OnPropertyChanged(nameof(DownLabelVisibility));
}
}
private bool _up = true;
public bool Up
{
get => _up;
set
{
SetProperty(ref _up, value);
OnPropertyChanged(nameof(UpLabelVisibility));
}
}
public Visibility DownLabelVisibility => _down? Visibility.Visible:Visibility.Collapsed;
public Visibility UpLabelVisibility => _up ? Visibility.Visible : Visibility.Collapsed;
public string ShortName => _key.ToString().Length > 4
? _key.ToString().Substring(0, 4)
: _key.ToString();
public MacroActionKeyboardButton()
{
Name = "键盘";
ActionType = MacroActionType.Keyboard;
Icon = PathDataGeometry.KeyBoard;
IsIconFilpped = true;
}
}
//鼠标
public class MacroActionMouseButton : MacroAction
{
public Array AvailableButtons => Enum.GetValues(typeof(MouseButtons));
private MouseButtons _button = MouseButtons.None;
public MouseButtons Button
{
get => _button;
set
{
SetProperty(ref _button, value);
OnPropertyChanged(nameof(ShortName));
}
}
public string ShortName => _button switch
{
MouseButtons.Left => "L",
MouseButtons.Right => "R",
MouseButtons.Middle => "M",
MouseButtons.XButton1 => "X1",
MouseButtons.XButton2 => "X2",
_ => _button.ToString()
};
private bool _down = true;
public bool Down
{
get => _down;
set
{
SetProperty(ref _down, value);
OnPropertyChanged(nameof(DownLabelVisibility));
}
}
private bool _up = true;
public bool Up
{
get => _up;
set
{
SetProperty(ref _up, value);
OnPropertyChanged(nameof(UpLabelVisibility));
}
}
public Visibility DownLabelVisibility => _down ? Visibility.Visible : Visibility.Collapsed;
public Visibility UpLabelVisibility => _up ? Visibility.Visible : Visibility.Collapsed;
public MacroActionMouseButton()
{
Name = "鼠标按键";
ActionType = MacroActionType.MouseButton;
Icon = PathDataGeometry.Mouse;
}
}
public class MacroActionMouseMove : MacroAction
{
public Array AvailableMoveModes => Enum.GetValues(typeof(MousMoveMode));
private MousMoveMode _moveMode = MousMoveMode.Absolute;
public MousMoveMode MoveMode
{
get => _moveMode;
set => SetProperty(ref _moveMode, value);
}
private int _fromX;
public int FromX
{
get => _fromX;
set => SetProperty(ref _fromX, value);
}
private int _fromY;
public int FromY
{
get => _fromY;
set => SetProperty(ref _fromY, value);
}
private int _toX;
public int ToX
{
get => _toX;
set => SetProperty(ref _toX, value);
}
private int _toY;
public int ToY
{
get => _toY;
set => SetProperty(ref _toY, value);
}
private int _steps = 10;
public int Steps
{
get => _steps;
set
{
int step_count = Math.Min(Math.Max(1, value), 100);
SetProperty(ref _steps, step_count);
}
}
[System.Text.Json.Serialization.JsonIgnore]
public ICommand CaptureFromCommand { get; }
[System.Text.Json.Serialization.JsonIgnore]
public ICommand CaptureToCommand { get; }
public MacroActionMouseMove()
{
Name = "鼠标移动";
ActionType = MacroActionType.MouseMove;
Icon = PathDataGeometry.Cursor;
CaptureFromCommand = new RelayCommand(() =>
{
FullScreenMask fullScreenMask = new FullScreenMask();
fullScreenMask.Cursor = System.Windows.Input.Cursors.Cross;
fullScreenMask.ShowDialog();
if (fullScreenMask.DialogResult == true)
{
FromX = fullScreenMask.X;
FromY = fullScreenMask.Y;
}
});
CaptureToCommand = new RelayCommand(() =>
{
FullScreenMask fullScreenMask = new FullScreenMask();
fullScreenMask.Cursor = System.Windows.Input.Cursors.Cross;
fullScreenMask.ShowDialog();
if (fullScreenMask.DialogResult == true)
{
ToX = fullScreenMask.X;
ToY = fullScreenMask.Y;
}
});
}
}
public class MacroActionMouseWheel : MacroAction
{
public Array AvailableWheelDirections => Enum.GetValues(typeof(MouseWheelDirection));
private MouseWheelDirection _wheelDirection = MouseWheelDirection.Up;
public MouseWheelDirection WheelDirection
{
get => _wheelDirection;
set => SetProperty(ref _wheelDirection, value);
}
private int _steps = 10;
public int Steps
{
get => _steps;
set
{
int step_count = Math.Min(Math.Max(1, value), 100);
SetProperty(ref _steps, step_count);
}
}
public MacroActionMouseWheel()
{
Name = "鼠标滚轮";
ActionType = MacroActionType.MouseWheel;
Icon = PathDataGeometry.MouseScroll;
IconShiftX = -2;
}
}
// 延时
public class MacroActionDelay : MacroAction
{
public override int Duration
{
get => _duration;
set
{
int duration_ms = Math.Max(0, value);
SetProperty(ref _duration, duration_ms);
}
}
public MacroActionDelay()
{
Name = "延时";
ActionType = MacroActionType.Delay;
Icon = PathDataGeometry.Right;
}
}
// =======================
// 手柄按键、扳机、摇杆暂不实现
// =======================
public class MacroActionGamepadButtonDown : MacroAction
{
private FIPGamepadButtonflags _button = FIPGamepadButtonflags.None;
public FIPGamepadButtonflags Button
{
get => _button;
set => SetProperty(ref _button, value);
}
public MacroActionGamepadButtonDown()
{
Name = "手柄按下";
Icon = PathDataGeometry.Gamepad;
}
}
public class MacroActionGamepadButtonUp : MacroAction
{
private FIPGamepadButtonflags _button = FIPGamepadButtonflags.None;
public FIPGamepadButtonflags Button
{
get => _button;
set => SetProperty(ref _button, value);
}
public MacroActionGamepadButtonUp()
{
Name = "手柄抬起";
Icon = PathDataGeometry.Gamepad;
}
}
public class MacroActionGamepadButtonPress : MacroAction
{
private FIPGamepadButtonflags _button = FIPGamepadButtonflags.None;
public FIPGamepadButtonflags Button
{
get => _button;
set => SetProperty(ref _button, value);
}
public override int Duration
{
get => _duration;
set => SetProperty(ref _duration, value);
}
public MacroActionGamepadButtonPress()
{
Name = "手柄点击";
Icon = PathDataGeometry.Gamepad;
}
}
public class MacroActionGamepadTrigger : MacroAction
{
private Side _side = Side.Left;
public Side Side
{
get => _side;
set => SetProperty(ref _side, value);
}
private int _value = 0;
public int Value
{
get => _value;
set => SetProperty(ref _value, value);
}
public MacroActionGamepadTrigger()
{
Name = "手柄扳机";
Icon = PathDataGeometry.Gamepad;
}
}
public class MacroActionGamepadTriggerSequence : MacroAction
{
private Side _side = Side.Left;
public Side Side
{
get => _side;
set => SetProperty(ref _side, value);
}
private int _from = 0;
public int From
{
get => _from;
set => SetProperty(ref _from, value);
}
private int _to = 0;
public int To
{
get => _to;
set => SetProperty(ref _to, value);
}
private int _steps = 10;
public int Steps
{
get => _steps;
set
{
int step_count = Math.Min(Math.Max(1, value), 100);
SetProperty(ref _steps, step_count);
}
}
public override int Duration
{
get => _duration;
set => SetProperty(ref _duration, value);
}
public MacroActionGamepadTriggerSequence()
{
Name = "手柄扳机序列";
Icon = PathDataGeometry.Gamepad;
}
}
public class MacroActionGamepadStick : MacroAction
{
private Side _side = Side.Left;
public Side Side
{
get => _side;
set => SetProperty(ref _side, value);
}
private int _x = 0;
public int X
{
get => _x;
set => SetProperty(ref _x, value);
}
private int _y = 0;
public int Y
{
get => _y;
set => SetProperty(ref _y, value);
}
public MacroActionGamepadStick()
{
Name = "手柄摇杆";
Icon = PathDataGeometry.Gamepad;
}
}
public class MacroActionGamepadStickSequence : MacroAction
{
private Side _side = Side.Left;
public Side Side
{
get => _side;
set => SetProperty(ref _side, value);
}
private int _fromX = 0;
public int FromX
{
get => _fromX;
set => SetProperty(ref _fromX, value);
}
private int _fromY = 0;
public int FromY
{
get => _fromY;
set => SetProperty(ref _fromY, value);
}
private int _toX = 0;
public int ToX
{
get => _toX;
set => SetProperty(ref _toX, value);
}
private int _toY = 0;
public int ToY
{
get => _toY;
set => SetProperty(ref _toY, value);
}
private int _steps = 10;
public int Steps
{
get => _steps;
set
{
int step_count = Math.Min(Math.Max(1, value), 100);
SetProperty(ref _steps, step_count);
}
}
public override int Duration
{
get => _duration;
set => SetProperty(ref _duration, value);
}
public MacroActionGamepadStickSequence()
{
Name = "手柄摇杆序列";
Icon = PathDataGeometry.Gamepad;
}
}
@@ -0,0 +1,548 @@
using FancyInput.Models;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Text.Json;
using System.Windows;
using System.Windows.Forms;
using System.Windows.Media;
using ButtonState = FancyInput.Models.ButtonState;
namespace FancyInput.ViewModels
{
public class MacroTrackViewModel : ViewModelBase, IDisposable
{
private string _name = "轨道";
public string Name
{
get => _name;
set => SetProperty(ref _name, value);
}
private int _idx = 0;
public int Idx
{
get => _idx;
set => SetProperty(ref _idx, value);
}
private bool _isSelected = false;
public bool IsSelected
{
get => _isSelected;
set
{
SetProperty(ref _isSelected, value);
OnPropertyChanged(nameof(BorderBrush));
}
}
private MacroAction? _selectedAction;
public MacroAction? SelectedAction
{
get => _selectedAction;
set
{
if (_selectedAction != null)
_selectedAction.IsSelected = false;
if (value != null)
value.IsSelected = true;
SetProperty(ref _selectedAction, value);
OnPropertyChanged(nameof(ActionVisibility));
}
}
public Visibility ActionVisibility => SelectedAction != null ? Visibility.Visible : Visibility.Collapsed;
public Array MacroAddDirectionList => Enum.GetValues(typeof(MacroAddDirection));
private MacroAddDirection _addDirection = MacroAddDirection.Right;
public MacroAddDirection AddDirection
{
get => _addDirection;
set => SetProperty(ref _addDirection, value);
}
public SolidColorBrush BorderBrush => IsSelected ? new SolidColorBrush(Colors.MediumPurple) : new SolidColorBrush(Colors.Gray);
public ObservableCollection<MacroAction> MacroActions { get; set; } = new ObservableCollection<MacroAction>();
private Stopwatch _stopwatch = new Stopwatch();
private int _startTime = 0;
public InputParser? InputParser { get; set; } = null;
private bool _recordMouseMove = false;
public bool RecordMouseMove
{
get => _recordMouseMove;
set => SetProperty(ref _recordMouseMove, value);
}
private int _recordStartIdx = 0;
public int RecordStartIdx
{
get => _recordStartIdx;
set => SetProperty(ref _recordStartIdx, value);
}
private int _recordingIdx = 0;
private bool _isRecording = false;
public bool IsRecording
{
get => _isRecording;
set
{
if (IsDetectable())
{
if (!_isRecording && value)
{
_stopwatch.Restart();
_startTime = 0;
ResetRecordingState();
StartDetecting();
}
else if (_isRecording && !value)
{
_stopwatch.Stop();
StopDetecting();
}
SetProperty(ref _isRecording, value);
}
else if (!_isRecording && value)
{
AppMessageBox.Show("输入解析器未初始化,无法录制输入");
}
}
}
// ===== O(1) 录制状态机 =====
// 键盘:当前按下的键
private FipRawKeys? _recordingDownKey = null;
private MacroActionKeyboardButton? _recordingCurrentKeyAction = null;
// 鼠标按键:当前按下的按钮
private MouseButtons? _recordingDownMouseButton = null;
private MacroActionMouseButton? _recordingCurrentMouseAction = null;
// 鼠标移动:当前是否在移动中
private bool _recordingIsMoving = false;
private MacroActionMouseMove? _recordingCurrentMouseMoveAction = null;
// 鼠标滚轮:当前滚轮方向
private bool _recordingIsWheelingUp = false;
private bool _recordingIsWheelingDown = false;
private MacroActionMouseWheel? _recordingCurrentWheelAction = null;
// 上一个动作(用于 PreWait/PostWait 计算)
private MacroAction? _recordingLastAction = null;
private void ResetRecordingState()
{
_recordingIdx = 0;
_recordingDownKey = null;
_recordingCurrentKeyAction = null;
_recordingDownMouseButton = null;
_recordingCurrentMouseAction = null;
_recordingIsMoving = false;
_recordingCurrentMouseMoveAction = null;
_recordingIsWheelingUp = false;
_recordingIsWheelingDown = false;
_recordingCurrentWheelAction = null;
_recordingLastAction = null;
}
public bool IsDetectable() => InputParser != null;
public void StartDetecting()
{
if (InputParser == null) throw new InvalidOperationException("InputParser is not set, cannot start detecting.");
InputParser.GetInput += GetInput;
}
public void StopDetecting()
{
if (InputParser == null) throw new InvalidOperationException("InputParser is not set, cannot stop detecting.");
InputParser.GetInput -= GetInput;
}
/// <summary>
/// O(1) 实时处理每个输入事件,直接创建/更新 MacroAction
/// </summary>
public void GetInput(object? sender, InputArgs e)
{
int timestamp = (int)_stopwatch.ElapsedMilliseconds;
if (_startTime == 0)
{
_startTime = timestamp;
}
timestamp = timestamp - _startTime;
switch (e.Device)
{
case InputDevice.Keyboard:
FinalizeCurrentWheel();
FinalizeCurrentMove();
HandleKeyboardInput(timestamp, e);
break;
case InputDevice.MouseButton:
FinalizeCurrentWheel();
FinalizeCurrentMove();
HandleMouseButtonInput(timestamp, e);
break;
case InputDevice.MouseMove:
FinalizeCurrentWheel();
if (_recordMouseMove)
HandleMouseMoveInput(timestamp, e);
break;
case InputDevice.MouseWheel:
FinalizeCurrentMove();
HandleMouseWheelInput(timestamp, e);
break;
default:
return;
}
}
private void HandleKeyboardInput(int timestamp, InputArgs e)
{
if (!e.RawKey.HasValue || !e.State.HasValue) return;
FipRawKeys key = e.RawKey.Value;
ButtonState state = e.State.Value;
if (state == ButtonState.Pressed)
{
_recordingDownKey = key;
var keyAction = new MacroActionKeyboardButton
{
Key = key,
Down = true,
Up = false,
StartTime = timestamp,
Duration = 0,
};
_recordingCurrentKeyAction = keyAction;
if (_recordingLastAction != null)
{
MacroAction? delay = SetWaitOrDelay(_recordingLastAction, _recordingCurrentKeyAction);
if (delay != null) AddActionToRecord(delay);
}
AddActionToRecord(keyAction);
}
else if (state == ButtonState.Released && _recordingDownKey == key)
{
_recordingDownKey = null;
if (_recordingCurrentKeyAction != null)
{
_recordingCurrentKeyAction.Up = true;
_recordingCurrentKeyAction.Duration = timestamp - _recordingCurrentKeyAction.StartTime;
}
_recordingCurrentKeyAction = null;
}
else
{
// 单独的释放
_recordingDownKey = null;
_recordingCurrentKeyAction = null;
var keyAction = new MacroActionKeyboardButton
{
Key = key,
Down = false,
Up = true,
StartTime = timestamp,
Duration = 0,
};
if (_recordingLastAction != null)
{
MacroAction? delay = SetWaitOrDelay(_recordingLastAction, keyAction);
if (delay != null) AddActionToRecord(delay);
}
AddActionToRecord(keyAction);
}
}
private void HandleMouseButtonInput(int timestamp, InputArgs e)
{
if (!e.MouseButton.HasValue || !e.State.HasValue) return;
MouseButtons button = e.MouseButton.Value;
ButtonState state = e.State.Value;
if (state == ButtonState.Pressed)
{
_recordingDownMouseButton = button;
var mouseAction = new MacroActionMouseButton
{
Button = button,
Down = true,
Up = false,
StartTime = timestamp,
Duration = 0,
};
_recordingCurrentMouseAction = mouseAction;
if (_recordingLastAction != null)
{
MacroAction? delay = SetWaitOrDelay(_recordingLastAction, _recordingCurrentMouseAction);
if (delay != null) AddActionToRecord(delay);
}
AddActionToRecord(mouseAction);
}
else if (state == ButtonState.Released && _recordingDownMouseButton == button)
{
_recordingDownMouseButton = null;
if (_recordingCurrentMouseAction != null)
{
_recordingCurrentMouseAction.Up = true;
_recordingCurrentMouseAction.Duration = timestamp - _recordingCurrentMouseAction.StartTime;
}
_recordingCurrentMouseAction = null;
}
else
{
_recordingDownMouseButton = null;
_recordingCurrentMouseAction = null;
var mouseAction = new MacroActionMouseButton
{
Button = button,
Down = false,
Up = true,
StartTime = timestamp,
Duration = 0,
};
if (_recordingLastAction != null)
{
MacroAction? delay = SetWaitOrDelay(_recordingLastAction, mouseAction);
if (delay != null) AddActionToRecord(delay);
}
AddActionToRecord(mouseAction);
}
}
private void HandleMouseMoveInput(int timestamp, InputArgs e)
{
if (!e.MoveState.HasValue || e.ValueVector == null || e.ValueVector.Length < 4) return;
MoveState moveState = e.MoveState.Value;
if (moveState == MoveState.StartMove)
{
_recordingIsMoving = true;
var moveAction = new MacroActionMouseMove
{
FromX = e.ValueVector[2],
FromY = e.ValueVector[3],
ToX = e.ValueVector[2],
ToY = e.ValueVector[3],
Steps = 1,
StartTime = timestamp,
Duration = 0,
};
_recordingCurrentMouseMoveAction = moveAction;
if (_recordingLastAction != null)
{
MacroAction? delay = SetWaitOrDelay(_recordingLastAction, moveAction);
if (delay != null) AddActionToRecord(delay);
}
AddActionToRecord(moveAction);
}
else if (moveState == MoveState.StopMove && _recordingIsMoving)
{
_recordingIsMoving = false;
if (_recordingCurrentMouseMoveAction != null)
{
_recordingCurrentMouseMoveAction.ToX = e.ValueVector[2];
_recordingCurrentMouseMoveAction.ToY = e.ValueVector[3];
_recordingCurrentMouseMoveAction.Duration = timestamp - _recordingCurrentMouseMoveAction.StartTime;
double distance = Math.Sqrt(
Math.Pow(_recordingCurrentMouseMoveAction.ToX - _recordingCurrentMouseMoveAction.FromX, 2) +
Math.Pow(_recordingCurrentMouseMoveAction.ToY - _recordingCurrentMouseMoveAction.FromY, 2));
_recordingCurrentMouseMoveAction.Steps = Math.Min(Math.Max((int)(distance / 10), 1), 100);
}
_recordingCurrentMouseMoveAction = null;
}
else if (moveState == MoveState.StopMove)
{
// 单独的停止移动
_recordingIsMoving = false;
_recordingCurrentMouseMoveAction = null;
var moveAction = new MacroActionMouseMove
{
FromX = e.ValueVector[2],
FromY = e.ValueVector[3],
ToX = e.ValueVector[2],
ToY = e.ValueVector[3],
Steps = 1,
StartTime = timestamp,
Duration = 0,
};
if (_recordingLastAction != null)
{
MacroAction? delay = SetWaitOrDelay(_recordingLastAction, moveAction);
if (delay != null) AddActionToRecord(delay);
}
AddActionToRecord(moveAction);
}
}
private void HandleMouseWheelInput(int timestamp, InputArgs e)
{
if (!e.Value.HasValue) return;
int delta = e.Value.Value;
MouseWheelDirection direction = delta > 0 ? MouseWheelDirection.Up : MouseWheelDirection.Down;
if (!_recordingIsWheelingUp && !_recordingIsWheelingDown)
{
// 开始新的滚轮序列
var wheelAction = new MacroActionMouseWheel
{
WheelDirection = direction,
Steps = 1,
StartTime = timestamp,
Duration = 0,
};
_recordingCurrentWheelAction = wheelAction;
_recordingIsWheelingUp = direction == MouseWheelDirection.Up;
_recordingIsWheelingDown = direction == MouseWheelDirection.Down;
if (_recordingLastAction != null)
{
MacroAction? delay = SetWaitOrDelay(_recordingLastAction, wheelAction);
if (delay != null) AddActionToRecord(delay);
}
AddActionToRecord(wheelAction);
}
else if ((_recordingIsWheelingUp && direction == MouseWheelDirection.Up) ||
(_recordingIsWheelingDown && direction == MouseWheelDirection.Down))
{
// 同方向继续滚动,累加步数和时长
if (_recordingCurrentWheelAction != null)
{
_recordingCurrentWheelAction.Steps += 1;
_recordingCurrentWheelAction.Duration = timestamp - _recordingCurrentWheelAction.StartTime;
}
}
else
{
// 方向改变,结束上一个,开始新的
FinalizeCurrentWheel();
var wheelAction = new MacroActionMouseWheel
{
WheelDirection = direction,
Steps = 1,
StartTime = timestamp,
Duration = 0,
};
_recordingCurrentWheelAction = wheelAction;
_recordingIsWheelingUp = direction == MouseWheelDirection.Up;
_recordingIsWheelingDown = direction == MouseWheelDirection.Down;
if (_recordingLastAction != null)
{
MacroAction? delay = SetWaitOrDelay(_recordingLastAction, wheelAction);
if (delay != null) AddActionToRecord(delay);
}
AddActionToRecord(wheelAction);
}
}
/// <summary>
/// 结束当前正在进行的滚轮动作
/// </summary>
private void FinalizeCurrentWheel()
{
_recordingIsWheelingUp = false;
_recordingIsWheelingDown = false;
_recordingCurrentWheelAction = null;
}
private void FinalizeCurrentMove()
{
_recordingIsMoving = false;
_recordingCurrentMouseMoveAction = null;
}
/// <summary>
/// O(1) 添加动作到录制列表,同时更新 UI
/// </summary>
private void AddActionToRecord(MacroAction action)
{
_recordingLastAction = action;
MacroActions.Insert(RecordStartIdx + _recordingIdx, action);
_recordingIdx++;
}
private MacroAction? SetWaitOrDelay(MacroAction lastAction, MacroAction newAction)
{
int interval = newAction.StartTime - lastAction.EndTime;
if (interval >= 5000)
{
var delay = new MacroActionDelay
{
Name = "延时",
ActionType = MacroActionType.Delay,
PreWait = 0,
Duration = interval,
PostWait = 0
};
lastAction.PostWait = 0;
newAction.PreWait = 0;
return delay;
}
else
{
lastAction.PostWait = interval / 2;
newAction.PreWait = interval / 2;
return null;
}
}
public void Dispose()
{
// 停止录制(取消订阅 InputParser.GetInput 事件)
if (_isRecording)
{
IsRecording = false;
}
// 确保事件已取消订阅
if (InputParser != null)
{
InputParser.GetInput -= GetInput;
InputParser = null;
}
_stopwatch.Stop();
}
public MacroTrackViewModelDto ToDto()
{
return new MacroTrackViewModelDto
{
Name = Name,
Idx = Idx,
AddDirection = AddDirection,
RecordMouseMove = RecordMouseMove,
MacroActionDtos = MacroActions.Select(a => a.ToDto()).ToList(),
};
}
public static MacroTrackViewModel FromDto(MacroTrackViewModelDto dto)
{
var vm = new MacroTrackViewModel();
vm.CopyFrom(dto);
return vm;
}
/// <summary>
/// 从 DTO 原地恢复数据到当前实例(保留 InputParser 引用和 UI 绑定)
/// </summary>
public void CopyFrom(MacroTrackViewModelDto dto)
{
Name = dto.Name;
Idx = dto.Idx;
AddDirection = dto.AddDirection;
RecordMouseMove = dto.RecordMouseMove;
MacroActions.Clear();
foreach (var actionDto in dto.MacroActionDtos)
{
MacroActions.Add(MacroAction.FromDto(actionDto));
}
}
}
public class MacroTrackViewModelDto
{
public string Name { get; set; } = "轨道";
public int Idx { get; set; }
public MacroAddDirection AddDirection { get; set; } = MacroAddDirection.Right;
public bool RecordMouseMove { get; set; }
public List<MacroActionDto> MacroActionDtos { get; set; } = new();
}
}
+826
View File
@@ -0,0 +1,826 @@
using FancyInput.Models;
using FancyInput.Views.Controls;
using System.Text.Json;
using System.Threading.Channels;
using System.Windows;
using System.Windows.Documents;
//using System.Windows.Forms;
using System.Windows.Input;
using System.Windows.Media;
using CancellationTokenSource = System.Threading.CancellationTokenSource;
using InputDevice = FancyInput.Models.InputDevice;
namespace FancyInput.ViewModels
{
public enum TriggerCommand
{
Start,
Stop
}
public class MacroViewModel:ViewModelBase,IDisposable
{
public event Action<MacroViewModel, string,OutputLevel>? LogEvent;
private void LogEventHandler(string msg, OutputLevel level)
{
if (_enableLog)
LogEvent?.Invoke(this, msg, level);
}
private void StdOutEventHandler(string msg) => LogEvent?.Invoke(this, msg, OutputLevel.Info);
private void StdErrEventHandler(string msg) => LogEvent?.Invoke(this, msg, OutputLevel.Error);
public MacroViewModel()
{
SelectedTriggerDevice = TriggerDevices.First();
}
public void Dispose()
{
// 1. 先取消订阅 InputParser 事件,防止 StopListenTrigger 期间仍有事件触发
if (InputParser != null)
{
InputParser.GetInput -= InputParser_GetInput;
InputParser.GetInput -= InputParser_MacroCommandTranslator;
}
// 2. 停止宏监听和运行中的宏
StopListenTrigger();
// 3. 清空 InputParser 引用
InputParser = null;
// 4. Dispose 子轨道(停止录制 + 清理事件订阅)
PreMacroTrack.Dispose();
OnMacroTrack.Dispose();
PostMacroTrack.Dispose();
// 5. 清空事件订阅者,防止外部持有引用无法 GC
LogEvent = null;
}
private InputParser? _inputParser;
public InputParser? InputParser
{
get=> _inputParser;
set
{
_inputParser = value;
PreMacroTrack.InputParser = value;
OnMacroTrack.InputParser = value;
PostMacroTrack.InputParser = value;
}
}
private string _name = "宏";
public string Name
{
get => _name;
set => SetProperty(ref _name, value);
}
public int Idx { get; set; } = 0;
private bool _isSelected = false;
public bool IsSelected
{
get => _isSelected;
set => SetProperty(ref _isSelected, value);
}
private bool _enableLog = true;
public bool EnableLog
{
get => _enableLog;
set => SetProperty(ref _enableLog, value);
}
private MacroTrackViewModel? _selectedTrack = null;
public MacroTrackViewModel? SelectedTrack
{
get => _selectedTrack;
set
{
if (_selectedTrack != null)
_selectedTrack.IsSelected = false;
if (value != null)
value.IsSelected = true;
if (_selectedTrack != null && !ReferenceEquals(value, _selectedTrack))
{
_selectedTrack.IsRecording = false;
}
SetProperty(ref _selectedTrack, value);
OnPropertyChanged(nameof(TrackVisibility));
}
}
public Visibility TrackVisibility => SelectedTrack != null ? Visibility.Visible : Visibility.Collapsed;
public List<InputDevice> TriggerDevices { get; set; } = new List<InputDevice>
{
InputDevice.Keyboard,
InputDevice.MouseButton,
InputDevice.XInputButton,
InputDevice.SDLButton
};
private InputDevice _selectedTriggerDevice;
public InputDevice SelectedTriggerDevice
{
get => _selectedTriggerDevice;
set
{
SetProperty(ref _selectedTriggerDevice, value);
switch (value)
{
case InputDevice.Keyboard:
TriggerBindingList = Enum.GetValues(typeof(FipRawKeys));
SelectedTriggerBinding = SelectedTriggerKey;
break;
case InputDevice.MouseButton:
TriggerBindingList = Enum.GetValues(typeof(System.Windows.Forms.MouseButtons));
SelectedTriggerBinding = SelectedMouseButton;
break;
case InputDevice.XInputButton:
case InputDevice.SDLButton:
TriggerBindingList = Enum.GetValues(typeof(FIPGamepadButtonflags));
SelectedTriggerBinding = SelectedGamepadButton;
break;
default:
TriggerBindingList = null;
SelectedTriggerBinding = null;
break;
}
}
}
private Array? _triggerBindingList;
public Array? TriggerBindingList
{
get => _triggerBindingList;
set => SetProperty(ref _triggerBindingList, value);
}
private Enum? _selectedTriggerBinding;
public Enum? SelectedTriggerBinding
{
get => _selectedTriggerBinding;
set => SetProperty(ref _selectedTriggerBinding, value);
}
public static Array TriggerKeyList => Enum.GetValues(typeof(FipRawKeys));
private FipRawKeys _selectedTriggerKey = FipRawKeys.None;
public FipRawKeys SelectedTriggerKey
{
get => _selectedTriggerKey;
set => SetProperty(ref _selectedTriggerKey, value);
}
public static Array MouseButtonList => Enum.GetValues(typeof(System.Windows.Forms.MouseButtons));
private System.Windows.Forms.MouseButtons _selectedMouseButton;
public System.Windows.Forms.MouseButtons SelectedMouseButton
{
get => _selectedMouseButton;
set => SetProperty(ref _selectedMouseButton, value);
}
public static Array GamepadButtonList => Enum.GetValues(typeof(FIPGamepadButtonflags));
private FIPGamepadButtonflags _selectedGamepadButton = Models.FIPGamepadButtonflags.A;
public FIPGamepadButtonflags SelectedGamepadButton
{
get => _selectedGamepadButton;
set => SetProperty(ref _selectedGamepadButton, value);
}
public static Array TriggerTimingList => Enum.GetValues(typeof(TriggerTiming));
private TriggerTiming _selectedTriggerTiming = TriggerTiming.Press;
public TriggerTiming SelectedTriggerTiming
{
get => _selectedTriggerTiming;
set
{
if (_selectedTriggerMode == TriggerMode.Hold && _selectedTriggerTiming== TriggerTiming.Press && value == TriggerTiming.Release)
{
LogEventHandler("触发模式为按住时,触发时机不能设置为释放",OutputLevel.Warn);
System.Windows.Application.Current.Dispatcher.BeginInvoke(
new Action(() => OnPropertyChanged(nameof(SelectedTriggerTiming))));
}
else
SetProperty(ref _selectedTriggerTiming, value);
}
}
public static Array TriggerModeList => Enum.GetValues(typeof(TriggerMode));
private TriggerMode _selectedTriggerMode;
public TriggerMode SelectedTriggerMode
{
get => _selectedTriggerMode;
set
{
if (value == TriggerMode.Hold && _selectedTriggerTiming == TriggerTiming.Release)
{
SelectedTriggerTiming = TriggerTiming.Press;
LogEventHandler("触发模式为按住时,触发时机已自动设置为按下", OutputLevel.Warn);
}
SetProperty(ref _selectedTriggerMode, value);
}
}
public MacroTrackViewModel PreMacroTrack { get; set; } = new() { Name = "轨道1", Idx = 1 };
public MacroTrackViewModel OnMacroTrack { get; set; } = new () { Name = "轨道2", Idx = 2 };
public MacroTrackViewModel PostMacroTrack { get; set; } = new () { Name = "轨道3", Idx = 3 };
private string _description = string.Empty;
public string Description
{
get => _description;
set => SetProperty(ref _description, value);
}
private string _triggerConditionString = string.Empty;
public string TriggerConditionString
{
get => _triggerConditionString;
set => SetProperty(ref _triggerConditionString, value);
}
private bool _isDetectingInput = false;
public bool IsDetectingInput
{
get => _isDetectingInput;
set
{
SetProperty(ref _isDetectingInput, value);
OnPropertyChanged(nameof(DetectBrush));
OnPropertyChanged(nameof(DetectString));
}
}
public SolidColorBrush DetectBrush => IsDetectingInput ? new SolidColorBrush(Colors.Red) : new SolidColorBrush(Color.FromArgb(0xFF, 0x65, 0x1F, 0x65));
public string DetectString => _isDetectingInput ? "检测中..." : "检测";
public ICommand DetectCommand => new RelayCommand(Detect);
public void Detect()
{
if (InputParser == null)
{
AppMessageBox.Show("输入解析器未初始化,无法检测输入");
return;
}
if (_isDetectingInput)
{
IsDetectingInput = false;
InputParser.GetInput -= InputParser_GetInput;
return;
}
IsDetectingInput = true;
InputParser.GetInput -= InputParser_GetInput;
InputParser.GetInput += InputParser_GetInput;
}
private void InputParser_GetInput(object? sender, InputArgs e)
{
if (InputParser == null) return;
switch (e.Device)
{
case InputDevice.Keyboard:
if (e.RawKey.HasValue)
{
SelectedTriggerKey = e.RawKey.Value;
}
break;
case InputDevice.MouseButton:
if (e.MouseButton.HasValue)
{
SelectedMouseButton = e.MouseButton.Value;
}
break;
case InputDevice.XInputButton:
case InputDevice.SDLButton:
if (e.Flag.HasValue)
{
SelectedGamepadButton = e.Flag.Value;
}
break;
default:
return;
}
SelectedTriggerDevice = e.Device;
IsDetectingInput = false;
InputParser.GetInput -= InputParser_GetInput;
}
public ICommand? ShotStartCommand=> new RelayCommand(() => _triggerCommandChannel?.Writer.TryWrite(TriggerCommand.Start));
public ICommand? ShotStopCommand => new RelayCommand(() => _triggerCommandChannel?.Writer.TryWrite(TriggerCommand.Stop));
private bool _isMacroListening = false;
public bool IsMacroListening
{
get => _isMacroListening;
set
{
if (_isMacroListening == value) return;
if (value)
{
if (IsTriggerMatchMacro())
{
AppMessageBox.Show("宏的动作序列中包含触发器的动作,无法启用宏,请修改宏的动作序列或触发器设置");
}
else if (IsMacroTooShort() && (SelectedTriggerMode == TriggerMode.Hold || SelectedTriggerMode == TriggerMode.Toggle))
{
AppMessageBox.Show($"当前触发模式下,宏的总时长不可小于{ShortTimeThreshold}ms,请修改宏的动作序列或触发器设置");
}
else
{
StartListenTrigger();
StartTriggerSource();
}
}
else
{
StopTriggerSource();
StopListenTrigger();
}
OnPropertyChanged(nameof(IsMacroListening));
OnPropertyChanged(nameof(IsEditable));
OnPropertyChanged(nameof(Cursor));
OnPropertyChanged(nameof(Tooltip));
}
}
private void StartTriggerSource()
{
if (InputParser == null) return;
InputParser.GetInput += InputParser_MacroCommandTranslator;
}
private void StopTriggerSource()
{
if (InputParser == null) return;
InputParser.GetInput -= InputParser_MacroCommandTranslator;
}
private void InputParser_MacroCommandTranslator(object? sender, InputArgs e)
{
if (InputParser == null) return;
switch (SelectedTriggerMode)
{
case TriggerMode.Once:
if (IsTriggerMatchInputArgs(e, includeTiming: true))
_triggerCommandChannel?.Writer.TryWrite(TriggerCommand.Start);
break;
case TriggerMode.Hold:
if (SelectedTriggerTiming != TriggerTiming.Press)
{
StdErrEventHandler("内部错误:按住模式只能设置触发时机为按下");
return;
}
if (!IsTriggerMatchInputArgs(e, includeTiming: false)) return;
if (e.State == ButtonState.Pressed)
_triggerCommandChannel?.Writer.TryWrite(TriggerCommand.Start);
else if (e.State == ButtonState.Released)
_triggerCommandChannel?.Writer.TryWrite(TriggerCommand.Stop);
break;
case TriggerMode.Toggle:
if (IsTriggerMatchInputArgs(e, includeTiming: true))
{
if (_isMacroRunning)
_triggerCommandChannel?.Writer.TryWrite(TriggerCommand.Stop);
else
_triggerCommandChannel?.Writer.TryWrite(TriggerCommand.Start);
}
break;
}
}
private bool IsTriggerMatchInputArgs(InputArgs e, bool includeTiming=false)
{
if (SelectedTriggerDevice != e.Device) return false;
bool timingMatch = (includeTiming && SelectedTriggerTiming == TriggerTiming.Press && e.State == ButtonState.Pressed) ||
(includeTiming && SelectedTriggerTiming == TriggerTiming.Release && e.State == ButtonState.Released);
switch (SelectedTriggerDevice)
{
case InputDevice.Keyboard:
return SelectedTriggerKey == e.RawKey && (!includeTiming|| timingMatch);
case InputDevice.MouseButton:
return SelectedMouseButton == e.MouseButton && (!includeTiming || timingMatch);
case InputDevice.XInputButton:
case InputDevice.SDLButton:
return SelectedGamepadButton == e.Flag && (!includeTiming || timingMatch);
default:
return false;
}
}
private bool IsTriggerMactchAction(MacroAction action)
{
switch (action.GetType())
{
case Type t when t == typeof(MacroActionKeyboardButton):
var keyAction = action as MacroActionKeyboardButton;
if (keyAction == null) return false;
bool sameKey = SelectedTriggerDevice == InputDevice.Keyboard && SelectedTriggerKey == keyAction.Key;
bool sameTiming = (SelectedTriggerTiming == TriggerTiming.Press && keyAction.Down) ||
(SelectedTriggerTiming == TriggerTiming.Release && keyAction.Up);
return sameKey && sameTiming;
case Type t when t == typeof(MacroActionMouseButton):
var mouseAction = action as MacroActionMouseButton;
if (mouseAction == null) return false;
bool sameButton = SelectedTriggerDevice == InputDevice.MouseButton && SelectedMouseButton == mouseAction.Button;
bool sameMouseTiming = (SelectedTriggerTiming == TriggerTiming.Press && mouseAction.Down) ||
(SelectedTriggerTiming == TriggerTiming.Release && mouseAction.Up);
return sameButton && sameMouseTiming;
default:
return false;
}
}
private bool IsTriggerMatchMacro()
{
foreach (var action in PreMacroTrack.MacroActions)
{
if (IsTriggerMactchAction(action))
return true;
}
foreach (var action in OnMacroTrack.MacroActions)
{
if (IsTriggerMactchAction(action))
return true;
}
foreach (var action in PostMacroTrack.MacroActions)
{
if (IsTriggerMactchAction(action))
return true;
}
return false;
}
public int ShortTimeThreshold { get; set; } = 200;
private bool IsMacroTooShort()
{
int totalTime = 0;
for (int i = 0; i < PreMacroTrack.MacroActions.Count; i++)
totalTime += PreMacroTrack.MacroActions[i].TotalDuration;
for (int i = 0; i < OnMacroTrack.MacroActions.Count; i++)
totalTime += OnMacroTrack.MacroActions[i].TotalDuration;
for (int i = 0; i < PostMacroTrack.MacroActions.Count; i++)
totalTime += PostMacroTrack.MacroActions[i].TotalDuration;
return totalTime < ShortTimeThreshold;
}
public bool IsEditable => !_isMacroListening;
public Cursor Cursor => _isMacroListening ? Cursors.No : Cursors.Arrow;
public string? Tooltip => IsMacroListening ? "宏正在运行中,无法编辑" : null;
private Channel<TriggerCommand>? _triggerCommandChannel;
private CancellationTokenSource? _listenTriggerCts;
//private Task? _triggerListeningTask;
private CancellationTokenSource? _macroCts;
//private Task? macroTask;
private bool _isMacroRunning = false;
private readonly object _lock = new object();
private void StartListenTrigger()
{
try
{
lock (_lock)
{
if (_isMacroListening) return;
//StopInternal(blocking: true);
_triggerCommandChannel = Channel.CreateUnbounded<TriggerCommand>();
_listenTriggerCts = new CancellationTokenSource();
_ = Task.Factory.StartNew(
ProcessTriggerAsync,
_listenTriggerCts.Token,
TaskCreationOptions.LongRunning,
TaskScheduler.Default
).Unwrap();
_isMacroListening = true;
LogEventHandler("已启用宏:(" + Name + ")", OutputLevel.Success);
}
}
catch (Exception ex)
{
StdErrEventHandler("启用宏时发生异常: " + ex.Message);
}
}
private void StopListenTrigger()
{
try
{
lock (_lock)
{
if (!_isMacroListening) return;
_listenTriggerCts?.Cancel();
_triggerCommandChannel?.Writer.TryComplete();
_isMacroListening = false;
if (_macroCts != null && !_macroCts.IsCancellationRequested )
{
try { _macroCts.Cancel(); }
catch (ObjectDisposedException) { }
}
_isMacroRunning = false;
}
}
catch (Exception ex)
{
StdErrEventHandler("禁用宏时发生异常: " + ex.Message);
}
}
private async Task ProcessTriggerAsync()
{
try
{
var token = _listenTriggerCts!.Token;
var reader = _triggerCommandChannel!.Reader;
await foreach (TriggerCommand cmd in reader.ReadAllAsync(token))
{
if (!_isMacroRunning && cmd == TriggerCommand.Start)
{
_isMacroRunning = true;
_macroCts = new CancellationTokenSource();
var localCts = _macroCts;
var macroToken = _macroCts.Token;
var triggerMode = SelectedTriggerMode;
_ = Task.Run(async () =>
{
try
{
await Play(triggerMode, macroToken);
}
finally
{
_isMacroRunning = false;
localCts?.Dispose();
}
});
}
else if (_isMacroRunning && cmd == TriggerCommand.Stop)
{
// 仅 Cancel,不置 _isMacroRunning = false
// 由 Play() 的 finally 负责重置,避免与新 Start 竞态
_macroCts?.Cancel();
}
else if (_isMacroRunning && cmd == TriggerCommand.Start)
{
LogEventHandler("宏:(" + Name + ")的动作序列正在执行中,请等待执行完成或停止后再尝试启动",OutputLevel.Warn);
}
}
}
catch (OperationCanceledException)
{
LogEventHandler("已禁用宏:(" + Name + ")",OutputLevel.Success);
}
catch
{
LogEventHandler("宏:(" + Name + ")监听触发器时发生异常", OutputLevel.Error);
}
}
private async Task Play( TriggerMode triggerMode, CancellationToken cancellationToken)
{
string workingDirectory = AppDomain.CurrentDomain.BaseDirectory;
List<MacroAction> allMacroActions = new List<MacroAction>();
for (int i = 0; i < PreMacroTrack.MacroActions.Count; i++)
allMacroActions.Add(PreMacroTrack.MacroActions[i]);
for (int i = 0; i < OnMacroTrack.MacroActions.Count; i++)
allMacroActions.Add(OnMacroTrack.MacroActions[i]);
for (int i = 0; i < PostMacroTrack.MacroActions.Count; i++)
allMacroActions.Add(PostMacroTrack.MacroActions[i]);
int resultCode = 0;
switch (triggerMode)
{
case TriggerMode.Once:
for (int i = 0; i < allMacroActions.Count; i++)
{
MacroAction macroAction = allMacroActions[i];
var startTime = DateTime.Now;
resultCode = await macroAction.Run(
stdoutCallback: StdOutEventHandler,
stderrCallback: StdErrEventHandler,
workingDirectory: workingDirectory,
cancellationToken: cancellationToken);
var elapsedTime = DateTime.Now - startTime;
if (resultCode == 0)
LogEventHandler($"({macroAction.Name})已执行,耗时 {elapsedTime.TotalMilliseconds} ms", OutputLevel.Success);
else
break;
}
break;
case TriggerMode.Hold:
case TriggerMode.Toggle:
bool shouldStop = false;
if (allMacroActions.Count == 0)
{
resultCode = -3;
shouldStop = true;
}
int iterCount = 0;
while (!shouldStop)
{
iterCount ++;
for (int i = 0; i < allMacroActions.Count; i++)
{
MacroAction macroAction = allMacroActions[i];
var startTime = DateTime.Now;
resultCode = await macroAction.Run(
stdoutCallback: StdOutEventHandler,
stderrCallback: StdErrEventHandler,
workingDirectory: workingDirectory,
cancellationToken: cancellationToken);
var elapsedTime = DateTime.Now - startTime;
if (resultCode == 0)
LogEventHandler($"第{iterCount}次循环,({macroAction.Name})已执行,耗时 {elapsedTime.TotalMilliseconds} ms", OutputLevel.Success);
else
{
shouldStop = true;
break;
}
}
try
{
if (!shouldStop && !cancellationToken.IsCancellationRequested)
await Task.Delay(10, cancellationToken);
}
catch (OperationCanceledException)
{
shouldStop = true;
resultCode = -3;
}
}
break;
default:
break;
}
switch (resultCode)
{
case 0:
LogEventHandler("宏:(" + Name + ")的动作序列已执行完成", OutputLevel.Success);
break;
case -1:
LogEventHandler("宏:(" + Name + ")的动作序列执行失败", OutputLevel.Error);
break;
case -2:
LogEventHandler("宏:(" + Name + ")的动作序列已取消", OutputLevel.Warn);
break;
case -3:
LogEventHandler("宏:(" + Name + ")的动作序列为空, 已退出", OutputLevel.Success);
break;
}
}
public ICommand SaveCommand => new RelayCommand(Save);
public ICommand LoadCommand => new RelayCommand(Load);
private void Save()
{
try
{
var dialog = new Microsoft.Win32.SaveFileDialog
{
Filter = "宏文件 (*.fipm)|*.fipm|所有文件 (*.*)|*.*",
DefaultExt = ".fipm",
FileName = $"{Name}.fipm",
};
if (dialog.ShowDialog() != true) return;
SaveToFile(dialog.FileName);
}
catch (Exception ex)
{
LogEventHandler($"保存宏失败: {ex.Message}", OutputLevel.Error);
}
}
private void Load()
{
try
{
var dialog = new Microsoft.Win32.OpenFileDialog
{
Filter = "宏文件 (*.fipm)|*.fipm|所有文件 (*.*)|*.*",
DefaultExt = ".fipm",
};
if (dialog.ShowDialog() != true) return;
LoadFromFile(dialog.FileName);
}
catch (Exception ex)
{
LogEventHandler($"加载宏失败: {ex.Message}", OutputLevel.Error);
}
}
public void LoadFromFile(string filePath)
{
try
{
var json = System.IO.File.ReadAllText(filePath);
var dto = JsonSerializer.Deserialize<MacroViewModelDto>(json);
if (dto == null)
{
LogEventHandler("加载失败: 文件内容为空或格式不正确", OutputLevel.Error);
return;
}
CopyFrom(dto);
LogEventHandler($"宏 \"{Name}\" 已从 {filePath} 加载", OutputLevel.Success);
}
catch (Exception ex)
{
LogEventHandler($"加载宏失败: {ex.Message}", OutputLevel.Error);
}
}
public void SaveToFile(string filePath)
{
try
{
var dto = ToDto();
var json = JsonSerializer.Serialize(dto, new JsonSerializerOptions { WriteIndented = true });
System.IO.File.WriteAllText(filePath, json);
LogEventHandler($"宏 \"{Name}\" 已保存到: {filePath}", OutputLevel.Success);
}
catch (Exception ex)
{
LogEventHandler($"保存宏失败: {ex.Message}", OutputLevel.Error);
}
}
/// <summary>
/// 从 DTO 恢复数据到当前实例(保留 InputParser、LogEvent 等运行时引用)
/// </summary>
public void CopyFrom(MacroViewModelDto dto)
{
Name = dto.Name;
Idx = dto.Idx;
Description = dto.Description;
TriggerConditionString = dto.TriggerConditionString;
// 先设具体按键值,再设 SelectedTriggerDevice
// 因为 SelectedTriggerDevice 的 setter 会读取 SelectedTriggerKey/SelectedMouseButton/SelectedGamepadButton
SelectedTriggerKey = dto.SelectedTriggerKey;
SelectedMouseButton = dto.SelectedMouseButton;
SelectedGamepadButton = dto.SelectedGamepadButton;
SelectedTriggerDevice = dto.SelectedTriggerDevice;
SelectedTriggerTiming = dto.SelectedTriggerTiming;
SelectedTriggerMode = dto.SelectedTriggerMode;
ShortTimeThreshold = dto.ShortTimeThreshold;
EnableLog = dto.EnableLog;
// 原地更新轨道数据(保留同一实例引用,不破坏 SelectedTrack 等 UI 绑定)
PreMacroTrack.CopyFrom(dto.PreMacroTrackDto);
OnMacroTrack.CopyFrom(dto.OnMacroTrackDto);
PostMacroTrack.CopyFrom(dto.PostMacroTrackDto);
}
public MacroViewModelDto ToDto()
{
return new MacroViewModelDto
{
Name = Name,
Idx = Idx,
Description = Description,
TriggerConditionString = TriggerConditionString,
SelectedTriggerDevice = SelectedTriggerDevice,
SelectedTriggerKey = SelectedTriggerKey,
SelectedMouseButton = SelectedMouseButton,
SelectedGamepadButton = SelectedGamepadButton,
SelectedTriggerTiming = SelectedTriggerTiming,
SelectedTriggerMode = SelectedTriggerMode,
ShortTimeThreshold = ShortTimeThreshold,
EnableLog = EnableLog,
PreMacroTrackDto = PreMacroTrack.ToDto(),
OnMacroTrackDto = OnMacroTrack.ToDto(),
PostMacroTrackDto = PostMacroTrack.ToDto(),
};
}
public static MacroViewModel FromDto(MacroViewModelDto dto)
{
var vm = new MacroViewModel
{
Name = dto.Name,
Idx = dto.Idx,
Description = dto.Description,
TriggerConditionString = dto.TriggerConditionString,
// 先设具体按键值,再设 SelectedTriggerDevicesetter 会读取它们)
SelectedTriggerKey = dto.SelectedTriggerKey,
SelectedMouseButton = dto.SelectedMouseButton,
SelectedGamepadButton = dto.SelectedGamepadButton,
SelectedTriggerDevice = dto.SelectedTriggerDevice,
SelectedTriggerTiming = dto.SelectedTriggerTiming,
SelectedTriggerMode = dto.SelectedTriggerMode,
ShortTimeThreshold = dto.ShortTimeThreshold,
EnableLog = dto.EnableLog,
PreMacroTrack = MacroTrackViewModel.FromDto(dto.PreMacroTrackDto),
OnMacroTrack = MacroTrackViewModel.FromDto(dto.OnMacroTrackDto),
PostMacroTrack = MacroTrackViewModel.FromDto(dto.PostMacroTrackDto),
};
return vm;
}
}
public class MacroViewModelDto
{
public string Name { get; set; } = "宏";
public int Idx { get; set; }
public string Description { get; set; } = string.Empty;
public string TriggerConditionString { get; set; } = string.Empty;
public InputDevice SelectedTriggerDevice { get; set; } = InputDevice.Keyboard;
public FipRawKeys SelectedTriggerKey { get; set; } = FipRawKeys.None;
public System.Windows.Forms.MouseButtons SelectedMouseButton { get; set; }
public FIPGamepadButtonflags SelectedGamepadButton { get; set; } = FIPGamepadButtonflags.A;
public TriggerTiming SelectedTriggerTiming { get; set; } = TriggerTiming.Press;
public TriggerMode SelectedTriggerMode { get; set; } = TriggerMode.Once;
public int ShortTimeThreshold { get; set; } = 200;
public bool EnableLog { get; set; } = true;
public MacroTrackViewModelDto PreMacroTrackDto { get; set; } = new();
public MacroTrackViewModelDto OnMacroTrackDto { get; set; } = new();
public MacroTrackViewModelDto PostMacroTrackDto { get; set; } = new();
}
}
@@ -0,0 +1,144 @@
using FancyInput.Models;
using FancyInput.Views.Controls;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Forms;
using System.Windows.Input;
using InputDevice = FancyInput.Models.InputDevice;
namespace FancyInput.ViewModels
{
public class MacroWindowViewModel : ViewModelBase,IDisposable
{
public event Action<MacroViewModel?, string, OutputLevel>? LogEvent;
private void StdOutHandler(MacroViewModel? macro, string msg, OutputLevel level) => LogEvent?.Invoke(macro, msg, level);
public MacroWindowViewModel() { }
public InputParser? InputParser { get; set; } = null;
private MacroViewModel? _selectedMacro;
public MacroViewModel? SelectedMacro
{
get => _selectedMacro;
set
{
if (_selectedMacro != null)
_selectedMacro.IsSelected = false;
if (value != null)
value.IsSelected = true;
SetProperty(ref _selectedMacro, value);
OnPropertyChanged(nameof(MacroVisibility));
}
}
public Visibility MacroVisibility => SelectedMacro != null ? Visibility.Visible : Visibility.Collapsed;
public ObservableCollection<MacroViewModel> Macros { get; set; } = new ObservableCollection<MacroViewModel>();
private double _actionsWidth = 600;
public double ActionsWidth
{
get => _actionsWidth;
set => SetProperty(ref _actionsWidth, value);
}
public ICommand AddMacroCommand => new RelayCommand(AddMacro);
private void AddMacro()
{
MacroViewModel macroViewModel = new MacroViewModel();
macroViewModel.Name = $"宏{Macros.Count + 1}";
macroViewModel.Idx = Macros.Count;
macroViewModel.InputParser = InputParser;
macroViewModel.LogEvent += StdOutHandler;
Macros.Add(macroViewModel);
SelectedMacro = macroViewModel;
}
public ICommand LoadMacroFromFolderCommand => new RelayCommand(LoadMacroFromFolder);
private void LoadMacroFromFolder()
{
try
{
using (var dialog = new FolderBrowserDialog())
{
dialog.Description = "选择宏文件夹";
if (dialog.ShowDialog() == DialogResult.OK)
{
string folderPath = dialog.SelectedPath;
LoadMacroFromFolder(folderPath);
}
}
}
catch (Exception ex)
{
StdOutHandler(null, $"加载宏文件夹时发生错误: {ex.Message}", OutputLevel.Error);
}
}
public void LoadMacroFromFolder(string folderPath)
{
try
{
var macroFiles = System.IO.Directory.GetFiles(folderPath, "*.fipm");
foreach (var file in macroFiles)
{
MacroViewModel macroViewModel = new MacroViewModel();
Macros.Add(macroViewModel);
macroViewModel.LogEvent += StdOutHandler;
macroViewModel.InputParser = InputParser;
macroViewModel.LoadFromFile(file);
StdOutHandler(macroViewModel, $"已加载宏 {macroViewModel.Name} 从 {file}", OutputLevel.Info);
}
if (macroFiles.Length == 0)
{
StdOutHandler(null, "没有找到宏文件", OutputLevel.Warn);
}
var lastMacro = Macros.LastOrDefault();
SelectedMacro = lastMacro;
}
catch (Exception ex)
{
StdOutHandler(null, $"加载宏文件夹时发生错误: {ex.Message}", OutputLevel.Error);
}
}
public ICommand SaveAllCommand => new RelayCommand(SaveAll);
private void SaveAll()
{
using (var dialog = new FolderBrowserDialog())
{
dialog.Description = "选择保存宏的文件夹";
if (dialog.ShowDialog() == DialogResult.OK)
{
string folderPath = dialog.SelectedPath;
foreach (var macro in Macros)
{
string filePath = System.IO.Path.Combine(folderPath, $"{macro.Name}.fipm");
macro.SaveToFile(filePath);
StdOutHandler(macro, $"已保存宏 {macro.Name} 到 {filePath}", OutputLevel.Info);
}
if (Macros.Count == 0)
{
StdOutHandler(null, "没有宏可保存", OutputLevel.Warn);
}
}
}
}
public void Dispose()
{
foreach (var item in Macros)
{
item.Dispose();
}
Macros.Clear();
SelectedMacro = null;
InputParser = null;
}
}
}
@@ -0,0 +1,658 @@
using FancyInput.Models;
using FancyInput.Views;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Security.Principal;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Media;
using SaveFileDialog = Microsoft.Win32.SaveFileDialog;
using OpenFileDialog = Microsoft.Win32.OpenFileDialog;
using System.Text.Json.Serialization;
using System.Reflection.Metadata;
namespace FancyInput.ViewModels
{
public enum LoadMode
{
PngJson,
ProjectFile
}
public class MainWindowViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler? PropertyChanged;
protected void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
if (OwnerWindow.IsConfigLoaded)
SaveConfig();
}
protected virtual bool SetProperty<T>(ref T field, T value, [CallerMemberName] string propertyName = "")
{
if (Equals(field, value)) return false;
field = value;
OnPropertyChanged(propertyName);
//PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
return true;
}
public MainWindowViewModel(FancyInputMainWindow window)
{
_ownerWindow = window;
_inputParser = new InputParser();
MacroWindowViewModel.InputParser = _inputParser;
}
private Brush _foregroundColor = Brushes.Black;
public Brush ForegroundColor
{
get => _foregroundColor;
set => SetProperty(ref _foregroundColor, value);
}
private Brush _backgroundColor = Brushes.AliceBlue;
public Brush BackgroundColor
{
get => _backgroundColor;
set => SetProperty(ref _backgroundColor, value);
}
private Brush _borderColor = Brushes.Gray;
public Brush BorderColor
{
get => _borderColor;
set => SetProperty(ref _borderColor, value);
}
private Brush _accentColor = new SolidColorBrush(Color.FromArgb(255, 0x65, 0x1F, 0x65));
public Brush AccentColor
{
get => _accentColor;
set => SetProperty(ref _accentColor, value);
}
private Brush _foreGroundAccentColor = Brushes.White;
public Brush ForeGroundAccentColor
{
get => _foreGroundAccentColor;
set => SetProperty(ref _foreGroundAccentColor, value);
}
private string _macroSavePath = string.Empty;
public string MacroSavePath
{
get => _macroSavePath;
set => SetProperty(ref _macroSavePath, value);
}
private MacroWindowViewModel _macroWindowViewModel = new MacroWindowViewModel();
public MacroWindowViewModel MacroWindowViewModel
{
get => _macroWindowViewModel;
set => SetProperty(ref _macroWindowViewModel, value);
}
private InputParser _inputParser;
public InputParser InputParser => _inputParser;
ObservableCollection<OverlayWindowViewModel> _overlayWindowViewModels = new();
public ObservableCollection<OverlayWindowViewModel> OverlayWindowViewModels
{
get { return _overlayWindowViewModels; }
set { SetProperty(ref _overlayWindowViewModels, value); }
}
private LoadMode _loadMode = LoadMode.PngJson;
public LoadMode LoadMode
{
get => _loadMode;
set => SetProperty(ref _loadMode, value);
}
private MainWindowPanelType _panelType = MainWindowPanelType.Read;
public static Array PanelTypeList => Enum.GetValues(typeof(MainWindowPanelType));
public MainWindowPanelType PanelType
{
get => _panelType;
set
{
SetProperty(ref _panelType, value);
OnPropertyChanged(nameof(SelectedPanelIndex));
OnPropertyChanged(nameof(ReadButtonStyle));
OnPropertyChanged(nameof(ManageButtonStyle));
OnPropertyChanged(nameof(CreateButtonStyle));
OnPropertyChanged(nameof(SettingsButtonStyle));
OnPropertyChanged(nameof(WorkshopButtonStyle));
}
}
public int SelectedPanelIndex
{
get => (int)PanelType;
set
{
if (value >= 0 && value <= PanelTypeList.Length - 1)
{
PanelType = (MainWindowPanelType)value;
}
}
}
public Style ReadButtonStyle => PanelType == MainWindowPanelType.Read ? (Style)OwnerWindow.FindResource("FancySelectedButton") : (Style)OwnerWindow.FindResource("FancyButton");
public Style ManageButtonStyle => PanelType == MainWindowPanelType.Manage ? (Style)OwnerWindow.FindResource("FancySelectedButton") : (Style)OwnerWindow.FindResource("FancyButton");
public Style CreateButtonStyle => PanelType == MainWindowPanelType.Create ? (Style)OwnerWindow.FindResource("FancySelectedButton") : (Style)OwnerWindow.FindResource("FancyButton");
public Style SettingsButtonStyle => PanelType == MainWindowPanelType.Settings ? (Style)OwnerWindow.FindResource("FancySelectedButton") : (Style)OwnerWindow.FindResource("FancyButton");
public Style WorkshopButtonStyle => PanelType == MainWindowPanelType.Workshop ? (Style)OwnerWindow.FindResource("FancySelectedButton") : (Style)OwnerWindow.FindResource("FancyButton");
private string _pngFilePath = string.Empty;
public string PngFilePath
{
get => _pngFilePath;
set => SetProperty(ref _pngFilePath, value);
}
private string _jsonFilePath = string.Empty;
public string JsonFilePath
{
get => _jsonFilePath;
set => SetProperty(ref _jsonFilePath, value);
}
private string _projectFilePath = string.Empty;
public string ProjectFilePath
{
get => _projectFilePath;
set => SetProperty(ref _projectFilePath, value);
}
//已加载信息
public List<LoadedSingleInfo> LoadedSingles { get; set; } = new List<LoadedSingleInfo>();
//Info
private SolidColorBrush _debugCircleFill = new SolidColorBrush(Colors.Green);
public SolidColorBrush DebugCircleFill
{
get => _debugCircleFill;
set => SetProperty(ref _debugCircleFill, value);
}
private String _debugLabelString = "已就绪";
public string DebugLabelString
{
get => _debugLabelString;
set => SetProperty(ref _debugLabelString, value);
}
//选项
public bool UseCenterMouseMove
{
get => InputHandler.USE_CENTER_MOUSEMOVE;
set
{
InputHandler.USE_CENTER_MOUSEMOVE = value;
OnPropertyChanged(nameof(UseCenterMouseMove));
}
}
public bool PreventMouseCentering
{
get => InputParser.PreventMouseCentering;
set
{
InputParser.PreventMouseCentering = value;
OnPropertyChanged(nameof(PreventMouseCentering));
}
}
public int MouseMoveSensitivity
{
get => InputHandler.MOUSE_SENSE;
set
{
InputHandler.MOUSE_SENSE = value;
OnPropertyChanged(nameof(MouseMoveSensitivity));
}
}
public double TimerTickInterval
{
get => InputParser.TimerInterval;
set
{
InputParser.TimerInterval = value;
OnPropertyChanged(nameof(TimerTickInterval));
OnPropertyChanged(nameof(TimerTickIntervalString));
}
}
public string TimerTickIntervalString => $"{TimerTickInterval:F1}";
private bool _detectInput = false;
public bool DetectInput
{
get => _detectInput;
set => SetProperty(ref _detectInput, value);
}
private string _inputDetectString = "";
public string InputDetectString
{
get => _inputDetectString;
set => SetProperty(ref _inputDetectString, value);
}
public string AdminRestartArg => "--restart-as-admin";
public string AdminRestartArgs => LoadMode == LoadMode.ProjectFile && ProjectFilePath != string.Empty ?
$"{AdminRestartArg} {ProjectFilePath}" : AdminRestartArg;
private bool _runAsAdmin = false;
public bool RunAsAdmin
{
get => _runAsAdmin;
set
{
if (_runAsAdmin != value)
{
_runAsAdmin = value;
OnPropertyChanged(nameof(RunAsAdmin));
//FancyInput.AppMessageBox.Show($"{value} {IsRunAsAdmin()} {OwnerWindow.IsConfigLoaded}");
if (value && !IsRunAsAdmin() && OwnerWindow.IsConfigLoaded)
{
var result = FancyInput.AppMessageBox.Show("是否以管理员身份重新启动?", "需要管理员权限", MessageBoxButton.YesNo, MessageBoxImage.Question);
if (result == MessageBoxResult.Yes)
{
string arguments = AdminRestartArg;
var processInfo = new ProcessStartInfo
{
FileName = Process.GetCurrentProcess().MainModule!.FileName,
UseShellExecute = true,
Arguments = arguments
};
Process.Start(processInfo);
}
else
{
_runAsAdmin = false;
OnPropertyChanged(nameof(RunAsAdmin));
}
}
}
}
}
private bool _minimizeToTray = false;
public bool MinimizeToTray
{
get => _minimizeToTray;
set => SetProperty(ref _minimizeToTray, value);
}
private Visibility _trayIconVisibility = Visibility.Visible;
public Visibility TrayIconVisibility
{
get => _trayIconVisibility;
set => SetProperty(ref _trayIconVisibility, value);
}
public Array KeyBoardMappingList => Enum.GetValues(typeof(KeyBoardMappingType));
private KeyBoardMappingType _selectedKeyBoardMapping = KeyBoardMappingType.Windows;
public KeyBoardMappingType SelectedKeyBoardMapping
{
get => _selectedKeyBoardMapping;
set
{
SetProperty(ref _selectedKeyBoardMapping, value);
InputParser.KeyBoardMappingType = value;
OnPropertyChanged(nameof(UseNumpadAsArrowVisibility));
}
}
public Visibility UseNumpadAsArrowVisibility => SelectedKeyBoardMapping == KeyBoardMappingType.Windows ? Visibility.Visible : Visibility.Collapsed;
private bool _useNumpadAsArrow = false;
public bool UseNumpadAsArrow
{
get => _useNumpadAsArrow;
set
{
SetProperty(ref _useNumpadAsArrow, value);
InputParser.NumPadAsArrow = value;
}
}
// GamepadBackEnd
public Array GamepadBackEndList => Enum.GetValues(typeof(GamepadBackend));
private GamepadBackend _selectedGamepadBackEnd = GamepadBackend.SDL;
public GamepadBackend SelectedGamepadBackEnd
{
get => _selectedGamepadBackEnd;
set
{
SetProperty(ref _selectedGamepadBackEnd, value);
InputParser.SetGamepadBackend(SelectedGamepadBackEnd);
}
}
//更新
private string _currentVersion = string.Empty;
public string CurrentVersion
{
get => _currentVersion;
set => SetProperty(ref _currentVersion, value);
}
private string[] _launchUpdateIgnoreVersions = new string[] { };
public string[] LaunchUpdateIgnoreVersions
{
get => _launchUpdateIgnoreVersions;
set => SetProperty(ref _launchUpdateIgnoreVersions, value);
}
private FancyInputMainWindow _ownerWindow;
public FancyInputMainWindow OwnerWindow
{
get => _ownerWindow;
set => SetProperty(ref _ownerWindow, value);
}
public static bool IsRunAsAdmin()
{
using var identity = WindowsIdentity.GetCurrent();
var principal = new WindowsPrincipal(identity);
return principal.IsInRole(WindowsBuiltInRole.Administrator);
}
public void UpMoveAt(int idx)
{
if (idx > 0 && idx < OverlayWindowViewModels.Count)
{
var temp = OverlayWindowViewModels[idx - 1];
OverlayWindowViewModels[idx - 1] = OverlayWindowViewModels[idx];
OverlayWindowViewModels[idx] = temp;
RefreshOverlayIndices();
}
RefreshTopMost();
}
public void DownMoveAt(int idx)
{
if (idx >= 0 && idx < OverlayWindowViewModels.Count - 1)
{
var temp = OverlayWindowViewModels[idx + 1];
OverlayWindowViewModels[idx + 1] = OverlayWindowViewModels[idx];
OverlayWindowViewModels[idx] = temp;
RefreshOverlayIndices();
}
RefreshTopMost();
}
public void RefreshOverlayIndices()
{
int gamepadIdx = 0;
for (int i = 0; i < OverlayWindowViewModels.Count; i++)
{
OverlayWindowViewModels[i].Idx = i;
if (OverlayWindowViewModels[i].ElementTreeViewModel.IsGamepad)
{
if (gamepadIdx < Enum.GetValues(typeof(GamepadUserIdx)).Length)
{
OverlayWindowViewModels[i].ElementTreeViewModel.GamepadUserIndex = (GamepadUserIdx)gamepadIdx;
gamepadIdx++;
}
else
{
OverlayWindowViewModels[i].ElementTreeViewModel.GamepadUserIndex = GamepadUserIdx.Four;
}
}
}
}
public void RefreshTopMost()
{
foreach (var vm in OverlayWindowViewModels)
{
bool shouldBeTopMost = vm.IsTopMost;
vm.IsTopMost = !shouldBeTopMost;
vm.IsTopMost = shouldBeTopMost;
}
}
private static string GetConfigPath()
{
string appDataDir = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
string configDir = Path.Combine(appDataDir, "FancyInput");
if (!Directory.Exists(configDir))
Directory.CreateDirectory(configDir);
return Path.Combine(configDir, "config.json");
}
public void InitAdminState()
{
if (IsRunAsAdmin())
{
_runAsAdmin = true;
OnPropertyChanged(nameof(RunAsAdmin));
}
else
{
_runAsAdmin = false;
OnPropertyChanged(nameof(RunAsAdmin));
}
}
public void SaveConfig()
{
var config = new FancyInputConfig
{
PngFilePath = this.PngFilePath,
JsonFilePath = this.JsonFilePath,
ProjectFilePath = this.ProjectFilePath,
LoadMode = this.LoadMode,
DetectInput = this.DetectInput,
UseCenterMouseMove = this.UseCenterMouseMove,
PreventMouseCentering = this.PreventMouseCentering,
MouseMoveSensitivity = this.MouseMoveSensitivity,
TimerTickInterval = this.TimerTickInterval,
CurrentVersion = this.CurrentVersion,
LaunchUpdateIgnoreVersions = this.LaunchUpdateIgnoreVersions,
RunAsAdmin = this.RunAsAdmin,
MinimizeToTray = this.MinimizeToTray,
KeyBoardMappingType = this.SelectedKeyBoardMapping,
UseNumPadAsArrow = this.UseNumpadAsArrow,
GamepadBackEnd = this.SelectedGamepadBackEnd,
//Workshop相关配置
MacroSavePath = this.MacroSavePath,
};
string json = JsonSerializer.Serialize(config, new JsonSerializerOptions { WriteIndented = true });
File.WriteAllText(GetConfigPath(), json);
}
public void LoadConfig()
{
string path = GetConfigPath();
if (File.Exists(path))
{
try
{
var config = JsonSerializer.Deserialize<FancyInputConfig>(File.ReadAllText(path));
if (config != null)
{
this.PngFilePath = config.PngFilePath;
this.JsonFilePath = config.JsonFilePath;
this.ProjectFilePath = config.ProjectFilePath;
this.LoadMode = config.LoadMode;
this.DetectInput = config.DetectInput;
this.UseCenterMouseMove = config.UseCenterMouseMove;
this.PreventMouseCentering = config.PreventMouseCentering;
this.MouseMoveSensitivity = config.MouseMoveSensitivity;
this.TimerTickInterval = config.TimerTickInterval;
this.CurrentVersion = config.CurrentVersion;
this.LaunchUpdateIgnoreVersions = config.LaunchUpdateIgnoreVersions;
this.RunAsAdmin = config.RunAsAdmin;
this.MinimizeToTray = config.MinimizeToTray;
this.SelectedKeyBoardMapping = config.KeyBoardMappingType;
this.UseNumpadAsArrow = config.UseNumPadAsArrow;
this.SelectedGamepadBackEnd = config.GamepadBackEnd;
//Workshop相关配置
this.MacroSavePath = config.MacroSavePath;
}
}
catch { /* 可加日志或提示 */ }
}
}
public void SaveLoadedGroup()
{
try
{
if (OverlayWindowViewModels.Count==0)
throw new Exception("加载至少一个叠加窗口配置,才能保存叠加组文件。");
LoadedSingles.Clear();
SaveFileDialog saveFileDialog = new SaveFileDialog();
saveFileDialog.Filter = "FancyInput Group Files (*.fips)|*.fips";
if (saveFileDialog.ShowDialog() == true)
{
string savePath = saveFileDialog.FileName;
bool saveRawData = FancyInput.AppMessageBox.Show(
"是否将原始数据一并保存到叠加组文件中?",
"保存选项",
MessageBoxButton.YesNo,
MessageBoxImage.Question
) == MessageBoxResult.Yes;
for (int i = 0; i < OverlayWindowViewModels.Count; i++)
{
LoadedSingleInfo singleInfo = new LoadedSingleInfo()
{
LoadMode = OverlayWindowViewModels[i].ElementTreeViewModel.LoadMode,
PngFilePath = OverlayWindowViewModels[i].ElementTreeViewModel.PngFilePath,
JsonFilePath = OverlayWindowViewModels[i].ElementTreeViewModel.JsonFilePath,
ProjectFilePath = OverlayWindowViewModels[i].ElementTreeViewModel.ProjectFilePath,
InitialWidth = OverlayWindowViewModels[i].InitialWidth,
InitialHeight = OverlayWindowViewModels[i].InitialHeight,
IsLocationFixed = OverlayWindowViewModels[i].IsLocationFixed,
IsTopMost = OverlayWindowViewModels[i].IsTopMost,
IsHide = OverlayWindowViewModels[i].IsHide,
IsRemoveYellowBorder = OverlayWindowViewModels[i].IsRemoveYellowBorder,
Scale = OverlayWindowViewModels[i].Scale,
Left = OverlayWindowViewModels[i].Left,
Top = OverlayWindowViewModels[i].Top,
ElementTreeViewModelDto = saveRawData ? ElementTreeViewModel.ToDto(OverlayWindowViewModels[i].ElementTreeViewModel) : null
};
LoadedSingles.Add(singleInfo);
}
var options = new JsonSerializerOptions
{
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
WriteIndented = !saveRawData
};
string json = JsonSerializer.Serialize(LoadedSingles, options);
File.WriteAllText(savePath, json);
FancyInput.AppMessageBox.Show("保存成功!", "成功", MessageBoxButton.OK, MessageBoxImage.Information);
}
}
catch (Exception ex)
{
FancyInput.AppMessageBox.Show($"保存失败:{ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
public void AddLoadedGroup()
{
OpenFileDialog openFileDialog = new OpenFileDialog();
openFileDialog.Filter = "FancyInput Group Files (*.fips)|*.fips";
if (openFileDialog.ShowDialog() == true)
{
string openPath = openFileDialog.FileName;
OpenFips(openPath);
}
}
public void OpenFips(string fipsFilePath)
{
try
{
//var bytes = File.ReadAllBytes(openPath);
//var loadedSingles = System.Text.Json.JsonSerializer.Deserialize<List<LoadedSingleInfo>>(bytes);
var json = File.ReadAllText(fipsFilePath);
var loadedSingles = System.Text.Json.JsonSerializer.Deserialize<List<LoadedSingleInfo>>(json);
if (loadedSingles != null)
{
foreach (var single in loadedSingles)
{
OverlayWindowViewModel? vm;
if (single.ElementTreeViewModelDto == null)
{
LoadMode = single.LoadMode;
PngFilePath = single.PngFilePath;
JsonFilePath = single.JsonFilePath;
ProjectFilePath = single.ProjectFilePath;
vm = OwnerWindow.LoadConfig();
}
else
{
ElementTreeViewModel elementTreeViewModel = ElementTreeViewModel.FromDto(single.ElementTreeViewModelDto);
vm = OwnerWindow.LoadElementTreeViewModel(elementTreeViewModel);
elementTreeViewModel.LoadMode = single.LoadMode;
elementTreeViewModel.PngFilePath = single.PngFilePath;
elementTreeViewModel.JsonFilePath = single.JsonFilePath;
elementTreeViewModel.ProjectFilePath = single.ProjectFilePath;
}
if (vm == null) break;
vm.IsLocationFixed = single.IsLocationFixed;
vm.IsTopMost = single.IsTopMost;
vm.IsHide = single.IsHide;
vm.IsRemoveYellowBorder = single.IsRemoveYellowBorder;
vm.InitialWidth = single.InitialWidth;
vm.InitialHeight = single.InitialHeight;
vm.Scale = single.Scale;
vm.Left = single.Left;
vm.Top = single.Top;
}
RefreshOverlayIndices();
}
}
catch
{
FancyInput.AppMessageBox.Show("加载失败,文件可能已损坏或格式不正确。", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
}
public class FancyInputConfig
{
public string PngFilePath { get; set; } = string.Empty;
public string JsonFilePath { get; set; } = string.Empty;
public string ProjectFilePath { get; set; } = string.Empty;
public LoadMode LoadMode { get; set; } = LoadMode.PngJson;
public bool DetectInput { get; set; } = false;
public bool UseCenterMouseMove { get; set; } = false;
public bool PreventMouseCentering { get; set; } = false;
public int MouseMoveSensitivity { get; set; } = 10;
public double TimerTickInterval { get; set; } = 16.67;
public string CurrentVersion { get; set; } = string.Empty;
public string[] LaunchUpdateIgnoreVersions { get; set; } = new string[] { };
public bool RunAsAdmin { get; set; } = false;
public bool MinimizeToTray { get; set; } = false;
public KeyBoardMappingType KeyBoardMappingType { get; set; } = KeyBoardMappingType.Windows;
public bool UseNumPadAsArrow { get; set; } = false;
public GamepadBackend GamepadBackEnd { get; set; } = GamepadBackend.SDL;
//Workshop相关配置
public string MacroSavePath { get; set; } = string.Empty;
}
public class LoadedSingleInfo
{
public LoadMode LoadMode { get; set; } = LoadMode.PngJson;
public string PngFilePath { get; set; } = string.Empty;
public string JsonFilePath { get; set; } = string.Empty;
public string ProjectFilePath { get; set; } = string.Empty;
public double InitialWidth { get; set; } = 800;
public double InitialHeight { get; set; } = 800;
public double Scale { get; set; } = 0;
public bool IsLocationFixed { get; set; } = false;
public bool IsTopMost { get; set; } = true;
public bool IsHide { get; set; } = false;
public bool IsRemoveYellowBorder { get; set; } = false;
public double Left { get; set; } = 0;
public double Top { get; set; } = 0;
public ElementTreeViewModelDto? ElementTreeViewModelDto { get; set; } = null;
}
}
@@ -0,0 +1,295 @@
using FancyInput.Views;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Diagnostics;
using System.Drawing;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Media;
using System.Windows.Threading;
using Brush = System.Windows.Media.Brush;
using Color = System.Windows.Media.Color;
namespace FancyInput.ViewModels
{
public class OverlayWindowViewModel:INotifyPropertyChanged
{
private const double GeometryEpsilon = 0.5;
private bool _lastRemoveYellowBorderMode = false;
public event PropertyChangedEventHandler? PropertyChanged;
protected void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
protected virtual bool SetProperty<T>(ref T field, T value, [CallerMemberName] string propertyName = "")
{
if (Equals(field, value)) return false;
field = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
return true;
}
public const double DEFAULT_MARGIN = 10;
private bool _isRemoveYellowBorder = false;
public bool IsRemoveYellowBorder
{
get => _isRemoveYellowBorder;
set
{
SetProperty(ref _isRemoveYellowBorder, value);
UpdateWindowSize();
}
}
private void UpdateWindowSize()
{
bool modeChanged = _lastRemoveYellowBorderMode != _isRemoveYellowBorder;
double targetMargin;
double targetLeft;
double targetTop;
double targetWidth;
double targetHeight;
if (_isRemoveYellowBorder)
{
targetMargin = DEFAULT_MARGIN;
targetLeft = -targetMargin;
targetTop = -targetMargin;
targetWidth = _windowWidth + targetMargin * 2;
targetHeight = _windowHeight + targetMargin * 2;
}
else
{
targetMargin = 0;
targetLeft = Left;
targetTop = Top;
targetWidth = CurrentWidth;
targetHeight = CurrentHeight;
}
_margin = targetMargin;
OnPropertyChanged(nameof(Margin));
OnPropertyChanged(nameof(CurrentHeight));
OnPropertyChanged(nameof(CurrentWidth));
OnPropertyChanged(nameof(ConditionalLeft));
OnPropertyChanged(nameof(ConditionalTop));
if (modeChanged)
{
OverlayWindow.Opacity = 0;
}
if (Math.Abs(OverlayWindow.Width - targetWidth) > GeometryEpsilon)
{
OverlayWindow.Width = targetWidth;
}
if (Math.Abs(OverlayWindow.Height - targetHeight) > GeometryEpsilon)
{
OverlayWindow.Height = targetHeight;
}
if (Math.Abs(OverlayWindow.Left - targetLeft) > GeometryEpsilon)
{
OverlayWindow.Left = targetLeft;
}
if (Math.Abs(OverlayWindow.Top - targetTop) > GeometryEpsilon)
{
OverlayWindow.Top = targetTop;
}
if (modeChanged)
{
OverlayWindow.Dispatcher.BeginInvoke(DispatcherPriority.Render, new Action(() =>
{
OverlayWindow.Opacity = 1;
}));
}
_lastRemoveYellowBorderMode = _isRemoveYellowBorder;
}
private double _windowWidth = 300;
public double WindowWidth
{
get => _windowWidth;
set
{
SetProperty(ref _windowWidth, value);
UpdateWindowSize();
}
}
private double _windowHeight = 300;
public double WindowHeight
{
get => _windowHeight;
set
{
SetProperty(ref _windowHeight, value);
UpdateWindowSize();
}
}
private double _margin = DEFAULT_MARGIN;
public double Margin
{
get => _margin;
set
{
SetProperty(ref _margin, value);
UpdateWindowSize();
}
}
private int _idx = 0;
public int Idx
{
get => _idx;
set => SetProperty(ref _idx, value);
}
private bool _isClosed = false;
public bool IsClosed
{
get => _isClosed;
set => SetProperty(ref _isClosed, value);
}
private bool _isLocationFixed = false;
public bool IsLocationFixed
{
get => _isLocationFixed;
set
{
SetProperty(ref _isLocationFixed, value);
OnPropertyChanged(nameof(IsHitTestVisible));
OnPropertyChanged(nameof(CanvasBackground));
}
}
public bool IsHitTestVisible => !IsLocationFixed;
public Brush CanvasBackground => IsLocationFixed ? new SolidColorBrush(Colors.Transparent) : new SolidColorBrush(Color.FromArgb(1,0, 0, 0));
private bool _isTopMost = true;
public bool IsTopMost
{
get => _isTopMost;
set => SetProperty(ref _isTopMost, value);
}
private bool _isHide = false;
public bool IsHide
{
get => _isHide;
set
{
SetProperty(ref _isHide, value);
OnPropertyChanged(nameof(CurrentVisibility));
}
}
//public WindowState CurrentWindowState => IsHide ? WindowState.Minimized : WindowState.Normal;
public Visibility CurrentVisibility => IsHide ? Visibility.Collapsed : Visibility.Visible;
private double _scale = 0;
public double Scale
{
get => _scale;
set
{
SetProperty(ref _scale, value);
OnPropertyChanged(nameof(CurrentHeight));
OnPropertyChanged(nameof(CurrentWidth));
if (!_isRemoveYellowBorder)
{
OverlayWindow.Width = CurrentWidth;
OverlayWindow.Height = CurrentHeight;
}
}
}
public double ConditionalLeft => IsRemoveYellowBorder ? Left : 0;
public double ConditionalTop => IsRemoveYellowBorder ? Top : 0;
private double _left = 0;
public double Left
{
get => _left;
set
{
SetProperty(ref _left, value);
OnPropertyChanged(nameof(ConditionalLeft));
if (!_isRemoveYellowBorder)
{
OverlayWindow.Left = value;
}
}
}
private double _top = 0;
public double Top
{
get => _top;
set
{
SetProperty(ref _top, value);
OnPropertyChanged(nameof(ConditionalTop));
if (!_isRemoveYellowBorder)
{
OverlayWindow.Top = value;
}
}
}
private double _initialHeight =100;
private double _initialWidth = 100;
public double InitialHeight
{
get => _initialHeight;
set
{
SetProperty(ref _initialHeight, value);
OnPropertyChanged(nameof(CurrentHeight));
}
}
public double InitialWidth
{
get => _initialWidth;
set
{
SetProperty(ref _initialWidth, value);
OnPropertyChanged(nameof(CurrentWidth));
}
}
public double CurrentHeight => InitialHeight * Math.Pow(2, Scale);
public double CurrentWidth => InitialWidth * Math.Pow(2, Scale);
private ElementTreeViewModel _elementTreeViewModel;
public ElementTreeViewModel ElementTreeViewModel
{
get => _elementTreeViewModel;
set => SetProperty(ref _elementTreeViewModel, value);
}
private OverlayWindow _overlayWindow;
public OverlayWindow OverlayWindow => _overlayWindow;
public OverlayWindowViewModel(OverlayWindow overlayWindow, ElementTreeViewModel elementTreeViewModel)
{
_overlayWindow = overlayWindow;
_elementTreeViewModel = elementTreeViewModel;
}
}
}
+356
View File
@@ -0,0 +1,356 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Input;
using System.IO;
using System.Windows.Media;
using FancyInput.Models;
namespace FancyInput.ViewModels
{
public class TerminalViewModel : ViewModelBase
{
private int _fontSize = 15;
public int FontSize
{
get => _fontSize;
set
{
int newValue = Math.Min(Math.Max(value, 10), 30);
SetProperty(ref _fontSize, newValue);
}
}
private SolidColorBrush _backgroundColor = new SolidColorBrush(Colors.Black);
public SolidColorBrush BackgroundColor
{
get => _backgroundColor;
set => SetProperty(ref _backgroundColor, value);
}
private SolidColorBrush _foregroundColor = new SolidColorBrush(Colors.White);
public SolidColorBrush ForegroundColor
{
get => _foregroundColor;
set => SetProperty(ref _foregroundColor, value);
}
public CancellationTokenSource CancellationTokenSource { get; set; } = new CancellationTokenSource();
public ObservableCollection<TerminalCommandViewModel> Commands { get; set; } = new ObservableCollection<TerminalCommandViewModel>();
private TerminalCommandViewModel? _currentCommandViewModel;
public TerminalCommandViewModel? CurrentCommandViewModel
{
get => _currentCommandViewModel;
set => SetProperty(ref _currentCommandViewModel, value);
}
public Array BackEndList => Enum.GetValues(typeof(CommandBackEnd));
private CommandBackEnd _backEnd = CommandBackEnd.Cmd;
public CommandBackEnd BackEnd
{
get => _backEnd;
set
{
if (CurrentCommandViewModel != null)
CurrentCommandViewModel.BackEnd = value;
SetProperty(ref _backEnd, value);
}
}
public TerminalViewModel()
{
InitializeNewCommand();
ClearCommand = new RelayCommand(() =>
{
if (_currentCommandViewModel != null)
_currentCommandViewModel.CancelCommand();
Commands.Clear();
InitializeNewCommand();
});
}
private void InitializeNewCommand()
{
TerminalCommandViewModel firstCmdViewModel = new TerminalCommandViewModel();
firstCmdViewModel.WorkingDirectory = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
firstCmdViewModel.BackEnd = BackEnd;
firstCmdViewModel.CommandExecuted += AddNewCommand;
firstCmdViewModel.StdoutUpdated += StdOutErrUpdated;
firstCmdViewModel.StdErrUpdated += StdOutErrUpdated;
CurrentCommandViewModel = firstCmdViewModel;
Commands.Add(firstCmdViewModel);
}
private void AddNewCommand(TerminalCommandViewModel sender)
{
CurrentCommandViewModel!.CommandExecuted -= AddNewCommand;
CurrentCommandViewModel.StdoutUpdated -= StdOutErrUpdatedHanler;
CurrentCommandViewModel.StdErrUpdated -= StdOutErrUpdatedHanler;
TerminalCommandViewModel newCmdViewModel = new TerminalCommandViewModel();
string workDir = sender.WorkingDirectory ?? Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
newCmdViewModel.WorkingDirectory = workDir;
newCmdViewModel.BackEnd = BackEnd;
newCmdViewModel.CommandExecuted += AddNewCommand;
newCmdViewModel.StdoutUpdated += StdOutErrUpdatedHanler;
newCmdViewModel.StdErrUpdated += StdOutErrUpdatedHanler;
CurrentCommandViewModel = newCmdViewModel;
Commands.Add(newCmdViewModel);
}
public event Action<TerminalCommandViewModel>? StdOutErrUpdated;
private void StdOutErrUpdatedHanler(TerminalCommandViewModel sender)=> StdOutErrUpdated?.Invoke(sender);
private ICommand? _clearCommand;
public ICommand? ClearCommand
{
get => _clearCommand;
set => SetProperty(ref _clearCommand, value);
}
}
public class TerminalCommandViewModel : ViewModelBase
{
private string _name = string.Empty;
public string Name
{
get => _name;
set => SetProperty(ref _name, value);
}
private string _workingDirectory = string.Empty;
public string WorkingDirectory
{
get => _workingDirectory;
set
{
SetProperty(ref _workingDirectory, value);
OnPropertyChanged(nameof(WorkingDirectoryShort));
}
}
public string WorkingDirectoryShort
{
get
{
if (WorkingDirectory.Length > 20)
{
// 显示前 5 个字符 + "..." + 后 15 个字符
string start = WorkingDirectory.Substring(0, 5);
string end = WorkingDirectory.Substring(WorkingDirectory.Length - 15);
return "FIP "+ start + "..."+ end + _prompt+ " ";
}
else
{
return "FIP " + WorkingDirectory + _prompt + " ";
}
}
}
private string _prompt = ">";
public string Prompt
{
get => _prompt;
set => SetProperty(ref _prompt, value);
}
private string _command = string.Empty;
public string Command
{
get => _command;
set => SetProperty(ref _command, value);
}
private CommandBackEnd _backEnd = CommandBackEnd.Cmd;
public CommandBackEnd BackEnd
{
get => _backEnd;
set => SetProperty(ref _backEnd, value);
}
private bool _isReadOnly = false;
public bool IsReadOnly
{
get => _isReadOnly;
set => SetProperty(ref _isReadOnly, value);
}
private string _stdout = string.Empty;
public string Stdout
{
get => _stdout;
set
{
SetProperty(ref _stdout, value);
OnPropertyChanged(nameof(StdoutVisibility));
}
}
public Visibility StdoutVisibility => string.IsNullOrEmpty(Stdout) ? Visibility.Collapsed : Visibility.Visible;
private string _stderr = string.Empty;
public string StdErr
{
get => _stderr;
set
{
SetProperty(ref _stderr, value);
OnPropertyChanged(nameof(StdErrVisibility));
}
}
public Visibility StdErrVisibility => string.IsNullOrEmpty(StdErr) ? Visibility.Collapsed : Visibility.Visible;
private ICommand? _executeCommand;
public ICommand? ExecuteCommand
{
get => _executeCommand;
set => SetProperty(ref _executeCommand, value);
}
private void StdoutWriteLine(string text)
{
if (string.IsNullOrEmpty(Stdout))
{
Stdout = text;
}
else
{
Stdout += Environment.NewLine + text;
}
StdoutUpdated?.Invoke(this);
}
private void StdErrWriteLine(string text)
{
if (string.IsNullOrEmpty(StdErr))
{
StdErr = text;
}
else
{
StdErr += Environment.NewLine + text;
}
StdErrUpdated?.Invoke(this);
}
public event Action<TerminalCommandViewModel>? CommandExecuted;
public event Action<TerminalCommandViewModel>? StdoutUpdated;
public event Action<TerminalCommandViewModel>? StdErrUpdated;
private CancellationTokenSource? _cts;
public void CancelCommand() => _cts?.Cancel();
private async void Execute()
{
try
{
if (string.IsNullOrEmpty(Command)) return;
_cts?.Cancel();
_cts?.Dispose();
_cts = new CancellationTokenSource();
// 检测是否是 cd/chdir 内置命令,在进程内切换目录而不启动外部进程
string trimmedCommand = Command.TrimStart();
bool isCd = trimmedCommand.StartsWith("cd ", StringComparison.OrdinalIgnoreCase) ||
trimmedCommand.Equals("cd", StringComparison.OrdinalIgnoreCase) ||
trimmedCommand.StartsWith("chdir ", StringComparison.OrdinalIgnoreCase) ||
trimmedCommand.Equals("chdir", StringComparison.OrdinalIgnoreCase);
if (isCd)
{
HandleCdCommand(trimmedCommand);
return;
}
Stdout = string.Empty;
StdErr = string.Empty;
MacroActionCommand action = new MacroActionCommand();
action.PreWait = 0;
action.PostWait = 0;
action.WaitUntilExit = true;
action.Command = Command;
var (exitCode, _) = await MacroActionPlayer.RunCommandAction(
action,
stdoutCallback: StdoutWriteLine,
stderrCallback: StdErrWriteLine,
workingDirectory: WorkingDirectory,
cancellationToken: _cts.Token,
BackEnd
);
if (exitCode == -2)
{
StdErrWriteLine("命令已被用户取消。");
}
else if (exitCode != 0)
{
StdErrWriteLine($"命令退出代码: {exitCode}");
}
}
catch (Exception ex)
{
StdErrWriteLine($"命令执行失败: {ex.Message}");
}
finally
{
IsReadOnly = true;
CommandExecuted?.Invoke(this);
}
}
/// <summary>
/// 处理 cd/chdir 内置命令,在当前进程内切换工作目录
/// </summary>
private void HandleCdCommand(string rawCommand)
{
// 去掉 "cd" 或 "chdir" 前缀,提取路径部分
string afterCmd = rawCommand;
if (rawCommand.StartsWith("chdir ", StringComparison.OrdinalIgnoreCase))
afterCmd = rawCommand.Substring(6).TrimStart();
else if (rawCommand.Equals("chdir", StringComparison.OrdinalIgnoreCase))
afterCmd = "";
else if (rawCommand.StartsWith("cd ", StringComparison.OrdinalIgnoreCase))
afterCmd = rawCommand.Substring(3).TrimStart();
else if (rawCommand.Equals("cd", StringComparison.OrdinalIgnoreCase))
afterCmd = "";
string pathPart = afterCmd.Trim();
// 处理 "/D" 开关(cmd cd 命令的跨驱动器开关)
if (pathPart.StartsWith("/D ", StringComparison.OrdinalIgnoreCase))
{
pathPart = pathPart.Substring(3).TrimStart();
}
if (string.IsNullOrEmpty(pathPart))
{
// cd 无参数 → 显示当前目录
StdoutWriteLine(WorkingDirectory);
return;
}
// 展开环境变量 %VAR% 并解析为绝对路径
string expandedPath = Environment.ExpandEnvironmentVariables(pathPart);
try
{
string fullPath = Path.GetFullPath(expandedPath);
if (Directory.Exists(fullPath))
{
WorkingDirectory = fullPath;
}
else
{
StdErrWriteLine($"找不到路径 '{pathPart}',因为该目录不存在。");
}
}
catch (Exception ex)
{
StdErrWriteLine($"cd 命令出错: {ex.Message}");
}
}
public TerminalCommandViewModel()
{
ExecuteCommand = new RelayCommand(Execute);
}
}
}
+44
View File
@@ -0,0 +1,44 @@
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Windows.Input;
namespace FancyInput.ViewModels
{
public class ViewModelBase:INotifyPropertyChanged
{
public event PropertyChangedEventHandler? PropertyChanged;
protected void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
protected virtual bool SetProperty<T>(ref T field, T value, [CallerMemberName] string propertyName = "")
{
if (Equals(field, value)) return false;
field = value;
OnPropertyChanged(propertyName);
return true;
}
}
public class RelayCommand : ICommand
{
private readonly Action _execute;
private readonly Func<bool>? _canExecute;
public RelayCommand(Action execute, Func<bool>? canExecute = null)
{
_execute = execute ?? throw new ArgumentNullException(nameof(execute));
_canExecute = canExecute;
}
public bool CanExecute(object? parameter) => _canExecute == null || _canExecute();
public void Execute(object? parameter) => _execute();
public event EventHandler? CanExecuteChanged
{
add => CommandManager.RequerySuggested += value;
remove => CommandManager.RequerySuggested -= value;
}
}
}
+100
View File
@@ -0,0 +1,100 @@
<Window x:Class="FancyInput.Views.AboutWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:FancyInput.Views"
mc:Ignorable="d"
Title="关于" Height="410" Width="500"
WindowStartupLocation="CenterScreen"
ResizeMode="NoResize">
<Grid Margin="0,0,0,0">
<Grid.RowDefinitions>
<RowDefinition Height="130"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Label Grid.Row="0" HorizontalContentAlignment="Center"
VerticalContentAlignment="Bottom" Margin="0,0,0,10">
<Image Source="/Resources/Icons/Icon.png" Margin="0,0,0,0"/>
</Label>
<StackPanel Grid.Row="1" Orientation="Vertical" VerticalAlignment="Top">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
<Label Content="Fancy" Foreground="#FF490072" HorizontalAlignment="Center"
FontSize="40" FontFamily="Gill Sans MT Condensed" Padding="0"/>
<Label Content="Input" Foreground="#FF7245EE" HorizontalAlignment="Center"
FontSize="40" FontFamily="Gill Sans MT Condensed" Padding="0"/>
</StackPanel>
<Label Content="独立的输入显示工具,旨在帮助用户在录制、直播或演示时直观展示键盘、鼠标与手柄输入。"
Foreground="{Binding Foreground, RelativeSource={RelativeSource AncestorType=Window}}"
FontFamily="Global User Interface"
HorizontalAlignment="Center"
VerticalAlignment="Top"
Margin="0,3,0,0"
FontSize="12" />
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center"
VerticalAlignment="Top" Margin="0,5,0,0">
<Label Content="版本号:" FontFamily="Global User Interface" Margin="2" Padding="0"/>
<Label x:Name="VersionLabel" Content="v1.0.0" FontFamily="Global User Interface" Margin="2" Padding="0"/>
<Button x:Name="CheckUpdateButton" Content="检查更新" FontFamily="Global User Interface" Margin="2"
VerticalAlignment="Center" VerticalContentAlignment="Center" FontSize="10"
Background="White" BorderBrush="Black" BorderThickness="0.5"
Foreground="Black" Cursor="Hand" Click="CheckUpdateButton_Click">
</Button>
<Button x:Name="UpdateTimelineButton" Content="更新记录" FontFamily="Global User Interface" Margin="2"
VerticalAlignment="Center" VerticalContentAlignment="Center" FontSize="10"
Background="White" BorderBrush="Black" BorderThickness="0.5"
Foreground="Black" Cursor="Hand" Click="UpdateTimelineButton_Click">
</Button>
</StackPanel>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center"
VerticalAlignment="Top" Margin="0,5,0,0">
<Label Content="最后一次更新:" FontFamily="Global User Interface" Margin="2" Padding="0"/>
<Label x:Name="DateLabel" Content="2026-2-8" FontFamily="Global User Interface" Margin="2" Padding="0"/>
</StackPanel>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center"
VerticalAlignment="Top" Margin="0,5,0,0">
<Label Content="官方邮箱:" FontFamily="Global User Interface" Margin="2" Padding="0" VerticalAlignment="Center"/>
<Button x:Name="MailButton" Content="contact@xlworkspace.com" FontFamily="Global User Interface" Margin="2"
VerticalAlignment="Center" Padding="0" BorderBrush="Transparent" Background="White"
Foreground="Purple" Cursor="Hand" Click="MailButton_Click">
</Button>
</StackPanel>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center"
VerticalAlignment="Top" Margin="0,5,0,0">
<Label Content="支持此软件:" FontFamily="Global User Interface" Margin="2" Padding="0" VerticalAlignment="Center"/>
<Button x:Name="SupportButton" Content="联系与捐赠方式" FontFamily="Global User Interface" Margin="2"
VerticalAlignment="Center" VerticalContentAlignment="Center" FontSize="10"
Background="White" BorderBrush="Black" BorderThickness="0.5"
Foreground="Black" Cursor="Hand" Click="SupportButton_Click">
</Button>
</StackPanel>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center"
VerticalAlignment="Top" Margin="0,5,0,0">
<Label Content="许可证:" FontFamily="Global User Interface" Margin="2" Padding="0" VerticalAlignment="Center"/>
<Button x:Name="LicenseButton" Content="开源软件声明" FontFamily="Global User Interface" Margin="2"
VerticalAlignment="Center" VerticalContentAlignment="Center" FontSize="10"
Background="White" BorderBrush="Black" BorderThickness="0.5"
Foreground="Black" Cursor="Hand" Click="LicenseButton_Click">
</Button>
</StackPanel>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center"
VerticalAlignment="Top" Margin="0,5,0,0">
<Label Content="Copyright (c) 2026 XLworkspace。" FontFamily="Global User Interface"
Foreground="{Binding Foreground, RelativeSource={RelativeSource AncestorType=Window}}"
FontSize="12" Padding="0" VerticalAlignment="Center"/>
</StackPanel>
<Label Content="保留所有权利。" FontFamily="Global User Interface"
Foreground="{Binding Foreground, RelativeSource={RelativeSource AncestorType=Window}}"
HorizontalAlignment="Center"
VerticalAlignment="Top"
Margin="0,5,0,0"
FontSize="12"
Padding="0"/>
</StackPanel>
</Grid>
</Window>
+93
View File
@@ -0,0 +1,93 @@
using System.Windows;
namespace FancyInput.Views
{
/// <summary>
/// helpWindow.xaml 的交互逻辑
/// </summary>
public partial class AboutWindow : Window
{
private string _version = "1.0.0";
public string Version
{
get { return _version; }
set
{
_version = value;
VersionLabel.Content = _version;
}
}
private string _date = "2026-2-11";
public string Date
{
get { return _date; }
set
{
_date = value;
DateLabel.Content = _date;
}
}
private string _mail = "contact@xlworkspace.com";
public string Mail
{
get { return _mail; }
set
{
_mail = value;
MailButton.Content = _mail;
}
}
public event Action? CheckUpdateRequested;
public event Action? UpdateTimelineRequested;
public AboutWindow(string version, string date,string mail)
{
InitializeComponent();
Version = version;
Date = date;
Mail = mail;
}
private void MailButton_Click(object sender, RoutedEventArgs e)
{
try
{
System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo
{
FileName = $"mailto:{Mail}",
UseShellExecute = true
});
}
catch (System.Exception ex)
{
FancyInput.AppMessageBox.Show($"无法打开邮件客户端: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
private void CheckUpdateButton_Click(object sender, RoutedEventArgs e)
{
CheckUpdateRequested?.Invoke();
}
private void UpdateTimelineButton_Click(object sender, RoutedEventArgs e)
{
UpdateTimelineRequested?.Invoke();
}
private void SupportButton_Click(object sender, RoutedEventArgs e)
{
QRCodeWindow qRCodeWindow = new QRCodeWindow();
qRCodeWindow.Owner = this;
qRCodeWindow.WindowStartupLocation = WindowStartupLocation.CenterOwner;
qRCodeWindow.ShowDialog();
}
private void LicenseButton_Click(object sender, RoutedEventArgs e)
{
OpenSourceStatementWindow window = new OpenSourceStatementWindow();
window.Owner = this;
window.WindowStartupLocation = WindowStartupLocation.CenterOwner;
window.ShowDialog();
}
}
}
+18
View File
@@ -0,0 +1,18 @@
<Window x:Class="FancyInput.Views.Windows.ConsoleWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="控制台输出" Height="450" Width="500"
WindowStartupLocation="CenterScreen">
<Grid Background="#222">
<TextBox x:Name="OutputBox"
Margin="10"
FontFamily="Consolas"
FontSize="14"
Foreground="#eee"
Background="#333"
IsReadOnly="True"
TextWrapping="Wrap"
VerticalScrollBarVisibility="Auto"
HorizontalScrollBarVisibility="Auto"/>
</Grid>
</Window>
+40
View File
@@ -0,0 +1,40 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;
namespace FancyInput.Views.Windows
{
/// <summary>
/// ConsoleWindow.xaml 的交互逻辑
/// </summary>
public partial class ConsoleWindow : Window
{
public ConsoleWindow()
{
InitializeComponent();
}
public void WriteLine(string text)
{
OutputBox.AppendText(text + "\r\n");
OutputBox.ScrollToEnd();
}
// 可选:批量输出
public void Write(string text)
{
OutputBox.AppendText(text);
OutputBox.ScrollToEnd();
}
}
}
@@ -0,0 +1,8 @@
<Image x:Class="FancyInput.Views.Controls.AnimatedImage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
d:Width="100"
d:Height="100" />
@@ -0,0 +1,127 @@
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System;
using System.IO;
using System.Runtime.InteropServices;
namespace FancyInput.Views.Controls
{
/// <summary>
/// AnimatedImage.xaml 的交互逻辑
/// </summary>
public partial class AnimatedImage : Image
{
private ImageSource? _lastAppliedSource;
private bool _lastWasAnimated;
public AnimatedImage()
{
InitializeComponent();
}
// Keep an input property separate from Image.Source.
// WpfAnimatedGif internally animates Source each frame, so using Source as input causes re-entry issues.
public static readonly DependencyProperty ImageSourceProperty = DependencyProperty.Register(
nameof(ImageSource),
typeof(ImageSource),
typeof(AnimatedImage),
new PropertyMetadata(null, OnImageSourceChanged));
public ImageSource? ImageSource
{
get => (ImageSource?)GetValue(ImageSourceProperty);
set => SetValue(ImageSourceProperty, value);
}
private static void OnImageSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is AnimatedImage control)
{
control.UpdateAnimatedGif(e.NewValue as ImageSource);
}
}
private static bool IsGifImageSource(ImageSource? source)
{
if (source is BitmapFrame frame)
{
string mimeTypes = frame.Decoder?.CodecInfo?.MimeTypes ?? string.Empty;
return mimeTypes.IndexOf("gif", StringComparison.OrdinalIgnoreCase) >= 0;
}
if (source is BitmapImage bitmapImage)
{
if (bitmapImage.UriSource != null &&
bitmapImage.UriSource.AbsolutePath.EndsWith(".gif", StringComparison.OrdinalIgnoreCase))
{
return true;
}
var stream = bitmapImage.StreamSource;
if (stream != null && stream.CanSeek)
{
long oldPosition = stream.Position;
try
{
stream.Seek(0, SeekOrigin.Begin);
Span<byte> header = stackalloc byte[6];
int read = stream.Read(header);
return read >= 6 &&
header[0] == (byte)'G' &&
header[1] == (byte)'I' &&
header[2] == (byte)'F';
}
finally
{
stream.Seek(oldPosition, SeekOrigin.Begin);
}
}
}
return false;
}
private void UpdateAnimatedGif(ImageSource? source)
{
bool shouldAnimate = IsGifImageSource(source);
if (ReferenceEquals(source, _lastAppliedSource) && shouldAnimate == _lastWasAnimated)
return;
_lastAppliedSource = source;
_lastWasAnimated = shouldAnimate;
if (shouldAnimate)
{
try
{
// Avoid Freezable inheritance-context conflicts between Source and AnimatedSource.
WpfAnimatedGif.ImageBehavior.SetAnimatedSource(this, null);
SetCurrentValue(SourceProperty, null);
WpfAnimatedGif.ImageBehavior.SetAnimatedSource(this, source);
}
catch (COMException)
{
// Fallback: show static image instead of crashing on problematic GIF metadata/frame composition.
WpfAnimatedGif.ImageBehavior.SetAnimatedSource(this, null);
SetCurrentValue(SourceProperty, source);
_lastWasAnimated = false;
}
catch (ArgumentException)
{
// Fallback for context/argument issues when initializing animation.
WpfAnimatedGif.ImageBehavior.SetAnimatedSource(this, null);
SetCurrentValue(SourceProperty, source);
_lastWasAnimated = false;
}
return;
}
WpfAnimatedGif.ImageBehavior.SetAnimatedSource(this, null);
SetCurrentValue(SourceProperty, source);
}
}
}
+52
View File
@@ -0,0 +1,52 @@
<UserControl x:Class="FancyInput.Views.Controls.CreatePage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:FancyInput.Views.Controls"
xmlns:md="clr-namespace:FancyInput.Models"
xmlns:vm ="clr-namespace:FancyInput.ViewModels"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<Grid Height="200">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="100*"/>
<ColumnDefinition Width="0*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="100*"/>
<RowDefinition Height="35*"/>
</Grid.RowDefinitions>
<Border Grid.Column="0" Width="180" Height="130" BorderBrush="Gray" BorderThickness="1" CornerRadius="10" VerticalAlignment="Bottom"
Cursor="Hand" MouseLeftButtonDown="CreateFromEmpty" >
<Border.Style>
<Style TargetType="Border">
<Setter Property="Background" Value="#05808080"/>
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="#0B5B06DE"/>
</Trigger>
</Style.Triggers>
</Style>
</Border.Style>
<Viewbox Grid.Column="0" Height="100" Width="100" IsHitTestVisible="False">
<Path Data="M1408-85.333h-1280c-70.685 0.019-127.981 57.315-128 127.998v768.002c0.019 70.685 57.315 127.981 127.998 128h1280.002c70.685-0.019 127.981-57.315 128-127.998v-768.002c-0.019-70.685-57.315-127.981-127.998-128h-0.002zM341.333 629.317v106.701c-0.058 17.64-14.343 31.925-31.977 31.983h-106.707c-17.64-0.058-31.925-14.343-31.983-31.977v-106.707c0.058-17.64 14.343-31.925 31.977-31.983h106.707c17.64 0.058 31.925 14.343 31.983 31.977v0.006zM597.333 629.317v106.701c-0.058 17.64-14.343 31.925-31.977 31.983h-106.707c-17.64-0.058-31.925-14.343-31.983-31.977v-106.707c0.058-17.64 14.343-31.925 31.977-31.983h106.673c17.64 0.058 31.925 14.343 31.983 31.977v0.006zM853.333 629.317v106.701c-0.058 17.64-14.343 31.925-31.977 31.983h-106.707c-17.64-0.058-31.925-14.343-31.983-31.977v-106.707c0.058-17.64 14.343-31.925 31.977-31.983h106.673c17.655 0.039 31.959 14.331 32.017 31.977v0.006zM1109.333 629.317v106.701c-0.058 17.64-14.343 31.925-31.977 31.983h-106.707c-17.64-0.058-31.925-14.343-31.983-31.977v-106.707c0.058-17.64 14.343-31.925 31.977-31.983h106.673c17.64 0.058 31.925 14.343 31.983 31.977v0.006zM1365.333 629.317v106.701c-0.058 17.628-14.323 31.906-31.941 31.983h-106.776c-17.626-0.077-31.891-14.355-31.949-31.977v-106.707c0.058-17.652 14.362-31.944 32.013-31.983h106.807c17.582 0.135 31.789 14.39 31.846 31.977v0.006zM341.333 373.317v106.701c-0.058 17.64-14.343 31.925-31.977 31.983h-106.707c-17.64-0.058-31.925-14.343-31.983-31.977v-106.673c0.058-17.64 14.343-31.925 31.977-31.983h106.707c17.628 0.058 31.906 14.323 31.983 31.941v0.007zM597.333 373.317v106.701c-0.058 17.64-14.343 31.925-31.977 31.983h-106.707c-17.64-0.058-31.925-14.343-31.983-31.977v-106.673c0.058-17.64 14.343-31.925 31.977-31.983h106.673c17.64 0.058 31.925 14.343 31.983 31.977v0.006zM853.333 373.317v106.701c-0.058 17.64-14.343 31.925-31.977 31.983h-106.707c-17.64-0.058-31.925-14.343-31.983-31.977v-106.673c0.058-17.64 14.343-31.925 31.977-31.983h106.673c17.643 0.039 31.94 14.311 32.017 31.941v0.007zM1109.333 373.317v106.701c-0.058 17.64-14.343 31.925-31.977 31.983h-106.707c-17.64-0.058-31.925-14.343-31.983-31.977v-106.673c0.058-17.64 14.343-31.925 31.977-31.983h106.673c17.643 0.039 31.94 14.311 32.017 31.941v0.007zM1365.333 373.317v106.701c-0.058 17.616-14.304 31.886-31.905 31.983h-106.812c-17.655-0.039-31.959-14.331-32.017-31.977v-106.673c0.058-17.652 14.362-31.944 32.013-31.983h106.807c17.599 0.096 31.838 14.347 31.915 31.941v0.007zM341.333 117.317v106.701c-0.058 17.64-14.343 31.925-31.977 31.983h-106.707c-17.64-0.058-31.925-14.343-31.983-31.977v-106.673c0.039-17.655 14.331-31.959 31.977-32.017h106.707c17.64 0.058 31.925 14.343 31.983 31.977v0.006zM1109.333 117.317v106.701c-0.058 17.64-14.343 31.925-31.977 31.983h-618.707c-17.64-0.058-31.925-14.343-31.983-31.977v-106.673c0.058-17.64 14.343-31.925 31.977-31.983h618.707c17.64 0.058 31.925 14.343 31.983 31.977v0.006zM1365.333 117.317v106.701c-0.058 17.628-14.323 31.906-31.941 31.983h-106.776c-17.655-0.039-31.959-14.331-32.017-31.977v-106.673c0.058-17.652 14.362-31.944 32.013-31.983h106.807c17.599 0.096 31.838 14.347 31.915 31.941v0.007z"
StrokeThickness="1" Fill="#FF610161" RenderTransformOrigin="0.5,0.5">
<Path.RenderTransform>
<TransformGroup>
<ScaleTransform ScaleY="-1"/>
<ScaleTransform ScaleX="-1"/>
<SkewTransform/>
<RotateTransform/>
<TranslateTransform/>
</TransformGroup>
</Path.RenderTransform>
</Path>
</Viewbox>
</Border>
<Label Grid.Row="1" Grid.Column="0" Content="从空白开始创建" HorizontalAlignment="Center" VerticalAlignment="Top"
Margin="0,5" FontSize="15" Foreground="#FF610161" FontWeight="Bold"/>
<Label Grid.Row="1" Grid.Column="0" Content="利用自备的贴图文件,轻松配置你的输入显示方案" HorizontalAlignment="Center" VerticalAlignment="Top"
Margin="0,25,0,0" FontSize="10" Foreground="#CC610161"/>
</Grid>
</UserControl>
@@ -0,0 +1,44 @@
using FancyInput.ViewModels;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace FancyInput.Views.Controls
{
/// <summary>
/// CreatePage.xaml 的交互逻辑
/// </summary>
public partial class CreatePage : UserControl
{
public CreatePage()
{
InitializeComponent();
}
// ConfigEditClick
public static readonly RoutedEvent CreateFromEmptyClickEvent = EventManager.RegisterRoutedEvent(nameof(CreateFromEmptyClick), RoutingStrategy.Bubble,
typeof(RoutedEventHandler), typeof(CreatePage));
public event RoutedEventHandler CreateFromEmptyClick
{
add => AddHandler(CreateFromEmptyClickEvent, value);
remove => RemoveHandler(CreateFromEmptyClickEvent, value);
}
private void CreateFromEmpty(object sender, MouseButtonEventArgs e)
{
RaiseEvent(new RoutedEventArgs(CreateFromEmptyClickEvent));
}
}
}
@@ -0,0 +1,45 @@
<UserControl x:Class="FancyInput.Views.Controls.ElementViewPanel"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:FancyInput.Views.Controls"
xmlns:uc ="clr-namespace:FancyInput.Views.Controls"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<Grid >
<Grid.RowDefinitions>
<RowDefinition Height="{Binding RowHeight1, RelativeSource={RelativeSource AncestorType=UserControl}}"/>
<RowDefinition Height="{Binding RowHeight2, RelativeSource={RelativeSource AncestorType=UserControl}}"/>
<RowDefinition Height="{Binding RowHeight3, RelativeSource={RelativeSource AncestorType=UserControl}}"/>
</Grid.RowDefinitions>
<Label Content="{Binding Title, RelativeSource={RelativeSource AncestorType=UserControl}}"
FontSize="{Binding FontSize, RelativeSource={RelativeSource AncestorType=UserControl}}"
FontFamily="Cascadia Mono" HorizontalAlignment="Center" VerticalAlignment="Bottom"/>
<Border Grid.Row="1" Background="{Binding ImageBackground, RelativeSource={RelativeSource AncestorType=UserControl}}"
CornerRadius="20" BorderBrush="Gray" BorderThickness="1" Margin="{Binding ImageMargin, RelativeSource={RelativeSource AncestorType=UserControl}}">
<Viewbox>
<uc:AnimatedImage x:Name="MainImage" ImageSource="{Binding ImageSource, RelativeSource={RelativeSource AncestorType=UserControl}}"
RenderOptions.BitmapScalingMode="NearestNeighbor"/>
</Viewbox>
</Border>
<Grid Grid.Row="2" Margin="6,2">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="50*"/>
<ColumnDefinition Width="50*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="50*"/>
<RowDefinition Height="50*"/>
</Grid.RowDefinitions>
<Button Grid.Row="0" Grid.Column="0" Content="加载" Style="{StaticResource FancyButton}" FontSize="{Binding FontSize, RelativeSource={RelativeSource AncestorType=UserControl}}"
Margin="{Binding ButtonMargin, RelativeSource={RelativeSource AncestorType=UserControl}}" Click="LoadImage"/>
<Button Grid.Row="0" Grid.Column="1" Content="修改" Style="{StaticResource FancyButton}" FontSize="{Binding FontSize, RelativeSource={RelativeSource AncestorType=UserControl}}"
Margin="{Binding ButtonMargin, RelativeSource={RelativeSource AncestorType=UserControl}}" Click="EditImage"/>
<Button Grid.Row="1" Grid.Column="0" Content="复制" Style="{StaticResource FancyButton}" FontSize="{Binding FontSize, RelativeSource={RelativeSource AncestorType=UserControl}}"
Margin="{Binding ButtonMargin, RelativeSource={RelativeSource AncestorType=UserControl}}" Click="CopyImage"/>
<Button Grid.Row="1" Grid.Column="1" Content="粘贴" Style="{StaticResource FancyButton}" FontSize="{Binding FontSize, RelativeSource={RelativeSource AncestorType=UserControl}}"
Margin="{Binding ButtonMargin, RelativeSource={RelativeSource AncestorType=UserControl}}" Click="PasteImage"/>
</Grid>
</Grid>
</UserControl>
@@ -0,0 +1,163 @@
using System.Diagnostics;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.Imaging;
namespace FancyInput.Views.Controls
{
public class ImageActionEventArgs : RoutedEventArgs
{
public int Index { get; }
public ImageActionEventArgs(RoutedEvent routedEvent, int index) : base(routedEvent)
{
Index = index;
}
}
/// <summary>
/// ElementViewPanel.xaml 的交互逻辑
/// </summary>
public partial class ElementViewPanel : UserControl
{
public ElementViewPanel()
{
InitializeComponent();
}
// Index
public static readonly DependencyProperty IndexProperty = DependencyProperty.Register(nameof(Index),
typeof(int), typeof(ElementViewPanel), new PropertyMetadata(0));
public int Index
{
get => (int)GetValue(IndexProperty);
set => SetValue(IndexProperty, value);
}
// Title
public static readonly DependencyProperty TitleProperty = DependencyProperty.Register(nameof(Title),
typeof(string), typeof(ElementViewPanel), new PropertyMetadata(string.Empty));
public string Title
{
get => (string)GetValue(TitleProperty);
set => SetValue(TitleProperty, value);
}
// ButtonMargin
public static readonly DependencyProperty ButtonMarginProperty = DependencyProperty.Register(nameof(ButtonMargin),
typeof(Thickness), typeof(ElementViewPanel), new PropertyMetadata(new Thickness(3)));
public Thickness ButtonMargin
{
get => (Thickness)GetValue(ButtonMarginProperty);
set => SetValue(ButtonMarginProperty, value);
}
// ImageMargin
public static readonly DependencyProperty ImageMarginProperty = DependencyProperty.Register(nameof(ImageMargin),
typeof(Thickness), typeof(ElementViewPanel), new PropertyMetadata(new Thickness(20,5,20,5)));
public Thickness ImageMargin
{
get => (Thickness)GetValue(ImageMarginProperty);
set => SetValue(ImageMarginProperty, value);
}
// RowHeight1
public static readonly DependencyProperty RowHeight1Property =
DependencyProperty.Register(nameof(RowHeight1), typeof(GridLength), typeof(ElementViewPanel),
new PropertyMetadata(new GridLength(100, GridUnitType.Star)));
public GridLength RowHeight1
{
get => (GridLength)GetValue(RowHeight1Property);
set => SetValue(RowHeight1Property, value);
}
// RowHeight2
public static readonly DependencyProperty RowHeight2Property =
DependencyProperty.Register(nameof(RowHeight2), typeof(GridLength), typeof(ElementViewPanel),
new PropertyMetadata(new GridLength(1000, GridUnitType.Star)));
public GridLength RowHeight2
{
get => (GridLength)GetValue(RowHeight2Property);
set => SetValue(RowHeight2Property, value);
}
// RowHeight3
public static readonly DependencyProperty RowHeight3Property =
DependencyProperty.Register(nameof(RowHeight3), typeof(GridLength), typeof(ElementViewPanel),
new PropertyMetadata(new GridLength(250, GridUnitType.Star)));
public GridLength RowHeight3
{
get => (GridLength)GetValue(RowHeight3Property);
set => SetValue(RowHeight3Property, value);
}
//ImageBackground
public static readonly DependencyProperty ImageBackgroundProperty = DependencyProperty.Register(nameof(ImageBackground),
typeof(Brush), typeof(ElementViewPanel), new PropertyMetadata(Brushes.Transparent));
public Brush ImageBackground
{
get => (Brush)GetValue(ImageBackgroundProperty);
set => SetValue(ImageBackgroundProperty, value);
}
//ImageSource
public static readonly DependencyProperty ImageSourceProperty = DependencyProperty.Register(nameof(ImageSource),
typeof(ImageSource), typeof(ElementViewPanel), new PropertyMetadata(null));
public ImageSource ImageSource
{
get => (ImageSource)GetValue(ImageSourceProperty);
set => SetValue(ImageSourceProperty, value);
}
// LoadImageClick
public static readonly RoutedEvent LoadImageClickEvent = EventManager.RegisterRoutedEvent(nameof(LoadImageClick), RoutingStrategy.Bubble,
typeof(EventHandler<ImageActionEventArgs>), typeof(ElementViewPanel));
public event EventHandler<ImageActionEventArgs> LoadImageClick
{
add => AddHandler(LoadImageClickEvent, value);
remove => RemoveHandler(LoadImageClickEvent, value);
}
// EditImageClick
public static readonly RoutedEvent EditImageClickEvent = EventManager.RegisterRoutedEvent(nameof(EditImageClick), RoutingStrategy.Bubble,
typeof(EventHandler<ImageActionEventArgs>), typeof(ElementViewPanel));
public event EventHandler<ImageActionEventArgs> EditImageClick
{
add => AddHandler(EditImageClickEvent, value);
remove => RemoveHandler(EditImageClickEvent, value);
}
// CopyImageClick
public static readonly RoutedEvent CopyImageClickEvent = EventManager.RegisterRoutedEvent(nameof(CopyImageClick), RoutingStrategy.Bubble,
typeof(EventHandler<ImageActionEventArgs>), typeof(ElementViewPanel));
public event EventHandler<ImageActionEventArgs> CopyImageClick
{
add => AddHandler(CopyImageClickEvent, value);
remove => RemoveHandler(CopyImageClickEvent, value);
}
// PasteImageClick
public static readonly RoutedEvent PasteImageClickEvent = EventManager.RegisterRoutedEvent(nameof(PasteImageClick), RoutingStrategy.Bubble,
typeof(EventHandler<ImageActionEventArgs>), typeof(ElementViewPanel));
public event EventHandler<ImageActionEventArgs> PasteImageClick
{
add => AddHandler(PasteImageClickEvent, value);
remove => RemoveHandler(PasteImageClickEvent, value);
}
private void LoadImage(object sender, RoutedEventArgs e) => RaiseEvent(new ImageActionEventArgs(LoadImageClickEvent, Index));
private void EditImage(object sender, RoutedEventArgs e) => RaiseEvent(new ImageActionEventArgs(EditImageClickEvent, Index));
private void CopyImage(object sender, RoutedEventArgs e) => RaiseEvent(new ImageActionEventArgs(CopyImageClickEvent, Index));
private void PasteImage(object sender, RoutedEventArgs e)=> RaiseEvent(new ImageActionEventArgs(PasteImageClickEvent, Index));
}
}
+50
View File
@@ -0,0 +1,50 @@
<UserControl x:Class="FancyInput.Views.Controls.IconButton"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:FancyInput.Views.Controls"
mc:Ignorable="d"
d:DesignHeight="100" d:DesignWidth="100"
x:Name="Root">
<UserControl.Resources>
<local:BoolToScaleYConverter x:Key="BoolToScaleYConverter"/>
<local:NullToVisibilityConverter x:Key="NullToVisibilityConverter"/>
</UserControl.Resources>
<UserControl.RenderTransform>
<ScaleTransform x:Name="IconScale" ScaleX="1" ScaleY="1"/>
</UserControl.RenderTransform>
<Border CornerRadius="{Binding CornerRadius, ElementName=Root}"
Background="{Binding PathBackground, ElementName=Root}"
BorderBrush="{Binding ProfileBrush, ElementName=Root}"
BorderThickness="{Binding ProfileThickness, ElementName=Root}">
<Grid Margin="0" Background="#01FFFFFF"
MouseLeftButtonDown="Grid_MouseLeftButtonDown" MouseEnter="Grid_MouseEnter" MouseLeave="Grid_MouseLeave"
MouseLeftButtonUp="Grid_MouseLeftButtonUp">
<Viewbox Margin="{Binding IconMargin, ElementName=Root}" RenderTransformOrigin="0.5,0.5">
<Viewbox.RenderTransform>
<TransformGroup>
<TranslateTransform X="{Binding IconShiftX, ElementName=Root}"
Y="{Binding IconShiftY, ElementName=Root}"/>
</TransformGroup>
</Viewbox.RenderTransform>
<Path x:Name="MainPath"
Margin="{Binding PathMargin, ElementName=Root}"
Data="{Binding PathData, ElementName=Root}"
Fill="{Binding PathFill, ElementName=Root}"
Stroke="{Binding PathStroke, ElementName=Root}"
StrokeThickness="{Binding PathStrokeThickness, ElementName=Root}"
RenderTransformOrigin="0.5,0.5">
<Path.RenderTransform>
<TransformGroup>
<ScaleTransform ScaleY="{Binding IsFlipped, ElementName=Root, Converter={StaticResource BoolToScaleYConverter}}"/>
</TransformGroup>
</Path.RenderTransform>
</Path>
</Viewbox>
<ContentPresenter Content="{Binding CustomContent, ElementName=Root}"
Visibility="{Binding PathData, ElementName=Root, Converter={StaticResource NullToVisibilityConverter}}"
HorizontalAlignment="Stretch" VerticalAlignment="Stretch"/>
</Grid>
</Border>
</UserControl>
@@ -0,0 +1,345 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace FancyInput.Views.Controls
{
/// <summary>
/// PathButton.xaml 的交互逻辑
/// </summary>
public partial class IconButton : UserControl
{
public IconButton()
{
InitializeComponent();
}
// CustomContent
public static readonly DependencyProperty CustomContentProperty =
DependencyProperty.Register(nameof(CustomContent), typeof(object), typeof(IconButton));
public object CustomContent
{
get => GetValue(CustomContentProperty);
set => SetValue(CustomContentProperty, value);
}
// Title
public static readonly DependencyProperty TitleProperty =
DependencyProperty.Register(nameof(Title), typeof(string), typeof(IconButton));
public string Title
{
get => (string)GetValue(TitleProperty);
set => SetValue(TitleProperty, value);
}
// CornerRadius
public static readonly DependencyProperty CornerRadiusProperty =
DependencyProperty.Register(nameof(CornerRadius), typeof(double), typeof(IconButton), new PropertyMetadata(20.0));
public double CornerRadius
{
get => (double)GetValue(CornerRadiusProperty);
set => SetValue(CornerRadiusProperty, value);
}
//ProfileThickness
public static readonly DependencyProperty ProfileThicknessProperty =
DependencyProperty.Register(nameof(ProfileThickness), typeof(double), typeof(IconButton));
public double ProfileThickness
{
get => (double)GetValue(ProfileThicknessProperty);
set => SetValue(ProfileThicknessProperty, value);
}
// PathBackground
public static readonly DependencyProperty PathBackgroundProperty =
DependencyProperty.Register(nameof(PathBackground), typeof(Brush), typeof(IconButton));
public Brush PathBackground
{
get => (Brush)GetValue(PathBackgroundProperty);
set => SetValue(PathBackgroundProperty, value);
}
// Size
//public static readonly DependencyProperty SizeProperty =
// DependencyProperty.Register(nameof(Size), typeof(double), typeof(IconButton), new PropertyMetadata(80.0));
//public double Size
//{
// get => (double)GetValue(SizeProperty);
// set => SetValue(SizeProperty, value);
//}
// PathData
public Geometry PathData
{
get { return (Geometry)GetValue(PathDataProperty); }
set { SetValue(PathDataProperty, value); }
}
public static readonly DependencyProperty PathDataProperty =
DependencyProperty.Register(nameof(PathData), typeof(Geometry), typeof(IconButton), new PropertyMetadata(null));
//PathStrokeThickness
public static readonly DependencyProperty PathStrokeThicknessProperty =
DependencyProperty.Register(nameof(PathStrokeThickness), typeof(double), typeof(IconButton), new PropertyMetadata(0.0));
public double PathStrokeThickness
{
get => (double)GetValue(PathStrokeThicknessProperty);
set => SetValue(PathStrokeThicknessProperty, value);
}
// ProfileBrush
public static readonly DependencyProperty ProfileBrushProperty =
DependencyProperty.Register(nameof(ProfileBrush), typeof(Brush), typeof(IconButton));
public Brush ProfileBrush
{
get => (Brush)GetValue(ProfileBrushProperty);
set => SetValue(ProfileBrushProperty, value);
}
//PathStroke
public static readonly DependencyProperty PathStrokeProperty =
DependencyProperty.Register(nameof(PathStroke), typeof(Brush), typeof(IconButton), new PropertyMetadata(Brushes.Transparent));
public Brush PathStroke
{
get => (Brush)GetValue(PathStrokeProperty);
set => SetValue(PathStrokeProperty, value);
}
//PathFill
public static readonly DependencyProperty PathFillProperty =
DependencyProperty.Register(nameof(PathFill), typeof(Brush), typeof(IconButton), new PropertyMetadata(Brushes.Transparent));
public Brush PathFill
{
get => (Brush)GetValue(PathFillProperty);
set => SetValue(PathFillProperty, value);
}
// DefaultColor
public static readonly DependencyProperty DefaultColorProperty =
DependencyProperty.Register(nameof(DefaultColor), typeof(Brush), typeof(IconButton), new PropertyMetadata(Brushes.Transparent));
public Brush DefaultColor
{
get => (Brush)GetValue(DefaultColorProperty);
set => SetValue(DefaultColorProperty, value);
}
// ActiveColor
public static readonly DependencyProperty ActiveColorProperty =
DependencyProperty.Register(nameof(ActiveColor), typeof(Brush), typeof(IconButton), new PropertyMetadata(Brushes.Transparent));
public Brush ActiveColor
{
get => (Brush)GetValue(ActiveColorProperty);
set=> SetValue(ActiveColorProperty, value);
}
private void AnimateScale(double targetScale)
{
var anim = new DoubleAnimation(targetScale, TimeSpan.FromMilliseconds(300))
{
EasingFunction = new QuadraticEase { EasingMode = EasingMode.EaseOut }
};
IconScale.BeginAnimation(System.Windows.Media.ScaleTransform.ScaleXProperty, anim);
IconScale.BeginAnimation(System.Windows.Media.ScaleTransform.ScaleYProperty, anim);
}
private void AnimateScaleSequence()
{
var shrinkAnim = new DoubleAnimation(0.98, TimeSpan.FromMilliseconds(100))
{
EasingFunction = new QuadraticEase { EasingMode = EasingMode.EaseOut }
};
shrinkAnim.Completed += (s, e) =>
{
var restoreAnim = new DoubleAnimation(1.02, TimeSpan.FromMilliseconds(200))
{
EasingFunction = new QuadraticEase { EasingMode = EasingMode.EaseOut }
};
IconScale.BeginAnimation(ScaleTransform.ScaleXProperty, restoreAnim);
IconScale.BeginAnimation(ScaleTransform.ScaleYProperty, restoreAnim);
};
IconScale.BeginAnimation(ScaleTransform.ScaleXProperty, shrinkAnim);
IconScale.BeginAnimation(ScaleTransform.ScaleYProperty, shrinkAnim);
}
// IsFlipped
public static readonly DependencyProperty IsFlippedProperty =
DependencyProperty.Register(nameof(IsFlipped), typeof(bool), typeof(IconButton), new PropertyMetadata(false));
public bool IsFlipped
{
get => (bool)GetValue(IsFlippedProperty);
set => SetValue(IsFlippedProperty, value);
}
// PathMargin
public static readonly DependencyProperty PathMarginProperty =
DependencyProperty.Register(nameof(PathMargin), typeof(Thickness), typeof(IconButton), new PropertyMetadata(new Thickness(0)));
public Thickness PathMargin
{
get => (Thickness)GetValue(PathMarginProperty);
set => SetValue(PathMarginProperty, value);
}
// IconMargin
public static readonly DependencyProperty IconMarginProperty =
DependencyProperty.Register(nameof(IconMargin), typeof(Thickness), typeof(IconButton), new PropertyMetadata(new Thickness(0)));
public Thickness IconMargin
{
get => (Thickness)GetValue(IconMarginProperty);
set => SetValue(IconMarginProperty, value);
}
// IconShiftX
public static readonly DependencyProperty IconShiftXProperty =
DependencyProperty.Register(nameof(IconShiftX), typeof(double), typeof(IconButton), new PropertyMetadata(0.0));
public double IconShiftX
{
get => (double)GetValue(IconShiftXProperty);
set => SetValue(IconShiftXProperty, value);
}
// IconShiftY
public static readonly DependencyProperty IconShiftYProperty =
DependencyProperty.Register(nameof(IconShiftY), typeof(double), typeof(IconButton), new PropertyMetadata(0.0));
public double IconShiftY
{
get => (double)GetValue(IconShiftYProperty);
set => SetValue(IconShiftYProperty, value);
}
public static readonly DependencyProperty CommandProperty =
DependencyProperty.Register(nameof(Command), typeof(ICommand), typeof(IconButton));
public ICommand Command
{
get => (ICommand)GetValue(CommandProperty);
set => SetValue(CommandProperty, value);
}
public static readonly DependencyProperty CommandParameterProperty =
DependencyProperty.Register(nameof(CommandParameter), typeof(object), typeof(IconButton));
public object CommandParameter
{
get => GetValue(CommandParameterProperty);
set => SetValue(CommandParameterProperty, value);
}
// Click 路由事件定义
public static readonly RoutedEvent ClickEvent =
EventManager.RegisterRoutedEvent(
nameof(Click),
RoutingStrategy.Bubble,
typeof(RoutedEventHandler),
typeof(IconButton));
public event RoutedEventHandler Click
{
add { AddHandler(ClickEvent, value); }
remove { RemoveHandler(ClickEvent, value); }
}
private bool _isPressed = false;
private void Grid_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
AnimateScale(0.98);
_isPressed = true;
}
private void Grid_MouseEnter(object sender, MouseEventArgs e)
{
AnimateScale(1.02);
}
private void Grid_MouseLeave(object sender, MouseEventArgs e)
{
_isPressed = false;
AnimateScale(1.0);
}
private void Grid_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
AnimateScale(1.02);
if (_isPressed)
{
_isPressed = false;
RaiseEvent(new RoutedEventArgs(ClickEvent, this));
Command?.Execute(CommandParameter ?? DataContext);
}
}
}
public class BoolToOpacityConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
return (value is bool b && b) ? 1.0 : 0.45;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is double d)
{
return d >= 0.999;
}
return true;
}
}
public class BoolToScaleYConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
return (value is bool b && b) ? -1.0 : 1.0;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
return (value is double d && d == -1.0);
}
}
public class BoolToAngleConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
return (value is bool b && b) ? 180.0 : 0.0;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
return (value is double d && d == 180.0);
}
}
// 在 IconButton.xaml.cs 所在命名空间中添加
public class NullToVisibilityConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
=> value == null ? Visibility.Visible : Visibility.Collapsed;
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
=> throw new NotImplementedException();
}
}
File diff suppressed because one or more lines are too long
@@ -0,0 +1,114 @@
using FancyInput.ViewModels;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace FancyInput.Views.Controls
{
public class ElementMoveActionEventArgs : RoutedEventArgs
{
public int Index { get; }
public ElementMoveActionEventArgs(RoutedEvent routedEvent, int index) : base(routedEvent)
{
Index = index;
}
}
/// <summary>
/// ManagePage.xaml 的交互逻辑
/// </summary>
public partial class ManagePage : UserControl
{
public ManagePage()
{
InitializeComponent();
}
// OverlayWindowViewModels
public static readonly DependencyProperty OverlayWindowViewModelsProperty = DependencyProperty.Register(nameof(OverlayWindowViewModels),
typeof(ObservableCollection<OverlayWindowViewModel>), typeof(ManagePage), new PropertyMetadata(new ObservableCollection<OverlayWindowViewModel>()));
public ObservableCollection<OverlayWindowViewModel> OverlayWindowViewModels
{
get => (ObservableCollection<OverlayWindowViewModel>)GetValue(OverlayWindowViewModelsProperty);
set => SetValue(OverlayWindowViewModelsProperty, value);
}
// ConfigLoadClick
public static readonly RoutedEvent SaveGroupClickEvent = EventManager.RegisterRoutedEvent(nameof(SaveGroupClick), RoutingStrategy.Bubble,
typeof(RoutedEventHandler), typeof(ManagePage));
public event RoutedEventHandler SaveGroupClick
{
add => AddHandler(SaveGroupClickEvent, value);
remove => RemoveHandler(SaveGroupClickEvent, value);
}
// ConfigEditClick
public static readonly RoutedEvent LoadGroupClickEvent = EventManager.RegisterRoutedEvent(nameof(LoadGroupClick), RoutingStrategy.Bubble,
typeof(RoutedEventHandler), typeof(ManagePage));
public event RoutedEventHandler LoadGroupClick
{
add => AddHandler(LoadGroupClickEvent, value);
remove => RemoveHandler(LoadGroupClickEvent, value);
}
// UpMoveClick
public static readonly RoutedEvent UpMoveClickEvent = EventManager.RegisterRoutedEvent(nameof(UpMoveClick), RoutingStrategy.Bubble,
typeof(EventHandler<ElementMoveActionEventArgs>), typeof(ManagePage));
public event EventHandler<ElementMoveActionEventArgs> UpMoveClick
{
add => AddHandler(UpMoveClickEvent, value);
remove => RemoveHandler(UpMoveClickEvent, value);
}
// DownMoveClick
public static readonly RoutedEvent DownMoveClickEvent = EventManager.RegisterRoutedEvent(nameof(DownMoveClick), RoutingStrategy.Bubble,
typeof(EventHandler<ElementMoveActionEventArgs>), typeof(ManagePage));
public event EventHandler<ElementMoveActionEventArgs> DownMoveClick
{
add => AddHandler(DownMoveClickEvent, value);
remove => RemoveHandler(DownMoveClickEvent, value);
}
private void DeleteOverlayWindow_Click(object sender, RoutedEventArgs e)
{
if (sender is Button btn && btn.DataContext is OverlayWindowViewModel vm)
{
OverlayWindow overlayWindow = vm.OverlayWindow;
overlayWindow.Close();
}
}
private void SaveGroup(object sender, RoutedEventArgs e) => RaiseEvent(new RoutedEventArgs(SaveGroupClickEvent));
private void LoadGroup(object sender, RoutedEventArgs e)=> RaiseEvent(new RoutedEventArgs(LoadGroupClickEvent));
private void UpMove(object sender, RoutedEventArgs e)
{
if (sender is Button btn && btn.DataContext is OverlayWindowViewModel vm)
{
RaiseEvent(new ElementMoveActionEventArgs(UpMoveClickEvent, OverlayWindowViewModels.IndexOf(vm)));
}
}
private void DownMove(object sender, RoutedEventArgs e)
{
if (sender is Button btn && btn.DataContext is OverlayWindowViewModel vm)
{
RaiseEvent(new ElementMoveActionEventArgs(DownMoveClickEvent, OverlayWindowViewModels.IndexOf(vm)));
}
}
}
}
@@ -0,0 +1,32 @@
<UserControl x:Class="FancyInput.Views.Controls.OutputPanel"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:FancyInput.Views.Controls"
xmlns:vm ="clr-namespace:FancyInput.ViewModels"
xmlns:cm="clr-namespace:FancyInput.Common"
xmlns:uc="clr-namespace:FancyInput.Views.Controls"
mc:Ignorable="d" x:Name="Root"
d:DesignHeight="450" d:DesignWidth="800">
<Grid>
<RichTextBox x:Name="OutputRichTextBox" IsReadOnly="True" VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Auto"
Background="{Binding BackgroundColor,ElementName=Root}" Padding="2"
PreviewMouseWheel="OutputRichTextBox_PreviewMouseWheel"
FontSize="{Binding FontSize,ElementName=Root}" FontFamily="Consolas, Cascadia Mono, Lucida Console, Courier New">
<RichTextBox.Resources>
<Style TargetType="ScrollViewer" BasedOn="{StaticResource SimpleScrollViewerStyle}" />
</RichTextBox.Resources>
<FlowDocument x:Name="MainFlowDocument"/>
</RichTextBox>
<uc:IconButton x:Name="CopyButton" Width="25" Height="25" HorizontalAlignment="Right" VerticalAlignment="Top"
ProfileBrush="Gray" ProfileThickness="1" CornerRadius="1" Margin="40,4"
PathData="{x:Static cm:PathDataGeometry.Copy}" PathFill="{Binding ForegroundColor,ElementName=Root}"
IconMargin="4" ToolTip="清空" Click="Copy"/>
<uc:IconButton Width="25" Height="25" HorizontalAlignment="Right" VerticalAlignment="Top"
ProfileBrush="Gray" ProfileThickness="1" CornerRadius="1" Margin="10,4"
PathData="{x:Static cm:PathDataGeometry.Broom}" PathFill="{Binding ForegroundColor,ElementName=Root}"
IconMargin="4" ToolTip="清空" Click="IconButton_Click"/>
</Grid>
</UserControl>
@@ -0,0 +1,134 @@
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using static System.Net.Mime.MediaTypeNames;
using Application = System.Windows.Application;
using FancyInput.Models;
using FancyInput.Common;
namespace FancyInput.Views.Controls
{
public enum OutputLevel
{
/// <summary>普通信息(白色)</summary>
Info,
/// <summary>调试日志(灰色)</summary>
Debug,
/// <summary>成功信息(绿色)</summary>
Success,
/// <summary>警告信息(黄色)</summary>
Warn,
/// <summary>错误信息(红色)</summary>
Error
}
/// <summary>
/// OutputPanel.xaml 的交互逻辑
/// </summary>
public partial class OutputPanel : UserControl
{
public static readonly DependencyProperty BackgroundColorProperty = DependencyProperty.Register(
nameof(BackgroundColor), typeof(Brush), typeof(OutputPanel),
new PropertyMetadata(Brushes.Black));
public Brush BackgroundColor
{
get => (Brush)GetValue(BackgroundColorProperty);
set => SetValue(BackgroundColorProperty, value);
}
public static readonly DependencyProperty ForegroundColorProperty = DependencyProperty.Register(
nameof(ForegroundColor), typeof(Brush), typeof(OutputPanel),
new PropertyMetadata(Brushes.White));
public Brush ForegroundColor
{
get => (Brush)GetValue(ForegroundColorProperty);
set => SetValue(ForegroundColorProperty, value);
}
public OutputPanel()
{
InitializeComponent();
InitFlowDocument();
}
private Paragraph _outputParagraph = null!;
private void InitFlowDocument()
{
_outputParagraph = new Paragraph { Margin = new Thickness(0) };
MainFlowDocument.Blocks.Add(_outputParagraph);
}
private static readonly Dictionary<OutputLevel, Brush> LevelColors = new()
{
[OutputLevel.Info] = Brushes.White,
[OutputLevel.Debug] = Brushes.Gray,
[OutputLevel.Success] = Brushes.Green,
[OutputLevel.Warn] = Brushes.Yellow,
[OutputLevel.Error] = Brushes.Red,
};
public void Write(string text, OutputLevel level = OutputLevel.Info, bool showTimestamp = true)
{
Application.Current.Dispatcher.Invoke(() =>
{
// 非首行时先插入换行
if (_outputParagraph.Inlines.Count > 0)
_outputParagraph.Inlines.Add(new LineBreak());
if (showTimestamp)
{
string timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff");
_outputParagraph.Inlines.Add(new Run($"[{timestamp}] ") { Foreground = Brushes.Gray });
}
_outputParagraph.Inlines.Add(new Run(text)
{
Foreground = LevelColors.TryGetValue(level, out var brush) ? brush : ForegroundColor
});
OutputRichTextBox.ScrollToEnd();
});
}
public void ClearOutput()
{
Application.Current.Dispatcher.Invoke(() =>
{
_outputParagraph.Inlines.Clear();
});
}
private void IconButton_Click(object sender, RoutedEventArgs e)
{
_outputParagraph.Inlines.Clear(); // 原来: MainFlowDocument.Blocks.Clear();
}
private void OutputRichTextBox_PreviewMouseWheel(object sender, System.Windows.Input.MouseWheelEventArgs e)
{
if (Keyboard.Modifiers == ModifierKeys.Control)
{
int targetFontSize = (int)FontSize;
if (e.Delta > 0)
targetFontSize += 1;
else if (e.Delta < 0 )
targetFontSize -= 1;
targetFontSize = Math.Min(Math.Max(targetFontSize, 8), 30);
FontSize = targetFontSize;
e.Handled = true;
}
}
private async void Copy(object sender, RoutedEventArgs e)
{
string textToCopy = new TextRange(MainFlowDocument.ContentStart, MainFlowDocument.ContentEnd).Text;
bool success = Utility.TrySetClipboardText(textToCopy, out string errorDetail);
if (!success)
{
AppMessageBox.Show($"无法将文本复制到剪贴板。\n{errorDetail}", "复制失败", MessageBoxButton.OK, MessageBoxImage.Error);
}
else
{
CopyButton.PathData = PathDataGeometry.CheckSolid;
await Task.Delay(200);
CopyButton.PathData = PathDataGeometry.Copy;
}
}
}
}
@@ -0,0 +1,34 @@
<UserControl x:Class="FancyInput.Views.Controls.RGBColorSelect"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:FancyInput.Views.Controls"
mc:Ignorable="d"
d:DesignHeight="200" d:DesignWidth="300" SnapsToDevicePixels="True">
<Grid>
<StackPanel Orientation="Horizontal">
<Grid x:Name="MainGrid" Width="200" Height="200" MouseEnter="DisableDrag" MouseLeave="DisableDrag"/>
<Grid x:Name="HueGridAndBar" Width="30" Height="200" Margin="10,0,0,0">
<Grid x:Name="HueGrid" Width="30" Height="200" MouseEnter="DisableDrag" MouseLeave="DisableDrag"
PreviewMouseDown="HueGrid_PreviewMouseDown" PreviewMouseMove="HueGrid_PreviewMouseMove"/>
<Border Background="#7FFFFFFF" BorderBrush="White" BorderThickness="0.5"
Height="3" HorizontalAlignment="Stretch"
RenderTransformOrigin="0.5,0.5" VerticalAlignment="Bottom"
IsHitTestVisible="False">
<Border.RenderTransform>
<TransformGroup>
<SkewTransform/>
<RotateTransform/>
<TranslateTransform Y="{Binding ElementName=MainSlider, Path=Value}"/>
<ScaleTransform ScaleY="-1"/>
</TransformGroup>
</Border.RenderTransform>
</Border>
</Grid>
<Slider x:Name="MainSlider" Orientation="Vertical" Width="30" Height="210" Margin="5,0,0,0"
Minimum="0" Maximum="200" ValueChanged="MainSlider_ValueChanged" Style="{StaticResource VerticalMiniSlider}"/>
</StackPanel>
</Grid>
</UserControl>
@@ -0,0 +1,255 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace FancyInput.Views.Controls
{
/// <summary>
/// RGBColorSelect.xaml 的交互逻辑
/// </summary>
public partial class RGBColorSelect : UserControl
{
private const int _xNumber = 50;
private const int _yNumber = 50;
//private const double _mainLabelWidth = 200;
//private const double _mainLabelHeight = 200;
//private const double _hueGridWidth = 30;
//private const double _hueGridHeight = 200;
private bool _allowDrag = false;
public Color SelectedColor = Color.FromRgb(0, 0, 0);
public event Action<object,Color>? SelectedColorChanged;
List<List<Rectangle>> rectangles = new List<List<Rectangle>>();
public RGBColorSelect()
{
InitializeComponent();
InitGrid(MainGrid);
InitHueGrid(HueGrid);
}
private void InitGrid(Grid grid, float hue=1)
{
grid.Children.Clear();
double labelWidth = grid.Width/_xNumber;
double labelHeight = grid.Height / _yNumber;
for (int i=0;i<_xNumber;i++)
{
ColumnDefinition colDef = new ColumnDefinition();
colDef.Width = new GridLength(1, GridUnitType.Star);
grid.ColumnDefinitions.Add(colDef);
}
for (int j=0;j<_yNumber;j++)
{
RowDefinition rowDef = new RowDefinition();
rowDef.Height = new GridLength(1, GridUnitType.Star);
grid.RowDefinitions.Add(rowDef);
}
for (int i = 0; i < _xNumber; i++)
{
List<Rectangle> singleCol= new List<Rectangle>();
for (int j = 0; j < _yNumber; j++)
{
Rectangle rectangle = new Rectangle()
{
Fill = new SolidColorBrush(HsbToRgb(hue, (float)i / _xNumber,
(float)(_yNumber - j) / _yNumber)),
Stroke = new SolidColorBrush(Color.FromRgb(255, 255, 255)),
StrokeThickness = 0,
HorizontalAlignment = HorizontalAlignment.Stretch,
VerticalAlignment = VerticalAlignment.Stretch,
};
rectangle.MouseDown += Rectangle_MouseDown;
rectangle.MouseUp += Rectangle_MouseUp;
rectangle.MouseEnter += Rectangle_MouseEnter;
rectangle.MouseLeave += Rectangle_MouseLeave;
singleCol.Add(rectangle);
}
rectangles.Add(singleCol);
}
for (int i = 0; i < _xNumber; i++)
{
for (int j = 0; j < _yNumber; j++)
{
Rectangle rectangle = rectangles[i][j];
grid.Children.Add(rectangle);
Grid.SetColumn(rectangle, i);
Grid.SetRow(rectangle, j);
}
}
}
private void InitHueGrid(Grid grid)
{
double labelWidth = grid.Width;
double labelHeight = grid.Height / _yNumber;
for (int j = 0; j < _yNumber; j++)
{
RowDefinition rowDef = new RowDefinition();
rowDef.Height = new GridLength(1, GridUnitType.Star);
grid.RowDefinitions.Add(rowDef);
}
for (int j = 0; j < _yNumber; j++)
{
Rectangle rectangle = new Rectangle()
{
Fill = new SolidColorBrush(HsbToRgb(((float)(_yNumber- j)/_yNumber)*359, 1, 1)),
Stroke = new SolidColorBrush(Color.FromRgb(255, 255, 255)),
StrokeThickness = 0,
HorizontalAlignment = HorizontalAlignment.Stretch,
VerticalAlignment = VerticalAlignment.Stretch,
};
rectangle.MouseDown += Rectangle_MouseDown;
rectangle.MouseUp += Rectangle_MouseUp;
rectangle.MouseEnter += Rectangle_MouseEnter;
rectangle.MouseLeave += Rectangle_MouseLeave;
grid.Children.Add(rectangle);
Grid.SetRow(rectangle, j);
}
}
private void Rectangle_MouseEnter(object sender, MouseEventArgs e)
{
Rectangle? rectangle = (Rectangle)sender;
if (rectangle==null) return;
rectangle.StrokeThickness = 1;
SolidColorBrush? brush = ((Rectangle)sender).Fill as SolidColorBrush;
if (brush != null && _allowDrag)
{
SelectedColor = brush.Color;
SelectedColorChanged?.Invoke(this, SelectedColor);
}
}
private void Rectangle_MouseLeave(object sender, MouseEventArgs e)
{
Rectangle rectangle = (Rectangle)sender;
rectangle.StrokeThickness = 0;
}
private void Rectangle_MouseDown(object sender, MouseButtonEventArgs e)
{
_allowDrag = true;
SolidColorBrush? brush = ((Rectangle)sender).Fill as SolidColorBrush;
if (brush != null)
{
SelectedColor = brush.Color;
SelectedColorChanged?.Invoke(this, SelectedColor);
}
}
private void Rectangle_MouseUp(object sender, MouseButtonEventArgs e)
{
_allowDrag = false;
}
public static Color HsbToRgb(float hue, float saturation, float brightness)
{
float r = 0, g = 0, b = 0;
if (saturation == 0)
{
r = g = b = brightness;
}
else
{
float sectorPos = hue / 60.0f;
int sectorNumber = (int)(Math.Floor(sectorPos));
// 计算扇区内的相对位置
float fractionalSector = sectorPos - sectorNumber;
float p = brightness * (1.0f - saturation);
float q = brightness * (1.0f - (saturation * fractionalSector));
float t = brightness * (1.0f - (saturation * (1 - fractionalSector)));
switch (sectorNumber)
{
case 0:
r = brightness;
g = t;
b = p;
break;
case 1:
r = q;
g = brightness;
b = p;
break;
case 2:
r = p;
g = brightness;
b = t;
break;
case 3:
r = p;
g = q;
b = brightness;
break;
case 4:
r = t;
g = p;
b = brightness;
break;
case 5:
r = brightness;
g = p;
b = q;
break;
}
}
return Color.FromArgb(255, (byte)(r * 255), (byte)(g * 255), (byte)(b * 255));
}
private void MainSlider_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
{
Slider? slider = sender as Slider;
if (slider == null) return;
for (int i = 0; i < _xNumber; i++)
{
for (int j = 0; j < _yNumber; j++)
{
Rectangle rctangle = rectangles[i][j];
rctangle.Fill = new SolidColorBrush(HsbToRgb((float)(slider.Value*1.79),
(float)i / _xNumber,
(float)(_yNumber - j) / _yNumber));
}
}
SolidColorBrush brush = new SolidColorBrush(HsbToRgb((float)(slider.Value * 1.79), 1, 1));
SelectedColor = brush.Color;
SelectedColorChanged?.Invoke(this, SelectedColor);
}
private void DisableDrag(object sender, MouseEventArgs e)
{
_allowDrag = false;
}
private void HueGrid_PreviewMouseDown(object sender, MouseButtonEventArgs e)
{
Point point = e.GetPosition(HueGrid);
MainSlider.Value = HueGrid.ActualHeight - point.Y;
}
private void HueGrid_PreviewMouseMove(object sender, MouseEventArgs e)
{
if (!_allowDrag) return;
Point point = e.GetPosition(HueGrid);
MainSlider.Value = HueGrid.ActualHeight - point.Y;
}
}
}
+114
View File
@@ -0,0 +1,114 @@
<UserControl x:Class="FancyInput.Views.Controls.ReadPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:FancyInput.Views.Controls"
xmlns:md="clr-namespace:FancyInput.Models"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<UserControl.Resources>
<local:LoadModeToVisibilityConverter x:Key="PngJsonVisibilityConverter" TargetMode="PngJson"/>
<local:LoadModeToVisibilityConverter x:Key="ProjectFileVisibilityConverter" TargetMode="ProjectFile"/>
</UserControl.Resources>
<StackPanel Orientation="Vertical">
<Grid Margin="20,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="1200*"/>
<ColumnDefinition Width="100*"/>
</Grid.ColumnDefinitions>
<Grid Grid.Column="0" Visibility="{Binding LoadMode, RelativeSource={RelativeSource AncestorType=UserControl}, Converter={StaticResource PngJsonVisibilityConverter}}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="1000*"/>
<ColumnDefinition Width="200*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="500*"/>
<RowDefinition Height="500*"/>
</Grid.RowDefinitions>
<TextBox Grid.Row="0" Grid.Column="0" FontSize="15" Height="50" Margin="10"
Text="{Binding PngFilePath, RelativeSource={RelativeSource AncestorType=UserControl}, Mode=TwoWay}"
FontFamily="Cascadia Mono" Style="{StaticResource FancyTextBox}"
HorizontalContentAlignment="Left" VerticalContentAlignment="Center"
AllowDrop="True" Drop="PngTextBox_Drop" PreviewDragOver="TextBox_PreviewDragOver"/>
<Button Grid.Row="0" Grid.Column="1" Content="Png" Margin="10"
Height="50" FontSize="20" FontFamily="Cascadia Mono"
Click="TextureButton_Click" Style="{StaticResource FancyButton}"
AllowDrop="True" Drop="PngTextBox_Drop" PreviewDragOver="TextBox_PreviewDragOver"/>
<TextBox Grid.Row="1" Grid.Column="0" FontSize="15" Height="50" Margin="10"
Text="{Binding JsonFilePath, RelativeSource={RelativeSource AncestorType=UserControl}, Mode=TwoWay}"
FontFamily="Cascadia Mono" Style="{StaticResource FancyTextBox}"
AllowDrop="True" Drop="JsonTextBox_Drop" PreviewDragOver="TextBox_PreviewDragOver"
HorizontalContentAlignment="Left" VerticalContentAlignment="Center"/>
<Button Grid.Row="1" Grid.Column="1" Content="JSON" Margin="10"
Height="50" FontSize="20" FontFamily="Cascadia Mono"
Click="ConfigButton_Click" Style="{StaticResource FancyButton}"
AllowDrop="True" Drop="PngTextBox_Drop" PreviewDragOver="TextBox_PreviewDragOver"/>
</Grid>
<Grid Grid.Column="0" Visibility="{Binding LoadMode, RelativeSource={RelativeSource AncestorType=UserControl}, Converter={StaticResource ProjectFileVisibilityConverter}}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="1000*"/>
<ColumnDefinition Width="200*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="500*"/>
<RowDefinition Height="500*"/>
</Grid.RowDefinitions>
<TextBox Grid.Row="0" Grid.Column="0" Grid.RowSpan="2" FontSize="15" Height="50" Margin="10"
Text="{Binding ProjectFilePath, RelativeSource={RelativeSource AncestorType=UserControl}, Mode=TwoWay}"
FontFamily="Cascadia Mono" Style="{StaticResource FancyTextBox}"
HorizontalContentAlignment="Left" VerticalContentAlignment="Center"
AllowDrop="True" Drop="PngTextBox_Drop" PreviewDragOver="TextBox_PreviewDragOver"/>
<Button Grid.Row="0" Grid.Column="1" Grid.RowSpan="2" Content="Fip" Margin="10"
Height="50" FontSize="20" FontFamily="Cascadia Mono"
Click="ChoseProjectFile" Style="{StaticResource FancyButton}"
AllowDrop="True" Drop="ProjectFileTextBox_Drop" PreviewDragOver="TextBox_PreviewDragOver"/>
</Grid>
<Button Grid.Column="1" Grid.Row="0" Grid.RowSpan="2" Height="40" FontFamily="Cascadia Mono" FontSize="30" Margin="0,2"
Style="{StaticResource RoundCornerButton}" Click="ChangeLoadMode">
<Viewbox Height="25" Width="25">
<Path Data="M21.71,9.29l-4-4a1,1,0,0,0-1.42,1.42L18.59,9H7a1,1,0,0,0,0,2H21a1,1,0,0,0,.92-.62A1,1,0,0,0,21.71,9.29ZM17,13H3a1,1,0,0,0-.92.62,1,1,0,0,0,.21,1.09l4,4a1,1,0,0,0,1.42,0,1,1,0,0,0,0-1.42L5.41,15H17a1,1,0,0,0,0-2Z"
StrokeThickness="1" Stroke="White" RenderTransformOrigin="0.5,0.5" Fill="White"/>
</Viewbox>
</Button>
</Grid>
<Grid Margin="15,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="500*"/>
<ColumnDefinition Width="500*"/>
</Grid.ColumnDefinitions>
<Button Height="40" FontFamily="Cascadia Mono" FontSize="30" Margin="10,2" Grid.Column="0"
Style="{StaticResource FancyButton}" Click="LoadConfig">
<StackPanel Orientation="Horizontal">
<Viewbox Height="25" Width="25">
<Path Data="M73 39c-14.8-9.1-33.4-9.4-48.5-.9S0 62.6 0 80V432c0 17.4 9.4 33.4 24.5 41.9s33.7 8.1 48.5-.9L361 297c14.3-8.7 23-24.2 23-41s-8.7-32.2-23-41L73 39z"
StrokeThickness="10" Stroke="White" RenderTransformOrigin="0.5,0.5" Fill="White">
</Path>
</Viewbox>
<Label Content="加载" Foreground="White" FontSize="20" />
</StackPanel>
</Button>
<Button Height="40" FontFamily="Cascadia Mono" FontSize="30" Margin="10,2" Grid.Column="1"
Style="{StaticResource FancyButton}" Click="EditConfig">
<StackPanel Orientation="Horizontal">
<Viewbox Height="25" Width="25">
<Path Data="M475 100H125A25 25 0 0 1 125 50H475A25 25 0 0 1 475 100zM125 150H127.25L231.5 159.5A50 50 0 0 1 261.75 173.75L486.75 398.75A48.00000000000001 48.00000000000001 0 0 1 484.9999999999999 466.5L416.5 535A50 50 0 0 1 350 536.75L125 311.75A50 50 0 0 1 110.75 281.5000000000001L100 177.25A25 25 0 0 1 107.25 157.25A25 25 0 0 1 125 150zM381.75 500L450 431.75L400 383L333 450z"
StrokeThickness="10" Stroke="White" RenderTransformOrigin="0.5,0.5" Fill="White">
<Path.RenderTransform>
<TransformGroup>
<ScaleTransform ScaleY="-1"/>
<SkewTransform/>
<RotateTransform/>
<TranslateTransform/>
</TransformGroup>
</Path.RenderTransform>
</Path>
</Viewbox>
<Label Content="修改" Foreground="White" FontSize="20" />
</StackPanel>
</Button>
</Grid>
</StackPanel>
</UserControl>
+211
View File
@@ -0,0 +1,211 @@
using FancyInput.ViewModels;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace FancyInput.Views.Controls
{
/// <summary>
/// ReadPage.xaml 的交互逻辑
/// </summary>
public partial class ReadPage : UserControl
{
// PngPath
public static readonly DependencyProperty PngPathProperty = DependencyProperty.Register( nameof(PngFilePath),
typeof(string), typeof(ReadPage), new PropertyMetadata(string.Empty));
public string PngFilePath
{
get => (string)GetValue(PngPathProperty);
set => SetValue(PngPathProperty, value);
}
// JsonPath
public static readonly DependencyProperty JsonPathProperty = DependencyProperty.Register(nameof(JsonFilePath),
typeof(string), typeof(ReadPage), new PropertyMetadata(string.Empty));
public string JsonFilePath
{
get => (string)GetValue(JsonPathProperty);
set => SetValue(JsonPathProperty, value);
}
// ProjectFilePath
public static readonly DependencyProperty ProjectFilePathProperty = DependencyProperty.Register(nameof(ProjectFilePath),
typeof(string), typeof(ReadPage), new PropertyMetadata(string.Empty));
public string ProjectFilePath
{
get => (string)GetValue(ProjectFilePathProperty);
set => SetValue(ProjectFilePathProperty, value);
}
// LoadMode
public static readonly DependencyProperty LoadModeProperty = DependencyProperty.Register(nameof(LoadMode),
typeof(LoadMode), typeof(ReadPage), new PropertyMetadata(LoadMode.PngJson));
public LoadMode LoadMode
{
get => (LoadMode)GetValue(LoadModeProperty);
set => SetValue(LoadModeProperty, value);
}
// ConfigLoadClick
public static readonly RoutedEvent ConfigLoadClickEvent = EventManager.RegisterRoutedEvent( nameof(ConfigLoadClick), RoutingStrategy.Bubble,
typeof(RoutedEventHandler), typeof(ReadPage));
public event RoutedEventHandler ConfigLoadClick
{
add => AddHandler(ConfigLoadClickEvent, value);
remove => RemoveHandler(ConfigLoadClickEvent, value);
}
// ConfigEditClick
public static readonly RoutedEvent ConfigEditClickEvent = EventManager.RegisterRoutedEvent( nameof(ConfigEditClick), RoutingStrategy.Bubble,
typeof(RoutedEventHandler), typeof(ReadPage));
public event RoutedEventHandler ConfigEditClick
{
add => AddHandler(ConfigEditClickEvent, value);
remove => RemoveHandler(ConfigEditClickEvent, value);
}
public ReadPage()
{
InitializeComponent();
}
private void PngTextBox_Drop(object sender, DragEventArgs e)
{
if (e.Data.GetDataPresent(DataFormats.FileDrop))
{
var files = (string[])e.Data.GetData(DataFormats.FileDrop);
if (files.Length > 0)
{
PngFilePath = files[0];
}
}
}
private void JsonTextBox_Drop(object sender, DragEventArgs e)
{
if (e.Data.GetDataPresent(DataFormats.FileDrop))
{
var files = (string[])e.Data.GetData(DataFormats.FileDrop);
if (files.Length > 0)
{
JsonFilePath = files[0];
}
}
}
private void ProjectFileTextBox_Drop(object sender, DragEventArgs e)
{
if (e.Data.GetDataPresent(DataFormats.FileDrop))
{
var files = (string[])e.Data.GetData(DataFormats.FileDrop);
if (files.Length > 0)
{
ProjectFilePath = files[0];
}
}
}
private void TextBox_PreviewDragOver(object sender, DragEventArgs e)
{
if (e.Data.GetDataPresent(DataFormats.FileDrop))
{
e.Effects = DragDropEffects.Copy;
}
else
{
e.Effects = DragDropEffects.None;
}
e.Handled = true;
}
private void TextureButton_Click(object sender, RoutedEventArgs e)
{
var dlg = new Microsoft.Win32.OpenFileDialog
{
Filter = "PNG 图片 (*.png)|*.png",
Title = "选择 PNG 文件"
};
if (dlg.ShowDialog() == true)
{
string filePath = dlg.FileName;
PngFilePath = filePath;
}
}
private void ConfigButton_Click(object sender, RoutedEventArgs e)
{
var dlg = new Microsoft.Win32.OpenFileDialog
{
Filter = "JSON 配置文件 (*.json)|*.json",
Title = "选择 JSON 文件"
};
if (dlg.ShowDialog() == true)
{
string filePath = dlg.FileName;
JsonFilePath = filePath;
}
}
private void ChoseProjectFile(object sender, RoutedEventArgs e)
{
var dlg = new Microsoft.Win32.OpenFileDialog
{
Filter = "FancyInput 项目文件 (*.fip)|*.fip",
Title = "选择 FancyInput 项目文件"
};
if (dlg.ShowDialog() == true)
{
string filePath = dlg.FileName;
ProjectFilePath = filePath;
}
}
private void ChangeLoadMode(object sender, RoutedEventArgs e)
{
LoadMode = LoadMode == LoadMode.PngJson ? LoadMode.ProjectFile : LoadMode.PngJson;
}
private void LoadConfig(object sender, RoutedEventArgs e)
{
RaiseEvent(new RoutedEventArgs(ConfigLoadClickEvent));
}
private void EditConfig(object sender, RoutedEventArgs e)
{
RaiseEvent(new RoutedEventArgs(ConfigEditClickEvent));
}
}
public class LoadModeToVisibilityConverter : IValueConverter
{
public LoadMode TargetMode { get; set; } = LoadMode.PngJson;
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is LoadMode mode)
return mode == TargetMode ? Visibility.Visible : Visibility.Collapsed;
return Visibility.Collapsed;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}
+195
View File
@@ -0,0 +1,195 @@
<UserControl x:Class="FancyInput.Views.Controls.SettingsPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:FancyInput.Views.Controls"
xmlns:md="clr-namespace:FancyInput.Models"
xmlns:vm ="clr-namespace:FancyInput.ViewModels"
d:DataContext="{d:DesignInstance Type=vm:MainWindowViewModel}"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<Border CornerRadius="10" BorderBrush="Gray" BorderThickness="1" >
<ScrollViewer Margin="5" Style="{StaticResource SimpleScrollViewerStyle}">
<StackPanel Orientation="Vertical">
<Grid Margin="10,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="2*" />
<ColumnDefinition Width="2*" />
</Grid.ColumnDefinitions>
<StackPanel Orientation="Horizontal">
<Label Content="以管理员身份运行" FontSize="15"
HorizontalAlignment="Left" VerticalAlignment="Center" Grid.Column="0"/>
<Button Style="{StaticResource InfoTipButtonStyle}" ToolTip="当具有管理员权限的软件在前台运行时,如果本应用以非管理员身份运行,可能导致输入捕获失败;&#x0a;注意:从非管理员提升至管理员权限后,此次打开的应用默认保持管理员权限状态,不受此选项关闭的影响。"/>
</StackPanel>
<Viewbox Width="50" Grid.Column="1" HorizontalAlignment="Right" VerticalAlignment="Center" Margin="10,0">
<CheckBox Style="{StaticResource SimpleToggleSwitch}" IsChecked="{Binding RunAsAdmin}"/>
</Viewbox>
</Grid>
<Grid Margin="10,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="2*" />
<ColumnDefinition Width="2*" />
</Grid.ColumnDefinitions>
<StackPanel Orientation="Horizontal">
<Label Content="鼠标移动-相对屏幕中心" FontSize="15"
HorizontalAlignment="Left" VerticalAlignment="Center" Grid.Column="0"/>
<Button Style="{StaticResource InfoTipButtonStyle}" ToolTip="Dot模式下,开启时,使用鼠标相对屏幕中心的绝对位置,关闭时则使用每一帧鼠标的相对位移"/>
</StackPanel>
<Viewbox Width="50" Grid.Column="1" HorizontalAlignment="Right" VerticalAlignment="Center" Margin="10,0">
<CheckBox Style="{StaticResource SimpleToggleSwitch}" IsChecked="{Binding UseCenterMouseMove}"/>
</Viewbox>
</Grid>
<Grid Margin="10,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="2*" />
<ColumnDefinition Width="2*" />
</Grid.ColumnDefinitions>
<StackPanel Orientation="Horizontal">
<Label Content="鼠标移动-锁中心游戏模式" FontSize="15"
HorizontalAlignment="Left" VerticalAlignment="Center" Grid.Column="0"/>
<Button Style="{StaticResource InfoTipButtonStyle}" ToolTip="部分游戏会强制将鼠标位置锁定在屏幕中心,为了让鼠标移动输入检测在这些游戏中也能正常使用,可以开启此选项;&#x0a;注意:此功能可能会造成本应用卡顿,建议不用时保持关闭。"/>
</StackPanel>
<Viewbox Width="50" Grid.Column="1" HorizontalAlignment="Right" VerticalAlignment="Center" Margin="10,0">
<CheckBox Style="{StaticResource SimpleToggleSwitch}" IsChecked="{Binding PreventMouseCentering}"/>
</Viewbox>
</Grid>
<Grid Margin="10,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="2*" />
<ColumnDefinition Width="2*" />
</Grid.ColumnDefinitions>
<StackPanel Orientation="Horizontal">
<Label Content="输入检测轮询周期" FontSize="15"
HorizontalAlignment="Left" VerticalAlignment="Center" Grid.Column="0"/>
<Button Style="{StaticResource InfoTipButtonStyle}" ToolTip="值越小,对输入的检测越灵敏,但值过小会导致程序卡顿;&#x0a;受此项影响的元素包括手柄操作、鼠标滚轮等。" />
</StackPanel>
<StackPanel Grid.Column="1" Orientation="Horizontal" HorizontalAlignment="Right" VerticalAlignment="Center" >
<Slider Style="{StaticResource MiniSlider}" Value="{Binding TimerTickInterval}" Width="150"
Minimum="8" Maximum="200" />
<TextBox Style="{StaticResource RoundCornerTextBox}" Text="{Binding TimerTickIntervalString,Mode=OneWay}"
IsReadOnly="True" Margin="10,0"
Height="20" FontSize="10" Width="35"/>
<Label Content="ms" VerticalAlignment="Center" Padding="0" Width="15"/>
</StackPanel>
</Grid>
<Grid Margin="10,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="2*" />
<ColumnDefinition Width="2*" />
</Grid.ColumnDefinitions>
<StackPanel Orientation="Horizontal">
<Label Content="鼠标灵敏度" FontSize="15"
HorizontalAlignment="Left" VerticalAlignment="Center" Grid.Column="0"/>
<Button Style="{StaticResource InfoTipButtonStyle}" ToolTip="值越小,鼠标移动检测更加灵敏" />
</StackPanel>
<StackPanel Grid.Column="1" Orientation="Horizontal" HorizontalAlignment="Right" VerticalAlignment="Center" >
<Slider Style="{StaticResource MiniSlider}" Value="{Binding MouseMoveSensitivity}" Width="150"
Minimum="1" Maximum="50" TickFrequency="1"/>
<TextBox Style="{StaticResource RoundCornerTextBox}" Text="{Binding MouseMoveSensitivity}" Margin="10,0"
Height="20" FontSize="15" Width="35" IsReadOnly="True"/>
<Label Content=" " VerticalAlignment="Center" Padding="0" Width="15"/>
</StackPanel>
</Grid>
<Grid Margin="10,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="1.3*" />
<ColumnDefinition Width="1.1*" />
<ColumnDefinition Width="5*" />
</Grid.ColumnDefinitions>
<StackPanel Orientation="Horizontal">
<Label Content="输入检测" FontSize="15"
HorizontalAlignment="Left" VerticalAlignment="Center" Grid.Column="0"/>
<Button Style="{StaticResource InfoTipButtonStyle}" ToolTip="请及时关闭,保持开启容易导致程序卡顿" />
</StackPanel>
<Viewbox Grid.Column="1" Width="50" Margin="10,0">
<CheckBox Style="{StaticResource SimpleToggleSwitch}"
HorizontalAlignment="Right" VerticalAlignment="Center"
IsChecked="{Binding DetectInput}"/>
</Viewbox>
<TextBox Style="{StaticResource RoundCornerTextBox}" Grid.Column="2"
Margin="0,0,10,0" Height="20" FontSize="8" Padding="0"
HorizontalContentAlignment="Left" VerticalContentAlignment="Center"
Text="{Binding InputDetectString}"/>
</Grid>
<Grid Margin="10,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="2*" />
<ColumnDefinition Width="2*" />
</Grid.ColumnDefinitions>
<StackPanel Orientation="Horizontal">
<Label Content="退出时最小化到托盘" FontSize="15"
HorizontalAlignment="Left" VerticalAlignment="Center" Grid.Column="0"/>
</StackPanel>
<Viewbox Width="50" Grid.Column="1" HorizontalAlignment="Right" VerticalAlignment="Center" Margin="10,0">
<CheckBox Style="{StaticResource SimpleToggleSwitch}" IsChecked="{Binding MinimizeToTray}"/>
</Viewbox>
</Grid>
<Grid Margin="10,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="2*" />
<ColumnDefinition Width="2*" />
</Grid.ColumnDefinitions>
<StackPanel Orientation="Horizontal">
<Label Content="使用键码类型" FontSize="15"
HorizontalAlignment="Left" VerticalAlignment="Center" Grid.Column="0"/>
</StackPanel>
<Viewbox Width="80" Grid.Column="1" HorizontalAlignment="Right" VerticalAlignment="Center" Margin="10,0">
<ComboBox Height="30" Width="80" Margin="5,0"
Style="{StaticResource RoundCornerComboBox}" ItemContainerStyle="{StaticResource CustomComboBoxItemStyle}"
IsSynchronizedWithCurrentItem="False"
HorizontalContentAlignment="Center" VerticalContentAlignment="Center"
ItemsSource="{Binding KeyBoardMappingList}" SelectedItem="{Binding SelectedKeyBoardMapping}"/>
</Viewbox>
</Grid>
<Grid Margin="10,0" Visibility="{Binding UseNumpadAsArrowVisibility}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="2*" />
<ColumnDefinition Width="2*" />
</Grid.ColumnDefinitions>
<StackPanel Orientation="Horizontal">
<Label Content="允许小键盘作为功能键" FontSize="15"
HorizontalAlignment="Left" VerticalAlignment="Center" Grid.Column="0"/>
</StackPanel>
<Viewbox Width="50" Grid.Column="1" HorizontalAlignment="Right" VerticalAlignment="Center" Margin="10,0">
<CheckBox Style="{StaticResource SimpleToggleSwitch}" IsChecked="{Binding UseNumpadAsArrow}"/>
</Viewbox>
</Grid>
<Grid Margin="10,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="2*" />
<ColumnDefinition Width="2*" />
</Grid.ColumnDefinitions>
<StackPanel Orientation="Horizontal">
<Label Content="手柄输入方式" FontSize="15"
HorizontalAlignment="Left" VerticalAlignment="Center" Grid.Column="0"/>
</StackPanel>
<Viewbox Width="80" Grid.Column="1" HorizontalAlignment="Right" VerticalAlignment="Center" Margin="10,0">
<ComboBox Height="30" Width="80" Margin="5,0"
Style="{StaticResource RoundCornerComboBox}" ItemContainerStyle="{StaticResource CustomComboBoxItemStyle}"
IsSynchronizedWithCurrentItem="False"
HorizontalContentAlignment="Center" VerticalContentAlignment="Center"
ItemsSource="{Binding GamepadBackEndList}" SelectedItem="{Binding SelectedGamepadBackEnd}"/>
</Viewbox>
</Grid>
<Grid Margin="10,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="2*" />
<ColumnDefinition Width="2*" />
</Grid.ColumnDefinitions>
<StackPanel Orientation="Horizontal">
<Label Content="其他" FontSize="15"
HorizontalAlignment="Left" VerticalAlignment="Center" Grid.Column="0"/>
</StackPanel>
<StackPanel Grid.Column="1" Orientation="Horizontal" HorizontalAlignment="Right" VerticalAlignment="Center" >
<Button Style="{StaticResource RoundCornerButton}" Content="关于" FontSize="15" Width="50" Margin="10,0" Click="About"/>
</StackPanel>
</Grid>
</StackPanel>
</ScrollViewer>
</Border>
</UserControl>
@@ -0,0 +1,52 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace FancyInput.Views.Controls
{
/// <summary>
/// SettingsPage.xaml 的交互逻辑
/// </summary>
public partial class SettingsPage : UserControl
{
public SettingsPage()
{
InitializeComponent();
}
// AboutClick
public static readonly RoutedEvent AboutClickEvent = EventManager.RegisterRoutedEvent(nameof(AboutClick), RoutingStrategy.Bubble,
typeof(RoutedEventHandler), typeof(SettingsPage));
public event RoutedEventHandler AboutClick
{
add => AddHandler(AboutClickEvent, value);
remove => RemoveHandler(AboutClickEvent, value);
}
private void About(object sender, RoutedEventArgs e)
{
RaiseEvent(new RoutedEventArgs(AboutClickEvent));
}
//private void Help(object sender, RoutedEventArgs e)
//{
// var helpWindow = new ElementTreeHelpWindow
// {
// Owner = Application.Current.MainWindow,
// WindowStartupLocation = WindowStartupLocation.CenterOwner,
// };
// helpWindow.ShowDialog();
//}
}
}
+82
View File
@@ -0,0 +1,82 @@
<UserControl x:Class="FancyInput.Views.Controls.Terminal"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:FancyInput.Views.Controls"
xmlns:vm ="clr-namespace:FancyInput.ViewModels"
xmlns:cm="clr-namespace:FancyInput.Common"
xmlns:uc="clr-namespace:FancyInput.Views.Controls"
mc:Ignorable="d" d:DataContext="{d:DesignInstance Type=vm:TerminalViewModel}"
d:DesignHeight="450" d:DesignWidth="800"
x:Name="Root">
<UserControl.Resources>
<Style TargetType="TextBox" x:Key="TerminalTextBox">
<Setter Property="FontFamily" Value="Consolas, Cascadia Mono, Lucida Console, Courier New"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="TextWrapping" Value="NoWrap"/>
<Setter Property="IsReadOnly" Value="True"/>
</Style>
</UserControl.Resources>
<Grid PreviewMouseWheel="TerminalScroller_PreviewMouseWheel">
<Grid x:Name="TerminalPanel" >
<ScrollViewer x:Name="TerminalScroller" Loaded="TerminalScroller_Loaded"
MouseLeftButtonUp="TerminalScroller_MouseLeftButtonUp"
PreviewKeyDown="TerminalScroller_PreviewKeyDown"
Background="{Binding BackgroundColor}" Style="{StaticResource SimpleScrollViewerStyle}"
>
<ItemsControl ItemsSource="{Binding Commands}" x:Name="CommandItemsControl">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Vertical" HorizontalAlignment="Stretch"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate DataType="{x:Type vm:TerminalCommandViewModel}">
<StackPanel Orientation="Vertical">
<Grid HorizontalAlignment="Stretch">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBox Grid.Column="0" Text="{Binding WorkingDirectoryShort,Mode=OneWay}" Style="{StaticResource TerminalTextBox}"
Foreground="Green"
Background="{Binding ElementName=Root, Path=DataContext.BackgroundColor}"
FontSize="{Binding ElementName=Root, Path=DataContext.FontSize}"/>
<TextBox Grid.Column="2" Text="{Binding Command,Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" IsReadOnly="{Binding IsReadOnly}"
Style="{StaticResource TerminalTextBox}" TextWrapping="Wrap" HorizontalAlignment="Stretch"
Foreground="{Binding ElementName=Root, Path=DataContext.ForegroundColor}"
Background="{Binding ElementName=Root, Path=DataContext.BackgroundColor}"
FontSize="{Binding ElementName=Root, Path=DataContext.FontSize}">
<TextBox.InputBindings>
<KeyBinding Key="Enter" Command="{Binding ExecuteCommand}" />
</TextBox.InputBindings>
</TextBox>
</Grid>
<TextBox Text="{Binding Stdout}" Visibility="{Binding StdoutVisibility}" Style="{StaticResource TerminalTextBox}" TextWrapping="Wrap"
Foreground="{Binding ElementName=Root, Path=DataContext.ForegroundColor}"
Background="{Binding ElementName=Root, Path=DataContext.BackgroundColor}"
FontSize="{Binding ElementName=Root, Path=DataContext.FontSize}"/>
<TextBox Text="{Binding StdErr}" Visibility="{Binding StdErrVisibility}" Style="{StaticResource TerminalTextBox}"
TextWrapping="Wrap" Foreground="Red"
Background="{Binding ElementName=Root, Path=DataContext.BackgroundColor}"
FontSize="{Binding ElementName=Root, Path=DataContext.FontSize}"/>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
<ComboBox ItemsSource="{Binding BackEndList}" SelectedItem="{Binding BackEnd}"
HorizontalAlignment="Right" VerticalAlignment="Top" FontSize="12"
Background="{Binding BackgroundColor}" Foreground="{Binding ForegroundColor}"
FontFamily="Consolas, Cascadia Mono, Lucida Console, Courier New" Margin="30,4"
Width="100" Height="25" Style="{StaticResource RoundCornerComboBox}"/>
<local:IconButton Width="25" Height="25" HorizontalAlignment="Right" VerticalAlignment="Top"
ProfileBrush="Gray" ProfileThickness="1" CornerRadius="1" Margin="140,4"
PathData="{x:Static cm:PathDataGeometry.Broom}" PathFill="{Binding ForegroundColor}"
IconMargin="4" ToolTip="清空" Command="{Binding ClearCommand}"/>
</Grid>
</Grid>
</UserControl>
+130
View File
@@ -0,0 +1,130 @@
using FancyInput.ViewModels;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace FancyInput.Views.Controls
{
/// <summary>
/// Terminal.xaml 的交互逻辑
/// </summary>
public partial class Terminal : UserControl
{
public TerminalViewModel ViewModel { get; set; } = new TerminalViewModel();
public Terminal()
{
InitializeComponent();
this.DataContext = ViewModel;
ViewModel.Commands.CollectionChanged += Commands_CollectionChanged;
ViewModel.StdOutErrUpdated += (s) =>
{
Dispatcher.BeginInvoke(new Action(() =>
{
TerminalScroller.ScrollToEnd();
}), System.Windows.Threading.DispatcherPriority.Loaded);
};
}
private void TerminalScroller_PreviewMouseWheel(object sender, MouseWheelEventArgs e)
{
if (Keyboard.Modifiers == ModifierKeys.Control)
{
// Ctrl + 滚轮 → 放大/缩小字体
if (e.Delta > 0)
ViewModel.FontSize += 1;
else if (e.Delta < 0 && ViewModel.FontSize > 1)
ViewModel.FontSize -= 1;
e.Handled = true; // 阻止滚动事件继续传递
}
}
private void TerminalScroller_Loaded(object sender, RoutedEventArgs e)
{
// 使用 Dispatcher 延迟,确保 ItemsControl 已完成布局生成容器
Dispatcher.BeginInvoke(new Action(FocusAndScrollToLastCommand),
System.Windows.Threading.DispatcherPriority.Loaded);
}
/// <summary>
/// 集合新增条目时 → 聚焦新命令行 + 滚动到底部
/// </summary>
private void Commands_CollectionChanged(object? sender,
System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
if (e.Action == System.Collections.Specialized.NotifyCollectionChangedAction.Add)
{
Dispatcher.BeginInvoke(new Action(FocusAndScrollToLastCommand),
System.Windows.Threading.DispatcherPriority.Loaded);
}
}
/// <summary>
/// 找到最后一条命令的可编辑 TextBox,聚焦并使光标闪烁;同时滚动到最底部
/// </summary>
private void FocusAndScrollToLastCommand()
{
if (ViewModel.Commands.Count == 0) return;
var lastIndex = ViewModel.Commands.Count - 1;
var container = CommandItemsControl.ItemContainerGenerator.ContainerFromIndex(lastIndex);
if (container != null)
{
var textBox = FindEditableTextBox(container);
if (textBox != null)
{
textBox.Focus();
Keyboard.Focus(textBox);
//textBox.CaretIndex = textBox.Text.Length;
}
}
TerminalScroller.ScrollToEnd();
}
/// <summary>
/// 递归遍历可视化树,找到第一个 IsReadOnly=false 的 TextBox
/// </summary>
private static TextBox? FindEditableTextBox(DependencyObject parent)
{
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(parent); i++)
{
var child = VisualTreeHelper.GetChild(parent, i);
if (child is TextBox tb && !tb.IsReadOnly)
return tb;
var result = FindEditableTextBox(child);
if (result != null)
return result;
}
return null;
}
private void TerminalScroller_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
FocusAndScrollToLastCommand();
}
private void TerminalScroller_PreviewKeyDown(object sender, KeyEventArgs e)
{
if (e.KeyboardDevice.Modifiers == ModifierKeys.Control && e.Key == Key.C)
{
ViewModel.CurrentCommandViewModel?.CancelCommand();
}
}
}
}
@@ -0,0 +1,74 @@
<UserControl x:Class="FancyInput.Views.Controls.WindowControlBar"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:FancyInput.Views.Controls"
mc:Ignorable="d"
d:DesignHeight="26" d:DesignWidth="400" Foreground="#FFFBFBFB">
<DockPanel Height="26" x:Name="BarGrid" >
<Button x:Name="CloseButton" Click="CloseButton_Click"
DockPanel.Dock="Right"
Style="{StaticResource CloseButton}"
Width="46" Padding="0" Height="26" BorderThickness="0" Background="Transparent">
<Path Stroke="{Binding Foreground, RelativeSource={RelativeSource AncestorType=UserControl}}"
StrokeThickness="1"
StrokeStartLineCap="Round"
StrokeEndLineCap="Round"
Data="M 18,8 L 28,18 M 18,18 L 28,8"
Width="46" Height="26" Margin="-1"/>
</Button>
<Button x:Name="NormalmizeButton" DockPanel.Dock="Right"
Style="{StaticResource NormalButton}"
Width="46" BorderThickness="0" Background="Transparent"
Click="NormalmizeButton_Click" Visibility="Collapsed">
<Grid Margin="-1">
<Path Stroke="{Binding Foreground, RelativeSource={RelativeSource AncestorType=UserControl}}"
StrokeThickness="1"
StrokeStartLineCap="Round"
StrokeEndLineCap="Round"
StrokeLineJoin="Round"
Data="M17,9 L27,9 L27,19 L17,19 Z"
Width="46" Height="26" />
<Path Stroke="{Binding Foreground, RelativeSource={RelativeSource AncestorType=UserControl}}"
StrokeThickness="1"
StrokeStartLineCap="Round"
StrokeEndLineCap="Round"
StrokeLineJoin="Round"
Data="M19,9 L19,7 L29,7 L29,17 L27,17"
Width="46" Height="26" />
</Grid>
</Button>
<Button x:Name="MaximizeButton" DockPanel.Dock="Right"
Style="{StaticResource NormalButton}"
Width="46" BorderThickness="0" Background="Transparent"
Click="MaximizeButton_Click" Visibility="Visible">
<Path Stroke="{Binding Foreground, RelativeSource={RelativeSource AncestorType=UserControl}}"
StrokeThickness="1"
StrokeStartLineCap="Round"
StrokeEndLineCap="Round"
StrokeLineJoin="Round"
Data="M18, 8 L 28,8 L28,18 L18,18 Z"
Width="46" Height="26" Margin="-1"/>
</Button>
<Button x:Name="MinimizeButton" DockPanel.Dock="Right"
Style="{StaticResource NormalButton}"
Width="46" BorderThickness="0" Background="Transparent"
Click="MinimizeButton_Click">
<Path Stroke="{Binding Foreground, RelativeSource={RelativeSource AncestorType=UserControl}}"
StrokeThickness="1"
StrokeStartLineCap="Round"
StrokeEndLineCap="Round"
StrokeLineJoin="Round"
Data="M18, 13 L 28,13"
Width="46" Height="26" Margin="-1"/>
</Button>
<Grid DockPanel.Dock="Right">
<Label x:Name="DragLabel" MouseLeftButtonDown="DragLabel_MouseLeftButtonDown"
MouseLeftButtonUp="DragLabel_MouseLeftButtonUp"
MouseMove="DragLabel_MouseMove"
MouseEnter="DragLabel_MouseEnter"
MouseLeave="DragLabel_MouseLeave"/>
</Grid>
</DockPanel>
</UserControl>
@@ -0,0 +1,217 @@
using System;
using System.Diagnostics;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Timers;
namespace FancyInput.Views.Controls
{
/// <summary>
/// WindowControlBar.xaml 的交互逻辑
/// </summary>
public partial class WindowControlBar : UserControl
{
public static readonly DependencyProperty ParentWindowProperty =
DependencyProperty.Register(
"ParentWindow",
typeof(Window),
typeof(WindowControlBar),
new PropertyMetadata(null)
);
public Window ParentWindow
{
get { return (Window)GetValue(ParentWindowProperty); }
set { SetValue(ParentWindowProperty, value); }
}
public int i = 0;
public static readonly DependencyProperty ResizeModeProperty =
DependencyProperty.Register("ResizeMode",
typeof(ResizeMode),
typeof(WindowControlBar),
new PropertyMetadata(ResizeMode.CanResize, OnResizeModeChanged)
);
public ResizeMode ResizeMode
{
get { return (ResizeMode)GetValue(ResizeModeProperty); }
set { SetValue(ResizeModeProperty, value); }
}
public WindowControlBar()
{
InitializeComponent();
}
// 属性改变回调函数
private static void OnResizeModeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
WindowControlBar myUserControl = (WindowControlBar)d;
ResizeMode newResizeMode = (ResizeMode)e.NewValue;
if (newResizeMode == ResizeMode.NoResize)
{
myUserControl.MaximizeButton.Visibility = Visibility.Collapsed;
myUserControl.NormalmizeButton.Visibility = Visibility.Collapsed;
}
else if(newResizeMode == ResizeMode.CanResize)
{
myUserControl.MaximizeButton.Visibility = Visibility.Visible;
myUserControl.NormalmizeButton.Visibility = Visibility.Visible;
Grid.SetColumn(myUserControl.MinimizeButton, 1);
}
}
private void CloseButton_Click(object sender, RoutedEventArgs e)
{
if (ParentWindow != null) { ParentWindow.Close(); }
}
private void MinimizeButton_Click(object sender, RoutedEventArgs e)
{
// Debug.WriteLine($"ResizeMode = {ResizeMode}");
if (ParentWindow != null) { ParentWindow.WindowState = WindowState.Minimized; }
}
private void MaximizeButton_Click(object sender, RoutedEventArgs e)
{
if (ParentWindow != null)
{
NormalmizeButton.Visibility = Visibility.Visible;
MaximizeButton.Visibility = Visibility.Collapsed;
ParentWindow.WindowState = WindowState.Maximized;
}
}
private void NormalmizeButton_Click(object sender, RoutedEventArgs e)
{
if (ParentWindow != null)
{
NormalmizeButton.Visibility = Visibility.Collapsed;
MaximizeButton.Visibility = Visibility.Visible;
ParentWindow.WindowState = WindowState.Normal;
}
}
private void DragLabel_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
_isMousePressed = true;
//if (ParentWindow == null) return;
//var mousePos = e.GetPosition(this);
//var lastMousePos = mousePos;
//var allowDrag = true;
//while (true)
//{
// var mousePosOnBar = e.GetPosition(this);
// if (lastMousePos != mousePos)
// break;
// if (Mouse.LeftButton != MouseButtonState.Pressed)
// {
// allowDrag = false;
// break;
// }
// Debug.WriteLine($"Mouse Position: {mousePosOnBar.X}, {mousePosOnBar.Y},lastMousePos: {lastMousePos.X}, {lastMousePos.Y},Mouse.LeftButton={Mouse.LeftButton}");
// lastMousePos = mousePosOnBar;
//}
//if (!allowDrag) return;
//if (ParentWindow.WindowState != WindowState.Maximized)
//{
// CompositionTarget.Rendering += WindowControlBar_Rendering;
// if (e.LeftButton == MouseButtonState.Pressed && ParentWindow != null) { ParentWindow.DragMove(); }
// CompositionTarget.Rendering -= WindowControlBar_Rendering;
//}
//else
//{
// var mousePosOnBar = e.GetPosition(this);
// var mousePosOnScreen = PointToScreen(mousePosOnBar);
// var width1 = ParentWindow.ActualWidth;
// NormalmizeButton.Visibility = Visibility.Collapsed;
// MaximizeButton.Visibility = Visibility.Visible;
// ParentWindow.WindowState = WindowState.Normal;
// var width2 = ParentWindow.ActualWidth;
// var horizontalScale = (float)width2 / (float)width1;
// var xmove = mousePosOnScreen.X - (int)(mousePosOnScreen.X* horizontalScale);
// ParentWindow.Left = xmove;
// ParentWindow.Top = mousePosOnScreen.Y;
// CompositionTarget.Rendering += WindowControlBar_Rendering;
// if (e.LeftButton == MouseButtonState.Pressed) { ParentWindow.DragMove(); }
// CompositionTarget.Rendering -= WindowControlBar_Rendering;
//}
}
//private bool _isMouseInLabel = false;
private bool _isMousePressed = false;
public void WindowControlBar_Rendering(object? sender, EventArgs e)
{
if (ParentWindow != null)
{
if (ParentWindow.WindowState == WindowState.Maximized)
{
NormalmizeButton.Visibility = Visibility.Visible;
MaximizeButton.Visibility = Visibility.Collapsed;
}
}
}
private void DragLabel_MouseMove(object sender, MouseEventArgs e)
{
if (ParentWindow == null) return;
if (_isMousePressed)
{
if (ParentWindow.WindowState != WindowState.Maximized)
{
CompositionTarget.Rendering += WindowControlBar_Rendering;
if (e.LeftButton == MouseButtonState.Pressed && ParentWindow != null) { ParentWindow.DragMove(); }
CompositionTarget.Rendering -= WindowControlBar_Rendering;
}
else
{
var mousePosOnBar = e.GetPosition(this);
var mousePosOnScreen = PointToScreen(mousePosOnBar);
var width1 = ParentWindow.ActualWidth;
NormalmizeButton.Visibility = Visibility.Collapsed;
MaximizeButton.Visibility = Visibility.Visible;
ParentWindow.WindowState = WindowState.Normal;
var width2 = ParentWindow.ActualWidth;
var horizontalScale = (float)width2 / (float)width1;
var xmove = mousePosOnScreen.X - (int)(mousePosOnScreen.X * horizontalScale);
ParentWindow.Left = xmove;
ParentWindow.Top = mousePosOnScreen.Y;
CompositionTarget.Rendering += WindowControlBar_Rendering;
if (e.LeftButton == MouseButtonState.Pressed) { ParentWindow.DragMove(); }
CompositionTarget.Rendering -= WindowControlBar_Rendering;
}
}
}
private void DragLabel_MouseEnter(object sender, MouseEventArgs e)
{
//_isMouseInLabel = true;
if (_isMousePressed) _isMousePressed = false;
}
private void DragLabel_MouseLeave(object sender, MouseEventArgs e)
{
//_isMouseInLabel = false;
if (_isMousePressed) _isMousePressed = false;
}
private void DragLabel_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
_isMousePressed = false;
}
}
}
@@ -0,0 +1,61 @@
<UserControl x:Class="FancyInput.Views.Controls.WorkshopPanel"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:FancyInput.Views.Controls"
xmlns:md="clr-namespace:FancyInput.Models"
xmlns:vm ="clr-namespace:FancyInput.ViewModels"
xmlns:uc="clr-namespace:FancyInput.Views.Controls"
xmlns:cm="clr-namespace:FancyInput.Common"
d:DataContext="{d:DesignInstance Type=vm:MainWindowViewModel}"
mc:Ignorable="d" x:Name="Root"
d:DesignHeight="450" d:DesignWidth="800">
<UserControl.Resources>
<Style x:Key="ShadowIconButton" TargetType="uc:IconButton">
<Setter Property="PathBackground" Value="{Binding AccentColor}"/>
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="PathBackground" Value="#FF953895"/>
</Trigger>
</Style.Triggers>
</Style>
</UserControl.Resources>
<StackPanel Orientation="Vertical">
<Border BorderBrush="{Binding BorderColor}" CornerRadius="10" BorderThickness="1">
<ScrollViewer Style="{StaticResource SimpleScrollViewerStyle}">
<StackPanel x:Name="MacroStackPanel" Orientation="Vertical" Margin="10,0">
<Label Content="宏" FontSize="20" FontFamily="Cascadia Mono" Margin="10,10,10,0" Foreground="{Binding AccentColor}" Padding="0"/>
<Border Height="1" HorizontalAlignment="Stretch" Background="{Binding AccentColor}" Margin="10,2"/>
<Grid Margin="10,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="40*" />
<ColumnDefinition Width="80*" />
</Grid.ColumnDefinitions>
<Label Content="打开宏编辑器窗口" FontSize="15" HorizontalAlignment="Left" VerticalAlignment="Center" Grid.Column="0"
Foreground="{Binding ForegroundColor}" Padding="0"/>
<uc:IconButton Grid.Column="1" PathData="{x:Static cm:PathDataGeometry.Edit}" PathFill="{Binding ForeGroundAccentColor}" IconMargin="3"
PathStroke="{Binding ForeGroundAccentColor}" ProfileThickness="2" CornerRadius="5"
Width="30" Height="30" HorizontalAlignment="Right" Click="OpenMacroWindow" Style="{StaticResource ShadowIconButton}"/>
</Grid>
<Grid Margin="10,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="30*" />
<ColumnDefinition Width="80*" />
</Grid.ColumnDefinitions>
<Label Content="默认加载路径" FontSize="15" HorizontalAlignment="Left" VerticalAlignment="Center" Grid.Column="0"
Foreground="{Binding ForegroundColor}" Padding="0"/>
<TextBox x:Name="MacroPathTextBox" Style="{StaticResource RoundCornerTextBox}" Grid.Column="1"
Margin="0,0,50,0" Height="20" FontSize="10" Padding="0"
HorizontalContentAlignment="Left" VerticalContentAlignment="Center"
Text="{Binding MacroSavePath,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}"/>
<uc:IconButton Grid.Column="1" PathData="{x:Static cm:PathDataGeometry.Folder}" PathFill="{Binding ForeGroundAccentColor}" IconMargin="3"
PathStroke="{Binding ForeGroundAccentColor}" ProfileThickness="2" CornerRadius="5"
Width="30" Height="30" HorizontalAlignment="Right" Click="ChangeMacroPath" Style="{StaticResource ShadowIconButton}"/>
</Grid>
</StackPanel>
</ScrollViewer>
</Border>
</StackPanel>
</UserControl>
@@ -0,0 +1,43 @@
using System.Windows;
using System.Windows.Controls;
namespace FancyInput.Views.Controls
{
/// <summary>
/// WorkshopPanel.xaml 的交互逻辑
/// </summary>
public partial class WorkshopPanel : UserControl
{
public WorkshopPanel()
{
InitializeComponent();
}
private void ChangeMacroPath(object sender, RoutedEventArgs e)
{
using (var dialog = new System.Windows.Forms.FolderBrowserDialog())
{
dialog.Description = "选择宏文件夹";
if (dialog.ShowDialog() == System.Windows.Forms.DialogResult.OK)
{
string folderPath = dialog.SelectedPath;
MacroPathTextBox.Text = folderPath;
}
}
}
public static readonly RoutedEvent MacroWindowOpenClickEvent = EventManager.RegisterRoutedEvent(nameof(MacroWindowOpenClick), RoutingStrategy.Bubble,
typeof(RoutedEventHandler), typeof(WorkshopPanel));
public event RoutedEventHandler MacroWindowOpenClick
{
add { AddHandler(MacroWindowOpenClickEvent, value); }
remove { RemoveHandler(MacroWindowOpenClickEvent, value); }
}
private void OpenMacroWindow(object sender, RoutedEventArgs e)
{
RaiseEvent(new RoutedEventArgs(MacroWindowOpenClickEvent));
}
}
}
+384
View File
@@ -0,0 +1,384 @@
<Window x:Class="FancyInput.Views.ElementTreeHelpWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
Title="帮助"
Width="620"
Height="820" d:Height="2500"
MinWidth="600"
MinHeight="680"
ResizeMode="CanResize"
Background="#FFF5F3F8">
<Window.Resources>
<Style x:Key="CardBorderStyle" TargetType="Border">
<Setter Property="Background" Value="White"/>
<Setter Property="BorderBrush" Value="#22B18FC7"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="CornerRadius" Value="14"/>
<Setter Property="Padding" Value="16"/>
<Setter Property="Margin" Value="0,0,0,10"/>
</Style>
<Style x:Key="SectionTitleStyle" TargetType="TextBlock">
<Setter Property="FontSize" Value="20"/>
<Setter Property="FontWeight" Value="SemiBold"/>
<Setter Property="Foreground" Value="#FF4D1A62"/>
</Style>
<Style x:Key="BodyStyle" TargetType="TextBlock">
<Setter Property="Margin" Value="0,8,0,0"/>
<Setter Property="FontSize" Value="14"/>
<Setter Property="Foreground" Value="#FF2F2A35"/>
<Setter Property="LineHeight" Value="23"/>
<Setter Property="TextWrapping" Value="Wrap"/>
</Style>
<Style x:Key="PlaceholderStyle" TargetType="Border">
<Setter Property="Background" Value="#FFF8F2FB"/>
<Setter Property="BorderBrush" Value="#FFA173BB"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="CornerRadius" Value="12"/>
<Setter Property="Padding" Value="14"/>
<Setter Property="Margin" Value="0,10,0,0"/>
</Style>
</Window.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="130"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Border Grid.Row="0" Margin="18,16,18,8" CornerRadius="20" Background="#FF220E22">
<Grid>
<Grid.Background>
<LinearGradientBrush StartPoint="0,0" EndPoint="1,1">
<GradientStop Color="#FF220E22" Offset="0.0"/>
<GradientStop Color="#FF651F65" Offset="0.62"/>
<GradientStop Color="#FF8240A1" Offset="1.0"/>
</LinearGradientBrush>
</Grid.Background>
<Ellipse Width="220" Height="220" Fill="#29FFFFFF" HorizontalAlignment="Right" VerticalAlignment="Top" Margin="0,-90,-70,0"/>
<Ellipse Width="160" Height="160" Fill="#1EFFFFFF" HorizontalAlignment="Right" VerticalAlignment="Bottom" Margin="0,0,40,-80"/>
<StackPanel Margin="26,20,26,20" VerticalAlignment="Center">
<TextBlock Text="FancyInput功能一览" Foreground="White" FontSize="32" FontWeight="SemiBold"/>
<TextBlock x:Name="VersionTextBlock" Margin="0,8,0,0" Foreground="#FFEADAF2" FontSize="15" TextWrapping="Wrap">
当前版本:Unknown
</TextBlock>
</StackPanel>
</Grid>
</Border>
<ScrollViewer Grid.Row="1" Margin="18,8,18,16" VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled" Style="{StaticResource SimpleScrollViewerStyle}">
<StackPanel>
<Border Style="{StaticResource CardBorderStyle}">
<StackPanel>
<TextBlock Style="{StaticResource SectionTitleStyle}" Text="1. 特色功能总览"/>
<Grid Margin="0,10,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="160"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="36"/>
<RowDefinition Height="50"/>
<RowDefinition Height="50"/>
<RowDefinition Height="50"/>
<RowDefinition Height="50"/>
<RowDefinition Height="50"/>
<RowDefinition Height="50"/>
<RowDefinition Height="50"/>
<RowDefinition Height="50"/>
<RowDefinition Height="50"/>
<RowDefinition Height="50"/>
</Grid.RowDefinitions>
<Border Grid.Row="0" Grid.Column="0" Background="#FFF3EAF7" BorderBrush="#FFD7BEDF" BorderThickness="1" Padding="8,6"><TextBlock Text="项目" FontWeight="SemiBold" Foreground="#FF4D1A62"/></Border>
<Border Grid.Row="0" Grid.Column="1" Background="#FFF3EAF7" BorderBrush="#FFD7BEDF" BorderThickness="1" Padding="8,6"><TextBlock Text="说明" FontWeight="SemiBold" Foreground="#FF4D1A62"/></Border>
<Border Grid.Row="1" Grid.Column="0" BorderBrush="#FFE3D2EA" BorderThickness="1" Padding="8,6"><TextBlock Text="多源输入统一解析"/></Border>
<Border Grid.Row="1" Grid.Column="1" BorderBrush="#FFE3D2EA" BorderThickness="1" Padding="8,6"><TextBlock Text="统一处理键盘、鼠标按键/滚轮/移动、手柄输入;手柄后端支持 SDL 与 XInput 切换。" TextWrapping="Wrap"/></Border>
<Border Grid.Row="2" Grid.Column="0" BorderBrush="#FFE3D2EA" BorderThickness="1" Padding="8,6"><TextBlock Text="多设备元素体系"/></Border>
<Border Grid.Row="2" Grid.Column="1" BorderBrush="#FFE3D2EA" BorderThickness="1" Padding="8,6"><TextBlock Text="支持背景图、键盘、鼠标按键/滚轮/移动、手柄按键/摇杆/扳机/十字键/玩家标识等元素。" TextWrapping="Wrap"/></Border>
<Border Grid.Row="3" Grid.Column="0" BorderBrush="#FFE3D2EA" BorderThickness="1" Padding="8,6"><TextBlock Text="多选与批量排版"/></Border>
<Border Grid.Row="3" Grid.Column="1" BorderBrush="#FFE3D2EA" BorderThickness="1" Padding="8,6"><TextBlock Text="支持框选、追加/减选、对齐(顶中底/左右中)与横向/纵向分布,适合批量校准布局。" TextWrapping="Wrap"/></Border>
<Border Grid.Row="4" Grid.Column="0" BorderBrush="#FFE3D2EA" BorderThickness="1" Padding="8,6"><TextBlock Text="锁定与显隐控制"/></Border>
<Border Grid.Row="4" Grid.Column="1" BorderBrush="#FFE3D2EA" BorderThickness="1" Padding="8,6"><TextBlock Text="支持全体锁定、选中锁定、选中隐藏;锁定元素会自动从选择集中移除,避免误操作。" TextWrapping="Wrap"/></Border>
<Border Grid.Row="5" Grid.Column="0" BorderBrush="#FFE3D2EA" BorderThickness="1" Padding="8,6"><TextBlock Text="对齐与测试"/></Border>
<Border Grid.Row="5" Grid.Column="1" BorderBrush="#FFE3D2EA" BorderThickness="1" Padding="8,6"><TextBlock Text="面向单元素多状态贴图的专用调试功能:可逐状态预览、对齐并实时测试输入响应,快速修正按下态/方向态等偏移与错位。" TextWrapping="Wrap"/></Border>
<Border Grid.Row="6" Grid.Column="0" BorderBrush="#FFE3D2EA" BorderThickness="1" Padding="8,6"><TextBlock Text="元素跟随移动绑定"/></Border>
<Border Grid.Row="6" Grid.Column="1" BorderBrush="#FFE3D2EA" BorderThickness="1" Padding="8,6"><TextBlock Text="支持把元素绑定到鼠标移动、手柄摇杆等移动源;包含环路检测与清空绑定,便于构建联动布局。" TextWrapping="Wrap"/></Border>
<Border Grid.Row="7" Grid.Column="0" BorderBrush="#FFE3D2EA" BorderThickness="1" Padding="8,6"><TextBlock Text="单图片编辑器"/></Border>
<Border Grid.Row="7" Grid.Column="1" BorderBrush="#FFE3D2EA" BorderThickness="1" Padding="8,6"><TextBlock Text="支持裁剪、矩形蒙版、圆形笔刷蒙版、笔刷半径预览与快捷键加速,适合精细贴图处理。" TextWrapping="Wrap"/></Border>
<Border Grid.Row="8" Grid.Column="0" BorderBrush="#FFE3D2EA" BorderThickness="1" Padding="8,6"><TextBlock Text="项目与导出双链路"/></Border>
<Border Grid.Row="8" Grid.Column="1" BorderBrush="#FFE3D2EA" BorderThickness="1" Padding="8,6"><TextBlock Text="支持 fip 单项目保存/加载,也支持导出纹理图集 + JSON(Input Overlay 目标格式)。" TextWrapping="Wrap"/></Border>
<Border Grid.Row="9" Grid.Column="0" BorderBrush="#FFE3D2EA" BorderThickness="1" Padding="8,6"><TextBlock Text="叠加组工作流"/></Border>
<Border Grid.Row="9" Grid.Column="1" BorderBrush="#FFE3D2EA" BorderThickness="1" Padding="8,6"><TextBlock Text="支持将多个叠加窗口打包为 fips,且可选择是否写入原始项目数据,便于迁移与分发。" TextWrapping="Wrap"/></Border>
<Border Grid.Row="10" Grid.Column="0" BorderBrush="#FFE3D2EA" BorderThickness="1" Padding="8,6"><TextBlock Text="运行与交互增强"/></Border>
<Border Grid.Row="10" Grid.Column="1" BorderBrush="#FFE3D2EA" BorderThickness="1" Padding="8,6"><TextBlock Text="支持管理员运行、托盘最小化、位置固定/置顶/隐藏、消除录制黄框等实用能力,并持续优化透明区域命中体验。" TextWrapping="Wrap"/></Border>
</Grid>
</StackPanel>
</Border>
<Border Style="{StaticResource CardBorderStyle}">
<StackPanel>
<TextBlock Style="{StaticResource SectionTitleStyle}" Text="2. 元素树与画布操作"/>
<Grid Margin="0,10,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="160"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="36"/>
<RowDefinition Height="36"/>
<RowDefinition Height="36"/>
<RowDefinition Height="36"/>
<RowDefinition Height="36"/>
<RowDefinition Height="36"/>
<RowDefinition Height="36"/>
<RowDefinition Height="36"/>
<RowDefinition Height="36"/>
</Grid.RowDefinitions>
<Border Grid.Row="0" Grid.Column="0" Background="#FFF3EAF7" BorderBrush="#FFD7BEDF" BorderThickness="1" Padding="8,6"><TextBlock Text="项目" FontWeight="SemiBold" Foreground="#FF4D1A62"/></Border>
<Border Grid.Row="0" Grid.Column="1" Background="#FFF3EAF7" BorderBrush="#FFD7BEDF" BorderThickness="1" Padding="8,6"><TextBlock Text="说明" FontWeight="SemiBold" Foreground="#FF4D1A62"/></Border>
<Border Grid.Row="1" Grid.Column="0" BorderBrush="#FFE3D2EA" BorderThickness="1" Padding="8,6"><TextBlock Text="单击元素"/></Border>
<Border Grid.Row="1" Grid.Column="1" BorderBrush="#FFE3D2EA" BorderThickness="1" Padding="8,6">
<TextBlock Text="选中元素;若该元素已选中且未按 Ctrl,再次单击会取消选中。" TextWrapping="Wrap"/></Border>
<Border Grid.Row="2" Grid.Column="0" BorderBrush="#FFE3D2EA" BorderThickness="1" Padding="8,6"><TextBlock Text="双击元素(列表或画布)"/></Border>
<Border Grid.Row="2" Grid.Column="1" BorderBrush="#FFE3D2EA" BorderThickness="1" Padding="8,6"><TextBlock Text="打开该元素的编辑窗口。"/></Border>
<Border Grid.Row="3" Grid.Column="0" BorderBrush="#FFE3D2EA" BorderThickness="1" Padding="8,6"><TextBlock Text="Ctrl + 单击"/></Border>
<Border Grid.Row="3" Grid.Column="1" BorderBrush="#FFE3D2EA" BorderThickness="1" Padding="8,6"><TextBlock Text="将元素追加到当前选择。"/></Border>
<Border Grid.Row="4" Grid.Column="0" BorderBrush="#FFE3D2EA" BorderThickness="1" Padding="8,6">
<TextBlock Text="Ctrl + Alt + 单击已选元素" TextWrapping="Wrap"/></Border>
<Border Grid.Row="4" Grid.Column="1" BorderBrush="#FFE3D2EA" BorderThickness="1" Padding="8,6"><TextBlock Text="从当前选择中移除该元素。"/></Border>
<Border Grid.Row="5" Grid.Column="0" BorderBrush="#FFE3D2EA" BorderThickness="1" Padding="8,6"><TextBlock Text="空白画布拖拽框选"/></Border>
<Border Grid.Row="5" Grid.Column="1" BorderBrush="#FFE3D2EA" BorderThickness="1" Padding="8,6"><TextBlock Text="创建新选择。Ctrl + 框选为追加,Ctrl + Alt + 框选为减选。"/></Border>
<Border Grid.Row="6" Grid.Column="0" BorderBrush="#FFE3D2EA" BorderThickness="1" Padding="8,6"><TextBlock Text="拖拽已选元素"/></Border>
<Border Grid.Row="6" Grid.Column="1" BorderBrush="#FFE3D2EA" BorderThickness="1" Padding="8,6"><TextBlock Text="移动所有选中元素(锁定元素不会参与移动)。"/></Border>
<Border Grid.Row="7" Grid.Column="0" BorderBrush="#FFE3D2EA" BorderThickness="1" Padding="8,6"><TextBlock Text="方向键 / Shift + 方向键"/></Border>
<Border Grid.Row="7" Grid.Column="1" BorderBrush="#FFE3D2EA" BorderThickness="1" Padding="8,6"><TextBlock Text="微调 1 像素 / 快速移动 10 像素。"/></Border>
<Border Grid.Row="8" Grid.Column="0" BorderBrush="#FFE3D2EA" BorderThickness="1" Padding="8,6"><TextBlock Text="画布滚轮(Shift 可加速)"/></Border>
<Border Grid.Row="8" Grid.Column="1" BorderBrush="#FFE3D2EA" BorderThickness="1" Padding="8,6"><TextBlock Text="按对数步长缩放画布;Shift 时步长更大。"/></Border>
</Grid>
</StackPanel>
</Border>
<Border Style="{StaticResource CardBorderStyle}">
<StackPanel>
<TextBlock Style="{StaticResource SectionTitleStyle}" Text="3. 右侧面板功能"/>
<Grid Margin="0,10,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="160"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="36"/>
<RowDefinition Height="40"/>
<RowDefinition Height="40"/>
<RowDefinition Height="40"/>
<RowDefinition Height="40"/>
<RowDefinition Height="46"/>
<RowDefinition Height="44"/>
</Grid.RowDefinitions>
<Border Grid.Row="0" Grid.Column="0" Background="#FFF3EAF7" BorderBrush="#FFD7BEDF" BorderThickness="1" Padding="8,6"><TextBlock Text="项目" FontWeight="SemiBold" Foreground="#FF4D1A62"/></Border>
<Border Grid.Row="0" Grid.Column="1" Background="#FFF3EAF7" BorderBrush="#FFD7BEDF" BorderThickness="1" Padding="8,6"><TextBlock Text="说明" FontWeight="SemiBold" Foreground="#FF4D1A62"/></Border>
<Border Grid.Row="1" Grid.Column="0" BorderBrush="#FFE3D2EA" BorderThickness="1" Padding="8,6"><TextBlock Text="全局设置"/></Border>
<Border Grid.Row="1" Grid.Column="1" BorderBrush="#FFE3D2EA" BorderThickness="1" Padding="8,6"><TextBlock Text="画布宽高、背景色、测试开关、全体锁定/解除锁定。" TextWrapping="Wrap"/></Border>
<Border Grid.Row="2" Grid.Column="0" BorderBrush="#FFE3D2EA" BorderThickness="1" Padding="8,6"><TextBlock Text="对齐与分布"/></Border>
<Border Grid.Row="2" Grid.Column="1" BorderBrush="#FFE3D2EA" BorderThickness="1" Padding="8,6"><TextBlock Text="多选元素时可进行对齐与均匀分布。" TextWrapping="Wrap"/></Border>
<Border Grid.Row="3" Grid.Column="0" BorderBrush="#FFE3D2EA" BorderThickness="1" Padding="8,6"><TextBlock Text="移动绑定"/></Border>
<Border Grid.Row="3" Grid.Column="1" BorderBrush="#FFE3D2EA" BorderThickness="1" Padding="8,6"><TextBlock Text="支持一键移动绑定,可绑定到其他元素,也可清空。" TextWrapping="Wrap"/></Border>
<Border Grid.Row="4" Grid.Column="0" BorderBrush="#FFE3D2EA" BorderThickness="1" Padding="8,6"><TextBlock Text="数值输入增强"/></Border>
<Border Grid.Row="4" Grid.Column="1" BorderBrush="#FFE3D2EA" BorderThickness="1" Padding="8,6"><TextBlock Text="多数数值框支持滚轮调整,Shift 提升步长。" TextWrapping="Wrap"/></Border>
<Border Grid.Row="5" Grid.Column="0" BorderBrush="#FFE3D2EA" BorderThickness="1" Padding="8,6"><TextBlock Text="对齐与测试入口"/></Border>
<Border Grid.Row="5" Grid.Column="1" BorderBrush="#FFE3D2EA" BorderThickness="1" Padding="8,6"><TextBlock Text="在元素编辑窗口中可进入“对齐与测试”,用于统一校准该元素各状态图片,并在测试模式下验证触发效果。" TextWrapping="Wrap"/></Border>
<Border Grid.Row="6" Grid.Column="0" BorderBrush="#FFE3D2EA" BorderThickness="1" Padding="8,6"><TextBlock Text="单元素属性"/></Border>
<Border Grid.Row="6" Grid.Column="1" BorderBrush="#FFE3D2EA" BorderThickness="1" Padding="8,6"><TextBlock Text="可编辑 ID、位置、缩放,查看映射尺寸与 JSON 预览。" TextWrapping="Wrap"/></Border>
</Grid>
</StackPanel>
</Border>
<Border Style="{StaticResource CardBorderStyle}">
<StackPanel>
<TextBlock Style="{StaticResource SectionTitleStyle}" Text="4. 单图片编辑器"/>
<Grid Margin="0,10,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="160"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="36"/>
<RowDefinition Height="38"/>
<RowDefinition Height="38"/>
<RowDefinition Height="38"/>
<RowDefinition Height="38"/>
<RowDefinition Height="40"/>
</Grid.RowDefinitions>
<Border Grid.Row="0" Grid.Column="0" Background="#FFF3EAF7" BorderBrush="#FFD7BEDF" BorderThickness="1" Padding="8,6"><TextBlock Text="项目" FontWeight="SemiBold" Foreground="#FF4D1A62"/></Border>
<Border Grid.Row="0" Grid.Column="1" Background="#FFF3EAF7" BorderBrush="#FFD7BEDF" BorderThickness="1" Padding="8,6"><TextBlock Text="说明" FontWeight="SemiBold" Foreground="#FF4D1A62"/></Border>
<Border Grid.Row="1" Grid.Column="0" BorderBrush="#FFE3D2EA" BorderThickness="1" Padding="8,6"><TextBlock Text="画布滚轮"/></Border>
<Border Grid.Row="1" Grid.Column="1" BorderBrush="#FFE3D2EA" BorderThickness="1" Padding="8,6"><TextBlock Text="缩放图片视图。" TextWrapping="Wrap"/></Border>
<Border Grid.Row="2" Grid.Column="0" BorderBrush="#FFE3D2EA" BorderThickness="1" Padding="8,6"><TextBlock Text="蒙版笔刷"/></Border>
<Border Grid.Row="2" Grid.Column="1" BorderBrush="#FFE3D2EA" BorderThickness="1" Padding="8,6"><TextBlock Text="默认添加区域,按住 Ctrl 改为减少区域。" TextWrapping="Wrap"/></Border>
<Border Grid.Row="3" Grid.Column="0" BorderBrush="#FFE3D2EA" BorderThickness="1" Padding="8,6"><TextBlock Text="笔刷优先级"/></Border>
<Border Grid.Row="3" Grid.Column="1" BorderBrush="#FFE3D2EA" BorderThickness="1" Padding="8,6"><TextBlock Text="笔刷功能与剪裁框编辑同时开启时,优先使用笔刷。" TextWrapping="Wrap"/></Border>
<Border Grid.Row="4" Grid.Column="0" BorderBrush="#FFE3D2EA" BorderThickness="1" Padding="8,6"><TextBlock Text="笔刷尺寸"/></Border>
<Border Grid.Row="4" Grid.Column="1" BorderBrush="#FFE3D2EA" BorderThickness="1" Padding="8,6"><TextBlock Text="Ctrl + 滚轮快速调整;半径文本框也支持滚轮。" TextWrapping="Wrap"/></Border>
<Border Grid.Row="5" Grid.Column="0" BorderBrush="#FFE3D2EA" BorderThickness="1" Padding="8,6"><TextBlock Text="裁剪与蒙版"/></Border>
<Border Grid.Row="5" Grid.Column="1" BorderBrush="#FFE3D2EA" BorderThickness="1" Padding="8,6"><TextBlock Text="支持拖拽裁剪、矩形蒙版、圆形笔刷蒙版与实时预览。" TextWrapping="Wrap"/></Border>
</Grid>
</StackPanel>
</Border>
<Border Style="{StaticResource CardBorderStyle}">
<StackPanel>
<TextBlock Style="{StaticResource SectionTitleStyle}" Text="5. 文件格式与兼容性"/>
<Grid Margin="0,10,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="160"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="36"/>
<RowDefinition Height="44"/>
<RowDefinition Height="52"/>
<RowDefinition Height="44"/>
<RowDefinition Height="52"/>
</Grid.RowDefinitions>
<Border Grid.Row="0" Grid.Column="0" Background="#FFF3EAF7" BorderBrush="#FFD7BEDF" BorderThickness="1" Padding="8,6"><TextBlock Text="项目" FontWeight="SemiBold" Foreground="#FF4D1A62"/></Border>
<Border Grid.Row="0" Grid.Column="1" Background="#FFF3EAF7" BorderBrush="#FFD7BEDF" BorderThickness="1" Padding="8,6"><TextBlock Text="说明" FontWeight="SemiBold" Foreground="#FF4D1A62"/></Border>
<Border Grid.Row="1" Grid.Column="0" BorderBrush="#FFE3D2EA" BorderThickness="1" Padding="8,6"><TextBlock Text="fip(单项目文件)"/></Border>
<Border Grid.Row="1" Grid.Column="1" BorderBrush="#FFE3D2EA" BorderThickness="1" Padding="8,6"><TextBlock Text="推荐作为日常保存与加载格式:单文件、信息完整、功能覆盖最全。" TextWrapping="Wrap"/></Border>
<Border Grid.Row="2" Grid.Column="0" BorderBrush="#FFE3D2EA" BorderThickness="1" Padding="8,6"><TextBlock Text="json+png (input overla兼容y格式)" TextWrapping="Wrap" /></Border>
<Border Grid.Row="2" Grid.Column="1" BorderBrush="#FFE3D2EA" BorderThickness="1" Padding="8,6"><TextBlock Text="使用新版本特性时,保存为 Input Overlay 可能兼容性不足;建议同时保留 fip 主工程文件。" TextWrapping="Wrap"/></Border>
<Border Grid.Row="3" Grid.Column="0" BorderBrush="#FFE3D2EA" BorderThickness="1" Padding="8,6"><TextBlock Text="fips(叠加组文件)"/></Border>
<Border Grid.Row="3" Grid.Column="1" BorderBrush="#FFE3D2EA" BorderThickness="1" Padding="8,6"><TextBlock Text="可将多个 fip 组合为一个 fips,用于批量加载与管理整套叠加配置。" TextWrapping="Wrap"/></Border>
<Border Grid.Row="4" Grid.Column="0" BorderBrush="#FFE3D2EA" BorderThickness="1" Padding="8,6"><TextBlock Text="推荐保存流程"/></Border>
<Border Grid.Row="4" Grid.Column="1" BorderBrush="#FFE3D2EA" BorderThickness="1" Padding="8,6"><TextBlock Text="编辑阶段优先保存 fip;需要整套分发时再打包 fips;导出 Input Overlay 仅作为目标格式产物。" TextWrapping="Wrap"/></Border>
</Grid>
</StackPanel>
</Border>
<Border Style="{StaticResource CardBorderStyle}">
<StackPanel>
<TextBlock Style="{StaticResource SectionTitleStyle}" Text="6. 对齐与测试操作"/>
<Grid Margin="0,10,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="160"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="36"/>
<RowDefinition Height="42"/>
<RowDefinition Height="42"/>
<RowDefinition Height="42"/>
<RowDefinition Height="42"/>
<RowDefinition Height="46"/>
<RowDefinition Height="42"/>
<RowDefinition Height="42"/>
<RowDefinition Height="46"/>
<RowDefinition Height="42"/>
</Grid.RowDefinitions>
<Border Grid.Row="0" Grid.Column="0" Background="#FFF3EAF7" BorderBrush="#FFD7BEDF" BorderThickness="1" Padding="8,6"><TextBlock Text="操作" FontWeight="SemiBold" Foreground="#FF4D1A62"/></Border>
<Border Grid.Row="0" Grid.Column="1" Background="#FFF3EAF7" BorderBrush="#FFD7BEDF" BorderThickness="1" Padding="8,6"><TextBlock Text="说明" FontWeight="SemiBold" Foreground="#FF4D1A62"/></Border>
<Border Grid.Row="1" Grid.Column="0" BorderBrush="#FFE3D2EA" BorderThickness="1" Padding="8,6"><TextBlock Text="入口"/></Border>
<Border Grid.Row="1" Grid.Column="1" BorderBrush="#FFE3D2EA" BorderThickness="1" Padding="8,6">
<TextBlock Text="在元素树编辑器菜单点击“编辑>>贴图对齐”或在元素编辑窗口点击“对齐与测试”进入。" TextWrapping="Wrap"/></Border>
<Border Grid.Row="2" Grid.Column="0" BorderBrush="#FFE3D2EA" BorderThickness="1" Padding="8,6"><TextBlock Text="片段选择"/></Border>
<Border Grid.Row="2" Grid.Column="1" BorderBrush="#FFE3D2EA" BorderThickness="1" Padding="8,6"><TextBlock Text="支持单击、Ctrl 追加、Ctrl+Alt 减选、空白区域框选。" TextWrapping="Wrap"/></Border>
<Border Grid.Row="3" Grid.Column="0" BorderBrush="#FFE3D2EA" BorderThickness="1" Padding="8,6"><TextBlock Text="拖拽与微调"/></Border>
<Border Grid.Row="3" Grid.Column="1" BorderBrush="#FFE3D2EA" BorderThickness="1" Padding="8,6"><TextBlock Text="可直接拖拽选中片段;方向键微调 1 像素,Shift + 方向键快速移动 10 像素。" TextWrapping="Wrap"/></Border>
<Border Grid.Row="4" Grid.Column="0" BorderBrush="#FFE3D2EA" BorderThickness="1" Padding="8,6"><TextBlock Text="参数调整"/></Border>
<Border Grid.Row="4" Grid.Column="1" BorderBrush="#FFE3D2EA" BorderThickness="1" Padding="8,6"><TextBlock Text="每个片段可调整缩放、δx、δy、透明度;支持滑条与文本框输入。" TextWrapping="Wrap"/></Border>
<Border Grid.Row="5" Grid.Column="0" BorderBrush="#FFE3D2EA" BorderThickness="1" Padding="8,6"><TextBlock Text="滚轮加速"/></Border>
<Border Grid.Row="5" Grid.Column="1" BorderBrush="#FFE3D2EA" BorderThickness="1" Padding="8,6"><TextBlock Text="数值文本框支持滚轮调节:Shift 放大步长,Ctrl 细化步长;部分区域支持 Ctrl + 滚轮按对数调节缩放。" TextWrapping="Wrap"/></Border>
<Border Grid.Row="6" Grid.Column="0" BorderBrush="#FFE3D2EA" BorderThickness="1" Padding="8,6"><TextBlock Text="测试模式"/></Border>
<Border Grid.Row="6" Grid.Column="1" BorderBrush="#FFE3D2EA" BorderThickness="1" Padding="8,6"><TextBlock Text="“测试”开关开启后可实时响应输入,用于验证状态切换与显示效果。" TextWrapping="Wrap"/></Border>
<Border Grid.Row="7" Grid.Column="0" BorderBrush="#FFE3D2EA" BorderThickness="1" Padding="8,6"><TextBlock Text="可见/锁定"/></Border>
<Border Grid.Row="7" Grid.Column="1" BorderBrush="#FFE3D2EA" BorderThickness="1" Padding="8,6"><TextBlock Text="支持单片段显隐与锁定;锁定片段不会参与拖拽和批量编辑。" TextWrapping="Wrap"/></Border>
<Border Grid.Row="8" Grid.Column="0" BorderBrush="#FFE3D2EA" BorderThickness="1" Padding="8,6"><TextBlock Text="擦除预览"/></Border>
<Border Grid.Row="8" Grid.Column="1" BorderBrush="#FFE3D2EA" BorderThickness="1" Padding="8,6"><TextBlock Text="在擦除区域可左键涂抹预览;Ctrl + 滚轮调整笔刷半径,支持提交或舍弃当前擦除结果。" TextWrapping="Wrap"/></Border>
<Border Grid.Row="9" Grid.Column="0" BorderBrush="#FFE3D2EA" BorderThickness="1" Padding="8,6"><TextBlock Text="保存与舍弃"/></Border>
<Border Grid.Row="9" Grid.Column="1" BorderBrush="#FFE3D2EA" BorderThickness="1" Padding="8,6"><TextBlock Text="底部“保存”会应用到元素,“舍弃”则放弃本次修改并关闭窗口。" TextWrapping="Wrap"/></Border>
</Grid>
</StackPanel>
</Border>
<Border Style="{StaticResource CardBorderStyle}">
<StackPanel>
<TextBlock Style="{StaticResource SectionTitleStyle}" Text="7. 常见问题排查"/>
<Grid Margin="0,10,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="160"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="36"/>
<RowDefinition Height="42"/>
<RowDefinition Height="42"/>
<RowDefinition Height="42"/>
</Grid.RowDefinitions>
<Border Grid.Row="0" Grid.Column="0" Background="#FFF3EAF7" BorderBrush="#FFD7BEDF" BorderThickness="1" Padding="8,6"><TextBlock Text="项目" FontWeight="SemiBold" Foreground="#FF4D1A62"/></Border>
<Border Grid.Row="0" Grid.Column="1" Background="#FFF3EAF7" BorderBrush="#FFD7BEDF" BorderThickness="1" Padding="8,6"><TextBlock Text="说明" FontWeight="SemiBold" Foreground="#FF4D1A62"/></Border>
<Border Grid.Row="1" Grid.Column="0" BorderBrush="#FFE3D2EA" BorderThickness="1" Padding="8,6"><TextBlock Text="操作无响应"/></Border>
<Border Grid.Row="1" Grid.Column="1" BorderBrush="#FFE3D2EA" BorderThickness="1" Padding="8,6"><TextBlock Text="先检查元素是否被锁定,锁定元素不会参与拖拽与编辑。" TextWrapping="Wrap"/></Border>
<Border Grid.Row="2" Grid.Column="0" BorderBrush="#FFE3D2EA" BorderThickness="1" Padding="8,6"><TextBlock Text="批量工具不可用"/></Border>
<Border Grid.Row="2" Grid.Column="1" BorderBrush="#FFE3D2EA" BorderThickness="1" Padding="8,6"><TextBlock Text="对齐/分布功能需要至少选中 2 个元素。" TextWrapping="Wrap"/></Border>
<Border Grid.Row="3" Grid.Column="0" BorderBrush="#FFE3D2EA" BorderThickness="1" Padding="8,6"><TextBlock Text="关闭窗口提示保存"/></Border>
<Border Grid.Row="3" Grid.Column="1" BorderBrush="#FFE3D2EA" BorderThickness="1" Padding="8,6"><TextBlock Text="这是未保存保护机制,请根据提示先保存 fip 或完成导出。" TextWrapping="Wrap"/></Border>
</Grid>
</StackPanel>
</Border>
</StackPanel>
</ScrollViewer>
</Grid>
</Window>
@@ -0,0 +1,16 @@
using System.Windows;
namespace FancyInput.Views
{
/// <summary>
/// ElementTreeHelpWindow.xaml 的交互逻辑
/// </summary>
public partial class ElementTreeHelpWindow : Window
{
public ElementTreeHelpWindow()
{
InitializeComponent();
VersionTextBlock.Text = $"当前版本: {System.Reflection.Assembly.GetExecutingAssembly().GetName().Version}";
}
}
}
+480
View File
@@ -0,0 +1,480 @@
<Window x:Class="FancyInput.Views.ElementTreeWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:FancyInput.Views"
xmlns:md="clr-namespace:FancyInput.Models"
xmlns:vm="clr-namespace:FancyInput.ViewModels"
xmlns:uc="clr-namespace:FancyInput.Views.Controls"
xmlns:cm="clr-namespace:FancyInput.Common"
d:DataContext="{d:DesignInstance Type=vm:ElementTreeViewModel}"
mc:Ignorable="d" Closed="Window_Closed"
Title="元素树编辑器" Height="500" Width="1000"
PreviewKeyDown="MoveSelectedElement" Loaded="Window_Loaded"
d:Height="900">
<Window.Resources>
<md:ScaleToTransformConverter x:Key="ScaleToTransformConverter"/>
<md:AngleToRotateTransformConverter x:Key="AngleToRotateTransformConverter"/>
</Window.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="30"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Menu x:Name="MenuRoot" Grid.Row="0" FontSize="15" Foreground="White" Background="#FF220E22">
<Menu.ItemContainerStyle>
<StaticResource ResourceKey="SimpleMenuItem"/>
</Menu.ItemContainerStyle>
<MenuItem Header="添加">
<MenuItem Header="背景图" Tag="{x:Static md:ElementType.Texture}" Click="AddElement"/>
<MenuItem Header="键盘" Tag="{x:Static md:ElementType.KeyboardButton}" Click="AddElement"/>
<MenuItem Header="手柄按键" Tag="{x:Static md:ElementType.GamepadButton}" Click="AddElement"/>
<MenuItem Header="鼠标按键" Tag="{x:Static md:ElementType.MouseButton}" Click="AddElement"/>
<MenuItem Header="鼠标滚轮" Tag="{x:Static md:ElementType.MouseWheel}" Click="AddElement"/>
<MenuItem Header="手柄摇杆" Tag="{x:Static md:ElementType.AnalogStick}" Click="AddElement"/>
<MenuItem Header="手柄扳机" Tag="{x:Static md:ElementType.GamepadTrigger}" Click="AddElement"/>
<MenuItem Header="手柄玩家" Tag="{x:Static md:ElementType.GamepadPlayerId}" Click="AddElement"/>
<MenuItem Header="手柄十字键" Tag="{x:Static md:ElementType.DPad}" Click="AddElement"/>
<MenuItem Header="鼠标移动" Tag="{x:Static md:ElementType.MouseMovement}" Click="AddElement"/>
</MenuItem>
<MenuItem Header="编辑" Visibility="{Binding HasSelectedVisibility}">
<MenuItem Header="贴图替换" Tag="{x:Static md:ExportType.PngAndJson}" Click="EditElement"/>
<MenuItem Header="贴图对齐" Tag="{x:Static md:ExportType.ProjectFile}" Click="AlignSegments"/>
</MenuItem>
<MenuItem Header="保存">
<MenuItem Header="纹理图集与JSON" Tag="{x:Static md:ExportType.PngAndJson}" Click="SaveFile"/>
<MenuItem Header="单个项目文件" Tag="{x:Static md:ExportType.ProjectFile}" Click="SaveFile"/>
</MenuItem>
<MenuItem Header="帮助" Click="Help"/>
</Menu>
<Grid Grid.Row="1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="300" MinWidth="200"/>
<ColumnDefinition Width="600*"/>
<ColumnDefinition Width="300*" MinWidth="200"/>
</Grid.ColumnDefinitions>
<Grid Grid.Column="0" Background="#EEE" Margin="0,0,0,0">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Border Grid.Row="0" CornerRadius="10" Background="#FF651F65" Margin="5">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="80*"/>
<ColumnDefinition Width="150*"/>
<ColumnDefinition Width="150*"/>
<ColumnDefinition Width="30*"/>
<ColumnDefinition Width="30*"/>
<ColumnDefinition Width="30*"/>
<ColumnDefinition Width="30*"/>
<ColumnDefinition Width="30*"/>
<ColumnDefinition Width="15"/>
</Grid.ColumnDefinitions>
<Label Grid.Column="0" Content="编号" FontSize="15" Padding="0,3" HorizontalAlignment="Center" VerticalContentAlignment="Center" Foreground="White"/>
<Label Grid.Column="1" Content="类型" FontSize="15" Padding="0,3" HorizontalAlignment="Center" VerticalContentAlignment="Center" Foreground="White"/>
<Label Grid.Column="2" Content="ID" FontSize="15" Padding="0,3" HorizontalAlignment="Center" VerticalContentAlignment="Center" Foreground="White"/>
</Grid>
</Border>
<ScrollViewer Grid.Row="1" Margin="0" Style="{StaticResource SimpleScrollViewerStyle}">
<ItemsControl ItemsSource="{Binding ElementViewModels}" >
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Vertical" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate DataType="{x:Type vm:ElementViewModel}">
<Border CornerRadius="5" Margin="3,3,3,3" Background="{Binding SelectColor2}" BorderBrush="{Binding SelectColor1}" BorderThickness="1"
MouseLeftButtonDown="ChangeSelect">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="80*"/>
<ColumnDefinition Width="150*"/>
<ColumnDefinition Width="150*"/>
<ColumnDefinition Width="30*"/>
<ColumnDefinition Width="30*"/>
<ColumnDefinition Width="30*"/>
<ColumnDefinition Width="30*"/>
<ColumnDefinition Width="30*"/>
</Grid.ColumnDefinitions>
<Label Style="{StaticResource RoundedLabel}" Grid.Column="0"
Content="{Binding IdxString}" Width="20" FontSize="15" Height="20" Padding="0"
HorizontalAlignment="Center" VerticalContentAlignment="Center" >
</Label>
<Label Style="{StaticResource RoundedLabel}" Grid.Column="1"
Content="{Binding ElementType}" Width="70" FontSize="10" Height="20" Padding="0"
HorizontalAlignment="Center" VerticalContentAlignment="Center" >
</Label>
<Label Style="{StaticResource RoundedLabel}" Grid.Column="2"
Content="{Binding Id}" Width="50" FontSize="15" Height="20" Padding="0"
HorizontalAlignment="Center" VerticalContentAlignment="Center" >
</Label>
<Button Grid.Column="3" Style="{StaticResource RoundCornerButton}" Cursor="Hand" FontSize="12" Height="15" Width="15" Margin="0,2"
VerticalAlignment="Center" HorizontalAlignment="Center" ToolTip="下移一层" Padding="0" FontFamily="Cascadia Mono" Content="↑"
Click="UpMove">
</Button>
<Button Grid.Column="4" Style="{StaticResource RoundCornerButton}" Cursor="Hand" FontSize="12" Height="15" Width="15" Margin="0,2"
VerticalAlignment="Center" HorizontalAlignment="Center" ToolTip="上移一层" Padding="0" FontFamily="Cascadia Mono" Content="↓"
Click="DownMove">
</Button>
<uc:IconButton Grid.Column="5" Cursor="Hand" Width="15" Height="15" Margin="0,2"
VerticalAlignment="Center" HorizontalAlignment="Center" ToolTip="{Binding VisibilityToolTip}"
PathData="{Binding VisibilityIcon}" PathMargin="0,5,0,0"
PathFill="{Binding VisibilityButtonBrush}" CornerRadius="4" PathBackground="#FF651F65"
IsFlipped="True" MouseLeftButtonDown="ChangeVisibleState" ActiveColor="#FFF48C8C"/>
<uc:IconButton Grid.Column="6" Cursor="Hand" Width="15" Height="15" Margin="0,2"
VerticalAlignment="Center" HorizontalAlignment="Center" ToolTip="{Binding LockToolTip}"
PathData="{Binding LockIcon}"
PathFill="{Binding LockButtonBrush}" CornerRadius="4" PathBackground="#FF651F65" IsFlipped="True" MouseLeftButtonDown="ChangeLockState">
</uc:IconButton>
<Button Grid.Column="7" Style="{StaticResource RoundCornerButton}" Cursor="Hand" FontSize="12" Height="15" Width="15" Margin="0,2"
VerticalAlignment="Center" HorizontalAlignment="Center" ToolTip="删除" Padding="0" FontFamily="Cascadia Mono" Content="×"
Click="RemoveItem">
</Button>
</Grid>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</Grid>
<Border Grid.Column="1" >
<ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto" Style="{StaticResource SimpleScrollViewerStyle}">
<Grid>
<ItemsControl x:Name="ElementItemsControl" ItemsSource="{Binding ElementViewModels}" >
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas d:DataContext="{d:DesignInstance Type=vm:ElementTreeViewModel}"
PreviewMouseWheel="MainCanvas_PreviewMouseWheel"
Background="{Binding ImageBackground}"
MouseLeftButtonDown="Canvas_MouseLeftButtonDown"
MouseMove="Canvas_MouseMove"
MouseLeftButtonUp="Canvas_MouseLeftButtonUp"
Loaded="MainCanvas_Loaded"
Width="{Binding CanvasWidth}"
Height="{Binding CanvasHeight}"
LayoutTransform="{Binding CanvasScale, Converter={StaticResource ScaleToTransformConverter}}">
</Canvas>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemContainerStyle>
<Style TargetType="ContentPresenter">
<Setter Property="Canvas.Left" Value="{Binding VisualPosX}"/>
<Setter Property="Canvas.Top" Value="{Binding VisualPosY}"/>
</Style>
</ItemsControl.ItemContainerStyle>
<ItemsControl.ItemTemplate>
<DataTemplate DataType="{x:Type vm:ElementViewModel}">
<Grid RenderTransformOrigin="0,0"
LayoutTransform="{Binding Scale, Converter={StaticResource ScaleToTransformConverter}}">
<uc:AnimatedImage ImageSource="{Binding CurrentImageSource}" Width="{Binding MappingW}" Height="{Binding MappingH}"
DataContextChanged="AnimatedImage_DataContextChanged" Loaded="AnimatedImage_Loaded"
MouseLeftButtonDown="Draggable_MouseLeftButtonDown" MouseLeftButtonUp="Draggable_MouseLeftButtonUp"
MouseMove="Draggable_MouseMove" Visibility="{Binding ElementVisibility}"
RenderOptions.BitmapScalingMode="NearestNeighbor" HorizontalAlignment="Left" VerticalAlignment="Top"
RenderTransformOrigin="0.5,0.5"
RenderTransform="{Binding RotateAngle,Converter={StaticResource AngleToRotateTransformConverter}}">
<uc:AnimatedImage.ContextMenu>
<ContextMenu>
<ContextMenu.ItemContainerStyle>
<StaticResource ResourceKey="ContextSimpleMenuItem"/>
</ContextMenu.ItemContainerStyle>
<MenuItem Header="上移一层" Click="DownMove"/>
<MenuItem Header="下移一层" Click="UpMove"/>
<MenuItem Header="置于顶层" Click="BottomMove"/>
<MenuItem Header="置于底层" Click="TopMove"/>
<MenuItem Header="删除" Click="RemoveItem"/>
</ContextMenu>
</uc:AnimatedImage.ContextMenu>
</uc:AnimatedImage>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<ItemsControl ItemsSource="{Binding SelectedElementViewModels}" IsHitTestVisible="False">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate >
<Canvas Background="Transparent" Width="{Binding CanvasWidth}" Height="{Binding CanvasHeight}"
d:DataContext="{d:DesignInstance Type=vm:ElementTreeViewModel}"
LayoutTransform="{Binding CanvasScale, Converter={StaticResource ScaleToTransformConverter}}">
</Canvas>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemContainerStyle>
<Style TargetType="ContentPresenter">
<Setter Property="Canvas.Left" Value="{Binding VisualPosX}"/>
<Setter Property="Canvas.Top" Value="{Binding VisualPosY}"/>
</Style>
</ItemsControl.ItemContainerStyle>
<ItemsControl.ItemTemplate>
<DataTemplate DataType="{x:Type vm:ElementViewModel}">
<Grid RenderTransformOrigin="0,0" IsHitTestVisible="{Binding IsHitTestVisible}"
LayoutTransform="{Binding Scale, Converter={StaticResource ScaleToTransformConverter}}">
<Border BorderThickness="1" BorderBrush="Red" Width="{Binding MappingW}" Height="{Binding MappingH}"
RenderTransformOrigin="0.5,0.5" HorizontalAlignment="Left" VerticalAlignment="Top"
RenderTransform="{Binding RotateAngle,Converter={StaticResource AngleToRotateTransformConverter}}">
</Border>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<Canvas IsHitTestVisible="False" Width="{Binding CanvasWidth}" Height="{Binding CanvasHeight}"
LayoutTransform="{Binding CanvasScale, Converter={StaticResource ScaleToTransformConverter}}">
<Rectangle Visibility="{Binding SelectionRectVisibility}"
Canvas.Left="{Binding SelectionRectLeft}"
Canvas.Top="{Binding SelectionRectTop}"
Width="{Binding SelectionRectWidth}"
Height="{Binding SelectionRectHeight}"
Fill="#3379B8FF"
Stroke="#FF2F7FDB"
StrokeThickness="0.5"/>
</Canvas>
</Grid>
</ScrollViewer>
</Border>
<GridSplitter Grid.Column="1" Cursor="SizeWE" Width="2" HorizontalAlignment="Left" VerticalAlignment="Stretch" Background="Gray" ShowsPreview="True"/>
<Grid Grid.Column="2" Background="#EEE">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<StackPanel Orientation="Vertical" Grid.Row="0">
<Label Style="{StaticResource RoundedLabel}" Content="全局设置" Margin="5" BorderThickness="1" />
<Grid Margin="10,3" Height="30">
<Label Style="{StaticResource RoundedLabel}" Content="开启测试" Width="70" HorizontalAlignment="Left" Background="#FFDDDCDE"/>
<CheckBox Style="{StaticResource LabeledToggleSwitch}" HorizontalAlignment="Right"
FontSize="15" Margin="10,3" IsChecked="{Binding IsTestEnabled}"/>
</Grid>
<Grid Margin="10,3" Height="28">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="80"/>
<ColumnDefinition Width="100*"/>
<ColumnDefinition Width="20*"/>
</Grid.ColumnDefinitions>
<Label Grid.Column="0" Style="{StaticResource RoundedLabel}" Content="画布宽度" Width="70" HorizontalAlignment="Left" Background="#FFDDDCDE"/>
<Slider Grid.Column="1" Minimum="{Binding MinCanvasWidth}" Maximum="{Binding MaxCanvasWidth}" HorizontalAlignment="Stretch" Margin="0,0,10,0"
Style="{StaticResource SimpleSlider}" Value="{Binding CanvasWidth}"
VerticalAlignment="Center" Height="20"/>
<TextBox Grid.Column="2" Grid.ColumnSpan="3" Style="{StaticResource RoundCornerTextBox}" HorizontalAlignment="Stretch"
Height="20" FontSize="15" Text="{Binding CanvasWidth, StringFormat={}{0:F1}}" Margin="0"
KeyDown="ConfirmTextInput" PreviewMouseWheel="ChangeBindDouble"/>
</Grid>
<Grid Margin="10,3" Height="28">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="80"/>
<ColumnDefinition Width="100*"/>
<ColumnDefinition Width="20*"/>
</Grid.ColumnDefinitions>
<Label Grid.Column="0" Style="{StaticResource RoundedLabel}" Content="画布高度" Width="70" HorizontalAlignment="Left" Background="#FFDDDCDE"/>
<Slider Grid.Column="1" Minimum="{Binding MinCanvasHeight}" Maximum="{Binding MaxCanvasHeight}" HorizontalAlignment="Stretch" Margin="0,0,10,0"
Style="{StaticResource SimpleSlider}" Value="{Binding CanvasHeight}"
VerticalAlignment="Center" Height="20"/>
<TextBox Grid.Column="2" Grid.ColumnSpan="3" Style="{StaticResource RoundCornerTextBox}" HorizontalAlignment="Stretch"
Height="20" FontSize="15" Text="{Binding CanvasHeight, StringFormat={}{0:F1}}" Margin="0" KeyDown="ConfirmTextInput"
PreviewMouseWheel="ChangeBindDouble"/>
</Grid>
<Grid Margin="10,3" Height="28">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="100"/>
<ColumnDefinition Width="100"/>
</Grid.ColumnDefinitions>
<Label Grid.Column="0" Style="{StaticResource RoundedLabel}" Content="画布背景色" Width="90" HorizontalAlignment="Left" Background="#FFDDDCDE"/>
<Border Grid.Column="1" Height="20" Margin="0,0,10,0" BorderBrush="Gray" BorderThickness="1" CornerRadius="4" Background="{Binding ImageBackground}"
MouseLeftButtonDown="ChangeBackground"/>
</Grid>
<StackPanel Orientation="Vertical" Margin="0,0,4,0">
<Grid Visibility="{Binding HasManySelectedVisibility}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="80"/>
<ColumnDefinition Width="100*"/>
<ColumnDefinition Width="100*"/>
<ColumnDefinition Width="100*"/>
<ColumnDefinition Width="100*"/>
<ColumnDefinition Width="100*"/>
<ColumnDefinition Width="100*"/>
</Grid.ColumnDefinitions>
<Label Grid.Column ="0" Style="{StaticResource RoundedLabel}" Content="对齐" Width="60" HorizontalAlignment="Center"
Margin="10,3" Background="#FFDDDCDE"/>
<uc:IconButton Grid.Column="1" PathData="{x:Static cm:PathDataGeometry.AlignTop}" Width="25" Height="25" PathFill="Black" Margin="2,2" Padding="0"
PathBackground="White" CornerRadius="4" ProfileBrush="Black" ProfileThickness="1" Click="AlignTop"/>
<uc:IconButton Grid.Column="2" PathData="{x:Static cm:PathDataGeometry.AlignCenterV}" Width="25" Height="25" PathFill="Black" Margin="2,2"
PathBackground="White" CornerRadius="4" ProfileBrush="Black" ProfileThickness="1" Click="AlignCenterV"/>
<uc:IconButton Grid.Column="3" PathData="{x:Static cm:PathDataGeometry.AlignBottom}" Width="25" Height="25" PathFill="Black" Margin="2,2"
PathBackground="White" CornerRadius="4" ProfileBrush="Black" ProfileThickness="1" Click="AlignBottom"/>
<uc:IconButton Grid.Column="4" PathData="{x:Static cm:PathDataGeometry.AlignLeft}" Width="25" Height="25" PathFill="Black" Margin="2,2"
PathBackground="White" CornerRadius="4" ProfileBrush="Black" ProfileThickness="1" Click="AlignLeft"/>
<uc:IconButton Grid.Column="5" PathData="{x:Static cm:PathDataGeometry.AlignCenterH}" Width="25" Height="25" PathFill="Black" Margin="2,2"
PathBackground="White" CornerRadius="4" ProfileBrush="Black" ProfileThickness="1" Click="AlignCenterH"/>
<uc:IconButton Grid.Column="6" PathData="{x:Static cm:PathDataGeometry.AlignRight}" Width="25" Height="25" PathFill="Black" Margin="2,2"
PathBackground="White" CornerRadius="4" ProfileBrush="Black" ProfileThickness="1" Click="AlignRight"/>
</Grid>
<Grid Visibility="{Binding HasManySelectedVisibility}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="80"/>
<ColumnDefinition Width="100*"/>
<ColumnDefinition Width="100*"/>
</Grid.ColumnDefinitions>
<Label Grid.Column ="0" Style="{StaticResource RoundedLabel}" Content="分布" Width="60" HorizontalAlignment="Center"
Margin="10,3" Background="#FFDDDCDE"/>
<uc:IconButton Grid.Column="1" PathData="{x:Static cm:PathDataGeometry.AlignVDistribute}" Width="25" Height="25" PathFill="Black" Margin="2,2" Padding="0"
PathBackground="White" CornerRadius="4" ProfileBrush="Black" ProfileThickness="1" Click="AlignVDistribute"/>
<uc:IconButton Grid.Column="2" PathData="{x:Static cm:PathDataGeometry.AlignHDistribute}" Width="25" Height="25" PathFill="Black" Margin="2,2"
PathBackground="White" CornerRadius="4" ProfileBrush="Black" ProfileThickness="1" Click="AlignHDistribute"/>
</Grid>
</StackPanel>
<Grid Margin="10,3" Height="30">
<Label Style="{StaticResource RoundedLabel}" Content="全部锁定/解除锁定" Width="120" HorizontalAlignment="Left" Background="#FFDDDCDE"/>
<CheckBox Style="{StaticResource LabeledToggleSwitch}" HorizontalAlignment="Right"
FontSize="15" Margin="10,3" IsChecked="{Binding IsAllLocked}"/>
</Grid>
<Grid Margin="10,3" Height="30" Visibility="{Binding HasSelectedVisibility}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="50"/>
<ColumnDefinition Width="100*"/>
<ColumnDefinition Width="50"/>
<ColumnDefinition Width="100*"/>
</Grid.ColumnDefinitions>
<Label Grid.Column ="0" Style="{StaticResource RoundedLabel}" Content="锁定" Width="40" HorizontalAlignment="Left" Background="#FFDDDCDE"/>
<CheckBox Grid.Column ="1" Style="{StaticResource LabeledToggleSwitch}" HorizontalAlignment="Stretch"
FontSize="15" IsChecked="{Binding IsSelectedLocked}"/>
<Label Grid.Column ="2" Style="{StaticResource RoundedLabel}" Content="隐藏" Width="40" HorizontalAlignment="Left" Background="#FFDDDCDE"/>
<CheckBox Grid.Column ="3" Style="{StaticResource LabeledToggleSwitch}" HorizontalAlignment="Stretch"
FontSize="15" IsChecked="{Binding IsSelectedHidden}"/>
</Grid>
<Grid Visibility="{Binding HasSelectedVisibility}">
<StackPanel Orientation="Horizontal">
<Label Style="{StaticResource RoundedLabel}" Content="一键移动绑定" Width="80" FontSize="10" HorizontalAlignment="Left" Margin="10,3" Background="#FFDDDCDE"/>
<Button Content="清空" Background="White" Foreground="Black" Margin="0,5"
FontSize="12" Width="30" BorderBrush="Purple" Style="{StaticResource RoundCornerButton}"
Click="ClearSelectedMoveBind"/>
</StackPanel>
<Grid Height="30" Margin="5,0" HorizontalAlignment="Right">
<ComboBox x:Name="BindComboBox" Height="30" Width="80" Style="{StaticResource RoundCornerComboBox}"
ItemContainerStyle="{StaticResource CustomComboBoxItemStyle}"
HorizontalContentAlignment="Center" VerticalContentAlignment="Center" HorizontalAlignment="Right"
ItemsSource="{Binding ElementViewModels}" SelectionChanged ="ComboBox_SelectionChanged"/>
<TextBlock Text="无" IsHitTestVisible="False" HorizontalAlignment="Center" VerticalAlignment="Center" Foreground="Black">
<TextBlock.Style>
<Style TargetType="TextBlock">
<Setter Property="Visibility" Value="Collapsed"/>
<Style.Triggers>
<DataTrigger Binding="{Binding SelectedItem, ElementName=BindComboBox}" Value="{x:Null}">
<Setter Property="Visibility" Value="Visible"/>
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
</Grid>
</Grid>
</StackPanel>
<Label Grid.Row="1" Style="{StaticResource RoundedLabel}" Content="单元素属性" Margin="5" BorderThickness="1" />
<ScrollViewer Grid.Row="2" Visibility="{Binding HasSelectedVisibility}" Style="{StaticResource SimpleScrollViewerStyle}">
<StackPanel Orientation="Vertical" DataContext="{Binding SelectedElementViewModel}">
<Grid>
<Label Style="{StaticResource RoundedLabel}" Content="Id" Width="40" HorizontalAlignment="Left" Margin="10,3" Background="#FFDDDCDE"/>
<TextBox Style="{StaticResource RoundCornerTextBox}" HorizontalAlignment="Right"
Width="100" Height="30" FontSize="15" Text="{Binding Id, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Margin="10,3" KeyDown="ConfirmTextInput" />
</Grid>
<Grid>
<StackPanel Orientation="Horizontal">
<Label Style="{StaticResource RoundedLabel}" Content="移动绑定" Width="80" HorizontalAlignment="Left" Margin="10,3" Background="#FFDDDCDE"/>
<Button x:Name="BindMoveButton" Content="清空" Background="White" Foreground="Black" Margin="0,5"
FontSize="12" Width="30" BorderBrush="Purple" Style="{StaticResource RoundCornerButton}" Click="ClearMoveBind"/>
</StackPanel>
<Grid Height="30" Margin="5,0" HorizontalAlignment="Right">
<ComboBox Height="30" Width="80" Style="{StaticResource RoundCornerComboBox}"
ItemContainerStyle="{StaticResource CustomComboBoxItemStyle}"
HorizontalContentAlignment="Center" VerticalContentAlignment="Center" HorizontalAlignment="Right"
ItemsSource="{Binding ParentCollection}" SelectedItem="{Binding OffsetAnchorViewModel}"
SelectionChanged ="ComboBox_SelectionChanged"/>
<TextBlock Text="无" IsHitTestVisible="False" HorizontalAlignment="Center" VerticalAlignment="Center" Foreground="Black">
<TextBlock.Style>
<Style TargetType="TextBlock">
<Setter Property="Visibility" Value="Collapsed"/>
<Style.Triggers>
<DataTrigger Binding="{Binding OffsetAnchorViewModel}" Value="{x:Null}">
<Setter Property="Visibility" Value="Visible"/>
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
</Grid>
</Grid>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="40*"/>
<ColumnDefinition Width="80*"/>
<ColumnDefinition Width="40*"/>
</Grid.ColumnDefinitions>
<Label Grid.Column="0" Grid.ColumnSpan="3" Style="{StaticResource RoundedLabel}" Content="缩放" Width="40" IsHitTestVisible="False"
HorizontalAlignment="Left" Margin="10,3" Background="#FFDDDCDE"/>
<Slider Grid.Column="1" Minimum="-1" Maximum="1" HorizontalAlignment="Stretch" Margin="0,0,0,0" Style="{StaticResource SimpleSlider}" Value="{Binding LogScale}"
VerticalAlignment="Center" Height="20"/>
<TextBox Grid.Column="0" Grid.ColumnSpan="3" Style="{StaticResource RoundCornerTextBox}" HorizontalAlignment="Right"
Width="30" Height="30" FontSize="15" Text="{Binding ScaleString}" Margin="10,3" KeyDown="ConfirmTextInput"
PreviewMouseWheel="ChangeScale"/>
</Grid>
<Grid>
<Label Style="{StaticResource RoundedLabel}" Content="类型" Width="40" HorizontalAlignment="Left" Margin="10,3" Background="#FFDDDCDE"/>
<TextBox Style="{StaticResource RoundCornerTextBox}" HorizontalAlignment="Right"
Width="100" Height="30" FontSize="15" Text="{Binding ElementType}" Margin="10,3" IsReadOnly="True"/>
</Grid>
<Grid>
<Label Style="{StaticResource RoundedLabel}" Content="X" Width="40" HorizontalAlignment="Left" Margin="10,3" Background="#FFDDDCDE"/>
<TextBox Style="{StaticResource RoundCornerTextBox}" HorizontalAlignment="Right" KeyDown="ConfirmTextInput" PreviewMouseWheel="ChangeBindDouble"
Width="50" Height="30" FontSize="15" Text="{Binding PosX}" Margin="10,3" />
</Grid>
<Grid>
<Label Style="{StaticResource RoundedLabel}" Content="Y" Width="40" HorizontalAlignment="Left" Margin="10,3" Background="#FFDDDCDE"/>
<TextBox Style="{StaticResource RoundCornerTextBox}" HorizontalAlignment="Right" KeyDown="ConfirmTextInput" PreviewMouseWheel="ChangeBindDouble"
Width="50" Height="30" FontSize="15" Text="{Binding PosY}" Margin="10,3" />
</Grid>
<Grid>
<Label Style="{StaticResource RoundedLabel}" Content="U" Width="40" HorizontalAlignment="Left" Margin="10,3" Background="#FFDDDCDE"/>
<TextBox Style="{StaticResource RoundCornerTextBox}" HorizontalAlignment="Right"
Width="50" Height="30" FontSize="15" Text="{Binding MappingU}" Margin="10,3" IsReadOnly="True"/>
</Grid>
<Grid>
<Label Style="{StaticResource RoundedLabel}" Content="V" Width="40" HorizontalAlignment="Left" Margin="10,3" Background="#FFDDDCDE"/>
<TextBox Style="{StaticResource RoundCornerTextBox}" HorizontalAlignment="Right"
Width="50" Height="30" FontSize="15" Text="{Binding MappingV}" Margin="10,3" IsReadOnly="True"/>
</Grid>
<Grid>
<Label Style="{StaticResource RoundedLabel}" Content="W" Width="40" HorizontalAlignment="Left" Margin="10,3" Background="#FFDDDCDE"/>
<TextBox Style="{StaticResource RoundCornerTextBox}" HorizontalAlignment="Right"
Width="50" Height="30" FontSize="15" Text="{Binding MappingW}" Margin="10,3" IsReadOnly="True"/>
</Grid>
<Grid>
<Label Style="{StaticResource RoundedLabel}" Content="H" Width="40" HorizontalAlignment="Left" Margin="10,3" Background="#FFDDDCDE"/>
<TextBox Style="{StaticResource RoundCornerTextBox}" HorizontalAlignment="Right"
Width="50" Height="30" FontSize="15" Text="{Binding MappingH}" Margin="10,3" IsReadOnly="True"/>
</Grid>
<Label Style="{StaticResource RoundedLabel}" Content="JSON" Width="60" Height="40" HorizontalAlignment="Left" Margin="10,3" Background="#FFDDDCDE"/>
<TextBox Style="{StaticResource RoundCornerTextBox}" HorizontalAlignment="Stretch" HorizontalContentAlignment="Left"
FontSize="15" Text="{Binding OverlayJSON,Mode=OneWay}" Margin="10,3" IsReadOnly="True"/>
</StackPanel>
</ScrollViewer>
</Grid>
<GridSplitter Grid.Column="2" Cursor="SizeWE" Width="2" HorizontalAlignment="Left" VerticalAlignment="Stretch" Background="Gray" ShowsPreview="True"/>
</Grid>
</Grid>
</Window>
File diff suppressed because it is too large Load Diff
+112
View File
@@ -0,0 +1,112 @@
<local:ElementBase x:Class="FancyInput.Views.Elements.AnalogStick"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:FancyInput.Views.Elements"
xmlns:vm="clr-namespace:FancyInput.ViewModels"
xmlns:uc ="clr-namespace:FancyInput.Views.Controls"
d:DataContext="{d:DesignInstance Type=vm:ElementViewModel}"
mc:Ignorable="d"
Title="手柄摇杆" Height="400" Width="800">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="1000*"/>
<RowDefinition Height="100*"/>
<RowDefinition Height="20*"/>
</Grid.RowDefinitions>
<Grid Grid.Row="0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="100*"/>
<ColumnDefinition Width="100*"/>
<ColumnDefinition Width="80*"/>
</Grid.ColumnDefinitions>
<uc:ElementViewPanel Grid.Column="0" Index="0" Title="贴图(未按下)"
ImageBackground="{Binding ImageBackground}" ImageSource="{Binding Image0Source}"
LoadImageClick="LoadImage" EditImageClick="EditImage" CopyImageClick="CopyImage" PasteImageClick="PasteImage"/>
<uc:ElementViewPanel Grid.Column="1" Index="1" Title="贴图(按下)"
ImageBackground="{Binding ImageBackground}" ImageSource="{Binding Image1Source}"
LoadImageClick="LoadImage" EditImageClick="EditImage" CopyImageClick="CopyImage" PasteImageClick="PasteImage"/>
<Grid Grid.Column="2">
<Grid.RowDefinitions>
<RowDefinition Height="100*"/>
<RowDefinition Height="1110*"/>
</Grid.RowDefinitions>
<Label Grid.Row="0" Content="配置" FontSize="15" FontFamily="Cascadia Mono" HorizontalAlignment="Center" VerticalAlignment="Bottom"/>
<Border Grid.Row="1" Margin="5,5" BorderBrush="Gray" BorderThickness="1" CornerRadius="20">
<StackPanel VerticalAlignment="Center">
<StackPanel Orientation="Horizontal" Margin="0,10">
<Label Content="背景" FontFamily="Cascadia Mono" VerticalAlignment="Center" Margin="0,0" FontSize="15"/>
<Border Height="14" Width="14" BorderBrush="Purple" BorderThickness="1" CornerRadius="8" Margin="0,0,10,0"
Cursor="Hand" ToolTip="仅用于方便查看,不会真正改变像素">
<Border.Style>
<Style TargetType="Border">
<Setter Property="Background" Value="#05808080"/>
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="#0B5B06DE"/>
</Trigger>
</Style.Triggers>
</Style>
</Border.Style>
<Label Content="?" FontSize="8" Padding="0" HorizontalAlignment="Center" VerticalAlignment="Center" FontWeight="Bold" Foreground="Purple"/>
</Border>
<Border Width="60" Margin="0" BorderBrush="Gray" BorderThickness="1" CornerRadius="4" Background="{Binding ImageBackground}"
MouseLeftButtonDown="BackGroundBorder_MouseLeftButtonDown"/>
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="0,10">
<Label Content="名称" FontFamily="Cascadia Mono" VerticalAlignment="Center" Margin="0,0" FontSize="15"/>
<TextBox Width="70" Height="30" Margin="10,5" Style="{StaticResource RoundCornerTextBox}" Padding="0" Text="{Binding Id,Mode=TwoWay}"
FontSize="15" VerticalContentAlignment="Center" HorizontalContentAlignment="Center"/>
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="0,10">
<Label Content="触发" FontFamily="Cascadia Mono" VerticalAlignment="Center" Margin="0,0" FontSize="15"/>
<ComboBox Height="30" Width="80" Margin="5,0" Style="{StaticResource RoundCornerComboBox}" ItemContainerStyle="{StaticResource CustomComboBoxItemStyle}"
HorizontalContentAlignment="Center" VerticalContentAlignment="Center"
ItemsSource="{Binding SideList}" SelectedItem="{Binding SelectedSide}"/>
<Button x:Name="DetecButton" Content="{Binding DetectingString}" Background="{Binding DetectingColor}"
FontSize="8" Width="30" Height="15" BorderBrush="Purple" Style="{StaticResource RoundCornerButton}"
Click="DetectInput"/>
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="0,10">
<Label Content="半径" FontFamily="Cascadia Mono" VerticalAlignment="Center" Margin="0,0" FontSize="15"/>
<Border Height="14" Width="14" BorderBrush="Purple" BorderThickness="1" CornerRadius="8" Margin="0,0,5,0"
Cursor="Hand" ToolTip="摇杆活动范围,半径为0表示不移动">
<Border.Style>
<Style TargetType="Border">
<Setter Property="Background" Value="#05808080"/>
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="#0B5B06DE"/>
</Trigger>
</Style.Triggers>
</Style>
</Border.Style>
<Label Content="?" FontSize="8" Padding="0" HorizontalAlignment="Center" VerticalAlignment="Center" FontWeight="Bold" Foreground="Purple"/>
</Border>
<Slider Style="{StaticResource MiniSlider}" Width="100" Minimum="0" Maximum="{Binding MaxRadius}" TickFrequency="1" Value="{Binding Radius}"/>
<TextBox Width="30" Height="20" Margin="10,5" Style="{StaticResource RoundCornerTextBox}" Padding="0" IsReadOnly="True" Text="{Binding RadiusString,Mode=OneWay}"
FontSize="10" VerticalContentAlignment="Center" HorizontalContentAlignment="Center"/>
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="0,10">
<Label Content="W" FontFamily="Cascadia Mono" VerticalAlignment="Center" Margin="0,0" FontSize="15"/>
<TextBox Style="{StaticResource RoundCornerTextBox}" FontSize="15" Height="30" Width="60" IsReadOnly="True" Margin="10,0" Text="{Binding MappingW}"/>
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="0,10">
<Label Content="H" FontFamily="Cascadia Mono" VerticalAlignment="Center" Margin="0,0" FontSize="15"/>
<TextBox Style="{StaticResource RoundCornerTextBox}" FontSize="15" Height="30" Width="60" IsReadOnly="True" Margin="10,0" Text="{Binding MappingH}"/>
</StackPanel>
</StackPanel>
</Border>
</Grid>
</Grid>
<Border Grid.Row="1" Background="Gray" Height="0.5" VerticalAlignment="Top" Margin="0,1"/>
<StackPanel Grid.Row="1" Orientation="Horizontal" HorizontalAlignment="Right">
<Button Content="对齐" Style="{StaticResource FancyButton}" FontSize="15" Margin="5,4" Width="50" Click="AlignAndTest"/>
<Button Content="保存" Style="{StaticResource FancyButton}" FontSize="15" Margin="5,4" Click="SaveConfig"/>
<Button Content="取消" Style="{StaticResource FancyButton}" FontSize="15" Margin="10,4" Click="CloseWindow"/>
</StackPanel>
</Grid>
</local:ElementBase>
@@ -0,0 +1,58 @@
using FancyInput.Models;
using FancyInput.ViewModels;
using SharpDX.XInput;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;
namespace FancyInput.Views.Elements
{
/// <summary>
/// AnalogStick.xaml 的交互逻辑
/// </summary>
public partial class AnalogStick : ElementBase
{
public AnalogStick(ElementViewModel elementViewModel)
: base(elementViewModel)
{
InitializeComponent();
GetInputWhenDetecting += HandleGetInputWhenDetecting;
}
protected void HandleGetInputWhenDetecting(InputArgs args)
{
FIPGamepadButtonflags? flag = args.Flag;
Side? side = args.Side;
if (args.Device == Models.InputDevice.XInputButton && flag.HasValue)
{
if (flag.Value == FIPGamepadButtonflags.LS)
{
ElementViewModel.SelectedSide = Side.Left;
OFFDetect();
return;
}
else if (flag.Value == FIPGamepadButtonflags.RS)
{
ElementViewModel.SelectedSide = Side.Right;
OFFDetect();
return;
}
}
if (args.Device == Models.InputDevice.XInputStick && side.HasValue)
{
ElementViewModel.SelectedSide = side.Value;
OFFDetect();
}
}
}
}
+358
View File
@@ -0,0 +1,358 @@
using FancyInput.Models;
using FancyInput.ViewModels;
using FancyInput.Views.Windows;
using Microsoft.Win32;
using System.ComponentModel;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using SharpDX.XInput;
using System.Drawing;
using Color = System.Windows.Media.Color;
using FancyInput.Views.Controls;
using System;
namespace FancyInput.Views.Elements
{
public class ElementBase: Window
{
private WindowCloseType CloseType = WindowCloseType.Discard;
protected event Action<InputArgs>? GetInputWhenDetecting;
protected void OnGetInputWhenDetecting(InputArgs args) => GetInputWhenDetecting?.Invoke(args);
protected InputParser? _inputParser=null;
public InputParser? InputParser
{
get => _inputParser;
set => _inputParser = value;
}
public Color ImageBackGround { get; set; } = Colors.Black;
public ElementViewModel ElementViewModel { get; set; }
protected XImage? ClipboardImage { get; set; }
public ElementBase(ElementViewModel elementViewModel)
{
ElementViewModel = elementViewModel.Copy();
this.DataContext = ElementViewModel;
this.Loaded += ElementBase_Loaded;
}
private void ElementBase_Loaded(object sender, RoutedEventArgs e)
{
WindowHelper.TryAddRawInputHook(this);
}
protected void ConfirmTextInput(object sender, KeyEventArgs e)
{
if (e.Key == Key.Enter)
{
var tb = sender as TextBox;
if (tb != null)
{
var binding = tb.GetBindingExpression(TextBox.TextProperty);
binding?.UpdateSource();
// 让输入框失去焦点
Keyboard.ClearFocus();
}
}
}
protected void WheelStopwatch(object sender, MouseWheelEventArgs e)
{
int delta = e.Delta > 0 ? 1 : -1;
bool isShiftPressed = Keyboard.IsKeyDown(Key.LeftShift) || Keyboard.IsKeyDown(Key.RightShift);
int valueChange = isShiftPressed ? delta * 100 : delta * 10;
int targetValue = ElementViewModel.StopwatchMilliseconds + valueChange;
if (targetValue >= 50 && targetValue <= 5000)
{
ElementViewModel.StopwatchMilliseconds = targetValue;
}
e.Handled = true;
}
protected void ChangeStopwatch(object sender, TextChangedEventArgs e)
{
if (sender is TextBox tb)
{
string newText = tb.Text;
if (int.TryParse(newText, out int value))
{
if (value >= 50 && value <= 5000)
{
ElementViewModel.StopwatchMilliseconds = value;
}
}
}
}
protected void LoadImage(object sender, ImageActionEventArgs e)
{
OpenFileDialog openFileDialog = new OpenFileDialog();
openFileDialog.Filter = "Png Files (*.png)|*.png|GIF Files(*.gif)|*.gif";
if (openFileDialog.ShowDialog() == true)
{
try
{
XImage xImage = new XImage(openFileDialog.FileName);
int index = e.Index;
ElementViewModel[index] = xImage;
}
catch (Exception ex)
{
FancyInput.AppMessageBox.Show("加载图片失败:" + ex.Message);
}
}
}
protected void EditImage(object sender, ImageActionEventArgs e)
{
int index = e.Index;
XImage? xImage = ElementViewModel[index];
if (xImage == null)
{
FancyInput.AppMessageBox.Show("请先加载图片!");
return;
}
if (xImage.ImageType == XImageType.GIF || xImage.ImageType == XImageType.Unknown)
{
FancyInput.AppMessageBox.Show("当前图片格式不受支持编辑!");
return;
}
ImageEditor imageEditor = new ImageEditor(xImage);
imageEditor.Owner = this;
imageEditor.WindowStartupLocation = WindowStartupLocation.CenterOwner;
imageEditor.ShowDialog();
if (imageEditor.DialogResult == true)
{
ElementViewModel[index] = imageEditor.ImageEditorViewModel.MainImage.Copy();
}
}
protected void CopyImage(object sender, ImageActionEventArgs e)
{
int index = e.Index;
XImage? xImage = ElementViewModel[index];
if (xImage == null)
{
FancyInput.AppMessageBox.Show("请先加载图片!");
return;
}
ClipboardImage = xImage.Copy();
}
protected void PasteImage(object sender, ImageActionEventArgs e)
{
if (ClipboardImage == null)
{
FancyInput.AppMessageBox.Show("剪贴板中没有图片!");
return;
}
int index = e.Index;
ElementViewModel[index] = ClipboardImage.Copy();
}
protected void BackGroundBorder_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
Color currentColor = ImageBackGround;
RgbInputDialog rgbInputDialog = new RgbInputDialog(currentColor);
rgbInputDialog.Owner = this;
rgbInputDialog.WindowStartupLocation = WindowStartupLocation.CenterOwner;
rgbInputDialog.ShowDialog();
if (rgbInputDialog.DialogResult == true)
{
ElementViewModel.ImageBackGroundColor = rgbInputDialog.SelectedColor;
}
}
protected void ONDetect()
{
ElementViewModel.IsDetectingInput = true;
if (_inputParser == null) return;
_inputParser.GetInput -= DetectInputHandler;
_inputParser.GetInput += DetectInputHandler;
}
protected void OFFDetect()
{
ElementViewModel.IsDetectingInput = false;
if (_inputParser == null) return;
_inputParser.GetInput -= DetectInputHandler;
}
protected void DetectInput(object sender, RoutedEventArgs e)
{
if (ElementViewModel.IsDetectingInput)
OFFDetect();
else
ONDetect();
}
public void DetectInputHandler(object? sender, InputArgs args)
{
if (!ElementViewModel.IsDetectingInput)
return;
OnGetInputWhenDetecting(args);
}
protected override void OnClosing(CancelEventArgs e)
{
base.OnClosing(e);
if (CloseType == WindowCloseType.Discard)
{
if (_inputParser != null)
_inputParser.GetInput -= DetectInputHandler;
DialogResult = false;
return;
}
if (this.Owner != null)
{
bool isMainImageLoaded = ElementViewModel.IsMainImageLoaded;
bool isAllImageLoaded = ElementViewModel.IsImagesLoaded;
bool isAllSameShape = ElementViewModel.IsAllSameShape;
bool isGIFExist = ElementViewModel.IsGIFExist;
ElementType elementType = ElementViewModel.ElementType;
// GamepadTrigger 特殊处理
if (elementType == ElementType.GamepadTrigger)
{
if (isGIFExist)
{
DialogResult = false;
FancyInput.AppMessageBox.Show("手柄扳机元素不支持GIF图片,请移除GIF图片后重试。", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
return;
}
if (!isAllImageLoaded)
{
DialogResult = false;
FancyInput.AppMessageBox.Show("手柄扳机元素不支持存在空图片,请补全图片后重试。", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
return;
}
if (!isAllSameShape)
{
ElementViewModel.ResizeAllImagesToMaxSize();
DialogResult = true;
return;
}
}
// GIF 存在时的处理
if (isGIFExist)
{
if (!isAllSameShape)
{
var result = FancyInput.AppMessageBox.Show("存在GIF图片且图片尺寸不一致,是否全缩放至GIF尺寸?", "提示", MessageBoxButton.YesNo, MessageBoxImage.Question);
if (result == MessageBoxResult.Yes)
{
ElementViewModel.ResizeAllImagesToFirstGIF();
DialogResult = true;
}
else if (result == MessageBoxResult.No)
{
DialogResult = true;
}
else
{
DialogResult = false;
e.Cancel = true;
}
return;
}
DialogResult = true;
return;
}
// 主图未加载
if (!isMainImageLoaded)
{
DialogResult = true;
return;
}
// 存在未加载图片
if (isMainImageLoaded && !isAllImageLoaded)
{
var result = FancyInput.AppMessageBox.Show("有图片未加载,是否全使用主图替代?", "提示", MessageBoxButton.YesNo, MessageBoxImage.Question);
if (result == MessageBoxResult.Yes)
{
ElementViewModel.FillMissingImagesWithMainImage();
DialogResult = true;
}
else if (result == MessageBoxResult.No)
{
DialogResult = true;
}
else
{
DialogResult = false;
e.Cancel = true;
}
return;
}
// 图片尺寸不一致
if (!isAllSameShape)
{
var result = FancyInput.AppMessageBox.Show("图片尺寸不一致,是否全缩放至最大尺寸?", "提示", MessageBoxButton.YesNo, MessageBoxImage.Question);
if (result == MessageBoxResult.Yes)
{
ElementViewModel.ResizeAllImagesToMaxSize();
DialogResult = true;
}
else if (result == MessageBoxResult.No)
{
DialogResult = true;
}
else
{
DialogResult = false;
e.Cancel = true;
}
return;
}
// 正常情况
DialogResult = true;
if (_inputParser != null)
_inputParser.GetInput -= DetectInputHandler;
if (!DialogResult.HasValue)
throw new Exception("无法确定对话框结果状态");
}
CloseType = WindowCloseType.Discard;
}
protected void SaveConfig(object sender, RoutedEventArgs e)
{
CloseType = WindowCloseType.Save;
Close();
}
protected void CloseWindow(object sender, RoutedEventArgs e)
{
CloseType = WindowCloseType.Discard;
Close();
}
protected void AlignAndTest(object sender, RoutedEventArgs e)
{
if (InputParser == null || ElementViewModel == null)
{
FancyInput.AppMessageBox.Show("InputParser 或 ElementViewModel 未正确初始化。", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
return;
}
ElementTest testWindow = new ElementTest(InputParser, ElementViewModel);
testWindow.ShowDialog();
testWindow.Dispose();
}
}
}
+311
View File
@@ -0,0 +1,311 @@
<Window x:Class="FancyInput.Views.Elements.ElementTest"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:FancyInput.Views.Elements"
xmlns:cm ="clr-namespace:FancyInput.Common"
xmlns:md ="clr-namespace:FancyInput.Models"
xmlns:vm="clr-namespace:FancyInput.ViewModels"
xmlns:uc ="clr-namespace:FancyInput.Views.Controls"
mc:Ignorable="d" Loaded="Window_Loaded"
d:DataContext="{d:DesignInstance Type=vm:ElementViewModel}"
Title="对齐与测试" Height="600" Width="900" MinWidth="800" MinHeight="400"
PreviewKeyDown="MoveSelectedElement">
<Window.Resources>
<md:ScaleToTransformConverter x:Key="ScaleToTransformConverter"/>
<md:AngleToRotateTransformConverter x:Key="AngleToRotateTransformConverter"/>
</Window.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="40"/>
</Grid.RowDefinitions>
<Border CornerRadius="4" BorderBrush="Black" BorderThickness="2">
<Border.Effect>
<DropShadowEffect Color="Black" BlurRadius="5" ShadowDepth="0" Opacity="0.5"/>
</Border.Effect>
</Border>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="40"/>
<RowDefinition Height="600*"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="250"/>
<ColumnDefinition Width="600*"/>
<ColumnDefinition Width="300*"/>
</Grid.ColumnDefinitions>
<Label Grid.Row="0" Grid.Column="0" Style="{StaticResource RoundedLabel}" Background="#FF540754" Foreground="White" Content="列表" Margin="10"/>
<Label Grid.Row="0" Grid.Column="1" Style="{StaticResource RoundedLabel}" Background="#FF540754" Foreground="White" Content="堆叠" Margin="10"/>
<Grid Grid.Row="1" Grid.Column="0" Background="#EEE" Margin="0,0,0,0">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<ScrollViewer Grid.Row="1" Margin="0" Style="{StaticResource SimpleScrollViewerStyle}">
<ItemsControl ItemsSource="{Binding Segments}" >
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Vertical" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate DataType="{x:Type vm:ElementSegmentViewModel}">
<Border Grid.Row="0" CornerRadius="10" Margin="5" Height="120"
BorderBrush="{Binding SelectColor1}" BorderThickness="1" Background="{Binding SelectColor2}"
MouseLeftButtonDown="ChangeSelect">
<Grid Margin="2,2">
<Grid.RowDefinitions>
<RowDefinition Height="100*"/>
<RowDefinition Height="100*"/>
<RowDefinition Height="100*"/>
<RowDefinition Height="100*"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="80*"/>
<ColumnDefinition Width="80*"/>
<ColumnDefinition Width="120*"/>
<ColumnDefinition Width="300*"/>
<ColumnDefinition Width="100*"/>
</Grid.ColumnDefinitions>
<Label Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2" Style="{StaticResource RoundedLabel}" Margin="5" FontSize="12" Padding="0"
Background="#FF651F65" Foreground="White" Content="{Binding Name}" VerticalAlignment="Stretch" HorizontalAlignment="Stretch"/>
<Viewbox Margin="2" Grid.Row="1" Grid.Column="0" Grid.RowSpan="2" Grid.ColumnSpan="2" HorizontalAlignment="Center" VerticalAlignment="Center">
<Grid Background="{Binding DataContext.ImageBackground,RelativeSource={RelativeSource AncestorType=Window}}">
<uc:AnimatedImage ImageSource="{Binding ImageSource}" RenderOptions.BitmapScalingMode="NearestNeighbor"/>
</Grid>
</Viewbox>
<Label Grid.Row="0" Grid.Column="2" VerticalAlignment="Center" HorizontalAlignment="Center" Content="缩放" FontSize="12" Padding="0" />
<Label Grid.Row="1" Grid.Column="2" VerticalAlignment="Center" HorizontalAlignment="Center" Content="δx" FontSize="12" Padding="0" />
<Label Grid.Row="2" Grid.Column="2" VerticalAlignment="Center" HorizontalAlignment="Center" Content="δy" FontSize="12" Padding="0" />
<Label Grid.Row="3" Grid.Column="2" VerticalAlignment="Center" HorizontalAlignment="Center" Content="透明度" FontSize="12" Padding="0" />
<Slider Grid.Row="0" Grid.Column="3" Style="{StaticResource SimpleSlider}" Height="15" Margin="4,0" Minimum="{Binding LocalScaleMin}" Maximum="{Binding LocalScaleMax}" Value="{Binding LocalScale}"/>
<Slider Grid.Row="1" Grid.Column="3" Style="{StaticResource SimpleSlider}" Height="15" Margin="4,0" Minimum="{Binding OffsetXMin}" Maximum="{Binding OffsetXMax}" Value="{Binding OffsetX}"/>
<Slider Grid.Row="2" Grid.Column="3" Style="{StaticResource SimpleSlider}" Height="15" Margin="4,0" Minimum="{Binding OffsetYMin}" Maximum="{Binding OffsetYMax}" Value="{Binding OffsetY}"/>
<Slider Grid.Row="3" Grid.Column="3" Style="{StaticResource SimpleSlider}" Height="15" Margin="4,0" Minimum="{Binding AlphaMin}" Maximum="{Binding AlphaMax}" Value="{Binding Alpha}"/>
<TextBox Grid.Row="0" Grid.Column="4" Style="{StaticResource RoundCornerTextBox}" Margin="2" FontSize="12" Text="{Binding LocalScale, StringFormat={}{0:F2}}"
KeyDown="ConfirmTextInput" PreviewMouseWheel="ChangeBindDouble"/>
<TextBox Grid.Row="1" Grid.Column="4" Style="{StaticResource RoundCornerTextBox}" Margin="2" FontSize="12" Text="{Binding OffsetX, StringFormat={}{0:F1}}"
KeyDown="ConfirmTextInput" PreviewMouseWheel="ChangeBindDouble"/>
<TextBox Grid.Row="2" Grid.Column="4" Style="{StaticResource RoundCornerTextBox}" Margin="2" FontSize="12" Text="{Binding OffsetY, StringFormat={}{0:F1}}"
KeyDown="ConfirmTextInput" PreviewMouseWheel="ChangeBindDouble"/>
<TextBox Grid.Row="3" Grid.Column="4" Style="{StaticResource RoundCornerTextBox}" Margin="2" FontSize="12" Text="{Binding Alpha, StringFormat={}{0:F2}}"
KeyDown="ConfirmTextInput" PreviewMouseWheel="ChangeBindDouble"/>
<uc:IconButton Grid.Row="3" Grid.Column="0" Cursor="Hand" Margin="3" Width="20" Height="20"
VerticalAlignment="Stretch" HorizontalAlignment="Stretch" ToolTip="{Binding VisibilityToolTip}"
PathData="{Binding VisibilityIcon}" PathMargin="0,5,0,0" PathFill="{Binding VisibilityButtonBrush}" CornerRadius="4" PathBackground="#FF651F65"
IsFlipped="True" MouseLeftButtonDown="ChangeVisibleState" ActiveColor="#FFF48C8C"/>
<uc:IconButton Grid.Row="3" Grid.Column="1" Cursor="Hand" Margin="3" Width="20" Height="20" VerticalAlignment="Stretch" HorizontalAlignment="Stretch" ToolTip="{Binding LockToolTip}"
PathData="{Binding LockIcon}" PathFill="{Binding LockButtonBrush}" CornerRadius="4"
PathBackground="#FF651F65" IsFlipped="True" MouseLeftButtonDown="ChangeLockState">
</uc:IconButton>
</Grid>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</Grid>
<Viewbox Grid.Row="1" Grid.Column="1" Margin="20" ClipToBounds="True">
<Grid>
<ItemsControl ItemsSource="{Binding Segments}" >
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas d:DataContext="{d:DesignInstance Type=vm:ElementViewModel}" Background="{Binding ImageBackground}"
Loaded="MainCanvas_Loaded"
MouseLeftButtonDown="Canvas_MouseLeftButtonDown"
MouseMove="Canvas_MouseMove"
MouseLeftButtonUp="Canvas_MouseLeftButtonUp"
Width="{Binding CanvasWidth}" Height="{Binding CanvasHeight}"
HorizontalAlignment="Left" VerticalAlignment="Top"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemContainerStyle>
<Style TargetType="ContentPresenter">
<Setter Property="Canvas.Left" Value="{Binding OffsetX}"/>
<Setter Property="Canvas.Top" Value="{Binding OffsetY}"/>
</Style>
</ItemsControl.ItemContainerStyle>
<ItemsControl.ItemTemplate>
<DataTemplate DataType="{x:Type vm:ElementSegmentViewModel}">
<Grid d:DataContext="{d:DesignInstance Type=vm:ElementSegmentViewModel}" Visibility="{Binding SegmentVisibility}"
IsHitTestVisible="{Binding IsHitTestVisible}"
MouseLeftButtonDown="Draggable_MouseLeftButtonDown"
MouseLeftButtonUp="Draggable_MouseLeftButtonUp"
MouseMove="Draggable_MouseMove" MouseWheel="Grid_MouseWheel"
LayoutTransform="{Binding LocalScale, Converter={StaticResource ScaleToTransformConverter}}">
<uc:AnimatedImage ImageSource="{Binding ImageSource}" Width="{Binding Width}" Height="{Binding Height}"
HorizontalAlignment="Left" VerticalAlignment="Top" Opacity="{Binding Alpha}"
RenderOptions.BitmapScalingMode="NearestNeighbor">
</uc:AnimatedImage>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<ItemsControl IsHitTestVisible="False" ItemsSource="{Binding SelectedSegmentViewModels}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas d:DataContext="{d:DesignInstance Type=vm:ElementViewModel}"
Width="{Binding CanvasWidth}" Height="{Binding CanvasHeight}"
HorizontalAlignment="Left" VerticalAlignment="Top"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemContainerStyle>
<Style TargetType="ContentPresenter">
<Setter Property="Canvas.Left" Value="{Binding OffsetX}"/>
<Setter Property="Canvas.Top" Value="{Binding OffsetY}"/>
</Style>
</ItemsControl.ItemContainerStyle>
<ItemsControl.ItemTemplate>
<DataTemplate DataType="{x:Type vm:ElementSegmentViewModel}">
<Border x:Name="SelectedSegmentBorder" Visibility="{Binding SegmentVisibility}" Width="{Binding Width}" Height="{Binding Height}"
BorderBrush="#FFFF3B30" BorderThickness="0.5" Background="Transparent"
LayoutTransform="{Binding LocalScale, Converter={StaticResource ScaleToTransformConverter}}"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<Canvas IsHitTestVisible="False" Width="{Binding CanvasWidth}" Height="{Binding CanvasHeight}"
HorizontalAlignment="Left" VerticalAlignment="Top">
<Rectangle Visibility="{Binding SelectionRectVisibility}"
Canvas.Left="{Binding SelectionRectLeft}"
Canvas.Top="{Binding SelectionRectTop}"
Width="{Binding SelectionRectWidth}"
Height="{Binding SelectionRectHeight}"
Fill="#3379B8FF"
Stroke="#FF2F7FDB"
StrokeThickness="0.5"/>
</Canvas>
</Grid>
</Viewbox>
<Grid Grid.Row="0" Grid.Column="2" Grid.RowSpan="2">
<Grid.RowDefinitions>
<RowDefinition Height="100"/>
<RowDefinition Height="90*"/>
<RowDefinition Height="120*"/>
</Grid.RowDefinitions>
<Grid Grid.Row="0" >
<Grid.RowDefinitions>
<RowDefinition Height="40"/>
<RowDefinition Height="100*"/>
</Grid.RowDefinitions>
<Label Grid.Row="0" Style="{StaticResource RoundedLabel}" Background="#FF540754" Foreground="White" Content="设置" Margin="10"/>
<ScrollViewer Grid.Row="1" Style="{StaticResource SimpleScrollViewerStyle}" Margin="0">
<StackPanel Orientation="Vertical">
<Grid Margin="10,3" Height="30">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="50"/>
<ColumnDefinition Width="100*"/>
<ColumnDefinition Width="50"/>
<ColumnDefinition Width="100*"/>
</Grid.ColumnDefinitions>
<Label Grid.Column ="0" Style="{StaticResource RoundedLabel}" Content="测试" Width="40" HorizontalAlignment="Left" Background="#FFDDDCDE"/>
<CheckBox Grid.Column ="1" Style="{StaticResource LabeledToggleSwitch}" HorizontalAlignment="Stretch"
FontSize="15" IsChecked="{Binding AllowTesting}"/>
<Label Grid.Column ="2" Style="{StaticResource RoundedLabel}" Content="背景" Width="40" HorizontalAlignment="Left" Background="#FFDDDCDE"/>
<Border Grid.Column ="3" CornerRadius="2" Margin="2" Background="{Binding ImageBackground}" MouseLeftButtonDown="ChangeBackground"/>
</Grid>
</StackPanel>
</ScrollViewer>
</Grid>
<Grid Grid.Row="1" >
<Grid.RowDefinitions>
<RowDefinition Height="40"/>
<RowDefinition Height="100*"/>
</Grid.RowDefinitions>
<Label Grid.Row="0" Style="{StaticResource RoundedLabel}" Background="#FF540754" Foreground="White" Content="预览" Margin="10"/>
<Viewbox Grid.Row="1" Margin="10">
<Canvas Width="{Binding ModifiedCanvasWidth}" Height="{Binding ModifiedCanvasHeight}"
Background="{Binding ImageBackground}" HorizontalAlignment="Left" VerticalAlignment="Top">
<Grid Canvas.Left="{Binding OffsetX}" Canvas.Top="{Binding OffsetY}">
<uc:AnimatedImage ImageSource="{Binding CurrentModifiedImageSource}"
Width="{Binding CurrentModifiedImageWidth}" Height="{Binding CurrentModifiedImageHeight}"
HorizontalAlignment="Left" VerticalAlignment="Top"
RenderOptions.BitmapScalingMode="NearestNeighbor"
RenderTransformOrigin="0.5,0.5">
<uc:AnimatedImage.RenderTransform>
<RotateTransform Angle="{Binding RotateAngle}"/>
</uc:AnimatedImage.RenderTransform>
</uc:AnimatedImage>
</Grid>
</Canvas>
</Viewbox>
</Grid>
<Grid Grid.Row="2" Visibility="{Binding HasSelectedSegmentVisibility}">
<Grid.RowDefinitions>
<RowDefinition Height="40"/>
<RowDefinition Height="40"/>
<RowDefinition Height="100*"/>
<RowDefinition Height="40"/>
</Grid.RowDefinitions>
<Label Grid.Row="0" Style="{StaticResource RoundedLabel}" Background="#FF540754" Foreground="White" Content="擦除" Margin="10"/>
<Grid Grid.Row="1" Margin="10,3" Height="28" Visibility="{Binding HasSelectedSegmentVisibility}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="70"/>
<ColumnDefinition Width="100*"/>
<ColumnDefinition Width="30*"/>
</Grid.ColumnDefinitions>
<Label Grid.Column="0" Style="{StaticResource RoundedLabel}" Content="笔刷半径" Width="70" HorizontalAlignment="Left" Background="#FFDDDCDE"/>
<Slider Grid.Column="1" Minimum="{Binding MinEraseBrushRadius}" Maximum="{Binding MaxEraseBrushRadius}" HorizontalAlignment="Stretch"
Margin="10,0,10,0"
Style="{StaticResource SimpleSlider}" Value="{Binding EraseBrushRadius}"
VerticalAlignment="Center" Height="20" />
<TextBox Grid.Column="2" Grid.ColumnSpan="3" Style="{StaticResource RoundCornerTextBox}" HorizontalAlignment="Stretch"
Height="20" FontSize="13" Text="{Binding EraseBrushRadius, StringFormat={}{0:F1}}" Margin="0"
KeyDown="ConfirmTextInput" PreviewMouseWheel="ChangeBindDouble"/>
</Grid>
<Viewbox Grid.Row="2" Margin="10,3">
<Canvas Width="{Binding CanvasWidth}" Height="{Binding CanvasHeight}"
Background="{Binding ImageBackground}" HorizontalAlignment="Left" VerticalAlignment="Top"
PreviewMouseWheel="ErasePreview_PreviewMouseWheel" Cursor="Pen">
<Grid Canvas.Left="0" Canvas.Top="0">
<uc:AnimatedImage ImageSource="{Binding SelectedSegmentSource}"
Width="{Binding SelectedSegmentWidth}" Height="{Binding SelectedSegmentHeight}"
HorizontalAlignment="Left" VerticalAlignment="Top"
RenderOptions.BitmapScalingMode="NearestNeighbor"
MouseLeftButtonDown="ErasePreview_MouseLeftButtonDown"
MouseEnter="ErasePreview_MouseEnter"
MouseMove="ErasePreview_MouseMove"
MouseLeave="ErasePreview_MouseLeave"
MouseLeftButtonUp="ErasePreview_MouseLeftButtonUp"/>
</Grid>
<Ellipse IsHitTestVisible="False"
Visibility="{Binding EraseBrushPreviewVisibility}"
Canvas.Left="{Binding EraseBrushPreviewLeft}"
Canvas.Top="{Binding EraseBrushPreviewTop}"
Width="{Binding EraseBrushDiameter}"
Height="{Binding EraseBrushDiameter}"
Fill="#3AFFFFFF"
Stroke="#FFFF8C8C"
StrokeThickness="0.5"/>
</Canvas>
</Viewbox>
<StackPanel Grid.Row="3" Orientation="Horizontal" HorizontalAlignment="Center">
<uc:IconButton PathData="{x:Static cm:PathDataGeometry.CheckCircle}" Margin="5,0" Width="70" Height="24" FontSize="12" IsFlipped="True"
IsEnabled="{Binding HasPendingErase}" PathFill="White" PathBackground="#FF540754" CornerRadius="4"
Click="SaveEraseResult"/>
<uc:IconButton PathData="{x:Static cm:PathDataGeometry.XCircle}" PathFill="White" PathBackground="#FF540754" IsFlipped="True"
Margin="5,0" Width="70" Height="24" FontSize="12" CornerRadius="4"
IsEnabled="{Binding HasPendingErase}" Click="DiscardEraseResult"/>
</StackPanel>
</Grid>
</Grid>
<GridSplitter Grid.Column="1" Grid.Row="0" Grid.RowSpan="2" Cursor="SizeWE" Width="2" HorizontalAlignment="Left" VerticalAlignment="Stretch" Background="Gray" ShowsPreview="True"/>
<GridSplitter Grid.Column="2" Grid.Row="0" Grid.RowSpan="2" Cursor="SizeWE" Width="2" HorizontalAlignment="Left" VerticalAlignment="Stretch" Background="Gray" ShowsPreview="True"/>
</Grid>
<Grid Grid.Row="1" >
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
<Button Style="{StaticResource RoundCornerButton}" Content="保存" Margin="5,5" Width="60" Height="25" FontSize="15" Click="SaveConfig"/>
<Button Style="{StaticResource RoundCornerButton}" Content="舍弃" Margin="5,5" Width="60" Height="25" FontSize="15" Click="CloseWindow"/>
</StackPanel>
</Grid>
</Grid>
</Window>
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,97 @@
<local:ElementBase x:Class="FancyInput.Views.Elements.GamepadButton"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:FancyInput.Views.Elements"
xmlns:vm="clr-namespace:FancyInput.ViewModels"
xmlns:uc ="clr-namespace:FancyInput.Views.Controls"
d:DataContext="{d:DesignInstance Type=vm:ElementViewModel}"
mc:Ignorable="d"
Title="手柄按键" Height="400" Width="800">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="1000*"/>
<RowDefinition Height="100*"/>
<RowDefinition Height="20*"/>
</Grid.RowDefinitions>
<Grid Grid.Row="0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="100*"/>
<ColumnDefinition Width="100*"/>
<ColumnDefinition Width="90*"/>
</Grid.ColumnDefinitions>
<uc:ElementViewPanel Grid.Column="0" Index="0" Title="贴图(未按下)"
ImageBackground="{Binding ImageBackground}" ImageSource="{Binding Image0Source}"
LoadImageClick="LoadImage" EditImageClick="EditImage" CopyImageClick="CopyImage" PasteImageClick="PasteImage"/>
<uc:ElementViewPanel Grid.Column="1" Index="1" Title="贴图(按下)"
ImageBackground="{Binding ImageBackground}" ImageSource="{Binding Image1Source}"
LoadImageClick="LoadImage" EditImageClick="EditImage" CopyImageClick="CopyImage" PasteImageClick="PasteImage"/>
<Grid Grid.Column="2">
<Grid.RowDefinitions>
<RowDefinition Height="100*"/>
<RowDefinition Height="1110*"/>
</Grid.RowDefinitions>
<Label Grid.Row="0" Content="配置" FontSize="15" FontFamily="Cascadia Mono" HorizontalAlignment="Center" VerticalAlignment="Bottom"/>
<Border Grid.Row="1" Margin="5,5" BorderBrush="Gray" BorderThickness="1" CornerRadius="20">
<ScrollViewer Style="{StaticResource SimpleScrollViewerStyle}">
<StackPanel VerticalAlignment="Center">
<StackPanel Orientation="Horizontal" Margin="0,10">
<Label Content="背景" FontFamily="Cascadia Mono" VerticalAlignment="Center" Margin="0,0" FontSize="15"/>
<Button Style="{StaticResource InfoTipButtonStyle}" ToolTip="仅用于方便查看,不会真正改变像素" />
<Border Width="60" Margin="0" BorderBrush="Gray" BorderThickness="1" CornerRadius="4" Background="{Binding ImageBackground}"
MouseLeftButtonDown="BackGroundBorder_MouseLeftButtonDown"/>
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="0,10">
<Label Content="名称" FontFamily="Cascadia Mono" VerticalAlignment="Center" Margin="0,0" FontSize="15"/>
<TextBox Width="70" Height="30" Margin="10,5" Style="{StaticResource RoundCornerTextBox}" Padding="0" Text="{Binding Id,Mode=TwoWay}"
FontSize="15" VerticalContentAlignment="Center" HorizontalContentAlignment="Center"/>
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="0,10">
<Label Content="触发" FontFamily="Cascadia Mono" VerticalAlignment="Center" Margin="0,0" FontSize="15"/>
<ComboBox Height="30" Width="80" Margin="5,0" Style="{StaticResource RoundCornerComboBox}" ItemContainerStyle="{StaticResource CustomComboBoxItemStyle}"
HorizontalContentAlignment="Center" VerticalContentAlignment="Center"
ItemsSource="{Binding GamepadButtonList}" SelectedItem="{Binding SelectedGamepadButton}"/>
<Button x:Name="DetecButton" Content="{Binding DetectingString}" Background="{Binding DetectingColor}"
FontSize="8" Width="30" Height="15" BorderBrush="Purple" Style="{StaticResource RoundCornerButton}"
Click="DetectInput"/>
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="0,10">
<Label Content="仅下降沿" FontFamily="Cascadia Mono" VerticalAlignment="Center" Margin="0,0" FontSize="15"/>
<Button Style="{StaticResource InfoTipButtonStyle}" ToolTip="仅当按键按键按下时触发,不响应按键松开,因此需要手动设置持续时间" />
<Viewbox Height="30">
<CheckBox Style="{StaticResource LabeledToggleSwitch}" IsChecked="{Binding UseStopwatch}"/>
</Viewbox>
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="0,10" Visibility="{Binding StopwatchVisibility}">
<Label Content="持续时间" FontFamily="Cascadia Mono" VerticalAlignment="Center" Margin="0,0" FontSize="15"/>
<Slider Style="{StaticResource MiniSlider}" Width="70" Minimum="50" Maximum="5000" TickFrequency="1" Value="{Binding StopwatchMilliseconds}"/>
<TextBox Width="30" Height="20" Margin="5,5" Style="{StaticResource RoundCornerTextBox}" Padding="0" Text="{Binding StopwatchMilliseconds}"
FontSize="10" VerticalContentAlignment="Center" HorizontalContentAlignment="Center"
TextChanged="ChangeStopwatch" KeyDown="ConfirmTextInput" MouseWheel="WheelStopwatch"/>
<Label Content="ms" FontSize="10" Padding="0" HorizontalAlignment="Left" VerticalAlignment="Center"/>
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="0,10">
<Label Content="W" FontFamily="Cascadia Mono" VerticalAlignment="Center" Margin="0,0" FontSize="15"/>
<TextBox Style="{StaticResource RoundCornerTextBox}" FontSize="15" Height="30" Width="60" IsReadOnly="True" Margin="10,0" Text="{Binding MappingW}"/>
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="0,10">
<Label Content="H" FontFamily="Cascadia Mono" VerticalAlignment="Center" Margin="0,0" FontSize="15"/>
<TextBox Style="{StaticResource RoundCornerTextBox}" FontSize="15" Height="30" Width="60" IsReadOnly="True" Margin="10,0" Text="{Binding MappingH}"/>
</StackPanel>
</StackPanel>
</ScrollViewer>
</Border>
</Grid>
</Grid>
<Border Grid.Row="1" Background="Gray" Height="0.5" VerticalAlignment="Top" Margin="0,1"/>
<StackPanel Grid.Row="1" Orientation="Horizontal" HorizontalAlignment="Right">
<Button Content="对齐与测试" Style="{StaticResource FancyButton}" FontSize="15" Margin="5,4" Width="90" Click="AlignAndTest"/>
<Button Content="保存" Style="{StaticResource FancyButton}" FontSize="15" Margin="5,4" Click="SaveConfig"/>
<Button Content="取消" Style="{StaticResource FancyButton}" FontSize="15" Margin="10,4" Click="CloseWindow"/>
</StackPanel>
</Grid>
</local:ElementBase>
@@ -0,0 +1,56 @@
using FancyInput.Models;
using FancyInput.ViewModels;
using FancyInput.Views.Windows;
using Microsoft.Win32;
using System.ComponentModel;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using SharpDX.XInput;
namespace FancyInput.Views.Elements
{
/// <summary>
/// GamepadButton.xaml 的交互逻辑
/// </summary>
public partial class GamepadButton : ElementBase
{
public GamepadButton(ElementViewModel elementViewModel)
: base(elementViewModel)
{
InitializeComponent();
GetInputWhenDetecting += HandleGetInputWhenDetecting;
}
protected void HandleGetInputWhenDetecting(InputArgs args)
{
if (args.Device == Models.InputDevice.XInputButton || args.Device == Models.InputDevice.XInputTrigger)
{
FIPGamepadButtonflags? flag = args.Flag;
if (flag.HasValue)
{
if (Enum.TryParse<GamepadCodeType>(flag.Value.ToString(), out GamepadCodeType codeType))
{
ElementViewModel.SelectedGamepadButton = codeType;
}
else if (flag.Value == FIPGamepadButtonflags.LS || flag.Value == FIPGamepadButtonflags.RS)
{
FancyInput.AppMessageBox.Show("摇杆按下的输入请通过“手柄摇杆”元素来设置,此页面用于创建“手柄按键”。", "提示", MessageBoxButton.OK, MessageBoxImage.Information);
}
else
{
FancyInput.AppMessageBox.Show($"无法将检测到的按键转换为GamepadCodeType:{args.Tag}", "提示", MessageBoxButton.OK, MessageBoxImage.Warning);
}
}
if (args.Side.HasValue)
{
FancyInput.AppMessageBox.Show("扳机按下的输入请通过“手柄扳机”元素来设置,此页面用于创建“手柄按键”", "提示", MessageBoxButton.OK, MessageBoxImage.Information);
}
OFFDetect();
}
}
}
}
+128
View File
@@ -0,0 +1,128 @@
<local:ElementBase x:Class="FancyInput.Views.Elements.GamepadDpad"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:FancyInput.Views.Elements"
xmlns:vm="clr-namespace:FancyInput.ViewModels"
xmlns:uc ="clr-namespace:FancyInput.Views.Controls"
d:DataContext="{d:DesignInstance Type=vm:ElementViewModel}"
mc:Ignorable="d"
Title="手柄十字键" Height="400" Width="800">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="1000*"/>
<RowDefinition Height="100*"/>
<RowDefinition Height="20*"/>
</Grid.RowDefinitions>
<Grid Grid.Row="0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="400*"/>
<ColumnDefinition Width="100*"/>
</Grid.ColumnDefinitions>
<Grid Grid.Column="0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="100*"/>
<ColumnDefinition Width="100*"/>
<ColumnDefinition Width="100*"/>
<ColumnDefinition Width="100*"/>
<ColumnDefinition Width="100*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="100*"/>
<RowDefinition Height="100*"/>
</Grid.RowDefinitions>
<uc:ElementViewPanel Grid.Row="0" Grid.Column="0" Index="0" Title="贴图(未按下)" FontSize="10" Margin="2"
RowHeight1="150*" RowHeight2="800*" RowHeight3="300*" ImageMargin="10,0"
ImageBackground="{Binding ImageBackground}" ImageSource="{Binding Image0Source}"
LoadImageClick="LoadImage" EditImageClick="EditImage" CopyImageClick="CopyImage" PasteImageClick="PasteImage"/>
<uc:ElementViewPanel Grid.Row="0" Grid.Column="1" Index="1" Title="贴图(左)" FontSize="10" Margin="2"
RowHeight1="150*" RowHeight2="800*" RowHeight3="300*" ImageMargin="10,0"
ImageBackground="{Binding ImageBackground}" ImageSource="{Binding Image1Source}"
LoadImageClick="LoadImage" EditImageClick="EditImage" CopyImageClick="CopyImage" PasteImageClick="PasteImage"/>
<uc:ElementViewPanel Grid.Row="0" Grid.Column="2" Index="2" Title="贴图(右)" FontSize="10" Margin="2"
RowHeight1="150*" RowHeight2="800*" RowHeight3="300*" ImageMargin="10,0"
ImageBackground="{Binding ImageBackground}" ImageSource="{Binding Image2Source}"
LoadImageClick="LoadImage" EditImageClick="EditImage" CopyImageClick="CopyImage" PasteImageClick="PasteImage"/>
<uc:ElementViewPanel Grid.Row="0" Grid.Column="3" Index="3" Title="贴图(上)" FontSize="10" Margin="2"
RowHeight1="150*" RowHeight2="800*" RowHeight3="300*" ImageMargin="10,0"
ImageBackground="{Binding ImageBackground}" ImageSource="{Binding Image3Source}"
LoadImageClick="LoadImage" EditImageClick="EditImage" CopyImageClick="CopyImage" PasteImageClick="PasteImage"/>
<uc:ElementViewPanel Grid.Row="0" Grid.Column="4" Index="4" Title="贴图(下)" FontSize="10" Margin="2"
RowHeight1="150*" RowHeight2="800*" RowHeight3="300*" ImageMargin="10,0"
ImageBackground="{Binding ImageBackground}" ImageSource="{Binding Image4Source}"
LoadImageClick="LoadImage" EditImageClick="EditImage" CopyImageClick="CopyImage" PasteImageClick="PasteImage"/>
<uc:ElementViewPanel Grid.Row="1" Grid.Column="0" Index="5" Title="贴图(左上)" FontSize="10" Margin="2"
RowHeight1="150*" RowHeight2="800*" RowHeight3="300*" ImageMargin="10,0"
ImageBackground="{Binding ImageBackground}" ImageSource="{Binding Image5Source}"
LoadImageClick="LoadImage" EditImageClick="EditImage" CopyImageClick="CopyImage" PasteImageClick="PasteImage"/>
<uc:ElementViewPanel Grid.Row="1" Grid.Column="1" Index="6" Title="贴图(右上)" FontSize="10" Margin="2"
RowHeight1="150*" RowHeight2="800*" RowHeight3="300*" ImageMargin="10,0"
ImageBackground="{Binding ImageBackground}" ImageSource="{Binding Image6Source}"
LoadImageClick="LoadImage" EditImageClick="EditImage" CopyImageClick="CopyImage" PasteImageClick="PasteImage"/>
<uc:ElementViewPanel Grid.Row="1" Grid.Column="2" Index="7" Title="贴图(左下)" FontSize="10" Margin="2"
RowHeight1="150*" RowHeight2="800*" RowHeight3="300*" ImageMargin="10,0"
ImageBackground="{Binding ImageBackground}" ImageSource="{Binding Image7Source}"
LoadImageClick="LoadImage" EditImageClick="EditImage" CopyImageClick="CopyImage" PasteImageClick="PasteImage"/>
<uc:ElementViewPanel Grid.Row="1" Grid.Column="3" Index="8" Title="贴图(右下)" FontSize="10" Margin="2"
RowHeight1="150*" RowHeight2="800*" RowHeight3="300*" ImageMargin="10,0"
ImageBackground="{Binding ImageBackground}" ImageSource="{Binding Image8Source}"
LoadImageClick="LoadImage" EditImageClick="EditImage" CopyImageClick="CopyImage" PasteImageClick="PasteImage"/>
</Grid>
<Grid Grid.Column="1">
<Grid.RowDefinitions>
<RowDefinition Height="100*"/>
<RowDefinition Height="1110*"/>
</Grid.RowDefinitions>
<Label Grid.Row="0" Content="配置" FontSize="15" FontFamily="Cascadia Mono" HorizontalAlignment="Center" VerticalAlignment="Bottom"/>
<Border Grid.Row="1" Margin="5,5" BorderBrush="Gray" BorderThickness="1" CornerRadius="20">
<StackPanel VerticalAlignment="Center">
<StackPanel Orientation="Horizontal" Margin="0,10">
<Label Content="背景" FontFamily="Cascadia Mono" VerticalAlignment="Center" Margin="0,0" FontSize="15"/>
<Border Height="14" Width="14" BorderBrush="Purple" BorderThickness="1" CornerRadius="8" Margin="0,0,10,0"
Cursor="Hand" ToolTip="仅用于方便查看,不会真正改变像素">
<Border.Style>
<Style TargetType="Border">
<Setter Property="Background" Value="#05808080"/>
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="#0B5B06DE"/>
</Trigger>
</Style.Triggers>
</Style>
</Border.Style>
<Label Content="?" FontSize="8" Padding="0" HorizontalAlignment="Center" VerticalAlignment="Center" FontWeight="Bold" Foreground="Purple"/>
</Border>
<Border Width="60" Margin="0" BorderBrush="Gray" BorderThickness="1" CornerRadius="4" Background="{Binding ImageBackground}"
MouseLeftButtonDown="BackGroundBorder_MouseLeftButtonDown"/>
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="0,10">
<Label Content="名称" FontFamily="Cascadia Mono" VerticalAlignment="Center" Margin="0,0" FontSize="15"/>
<TextBox Width="70" Height="30" Margin="10,5" Style="{StaticResource RoundCornerTextBox}" Padding="0" Text="{Binding Id,Mode=TwoWay}"
FontSize="15" VerticalContentAlignment="Center" HorizontalContentAlignment="Center"/>
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="0,10">
<Label Content="W" FontFamily="Cascadia Mono" VerticalAlignment="Center" Margin="0,0" FontSize="15"/>
<TextBox Style="{StaticResource RoundCornerTextBox}" FontSize="15" Height="30" Width="60" IsReadOnly="True" Margin="10,0" Text="{Binding MappingW}"/>
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="0,10">
<Label Content="H" FontFamily="Cascadia Mono" VerticalAlignment="Center" Margin="0,0" FontSize="15"/>
<TextBox Style="{StaticResource RoundCornerTextBox}" FontSize="15" Height="30" Width="60" IsReadOnly="True" Margin="10,0" Text="{Binding MappingH}"/>
</StackPanel>
</StackPanel>
</Border>
</Grid>
</Grid>
<Border Grid.Row="1" Background="Gray" Height="0.5" VerticalAlignment="Top" Margin="0,1"/>
<StackPanel Grid.Row="1" Orientation="Horizontal" HorizontalAlignment="Right">
<Button Content="对齐与测试" Style="{StaticResource FancyButton}" FontSize="15" Margin="5,4" Width="90" Click="AlignAndTest"/>
<Button Content="保存" Style="{StaticResource FancyButton}" FontSize="15" Margin="5,4" Click="SaveConfig"/>
<Button Content="取消" Style="{StaticResource FancyButton}" FontSize="15" Margin="10,4" Click="CloseWindow"/>
</StackPanel>
</Grid>
</local:ElementBase>
@@ -0,0 +1,38 @@
using FancyInput.Models;
using FancyInput.ViewModels;
using SharpDX.XInput;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;
namespace FancyInput.Views.Elements
{
/// <summary>
/// GamepadDpad.xaml 的交互逻辑
/// </summary>
public partial class GamepadDpad : ElementBase
{
public GamepadDpad(ElementViewModel elementViewModel)
: base(elementViewModel)
{
InitializeComponent();
GetInputWhenDetecting += HandleGetInputWhenDetecting;
}
protected void HandleGetInputWhenDetecting(InputArgs args)
{
}
}
}

Some files were not shown because too many files have changed in this diff Show More