如何在Windows系统上进行截图并保存为JPEG格式?

16

我正在尝试找到一种(相对)简单的方法,在窗口上截取屏幕并将生成的HBITMAP保存为JPEG。这里棘手的部分在于,由于代码是用C编写的,所以我不能使用GDI+,而且由于代码是更大程序的一个模块,因此我也不能使用外部库(如libjpeg)。

这段代码截取屏幕并返回HBITMAP。将该位图保存到文件中很容易。问题在于该位图大小为2或3MB。

HDC hDCMem = CreateCompatibleDC(NULL);
HBITMAP hBmp;
RECT rect;
HDC hDC;
HGDIOBJ hOld;    

GetWindowRect(hWnd, & rect);

hBmp = NULL;

{
    hDC = GetDC(hWnd);
    hBmp = CreateCompatibleBitmap(hDC, rect.right - rect.left, rect.bottom - rect.top);
    ReleaseDC(hWnd, hDC);
}

hOld = SelectObject(hDCMem, hBmp);
SendMessage(hWnd, WM_PRINT, (WPARAM) hDCMem, PRF_CHILDREN | PRF_CLIENT | PRF_ERASEBKGND | PRF_NONCLIENT | PRF_OWNED);

SelectObject(hDCMem, hOld);
DeleteObject(hDCMem);

return hBmp;

有没有关于如何做到这一点的想法? 非常感谢,任何帮助都受到赞赏。

编辑: 由于我们走向了GDI+的方向,我想发布一下使用C++可以获取屏幕截图并使用GDI+将其转换为JPEG的代码。如果有人知道如何使用FLAT GDI+来实现这一点,我会非常感激您的帮助。 代码:

    #include <windows.h>
#include <stdio.h>
#include <gdiplus.h>

using namespace Gdiplus;


int GetEncoderClsid(WCHAR *format, CLSID *pClsid)
{
    unsigned int num = 0,  size = 0;
    GetImageEncodersSize(&num, &size);
    if(size == 0) return -1;
    ImageCodecInfo *pImageCodecInfo = (ImageCodecInfo *)(malloc(size));
    if(pImageCodecInfo == NULL) return -1;
    GetImageEncoders(num, size, pImageCodecInfo);
    for(unsigned int j = 0; j < num; ++j)
    {
        if(wcscmp(pImageCodecInfo[j].MimeType, format) == 0){
            *pClsid = pImageCodecInfo[j].Clsid;
            free(pImageCodecInfo);
            return j;
        }    
    }
    free(pImageCodecInfo);
    return -1;
}

