pywinrt / python-winsdk

Python package with bindings for Windows SDK
https://python-winsdk.readthedocs.io
MIT License
74 stars 8 forks source link

Request to add binding for CreateForWindow or GetWindowIdFromWindow #5

Closed Avasam closed 2 years ago

Avasam commented 2 years ago

https://docs.microsoft.com/en-us/windows/win32/api/windows.graphics.capture.interop/nf-windows-graphics-capture-interop-igraphicscaptureiteminterop-createforwindow

CreateForWindow is used to create a CaptureGraphicsItem form an HWND. But it seems the binding it missing from this project.

While doable, being forced to use the CaptureGraphicsPicker would complicated things a lot for us since we use a custom Picker that works across all other capture methods.

dlech commented 2 years ago

Can you give an example of how one would use this?

Does InitializeWithWindow work instead? We already have this at winsdk._winrt.initailze_with_window(). If not, we could consider adding a similar create_for_window() function. It isn't clear what the output type of that function is though.

dlech commented 2 years ago

If there is a way to get a WindowId from an HWIND, maybe https://docs.microsoft.com/en-us/uwp/api/windows.graphics.capture.graphicscaptureitem.trycreatefromwindowid would work instead.

So maybe we should add a binding for https://docs.microsoft.com/en-us/windows/windows-app-sdk/api/win32/microsoft.ui.interop/nf-microsoft-ui-interop-getwindowidfromwindow instead of CreateForWindow.

Avasam commented 2 years ago

