C++ --- Windows屏幕和窗口截图

目录

        • 前言
        • 屏幕和窗口截图
          • 枚举屏幕和窗口
          • 截取指定HWND的窗口图像
          • 测试用例
        • 一点小坑
        • 结束语
        • 完整代码
        • 参考资料

前言

最近学习了Windows下的屏幕截图,延伸到窗口截图,其中也遇到了不少的坑,好在最后还是实现了我的目标。顺便封装了一下Win32的api,方便后续使用。

屏幕和窗口截图

首先是先找了MS官方的屏幕截图示例,不过并没有很符合我的要求,不过也受到启发,使用窗口DC获取窗口图像。

枚举屏幕和窗口

为了将屏幕和窗口进行统一,我们定义一个结构体

//1. 屏幕的hwnd为NULL,这时截图的区域由rect指定
//2. 窗口的hwnd不为空,这时可以根据API获取窗口的大小
// 为了将屏幕和窗口进行统一,因此使用了结构体
struct WindowInfo
{
  HWND hwnd; /* 为空表示屏幕截图 */
  std::string desc; // 窗口标题
  RECT rect{ 0,0,0,0 }; /* hwnd不为空时,此参数无效 */
};
  • 如果电脑上有多个显示器,并且我们想截取指定的屏幕怎么办呢?这时就需要枚举显示器的API
  • 要截图指定的窗口,那我们要先知道窗口的句柄,如何获取呢?这时也需要枚举窗口的API或者获取当前鼠标指向的窗口
    // 判断当前鼠标指向哪个窗口
    HWND GetCurPointedWindow()
    {
      POINT point;
      BOOL ret = GetCursorPos(&point);
      if (ret) {
        HWND hwnd = WindowFromPoint(point);
        if (hwnd != nullptr && hwnd != INVALID_HANDLE_VALUE) {
          return hwnd;
        }
      }
      return nullptr;
    }
    

封装好的枚举类,使用示例在后面:

class Enumerator
{
public:
  using EnumCallback = std::function<void(const WindowInfo &)>;

  static bool EnumMonitor(EnumCallback callback)
  {
    // 调用Win32Api进行显示器遍历
    return ::EnumDisplayMonitors(NULL, NULL, MonitorEnumProc, (LPARAM)&callback);
  }

  static bool EnumWindow(EnumCallback callback)
  {
    // 调用Win32Api进行窗口遍历
    return ::EnumWindows(EnumWindowsProc, (LPARAM)&callback);
  }

private:
  static BOOL CALLBACK EnumWindowsProc(HWND hwnd, LPARAM lParam)
  {
    //::GetParent获取的有可能是所有者窗口,因此使用GetAncestor获取父窗口句柄
    HWND parent = ::GetAncestor(hwnd, GA_PARENT); 
    HWND desktop = ::GetDesktopWindow(); // 获取桌面的句柄
    TCHAR szTitle[MAX_PATH] = { 0 };
    ::GetWindowText(hwnd, szTitle, MAX_PATH); // 获取标题

    // 排除父窗口不是桌面的
    if (parent != nullptr && parent != desktop) return TRUE;

    // 排除标题为空的
    if (wcscmp(szTitle, L"") == 0) return TRUE;

    // 排除最小化窗口(因为获取最小化窗口的区域数据是不对的,因此也没办法进行截图等操作)
    if (::IsIconic(hwnd)) return TRUE;

    // 排除不可见窗口,被其他窗口遮挡的情况是可见的
    if (!::IsWindowVisible(hwnd)) return TRUE;

    // 排除对用户隐形的窗口,参考[https://docs.microsoft.com/en-us/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute]
    DWORD flag = 0;
    DwmGetWindowAttribute(hwnd, DWMWA_CLOAKED, &flag, sizeof(flag));
    if (flag) return TRUE;

    if (lParam) {
      WindowInfo wnd_info{ hwnd,(LPCSTR)CT2A(szTitle, CP_ACP) };
      EnumCallback * callback_ptr = reinterpret_cast<EnumCallback *>(lParam);
      callback_ptr->operator()(wnd_info);
    }
    return TRUE;
  }

  static BOOL CALLBACK MonitorEnumProc(HMONITOR hMonitor, HDC hdcMonitor, LPRECT lprcMonitor, LPARAM dwData)
  {
    MONITORINFOEX mi;
    mi.cbSize = sizeof(MONITORINFOEX);
    GetMonitorInfo(hMonitor, &mi);
    if (dwData) {
      std::string device_name = (LPCSTR)CT2A(mi.szDevice, CP_ACP);
      if (mi.dwFlags == MONITORINFOF_PRIMARY) device_name += "(Primary)"; // 主显示器,可根据需要进行操作
      WindowInfo wnd_info{ nullptr, device_name, mi.rcMonitor };

      EnumCallback *callback = reinterpret_cast<EnumCallback *>(dwData);
      (*callback)(wnd_info);
    }
    return TRUE;
  }
};
截取指定HWND的窗口图像

