Skip to content

Commit 526af9a

Browse files
authored
Introduce GDI test helper module and test IViewObject.Draw. (#911)
* refactor: Extract GDI-related definitions to `gdi_helper` module. Move GDI-related ctypes definitions from `test/test_stream.py` to a new dedicated module, `test/gdi_helper.py`. This improves modularity and reusability of GDI helper functions across tests. * feat: Enhance GDI helper module with context managers and utilities. Further enhance `test/gdi_helper.py` by introducing context managers (`get_dc`, `create_compatible_dc`, `select_object`, `create_image_rendering_dc`) and utility functions and constants (`create_24bitmap_info`, `BI_RGB`, `DIB_RGB_COLORS`). These additions centralize GDI object management and simplify off-screen rendering setup for tests. * refactor: Allow `Optional[int]` in `gdi_helper` context managers. Update type hints for `get_dc` and `create_compatible_dc` in `test/gdi_helper.py` to explicitly accept `Optional[int]`. * refactor: Extract `create_dib_section` context manager in `gdi_helper`. Introduce `create_dib_section` as a dedicated context manager in `test/gdi_helper.py` to handle the creation and destruction of DIB sections. Refactor `create_image_rendering_dc` to utilize this new manager, reducing nesting and improving readability. Additionally, update the return type hint of `create_image_rendering_dc` to use `int` for the bitmap bits pointer address, ensuring consistency with the handle types. * test: Add test for `IViewObject::Draw` method. Introduce `test_Draw` in `test_viewobject.py` to validate the `Draw` method of the `IViewObject` interface. This test creates an off-screen rendering context using helpers from `gdi_helper.py`. An `InkEdit` COM object is created, placed into a running state via `OleRun`, and then instructed to draw itself onto the device context. The test verifies the operation's success by asserting that the pixel data in the resulting bitmap matches the color set on the object.
1 parent 267e13d commit 526af9a

3 files changed

Lines changed: 242 additions & 175 deletions

File tree

comtypes/test/gdi_helper.py

Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
import contextlib
2+
from collections.abc import Iterator
3+
from ctypes import POINTER, Structure, WinDLL, byref, c_void_p, sizeof
4+
from ctypes.wintypes import (
5+
BOOL,
6+
DWORD,
7+
HANDLE,
8+
HDC,
9+
HGDIOBJ,
10+
HWND,
11+
INT,
12+
LONG,
13+
UINT,
14+
WORD,
15+
)
16+
from typing import Optional
17+
18+
BI_RGB = 0 # No compression
19+
DIB_RGB_COLORS = 0
20+
21+
_user32 = WinDLL("user32")
22+
23+
_GetDC = _user32.GetDC
24+
_GetDC.argtypes = (HWND,)
25+
_GetDC.restype = HDC
26+
27+
_ReleaseDC = _user32.ReleaseDC
28+
_ReleaseDC.argtypes = (HWND, HDC)
29+
_ReleaseDC.restype = INT
30+
31+
_gdi32 = WinDLL("gdi32")
32+
33+
_CreateCompatibleDC = _gdi32.CreateCompatibleDC
34+
_CreateCompatibleDC.argtypes = (HDC,)
35+
_CreateCompatibleDC.restype = HDC
36+
37+
_DeleteDC = _gdi32.DeleteDC
38+
_DeleteDC.argtypes = (HDC,)
39+
_DeleteDC.restype = BOOL
40+
41+
_SelectObject = _gdi32.SelectObject
42+
_SelectObject.argtypes = (HDC, HGDIOBJ)
43+
_SelectObject.restype = HGDIOBJ
44+
45+
_DeleteObject = _gdi32.DeleteObject
46+
_DeleteObject.argtypes = (HGDIOBJ,)
47+
_DeleteObject.restype = BOOL
48+
49+
_GdiFlush = _gdi32.GdiFlush
50+
_GdiFlush.argtypes = []
51+
_GdiFlush.restype = BOOL
52+
53+
54+
class BITMAPINFOHEADER(Structure):
55+
_fields_ = [
56+
("biSize", DWORD),
57+
("biWidth", LONG),
58+
("biHeight", LONG),
59+
("biPlanes", WORD),
60+
("biBitCount", WORD),
61+
("biCompression", DWORD),
62+
("biSizeImage", DWORD),
63+
("biXPelsPerMeter", LONG),
64+
("biYPelsPerMeter", LONG),
65+
("biClrUsed", DWORD),
66+
("biClrImportant", DWORD),
67+
]
68+
69+
70+
class BITMAPINFO(Structure):
71+
_fields_ = [
72+
("bmiHeader", BITMAPINFOHEADER),
73+
("bmiColors", DWORD * 1), # Placeholder for color table, not used for 32bpp
74+
]
75+
76+
77+
_CreateDIBSection = _gdi32.CreateDIBSection
78+
_CreateDIBSection.argtypes = (
79+
HDC,
80+
POINTER(BITMAPINFO),
81+
UINT, # DIB_RGB_COLORS
82+
POINTER(c_void_p), # lplpBits
83+
HANDLE, # hSection
84+
DWORD, # dwOffset
85+
)
86+
_CreateDIBSection.restype = HGDIOBJ
87+
88+
89+
@contextlib.contextmanager
90+
def get_dc(hwnd: Optional[int]) -> Iterator[int]:
91+
"""Context manager to get and release a device context (DC)."""
92+
dc = _GetDC(hwnd)
93+
assert dc, "Failed to get device context."
94+
try:
95+
yield dc
96+
finally:
97+
# Release the device context
98+
_ReleaseDC(hwnd, dc)
99+
100+
101+
@contextlib.contextmanager
102+
def create_compatible_dc(hdc: Optional[int]) -> Iterator[int]:
103+
"""Context manager to create and delete a compatible device context."""
104+
mem_dc = _CreateCompatibleDC(hdc)
105+
assert mem_dc, "Failed to create compatible memory DC."
106+
try:
107+
yield mem_dc
108+
finally:
109+
_DeleteDC(mem_dc)
110+
111+
112+
@contextlib.contextmanager
113+
def create_dib_section(
114+
hdc: int, bmi: BITMAPINFO, usage: int, hsection: int, dwoffset: int
115+
) -> Iterator[tuple[int, int]]:
116+
"""Context manager to create and manage a DIB section.
117+
118+
This function creates a device-independent bitmap (DIB) that applications
119+
can write to directly. It provides a handle to the DIB and a pointer
120+
address to its bitmap bits.
121+
"""
122+
bits = c_void_p()
123+
try:
124+
hbm = _CreateDIBSection(
125+
hdc,
126+
byref(bmi),
127+
usage,
128+
byref(bits),
129+
hsection,
130+
dwoffset,
131+
)
132+
assert hbm, "Failed to create DIB section."
133+
assert bits.value, "Failed to get the bitmap's bit value."
134+
yield hbm, bits.value
135+
finally:
136+
_DeleteObject(hbm)
137+
138+
139+
@contextlib.contextmanager
140+
def select_object(hdc: int, obj: int) -> Iterator[int]:
141+
"""Context manager to select a GDI object into a device context and restore
142+
the original.
143+
"""
144+
old_obj = _SelectObject(hdc, obj)
145+
assert old_obj, "Failed to select object into DC."
146+
try:
147+
yield obj
148+
finally:
149+
_SelectObject(hdc, old_obj)
150+
151+
152+
def create_24bitmap_info(width: int, height: int) -> BITMAPINFO:
153+
"""Creates a BITMAPINFO structure for a 24bpp BGR DIB section."""
154+
bmi = BITMAPINFO()
155+
bmi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER)
156+
bmi.bmiHeader.biWidth = width
157+
bmi.bmiHeader.biHeight = -height # Negative for top-down DIB
158+
bmi.bmiHeader.biPlanes = 1
159+
bmi.bmiHeader.biBitCount = 24
160+
bmi.bmiHeader.biCompression = BI_RGB
161+
# width*height pixels * 3 bytes/pixel (BGR)
162+
bmi.bmiHeader.biSizeImage = width * height * 3
163+
return bmi
164+
165+
166+
@contextlib.contextmanager
167+
def create_image_rendering_dc(
168+
hwnd: int,
169+
width: int,
170+
height: int,
171+
usage: int = DIB_RGB_COLORS,
172+
hsection: int = 0,
173+
dwoffset: int = 0,
174+
) -> Iterator[tuple[int, int, BITMAPINFO, int]]:
175+
"""Context manager to create a device context for off-screen image rendering.
176+
177+
This sets up a memory device context (DC) with a DIB section, allowing
178+
GDI operations to render into a memory buffer.
179+
180+
Args:
181+
hwnd: Handle to the window (0 for desktop).
182+
width: Width of the image buffer.
183+
height: Height of the image buffer.
184+
usage: The type of DIB. Default is DIB_RGB_COLORS (0).
185+
hsection: A handle to a file-mapping object. If NULL (0), the system
186+
allocates memory for the DIB.
187+
dwoffset: The offset from the beginning of the file-mapping object
188+
specified by `hsection` to where the DIB bitmap begins.
189+
190+
Yields:
191+
A tuple containing:
192+
- mem_dc: The handle to the memory device context.
193+
- bits: Pointer address to the pixel data of the DIB section.
194+
- bmi: The structure describing the DIB section.
195+
- hbm: The handle to the created DIB section bitmap.
196+
"""
197+
# Get a screen DC to use as a reference for creating a compatible DC
198+
with get_dc(hwnd) as screen_dc, create_compatible_dc(screen_dc) as mem_dc:
199+
bmi = create_24bitmap_info(width, height)
200+
with create_dib_section(mem_dc, bmi, usage, hsection, dwoffset) as (hbm, bits):
201+
with select_object(mem_dc, hbm):
202+
yield mem_dc, bits, bmi, hbm

0 commit comments

Comments
 (0)