如何开发类似Epic Pen的屏幕透明绘图应用?
实现类似Epic Pen的屏幕绘图应用解决方案
一、实现MSPaint风格的笔触绘制效果
要模拟连续流畅的笔触,核心是实时追踪鼠标轨迹并绘制连续线条,而非仅生成静态图形:
- 维护两个集合:
_currentStroke记录当前正在绘制的轨迹点,_allStrokes存储已完成的所有笔触 - 鼠标按下时启动轨迹记录,移动过程中持续添加点并触发窗体重绘,鼠标松开时完成当前笔触的保存
- 绘制时使用
DrawLines连接轨迹点,开启抗锯齿(SmoothingMode.AntiAlias)让笔触边缘更自然
二、解决透明窗体点击穿透问题
放弃TransparencyKey方案,改用**分层窗口(Layered Window)**实现透明且保留鼠标交互:
- 通过Win32 API设置窗体扩展样式为
WS_EX_LAYERED,支持分层绘制 - 结合
WS_EX_TRANSPARENT样式控制鼠标事件穿透:绘图模式下移除该样式,让窗体接收鼠标事件;光标模式下添加该样式,允许事件传递到下层窗口 - 使用
SetLayeredWindowAttributes设置窗体透明度,确保背景透明但绘制内容可见
三、绘图/光标模式切换逻辑
通过全局快捷键或UI按钮触发模式切换:
- 绘图模式:显示专用光标(如十字光标),移除
WS_EX_TRANSPARENT样式,允许窗体捕获鼠标事件进行绘制 - 光标模式:恢复系统默认光标,添加
WS_EX_TRANSPARENT样式,让鼠标事件穿透到下层窗口,同时暂停绘制逻辑
关键代码示例
窗体初始化与Win32 API导入
using System; using System.Collections.Generic; using System.Drawing; using System.Windows.Forms; using System.Runtime.InteropServices; public class ScreenDrawForm : Form { private List<Point> _currentStroke = new List<Point>(); private List<List<Point>> _allStrokes = new List<List<Point>>(); private Pen _drawPen = new Pen(Color.Red, 5); private bool _isDrawing = false; private bool _isDrawMode = true; // Win32 API声明 [DllImport("user32.dll")] private static extern IntPtr SetWindowLongPtr(IntPtr hWnd, int nIndex, IntPtr dwNewLong); [DllImport("user32.dll")] private static extern IntPtr GetWindowLongPtr(IntPtr hWnd, int nIndex); [DllImport("user32.dll")] private static extern bool SetLayeredWindowAttributes(IntPtr hWnd, uint crKey, byte bAlpha, uint dwFlags); private const int GWL_EXSTYLE = -20; private const int WS_EX_LAYERED = 0x80000; private const int WS_EX_TRANSPARENT = 0x20; private const uint LWA_ALPHA = 0x2; public ScreenDrawForm() { FormBorderStyle = FormBorderStyle.None; WindowState = FormWindowState.Maximized; TopMost = true; // 初始化分层窗口 var exStyle = GetWindowLongPtr(Handle, GWL_EXSTYLE); SetWindowLongPtr(Handle, GWL_EXSTYLE, (IntPtr)(exStyle.ToInt64() | WS_EX_LAYERED)); SetLayeredWindowAttributes(Handle, 0, 255, LWA_ALPHA); Cursor = Cursors.Cross; // 注册F1为模式切换快捷键 RegisterHotKey(Handle, 1, 0, (int)Keys.F1); }
鼠标事件与绘制逻辑
protected override void OnMouseDown(MouseEventArgs e) { if (_isDrawMode) { _isDrawing = true; _currentStroke.Clear(); _currentStroke.Add(e.Location); } base.OnMouseDown(e); } protected override void OnMouseMove(MouseEventArgs e) { if (_isDrawing && _isDrawMode) { _currentStroke.Add(e.Location); Invalidate(); } base.OnMouseMove(e); } protected override void OnMouseUp(MouseEventArgs e) { if (_isDrawing && _isDrawMode) { _currentStroke.Add(e.Location); _allStrokes.Add(new List<Point>(_currentStroke)); _isDrawing = false; Invalidate(); } base.OnMouseUp(e); } protected override void OnPaint(PaintEventArgs e) { base.OnPaint(e); e.Graphics.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias; // 绘制所有已完成的笔触 foreach (var stroke in _allStrokes) { if (stroke.Count > 1) e.Graphics.DrawLines(_drawPen, stroke.ToArray()); } // 绘制当前正在进行的笔触 if (_currentStroke.Count > 1) e.Graphics.DrawLines(_drawPen, _currentStroke.ToArray()); }
模式切换实现
protected override void WndProc(ref Message m) { const int WM_HOTKEY = 0x0312; if (m.Msg == WM_HOTKEY && m.WParam.ToInt32() == 1) { ToggleMode(); } base.WndProc(ref m); } private void ToggleMode() { _isDrawMode = !_isDrawMode; var exStyle = GetWindowLongPtr(Handle, GWL_EXSTYLE); if (_isDrawMode) { Cursor = Cursors.Cross; // 移除透明穿透样式,允许接收鼠标事件 SetWindowLongPtr(Handle, GWL_EXSTYLE, (IntPtr)(exStyle.ToInt64() & ~WS_EX_TRANSPARENT)); } else { Cursor = Cursors.Default; // 添加透明穿透样式,让鼠标事件传递到下层窗口 SetWindowLongPtr(Handle, GWL_EXSTYLE, (IntPtr)(exStyle.ToInt64() | WS_EX_TRANSPARENT)); } } [DllImport("user32.dll")] private static extern bool RegisterHotKey(IntPtr hWnd, int id, uint fsModifiers, int vk); [DllImport("user32.dll")] private static extern bool UnregisterHotKey(IntPtr hWnd, int id); protected override void Dispose(bool disposing) { UnregisterHotKey(Handle, 1); base.Dispose(disposing); } }
内容的提问来源于stack exchange,提问作者user123
相关产品推荐
相关产品推荐

