From 82ec56bc7dd2cec4e0d19e1bec3c345e86c366d5 Mon Sep 17 00:00:00 2001 From: Lea Anthony Date: Thu, 1 Jun 2023 20:20:59 +1000 Subject: [PATCH] [v3 windows] Support GetScreens and GetPrimaryScreen --- v3/pkg/application/application_windows.go | 49 +++- v3/pkg/application/webview_window_windows.go | 21 -- v3/pkg/w32/constants.go | 9 + v3/pkg/w32/screen.go | 231 +++++++++++-------- v3/pkg/w32/user32.go | 4 + 5 files changed, 191 insertions(+), 123 deletions(-) diff --git a/v3/pkg/application/application_windows.go b/v3/pkg/application/application_windows.go index 7a045007c..37598fb9d 100644 --- a/v3/pkg/application/application_windows.go +++ b/v3/pkg/application/application_windows.go @@ -3,7 +3,10 @@ package application import ( + "fmt" + "golang.org/x/sys/windows" "os" + "strconv" "syscall" "unsafe" @@ -39,13 +42,51 @@ func getNativeApplication() *windowsApp { } func (m *windowsApp) getPrimaryScreen() (*Screen, error) { - //TODO implement me - panic("implement me") + screens, err := m.getScreens() + if err != nil { + return nil, err + } + for _, screen := range screens { + if screen.IsPrimary { + return screen, nil + } + } + return nil, fmt.Errorf("no primary screen found") } func (m *windowsApp) getScreens() ([]*Screen, error) { - //TODO implement me - panic("implement me") + allScreens, err := w32.GetAllScreens() + if err != nil { + return nil, err + } + // Convert result to []*Screen + screens := make([]*Screen, len(allScreens)) + for id, screen := range allScreens { + x := int(screen.MONITORINFOEX.RcMonitor.Left) + y := int(screen.MONITORINFOEX.RcMonitor.Top) + right := int(screen.MONITORINFOEX.RcMonitor.Right) + bottom := int(screen.MONITORINFOEX.RcMonitor.Bottom) + width := right - x + height := bottom - y + screens[id] = &Screen{ + ID: strconv.Itoa(id), + Name: windows.UTF16ToString(screen.MONITORINFOEX.SzDevice[:]), + X: x, + Y: y, + Size: Size{Width: width, Height: height}, + Bounds: Rect{X: x, Y: y, Width: width, Height: height}, + WorkArea: Rect{ + X: int(screen.MONITORINFOEX.RcWork.Left), + Y: int(screen.MONITORINFOEX.RcWork.Top), + Width: int(screen.MONITORINFOEX.RcWork.Right - screen.MONITORINFOEX.RcWork.Left), + Height: int(screen.MONITORINFOEX.RcWork.Bottom - screen.MONITORINFOEX.RcWork.Top), + }, + IsPrimary: screen.IsPrimary, + Scale: screen.Scale, + Rotation: 0, + } + } + return screens, nil } func (m *windowsApp) hide() { diff --git a/v3/pkg/application/webview_window_windows.go b/v3/pkg/application/webview_window_windows.go index 04eed5e44..701bb01ea 100644 --- a/v3/pkg/application/webview_window_windows.go +++ b/v3/pkg/application/webview_window_windows.go @@ -493,27 +493,6 @@ func (w *windowsWebviewWindow) getScreen() (*Screen, error) { thisScreen.IsPrimary = mi.DwFlags&w32.MONITORINFOF_PRIMARY != 0 // TODO: Get screen rotation - // https://docs.microsoft.com/en-us/windows/win32/api/wingdi/ns-wingdi-devmodea - - //// get display settings for monitor - //var dm w32.DEVMODE - //dm.DmSize = uint16(unsafe.Sizeof(dm)) - //dm.DmDriverExtra = 0 - //w32.EnumDisplaySettingsEx(&mi.SzDevice[0], w32.ENUM_CURRENT_SETTINGS, &dm, 0) - // - //// check display settings for rotation - //rotationAngle := dm.DmDi - //if rotationAngle == DMDO_0 { - // printf("Monitor is not rotated\n") - //} else if rotationAngle == DMDO_90 { - // printf("Monitor is rotated 90 degrees\n") - //} else if rotationAngle == DMDO_180 { - // printf("Monitor is rotated 180 degrees\n") - //} else if rotationAngle == DMDO_270 { - // printf("Monitor is rotated 270 degrees\n") - //} else { - // printf("Monitor is rotated at an unknown angle\n") - //} return &thisScreen, nil } diff --git a/v3/pkg/w32/constants.go b/v3/pkg/w32/constants.go index 46e834ee9..0b9fc3500 100644 --- a/v3/pkg/w32/constants.go +++ b/v3/pkg/w32/constants.go @@ -52,6 +52,15 @@ const ( SE_ERR_NOASSOC = 31 ) +const ( + EDS_ROTATEDMODE = 0x00000001 + EDS_RAWMODE = 0x00000002 + DMDO_DEFAULT = 0 + DMDO_90 = 1 + DMDO_180 = 2 + DMDO_270 = 3 +) + const ( CW_USEDEFAULT = ^0x7fffffff ) diff --git a/v3/pkg/w32/screen.go b/v3/pkg/w32/screen.go index 7f43beb2b..0b935e3b0 100644 --- a/v3/pkg/w32/screen.go +++ b/v3/pkg/w32/screen.go @@ -8,111 +8,146 @@ import ( "unsafe" ) -func MonitorsEqual(first MONITORINFO, second MONITORINFO) bool { - // Checks to make sure all the fields are the same. - // A cleaner way would be to check identity of devices. but I couldn't find a way of doing that using the win32 API - return first.DwFlags == second.DwFlags && - first.RcMonitor.Top == second.RcMonitor.Top && - first.RcMonitor.Bottom == second.RcMonitor.Bottom && - first.RcMonitor.Right == second.RcMonitor.Right && - first.RcMonitor.Left == second.RcMonitor.Left && - first.RcWork.Top == second.RcWork.Top && - first.RcWork.Bottom == second.RcWork.Bottom && - first.RcWork.Right == second.RcWork.Right && - first.RcWork.Left == second.RcWork.Left -} - -func GetMonitorInformation(hMonitor HMONITOR) (*MONITORINFO, error) { - // Adapted from winc.utils.getMonitorInfo - // See docs for - //https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getmonitorinfoa - - var info MONITORINFO - info.CbSize = uint32(unsafe.Sizeof(info)) - succeeded := GetMonitorInfo(hMonitor, &info) - if !succeeded { - return &info, fmt.Errorf("Windows call to getMonitorInfo failed") - } - return &info, nil -} - type Screen struct { - IsCurrent bool + MONITORINFOEX + Name string IsPrimary bool - Width int - Height int + IsCurrent bool + Scale float32 + Rotation float32 } -func EnumProc(hMonitor HMONITOR, hdcMonitor HDC, lprcMonitor *RECT, screenContainer *ScreenContainer) uintptr { - // adapted from https://stackoverflow.com/a/23492886/4188138 - - // see docs for the following pages to better understand this function - // https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-enumdisplaymonitors - // https://docs.microsoft.com/en-us/windows/win32/api/winuser/nc-winuser-monitorenumproc - // https://docs.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-monitorinfo - // https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-monitorfromwindow - - ourMonitorData := Screen{} - currentMonHndl := MonitorFromWindow(screenContainer.mainWinHandle, MONITOR_DEFAULTTONEAREST) - currentMonInfo, currErr := GetMonitorInformation(currentMonHndl) - - if currErr != nil { - screenContainer.errors = append(screenContainer.errors, currErr) - screenContainer.monitors = append(screenContainer.monitors, Screen{}) - // not sure what the consequences of returning false are, so let's just return true and handle it ourselves - return TRUE - } - - monInfo, err := GetMonitorInformation(hMonitor) - if err != nil { - screenContainer.errors = append(screenContainer.errors, err) - screenContainer.monitors = append(screenContainer.monitors, Screen{}) - return TRUE - } - - height := lprcMonitor.Right - lprcMonitor.Left - width := lprcMonitor.Bottom - lprcMonitor.Top - ourMonitorData.IsPrimary = monInfo.DwFlags&MONITORINFOF_PRIMARY == 1 - ourMonitorData.Height = int(width) - ourMonitorData.Width = int(height) - ourMonitorData.IsCurrent = MonitorsEqual(*currentMonInfo, *monInfo) - - // the reason we need a container is that we have don't know how many times this function will be called - // this "append" call could potentially do an allocation and rewrite the pointer to monitors. So we save the pointer in screenContainer.monitors - // and retrieve the values after all EnumProc calls - // If EnumProc is multi-threaded, this could be problematic. Although, I don't think it is. - screenContainer.monitors = append(screenContainer.monitors, ourMonitorData) - // let's keep screenContainer.errors the same size as screenContainer.monitors in case we want to match them up later if necessary - screenContainer.errors = append(screenContainer.errors, nil) - return TRUE +type DISPLAY_DEVICE struct { + cb uint32 + DeviceName [32]uint16 + DeviceString [128]uint16 + StateFlags uint32 + DeviceID [128]uint16 + DeviceKey [128]uint16 } -type ScreenContainer struct { - monitors []Screen - errors []error - mainWinHandle HWND -} - -func GetAllScreens(mainWinHandle HWND) ([]Screen, error) { - // TODO fix hack of container sharing by having a proper data sharing mechanism between windows and the runtime - monitorContainer := ScreenContainer{mainWinHandle: mainWinHandle} - returnErr := error(nil) - var errorStrings []string - - dc := GetDC(0) - defer ReleaseDC(0, dc) - succeeded := EnumDisplayMonitors(dc, nil, syscall.NewCallback(EnumProc), unsafe.Pointer(&monitorContainer)) - if !succeeded { - return monitorContainer.monitors, fmt.Errorf("Windows call to EnumDisplayMonitors failed") - } - for idx, err := range monitorContainer.errors { - if err != nil { - errorStrings = append(errorStrings, fmt.Sprintf("Error from monitor #%v, %v", idx+1, err)) +func getMonitorName(deviceName string) (string, error) { + var device DISPLAY_DEVICE + device.cb = uint32(unsafe.Sizeof(device)) + i := uint32(0) + for { + res, _, _ := procEnumDisplayDevices.Call(uintptr(unsafe.Pointer(MustStringToUTF16Ptr(deviceName))), uintptr(i), uintptr(unsafe.Pointer(&device)), 0) + if res == 0 { + break + } + if device.StateFlags&0x1 != 0 { + return syscall.UTF16ToString(device.DeviceString[:]), nil } } - if len(errorStrings) > 0 { - returnErr = fmt.Errorf("%v errors encountered: %v", len(errorStrings), errorStrings) - } - return monitorContainer.monitors, returnErr + return "", fmt.Errorf("monitor name not found for device: %s", deviceName) +} + +// I'm not convinced this works properly +func GetRotationForMonitor(displayName [32]uint16) (float32, error) { + var devMode DEVMODE + devMode.DmSize = uint16(unsafe.Sizeof(devMode)) + resp, _, _ := procEnumDisplaySettings.Call(uintptr(unsafe.Pointer(&displayName[0])), ENUM_CURRENT_SETTINGS, uintptr(unsafe.Pointer(&devMode))) + if resp == 0 { + return 0, fmt.Errorf("EnumDisplaySettings failed") + } + + if (devMode.DmFields & DM_DISPLAYORIENTATION) == 0 { + return 0, fmt.Errorf("DM_DISPLAYORIENTATION not set") + } + + switch devMode.DmOrientation { + case DMDO_DEFAULT: + return 0, nil + case DMDO_90: + return 90, nil + case DMDO_180: + return 180, nil + case DMDO_270: + return 270, nil + } + + return -1, nil +} + +func GetAllScreens() ([]*Screen, error) { + var monitorList []MONITORINFOEX + + enumFunc := func(hMonitor uintptr, hdc uintptr, lprcMonitor *RECT, lParam uintptr) uintptr { + monitor := MONITORINFOEX{ + MONITORINFO: MONITORINFO{ + CbSize: uint32(unsafe.Sizeof(MONITORINFOEX{})), + }, + SzDevice: [32]uint16{}, + } + ret, _, _ := procGetMonitorInfo.Call(hMonitor, uintptr(unsafe.Pointer(&monitor))) + if ret == 0 { + return 1 // Continue enumeration + } + + monitorList = append(monitorList, monitor) + return 1 // Continue enumeration + } + + ret, _, _ := procEnumDisplayMonitors.Call(0, 0, syscall.NewCallback(enumFunc), 0) + if ret == 0 { + return nil, fmt.Errorf("EnumDisplayMonitors failed") + } + + // Get the active screen + var pt POINT + ret, _, _ = procGetCursorPos.Call(uintptr(unsafe.Pointer(&pt))) + if ret == 0 { + return nil, fmt.Errorf("GetCursorPos failed") + } + + hMonitor, _, _ := procMonitorFromPoint.Call(uintptr(unsafe.Pointer(&pt)), MONITOR_DEFAULTTONEAREST) + if hMonitor == 0 { + return nil, fmt.Errorf("MonitorFromPoint failed") + } + + var monitorInfo MONITORINFO + monitorInfo.CbSize = uint32(unsafe.Sizeof(monitorInfo)) + ret, _, _ = procGetMonitorInfo.Call(hMonitor, uintptr(unsafe.Pointer(&monitorInfo))) + if ret == 0 { + return nil, fmt.Errorf("GetMonitorInfo failed") + } + + var result []*Screen + + // Iterate through the screens and set the active one + for _, monitor := range monitorList { + thisContainer := &Screen{ + MONITORINFOEX: monitor, + } + thisContainer.IsCurrent = equalRect(monitor.RcMonitor, monitorInfo.RcMonitor) + thisContainer.IsPrimary = monitor.DwFlags == MONITORINFOF_PRIMARY + name, err := getMonitorName(syscall.UTF16ToString(monitor.SzDevice[:])) + if err != nil { + name = "" + } + // Get DPI for monitor + var dpiX, dpiY uint + ret = GetDPIForMonitor(hMonitor, MDT_EFFECTIVE_DPI, &dpiX, &dpiY) + if ret != S_OK { + return nil, fmt.Errorf("GetDpiForMonitor failed") + } + // Convert to float32 + thisContainer.Scale = float32(dpiX) / 96.0 + + // Get rotation of monitor + rot, err := GetRotationForMonitor(monitor.SzDevice) + if err != nil { + rot = 0 + } + thisContainer.Rotation = rot + thisContainer.Name = name + result = append(result, thisContainer) + } + + return result, nil +} + +func equalRect(a RECT, b RECT) bool { + return a.Left == b.Left && a.Top == b.Top && a.Right == b.Right && a.Bottom == b.Bottom } diff --git a/v3/pkg/w32/user32.go b/v3/pkg/w32/user32.go index bb293bc3f..18c8836d3 100644 --- a/v3/pkg/w32/user32.go +++ b/v3/pkg/w32/user32.go @@ -131,6 +131,8 @@ var ( procGetDpiForWindow = moduser32.NewProc("GetDpiForWindow") procSetProcessDPIAware = moduser32.NewProc("SetProcessDPIAware") procEnumDisplayMonitors = moduser32.NewProc("EnumDisplayMonitors") + procEnumDisplayDevices = moduser32.NewProc("EnumDisplayDevicesW") + procEnumDisplaySettings = moduser32.NewProc("EnumDisplaySettingsW") procEnumDisplaySettingsEx = moduser32.NewProc("EnumDisplaySettingsExW") procChangeDisplaySettingsEx = moduser32.NewProc("ChangeDisplaySettingsExW") procSendInput = moduser32.NewProc("SendInput") @@ -139,6 +141,8 @@ var ( procCallNextHookEx = moduser32.NewProc("CallNextHookEx") procGetForegroundWindow = moduser32.NewProc("GetForegroundWindow") procUpdateLayeredWindow = moduser32.NewProc("UpdateLayeredWindow") + getDisplayConfig = moduser32.NewProc("GetDisplayConfigBufferSizes") + queryDisplayConfig = moduser32.NewProc("QueryDisplayConfig") procSystemParametersInfo = moduser32.NewProc("SystemParametersInfoW") procSetClassLong = moduser32.NewProc("SetClassLongW")