diff --git a/FluentAutoClicker/App.xaml.cs b/FluentAutoClicker/App.xaml.cs index bbc2c99..8933114 100644 --- a/FluentAutoClicker/App.xaml.cs +++ b/FluentAutoClicker/App.xaml.cs @@ -27,7 +27,7 @@ namespace FluentAutoClicker; /// public partial class App : Application { - public static Window Window { get; private set; } = null!; + public static readonly MainWindow MainWindow = new(); /// /// Initializes the singleton application object. This is the first line of authored code @@ -44,7 +44,6 @@ public App() /// Details about the launch request and process. protected override void OnLaunched(Microsoft.UI.Xaml.LaunchActivatedEventArgs args) { - Window = new MainWindow(); - Window.Activate(); + MainWindow.Activate(); } } \ No newline at end of file diff --git a/FluentAutoClicker/FluentAutoClicker.csproj b/FluentAutoClicker/FluentAutoClicker.csproj index e515e3a..710ab2e 100644 --- a/FluentAutoClicker/FluentAutoClicker.csproj +++ b/FluentAutoClicker/FluentAutoClicker.csproj @@ -11,6 +11,7 @@ win-$(Platform).pubxml enable enable + true true true DISABLE_XAML_GENERATED_MAIN @@ -37,6 +38,10 @@ + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/FluentAutoClicker/Helpers/AutoClicker.cs b/FluentAutoClicker/Helpers/AutoClicker.cs index cebaa32..c296b4c 100644 --- a/FluentAutoClicker/Helpers/AutoClicker.cs +++ b/FluentAutoClicker/Helpers/AutoClicker.cs @@ -16,6 +16,8 @@ // along with Fluent Auto Clicker. If not, see . using System.Runtime.InteropServices; +using Windows.Win32; +using Windows.Win32.UI.Input.KeyboardAndMouse; namespace FluentAutoClicker.Helpers; @@ -24,9 +26,6 @@ namespace FluentAutoClicker.Helpers; /// public static class AutoClicker { - [DllImport("user32.dll", SetLastError = true)] - private static extern uint SendInput(uint nInputs, Input[] pInputs, int cbSize); - private static Thread? _autoClickerThread; private static bool _isAutoClickerRunning; @@ -37,114 +36,87 @@ public static class AutoClicker /// The number of clicks before stopping the auto clicker thread. /// The mouse button used to click. /// The amount of time in milliseconds to add randomly to the millisecond delay between clicks. - public static void StartAutoClicker(int millisecondsDelay, int clickAmount, int mouseButtonType, int clickDelayOffset) + public static void Start(int millisecondsDelay = 100, int clickAmount = 0, + int mouseButtonType = 0, int clickDelayOffset = 0) { - // TODO: Evaluate whether a thread is necessary for this. + // TODO: Move the parameters to another function to be able to change parameters while the thread is running. _isAutoClickerRunning = true; - _autoClickerThread = new Thread(() => AutoClickerThread(millisecondsDelay, clickAmount, mouseButtonType, clickDelayOffset)); + _autoClickerThread = new Thread(() => + AutoClickerThread(millisecondsDelay, clickAmount, mouseButtonType, clickDelayOffset)); _autoClickerThread.Start(); } /// /// Stops the auto clicker thread. /// - public static void StopAutoClicker() + public static void Stop() { _isAutoClickerRunning = false; - // HACK: Incorrectly stops the thread, but it works for now. _autoClickerThread?.Join(); } - private static async void AutoClickerThread(int clickInterval, int repeatAmount, int mouseButton, int clickOffset) + private static async void AutoClickerThread(int clickInterval, int repeatAmount, int mouseButton, + int clickOffset) { int clickCount = 0; - Random random = new(); + while (_isAutoClickerRunning) { + // Stop if we click more than repeat amount if (clickCount >= repeatAmount && repeatAmount != 0) { - StopAutoClicker(); + Stop(); break; } - // TODO: Move this to a enum instead of a number - switch (mouseButton) - { - case 0: - MouseEvent(0, 0, (uint)MouseEventF.LeftDown, 0, 0, IntPtr.Zero); - MouseEvent(0, 0, (uint)MouseEventF.LeftUp, 0, 0, IntPtr.Zero); - break; - case 1: - MouseEvent(0, 0, (uint)MouseEventF.MiddleDown, 0, 0, IntPtr.Zero); - MouseEvent(0, 0, (uint)MouseEventF.MiddleUp, 0, 0, IntPtr.Zero); - break; - case 2: - MouseEvent(0, 0, (uint)MouseEventF.RightDown, 0, 0, IntPtr.Zero); - MouseEvent(0, 0, (uint)MouseEventF.RightUp, 0, 0, IntPtr.Zero); - break; - } - - if (repeatAmount > 0) - { - clickCount++; - } + // Click mouse and increment click count + ClickMouse(mouseButton); + clickCount++; - int randomClickOffset = random.Next(0, clickOffset); + // Delay before next click + int randomClickOffset = new Random().Next(0, clickOffset); await Task.Delay(clickInterval + randomClickOffset); } } - private static void MouseEvent(int dx, int dy, uint dwFlags, uint dwData, uint time, nint dwExtraInfo) + /// + /// Clicks the mouse button. + /// + /// The mouse button to click. + private static void ClickMouse(int button) { - Input[] inputs = new Input[2]; - inputs[0] = MouseInput(dx, dy, dwData, dwFlags, time, dwExtraInfo); - inputs[1] = MouseInput(dx, dy, dwData, dwFlags, time, dwExtraInfo); - _ = SendInput((uint)inputs.Length, inputs, Marshal.SizeOf()); + if (button == 0) // Left mouse button + { + SendMouseInput(MOUSE_EVENT_FLAGS.MOUSEEVENTF_LEFTDOWN); + SendMouseInput(MOUSE_EVENT_FLAGS.MOUSEEVENTF_LEFTUP); + } + else if (button == 1) // Middle mouse button + { + SendMouseInput(MOUSE_EVENT_FLAGS.MOUSEEVENTF_MIDDLEDOWN); + SendMouseInput(MOUSE_EVENT_FLAGS.MOUSEEVENTF_MIDDLEUP); + } + else if (button == 2) // Right mouse button + { + SendMouseInput(MOUSE_EVENT_FLAGS.MOUSEEVENTF_RIGHTDOWN); + SendMouseInput(MOUSE_EVENT_FLAGS.MOUSEEVENTF_RIGHTUP); + } } - private static Input MouseInput(int dx, int dy, uint mouseData, uint dwFlags, uint time, nint dwExtraInfo) + /// + /// Sends a mouse input event. + /// + /// The mouse event flags that specify the type of mouse event. + private static void SendMouseInput(MOUSE_EVENT_FLAGS dwFlags) { - return new Input - { - type = 0, - mi = new InputMouse + INPUT[] inputs = + [ + new() { - dx = dx, - dy = dy, - mouseData = mouseData, - dwFlags = dwFlags, - time = time, - dwExtraInfo = dwExtraInfo + type = INPUT_TYPE.INPUT_MOUSE, + Anonymous = new INPUT._Anonymous_e__Union { mi = new MOUSEINPUT { dwFlags = dwFlags } } } - }; - } + ]; - [StructLayout(LayoutKind.Sequential)] - private struct Input - { - public int type; - public InputMouse mi; - } - - [StructLayout(LayoutKind.Sequential)] - private struct InputMouse - { - public int dx; - public int dy; - public uint mouseData; - public uint dwFlags; - public uint time; - public IntPtr dwExtraInfo; - } - - [Flags] - private enum MouseEventF : uint - { - LeftDown = 0x0002, - LeftUp = 0x0004, - RightDown = 0x0008, - RightUp = 0x0010, - MiddleDown = 0x0020, - MiddleUp = 0x0040 + _ = PInvoke.SendInput(inputs, Marshal.SizeOf()); } -} \ No newline at end of file +} diff --git a/FluentAutoClicker/Helpers/WindowMessageHook.cs b/FluentAutoClicker/Helpers/WindowMessageHook.cs deleted file mode 100644 index ba20471..0000000 --- a/FluentAutoClicker/Helpers/WindowMessageHook.cs +++ /dev/null @@ -1,148 +0,0 @@ -// Copyright (C) 2025 Ryan Luu -// -// This file is part of Fluent Auto Clicker. -// -// Fluent Auto Clicker is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published -// by the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Fluent Auto Clicker is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with Fluent Auto Clicker. If not, see . - -using Microsoft.UI.Xaml; -using System.Collections.Concurrent; -using System.ComponentModel; -using System.Runtime.InteropServices; - -namespace FluentAutoClicker.Helpers; - -public class WindowMessageHook : IEquatable, IDisposable -{ - private delegate nint Subclassproc(nint hWnd, uint uMsg, nint wParam, nint lParam, nint uIdSubclass, uint dwRefData); - - private static readonly ConcurrentDictionary Hooks = new(); - private static readonly Subclassproc Proc = SubclassProc; - - public event EventHandler? Message; - private nint _hWnd; - - public WindowMessageHook(Window window) : this(GetHandle(window)) { } - public WindowMessageHook(nint hWnd) - { - if (hWnd == 0) - { - throw new ArgumentException(null, nameof(hWnd)); - } - - _hWnd = hWnd; - _ = Hooks.AddOrUpdate(hWnd, this, (k, o) => - { - if (Equals(o)) - { - return o; - } - - o.Dispose(); - return this; - }); - if (!SetWindowSubclass(hWnd, Proc, 0, 0)) - { - throw new Win32Exception(Marshal.GetLastWin32Error()); - } - } - - protected virtual void OnMessage(object sender, MessageEventArgs e) - { - Message?.Invoke(sender, e); - } - - protected virtual void Dispose(bool disposing) - { - if (!disposing) - { - return; - } - - nint hWnd = Interlocked.Exchange(ref _hWnd, IntPtr.Zero); - if (hWnd != IntPtr.Zero) - { - _ = RemoveWindowSubclass(hWnd, Proc, 0); - _ = Hooks.Remove(hWnd, out _); - } - } - - ~WindowMessageHook() { Dispose(disposing: false); } - public void Dispose() { Dispose(disposing: true); GC.SuppressFinalize(this); } - - [DllImport("comctl32", SetLastError = true)] - private static extern bool SetWindowSubclass(nint hWnd, Subclassproc pfnSubclass, uint uIdSubclass, uint dwRefData); - - [DllImport("comctl32", SetLastError = true)] - private static extern nint DefSubclassProc(nint hWnd, uint uMsg, nint wParam, nint lParam); - - [DllImport("comctl32", SetLastError = true)] - private static extern bool RemoveWindowSubclass(nint hWnd, Subclassproc pfnSubclass, uint uIdSubclass); - - private static nint GetHandle(Window window) - { - ArgumentNullException.ThrowIfNull(window); - return WinRT.Interop.WindowNative.GetWindowHandle(window); - } - - private static nint SubclassProc(nint hWnd, uint uMsg, nint wParam, nint lParam, nint uIdSubclass, uint dwRefData) - { - if (Hooks.TryGetValue(hWnd, out WindowMessageHook? hook)) - { - MessageEventArgs e = new(hWnd, uMsg, wParam, lParam); - hook.OnMessage(hook, e); - if (e.Result.HasValue) - { - return e.Result.Value; - } - } - return DefSubclassProc(hWnd, uMsg, wParam, lParam); - } - - public override int GetHashCode() - { - return _hWnd.GetHashCode(); - } - - public override string? ToString() - { - return _hWnd.ToString(); - } - - public override bool Equals(object? obj) - { - return Equals(obj as WindowMessageHook); - } - - public virtual bool Equals(WindowMessageHook? other) - { - return other != null && _hWnd.Equals(other._hWnd); - } -} - -public class MessageEventArgs : EventArgs -{ - public MessageEventArgs(nint hWnd, uint uMsg, nint wParam, nint lParam) - { - HWnd = hWnd; - Message = uMsg; - WParam = wParam; - LParam = lParam; - } - - public nint HWnd { get; } - public uint Message { get; } - public nint WParam { get; } - public nint LParam { get; } - public virtual nint? Result { get; set; } -} diff --git a/FluentAutoClicker/MainPage.xaml b/FluentAutoClicker/MainPage.xaml index 1a56715..4cb3a57 100644 --- a/FluentAutoClicker/MainPage.xaml +++ b/FluentAutoClicker/MainPage.xaml @@ -102,8 +102,7 @@ + Click="CheckBox_Click" /> + Click="CheckBox_Click" /> hook.Dispose(); + uint WM_HOTKEY = 0x0312; // HotKey Window Message - hook.Message += (s, e) => + if (Msg == WM_HOTKEY) { - const int wmHotkey = 0x312; - if (e.Message == wmHotkey) + if (StartToggleButton.IsEnabled) { - // Toggle the StartToggleButton when the hotkey is pressed - ToggleButtonAutomationPeer pattern = (ToggleButtonAutomationPeer)FrameworkElementAutomationPeer.FromElement(StartToggleButton).GetPattern(PatternInterface.Toggle); - pattern.Toggle(); + StartToggleButton.IsChecked = !StartToggleButton.IsChecked; } - }; + } + + return PInvoke.CallWindowProc(origHotKeyProc, hWnd, Msg, wParam, lParam); + } - nint hwnd = WinRT.Interop.WindowNative.GetWindowHandle(App.Window); - int id = 1; + [LibraryImport("user32.dll", EntryPoint = "SetWindowLongW", SetLastError = true)] + private static partial int SetWindowLong_x86(nint hWnd, int nIndex, int dwNewLong); - // Register the F6 key - if (!RegisterHotKey(hwnd, id, Mod.ModNoRepeat, VirtualKey.F6)) - { - throw new Win32Exception(Marshal.GetLastWin32Error()); - } + [LibraryImport("user32.dll", EntryPoint = "SetWindowLongPtrW", SetLastError = true)] + private static partial nint SetWindowLongPtr_x64(nint hWnd, int nIndex, nint dwNewLong); - Unloaded += (s, e) => UnregisterHotKey(hwnd, id); + // CsWin32 doesn't allow specifying the calling convention between x86 and x64 + private static nint SetWindowLongPtr(HWND hWnd, int nIndex, nint dwNewLong) + { + nint hWndInt = hWnd.Value; + return IntPtr.Size == 4 + ? SetWindowLong_x86(hWndInt, nIndex, (int)dwNewLong) + : SetWindowLongPtr_x64(hWndInt, nIndex, dwNewLong); + } + + private void MainPage_Loaded(object sender, RoutedEventArgs e) + { + // Get window handle + MainWindow window = App.MainWindow; + HWND hWnd = new(WinRT.Interop.WindowNative.GetWindowHandle(window)); + + // Register hotkey + int id = 0x0000; + _ = PInvoke.RegisterHotKey(hWnd, id, HOT_KEY_MODIFIERS.MOD_NOREPEAT, 0x75); // F6 + + // Add hotkey function pointer to window procedure + int GWL_WNDPROC = -4; + hotKeyProcD = HotKeyProc; + IntPtr hotKeyProcPtr = Marshal.GetFunctionPointerForDelegate(hotKeyProcD); + IntPtr wndPtr = SetWindowLongPtr(hWnd, GWL_WNDPROC, hotKeyProcPtr); + origHotKeyProc = Marshal.GetDelegateForFunctionPointer(wndPtr); } private void SetControlsEnabled(bool isEnabled) @@ -80,18 +104,20 @@ private void SetControlsEnabled(bool isEnabled) ClickRepeatAmount.IsEnabled = ClickRepeatCheckBox.IsChecked == true && isEnabled; // TODO: Change this to use a custom control. See https://github.com/RyanLua/FluentAutoClicker/issues/42 - string brushKey = isEnabled ? "SystemControlForegroundBaseHighBrush" : "SystemControlForegroundBaseMediumLowBrush"; + string brushKey = + isEnabled ? "SystemControlForegroundBaseHighBrush" : "SystemControlForegroundBaseMediumLowBrush"; ClickIntervalTextBlock.Foreground = Application.Current.Resources[brushKey] as Brush; HotkeyTextBlock.Foreground = Application.Current.Resources[brushKey] as Brush; } - private int GetNumberBoxValue(NumberBox numberBox, int defaultValue) + private static int GetNumberBoxValue(NumberBox numberBox, int defaultValue) { if (!int.TryParse(numberBox.Value.ToString(CultureInfo.InvariantCulture), out int value)) { value = defaultValue; numberBox.Value = value; } + return value; } @@ -132,49 +158,25 @@ private async void StartToggleButton_OnChecked(object sender, RoutedEventArgs e) int repeatAmount = ClickRepeatCheckBox.IsEnabled == true ? Convert.ToInt32(ClickRepeatAmount.Value) : 0; int mouseButton = MouseButtonTypeComboBox.SelectedIndex; int clickOffset = ClickOffsetCheckBox.IsChecked == true ? Convert.ToInt32(ClickOffsetAmount.Value) : 0; - AutoClicker.StartAutoClicker(clickInterval, repeatAmount, mouseButton, clickOffset); + AutoClicker.Start(clickInterval, repeatAmount, mouseButton, clickOffset); } private void StartToggleButton_OnUnchecked(object sender, RoutedEventArgs e) { StartToggleButton.Content = "Start"; - AutoClicker.StopAutoClicker(); + AutoClicker.Stop(); SetControlsEnabled(true); } - private void ClickRepeatCheckBox_Unchecked(object sender, RoutedEventArgs e) - { - ClickRepeatAmount.IsEnabled = false; - } - - private void ClickRepeatCheckBox_Checked(object sender, RoutedEventArgs e) - { - ClickRepeatAmount.IsEnabled = true; - } - - // interop code for Windows API hotkey functions - [DllImport("user32", SetLastError = true)] - private static extern bool RegisterHotKey(nint hWnd, int id, Mod fsModifiers, VirtualKey vk); - - [DllImport("user32", SetLastError = true)] - private static extern bool UnregisterHotKey(nint hWnd, int id); - - [Flags] - private enum Mod - { - ModAlt = 0x1, - ModControl = 0x2, - ModShift = 0x4, - ModWin = 0x8, - ModNoRepeat = 0x4000, - } - private void ClickOffsetCheckBox_Unchecked(object sender, RoutedEventArgs e) + private void CheckBox_Click(object sender, RoutedEventArgs e) { - ClickOffsetAmount.IsEnabled = false; - } - - private void ClickOffsetCheckBox_Checked(object sender, RoutedEventArgs e) - { - ClickOffsetAmount.IsEnabled = true; + if (sender.Equals(ClickRepeatCheckBox)) + { + ClickRepeatAmount.IsEnabled = ClickRepeatCheckBox.IsChecked == true; + } + else if (sender.Equals(ClickOffsetCheckBox)) + { + ClickOffsetAmount.IsEnabled = ClickOffsetCheckBox.IsChecked == true; + } } -} \ No newline at end of file +} diff --git a/FluentAutoClicker/NativeMethods.txt b/FluentAutoClicker/NativeMethods.txt new file mode 100644 index 0000000..d0c05aa --- /dev/null +++ b/FluentAutoClicker/NativeMethods.txt @@ -0,0 +1,4 @@ +RegisterHotKey +UnregisterHotKey +CallWindowProc +SendInput \ No newline at end of file diff --git a/FluentAutoClicker/Program.cs b/FluentAutoClicker/Program.cs index b561ef7..b830b22 100644 --- a/FluentAutoClicker/Program.cs +++ b/FluentAutoClicker/Program.cs @@ -26,10 +26,10 @@ namespace FluentAutoClicker; /// /// Customized Program.cs file to implement single-instancing in a WinUI app with C#. Single-instanced apps only allow one instance of the app running at a time. /// -public class Program +public partial class Program { [STAThread] - private static int Main(string[] args) + private static int Main() { WinRT.ComWrappersSupport.InitializeComWrappers(); bool isRedirect = DecideRedirection(); @@ -68,21 +68,23 @@ private static bool DecideRedirection() return isRedirect; } - [DllImport("kernel32.dll", CharSet = CharSet.Unicode)] - private static extern IntPtr CreateEvent( - IntPtr lpEventAttributes, bool bManualReset, - bool bInitialState, string? lpName); + [LibraryImport("kernel32.dll", EntryPoint = "CreateEventW", StringMarshalling = StringMarshalling.Utf16)] + private static partial IntPtr CreateEvent( + IntPtr lpEventAttributes, [MarshalAs(UnmanagedType.Bool)] bool bManualReset, + [MarshalAs(UnmanagedType.Bool)] bool bInitialState, string? lpName); - [DllImport("kernel32.dll")] - private static extern bool SetEvent(IntPtr hEvent); + [LibraryImport("kernel32.dll")] + [return: MarshalAs(UnmanagedType.Bool)] + private static partial bool SetEvent(IntPtr hEvent); - [DllImport("ole32.dll")] - private static extern uint CoWaitForMultipleObjects( + [LibraryImport("ole32.dll")] + private static partial uint CoWaitForMultipleObjects( uint dwFlags, uint dwMilliseconds, ulong nHandles, - IntPtr[] pHandles, out uint dwIndex); + [In, Out] IntPtr[] pHandles, out uint dwIndex); - [DllImport("user32.dll")] - private static extern bool SetForegroundWindow(IntPtr hWnd); + [LibraryImport("user32.dll")] + [return: MarshalAs(UnmanagedType.Bool)] + private static partial bool SetForegroundWindow(IntPtr hWnd); private static IntPtr _redirectEventHandle = IntPtr.Zero;