int GetScreeny(LPWSTR lpszFilename, ULONG uQuality) // by Napalm
{
    ULONG_PTR gdiplusToken;
    GdiplusStartupInput gdiplusStartupInput;
    GdiplusStartup(&gdiplusToken, &gdiplusStartupInput, NULL);
    HWND hMyWnd = GetForegroundWindow(); // get my own window
    RECT  r;                             // the area we are going to capture 
    int w, h;                            // the width and height of the area
    HDC dc;                              // the container for the area
    int nBPP;
    HDC hdcCapture;
    LPBYTE lpCapture;
    int nCapture;
    int iRes;
    CLSID imageCLSID;
    Bitmap *pScreenShot;
    HGLOBAL hMem;
    int result;

    // get the area of my application's window  
    //GetClientRect(hMyWnd, &r);
    GetWindowRect(hMyWnd, &r);
    dc = GetWindowDC(hMyWnd);//   GetDC(hMyWnd) ;
    w = r.right - r.left;
    h = r.bottom - r.top;
    nBPP = GetDeviceCaps(dc, BITSPIXEL);
    hdcCapture = CreateCompatibleDC(dc);


    // create the buffer for the screenshot
    BITMAPINFO bmiCapture = {
          sizeof(BITMAPINFOHEADER), w, -h, 1, nBPP, BI_RGB, 0, 0, 0, 0, 0,
    };

    // create a container and take the screenshot
    HBITMAP hbmCapture = CreateDIBSection(dc, &bmiCapture,
        DIB_PAL_COLORS, (LPVOID *)&lpCapture, NULL, 0);

    // failed to take it
    if(!hbmCapture)
    {
        DeleteDC(hdcCapture);
        DeleteDC(dc);
        GdiplusShutdown(gdiplusToken);
        printf("failed to take the screenshot. err: %d\n", GetLastError());
        return 0;
    }

    // copy the screenshot buffer
    nCapture = SaveDC(hdcCapture);
    SelectObject(hdcCapture, hbmCapture);
    BitBlt(hdcCapture, 0, 0, w, h, dc, 0, 0, SRCCOPY);
    RestoreDC(hdcCapture, nCapture);
    DeleteDC(hdcCapture);
    DeleteDC(dc);

    // save the buffer to a file    
    pScreenShot = new Bitmap(hbmCapture, (HPALETTE)NULL);
    EncoderParameters encoderParams;
    encoderParams.Count = 1;
    encoderParams.Parameter[0].NumberOfValues = 1;
    encoderParams.Parameter[0].Guid  = EncoderQuality;
    encoderParams.Parameter[0].Type  = EncoderParameterValueTypeLong;
    encoderParams.Parameter[0].Value = &uQuality;
    GetEncoderClsid(L"image/jpeg", &imageCLSID);
    iRes = (pScreenShot->Save(lpszFilename, &imageCLSID, &encoderParams) == Ok);
    delete pScreenShot;
    DeleteObject(hbmCapture);
    GdiplusShutdown(gdiplusToken);
    return iRes;

}

它真的必须是JPG格式吗?如果PNG格式可以的话,你可以使用libpng。 - sean riley
是的,必须使用JPG格式。PNG对于我所需的内容来说不够小。此外,我不能使用外部或第三方库。libpng很大,并且它添加了很多我不需要的额外代码(我已经尝试过了)。 感谢您的回答。 - wonderer
JPG不是截图的好选择。对于照片等内容,它表现得更好,但对于截图来说,PNG提供了更好的结果。您可以通过缩小比例或减少颜色位深度来使其更小。 - Andriy Tylychko
11个回答

19

好的,在经过了很多努力之后,这里是答案:

int SaveJpeg(HBITMAP hBmp, LPCWSTR lpszFilename, ULONG uQuality)
{
    ULONG *pBitmap = NULL;
    CLSID imageCLSID;
    EncoderParameters encoderParams;
    int iRes = 0;

    typedef Status (WINAPI *pGdipCreateBitmapFromHBITMAP)(HBITMAP, HPALETTE, ULONG**);
    pGdipCreateBitmapFromHBITMAP lGdipCreateBitmapFromHBITMAP;

    typedef Status (WINAPI *pGdipSaveImageToFile)(ULONG *, const WCHAR*, const CLSID*, const EncoderParameters*);
    pGdipSaveImageToFile lGdipSaveImageToFile;

    // load GdipCreateBitmapFromHBITMAP
    lGdipCreateBitmapFromHBITMAP = (pGdipCreateBitmapFromHBITMAP)GetProcAddress(hModuleThread, "GdipCreateBitmapFromHBITMAP");
    if(lGdipCreateBitmapFromHBITMAP == NULL)
    {
        // error
        return 0;
    }

    // load GdipSaveImageToFile
    lGdipSaveImageToFile = (pGdipSaveImageToFile)GetProcAddress(hModuleThread, "GdipSaveImageToFile");
    if(lGdipSaveImageToFile == NULL)
    {
        // error
        return 0;
    }

        lGdipCreateBitmapFromHBITMAP(hBmp, NULL, &pBitmap);

       iRes = GetEncoderClsid(L"image/jpeg", &imageCLSID);
       if(iRes == -1)
    {
        // error
        return 0;
    }
    encoderParams.Count = 1;
    encoderParams.Parameter[0].NumberOfValues = 1;
    encoderParams.Parameter[0].Guid  = EncoderQuality;
    encoderParams.Parameter[0].Type  = EncoderParameterValueTypeLong;
    encoderParams.Parameter[0].Value = &uQuality;

    lGdipSaveImageToFile(pBitmap, lpszFilename, &imageCLSID, &encoderParams);


    return 1;
}
  • hModuleThread是什么?请在这里查看。您可以使用GetModuleHandle()替换。

  • GetEncoderClsid是什么?请在这里查看。

