前言和引用
我有个程序有获取图标的需求,但是我不想使用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