initialize_with_window is used to initialize the GraphicsCapturePicker (to give it its owner's WindowId) isn't it? (which is what I'd like to avoid doing, I don't want to have to use window's built-in GraphicsCapturePicker, since we'd have to maintain two different ways of selecting windows)

CreateForWindow was mentionned in this blog post: https://blogs.windows.com/windowsdeveloper/2019/09/16/new-ways-to-do-screen-capture/ I assume we'd get a GraphicsCaptureItem. But I agree it's not 100% clear.

From what I saw, going from a HWND to a WindowId using GetWindowIdFromWindow, and then using TryCreateFromWindowId should work as well. That was the other option I was looking at. Both require the same minimum Windows version, so either way is fine by me.

Avasam commented 2 years ago

Another use case I've remembered is that we can automatically start capturing when the python application start based on the captured window name the user has saved. As well as automatically recovering (restarting the capture) if the user closes the captured application/window then re-opens it.

dlech commented 2 years ago

I think it will work to just cast the HWND to WindowId.

from winsdk.windows.graphics.capture import GraphicsCaptureItem
from winsdk.windows.ui import WindowId

...

item = GraphicsCaptureItem.try_create_from_window_id(WindowId(hwnd))
Avasam commented 2 years ago
from winsdk.windows.graphics.capture import GraphicsCaptureItem
from winsdk.windows.ui import WindowId

window_id = WindowId(hwnd)
item = GraphicsCaptureItem.try_create_from_window_id(window_id)  # Error here
autosplit.windows_graphics_capture = __create_and_start_graphics_capture_session(item)

Good idea, but results in

    item = GraphicsCaptureItem.try_create_from_window_id(window_id)

OSError: [WinError -2147467262] No such interface supported

Noting that this is a different error then just passing the hwnd or window_id.value to try_create_from_window_id: OSError: [WinError -2147024809] The parameter is incorrect

dlech commented 2 years ago

I also read that not all windows can be captured. For example, I tried to capture a turtle (tkinter) window and it doesn't work. But then I tried using an hwnd from a VS Code window and it worked.

Avasam commented 2 years ago

I've only been trying with windows that the GraphicsPicker also sees: image

I've been trying with the HWND of a few different windows as well to make sure, always the same result:

dlech commented 2 years ago

Can you give a full working example to reproduce these errors?

Avasam commented 2 years ago

Can you give a full working example to reproduce these errors?

import win32gui
from winsdk.windows.graphics.capture import GraphicsCaptureItem
from winsdk.windows.ui import WindowId

def print_hwnd(hwnd, _):
    window_text = win32gui.GetWindowText(hwnd)
    if (
        window_text
        and window_text not in ("Default IME", "MSCTFIME UI")
        and "Window" not in window_text
    ):
        print(hwnd, window_text)

win32gui.EnumWindows(print_hwnd, '')

while True:
    try:
        hwnd = int(input("^ Enter an HWND from above ^ :"))
    except:
        print("not an int")
        continue

    try:
        print("Selected window:", win32gui.GetWindowText(hwnd))
    except:
        print("not a valid HWND")
        continue

    try:  # OSError: [WinError -2147024809] The parameter is incorrect
        item = GraphicsCaptureItem.try_create_from_window_id(hwnd)  # Error here
    except OSError as exception:
        print(exception)

    try:  # OSError: [WinError -2147467262] No such interface supported
        window_id = WindowId(hwnd)
        item = GraphicsCaptureItem.try_create_from_window_id(window_id)  # Error here
    except OSError as exception:
        print(exception)
dlech commented 2 years ago
    try:  # OSError: [WinError -2147024809] The parameter is incorrect
        item = GraphicsCaptureItem.try_create_from_window_id(hwnd)  # Error here

This is expected since hwind is int is not WindowId.


    try:  # OSError: [WinError -2147467262] No such interface supported
        window_id = WindowId(hwnd)
        item = GraphicsCaptureItem.try_create_from_window_id(window_id)  # Error here

I do not get an error here.

Maybe your Windows version is too old? I have Windows 11 (build 22000). This API was introduced in build 20348.

https://docs.microsoft.com/en-us/uwp/api/windows.graphics.capture.graphicscaptureitem.trycreatefromwindowid?view=winrt-22000

Avasam commented 2 years ago

Version 10.0.19044 Build 19044, which as far as I know, is the latest Windows 10 version. Yeah that'd do it. It works fine on my Windows 11 (22000) machine as well.

More reasons to want a binding for GetWindowIdFromWindow (since version 1809) for Windows 10 support.

dlech commented 2 years ago

More reasons to want a binding for GetWindowIdFromWindow (since version 1809) for Windows 10 support.

GetWindowIdFromWindow is part of the "Windows App SDK" which is not the regular Windows SDK, so it would required users to install the Windows App runtime. I would rather not add this dependency. Also, it wouldn't help anyway since it only converts HWND to WindowId which we can already do with WindowId(hwnd). Using the WindowId still requires the missing try_create_from_window_id() API. And the suggested IGraphicsCaptureItemInterop::CreateForWindow also requires Windows 10 Build 20348. So think the only option here is using the newer Windows version.

Avasam commented 2 years ago

Idk why it went over my head GetWindowIdFromWindow wouldn't help at all in this case indeed.

What I don't get it that OBS offers the Windows Graphics Capture API as a capture method, they flagged it as 1903 and up (build 18334 iirc) and it works on my current machine. image

And it seems they are using CreateForWindow (https://github.com/obsproject/obs-studio/blob/master/libobs-winrt/winrt-capture.cpp#L316)

So I asked them, here's their answer: image image https://stackoverflow.com/questions/65306347/which-version-does-support-windows-graphics-capture

dlech commented 2 years ago

Thanks for the research. I've added a new interop submodule so now it should be possible to do this:

from winsdk.windows.graphics.capture.interop import create_for_monitor, create_for_window

# replace with create_for_window(hwnd) as needed
item = create_for_monitor(0)
print(item.display_name)
Avasam commented 2 years ago

Thanks for the research. I've added a new interop submodule so now it should be possible to do this:

from winsdk.windows.graphics.capture.interop import create_for_monitor, create_for_window

# replace with create_for_window(hwnd) as needed
item = create_for_monitor(0)
print(item.display_name)

It works! A huge thanks to you for your continued support. This will make a lot of our app's users happy to have a more compatible screen capture option than BitBlt.