[Win32] python纯ctypes获取exe图标

前言和引用

我有个程序有获取图标的需求,但是我不想使用pywin32这个大家伙,于是找上了同样可以调用winapi的ctypes。
以下是参考链接:
https://www.zhihu.com/question/425053417
https://www.cnblogs.com/ibingshan/p/11057390.html
mss.windows 模块

最初的代码

从zhihu和cnblogs找到了使用pywin32和ctypes的两个例子:

# zhihu
作者:lollipopnougat
链接:https://www.zhihu.com/question/425053417/answer/1524323338
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

import ctypes
from ctypes.wintypes import HICON, LPCSTR, UINT, INT
import clr
import os

# 简写
ExIcon = ctypes.windll.user32.PrivateExtractIconsA
DesIcon = ctypes.windll.user32.DestroyIcon

# 加载 .Net Drawing 程序集
clr.AddReference('System.Drawing')

from System import IntPtr
from System.Drawing import Icon
from System.Drawing.Imaging import ImageFormat
from System import Int32

# path: 资源文件(可以是exe、dll等)绝对路径, out_dir: 导出图标文件夹绝对路径 返回实际导出的图标数
def save_icons_from_resfile(path: str, out_dir: str):
    # 输出文件夹是否存在
    if not os.path.exists(out_dir):
        return -1
    
    # 指定 PrivateExtractIconsA 返回类型 UINT
    ExIcon.restype = UINT

    # 获取内含图标总数,注意path需要转bytes类型
    icon_total_count = ExIcon(path.encode(), 0, 0, 0, None, None, 0, 0)
    
    # 指定 PrivateExtractIconsA 参数类型
    ExIcon.argtypes = [
        LPCSTR, INT, INT, INT,
        ctypes.POINTER(HICON * icon_total_count),
        ctypes.POINTER(UINT * icon_total_count), UINT,
        UINT
    ]

    # 初始化 Ctypes HICON 数组(存储导出的图标句柄)
    hIconArray = HICON * icon_total_count
    hicons = hIconArray()
    # 获取数组指针
    p_hicons = ctypes.pointer(hicons)

    # 初始化 Ctypes UINT 数组(存储导出的图标ID)
    IDArray = UINT * icon_total_count
    ids = IDArray()
    p_ids = ctypes.pointer(ids)

    # 导出最大的图标(icon最大支持256*256),返回值为成功导出的图标数
    success_count = ExIcon(path.encode(), 0, 256, 256, p_hicons, p_ids, icon_total_count, 0)
    
    # 实际图标数
    actual_count = 0
    for i in range(success_count):
        # 如果图标句柄是NULL
        if hicons[i] == 0:
            continue
        actual_count += 1
        
        # .Net 类库解决...幸好之前写过C#解决类似问题的程序
        hicon = IntPtr.Add(IntPtr.Zero, hicons[i])
        ico = Icon.FromHandle(hicon)
        mybitmap = ico.ToBitmap()
        cs_id = Int32(ids[i])
        mybitmap.Save(out_dir + '\\' + str(cs_id.ToString('000')) + '.ico', ImageFormat.Icon)

        # 用完后记得销毁 ICON 对象(句柄)
        DesIcon.argtypes = [HICON]
        hicon_c = HICON(hicons[i])
        DesIcon(hicon_c)

    return actual_count
#cnblogs
import win32ui
import win32gui
import win32con
import win32api

#ico_x = win32api.GetSystemMetrics(win32con.SM_CXICON)
#ico_y = win32api.GetSystemMetrics(win32con.SM_CYICON)
ico_x = 32
ico_y = 32

exePath = "c:/windows/system32/shell32.dll"

large, small = win32gui.ExtractIconEx(exePath, 0)
useIcon = large[0]
destroyIcon = small[0]
win32gui.DestroyIcon(destroyIcon)

hdc = win32ui.CreateDCFromHandle(win32gui.GetDC(0))
hbmp = win32ui.CreateBitmap()
hbmp.CreateCompatibleBitmap(hdc, ico_x, ico_x)
hdc = hdc.CreateCompatibleDC()

