Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add mouse click effect capability for screen recording #7622

Closed
wants to merge 10 commits into from
79 changes: 79 additions & 0 deletions ShareX.HelpersLib/Input/MouseClickEffectForm.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
using System;
using System.Drawing;
using System.Windows.Forms;

namespace ShareX.HelpersLib
{
// transparent form where mouse effect will be drawn
public class MouseClickEffectForm : Form
{
private bool _drawEffect = false;
public MouseClickEffectForm()
{
Size = new Size(40, 40);
FormBorderStyle = FormBorderStyle.None;
ShowIcon = false;
ShowInTaskbar = false;
TopMost = true;
AllowTransparency = true;
BackColor = Color.White; // Set a key color for transparency
TransparencyKey = Color.White; // Make that color transparent
}

/// <summary>
/// Draw mouse effect on given mouse position
/// </summary>
public void DrawMouseEffect(Point cursorPosition)
{
_drawEffect = true;
CenterFormToCursorPosition(cursorPosition);
Invalidate(); // redraw form
}

/// <summary>
/// Clear mouse effect
/// </summary>
public void ClearMouseEffect()
{
_drawEffect = false;
Invalidate(); // redraw form
}

/// <summary>
/// Draw a hollow circle as an mouse effect
/// </summary>
protected override void OnPaint(PaintEventArgs e)
{
if (_drawEffect)
{
Graphics g = e.Graphics;
g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;
// Define a pen to draw hollow circle
Pen pen = new Pen(Color.Red, 3);
int diameter = 10;

// Calculate the top-left corner to center the circle
int x = (ClientSize.Width - diameter) / 2;
int y = (ClientSize.Height - diameter) / 2;

// .NET GDI+ is not precise when drawign circles this would be better off with WPF/vector-based drawing
g.DrawEllipse(pen, x, y, diameter, diameter);
Copy link
Author

@Vertygo Vertygo Oct 2, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This might not be best solution due to GDI+ poor precision.
ShareX_7x7a6EO0wE

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't add that much to this, I just wanted to say that rn I'm using this tool https://github.com/Phaiax/Key-n-Stroke to achieve the same and as it is open-source already you guys can probably get inspiration from there and/or took over some of the code to not reinvent the wheel here :) (altho I dunno if the poor precision thing is really better with keynstroke --> at least from a enduser-experience I've never had simliar problems)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point @saintcore. I do get inspired by projects like this. I like keyboard capturing capability it has and I might take a stab at it after I'm done with mouse effect.

I don't know how things work regarding "taking over" some code in the open-source world. I guess some collaboration with the owner (@Phaiax) is needed.

Regarding the small issue I mentioned here, I think that might be solved with help from @Jaex but keynstroke uses WPF so it is slightly different than ShareX which uses Windows Forms.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well according to the provided license, it shouldn't be a problem (see https://github.com/Phaiax/Key-n-Stroke?tab=Apache-2.0-1-ov-file#readme ). A good summary of what apache 2.0 license allows us to do with and not is available over at snyk: https://snyk.io/learn/apache-license/

Under the Apache license, users are permitted to modify, distribute, and sublicense the original open source code. Commercial use, warranties, and patent claims are also allowed with the Apache license.
....
When using the Apache license, developers must include the original copyright notice, a copy of the license text itself, and in some cases, a copy of the notice file with attribution notes and a disclosure of any significant changes made to the original code.
....

With that said, ofc it would be nice to hear from the original author of keynstroke and his desires beside license thoughts :)

}

base.OnPaint(e);
}

private void CenterFormToCursorPosition(Point cursorPosition)
{
var x = cursorPosition.X - (Width / 2);
var y = cursorPosition.Y - (Height / 2);
var flags = SetWindowPosFlags.SWP_NOSIZE |
SetWindowPosFlags.SWP_NOOWNERZORDER |
SetWindowPosFlags.SWP_NOACTIVATE;

// Center the form/circle at the current mouse cursor position
NativeMethods.SetWindowPos(Handle, (IntPtr)NativeConstants.HWND_TOPMOST, x, y, 0, 0, flags);
}

}
}
81 changes: 81 additions & 0 deletions ShareX.HelpersLib/Input/MouseClickEffectManager.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
using System;
using System.Windows.Forms;

