参考
一、背景
WPF仿QQ、微信截图功能,功能主体源于【SnapShotWPFLibrary】,增加辅助捕获窗口、反向拖动、选框区域边界、支持DPI缩放等。
二、辅助捕获窗口
实现QQ、微信截图辅助选中窗口区域。
1. 获取桌面所有可见非最小化窗口
allWinInfos = WinAPI.FindAll();
internal class WinAPI
{
/// <summary>
/// 查找当前用户空间下所有符合条件的窗口。如果不指定条件,将仅查找可见窗口。
/// </summary>
/// <param name="match">过滤窗口的条件。如果设置为 null,将仅查找可见窗口。</param>
/// <returns>找到的所有窗口信息。</returns>
public static IReadOnlyList<WindowInfo> FindAll(Predicate<WindowInfo> match = null)
{
var windowList = new List<WindowInfo>();
EnumWindows(OnWindowEnum, 0);
return windowList.FindAll(match ?? DefaultPredicate);
bool OnWindowEnum(IntPtr hWnd, int lparam)
{
// 仅查找顶层窗口。
if (GetParent(hWnd) == IntPtr.Zero)
{
// 获取窗口类名。
var lpString = new StringBuilder(512);
GetClassName(hWnd, lpString, lpString.Capacity);
var className = lpString.ToString();
// 获取窗口标题。
var lptrString = new StringBuilder(512);
GetWindowText(hWnd, lptrString, lptrString.Capacity);
var title = lptrString.ToString().Trim();
// 获取窗口可见性。
var isVisible = IsWindowVisible(hWnd);
// 获取窗口位置和尺寸。
LPRECT rect = default;
GetWindowRect(hWnd, ref rect);
var bounds = new Rect(rect.Left, rect.Top, rect.Right - rect.Left, rect.Bottom - rect.Top);
// 添加到已找到的窗口列表。
windowList.Add(new WindowInfo(hWnd, className, title, isVisible, bounds));
}
return true;
}
}
/// <summary>
/// 默认的查找窗口的过滤条件。可见 + 非最小化 + 包含窗口标题。
/// </summary>
private static readonly Predicate<WindowInfo> DefaultPredicate = x => x.IsVisible && !x.IsMinimized && x.Title.Length > 0;
private delegate bool WndEnumProc(IntPtr hWnd, int lParam);
/// <summary>
/// 函数会按照Z序来枚举窗口
/// </summary>
/// <param name="lpEnumFunc"></param>
/// <param name="lParam"></param>
/// <returns></returns>
[DllImport("user32")]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool EnumWindows(WndEnumProc lpEnumFunc, int lParam);
[DllImport("user32")]
private static extern IntPtr GetParent(IntPtr hWnd);
[DllImport("user32")]
private static extern bool IsWindowVisible(IntPtr hWnd);
[DllImport("user32")]
private static extern int GetWindowText(IntPtr hWnd, StringBuilder lptrString, int nMaxCount);
[DllImport("user32")]
private static extern int GetClassName(IntPtr hWnd, StringBuilder lpString, int nMaxCount);
[DllImport("user32")]
private static extern void SwitchToThisWindow(IntPtr hWnd, bool fAltTab);
[DllImport("user32")]
private static extern bool GetWindowRect(IntPtr hWnd, ref LPRECT rect);
[StructLayout(LayoutKind.Sequential)]
private readonly struct LPRECT
{
public readonly int Left;
public readonly int Top;
public readonly int Right;
public readonly int Bottom;
}
private const int LOGPIXELSX = 88;
private const int LOGPIXELSY = 90;
[DllImport("gdi32.dll")]
private static extern int GetDeviceCaps(IntPtr hdc, int index);
[DllImport("user32.dll")]
private static extern IntPtr GetDC(IntPtr hWnd);
[DllImport("user32.dll")]
private static extern int ReleaseDC(IntPtr hWnd, IntPtr hDc);
public static Dpi GetDpiByWin32()
{
var hDc = GetDC(IntPtr.Zero);
var dpiX = GetDeviceCaps(hDc, LOGPIXELSX);
var dpiY = GetDeviceCaps(hDc, LOGPIXELSY);
ReleaseDC(IntPtr.Zero, hDc);
return new Dpi(dpiX, dpiY);
}
}
/// <summary>
/// 获取 Win32 窗口的一些基本信息。
/// </summary>
public readonly struct WindowInfo
{
public WindowInfo(IntPtr hWnd, string className, string title, bool isVisible, Rect bounds) : this()
{
Hwnd = hWnd;
ClassName = className;
Title = title;
IsVisible = isVisible;
Bounds = bounds;
}
/// <summary>
/// 获取窗口句柄。
/// </summary>
public IntPtr Hwnd { get; }
/// <summary>
/// 获取窗口类名。
/// </summary>
public string ClassName { get; }
/// <summary>
/// 获取窗口标题。
/// </summary>
public string Title { get; }
/// <summary>
/// 获取当前窗口是否可见。
/// </summary>
public bool IsVisible { get; }
/// <summary>
/// 获取窗口当前的位置和尺寸。
/// </summary>
public Rect Bounds { get; }
/// <summary>
/// 获取窗口当前是否是最小化的。
/// </summary>
public bool IsMinimized => Bounds.Left == -32000 && Bounds.Top == -32000;
}
public struct Dpi
{
public double X { get; set; }
public double Y { get; set; }
public Dpi(double x, double y)
{
X = x;
Y = y;
}
}
2. 初始状态辅助选中窗口
private void GridBg_MouseMove(object sender, MouseEventArgs e)
{
if (IsStartState)
{
AssistSltBox(e.GetPosition(this));
}
// 框选状态
if (isBoxSlt && !isDrag)
{
endPosition = e.GetPosition(this);
if (startPosition.X != endPosition.X || startPosition.Y != endPosition.Y)
{
MaskResize(true, true);
}
}
}
#region 辅助捕获窗口
private IReadOnlyList<WindowInfo> allWinInfos;
private WindowInfo currentWin;
/// <summary>
/// 辅助选框
/// </summary>
/// <param name="currentPosition"></param>
private void AssistSltBox(Point currentPosition)
{
Point point = new Point((int)currentPosition.X * dpiScale, (int)currentPosition.Y * dpiScale);
foreach (WindowInfo win in allWinInfos)
{
if (win.Bounds.Contains(point))
{
if (win.Equals(currentWin)) return;
currentWin = win;
startPosition.X = win.Bounds.X / dpiScale;
startPosition.Y = win.Bounds.Y / dpiScale;
endPosition.X = win.Bounds.X / dpiScale + win.Bounds.Width / dpiScale;
endPosition.Y = win.Bounds.Y / dpiScale + win.Bounds.Height / dpiScale;
MaskResize(true, true);
return;
}
}
}
#endregion
3. 效果预览
三、反向拖动
只需在鼠标按下时设置开始点与拖动时设置结束点即可。
1. 单击移动事件,设置开始点和结束点
private void GridBg_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
switch (e.ClickCount)
{
case 1:
Point point = e.GetPosition(this);
if (IsStartState)
{
isBoxSlt = true;
IsStartState = false;
}
if (isDrag == false && isBoxSlt == true) //框选状态
{
startPosition = point;
}
if (isDrag == false && isBoxSlt == false) //拖拽状态
{
isDrag = true;
mouseDownPosition = point;
mouseDownMargin = new Thickness(gridLeftMask.ActualWidth, gridTopMask.ActualHeight, gridRightMask.ActualWidth, gridBottomMask.ActualHeight);
}
//当鼠标位置在选框中,gridSltFrame强制捕获鼠标
if (isDrag && boxSltRect.Contains((int)point.X, (int)point.Y))
{
this.gridSltFrame.CaptureMouse();
}
break;
case 2: //截图
boxSltBitmap = desktopBg.Clone(boxSltRect, desktopBg.PixelFormat);
this.DialogResult = true;
break;
}
}
private void GridBg_MouseMove(object sender, MouseEventArgs e)
{
if (IsStartState)
{
AssistSltBox(e.GetPosition(this));
}
// 框选状态
if (isBoxSlt && !isDrag)
{
endPosition = e.GetPosition(this);
if (startPosition.X != endPosition.X || startPosition.Y != endPosition.Y)
{
MaskResize(true, true);
}
}
}
private void Thumb_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
var thumb = sender as Control; // 将sender转换为Control类型,并赋值给thumb变量
var h = thumb?.HorizontalAlignment;
var v = thumb?.VerticalAlignment;
//设置开始位置,反方向拖拽点为开始位置
if (h == HorizontalAlignment.Left && v == VerticalAlignment.Top) //左上角
{
//设置开始位置为右下角
startPosition.X = this.ActualWidth - gridRightMask.Width - 1;
startPosition.Y = this.ActualHeight - gridBottomMask.Height - 1;
}
else if (h == HorizontalAlignment.Right && v == VerticalAlignment.Top) //右上角
{
//设置开始位置为左下角
startPosition.X = gridLeftMask.Width;
startPosition.Y = this.ActualHeight - gridBottomMask.Height - 1;
}
else if (h == HorizontalAlignment.Left && v == VerticalAlignment.Bottom) //左下角
{
//设置开始位置为右上角
startPosition.X = this.ActualWidth - gridRightMask.Width - 1;
startPosition.Y = gridTopMask.Height;
}
else if (h == HorizontalAlignment.Right && v == VerticalAlignment.Bottom) //右下角
{
//设置开始位置为左上角
startPosition.X = gridLeftMask.Width;
startPosition.Y = gridTopMask.Height;
}
else if (v == VerticalAlignment.Top) //上
{
//设置开始位置为下边
startPosition.Y = this.ActualHeight - gridBottomMask.Height - 1;
}
else if (v == VerticalAlignment.Bottom) //下
{
//设置开始位置为上边
startPosition.Y = gridTopMask.Height;
}
else if (h == HorizontalAlignment.Left) //左
{
//设置开始位置为右边
startPosition.X = this.ActualWidth - gridRightMask.Width - 1;
}
else if (h == HorizontalAlignment.Right) //右
{
//设置开始位置为左边
startPosition.X = gridLeftMask.Width;
}
}
private void Thumb_DragDelta(object sender, DragDeltaEventArgs e)
{
var thumb = sender as Control; // 将sender转换为Control类型,并赋值给thumb变量
var h = thumb?.HorizontalAlignment;
var v = thumb?.VerticalAlignment;
endPosition = Mouse.GetPosition(this);
if (h != HorizontalAlignment.Center && (v == VerticalAlignment.Top || v == VerticalAlignment.Bottom)) //角落
{
MaskResize(true, true);
}
else if(v == VerticalAlignment.Top || v == VerticalAlignment.Bottom)
{
MaskResize(false, true);
}
else if(h == HorizontalAlignment.Left || h == HorizontalAlignment.Right)
{
MaskResize(true, false);
}
}
2. 选框区域大小位置调整
/// <summary>
/// 蒙板大小调整
/// </summary>
/// <param name="x">水平</param>
/// <param name="y">垂直</param>
private void MaskResize(bool x, bool y)
{
#region 上下左右蒙板大小调整
if (x)
{
double left = 0, right = 0;
if (startPosition.X < endPosition.X)
{
left = startPosition.X;
right = this.ActualWidth - endPosition.X - 1;
}
else
{
left = endPosition.X;
right = this.ActualWidth - startPosition.X - 1;
}
gridLeftMask.Width = left < 0 ? 0 : left;
gridRightMask.Width = right < 0 ? 0 : right;
}
if (y)
{
double top = 0, bottom = 0;
if (startPosition.Y < endPosition.Y)
{
top = startPosition.Y;
bottom = this.ActualHeight - endPosition.Y - 1;
}
else
{
top = endPosition.Y;
bottom = this.ActualHeight - startPosition.Y - 1;
}
gridTopMask.Height = top < 0 ? 0 : top;
gridBottomMask.Height = bottom < 0 ? 0 : bottom;
}
#endregion
#region 设置选框位置大小
int width = (int)((this.ActualWidth - gridLeftMask.Width - gridRightMask.Width) * dpiScale);
int height = (int)((this.ActualHeight - gridTopMask.Height - gridBottomMask.Height) * dpiScale);
boxSltRect = new System.Drawing.Rectangle((int)(gridLeftMask.Width * dpiScale), (int)(gridTopMask.Height * dpiScale), width, height);
#endregion
#region 选框区域文本信息及位置更新
tbSltFrameInfo.Text = $"{(int)(gridLeftMask.Width * dpiScale)},{(int)(gridTopMask.Height * dpiScale)} {width}×{height}";
tbSltFrameInfoMargin.Left = gridLeftMask.Width;
if (gridTopMask.Height < 20) tbSltFrameInfoMargin.Top = gridTopMask.Height;
else tbSltFrameInfoMargin.Top = gridTopMask.Height - 16;
tbSltFrameInfo.Margin = tbSltFrameInfoMargin;
#endregion
}
3. 效果预览
四、选框区域拖动和边界
需要注意选框区域背景透明导致无法拖动问题,通过单击顶层背景强制捕获鼠标实现。
1. 边界
/// <summary>
/// 透明背景不生效,需被强制捕获
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void GridSltFrame_MouseMove(object sender, MouseEventArgs e)
{
// 拖拽状态
if (isDrag && !isBoxSlt)
{
var pos = e.GetPosition(this); // 获取移动后的位置
var dp = pos - mouseDownPosition; // 计算偏移量
Thickness newThickness = new Thickness(mouseDownMargin.Left + dp.X, mouseDownMargin.Top + dp.Y, mouseDownMargin.Right - dp.X, mouseDownMargin.Bottom - dp.Y);
#region 选框拖拽边界
if (newThickness.Left < 0)
{
newThickness.Right += newThickness.Left;
newThickness.Left = 0;
}
if (newThickness.Right < 0)
{
newThickness.Left += newThickness.Right;
newThickness.Right = 0;
}
if (newThickness.Top < 0)
{
newThickness.Bottom += newThickness.Top;
newThickness.Top = 0;
}
if (newThickness.Bottom < 0)
{
newThickness.Top += newThickness.Bottom;
newThickness.Bottom = 0;
}
#endregion
gridLeftMask.Width = newThickness.Left < 0 ? 0 : newThickness.Left;
gridTopMask.Height = newThickness.Top < 0 ? 0 : newThickness.Top;
gridRightMask.Width = newThickness.Right < 0 ? 0 : newThickness.Right;
gridBottomMask.Height = newThickness.Bottom < 0 ? 0 : newThickness.Bottom;
//只更新大小位置信息
MaskResize(false, false);
}
}
2. 效果预览
五、结语
新手第一次写,有点乱,见谅!