hdc.SelectObject(hbmp)
hdc.DrawIcon((0,0), useIcon)
savePath = "d:/test.bmp"
hbmp.SaveBitmapFile(hdc, savePath)

本来lollipopnougat的已经很好了,但他不能使用winapi保存,使用了.net类库,但我不想这样。
随后我以lollipopnougat为母本,以cnblogs为参考,开始了改进。

改进思路

我曾经分享过python截图库mss(文章)其中mss.windows可以实现纯win32api获取raw数据并保存。那么我们能否借用一下呢?mss github上面写了,mss是以MIT许可发布的,这意味着我们可以使用它的代码。
在Mss._grab_impl函数中,我们能看到gdi32的BitBlt和GetDIBits,用这些函数可以得到。将mss中有关代码复制下来:

from ctypes import POINTER, Structure, WINFUNCTYPE, c_void_p
from ctypes.wintypes import (
    BOOL,
    DOUBLE,
    DWORD,
    HBITMAP,
    HDC,
    HGDIOBJ,
    HWND,
    INT,
    LONG,
    LPARAM,
    RECT,
    UINT,
    WORD,
)
CAPTUREBLT = 0x40000000
DIB_RGB_COLORS = 0
SRCCOPY = 0x00CC0020


class BITMAPINFOHEADER(Structure):
    """ Information about the dimensions and color format of a DIB. """

    _fields_ = [
        ("biSize", DWORD),
        ("biWidth", LONG),
        ("biHeight", LONG),
        ("biPlanes", WORD),
        ("biBitCount", WORD),
        ("biCompression", DWORD),
        ("biSizeImage", DWORD),
        ("biXPelsPerMeter", LONG),
        ("biYPelsPerMeter", LONG),
        ("biClrUsed", DWORD),
        ("biClrImportant", DWORD),
    ]


class BITMAPINFO(Structure):
    """
    Structure that defines the dimensions and color information for a DIB.
    """

    _fields_ = [("bmiHeader", BITMAPINFOHEADER), ("bmiColors", DWORD * 3)]
# ......
def __init__(self, **_):
        # type: (Any) -> None
        """ Windows initialisations. """

        super().__init__()

        self.user32 = ctypes.WinDLL("user32")
        self.gdi32 = ctypes.WinDLL("gdi32")
        self._set_cfunctions()
        self._set_dpi_awareness()

        self._bbox = {"height": 0, "width": 0}
        self._data = ctypes.create_string_buffer(0)  # type: ctypes.Array[ctypes.c_char]

        srcdc = self._get_srcdc()
        if not MSS.memdc:
            MSS.memdc = self.gdi32.CreateCompatibleDC(srcdc)

        bmi = BITMAPINFO()
        bmi.bmiHeader.biSize = ctypes.sizeof(BITMAPINFOHEADER)
        bmi.bmiHeader.biPlanes = 1  # Always 1
        bmi.bmiHeader.biBitCount = 32  # See grab.__doc__ [2]
        bmi.bmiHeader.biCompression = 0  # 0 = BI_RGB (no compression)
        bmi.bmiHeader.biClrUsed = 0  # See grab.__doc__ [3]
        bmi.bmiHeader.biClrImportant = 0  # See grab.__doc__ [3]
        self._bmi = bmi
def _get_srcdc(self):
        """
        Retrieve a thread-safe HDC from GetWindowDC().
        In multithreading, if the thread who creates *srcdc* is dead, *srcdc* will
        no longer be valid to grab the screen. The *srcdc* attribute is replaced
        with *_srcdc_dict* to maintain the *srcdc* values in multithreading.
        Since the current thread and main thread are always alive, reuse their *srcdc* value first.
        """
        cur_thread, main_thread = threading.current_thread(), threading.main_thread()
        srcdc = MSS._srcdc_dict.get(cur_thread) or MSS._srcdc_dict.get(main_thread)
        if not srcdc:
            srcdc = MSS._srcdc_dict[cur_thread] = self.user32.GetWindowDC(0)
        return srcdc
