C# WPF 截屏截图功能

2 篇文章 0 订阅
2 篇文章 0 订阅

参考

【SnapShotWPFLibrary】

一、背景

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. 效果预览 

五、结语

新手第一次写,有点乱,见谅!

  • 9
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值