namespace ShareX.HelpersLib
{
public class MouseClickEffectManager : IDisposable
{
private MouseHook mouseHook;
private MouseClickEffectForm clickEffectForm;
private bool _disposed;

// start click mouse effects
public void Start()
{
var action = new Action(() =>
{
clickEffectForm = new MouseClickEffectForm();
mouseHook = new MouseHook();
mouseHook.OnMouseEvent += OnMouseEvent;
clickEffectForm.Show();
});

// form needs to be running on main UI thread otherwise it will not show up
if (Application.OpenForms[0].InvokeRequired)
{
Application.OpenForms[0].Invoke(action);
}
else
{
action();
}
}

/// <summary>
/// Mouse event handler
/// </summary>
private void OnMouseEvent(MouseEventInfo eventInfo)
{
switch (eventInfo.ButtonState)
{
case ButtonState.LeftButtonDown:
clickEffectForm.DrawMouseEffect(eventInfo.CursorPosition);
break;
default:
clickEffectForm.ClearMouseEffect();
break;
}
}

public void Stop()
{
Dispose();
}

public void Dispose()
{
Dispose(true);
}

protected virtual void Dispose(bool disposing)
{
if (_disposed)
{
return;
}

if (disposing)
{
mouseHook.OnMouseEvent -= OnMouseEvent;
mouseHook.Dispose();
clickEffectForm.Close();
clickEffectForm.Dispose();
}

mouseHook = null;
clickEffectForm = null;

_disposed = true;
}
}
}
19 changes: 19 additions & 0 deletions ShareX.HelpersLib/Input/MouseEventInfo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@


using System.Drawing;

namespace ShareX.HelpersLib
{
public class MouseEventInfo
{
public ButtonState ButtonState { get; set; }
public Point CursorPosition { get; set; }
}

public enum ButtonState
{
LeftButtonDown,
RightButtonDown,
ButtonUp
}
}
65 changes: 65 additions & 0 deletions ShareX.HelpersLib/Input/MouseHook.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
using System;

namespace ShareX.HelpersLib
{
public class MouseHook : IDisposable
{
private HookProc _proc;
private static IntPtr _hookID = IntPtr.Zero;

public delegate void MouseEventHandler(MouseEventInfo eventInfo);
public event MouseEventHandler OnMouseEvent;

public MouseHook()
{
_proc = HookCallback;
_hookID = SetHook(_proc); // Set up global mouse hook
}

~MouseHook()
{
Dispose();
}

private static IntPtr SetHook(HookProc proc)
{
using (var curProcess = System.Diagnostics.Process.GetCurrentProcess())
using (var curModule = curProcess.MainModule)
{
return NativeMethods.SetWindowsHookEx(NativeConstants.WH_MOUSE_LL, proc, NativeMethods.GetModuleHandle(curModule.ModuleName), 0);
}
}

private IntPtr HookCallback(int nCode, IntPtr wParam, IntPtr lParam)
{
if (nCode >= 0)
{
switch ((WindowsMessages)wParam)
{
case WindowsMessages.LBUTTONDOWN:
OnMouseEvent(new MouseEventInfo
{
ButtonState = ButtonState.LeftButtonDown,
CursorPosition = CaptureHelpers.GetCursorPosition()
});
break;

case WindowsMessages.LBUTTONUP:
OnMouseEvent(new MouseEventInfo
{
ButtonState = ButtonState.ButtonUp,
CursorPosition = CaptureHelpers.GetCursorPosition()
});
break;
}
}

return NativeMethods.CallNextHookEx(_hookID, nCode, wParam, lParam);
}

public void Dispose()
{
NativeMethods.UnhookWindowsHookEx(_hookID); // Clean up hook on exit
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ public class ScreenRecordingOptions
public Rectangle CaptureArea { get; set; }
public float Duration { get; set; }
public bool DrawCursor { get; set; }
public bool ShowCursorEffect { get; set; }
public FFmpegOptions FFmpeg { get; set; } = new FFmpegOptions();

public string GetFFmpegCommands()
Expand Down
Loading