封装好的截图类:

class WindowCapture
{
public:
  using BitmapPtr = std::shared_ptr<Gdiplus::Bitmap>;

  static BitmapPtr Capture(const WindowInfo &wnd_info)
  {
    HDC hWndDC = GetWindowDC(wnd_info.hwnd);
    RECT capture_rect{ 0,0,0,0 }; // 最终要截取的区域
    RECT wnd_rect; // 窗口区域
    RECT real_rect; // 真实的窗口区域,实际上也不是百分百准确

    if (wnd_info.hwnd) {
      ::GetWindowRect(wnd_info.hwnd, &wnd_rect);
      DwmGetWindowAttribute(wnd_info.hwnd, DWMWINDOWATTRIBUTE::DWMWA_EXTENDED_FRAME_BOUNDS, &real_rect, sizeof(RECT));
      int offset_left = real_rect.left - wnd_rect.left;
      int offset_top = real_rect.top - wnd_rect.top;
      capture_rect = RECT{ offset_left,offset_top,real_rect.right - real_rect.left + offset_left,real_rect.bottom - real_rect.top + offset_top };
    } else {
      capture_rect = wnd_info.rect;
    }

    int width = capture_rect.right - capture_rect.left;
    int height = capture_rect.bottom - capture_rect.top;

    HDC hMemDC = CreateCompatibleDC(hWndDC);
    HBITMAP hBitmap = CreateCompatibleBitmap(hWndDC, width, height);
    SelectObject(hMemDC, hBitmap);

    BitmapPtr bitmap;
    // 获取指定区域的rgb数据
    bool ok = BitBlt(hMemDC, 0, 0, width, height, hWndDC, capture_rect.left, capture_rect.top, SRCCOPY);
    // hBitmap就是得到的图片对象,转GDI的Bitmap进行保存
    if (ok) bitmap = std::make_shared<Gdiplus::Bitmap>(hBitmap, nullptr);

    DeleteDC(hWndDC);
    DeleteDC(hMemDC);
    DeleteObject(hBitmap);

    return bitmap;
  }
};
测试用例
namespace TestCase
{
  void Run()
  {
    std::vector<WindowInfo> window_vec; // 用来保存窗口信息
    // 枚举显示器
    Enumerator::EnumMonitor([&window_vec](const WindowInfo &wnd_info)
                            {
                              window_vec.push_back(wnd_info);
                            });
    // 计算生成所有屏幕加在一起的区域大小
    if (window_vec.size() > 0) { // 也可大于1,这样只有一个显示器时不会显示全屏选项
      int width = 0, height = 0;
      for (const auto &wnd_info : window_vec) {
        width += wnd_info.rect.right - wnd_info.rect.left;
        int h = wnd_info.rect.bottom - wnd_info.rect.top;
        if (h > height) height = h; // 高度可能不一样,需要以最高的为准
      }
      WindowInfo wnd_info{ nullptr, "FullScreen", { 0, 0, width, height} };
      window_vec.push_back(wnd_info);
    }
    // 枚举窗口
    Enumerator::EnumWindow([&window_vec](const WindowInfo &wnd_info)
                           {
                             window_vec.push_back(wnd_info);
                           });
    // 示例: 遍历找到的所有窗口,将每一个都截图到指定路径,文件夹需存在,程序不会自己创建文件夹
    int cnt = 1;
    for (const auto &window : window_vec) {
      printf("%2d. %s\n", cnt, window.desc.c_str());
      auto bitmap = WindowCapture::Capture(window);
      if (bitmap) GdiplusUtil::SaveBitmapAsPng(bitmap, std::to_string(cnt) + ".png");
      ++cnt;
    }
  }
}

运行图就不贴了,自己下载代码运行一下即可。

一点小坑
  1. 截图窗口时,对于vs全屏这种,获取的窗口区域偏大,四周都会多8px。但是想idea或者浏览器这种又是正确的,暂时没找到办法解决
  2. 截图资源管理器窗口时,标题栏背景是透明的,暂时不知道原因

这两点影响还是比较小,大部分情况下是正常的。如果你解决了的话,请评论告诉我一下,谢谢!

结束语

码字和代码不易,如果对你有用就点个赞吧!谢谢!

完整代码

代码下载-CSDN(免费)

参考资料
  • 屏幕截图示例

你可能感兴趣的:(C++,有趣的应用,c++)