def _grab_impl(self, monitor):
        # type: (Monitor) -> ScreenShot
        """
        Retrieve all pixels from a monitor. Pixels have to be RGB.

        In the code, there are few interesting things:

        [1] bmi.bmiHeader.biHeight = -height

        A bottom-up DIB is specified by setting the height to a
        positive number, while a top-down DIB is specified by
        setting the height to a negative number.
        https://msdn.microsoft.com/en-us/library/ms787796.aspx
        https://msdn.microsoft.com/en-us/library/dd144879%28v=vs.85%29.aspx


        [2] bmi.bmiHeader.biBitCount = 32
            image_data = create_string_buffer(height * width * 4)

        We grab the image in RGBX mode, so that each word is 32bit
        and we have no striding.
        Inspired by https://github.com/zoofIO/flexx


        [3] bmi.bmiHeader.biClrUsed = 0
            bmi.bmiHeader.biClrImportant = 0

        When biClrUsed and biClrImportant are set to zero, there
        is "no" color table, so we can read the pixels of the bitmap
        retrieved by gdi32.GetDIBits() as a sequence of RGB values.
        Thanks to http://stackoverflow.com/a/3688682
        """

        srcdc, memdc = self._get_srcdc(), MSS.memdc
        width, height = monitor["width"], monitor["height"]

        if (self._bbox["height"], self._bbox["width"]) != (height, width):
            self._bbox = monitor
            self._bmi.bmiHeader.biWidth = width
            self._bmi.bmiHeader.biHeight = -height  # Why minus? [1]
            self._data = ctypes.create_string_buffer(width * height * 4)  # [2]
            if MSS.bmp:
                self.gdi32.DeleteObject(MSS.bmp)
            MSS.bmp = self.gdi32.CreateCompatibleBitmap(srcdc, width, height)
            self.gdi32.SelectObject(memdc, MSS.bmp)

        self.gdi32.BitBlt(
            memdc,
            0,
            0,
            width,
            height,
            srcdc,
            monitor["left"],
            monitor["top"],
            SRCCOPY | CAPTUREBLT,
        )
        bits = self.gdi32.GetDIBits(
            memdc, MSS.bmp, 0, height, self._data, self._bmi, DIB_RGB_COLORS
        )
        if bits != height:
            raise ScreenShotError("gdi32.GetDIBits() failed.")

        return self.cls_image(bytearray(self._data), monitor)

进行修改

接近成功

如果你按照上面的一步步来,应该能得到下面的代码

import ctypes
from ctypes.wintypes import HICON, LPCSTR, UINT, INT

ExIcon = ctypes.windll.user32.PrivateExtractIconsA
DesIcon = ctypes.windll.user32.DestroyIcon

from ctypes import POINTER, Structure, WINFUNCTYPE, c_void_p
from ctypes.wintypes import (
    BOOL,
    DOUBLE,
    DWORD,
    HBITMAP,
    HDC,
    HGDIOBJ,
    HWND,
    INT,
    LONG,
    LPARAM,
    RECT,
    UINT,
    WORD,
)

SRCCOPY = 0x00CC0020

user32 = ctypes.windll.user32
gdi32  = ctypes.windll.gdi32


class BITMAPINFOHEADER(Structure):
    " From mss.windows "

    _fields_ = [
        ("biSize", DWORD),
        ("biWidth", LONG),
        ("biHeight", LONG),
        ("biPlanes", WORD),
        ("biBitCount", WORD),
        ("biCompression", DWORD),
        ("biSizeImage", DWORD),
        ("biXPelsPerMeter", LONG),
        ("biYPelsPerMeter", LONG),
        ("biClrUsed", DWORD),
        ("biClrImportant", DWORD),
    ]


class BITMAPINFO(Structure):
    " From mss.windows "

    _fields_ = [("bmiHeader", BITMAPINFOHEADER), ("bmiColors", DWORD * 3)]

