Skip to content

Commit

Permalink
create Icon class for an ActiveX app
Browse files Browse the repository at this point in the history
  • Loading branch information
jborbely committed Mar 19, 2024
1 parent 3e1209b commit 2633d19
Show file tree
Hide file tree
Showing 2 changed files with 88 additions and 21 deletions.
87 changes: 66 additions & 21 deletions msl/loadlib/activex.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,19 +84,6 @@ class ExtendedWindowStyle(IntEnum):
PALETTEWINDOW = WINDOWEDGE | TOOLWINDOW | TOPMOST


class Icon(IntEnum):
"""Standard icons. See
`about-icons <https://learn.microsoft.com/en-us/windows/win32/menurc/about-icons>`_
for more details."""
APPLICATION = 32512
ERROR = 32513
QUESTION = 32514
WARNING = 32515
INFORMATION = 32516
WINLOGO = 32517
SHIELD = 32518


class MenuFlag(IntEnum):
"""Menu item flags. See
`append-menu <https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-appendmenuw>`_
Expand Down Expand Up @@ -236,6 +223,7 @@ def _err_check(result, func, arguments): # noqa: func and arguments are not use
gdi32 = ctypes.windll.gdi32
atl = ctypes.windll.atl
user32 = ctypes.windll.user32
shell32 = ctypes.windll.shell32

WNDPROC = ctypes.WINFUNCTYPE(LRESULT, wt.HWND, wt.UINT, wt.WPARAM, wt.LPARAM)

Expand Down Expand Up @@ -310,8 +298,11 @@ class WNDCLASSEXW(ctypes.Structure):
user32.UnregisterClassW.restype = ctypes.c_bool
user32.UnregisterClassW.argtypes = [wt.LPCWSTR, wt.HINSTANCE]

shell32.ExtractIconW.restype = wt.HICON
shell32.ExtractIconW.argtypes = [wt.HINSTANCE, wt.LPCWSTR, wt.UINT]

except AttributeError:
kernel32 = user32 = atl = gdi32 = WNDCLASSEXW = None
kernel32 = user32 = atl = gdi32 = shell32 = WNDCLASSEXW = None


CW_USEDEFAULT = 0x80000000
Expand All @@ -336,6 +327,52 @@ def _create_window(*,
menu, instance, param)


class Icon:

def __init__(self,
file: str,
*,
index: int = 0,
hinstance: int = None) -> None:
"""Extract an icon from an executable file, DLL or icon file.
:param file: The path to an executable file, DLL or icon file.
:param index: The zero-based index of the icon to extract.
:param hinstance: Handle to the instance of the calling application.
"""
self._hicon: int | None = None

if shell32 is None:
raise OSError('Loading an icon is not supported on this platform')

if index < 0:
raise ValueError('A negative index is not supported')

if hinstance is None:
hinstance = kernel32.GetModuleHandleW(None)

self._file = file
self._index = index
self._hicon = shell32.ExtractIconW(hinstance, file, index)

def __repr__(self) -> str:
return f'<Icon file={self._file!r} index={self._index}>'

def __del__(self) -> None:
self.destroy()

@property
def hicon(self) -> int | None:
"""Returns the handle to the icon or :data:`None` if no icon was found."""
return self._hicon

def destroy(self) -> None:
"""Destroys the icon and frees any memory the icon occupied."""
if self._hicon is not None:
user32.DestroyIcon(self._hicon)
self._hicon = None


class MenuItem:

def __init__(self, **kwargs) -> None:
Expand Down Expand Up @@ -552,7 +589,7 @@ def __init__(self,
*,
background: int = Background.WHITE,
class_style: int = ClassStyle.NONE,
icon: int = Icon.APPLICATION,
icon: Icon = None,
style: int = WindowStyle.OVERLAPPEDWINDOW,
title: str = 'ActiveX') -> None:
"""Create the main application window to display ActiveX controls.
Expand All @@ -567,25 +604,31 @@ def __init__(self,
"""
super().__init__()
self._atom = None
self._icon = icon # prevent an icon from being garbage collected
self._event_connections = []
self._msg_handlers: list[Callable[[int, int, int, int], None]] = []

if WNDCLASSEXW is None:
raise OSError('An ActiveX application is not supported on this platform')

if isinstance(icon, Icon):
h_icon = icon.hicon
else:
h_icon = user32.LoadIconW(None, wt.LPCWSTR(32512)) # IDI_APPLICATION

self._window = WNDCLASSEXW()
self._window.cbSize = ctypes.sizeof(WNDCLASSEXW)
self._window.style = class_style
self._window.lpfnWndProc = WNDPROC(self._window_procedure)
self._window.cbClsExtra = 0
self._window.cbWndExtra = 0
self._window.hInstance = kernel32.GetModuleHandleW(None)
self._window.hIcon = user32.LoadIconW(None, wt.LPCWSTR(icon))
self._window.hIcon = h_icon
self._window.hCursor = user32.LoadCursorW(None, wt.LPCWSTR(32512)) # IDC_ARROW
self._window.hbrBackground = gdi32.GetStockObject(background)
self._window.lpszMenuName = f'ActiveXMenu{id(self._window)}' # make the name unique
self._window.lpszClassName = f'ActiveXClass{id(self._window)}'
self._window.hIconSm = user32.LoadIconW(None, wt.LPCWSTR(icon))
self._window.hIconSm = h_icon

self._atom = user32.RegisterClassExW(self._window)

Expand Down Expand Up @@ -615,6 +658,8 @@ def __del__(self) -> None:
user32.UnregisterClassW(self._window.lpszClassName, self._window.hInstance)
self._atom = None

self._icon = None

def _window_procedure(self, hwnd: int, message: int, w_param: int, l_param: int) -> int:
for handler in self._msg_handlers:
handler(hwnd, message, w_param, l_param)
Expand All @@ -637,7 +682,7 @@ def add_message_handler(self, handler: Callable[[int, int, int, int], None]) ->
:param handler: A function that processes messages sent to a window.
The function must accept four positional arguments (all integer
values) and the returned object is ignored. See
`window-procedure <https://learn.microsoft.com/en-us/windows/win32/learnwin32/writing-the-window-procedure>`_
`WindowProc <https://learn.microsoft.com/en-us/windows/win32/learnwin32/writing-the-window-procedure>`_
for more details about the input arguments to the `handler`.
"""
self._msg_handlers.append(handler)
Expand Down Expand Up @@ -700,9 +745,6 @@ def load(self,
if comtypes is None:
raise OSError('comtypes must be installed to load an ActiveX library')

if parent is None:
parent = self._hwnd

try:
window_name = str(comtypes.GUID.from_progid(activex_id))
except (TypeError, OSError):
Expand All @@ -711,6 +753,9 @@ def load(self,
if not window_name:
raise OSError(f'Cannot find an ActiveX library with ID {activex_id!r}')

if parent is None:
parent = self._hwnd

hwnd = _create_window(
class_name='AtlAxWin',
window_name=window_name,
Expand Down
22 changes: 22 additions & 0 deletions tests/test_application.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import sys

from msl.loadlib import activex
from msl.loadlib.constants import IS_WINDOWS

Expand Down Expand Up @@ -156,3 +158,23 @@ def test_application():
def test_application_raises():
with pytest.raises(OSError, match='not supported on this platform'):
activex.Application()


@skipif_not_windows
def test_icon():
icon = activex.Icon('does not exist')
assert str(icon) == "<Icon file='does not exist' index=0>"
assert icon.hicon is None

# ok to call destroy() multiple times
icon.destroy()
icon.destroy()
icon.destroy()
icon.destroy()

with pytest.raises(ValueError, match='negative index'):
activex.Icon('', index=-1)

icon = activex.Icon(sys.executable)
assert icon.hicon > 0
icon.destroy()

0 comments on commit 2633d19

Please sign in to comment.