Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions FluentAutoClicker/App.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,10 @@ public App()
/// <param name="args">Details about the launch request and process.</param>
protected override void OnLaunched(Microsoft.UI.Xaml.LaunchActivatedEventArgs args)
{
m_window = new MainWindow();
m_window.Activate();
Window = new MainWindow();
Window.Activate();
}

private Window m_window;
public static Window Window { get; private set; }
}
}
}
102 changes: 102 additions & 0 deletions FluentAutoClicker/Helpers/WindowMessageHook.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.ComponentModel;
using System.Runtime.InteropServices;
using System.Threading;
using Microsoft.UI;
using Microsoft.UI.Xaml;

namespace FluentAutoClicker.Helpers;

public class WindowMessageHook : IEquatable<WindowMessageHook>, IDisposable
{
private delegate nint SUBCLASSPROC(nint hWnd, uint uMsg, nint wParam, nint lParam, nint uIdSubclass, uint dwRefData);

private static readonly ConcurrentDictionary<nint, WindowMessageHook> _hooks = new();
private static readonly SUBCLASSPROC _proc = SubclassProc;

public event EventHandler<MessageEventArgs> 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;
var 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 var hook))
{
var e = new MessageEventArgs(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() => _hWnd.GetHashCode();
public override string ToString() => _hWnd.ToString();
public override bool Equals(object obj) => Equals(obj as WindowMessageHook);
public virtual bool Equals(WindowMessageHook other) => 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; }
}
24 changes: 15 additions & 9 deletions FluentAutoClicker/MainPage.xaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8" ?>

<Page
x:Class="FluentAutoClicker.MainPage"
Expand Down Expand Up @@ -76,26 +76,32 @@
Value="100" />
</Grid>
</StackPanel>
<StackPanel Grid.Row="1" Grid.Column="0" Spacing="{StaticResource StackPanelRowSpacing}">
<StackPanel
Grid.Row="1"
Grid.Column="0"
Spacing="{StaticResource StackPanelRowSpacing}">
<ComboBox
x:Name="MouseButtonTypeComboBox"
x:Uid="ComboBoxMouseButton"
SelectionChanged="MouseButtonType_OnSelectionChanged"
SelectedIndex="0">
SelectedIndex="0"
SelectionChanged="MouseButtonType_OnSelectionChanged">
<ComboBoxItem x:Uid="ComboBoxItemLeft" />
<ComboBoxItem x:Uid="ComboBoxItemMiddle" />
<ComboBoxItem x:Uid="ComboBoxItemRight" />
</ComboBox>
<TextBlock x:Uid="TextBlockHotkey" />
<Button Click="HotkeyButton_OnClick" Content="F6" />
<Button
Click="HotkeyButton_OnClick"
Content="F6"
IsEnabled="False" />
</StackPanel>
<RadioButtons
x:Name="ClickRepeatType"
x:Uid="RadioButtonsClickRepeat"
Grid.Row="1"
Grid.Column="1"
SelectionChanged="ClickRepeatType_OnSelectionChanged"
SelectedIndex="1">
SelectedIndex="1"
SelectionChanged="ClickRepeatType_OnSelectionChanged">
<NumberBox
x:Name="ClickRepeatAmount"
AcceptsExpression="True"
Expand All @@ -116,8 +122,8 @@
Grid.Column="0"
Grid.ColumnSpan="2"
HorizontalAlignment="Stretch"
VerticalAlignment="Bottom"
Checked="StartToggleButton_OnChecked"
Unchecked="StartToggleButton_OnUnchecked"
VerticalAlignment="Bottom" />
Unchecked="StartToggleButton_OnUnchecked" />
</Grid>
</Page>
73 changes: 58 additions & 15 deletions FluentAutoClicker/MainPage.xaml.cs
Original file line number Diff line number Diff line change
@@ -1,24 +1,48 @@
// To learn more about WinUI, the WinUI project structure,
// and more about our project templates, see: http://aka.ms/winui-project-info.

using System;
using System.Globalization;
using System.Threading.Tasks;
using System.ComponentModel;
using System.Diagnostics;
using System.Runtime.InteropServices;
using FluentAutoClicker.Helpers;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Windows.System;
using System.Globalization;
using System.Threading.Tasks;
using Microsoft.UI.Xaml.Automation.Peers;

namespace FluentAutoClicker;

/// <summary>
/// An empty page that can be used on its own or navigated to within a Frame.
/// </summary>
public sealed partial class MainPage
namespace FluentAutoClicker
{
public MainPage()
public sealed partial class MainPage : Page
{
InitializeComponent();
}
public MainPage()
{
InitializeComponent();
Loaded += MainPage_Loaded;
}

private void MainPage_Loaded(object sender, RoutedEventArgs e)
{
var hook = new WindowMessageHook(App.Window);
Unloaded += (s, e) => hook.Dispose(); // unhook on close
hook.Message += (s, e) =>
{
const int WM_HOTKEY = 0x312;
if (e.Message == WM_HOTKEY)
{
// click on the button using UI Automation
var pattern = (ToggleButtonAutomationPeer)FrameworkElementAutomationPeer.FromElement(StartToggleButton).GetPattern(PatternInterface.Toggle);
pattern.Toggle();
}
};

// register CTRL + B as a global hotkey
var hwnd = WinRT.Interop.WindowNative.GetWindowHandle(App.Window);
var id = 1; // some arbitrary hotkey identifier
if (!RegisterHotKey(hwnd, id, MOD.MOD_NOREPEAT, VirtualKey.F6))
throw new Win32Exception(Marshal.GetLastWin32Error());

Unloaded += (s, e) => UnregisterHotKey(hwnd, id); // unregister hotkey on window close
}

private void SetClicker_Interval()
{
Expand Down Expand Up @@ -121,4 +145,23 @@ private void StartToggleButton_OnUnchecked(object sender, RoutedEventArgs e)
StartToggleButton.Content = "Start";
AutoClicker.StopAutoClicker();
}
}


// 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
{
MOD_ALT = 0x1,
MOD_CONTROL = 0x2,
MOD_SHIFT = 0x4,
MOD_WIN = 0x8,
MOD_NOREPEAT = 0x4000,
}
}
}