Skip to content
This repository was archived by the owner on Apr 21, 2021. It is now read-only.

Commit 1d27abc

Browse files
Merge pull request #16 from losttech/master
Added WindowHookEx, that provides Activated, (Un)Minimized, and TextChanged
2 parents df3ff28 + 4e4083b commit 1d27abc

File tree

3 files changed

+197
-5
lines changed

3 files changed

+197
-5
lines changed

.nuget/NuGet.targets

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,12 @@
2525
-->
2626
</ItemGroup>
2727

28-
<PropertyGroup Condition=" '$(OS)' == 'Windows_NT'">
28+
<PropertyGroup Condition=" '$(OS)' == 'Windows_NT' AND '$(NuGetToolsPath)' == '' ">
2929
<!-- Windows specific commands -->
3030
<NuGetToolsPath>$([System.IO.Path]::Combine($(SolutionDir), ".nuget"))</NuGetToolsPath>
3131
</PropertyGroup>
3232

33-
<PropertyGroup Condition=" '$(OS)' != 'Windows_NT'">
33+
<PropertyGroup Condition=" '$(OS)' != 'Windows_NT' AND '$(NuGetToolsPath)' == '' ">
3434
<!-- We need to launch nuget.exe with the mono command if we're not on windows -->
3535
<NuGetToolsPath>$(SolutionDir).nuget</NuGetToolsPath>
3636
</PropertyGroup>
@@ -61,8 +61,10 @@
6161
<PaddedSolutionDir Condition=" '$(OS)' == 'Windows_NT'">"$(SolutionDir) "</PaddedSolutionDir>
6262
<PaddedSolutionDir Condition=" '$(OS)' != 'Windows_NT' ">"$(SolutionDir)"</PaddedSolutionDir>
6363

64+
<PackageInstallDir Condition=" '$(PackageInstallDir)' == '' ">$(PaddedSolutionDir)</PackageInstallDir>
65+
6466
<!-- Commands -->
65-
<RestoreCommand>$(NuGetCommand) install "$(PackagesConfig)" -source "$(PackageSources)" $(NonInteractiveSwitch) $(RequireConsentSwitch) -solutionDir $(PaddedSolutionDir)</RestoreCommand>
67+
<RestoreCommand>$(NuGetCommand) install "$(PackagesConfig)" -source "$(PackageSources)" $(NonInteractiveSwitch) $(RequireConsentSwitch) -solutionDir $(PackageInstallDir)</RestoreCommand>
6668
<BuildCommand>$(NuGetCommand) pack "$(ProjectPath)" -Properties "Configuration=$(Configuration);Platform=$(Platform)" $(NonInteractiveSwitch) -OutputDirectory "$(PackageOutputDir)" -symbols</BuildCommand>
6769

6870
<!-- We need to ensure packages are restored prior to assembly resolve -->

EventHook/EventHook.csproj

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
<SolutionDir Condition="$(SolutionDir) == '' Or $(SolutionDir) == '*Undefined*'">..\</SolutionDir>
1515
<RestorePackages>true</RestorePackages>
1616
<TargetFrameworkProfile />
17+
<NuGetToolsPath>..\.nuget</NuGetToolsPath>
18+
<PackageInstallDir>..</PackageInstallDir>
1719
</PropertyGroup>
1820
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
1921
<DebugSymbols>true</DebugSymbols>
@@ -67,6 +69,7 @@
6769
<Compile Include="EventHookFactory.cs" />
6870
<Compile Include="Helpers\AsyncConcurrentQueue.cs" />
6971
<Compile Include="Helpers\SyncFactory.cs" />
72+
<Compile Include="Hooks\WindowHookEx.cs" />
7073
<Compile Include="KeyboardWatcher.cs" />
7174
<Compile Include="MouseWatcher.cs" />
7275
<Compile Include="PrintWatcher.cs" />
@@ -91,12 +94,12 @@
9194
<None Include="app.config" />
9295
</ItemGroup>
9396
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
94-
<Import Project="$(SolutionDir)\.nuget\NuGet.targets" Condition="Exists('$(SolutionDir)\.nuget\NuGet.targets')" />
97+
<Import Project="..\.nuget\NuGet.targets" Condition="Exists('..\.nuget\NuGet.targets')" />
9598
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
9699
<PropertyGroup>
97100
<ErrorText>This project references NuGet package(s) that are missing on this computer. Enable NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.</ErrorText>
98101
</PropertyGroup>
99-
<Error Condition="!Exists('$(SolutionDir)\.nuget\NuGet.targets')" Text="$([System.String]::Format('$(ErrorText)', '$(SolutionDir)\.nuget\NuGet.targets'))" />
102+
<Error Condition="!Exists('..\.nuget\NuGet.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\.nuget\NuGet.targets'))" />
100103
</Target>
101104
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
102105
Other similar extension points exist, see Microsoft.Common.targets.