def get(path):
    path = os.path.abspath(path)
    # From zhihu
    width = height = size

    ExIcon.restype = UINT

    icon_total_count = ExIcon(path.encode(), 0, 0, 0, None, None, 0, 0)

    ExIcon.argtypes = [
        LPCSTR, INT, INT, INT,
        ctypes.POINTER(HICON * icon_total_count),
        ctypes.POINTER(UINT * icon_total_count), UINT,
        UINT
    ]

    hIconArray = HICON * icon_total_count
    hicons = hIconArray()

    p_hicons = ctypes.pointer(hicons)

    IDArray = UINT * icon_total_count
    ids = IDArray()
    p_ids = ctypes.pointer(ids)

    success_count = ExIcon(path.encode(), 0, 32, 32, p_hicons, p_ids, icon_total_count, 0)
    # From mss.windows
    srcdc = user32.GetWindowDC(0)
    memdc = gdi32.CreateCompatibleDC(srcdc)

    bmp = gdi32.CreateCompatibleBitmap(srcdc, width, height)
    gdi32.SelectObject(memdc,bmp)

    bmi = BITMAPINFO()
    bmi.bmiHeader.biSize = ctypes.sizeof(BITMAPINFOHEADER)
    bmi.bmiHeader.biPlanes = 1  # Always 1
    bmi.bmiHeader.biBitCount = 32  # See grab.__doc__ [2]
    bmi.bmiHeader.biCompression = 0  # 0 = BI_RGB (no compression)
    bmi.bmiHeader.biClrUsed = 0  # See grab.__doc__ [3]
    bmi.bmiHeader.biClrImportant = 0  # See grab.__doc__ [3]

    bmi.bmiHeader.biWidth = width
    bmi.bmiHeader.biHeight = -height  # Why minus? [1]

    data = ctypes.create_string_buffer(width * height * 4)

    self.gdi32.BitBlt(
            memdc,
            0,
            0,
            width,
            height,
            srcdc,
            0,
            0,
            SRCCOPY | CAPTUREBLT,
        )

    bits = gdi32.GetDIBits(
            memdc, bmp, 0, height, data, ctypes.byref(bmi), 0
        )
    gdi32.DeleteObject(bmp)

    if bits != height:
        raise Exception("gdi32.GetDIBits() failed.")

    return bytearray(data)
def rgb(raw,width=32,height=32):
    # From mss.screenshot
    rgb = bytearray(height * width * 3)
    rgb[0::3] = raw[2::4]
    rgb[1::3] = raw[1::4]
    rgb[2::3] = raw[0::4]
    return bytes(rgb)

# 测试
data = rgb(get("C:\\windows\\explorer.exe",),32,32)
import mss.tools
mss.tools.to_png(data,(32,32),8,"explorer.png")

运行一下试试,你会发现,有问题!截取的是屏幕左上角32x32像素的内容!

最后解决

出问题以后,我研究了2个多小时,从cnblogs中的代码得到启发:BitBlt是截屏,不能使用;DrawIcon才是重点。hicons中的数据是DrawIcon中被画近bmp的数据。
有了这个结论,就可以得到以下代码(已优化,可之间使用):

"""
use ctypes to get image from exe/dll
ref:
  https://www.zhihu.com/question/425053417
  https://www.cnblogs.com/ibingshan/p/11057390.html
  mss.windows (mss grab library)
"""
import platform, os
system = platform.system().lower()
if system != "windows" and os.environ["IGNORE_SYSTEMCHECK"] != "True":
    raise Exception("GETICON is only avaliable on Windows!")

import ctypes
from ctypes.wintypes import HICON, LPCSTR, UINT, INT

ExIcon = ctypes.windll.user32.PrivateExtractIconsA
DesIcon = ctypes.windll.user32.DestroyIcon

from ctypes import POINTER, Structure, WINFUNCTYPE, c_void_p
from ctypes.wintypes import (
    BOOL,
    DOUBLE,
    DWORD,
    HBITMAP,
    HDC,
    HGDIOBJ,
    HWND,
    INT,
    LONG,
    LPARAM,
    RECT,
    UINT,
    WORD,
)

