FancyInput
This commit is contained in:
+39
@@ -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
|
||||
@@ -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
|
||||
@@ -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>
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
)]
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>(); }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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,
|
||||
// 先设具体按键值,再设 SelectedTriggerDevice(setter 会读取它们)
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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="当具有管理员权限的软件在前台运行时,如果本应用以非管理员身份运行,可能导致输入捕获失败;
注意:从非管理员提升至管理员权限后,此次打开的应用默认保持管理员权限状态,不受此选项关闭的影响。"/>
|
||||
</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="部分游戏会强制将鼠标位置锁定在屏幕中心,为了让鼠标移动输入检测在这些游戏中也能正常使用,可以开启此选项;
注意:此功能可能会造成本应用卡顿,建议不用时保持关闭。"/>
|
||||
</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="值越小,对输入的检测越灵敏,但值过小会导致程序卡顿;
受此项影响的元素包括手柄操作、鼠标滚轮等。" />
|
||||
</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();
|
||||
//}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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}";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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
Reference in New Issue
Block a user