[v3 Windows] Support setMin/MaxSize, setPosition

This commit is contained in:
Lea Anthony 2023-05-01 10:52:46 +10:00 committed by Misite Bao
commit 6e92a4f71e
6 changed files with 263 additions and 85 deletions

View file

@ -26,55 +26,54 @@ Application interface methods
Webview Window Interface Methods
| Method | Windows | Linux | Mac | Notes |
|----------------------------------------------------|---------|-------|-----|-------|
| setTitle(title string) | Y | | Y | |
| setSize(width, height int) | Y | | Y | |
| setAlwaysOnTop(alwaysOnTop bool) | Y | | Y | |
| setURL(url string) | | | Y | |
| setResizable(resizable bool) | Y | | Y | |
| setMinSize(width, height int) | | | Y | |
| setMaxSize(width, height int) | | | Y | |
| execJS(js string) | | | Y | |
| restore() | | | Y | |
| setBackgroundColour(color RGBA) | Y | | Y | |
| run() | Y | | Y | |
| center() | Y | | Y | |
| size() (int, int) | | | Y | |
| width() int | Y | | Y | |
| height() int | Y | | Y | |
| position() (int, int) | Y | | Y | |
| destroy() | | | Y | |
| reload() | | | Y | |
| forceReload() | | | Y | |
| toggleDevTools() | | | Y | |
| zoomReset() | | | Y | |
| zoomIn() | | | Y | |
| zoomOut() | | | Y | |
| getZoom() float64 | | | Y | |
| setZoom(zoom float64) | | | Y | |
| close() | | | Y | |
| zoom() | | | Y | |
| setHTML(html string) | | | Y | |
| setPosition(x int, y int) | | | Y | |
| on(eventID uint) | | | Y | |
| minimise() | Y | | Y | |
| unminimise() | Y | | Y | |
| maximise() | Y | | Y | |
| unmaximise() | Y | | Y | |
| fullscreen() | | | Y | |
| unfullscreen() | | | Y | |
| isMinimised() bool | Y | | Y | |
| isMaximised() bool | Y | | Y | |
| isFullscreen() bool | | | Y | |
| disableSizeConstraints() | | | Y | |
| setFullscreenButtonEnabled(enabled bool) | | | Y | |
| show() | Y | | Y | |
| hide() | Y | | Y | |
| getScreen() (*Screen, error) | | | Y | |
| setFrameless(bool) | | | Y | |
| openContextMenu(menu *Menu, data *ContextMenuData) | | | Y | |
| nativeWindowHandle() (uintptr, error) | Y | | | |
| Method | Windows | Linux | Mac | Notes |
|----------------------------------------------------|---------|-------|-----|------------------------------------------|
| center() | Y | | Y | |
| close() | | | Y | |
| destroy() | | | Y | |
| disableSizeConstraints() | | | Y | |
| execJS(js string) | | | Y | |
| forceReload() | | | Y | |
| fullscreen() | | | Y | |
| getScreen() (*Screen, error) | | | Y | |
| getZoom() float64 | | | Y | |
| height() int | Y | | Y | |
| hide() | Y | | Y | |
| isFullscreen() bool | | | Y | |
| isMaximised() bool | Y | | Y | |
| isMinimised() bool | Y | | Y | |
| maximise() | Y | | Y | |
| minimise() | Y | | Y | |
| nativeWindowHandle() (uintptr, error) | Y | | | |
| on(eventID uint) | | | Y | |
| openContextMenu(menu *Menu, data *ContextMenuData) | | | Y | |
| position() (int, int) | Y | | Y | |
| reload() | | | Y | |
| run() | Y | | Y | |
| setAlwaysOnTop(alwaysOnTop bool) | Y | | Y | |
| setBackgroundColour(color RGBA) | Y | | Y | |
| setFrameless(bool) | | | Y | |
| setFullscreenButtonEnabled(enabled bool) | X | | Y | There is no fullscreen button in Windows |
| setHTML(html string) | | | Y | |
| setMaxSize(width, height int) | Y | | Y | |
| setMinSize(width, height int) | Y | | Y | |
| setPosition(x int, y int) | Y | | Y | |
| setResizable(resizable bool) | Y | | Y | |
| setSize(width, height int) | Y | | Y | |
| setTitle(title string) | Y | | Y | |
| setURL(url string) | | | Y | |
| setZoom(zoom float64) | | | Y | |
| show() | Y | | Y | |
| size() (int, int) | | | Y | |
| toggleDevTools() | | | Y | |
| unfullscreen() | | | Y | |
| unmaximise() | Y | | Y | |
| unminimise() | Y | | Y | |
| width() int | Y | | Y | |
| zoom() | | | Y | |
| zoomIn() | | | Y | |
| zoomOut() | | | Y | |
| zoomReset() | | | Y | |
## Runtime