现在的问题是,我如何将编码后的pBitmap(作为JPEG)保存到BYTE缓冲区中?


6

翻译为平面 GDI+ API 是相当直接的:

void SaveJpeg(HBITMAP hBmp, LPCWSTR lpszFilename, ULONG uQuality)
{
    GpBitmap* pBitmap;
    GdipCreateBitmapFromHBITMAP(hBmp, NULL, &pBitmap);

    CLSID imageCLSID;
    GetEncoderClsid(L"image/jpeg", &imageCLSID);

    EncoderParameters encoderParams;
    encoderParams.Count = 1;
    encoderParams.Parameter[0].NumberOfValues = 1;
    encoderParams.Parameter[0].Guid  = EncoderQuality;
    encoderParams.Parameter[0].Type  = EncoderParameterValueTypeLong;
    encoderParams.Parameter[0].Value = &uQuality;

    GdipSaveImageToFile(pBitmap, lpszFilename, &imageCLSID, &encoderParams);
}

唯一不明显的是由GdipCreateBitmapFromHBITMAP()创建的GpBitmap的清理。 Gdiplus :: Bitmap类似乎没有析构函数,因此不会对其内部的GpBitmap执行任何操作。此外,没有像其他GDI +对象那样的GdipDeleteBitmap()。因此,我不确定需要做什么才能避免泄漏。

编辑: 此代码未解决Microsoft提供的GDI +头文件在C ++命名空间中声明所有必要函数的问题。 一种解决方案是将必要的声明复制(并根据需要转换为C代码)到您自己的头文件中。 另一个可能性是使用由WineMono项目提供的标头。 它们似乎都更适合作为C代码进行编译。


谢谢,让我试试看。 你包含了哪些头文件才能编译这个程序? - wonderer
是的,我无法编译这个程序,因为所有基于GDI的函数都找不到了。例如:GpBitmap。 添加#include <Gdiplusflat.h>会让情况变得更糟。我错过了什么? - wonderer
这就是问题所在。我无法将C++运行时引入我的模块,因此无法编译任何C++代码。 - wonderer
好的,你能把头文件发送到我的电子邮件吗?wonderer_isr@yahoo.com - wonderer
请查看回复以查看此问题的解决方案。 - wonderer
显示剩余4条评论

4
一种可能性是:如果您无法修改此程序以保存为Jpeg格式,请编写第二个程序,使用C# / GDI+ /其他高级技术来监视保存目录,并将保存的BMP文件转换为Jpeg格式。
如果您无法这样做,独立Jpeg组自20世纪末就提供了纯C Jpeg代码:这里有一个非常简洁的网页。

谢谢。那个库的问题在于它太大了。无论如何,我不能使用外部库。 - wonderer

3
在你的代码开头尝试这个:
#include "windows.h"
#include "gdiplus.h"
using namespace Gdiplus;
using namespace Gdiplus::DllExports;

而且GdipSaveImageToFile()可能会在c++中编译通过。

在纯C中,最好的方法可能是尝试制作单个包含文件。查找“gdiplus.h”中的函数声明,并为每个不包含命名空间的函数添加最小的包含文件,例如对于 GdipSaveImageToFile(),可以添加#include "Gdiplusflat.h"


不要定义WIN32_LEAN_AND_MEAN ;) - Puddle

0

我曾经遇到过同样的问题,并通过这段代码解决了它。 您还需要在链接器的输入中包含gdiplus.lib。