EventHook/Hooks/WindowHookEx.cs

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
namespace EventHook.Hooks
2+
{
3+
using System;
4+
using System.Collections.Generic;
5+
using System.ComponentModel;
6+
using System.Diagnostics;
7+
using System.Runtime.InteropServices;
8+
9+
/// <summary>
10+
/// Track events across all windows
11+
/// </summary>
12+
public sealed class WindowHookEx: IDisposable
13+
{
14+
readonly WinEventProc proc;
15+
readonly SortedDictionary<WindowEvent, IntPtr> hooks = new SortedDictionary<WindowEvent, IntPtr>();
16+
17+
/// <summary>
18+
/// Must be called from UI thread
19+
/// </summary>
20+
public WindowHookEx() {
21+
this.proc = this.Hook;
22+
}
23+
24+
EventHandler<WindowEventArgs> activated;
25+
/// <summary>
26+
/// Occurs when a window is about to be activated
27+
/// </summary>
28+
public event EventHandler<WindowEventArgs> Activated {
29+
add => this.EventAdd(ref this.activated, value, WindowEvent.ForegroundChanged);
30+
remove => this.EventRemove(ref this.activated, value, WindowEvent.ForegroundChanged);
31+
}
32+
33+
EventHandler<WindowEventArgs> minimized;
34+
/// <summary>
35+
/// Occurs when a window is about to be minimized
36+
/// </summary>
37+
public event EventHandler<WindowEventArgs> Minimized {
38+
add => this.EventAdd(ref this.minimized, value, WindowEvent.Minimized);
39+
remove => this.EventRemove(ref this.minimized, value, WindowEvent.Minimized);
40+
}
41+
42+
EventHandler<WindowEventArgs> unminimized;
43+
/// <summary>
44+
/// Occurs when a window is about to be restored from minimized state
45+
/// </summary>
46+
public event EventHandler<WindowEventArgs> Unminimized {
47+
add => this.EventAdd(ref this.unminimized, value, WindowEvent.Unmiminized);
48+
remove => this.EventRemove(ref this.unminimized, value, WindowEvent.Unmiminized);
49+
}
50+
51+
EventHandler<WindowEventArgs> textChanged;
52+
/// <summary>
53+
/// Occurs when window's text is changed
54+
/// </summary>
55+
public event EventHandler<WindowEventArgs> TextChanged {
56+
add => this.EventAdd(ref this.textChanged, value, WindowEvent.NameChanged);
57+
remove => this.EventRemove(ref this.textChanged, value, WindowEvent.NameChanged);
58+
}
59+
60+
void EventAdd(ref EventHandler<WindowEventArgs> handler, EventHandler<WindowEventArgs> user,
61+
WindowEvent @event) {
62+
lock (this.hooks) {
63+
handler += user;
64+
this.EnsureHook(@event);
65+
}
66+
}
67+
68+
void EventRemove(ref EventHandler<WindowEventArgs> handler, EventHandler<WindowEventArgs> user,
69+
WindowEvent @event) {
70+
lock (this.hooks) {
71+
if (handler != null && handler - user == null) {
72+
if (!UnhookWinEvent(this.hooks[@event]))
73+
throw new Win32Exception();
74+
bool existed = this.hooks.Remove(@event);
75+
Debug.Assert(existed);
76+
}
77+
78+
handler -= user;
79+
}
80+
}
81+
82+
void Hook(IntPtr hookHandle, WindowEvent @event,
83+
IntPtr hwnd,
84+
int @object, int child,
85+
int threadID, int timestampMs) {
86+
EventHandler<WindowEventArgs> handler;
87+
switch (@event) {
88+
case WindowEvent.ForegroundChanged: handler = this.activated; break;
89+
case WindowEvent.NameChanged: handler = this.textChanged; break;
90+
case WindowEvent.Minimized: handler = this.minimized; break;
91+
case WindowEvent.Unmiminized: handler = this.unminimized; break;
92+
default: Debug.Write($"Unexpected event {@event}"); return;
93+
}
94+
handler?.Invoke(this, new WindowEventArgs(hwnd));
95+
}
96+
97+
void EnsureHook(WindowEvent @event) {
98+
if (!this.hooks.TryGetValue(@event, out var hookID)) {
99+
hookID = this.SetHook(@event);
100+
this.hooks.Add(@event, hookID);
101+
}
102+
}
103+
104+
IntPtr SetHook(WindowEvent @event) {
105+
IntPtr hookID = SetWinEventHook(
106+
hookMin: @event, hookMax: @event,
107+
moduleHandle: IntPtr.Zero, callback: this.proc,
108+
processID: 0, threadID: 0,
109+
flags: HookFlags.OutOfContext);
110+
if (hookID == IntPtr.Zero)
111+
throw new Win32Exception();
112+
return hookID;
113+
}
114+
115+
// ReSharper disable once ParameterOnlyUsedForPreconditionCheck.Local
116+
void ReleaseUnmanagedResources(bool disposing) {
117+
lock (this.hooks) {
118+
Debug.Assert(!Environment.HasShutdownStarted && (this.hooks.Count == 0 || disposing), "Somebody forgot to dispose the hook. "
119+
+ "That will cause heap corruption, because the hook handler "
120+
+ "will be disposed before hooking is disabled by the code below.");
121+
foreach (IntPtr hook in this.hooks.Values) {
122+
if (!UnhookWinEvent(hook)) {
123+
var error = new Win32Exception();
124+
// handle is invalid
125+
if (error.NativeErrorCode != 0x6)
126+
throw error;
127+
else
128+
Debug.WriteLine("Can't dispose hook: the handle is invalid");
129+
}
130+
}
131+
132+
this.hooks.Clear();
133+
}
134+
135+
GC.KeepAlive(this.proc);
136+
}
137+
138+
/// <inheritdoc />
139+
public void Dispose() {
140+
this.ReleaseUnmanagedResources(disposing: true);
141+
this.activated = this.minimized = this.unminimized = this.textChanged = null;
142+
GC.SuppressFinalize(this);
143+
}
144+
145+
~WindowHookEx() {
146+
this.ReleaseUnmanagedResources(disposing: false);
147+
}
148+
149+
#region Native API
150+
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
151+
static extern IntPtr SetWinEventHook(WindowEvent hookMin, WindowEvent hookMax,
152+
IntPtr moduleHandle,
153+
WinEventProc callback, int processID, int threadID, HookFlags flags);
154+
155+
[Flags]
156+
enum HookFlags: int
157+
{
158+
OutOfContext = 0,
159+
}
160+
161+
enum WindowEvent
162+
{
163+
ForegroundChanged = 0x03,
164+
NameChanged = 0x800C,
165+
Minimized = 0x0016,
166+
Unmiminized = 0x0017,
167+
}
168+
169+
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
170+
[return: MarshalAs(UnmanagedType.Bool)]
171+
static extern bool UnhookWinEvent(IntPtr hhk);
172+
173+
delegate void WinEventProc(IntPtr hookHandle, WindowEvent @event,
174+
IntPtr hwnd,
175+
int @object, int child,
176+
int threadID, int timestampMs);
177+
#endregion
178+
}
179+
180+
public class WindowEventArgs
181+
{
182+
public WindowEventArgs(IntPtr handle) {
183+
this.Handle = handle;
184+
}
185+
public IntPtr Handle { get; }
186+
}
187+
}

0 commit comments

Comments
 (0)