From 1c93b4007effa05d143ea4775016281ed8f91d95 Mon Sep 17 00:00:00 2001 From: Filipsi Date: Sun, 17 Nov 2019 11:48:10 +0100 Subject: [PATCH] Reworked IClipboardService to use native methods instead of shadow winforms window to track clipboard changes --- .../ClipboardMachinery.Core.csproj | 4 - .../Services/Clipboard/ClipboardService.cs | 151 +++++++++++++----- .../Services/Clipboard/IClipboardService.cs | 6 +- .../Services/Clipboard/NotificationForm.cs | 52 ------ .../Services/HotKeys/HotKey.cs | 17 +- .../Services/HotKeys/HotKeyService.cs | 4 +- .../Plumbing/Installers/ServiceInstaller.cs | 23 ++- .../Windows/Shell/ShellView.xaml | 4 +- .../Windows/Shell/ShellView.xaml.cs | 10 ++ .../Windows/Shell/ShellViewModel.cs | 4 +- 10 files changed, 160 insertions(+), 115 deletions(-) delete mode 100644 ClipboardMachinery/ClipboardMachinery.Core/Services/Clipboard/NotificationForm.cs diff --git a/ClipboardMachinery/ClipboardMachinery.Core/ClipboardMachinery.Core.csproj b/ClipboardMachinery/ClipboardMachinery.Core/ClipboardMachinery.Core.csproj index 0dbe143..ee2b338 100644 --- a/ClipboardMachinery/ClipboardMachinery.Core/ClipboardMachinery.Core.csproj +++ b/ClipboardMachinery/ClipboardMachinery.Core/ClipboardMachinery.Core.csproj @@ -100,7 +100,6 @@ - @@ -126,9 +125,6 @@ - - Form - diff --git a/ClipboardMachinery/ClipboardMachinery.Core/Services/Clipboard/ClipboardService.cs b/ClipboardMachinery/ClipboardMachinery.Core/Services/Clipboard/ClipboardService.cs index ef4394e..58dd67e 100644 --- a/ClipboardMachinery/ClipboardMachinery.Core/Services/Clipboard/ClipboardService.cs +++ b/ClipboardMachinery/ClipboardMachinery.Core/Services/Clipboard/ClipboardService.cs @@ -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 @@ -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); + + } + } } diff --git a/ClipboardMachinery/ClipboardMachinery.Core/Services/Clipboard/IClipboardService.cs b/ClipboardMachinery/ClipboardMachinery.Core/Services/Clipboard/IClipboardService.cs index 3e48117..ad916d8 100644 --- a/ClipboardMachinery/ClipboardMachinery.Core/Services/Clipboard/IClipboardService.cs +++ b/ClipboardMachinery/ClipboardMachinery.Core/Services/Clipboard/IClipboardService.cs @@ -4,9 +4,13 @@ namespace ClipboardMachinery.Core.Services.Clipboard { public interface IClipboardService { + bool IsRunning { get; } + event EventHandler ClipboardChanged; - void IgnoreNextChange(string value); + void Start(IntPtr handle); + + void Stop(); void SetClipboardContent(string content); diff --git a/ClipboardMachinery/ClipboardMachinery.Core/Services/Clipboard/NotificationForm.cs b/ClipboardMachinery/ClipboardMachinery.Core/Services/Clipboard/NotificationForm.cs deleted file mode 100644 index 3a640ae..0000000 --- a/ClipboardMachinery/ClipboardMachinery.Core/Services/Clipboard/NotificationForm.cs +++ /dev/null @@ -1,52 +0,0 @@ -using System; -using System.Runtime.InteropServices; -using System.Windows.Forms; - -namespace ClipboardMachinery.Core.Services.Clipboard { - - internal class NotificationForm : Form { - - #region Events - - public event EventHandler ClipboardChanged; - - #endregion - - public NotificationForm() { - NativeMethods.SetParent(Handle, NativeMethods.HwndMessage); - NativeMethods.AddClipboardFormatListener(Handle); - } - - #region Handlers - - protected override void WndProc(ref Message m) { - if (m.Msg == NativeMethods.WmClipboardupdate) { - ClipboardChanged?.Invoke(this, EventArgs.Empty); - } - - base.WndProc(ref m); - } - - #endregion - - internal static class NativeMethods { - - // See http://msdn.microsoft.com/en-us/library/ms649021%28v=vs.85%29.aspx - public const int WmClipboardupdate = 0x031D; - public static IntPtr HwndMessage = new IntPtr(-3); - - // See 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); - - // See http://msdn.microsoft.com/en-us/library/ms633541%28v=vs.85%29.aspx - // See http://msdn.microsoft.com/en-us/library/ms649033%28VS.85%29.aspx - [DllImport("user32.dll", SetLastError = true)] - public static extern IntPtr SetParent(IntPtr hWndChild, IntPtr hWndNewParent); - - } - - } - -} diff --git a/ClipboardMachinery/ClipboardMachinery.Core/Services/HotKeys/HotKey.cs b/ClipboardMachinery/ClipboardMachinery.Core/Services/HotKeys/HotKey.cs index f78b2ea..1336b8c 100644 --- a/ClipboardMachinery/ClipboardMachinery.Core/Services/HotKeys/HotKey.cs +++ b/ClipboardMachinery/ClipboardMachinery.Core/Services/HotKeys/HotKey.cs @@ -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; @@ -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; @@ -40,8 +42,9 @@ public int Id { #endregion - internal HotKey(Key k, KeyModifier keyModifiers, Action action, bool register = true) { - Key = k; + internal HotKey(Key key, KeyModifier keyModifiers, Action action, bool register = true) { + Logger = NullLogger.Instance; + Key = key; KeyModifiers = keyModifiers; Action = action; @@ -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; @@ -120,8 +121,6 @@ internal static class NativeMethods { public static extern bool UnregisterHotKey(IntPtr hWnd, int id); } - #endregion - } } diff --git a/ClipboardMachinery/ClipboardMachinery.Core/Services/HotKeys/HotKeyService.cs b/ClipboardMachinery/ClipboardMachinery.Core/Services/HotKeys/HotKeyService.cs index 0a940fa..4f5e6fd 100644 --- a/ClipboardMachinery/ClipboardMachinery.Core/Services/HotKeys/HotKeyService.cs +++ b/ClipboardMachinery/ClipboardMachinery.Core/Services/HotKeys/HotKeyService.cs @@ -4,7 +4,7 @@ namespace ClipboardMachinery.Core.Services.HotKeys { - public class HotKeyService : IHotKeyService, IDisposable { + public class HotKeyService : IHotKeyService { #region Properties @@ -41,7 +41,7 @@ public HotKey Register(Key key, KeyModifier keyModifiers, Action action) #endregion - #region MyRegion + #region IDisposable private bool disposed; diff --git a/ClipboardMachinery/ClipboardMachinery/Plumbing/Installers/ServiceInstaller.cs b/ClipboardMachinery/ClipboardMachinery/Plumbing/Installers/ServiceInstaller.cs index 8daacf0..0759239 100644 --- a/ClipboardMachinery/ClipboardMachinery/Plumbing/Installers/ServiceInstaller.cs +++ b/ClipboardMachinery/ClipboardMachinery/Plumbing/Installers/ServiceInstaller.cs @@ -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 { @@ -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() + .ImplementedBy() + .StopUsingMethod(c => c.Stop) + .LifeStyle.Singleton + ); + + container.Register( + Component + .For() + .ImplementedBy() + .LifeStyle.Singleton ); } diff --git a/ClipboardMachinery/ClipboardMachinery/Windows/Shell/ShellView.xaml b/ClipboardMachinery/ClipboardMachinery/Windows/Shell/ShellView.xaml index 8558035..5189406 100644 --- a/ClipboardMachinery/ClipboardMachinery/Windows/Shell/ShellView.xaml +++ b/ClipboardMachinery/ClipboardMachinery/Windows/Shell/ShellView.xaml @@ -6,6 +6,7 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:helpers="clr-namespace:ClipboardMachinery.Common.Helpers" + xmlns:cal="http://www.caliburnproject.org" mc:Ignorable="d" WindowStartupLocation="CenterScreen" SnapsToDevicePixels="True" @@ -17,7 +18,8 @@ ShowInTaskbar="{Binding IsVisibleInTaskbar}" Topmost="{Binding IsTopmost}" SizeToContent="Width" - ResizeMode="NoResize"> + ResizeMode="NoResize" + Loaded="OnLoaded"> ().Start(new WindowInteropHelper(this).Handle); + } + } } diff --git a/ClipboardMachinery/ClipboardMachinery/Windows/Shell/ShellViewModel.cs b/ClipboardMachinery/ClipboardMachinery/Windows/Shell/ShellViewModel.cs index 24c37c8..e620631 100644 --- a/ClipboardMachinery/ClipboardMachinery/Windows/Shell/ShellViewModel.cs +++ b/ClipboardMachinery/ClipboardMachinery/Windows/Shell/ShellViewModel.cs @@ -6,6 +6,7 @@ using System.Threading.Tasks; using System.Windows; using System.Windows.Input; +using System.Windows.Interop; using Caliburn.Micro; using ClipboardMachinery.Common.Events; using ClipboardMachinery.Components.Clip; @@ -110,7 +111,6 @@ public ShellViewModel( Navigator.PropertyChanged += OnNavigatorPropertyChanged; } - #region Handlers protected override Task OnDeactivateAsync(bool close, CancellationToken cancellationToken) { @@ -126,7 +126,7 @@ public Task HandleAsync(ClipEvent message, CancellationToken cancellationToken) return Task.CompletedTask; } - clipboardService.IgnoreNextChange(message.Source.Content); + // TODO: clipboardService.IgnoreNextChange(message.Source.Content); clipboardService.SetClipboardContent(message.Source.Content); IsVisible = false; return Task.CompletedTask;