struct GdiplusStartupInput
{
    UINT32 GdiplusVersion;              // Must be 1  (or 2 for the Ex version)
    UINT_PTR    DebugEventCallback;         // Ignored on free builds
    BOOL SuppressBackgroundThread;      // FALSE unless you're prepared to call the hook/unhook functions properly
    BOOL SuppressExternalCodecs;        // FALSE unless you want GDI+ only to use its internal image codecs. 
};
int __stdcall GdiplusStartup(ULONG_PTR *token, struct GdiplusStartupInput *gstart, struct GdiplusStartupInput *gstartout);
int __stdcall GdiplusShutdown(ULONG_PTR token);
int __stdcall GdipCreateBitmapFromFile(WCHAR *filename, void **GpBitmap);
int __stdcall GdipSaveImageToFile(void *GpBitmap, WCHAR *filename, CLSID *encoder, void *params);
int __stdcall GdipCreateBitmapFromStream(IStream *pstm, void **GpBitmap);
int __stdcall GdipCreateHBITMAPFromBitmap(void *GpBitmap, HBITMAP *bm, COLORREF col);
int __stdcall GdipCreateBitmapFromHBITMAP(HBITMAP bm, HPALETTE hpal, void *GpBitmap);
int __stdcall GdipDisposeImage(void *GpBitmap);
int __stdcall GdipImageGetFrameDimensionsCount(void *GpBitmap, int *count);
int __stdcall GdipImageGetFrameDimensionsList(void *GpBitmap, GUID *guid, int count);
int __stdcall GdipImageGetFrameCount(void *GpBitmap, GUID *guid, int *count);
int __stdcall GdipImageSelectActiveFrame(void *GpBitmap, GUID *guid, int num);
int __stdcall GdipImageRotateFlip(void *GpBitmap, int num);

/*
    Save the image memory bitmap to a file (.bmp, .jpg, .png or .tif)
*/
int save_bitmap_to_file(char *filename, HBITMAP hbitmap)
{
    wchar_t wstr[MAX_FILENAME];
    int     sts;
    size_t  len;
    void    *imageptr;          /* actually an 'image' object pointer */
    CLSID   encoder;

    sts = FAILURE;
    _strlwr(filename);
    if(strstr(filename, ".png"))
        sts = CLSIDFromString(L"{557cf406-1a04-11d3-9a73-0000f81ef32e}", &encoder);     /* png encoder classid */
    else if(strstr(filename, ".jpg"))
        sts = CLSIDFromString(L"{557cf401-1a04-11d3-9a73-0000f81ef32e}", &encoder);     /* jpg encoder classid */
    else if(strstr(filename, ".jpeg"))
        sts = CLSIDFromString(L"{557cf401-1a04-11d3-9a73-0000f81ef32e}", &encoder);     /* jpg encoder classid */
    else if(strstr(filename, ".tif"))
        sts = CLSIDFromString(L"{557cf405-1a04-11d3-9a73-0000f81ef32e}", &encoder);     /* tif encoder classid */
    else if(strstr(filename, ".bmp"))
        sts = CLSIDFromString(L"{557cf400-1a04-11d3-9a73-0000f81ef32e}", &encoder);     /* bmp encoder classid */
    else if(strstr(filename, ".gif"))
        sts = CLSIDFromString(L"{557cf402-1a04-11d3-9a73-0000f81ef32e}", &encoder);     /* gif encoder classid */
    if(sts != 0) return(FAILURE);

    mbstowcs_s(&len, wstr, MAX_FILENAME, filename, MAX_FILENAME-1);     /* get filename in multi-byte format */
    sts = GdipCreateBitmapFromHBITMAP(hbitmap, NULL, &imageptr);
    if(sts != 0) return(FAILURE);

    sts = GdipSaveImageToFile(imageptr, wstr, &encoder, NULL);

    GdipDisposeImage(imageptr);                 /* destroy the 'Image' object */
    return(sts);
}

