Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
See Git commit messages for full history.

## 10.2.0.dev0 (2026-xx-xx)
- Add `is_primary` and `name` keys to Monitor dicts for primary monitor detection and device names (#153)
- Add `primary_monitor` property to MSS base class for easy access to the primary monitor (#153)
- Windows: add primary monitor detection using `GetMonitorInfoW` API (#153)
- Windows: add monitor device name extraction using `EnumDisplayDevicesW` API (#153)
- Windows: switch from `GetDIBits` to more memory efficient `CreateDIBSection` for `MSS.grab` implementation (#449)
- Windows: fix gdi32.GetDIBits() failed after a couple of minutes of recording (#268)
- Linux: check the server for Xrandr support version (#417)
Expand Down
17 changes: 17 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,22 @@
# Technical Changes

## 10.2.0 (2026-xx-xx)

### base.py
- Added `primary_monitor` property to return the primary monitor (or first monitor as fallback).

### models.py
- Changed `Monitor` type from `dict[str, int]` to `dict[str, Any]` to support new `is_primary` (bool) and `name` (str) fields.
- Added TODO comment for future Monitor class implementation (#470).

### windows.py
- Added `MONITORINFOEXW` structure for extended monitor information.
- Added `DISPLAY_DEVICEW` structure for device information.
- Added constants: `CCHDEVICENAME`, `MONITORINFOF_PRIMARY`, `EDD_GET_DEVICE_INTERFACE_NAME`.
- Added `GetMonitorInfoW` to `CFUNCTIONS` for querying monitor properties.
- Added `EnumDisplayDevicesW` to `CFUNCTIONS` for querying device details.
- Modified `_monitors_impl()` callback to extract primary monitor flag and device names using Win32 APIs.

## 10.1.1 (2025-xx-xx)

### linux/__init__.py
Expand Down
2 changes: 2 additions & 0 deletions docs/source/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@
"undoc-members": True,
"show-inheritance": True,
}
# Suppress duplicate target warnings for re-exported classes
suppress_warnings = ["ref.python"]

# Monkey-patch WINFUNCTYPE and WinError into ctypes, so that we can
# import mss.windows while building the documentation.
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,7 @@ ignore = [
"C90", # complexity
"COM812", # conflict
"D", # TODO
"FIX002", # Line contains TODO
"ISC001", # conflict
"T201", # `print()`
]
Expand Down
22 changes: 22 additions & 0 deletions src/mss/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,28 @@ def monitors(self) -> Monitors:
self._monitors_impl()
return self._monitors

@property
def primary_monitor(self) -> Monitor:
"""Get the primary monitor.

Returns the monitor marked as primary. If no monitor is marked as primary
(or the platform doesn't support primary monitor detection), returns the
first monitor (at index 1).

:raises ScreenShotError: If no monitors are available.

.. versionadded:: 10.2.0
"""
monitors = self.monitors
if len(monitors) <= 1: # Only the "all monitors" entry or empty
raise ScreenShotError("No monitor found.")

for monitor in monitors[1:]: # Skip the "all monitors" entry at index 0
if monitor.get("is_primary", False):
return monitor
# Fallback to the first monitor if no primary is found
return monitors[1]

def save(
self,
/,
Expand Down
5 changes: 4 additions & 1 deletion src/mss/models.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
# This is part of the MSS Python's module.
# Source: https://github.com/BoboTiG/python-mss.
from __future__ import annotations

from typing import TYPE_CHECKING, Any, Callable, NamedTuple

Monitor = dict[str, int]
# TODO @BoboTiG: https://github.com/BoboTiG/python-mss/issues/470
# Change this to a proper Monitor class in next major release.
Monitor = dict[str, Any]
Monitors = list[Monitor]

Pixel = tuple[int, int, int]
Expand Down
73 changes: 68 additions & 5 deletions src/mss/windows.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@
CAPTUREBLT = 0x40000000
DIB_RGB_COLORS = 0
SRCCOPY = 0x00CC0020
CCHDEVICENAME = 32
MONITORINFOF_PRIMARY = 0x01
EDD_GET_DEVICE_INTERFACE_NAME = 0x00000001


class BITMAPINFOHEADER(Structure):
Expand Down Expand Up @@ -74,6 +77,35 @@ class BITMAPINFO(Structure):
_fields_ = (("bmiHeader", BITMAPINFOHEADER), ("bmiColors", BYTE * 4))


class MONITORINFOEXW(Structure):
"""Extended monitor information structure.
https://learn.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-monitorinfoexw
"""

_fields_ = (
("cbSize", DWORD),
("rcMonitor", RECT),
("rcWork", RECT),
("dwFlags", DWORD),
("szDevice", WORD * CCHDEVICENAME),
)


class DISPLAY_DEVICEW(Structure): # noqa: N801
"""Display device information structure.
https://learn.microsoft.com/en-us/windows/win32/api/wingdi/ns-wingdi-display_devicew
"""

_fields_ = (
("cb", DWORD),
("DeviceName", WORD * 32),
("DeviceString", WORD * 128),
("StateFlags", DWORD),
("DeviceID", WORD * 128),
("DeviceKey", WORD * 128),
)


MONITORNUMPROC = WINFUNCTYPE(BOOL, HMONITOR, HDC, POINTER(RECT), LPARAM)


Expand Down Expand Up @@ -113,6 +145,7 @@ def _errcheck(result: BOOL | _Pointer, func: Callable, arguments: tuple) -> tupl
"CreateDIBSection": ("gdi32", [HDC, POINTER(BITMAPINFO), UINT, POINTER(LPVOID), HANDLE, DWORD], HBITMAP, _errcheck),
"DeleteDC": ("gdi32", [HDC], HDC, _errcheck),
"DeleteObject": ("gdi32", [HGDIOBJ], BOOL, _errcheck),
"EnumDisplayDevicesW": ("user32", [POINTER(WORD), DWORD, POINTER(DISPLAY_DEVICEW), DWORD], BOOL, None),
"EnumDisplayMonitors": ("user32", [HDC, LPCRECT, MONITORNUMPROC, LPARAM], BOOL, _errcheck),
# GdiFlush flushes the calling thread's current batch of GDI operations.
# This ensures DIB memory is fully updated before reading.
Expand All @@ -121,6 +154,7 @@ def _errcheck(result: BOOL | _Pointer, func: Callable, arguments: tuple) -> tupl
# parameter is valid but the value is actually 0 (e.g., SM_CLEANBOOT on a normal boot). Thus, we do not attach an
# errcheck function here.
"GetSystemMetrics": ("user32", [INT], INT, None),
"GetMonitorInfoW": ("user32", [HMONITOR, POINTER(MONITORINFOEXW)], BOOL, _errcheck),
"GetWindowDC": ("user32", [HWND], HDC, _errcheck),
"ReleaseDC": ("user32", [HWND, HDC], INT, _errcheck),
# SelectObject returns NULL on error the way we call it. If it's called to select a region, it returns HGDI_ERROR
Expand Down Expand Up @@ -242,17 +276,46 @@ def _monitors_impl(self) -> None:

# Each monitor
@MONITORNUMPROC
def callback(_monitor: HMONITOR, _data: HDC, rect: LPRECT, _dc: LPARAM) -> bool:
def callback(hmonitor: HMONITOR, _data: HDC, rect: LPRECT, _dc: LPARAM) -> bool:
"""Callback for monitorenumproc() function, it will return
a RECT with appropriate values.
"""
# Get monitor info to check if it's the primary monitor and get device name
info = MONITORINFOEXW()
info.cbSize = ctypes.sizeof(MONITORINFOEXW)
user32.GetMonitorInfoW(hmonitor, ctypes.byref(info))

rct = rect.contents
left = int_(rct.left)
top = int_(rct.top)
# Check the dwFlags field for MONITORINFOF_PRIMARY
is_primary = bool(info.dwFlags & MONITORINFOF_PRIMARY)
# Extract device name (null-terminated wide string)
device_name = ctypes.wstring_at(ctypes.addressof(info.szDevice))

# Get friendly device string (manufacturer/model info)
display_device = DISPLAY_DEVICEW()
display_device.cb = ctypes.sizeof(DISPLAY_DEVICEW)
device_string = device_name

# EnumDisplayDevicesW can get more detailed info about the device
if user32.EnumDisplayDevicesW(
ctypes.cast(ctypes.addressof(info.szDevice), POINTER(WORD)),
0,
ctypes.byref(display_device),
0,
):
# DeviceString contains the friendly name like "Generic PnP Monitor" or manufacturer name
device_string = ctypes.wstring_at(ctypes.addressof(display_device.DeviceString))

self._monitors.append(
{
"left": int_(rct.left),
"top": int_(rct.top),
"width": int_(rct.right) - int_(rct.left),
"height": int_(rct.bottom) - int_(rct.top),
"left": left,
"top": top,
"width": int_(rct.right) - left,
"height": int_(rct.bottom) - top,
"is_primary": is_primary,
"name": device_string,
},
)
return True
Expand Down
45 changes: 45 additions & 0 deletions src/tests/test_primary_monitor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
"""This is part of the MSS Python's module.
Source: https://github.com/BoboTiG/python-mss.
"""

from collections.abc import Callable

import pytest

from mss.base import MSSBase


def test_primary_monitor(mss_impl: Callable[..., MSSBase]) -> None:
"""Test that primary_monitor property works correctly."""
with mss_impl() as sct:
primary = sct.primary_monitor
monitors = sct.monitors

# Should return a valid monitor dict
assert isinstance(primary, dict)
assert "left" in primary
assert "top" in primary
assert "width" in primary
assert "height" in primary

# Should be in the monitors list (excluding index 0 which is "all monitors")
assert primary in monitors[1:]

# Should either be marked as primary or be the first monitor as fallback
if primary.get("is_primary", False):
assert primary["is_primary"] is True
else:
assert primary == monitors[1]


@pytest.mark.skipif("mss.windows" not in dir(), reason="Windows only")
def test_primary_monitor_coordinates_windows() -> None:
"""Test that on Windows, the primary monitor has coordinates at (0, 0)."""
import mss # noqa: PLC0415

with mss.mss() as sct:
primary = sct.primary_monitor
if primary.get("is_primary", False):
# On Windows, the primary monitor is at (0, 0)
assert primary["left"] == 0
assert primary["top"] == 0
1 change: 1 addition & 0 deletions src/tests/test_setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ def test_sdist() -> None:
f"mss-{__version__}/src/tests/test_issue_220.py",
f"mss-{__version__}/src/tests/test_leaks.py",
f"mss-{__version__}/src/tests/test_macos.py",
f"mss-{__version__}/src/tests/test_primary_monitor.py",
f"mss-{__version__}/src/tests/test_save.py",
f"mss-{__version__}/src/tests/test_setup.py",
f"mss-{__version__}/src/tests/test_tools.py",
Expand Down