Skip to content

Commit

Permalink
Reworked IClipboardService to use native methods instead of shadow wi…
Browse files Browse the repository at this point in the history
…nforms window to track clipboard changes
  • Loading branch information
Filipsi committed Nov 17, 2019
1 parent 54f07af commit 1c93b40
Show file tree
Hide file tree
Showing 10 changed files with 160 additions and 115 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,6 @@
</Reference>
<Reference Include="System.Web" />
<Reference Include="System.Windows" />
<Reference Include="System.Windows.Forms" />
<Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" />
<Reference Include="Microsoft.CSharp" />
Expand All @@ -126,9 +125,6 @@
<Compile Include="Services\Clipboard\ClipboardEventArgs.cs" />
<Compile Include="Services\Clipboard\ClipboardService.cs" />
<Compile Include="Services\Clipboard\IClipboardService.cs" />
<Compile Include="Services\Clipboard\NotificationForm.cs">
<SubType>Form</SubType>
</Compile>
<Compile Include="Services\Clipboard\WindowHelper.cs" />
<Compile Include="Services\HotKeys\HotKey.cs" />
<Compile Include="Services\HotKeys\HotKeyService.cs" />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,22 @@
using System;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Interop;
using System.Windows.Media.Imaging;
using Castle.Core.Logging;
using Forms = System.Windows.Forms;
using WinClipboard = System.Windows.Clipboard;