View file

@ -6,10 +6,7 @@ import (
"github.com/wailsapp/wails/v3/pkg/w32"
"runtime"
"sort"
"syscall"
"unsafe"
"github.com/samber/lo"
)
var (
@ -37,7 +34,7 @@ func (m *windowsApp) initMainLoop() {
m.mainThreadWindowHWND = w32.CreateWindowEx(
0,
windowClassName,
lo.Must(syscall.UTF16PtrFromString("__wails_hidden_mainthread")),
w32.MustStringToUTF16Ptr("__wails_hidden_mainthread"),
w32.WS_DISABLED,
w32.CW_USEDEFAULT,
w32.CW_USEDEFAULT,

View file

@ -46,7 +46,7 @@ func (m *MessageProcessor) processWindowMethod(method string, rw http.ResponseWr
window.UnFullscreen()
m.ok(rw)
case "Minimise":
window.Minimize()
window.Minimise()
m.ok(rw)
case "UnMinimise":
window.UnMinimise()

View file

@ -7,10 +7,7 @@ import (
"fmt"
"github.com/wailsapp/wails/v3/pkg/events"
"github.com/wailsapp/wails/v3/pkg/w32"
"syscall"
"unsafe"
"github.com/samber/lo"
)
var showDevTools = func(window unsafe.Pointer) {}
@ -19,6 +16,10 @@ type windowsWebviewWindow struct {
windowImpl unsafe.Pointer
parent *WebviewWindow
hwnd w32.HWND
// Size Restrictions
minWidth, minHeight int
maxWidth, maxHeight int
}
func (w *windowsWebviewWindow) nativeWindowHandle() uintptr {
@ -57,13 +58,13 @@ func (w *windowsWebviewWindow) setResizable(resizable bool) {
}
func (w *windowsWebviewWindow) setMinSize(width, height int) {
//TODO implement me
panic("implement me")
w.minWidth = width
w.minHeight = height
}
func (w *windowsWebviewWindow) setMaxSize(width, height int) {
//TODO implement me
panic("implement me")
w.maxWidth = width
w.maxHeight = height
}
func (w *windowsWebviewWindow) execJS(js string) {
@ -80,8 +81,15 @@ func (w *windowsWebviewWindow) run() {
}
func (w *windowsWebviewWindow) _run() {
var exStyle uint
// Copy options
options := w.parent.options
w.minWidth = options.MinWidth
w.minHeight = options.MinHeight
w.maxWidth = options.MaxWidth
w.maxHeight = options.MaxHeight
var exStyle uint
exStyle = w32.WS_EX_CONTROLPARENT | w32.WS_EX_APPWINDOW
if options.BackgroundType != BackgroundTypeSolid {
exStyle |= w32.WS_EX_NOREDIRECTIONBITMAP
@ -92,7 +100,7 @@ func (w *windowsWebviewWindow) _run() {
w.hwnd = w32.CreateWindowEx(
exStyle,
windowClassName,
lo.Must(syscall.UTF16PtrFromString(options.Title)),
w32.MustStringToUTF16Ptr(options.Title),
w32.WS_OVERLAPPEDWINDOW,
w32.CW_USEDEFAULT,
w32.CW_USEDEFAULT,
@ -139,7 +147,6 @@ func (w *windowsWebviewWindow) _run() {
switch options.Windows.Theme {
case SystemDefault:
w.updateTheme(w32.IsCurrentlyDarkMode())
// Setup a listener to respond to theme changes
w.parent.onApplicationEvent(events.Windows.SystemThemeChanged, func() {
w.updateTheme(w32.IsCurrentlyDarkMode())
})
@ -248,8 +255,9 @@ func (w *windowsWebviewWindow) setHTML(html string) {
}
func (w *windowsWebviewWindow) setPosition(x int, y int) {
//TODO implement me
panic("implement me")
info := w32.GetMonitorInfoForWindow(w.hwnd)
workRect := info.RcWork
w32.SetWindowPos(w.hwnd, w32.HWND_TOP, int(workRect.Left)+x, int(workRect.Top)+y, 0, 0, w32.SWP_NOSIZE)
}
func (w *windowsWebviewWindow) on(eventID uint) {
@ -476,9 +484,75 @@ func (w *windowsWebviewWindow) WndProc(msg uint32, wparam, lparam uintptr) uintp
windowsApp := globalApplication.impl.(*windowsApp)
windowsApp.unregisterWindow(w)
return 0
default:
return w32.DefWindowProc(w.hwnd, msg, wparam, lparam)
case w32.WM_GETMINMAXINFO:
mmi := (*w32.MINMAXINFO)(unsafe.Pointer(lparam))
hasConstraints := false
if w.minWidth > 0 || w.minHeight > 0 {
hasConstraints = true
width, height := w.scaleWithWindowDPI(w.minWidth, w.minHeight)
if width > 0 {
mmi.PtMinTrackSize.X = int32(width)
}
if height > 0 {
mmi.PtMinTrackSize.Y = int32(height)
}
}
if w.maxWidth > 0 || w.maxHeight > 0 {
hasConstraints = true
width, height := w.scaleWithWindowDPI(w.maxWidth, w.maxHeight)
if width > 0 {
mmi.PtMaxTrackSize.X = int32(width)
}
if height > 0 {
mmi.PtMaxTrackSize.Y = int32(height)
}
}
if hasConstraints {
return 0
}
}
return w32.DefWindowProc(w.hwnd, msg, wparam, lparam)
}
func (w *windowsWebviewWindow) DPI() (w32.UINT, w32.UINT) {
if w32.HasGetDpiForWindowFunc() {
// GetDpiForWindow is supported beginning with Windows 10, 1607 and is the most accureate
// one, especially it is consistent with the WM_DPICHANGED event.
dpi := w32.GetDpiForWindow(w.hwnd)
return dpi, dpi
}
if w32.HasGetDPIForMonitorFunc() {
// GetDpiForWindow is supported beginning with Windows 8.1
monitor := w32.MonitorFromWindow(w.hwnd, w32.MONITOR_DEFAULTTONEAREST)
if monitor == 0 {
return 0, 0
}
var dpiX, dpiY w32.UINT
w32.GetDPIForMonitor(monitor, w32.MDT_EFFECTIVE_DPI, &dpiX, &dpiY)
return dpiX, dpiY
}
// If none of the above is supported fallback to the System DPI.
screen := w32.GetDC(0)
x := w32.GetDeviceCaps(screen, w32.LOGPIXELSX)
y := w32.GetDeviceCaps(screen, w32.LOGPIXELSY)
w32.ReleaseDC(0, screen)
return w32.UINT(x), w32.UINT(y)
}
func (w *windowsWebviewWindow) scaleWithWindowDPI(width, height int) (int, int) {
dpix, dpiy := w.DPI()
scaledWidth := ScaleWithDPI(width, dpix)
scaledHeight := ScaleWithDPI(height, dpiy)
return scaledWidth, scaledHeight
}
func ScaleWithDPI(pixels int, dpi uint) int {
return (pixels * int(dpi)) / 96
}
func NewIconFromResource(instance w32.HINSTANCE, resId uint16) (w32.HICON, error) {

118
v3/pkg/w32/screen.go Normal file
View file

@ -0,0 +1,118 @@
//go:build windows
package w32
import (
"fmt"
"syscall"
"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
IsPrimary bool
Width int
Height int
}
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 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))
}
}
if len(errorStrings) > 0 {
returnErr = fmt.Errorf("%v errors encountered: %v", len(errorStrings), errorStrings)
}
return monitorContainer.monitors, returnErr
}

View file

@ -2,6 +2,7 @@ package w32
import (
"fmt"
"github.com/samber/lo"
"log"
"strconv"
"syscall"
@ -113,34 +114,23 @@ func showWindow(hwnd uintptr, cmdshow int) bool {
}
func MustStringToUTF16Ptr(input string) *uint16 {
ret, err := syscall.UTF16PtrFromString(input)
if err != nil {
panic(err)
}
return ret
return lo.Must(syscall.UTF16PtrFromString(input))
}
func MustStringToUTF16uintptr(input string) uintptr {
ret, err := syscall.UTF16PtrFromString(input)
if err != nil {
panic(err)
}
ret := lo.Must(syscall.UTF16PtrFromString(input))
return uintptr(unsafe.Pointer(ret))
}
func MustUTF16FromString(input string) []uint16 {
ret, err := syscall.UTF16FromString(input)
if err != nil {
panic(err)
}
return ret
func MustStringToUTF16(input string) []uint16 {
return lo.Must(syscall.UTF16FromString(input))
}
func CenterWindow(hwnd HWND) {
windowInfo := getWindowInfo(hwnd)
frameless := windowInfo.IsPopup()
info := getMonitorInfo(hwnd)
info := GetMonitorInfoForWindow(hwnd)
workRect := info.RcWork
screenMiddleW := workRect.Left + (workRect.Right-workRect.Left)/2
screenMiddleH := workRect.Top + (workRect.Bottom-workRect.Top)/2
@ -164,7 +154,7 @@ func getWindowInfo(hwnd HWND) *WINDOWINFO {
return &info
}
func getMonitorInfo(hwnd HWND) *MONITORINFO {
func GetMonitorInfoForWindow(hwnd HWND) *MONITORINFO {
currentMonitor := MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST)
var info MONITORINFO
info.CbSize = uint32(unsafe.Sizeof(info))