[v3 windows] Support GetScreens and GetPrimaryScreen

This commit is contained in:
Lea Anthony 2023-06-01 20:20:59 +10:00
commit 82ec56bc7d
No known key found for this signature in database
GPG key ID: 33DAF7BB90A58405
5 changed files with 179 additions and 111 deletions

View file

@ -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() {

View file

@ -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
}

View file

@ -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
)

View file

@ -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
}

View file

@ -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")