namespace ClipboardMachinery.Core.Services.Clipboard {

public class ClipboardService : IClipboardService {

#region MyRegion
#region Properties

public ILogger Logger { get; set; } = NullLogger.Instance;

public bool IsRunning { get; private set; }

#endregion

#region Events
Expand All @@ -23,72 +27,145 @@ public class ClipboardService : IClipboardService {

#region Fields

private static readonly NotificationForm notificationHandler = new NotificationForm();
private const string pngImageHeader = "data:image/png;base64,";

private string ignoreValue;
private string lastSetContent;
private HwndSource hwndSource;

#endregion

public ClipboardService() {
notificationHandler.ClipboardChanged += OnClipboardChanged;
}

#region IClipboardService
#region Logic

private void OnClipboardChanged(object sender, EventArgs e) {
string content = string.Empty;
public void Start(IntPtr handle) {
// Service is already running, ignore the call
if (IsRunning) {
return;
}

if (Forms.Clipboard.ContainsText()) {
content = Forms.Clipboard.GetText();
// Create HWND source from provided pointer
hwndSource = HwndSource.FromHwnd(handle);

} else if (Forms.Clipboard.ContainsImage()) {
using (Image image = Forms.Clipboard.GetImage()) {
if (image != null) {
using (MemoryStream rawImage = new MemoryStream()) {
image.Save(rawImage, ImageFormat.Png);
content = $"{pngImageHeader}{Convert.ToBase64String(rawImage.ToArray())}";
}
}
}
// Check if the pointer was a valid handle and create the HWND source
if (hwndSource == null) {
Logger.Error("Unable to create HwndSource from supplied pointer handle.");
return;
}

if (ignoreValue == content) {
Logger.Info($"Starting clipboard service, adding hook to {handle}.");

// Initialize the service and start listening for clipboard changes
NativeMethods.AddClipboardFormatListener(handle);
hwndSource.AddHook(WndProc);
IsRunning = true;
}

public void Stop() {
// Service is not running, ignore the call
if (!IsRunning) {
return;
}

ClipboardChanged?.Invoke(
sender: this,
e: new ClipboardEventArgs(
source: WindowHelper.GetActiveProcessName(),
payload: content
)
);
}
Logger.Info($"Stopping clipboard service, removing hook form {hwndSource.Handle}.");

public void IgnoreNextChange(string value) {
ignoreValue = value;
// Unhook the handle and remove the clipboard change listener
hwndSource.RemoveHook(WndProc);
NativeMethods.RemoveClipboardFormatListener(hwndSource.Handle);
IsRunning = false;
}

public void SetClipboardContent(string content) {
if (string.IsNullOrEmpty(content)) {
return;
}

if (content.StartsWith(pngImageHeader)) {
try {
byte[] rawImage = Convert.FromBase64String(content.Remove(0, pngImageHeader.Length));
using (MemoryStream imageStream = new MemoryStream(rawImage, 0, rawImage.Length)) {
Forms.Clipboard.SetImage(Image.FromStream(imageStream));
BitmapImage bitmap = new BitmapImage();
bitmap.BeginInit();
bitmap.StreamSource = imageStream;
bitmap.CacheOption = BitmapCacheOption.OnLoad;
bitmap.EndInit();
bitmap.Freeze();
WinClipboard.SetImage(bitmap);
}
} catch (FormatException ex) {
Logger.Error("Error while trying to set clipboard content!", ex);
}
catch (Exception ex) {
Logger.Error("Error while trying to set clipboard image content!", ex);
return;
}
}
else {
WinClipboard.SetText(content);
}

return;
lastSetContent = content;
}

private string GetClipboardContent() {
if (WinClipboard.ContainsText()) {
return WinClipboard.GetText();
}

Forms.Clipboard.SetText(content);
// ReSharper disable once InvertIf
if (WinClipboard.ContainsImage()) {
BitmapSource bitmap = WinClipboard.GetImage();

if (bitmap != null) {
using (MemoryStream rawImage = new MemoryStream()) {
BitmapEncoder encoder = new PngBitmapEncoder();
encoder.Frames.Add(BitmapFrame.Create(bitmap));
encoder.Save(rawImage);
return $"{pngImageHeader}{Convert.ToBase64String(rawImage.ToArray())}";
}
}

Logger.Error("Unable to create bitmap from image copied into the clipboard!");
}

return string.Empty;
}

#endregion

#region Handlers

private IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled) {
switch (msg) {
case NativeMethods.WM_CLIPBOARDUPDATE:
string content = GetClipboardContent();
if (content != lastSetContent) {
ClipboardChanged?.Invoke(this, new ClipboardEventArgs(WindowHelper.GetActiveProcessName(), content));
}
break;
}

return IntPtr.Zero;
}

#endregion

private static class NativeMethods {

// http://msdn.microsoft.com/en-us/library/ms649021%28v=vs.85%29.aspx
public const int WM_CLIPBOARDUPDATE = 0x031D;

// http://msdn.microsoft.com/en-us/library/ms632599%28VS.85%29.aspx#message_only
[DllImport("user32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool AddClipboardFormatListener(IntPtr hwnd);

// https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-removeclipboardformatlistener
[DllImport("user32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool RemoveClipboardFormatListener(IntPtr hwnd);

}

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,13 @@ namespace ClipboardMachinery.Core.Services.Clipboard {

public interface IClipboardService {

bool IsRunning { get; }

event EventHandler<ClipboardEventArgs> ClipboardChanged;

void IgnoreNextChange(string value);
void Start(IntPtr handle);

void Stop();

void SetClipboardContent(string content);

Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Windows.Input;
using System.Windows.Interop;
Expand All @@ -12,7 +11,10 @@ public class HotKey : IDisposable {

#region Properties

public ILogger Logger { get; set; } = NullLogger.Instance;
public ILogger Logger {
get;
set;
}

public Key Key {
get;
Expand Down Expand Up @@ -40,8 +42,9 @@ public int Id {

#endregion

internal HotKey(Key k, KeyModifier keyModifiers, Action<HotKey> action, bool register = true) {
Key = k;
internal HotKey(Key key, KeyModifier keyModifiers, Action<HotKey> action, bool register = true) {
Logger = NullLogger.Instance;
Key = key;
KeyModifiers = keyModifiers;
Action = action;

Expand Down Expand Up @@ -105,9 +108,7 @@ protected virtual void Dispose(bool disposing) {

#endregion

#region Native

internal static class NativeMethods {
private static class NativeMethods {
// Refer to https://msdn.microsoft.com/en-us/library/windows/desktop/ms646279(v=vs.85).aspx
public const int WmHotKey = 0x0312;

Expand All @@ -120,8 +121,6 @@ internal static class NativeMethods {
public static extern bool UnregisterHotKey(IntPtr hWnd, int id);
}

#endregion

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

namespace ClipboardMachinery.Core.Services.HotKeys {

public class HotKeyService : IHotKeyService, IDisposable {
public class HotKeyService : IHotKeyService {

#region Properties

Expand Down Expand Up @@ -41,7 +41,7 @@ public HotKey Register(Key key, KeyModifier keyModifiers, Action<HotKey> action)

#endregion

#region MyRegion
#region IDisposable

private bool disposed;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
using Castle.MicroKernel.Registration;
using Castle.Facilities.Startable;
using Castle.MicroKernel.Registration;
using Castle.MicroKernel.SubSystems.Configuration;
using Castle.Windsor;
using ClipboardMachinery.Core.DataStorage;
using ClipboardMachinery.Core.DataStorage.Impl;
using ClipboardMachinery.Core.Services.Clipboard;
using ClipboardMachinery.Core.Services.HotKeys;

namespace ClipboardMachinery.Plumbing.Installers {

Expand All @@ -28,12 +31,18 @@ public void Install(IWindsorContainer container, IConfigurationStore store) {
);

container.Register(
Classes
.FromAssemblyNamed("ClipboardMachinery.Core")
.InNamespace("ClipboardMachinery.Core.Services", includeSubnamespaces: true)
.If(type => type.Name.EndsWith("Service"))
.WithServiceDefaultInterfaces()
.LifestyleSingleton()
Component
.For<IClipboardService>()
.ImplementedBy<ClipboardService>()
.StopUsingMethod(c => c.Stop)
.LifeStyle.Singleton
);

container.Register(
Component
.For<IHotKeyService>()
.ImplementedBy<HotKeyService>()
.LifeStyle.Singleton
);
}

Expand Down
Loading

0 comments on commit 1c93b40

Please sign in to comment.