SRCCOPY = 0x00CC0020

user32 = ctypes.windll.user32
gdi32  = ctypes.windll.gdi32


class BITMAPINFOHEADER(Structure):
    " From mss.windows "

    _fields_ = [
        ("biSize", DWORD),
        ("biWidth", LONG),
        ("biHeight", LONG),
        ("biPlanes", WORD),
        ("biBitCount", WORD),
        ("biCompression", DWORD),
        ("biSizeImage", DWORD),
        ("biXPelsPerMeter", LONG),
        ("biYPelsPerMeter", LONG),
        ("biClrUsed", DWORD),
        ("biClrImportant", DWORD),
    ]


class BITMAPINFO(Structure):
    " From mss.windows "

    _fields_ = [("bmiHeader", BITMAPINFOHEADER), ("bmiColors", DWORD * 3)]

def rgb(raw,width=32,height=32):
    # From mss.screenshot
    rgb = bytearray(height * width * 3)
    rgb[0::3] = raw[2::4]
    rgb[1::3] = raw[1::4]
    rgb[2::3] = raw[0::4]
    return bytes(rgb)

def get_raw_data(path,index=0,size=32):
    path = os.path.abspath(path)
    # From zhihu
    width = height = size

    ExIcon.restype = UINT

    icon_total_count = ExIcon(path.encode(), 0, 0, 0, None, None, 0, 0)

    ExIcon.argtypes = [
        LPCSTR, INT, INT, INT,
        ctypes.POINTER(HICON * icon_total_count),
        ctypes.POINTER(UINT * icon_total_count), UINT,
        UINT
    ]

    hIconArray = HICON * icon_total_count
    hicons = hIconArray()

    p_hicons = ctypes.pointer(hicons)

    IDArray = UINT * icon_total_count
    ids = IDArray()
    p_ids = ctypes.pointer(ids)

    success_count = ExIcon(path.encode(), 0, 32, 32, p_hicons, p_ids, icon_total_count, 0)
    # From mss.windows
    srcdc = user32.GetWindowDC(0)
    memdc = gdi32.CreateCompatibleDC(srcdc)

    bmp = gdi32.CreateCompatibleBitmap(srcdc, width, height)
    gdi32.SelectObject(memdc,bmp)
    user32.DrawIcon(memdc,0,0, hicons[index]) #From cnblogs

    bmi = BITMAPINFO()
    bmi.bmiHeader.biSize = ctypes.sizeof(BITMAPINFOHEADER)
    bmi.bmiHeader.biPlanes = 1  # Always 1
    bmi.bmiHeader.biBitCount = 32  # See grab.__doc__ [2]
    bmi.bmiHeader.biCompression = 0  # 0 = BI_RGB (no compression)
    bmi.bmiHeader.biClrUsed = 0  # See grab.__doc__ [3]
    bmi.bmiHeader.biClrImportant = 0  # See grab.__doc__ [3]

    bmi.bmiHeader.biWidth = width
    bmi.bmiHeader.biHeight = -height  # Why minus? [1]

    data = ctypes.create_string_buffer(width * height * 4)

    bits = gdi32.GetDIBits(
            memdc, bmp, 0, height, data, ctypes.byref(bmi), 0
        )
    gdi32.DeleteObject(bmp)

    if bits != height:
        raise Exception("gdi32.GetDIBits() failed.")

    return bytearray(data)

def get_rgb_data(path):
    return rgb(get_raw_data(path))

if __name__ == "__main__":
    data = rgb(get_raw_data("C:\\windows\\explorer.exe",0,32),32,32)
    import mss.tools
    mss.tools.to_png(data,(32,32),8,"explorer.png")

忘写DestroyIcon了,不过也差不多了吧,收工。

总结

多查资料,还要会用

本文发于CSDN于 2022/8/10 12:32

  • 4
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值