通过为jpg文件输出添加质量值,可以增强此代码的功能,执行该操作的代码在本主题中较高位置。


0

如果你真的很关心空间,你可能也可以放弃 C 运行时库。如果你只使用了一些函数,那就自己写一个版本(例如 strcpy)。有可能编写只有几 K 字节大小的 Windows 应用程序。我可以给你发送一个小的示例应用程序来演示。

如果我将我的 C 图像处理代码精简到只有 JPEG 编码器,它大约会产生 20K 的代码(如果不需要支持灰度图像,则更少)。


谢谢,但是你可以看到我已经自己回答了这个问题。 - wonderer
我不明白你是如何自己回答了你的问题。你有解决方案吗?你有预算吗?这两个问题的答案决定了你是否能得到最佳解决方案。 - BitBank
也许你误解了,认为我在提到 GPL 代码。 - BitBank

0

在C代码中创建JPEG,您可能需要使用外部库--不会有标准库函数来完成此操作。当然,您也可以研究文件格式并编写一个基于标准的生成函数。您可以从维基百科开始。如果您真的不能使用外部库,您可以阅读相关库函数以了解如何生成自己的JPEG。但这听起来比仅使用库要多做很多工作。

此外,我认为库的大小在C中并不重要--我认为您只能获得从该库调用的函数的大小(我想是这样的)。当然,如果所有函数都紧密耦合,那么它将变得非常庞大。


我尝试的第一件事是使用libjpg和其他第三方库。 无论我从这些库中使用什么,它都会导入整个库。这使得我的代码非常庞大。 - wonderer
我正在阅读关于C版本的GDI+(称为flat GDI+)的内容,有人对此有经验吗?如果是肯定的话,我确实有所有的代码来截取屏幕截图,将其编码为JPEG格式,并将其保存到文件或缓冲区中。 - wonderer

0

好的图像压缩很难。常见格式有库代码存在的原因。需要大量的代码来实现。你对问题的约束听起来似乎没有实际解决方案。

可以尝试以下几个方法:

  1. 使用每像素8位的BMP,而不是真彩色BMP。当然,这假设颜色映射中的信息损失是可接受的。相比24位BMP,这将使文件大小缩小三倍。

  2. 尝试在编写的BMP中使用游程编码。RLE是BMP的一种文档化格式,应该有很多示例代码可供使用。它最适合于具有大量相同颜色长行的图像。没有太多渐变和填充的典型屏幕符合该描述。

  3. 如果这些方法都不足以满足需求,那么您可能需要采用更强的压缩方式。 libjpeg的源代码可以轻松获得,并且可以静态链接它而不是使用DLL。需要花费一些精力来配置您需要的部分,但压缩和解压缩代码几乎完全独立于彼此,这将将库几乎分成两半。


谢谢。我会尝试RLE。 我正在阅读关于GDI+的C版本(称为flat GDI+),有人有相关经验吗? 如果是肯定的,我有所有的代码来截屏,将其编码为JPEG并将其保存到文件或缓冲区中。 - wonderer
GDI+的网站链接是http://msdn.microsoft.com/en-us/library/ms533969(VS.85).aspx - wonderer

0

你看过FreeImage Project吗?是的,它是一个外部库,但它非常小(约1Mb)。它可以做任何你想要的事情,包括图像处理。即使不是为了这个项目,也绝对值得了解。


抱歉,正如我所说,我不能使用外部库。我的模块已经有200k了,加上那个库会变得非常庞大。谢谢。 - wonderer
明白了。我看到了那个评论,但是后来也看到了一些开放的想法。唉,也许另一个项目吧... - Karl E. Peterson

0

很遗憾,我的代码不是GPL,因此我无法使用他们的GPL代码。话虽如此,我看了一下他们的代码。由于它非常混乱,你不能单独提取一个函数而不必带上大量的代码,所以几乎不可能不包含整个库。 非常感谢您的评论。 - wonderer

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接