C#实现Windows桌面截图功能(二)

204 阅读6分钟

摘要: 本文描述如何使用C#实现Windows桌面截图功能,允许局部截图。


  • 前言

    • 通过案例学习是一种比较有趣的学习方式,下面是使用C#开发Windows桌面截图程序的案例。

    • 案例实现了基本的屏幕截图功能,包括捕获全屏、预览图像以及保存截图文件。

    • 通过该案例,可以让我们了解到使用GDI绘图的技术、文件对话框的使用以及事件驱动编程的概念。

    • 通过自定义图像控件优化UI设计、实现图像显示,滚动缩放、显示图像信息等。

    • 实现功能:

      • 点击按钮截图功能。
      • 屏幕遮罩截图功能:截图时透明显示屏幕,高亮显示截取的局部矩形区域。
      • 截图显示、并显示图像大小、保存图像功能。
      • 鼠标移动显示鼠标点位置及位置对应的RGB颜色。
    • 主要内容:

      • 实现局部截图功能需要创建下面的方法:该方法是DPI感知声明:启动时调用,应对屏幕缩放。如果不创建并调用该方法,屏幕缩放时会获取不到真正的区域。
      [System.Runtime.InteropServices.DllImport("user32.dll")]
      private static extern bool SetProcessDPIAware();
      
      • 除了实现上面的方法,还有一些参数需要设置,是实现桌面遮罩,然后局部截图的。
      this.FormBorderStyle = FormBorderStyle.None;    //无边框
      this.WindowState = FormWindowState.Maximized;   //最大化
      this.Location = Point.Empty;                    //桌面左上角起始位
      this.DoubleBuffered = true;                     //双缓冲
      this.AllowTransparency = true;                  //可调整透明度
      this.TransparencyKey = SystemColors.Control;    //透明区域颜色
      this.TopMost = true;                            //顶层窗体
      this.Opacity = 0.5;                             //透明度
      this.BackColor = Color.White;                   //背景色
      this.Cursor = Cursors.Cross;                    //十字光标
      

  • 预览

    • 主界面

    image.png

    • 截图

    image.png


  • 代码

    • 主窗体

    public partial class MainForm : Form
    {
        public MainForm()
        {
            InitializeComponent();
            this.CenterToParent();
            this.Location = new Point(100,100);
            this.MaximumSize = this.Size;
            this.MinimumSize = this.Size;
        }
        private void Btn_Screenshot_Click(object sender, EventArgs e)
        {
            try
            {
                this.Hide();
                FrmScreenshotEditor frmScreenshotEditor = new FrmScreenshotEditor();
                FrmScreenshot frmScreenshot = new FrmScreenshot(frmScreenshotEditor);
                frmScreenshot.ShowDialog();
                if (frmScreenshotEditor.Image == null)
                {
                    this.Show();
                    return;
                }
                else
                {
                    Image image = new Bitmap(frmScreenshotEditor.Image);
                    frmScreenshotEditor.PixelSize = image.Size;
                    Debug.WriteLine($"图像大小:X = {image.Width},Y = {image.Height}");
                    frmScreenshotEditor.ShowDialog();
                    this.Show();
                }
            }
            catch (Exception ex)
            {
                this.Show();
            }
        }
    }
    
    • 截图窗体

    public partial class FrmScreenshot : Form
    {
        #region 字段|属性
        private Point startPoint;
        private Point endPoint;
        private Point mouseLastPoint;
        private bool isSelecting = false;
        FrmScreenshotEditor frm_ScreenshotEidtor;
        #endregion
        
        #region 初始化|加载
        public FrmScreenshot()
        {
            InitializeComponent();
        }
        public FrmScreenshot(FrmScreenshotEditor form)
        {
            InitializeComponent();
            SetProcessDPIAware();
            frm_ScreenshotEidtor = form;
        }
        private void FrmScreenshot_Load(object sender, EventArgs e)
        {
            this.FormBorderStyle = FormBorderStyle.None;    //无边框
            this.WindowState = FormWindowState.Maximized;   //最大化
            this.Location = Point.Empty;                    //桌面左上角起始位
            this.DoubleBuffered = true;                     //双缓冲
            this.AllowTransparency = true;                  //可调整透明度
            this.TransparencyKey = SystemColors.Control;    //透明区域颜色
            this.TopMost = true;                            //顶层窗体
            this.Opacity = 0.5;                             //透明度
            this.BackColor = Color.White;                   //背景色
            this.Cursor = Cursors.Cross;                    //十字光标
        }
        #endregion
    
        #region 方法|自定义方法
        /// <summary>
        /// DPI感知声明:启动时调用,应对屏幕缩放
        /// </summary>
        [System.Runtime.InteropServices.DllImport("user32.dll")]
        private static extern bool SetProcessDPIAware();
        //采集选择区域
        public Bitmap CaptureSelectedRegion()
        {
            // 计算选择区域
            int captureX = Math.Min(startPoint.X, endPoint.X);
            int captureY = Math.Min(startPoint.Y, endPoint.Y);
            int captureWidth = Math.Abs(startPoint.X - endPoint.X);
            int captureHeight = Math.Abs(startPoint.Y - endPoint.Y);
            if (captureWidth <= 0 || captureHeight <= 0) return null;
    
            // 获取屏幕DPI缩放比例
            float dpiScaleX, dpiScaleY;
            using (Graphics g = this.CreateGraphics())
            {
                dpiScaleX = g.DpiX / 96f;
                dpiScaleY = g.DpiY / 96f;
            }
            // 将逻辑坐标转换为物理坐标
            Point upperLeftSource = this.PointToScreen(new Point(
                (int)(captureX * dpiScaleX),
                (int)(captureY * dpiScaleY)));
            // 计算物理尺寸
            int physicalWidth = (int)(captureWidth * dpiScaleX);
            int physicalHeight = (int)(captureHeight * dpiScaleY);
            this.Invalidate();
            Debug.WriteLine($"起始位置:{startPoint},结束位置:{endPoint}");
            Debug.WriteLine($"采集X:{captureX},采集Y:{captureY}");
            Debug.WriteLine($"采集宽度:{captureWidth},采集高度:{captureHeight}");
            Debug.WriteLine($"转换点:{upperLeftSource}");
    
            // 截取屏幕
            Bitmap bitmap = new Bitmap(captureWidth, captureHeight);
            using (Graphics g = Graphics.FromImage(bitmap))
            {
                g.CopyFromScreen(upperLeftSource, Point.Empty, new Size(captureWidth, captureHeight));
            }
            return bitmap;
        }
        // 显示(绘制)鼠标位置文本
        private void DrawMousePosition(Graphics graphics)
        {
            using (Pen pen = new Pen(Color.Red, 1))
            {
                int x = Math.Min(startPoint.X, endPoint.X);
                int y = Math.Min(startPoint.Y, endPoint.Y);
                int width = Math.Abs(startPoint.X - endPoint.X);
                int height = Math.Abs(startPoint.Y - endPoint.Y);
                // 绘制选择矩形
                DrawSelectedRegion(graphics, new Rectangle(x, y, width, height));
                // 显示尺寸信息
                string sizeText = $"{width} x {height}";
                Font font = new Font("Arial", 14);
                SizeF textSize = graphics.MeasureString(sizeText, font);
                // 确保文本不会超出屏幕
                float textX = x + width - textSize.Width - 5;
                float textY = y + height - textSize.Height - 5;
                if (textX < x) textX = x + 5;
                if (textY < y) textY = y + 5;
                // 绘制文本背景
                graphics.FillRectangle(Brushes.White, textX - 2, textY - 2, textSize.Width + 4, textSize.Height + 4);
                // 绘制文本
                graphics.DrawString(sizeText, font, Brushes.Blue, textX, textY);
            }
        }
        //绘制选择矩形
        private void DrawSelectedRegion(Graphics graphics, Rectangle selectionRect)
        {
            // 绘制半透明背景
            using (Brush overlayBrush = new SolidBrush(Color.FromArgb(128, Color.Black)))
            {
                graphics.FillRectangle(overlayBrush, this.ClientRectangle);
            }
            graphics.SetClip(selectionRect, CombineMode.Exclude);
            graphics.Clear(Color.FromArgb(255, 0, 0, 0));
            graphics.ResetClip();
            // 2. 如果有选择区域,使该区域完全透明
            using (Pen pen = new Pen(Color.Blue, 1))
            {
                graphics.DrawRectangle(pen, selectionRect);
            }
        }
        #endregion
    
        #region 重写事件方法
        protected override void OnMouseDown(MouseEventArgs e)
        {
            base.OnMouseDown(e);
            if (e.Button == MouseButtons.Left)
            {
                startPoint = e.Location;
                isSelecting = true;
            }
            else if (e.Button == MouseButtons.Right)
            {
                this.Close(); // 右键取消
            }
        }
        protected override void OnMouseMove(MouseEventArgs e)
        {
            base.OnMouseMove(e);
            if (isSelecting)
            {
                endPoint = e.Location;
                this.Invalidate();
            }
            else
            {
                mouseLastPoint = e.Location;
                this.Invalidate();
            }
        }
        protected override void OnMouseUp(MouseEventArgs e)
        {
            base.OnMouseUp(e);
            if (e.Button == MouseButtons.Left)
            {
                isSelecting = false;
                if (frm_ScreenshotEidtor != null)
                {
                    this.Opacity = 0;
                    Thread.Sleep(100);
                    this.Refresh();
                    frm_ScreenshotEidtor.Image = CaptureSelectedRegion();
                }
                this.Close();
            }
        }
        protected override void OnMouseHover(EventArgs e)
        {
            base.OnMouseHover(e);
        }
        protected override void OnKeyDown(KeyEventArgs e)
        {
            base.OnKeyDown(e);
            if (e.KeyCode == Keys.Escape)
            {
                this.Close(); // ESC键取消
            }
        }
        protected override void OnPaint(PaintEventArgs e)
        {
            base.OnPaint(e);
            if (isSelecting)
            {
                DrawMousePosition(e.Graphics);
            }
            else
            {
                using (Pen pen = new Pen(Color.Blue, 1))
                {
                    int x = mouseLastPoint.X;
                    int y = mouseLastPoint.Y;
                    int width = 100;
                    int height = 100;
                    string sizeText = $"{x} x {y}";
                    Font font = new Font("宋体", 14);
                    SizeF textSize = e.Graphics.MeasureString(sizeText, font);
                    float textX = x + width - textSize.Width - 5;
                    float textY = y + height - textSize.Height - 5;
                    // 绘制文本背景
                    e.Graphics.FillRectangle(Brushes.White, textX - 2, textY - 2,textSize.Width + 4, textSize.Height + 4);
                    // 绘制文本
                    e.Graphics.DrawString(sizeText, font, Brushes.Blue, x-20, y);
                }
            }
        }
        #endregion
    }
    
    • 截图编辑窗体

    public partial class FrmScreenshotEditor :Form
    {
        #region 字段|属性
        private Size pixelSize = new Size(0,0);
        public Image Image
        {
            get => uvCanvas.Image;
            set
            {
                uvCanvas.Image = value;
                label_PixelSize.Text = $"像素:{PixelSize.Width}×{PixelSize.Height}";
                Invalidate();
            }
        }
        public Size PixelSize { 
            get => pixelSize;
            set
            {
                pixelSize = value;
                label_PixelSize.Text = $"像素:{PixelSize.Width}×{PixelSize.Height}";
                Invalidate();
            }
        }
        #endregion
    
        #region 初始化|加载
        public FrmScreenshotEditor()
        {
            this.WindowState = FormWindowState.Maximized;
            InitializeComponent();
            this.Size = new Size(1000, 700);
            uvCanvas.ImagePixPointEvent += UvCanvas_ImagePixPointEvent;
        }
        private void UvCanvas_ImagePixPointEvent(PointF e)
        {
            Debug.WriteLine($"当前图像点:{(int)e.X},{(int)e.Y}");
        }
        #endregion
    
        #region 按钮事件方法
        private void btn_Save_Click(object sender, System.EventArgs e)
        {
            SaveFileDialog saveFileDialog = new SaveFileDialog();
            saveFileDialog.FileName = "截图1";     //设置初始文件名
            saveFileDialog.Filter = "PNG Image|*.png|JPEG Image|*.jpg|BMP Image|*.bmp";
            if (saveFileDialog.ShowDialog() == DialogResult.OK)
            {
                string extension = Path.GetExtension(saveFileDialog.FileName).ToLower();
                ImageFormat imageFormat;
                switch (extension)
                {
                    case ".png":
                        imageFormat = ImageFormat.Png;
                        break;
                    case ".jpg":
                        imageFormat = ImageFormat.Jpeg;
                        break;
                    case ".bmp":
                        imageFormat = ImageFormat.Bmp;
                        break;
                    default:
                        imageFormat = ImageFormat.Png;
                        break;
                }
                try
                {
                    Image.Save(saveFileDialog.FileName, imageFormat);
                    MessageBox.Show($"图片已成功保存至: {saveFileDialog.FileName}");
                }
                catch (Exception ex)
                {
                    MessageBox.Show($"保存图片时出错: {ex.Message}");
                }
            }
        }
        private void BtnUndo_Click(object sender, EventArgs e)
        {
    
        }
        private void BtnRedo_Click(object sender, EventArgs e)
        {
    
        }
        private void BtnAddText_Click(object sender, EventArgs e)
        {
        }
        #endregion
    
        #region 重写事件方法
        protected override void OnResize(EventArgs e)
        {
            base.OnResize(e);
        }
        #endregion
    }
    
    • 自定义控件代码

    public partial class UVCanvas : Control
    {
        #region 字段|属性
        public delegate void ImagePixPointCallback(PointF pointF);
        public event ImagePixPointCallback ImagePixPointEvent;
        //基本参数
        private Image _image;                               //图像
        private Point _lastMousePosition;                   //鼠标最后位置
        private PointF _imagePoint = new PointF(0, 0);      //图像位置点
        private PointF _imagePosition = new PointF(0, 0);   //图像位置
        private float _zoom = 1.0f;                         //当前缩放
        private const float MinZoom = 0.15f;                //最小缩放
        private const float MaxZoom = 10.0f;                //最大缩放
        private const float ZoomStep = 0.1f;                //缩放步长
        //控件背景棋盘格参数
        private int controlGridSize = 20;           // 每个格子的大小
        private Color backColor1 = Color.LightGray; // 第一种颜色
        private Color backColor2 = Color.WhiteSmoke;     // 第二种颜色
        //属性
        public Image Image
        {
            get => _image;
            set
            {
                _image = value;
                CenterImage();
                Invalidate();
            }
        }
        /// <summary>
        /// 当前像素点
        /// </summary>
        public PointF ImagePoint { get => _imagePoint;private set => _imagePoint = value; }
        #endregion
    
        #region 初始化
        public UVCanvas()
        {
            this.DoubleBuffered = true;
            this.SetStyle(ControlStyles.OptimizedDoubleBuffer | ControlStyles.UserPaint, true);
        }
        #endregion
        #region 其他方法
        private PointF ScreenToImage(Point screenPoint)
        {
            return new PointF((screenPoint.X - _imagePosition.X) / _zoom, (screenPoint.Y - _imagePosition.Y) / _zoom);
        }
        /// <summary>
        /// 获取目标矩形
        /// </summary>
        private RectangleF GetDestinationRectangle()
        {
            if (_image == null) return RectangleF.Empty;
            return new RectangleF(_imagePosition.X, _imagePosition.Y, (_image.Width * _zoom), (_image.Height * _zoom));
        }
        /// <summary>
        /// 图像居中
        /// </summary>
        private void CenterImage()
        {
            if (_image == null) return;
            _imagePosition = new PointF((this.Width - _image.Width * _zoom) / 2, (this.Height - _image.Height * _zoom) / 2);
            Invalidate();
        }
        public void ResetImage()
        {
            _zoom = 1.0f;
            CenterImage();
        }
        /// <summary>
        /// 获取像素值
        /// </summary>
        private Color GetPixelColor(PointF imagePoint)
        {
            if (_image == null
                ||imagePoint.X < 0 || imagePoint.X >= _image.Width 
                || imagePoint.Y < 0 || imagePoint.Y >= _image.Height)
            {
                return Color.Empty; // 无效坐标
            }
            // 如果是 Bitmap,直接读取像素
            if (_image is Bitmap bitmap)
            {
                return bitmap.GetPixel((int)imagePoint.X, (int)imagePoint.Y);
            }
            else
            {
                // 如果是其他 Image 类型(如 Metafile),先转换为 Bitmap
                using (Bitmap tempBitmap = new Bitmap(_image))
                {
                    return tempBitmap.GetPixel((int)imagePoint.X, (int)imagePoint.Y);
                }
            }
        }
        #region 绘制网格
        /// <summary>
        /// 绘制控件棋盘格背景
        /// </summary>
        private void DrawControlGridBackgroud(PaintEventArgs e)
        {
            // 获取绘制区域
            Rectangle clipRect = e.ClipRectangle;
            // 计算需要绘制的起始行列
            int startX = (int)(clipRect.Left / controlGridSize) * controlGridSize;
            int startY = (int)(clipRect.Top / controlGridSize) * controlGridSize;
            // 循环绘制棋盘格
            for (int y = startY; y < clipRect.Bottom; y += controlGridSize)
            {
                for (int x = startX; x < clipRect.Right; x += controlGridSize)
                {
                    // 根据位置绘制颜色
                    bool isAlternate = ((x / controlGridSize) + (y / controlGridSize)) % 2 == 0;
                    using (Brush brush = new SolidBrush(isAlternate ? backColor1 : backColor2))
                    {
                        e.Graphics.FillRectangle(brush, x, y, controlGridSize, controlGridSize);
                    }
                }
            }
        }
        #endregion
        #endregion
    
        #region 事件方法重写
        protected override void OnPaint(PaintEventArgs e)
        {
            base.OnPaint(e);
            DrawControlGridBackgroud(e);
            if (_image == null) return;
            e.Graphics.InterpolationMode = InterpolationMode.HighQualityBicubic;
            e.Graphics.PixelOffsetMode = PixelOffsetMode.HighQuality;
            RectangleF destRect = GetDestinationRectangle();
            e.Graphics.DrawImage(_image, destRect);
            // 新增:绘制当前鼠标位置的图像坐标
            string coordText = $"({ImagePoint.X:F1}, {ImagePoint.Y:F1})";
            // 新增:获取 RGB 值
            Color pixelColor = GetPixelColor(ImagePoint);
            using (var font = new Font("Arial", 10))
            using (var brush = new SolidBrush(Color.Red))
            {
                //判断显示 RGB 值
                if (pixelColor != Color.Empty)
                {
                    string rgbText = $"RGB: ({pixelColor.R}, {pixelColor.G}, {pixelColor.B})";
                    coordText = $"{coordText}\r\n{rgbText}";
                }
                e.Graphics.DrawString(coordText, font, brush, 10, 10);
            }
        }
        protected override void OnResize(EventArgs e)
        {
            base.OnResize(e);
            CenterImage();
        }
        protected override void OnDoubleClick(EventArgs e)
        {
            base.OnDoubleClick(e);
            ResetImage();
        }
        protected override void OnMouseDown(MouseEventArgs e)
        {
            base.OnMouseDown(e);
            if (e.Button == MouseButtons.Left && _image != null)
            {
                _lastMousePosition = e.Location;
                this.Cursor = Cursors.Hand;
            }
        }
        protected override void OnMouseUp(MouseEventArgs e)
        {
            base.OnMouseUp(e);
            this.Cursor = Cursors.Default;
        }
        protected override void OnMouseMove(MouseEventArgs e)
        {
            base.OnMouseMove(e);
            if (_image != null)
            {
                // 获取图像在屏幕上的绘制区域
                ImagePoint = ScreenToImage(e.Location);
                // 检查鼠标是否在图像区域内
                RectangleF imageRect = GetDestinationRectangle();
                //计算图像坐标(仅在图像区域内计算)
                if (imageRect.Contains(e.Location)){ ImagePoint = ScreenToImage(e.Location);}
                // 不在图像内,设为 (-1, -1)
                else { ImagePoint = new PointF(-1, -1); }
                // 回调图像点
                ImagePixPointEvent?.Invoke(ImagePoint);
                Invalidate();
            }
            if (e.Button == MouseButtons.Left && _image != null)
            {
                // 计算鼠标移动增量
                _imagePosition.X += (e.X - _lastMousePosition.X);
                _imagePosition.Y += (e.Y - _lastMousePosition.Y);
                // 记录鼠标最后位置
                _lastMousePosition = e.Location;
                Invalidate();
            }
        }
        protected override void OnMouseWheel(MouseEventArgs e)
        {
            base.OnMouseWheel(e);
            if (_image == null) return;
            // 1. 记录缩放前鼠标在 "图像坐标" 中的位置
            PointF mouseInImageSpace = ScreenToImage(e.Location);
            // 2. 计算新的缩放级别
            _zoom += (e.Delta > 0 ? ZoomStep : -ZoomStep);
            _zoom = Math.Max(MinZoom, Math.Min(MaxZoom, _zoom));
            // 3. 计算缩放后该点在屏幕上的新位置(基于鼠标点缩放)
            PointF mouseInScreenSpaceAfterZoom = new PointF(
                mouseInImageSpace.X * _zoom + _imagePosition.X,
                mouseInImageSpace.Y * _zoom + _imagePosition.Y);
            // 4. 计算偏移量,使鼠标下的点仍然在原来的屏幕位置
            _imagePosition.X += (e.Location.X - mouseInScreenSpaceAfterZoom.X);
            _imagePosition.Y += (e.Location.Y - mouseInScreenSpaceAfterZoom.Y);
            // 更新显示
            Invalidate();
        }
        #endregion
    }
    

  • 总结

    • 通过实现案例学习编程是一种高效的学习方法。这种学习方式将抽象概念具体化。通过上面的截图程序,我们实际应用了窗体控件、事件处理、GDI+绘图等核心知识点,比单纯学习理论更容易理解和记忆。
    • 其次通过案例可以获得正向反馈,看到自己编写的代码真正实现功能,极大提升了学习动力和成就感。
    • 最后,该案例后面可以自己扩展性,当你掌握了更多的基础知识后,可以逐步添加区域选择、图像编辑等更高级的功能。

  • 最后

    • 项目源码: gitee.com/incodenotes…
    • 如果你觉得这篇文章对你有帮助,不妨点个赞再走呗!
    • 如有其他疑问,欢迎评论区留言讨论!
    • 也可以关注微信公众号 [编程笔记in] ,一起交流学习!

❖ 感 谢 您 的 关 注 ❖