mirror of
https://github.com/wailsapp/wails.git
synced 2025-05-02 22:49:31 +08:00
403 lines
11 KiB
Go
403 lines
11 KiB
Go
//go:build windows
|
|
|
|
package application
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"slices"
|
|
"sync"
|
|
"syscall"
|
|
"unsafe"
|
|
|
|
"github.com/wailsapp/go-webview2/webviewloader"
|
|
"github.com/wailsapp/wails/v3/internal/operatingsystem"
|
|
|
|
"github.com/wailsapp/wails/v3/pkg/events"
|
|
"github.com/wailsapp/wails/v3/pkg/w32"
|
|
)
|
|
|
|
type windowsApp struct {
|
|
parent *App
|
|
|
|
windowClass w32.WNDCLASSEX
|
|
instance w32.HINSTANCE
|
|
|
|
windowMap map[w32.HWND]*windowsWebviewWindow
|
|
windowMapLock sync.RWMutex
|
|
|
|
systrayMap map[w32.HMENU]*windowsSystemTray
|
|
systrayMapLock sync.RWMutex
|
|
|
|
mainThreadID w32.HANDLE
|
|
mainThreadWindowHWND w32.HWND
|
|
|
|
// Windows hidden by application.Hide()
|
|
hiddenWindows []*windowsWebviewWindow
|
|
focusedWindow w32.HWND
|
|
|
|
// system theme
|
|
isCurrentlyDarkMode bool
|
|
currentWindowID uint
|
|
}
|
|
|
|
func (m *windowsApp) isDarkMode() bool {
|
|
return w32.IsCurrentlyDarkMode()
|
|
}
|
|
|
|
func (m *windowsApp) isOnMainThread() bool {
|
|
return m.mainThreadID == w32.GetCurrentThreadId()
|
|
}
|
|
|
|
func (m *windowsApp) GetFlags(options Options) map[string]any {
|
|
if options.Flags == nil {
|
|
options.Flags = make(map[string]any)
|
|
}
|
|
options.Flags["system"] = map[string]any{
|
|
"resizeHandleWidth": w32.GetSystemMetrics(w32.SM_CXSIZEFRAME),
|
|
"resizeHandleHeight": w32.GetSystemMetrics(w32.SM_CYSIZEFRAME),
|
|
}
|
|
return options.Flags
|
|
}
|
|
|
|
func (m *windowsApp) getWindowForHWND(hwnd w32.HWND) *windowsWebviewWindow {
|
|
m.windowMapLock.RLock()
|
|
defer m.windowMapLock.RUnlock()
|
|
return m.windowMap[hwnd]
|
|
}
|
|
|
|
func getNativeApplication() *windowsApp {
|
|
return globalApplication.impl.(*windowsApp)
|
|
}
|
|
|
|
func (m *windowsApp) hide() {
|
|
// Get the current focussed window
|
|
m.focusedWindow = w32.GetForegroundWindow()
|
|
|
|
// Iterate over all windows and hide them if they aren't already hidden
|
|
for _, window := range m.windowMap {
|
|
if window.isVisible() {
|
|
// Add to hidden windows
|
|
m.hiddenWindows = append(m.hiddenWindows, window)
|
|
window.hide()
|
|
}
|
|
}
|
|
// Switch focus to the next application
|
|
hwndNext := w32.GetWindow(m.mainThreadWindowHWND, w32.GW_HWNDNEXT)
|
|
w32.SetForegroundWindow(hwndNext)
|
|
}
|
|
|
|
func (m *windowsApp) show() {
|
|
// Iterate over all windows and show them if they were previously hidden
|
|
for _, window := range m.hiddenWindows {
|
|
window.show()
|
|
}
|
|
// Show the foreground window
|
|
w32.SetForegroundWindow(m.focusedWindow)
|
|
}
|
|
|
|
func (m *windowsApp) on(_ uint) {
|
|
}
|
|
|
|
func (m *windowsApp) setIcon(_ []byte) {
|
|
}
|
|
|
|
func (m *windowsApp) name() string {
|
|
//appName := C.getAppName()
|
|
//defer C.free(unsafe.Pointer(appName))
|
|
//return C.GoString(appName)
|
|
return ""
|
|
}
|
|
|
|
func (m *windowsApp) getCurrentWindowID() uint {
|
|
return m.currentWindowID
|
|
}
|
|
|
|
func (m *windowsApp) setApplicationMenu(menu *Menu) {
|
|
if menu == nil {
|
|
// Create a default menu for windows
|
|
menu = DefaultApplicationMenu()
|
|
}
|
|
menu.Update()
|
|
|
|
m.parent.ApplicationMenu = menu
|
|
}
|
|
|
|
func (m *windowsApp) run() error {
|
|
|
|
m.setupCommonEvents()
|
|
for eventID := range m.parent.applicationEventListeners {
|
|
m.on(eventID)
|
|
}
|
|
// EmitEvent application started event
|
|
applicationEvents <- &ApplicationEvent{
|
|
Id: uint(events.Windows.ApplicationStarted),
|
|
ctx: blankApplicationEventContext,
|
|
}
|
|
|
|
// Check if there is 1 parameter passed to the application
|
|
// and if the extension matches the options.FileAssociations string
|
|
if len(os.Args) == 2 {
|
|
arg := os.Args[1]
|
|
ext := filepath.Ext(arg)
|
|
if slices.Contains(m.parent.options.FileAssociations, ext) {
|
|
eventContext := newApplicationEventContext()
|
|
eventContext.setOpenedWithFile(arg)
|
|
// EmitEvent application started event
|
|
applicationEvents <- &ApplicationEvent{
|
|
Id: uint(events.Common.ApplicationOpenedWithFile),
|
|
ctx: eventContext,
|
|
}
|
|
}
|
|
}
|
|
|
|
_ = m.runMainLoop()
|
|
|
|
return nil
|
|
}
|
|
|
|
func (m *windowsApp) destroy() {
|
|
if !globalApplication.shouldQuit() {
|
|
return
|
|
}
|
|
globalApplication.cleanup()
|
|
// destroy the main thread window
|
|
w32.DestroyWindow(m.mainThreadWindowHWND)
|
|
// Post a quit message to the main thread
|
|
w32.PostQuitMessage(0)
|
|
}
|
|
|
|
func (m *windowsApp) init() {
|
|
// Register the window class
|
|
|
|
icon := w32.LoadIconWithResourceID(m.instance, w32.IDI_APPLICATION)
|
|
|
|
m.windowClass.Size = uint32(unsafe.Sizeof(m.windowClass))
|
|
m.windowClass.Style = w32.CS_HREDRAW | w32.CS_VREDRAW
|
|
m.windowClass.WndProc = syscall.NewCallback(m.wndProc)
|
|
m.windowClass.Instance = m.instance
|
|
m.windowClass.Background = w32.COLOR_BTNFACE + 1
|
|
m.windowClass.Icon = icon
|
|
m.windowClass.Cursor = w32.LoadCursorWithResourceID(0, w32.IDC_ARROW)
|
|
m.windowClass.ClassName = w32.MustStringToUTF16Ptr(m.parent.options.Windows.WndClass)
|
|
m.windowClass.MenuName = nil
|
|
m.windowClass.IconSm = icon
|
|
|
|
if ret := w32.RegisterClassEx(&m.windowClass); ret == 0 {
|
|
panic(syscall.GetLastError())
|
|
}
|
|
m.isCurrentlyDarkMode = w32.IsCurrentlyDarkMode()
|
|
}
|
|
|
|
func (m *windowsApp) wndProc(hwnd w32.HWND, msg uint32, wParam, lParam uintptr) uintptr {
|
|
|
|
// Handle the invoke callback
|
|
if msg == wmInvokeCallback {
|
|
m.invokeCallback(wParam, lParam)
|
|
return 0
|
|
}
|
|
|
|
// If the WndProcInterceptor is set in options, pass the message on
|
|
if m.parent.options.Windows.WndProcInterceptor != nil {
|
|
returnValue, shouldReturn := m.parent.options.Windows.WndProcInterceptor(hwnd, msg, wParam, lParam)
|
|
if shouldReturn {
|
|
return returnValue
|
|
}
|
|
}
|
|
|
|
// Handle the main thread window
|
|
// Quit the application if requested
|
|
// Reprocess and cache screens when display settings change
|
|
if hwnd == m.mainThreadWindowHWND {
|
|
if msg == w32.WM_ENDSESSION || msg == w32.WM_DESTROY || msg == w32.WM_CLOSE {
|
|
globalApplication.Quit()
|
|
}
|
|
if msg == w32.WM_DISPLAYCHANGE || (msg == w32.WM_SETTINGCHANGE && wParam == w32.SPI_SETWORKAREA) {
|
|
err := m.processAndCacheScreens()
|
|
if err != nil {
|
|
m.parent.error(err.Error())
|
|
}
|
|
}
|
|
}
|
|
|
|
switch msg {
|
|
|
|
/*
|
|
case WM_UAHDRAWMENU:
|
|
{
|
|
UAHMENU* pUDM = (UAHMENU*)lParam;
|
|
RECT rc = { 0 };
|
|
|
|
// get the menubar rect
|
|
{
|
|
MENUBARINFO mbi = { sizeof(mbi) };
|
|
GetMenuBarInfo(hWnd, OBJID_MENU, 0, &mbi);
|
|
|
|
RECT rcWindow;
|
|
GetWindowRect(hWnd, &rcWindow);
|
|
|
|
// the rcBar is offset by the window rect
|
|
rc = mbi.rcBar;
|
|
OffsetRect(&rc, -rcWindow.left, -rcWindow.top);
|
|
}
|
|
|
|
FillRect(pUDM->hdc, &rc, g_brBarBackground);
|
|
|
|
return true;
|
|
}
|
|
*/
|
|
case w32.WM_SETTINGCHANGE:
|
|
settingChanged := w32.UTF16PtrToString((*uint16)(unsafe.Pointer(lParam)))
|
|
if settingChanged == "ImmersiveColorSet" {
|
|
isDarkMode := w32.IsCurrentlyDarkMode()
|
|
if isDarkMode != m.isCurrentlyDarkMode {
|
|
eventContext := newApplicationEventContext()
|
|
eventContext.setIsDarkMode(isDarkMode)
|
|
applicationEvents <- &ApplicationEvent{
|
|
Id: uint(events.Windows.SystemThemeChanged),
|
|
ctx: eventContext,
|
|
}
|
|
m.isCurrentlyDarkMode = isDarkMode
|
|
}
|
|
}
|
|
return 0
|
|
case w32.WM_POWERBROADCAST:
|
|
switch wParam {
|
|
case w32.PBT_APMPOWERSTATUSCHANGE:
|
|
applicationEvents <- newApplicationEvent(events.Windows.APMPowerStatusChange)
|
|
case w32.PBT_APMSUSPEND:
|
|
applicationEvents <- newApplicationEvent(events.Windows.APMSuspend)
|
|
case w32.PBT_APMRESUMEAUTOMATIC:
|
|
applicationEvents <- newApplicationEvent(events.Windows.APMResumeAutomatic)
|
|
case w32.PBT_APMRESUMESUSPEND:
|
|
applicationEvents <- newApplicationEvent(events.Windows.APMResumeSuspend)
|
|
case w32.PBT_POWERSETTINGCHANGE:
|
|
applicationEvents <- newApplicationEvent(events.Windows.APMPowerSettingChange)
|
|
}
|
|
return 0
|
|
}
|
|
|
|
if window, ok := m.windowMap[hwnd]; ok {
|
|
return window.WndProc(msg, wParam, lParam)
|
|
}
|
|
|
|
m.systrayMapLock.Lock()
|
|
systray, ok := m.systrayMap[hwnd]
|
|
m.systrayMapLock.Unlock()
|
|
if ok {
|
|
return systray.wndProc(msg, wParam, lParam)
|
|
}
|
|
|
|
// Dispatch the message to the appropriate window
|
|
|
|
return w32.DefWindowProc(hwnd, msg, wParam, lParam)
|
|
}
|
|
|
|
func (m *windowsApp) registerWindow(result *windowsWebviewWindow) {
|
|
m.windowMapLock.Lock()
|
|
m.windowMap[result.hwnd] = result
|
|
m.windowMapLock.Unlock()
|
|
}
|
|
|
|
func (m *windowsApp) registerSystemTray(result *windowsSystemTray) {
|
|
m.systrayMapLock.Lock()
|
|
defer m.systrayMapLock.Unlock()
|
|
m.systrayMap[result.hwnd] = result
|
|
}
|
|
|
|
func (m *windowsApp) unregisterSystemTray(result *windowsSystemTray) {
|
|
m.systrayMapLock.Lock()
|
|
defer m.systrayMapLock.Unlock()
|
|
delete(m.systrayMap, result.hwnd)
|
|
}
|
|
|
|
func (m *windowsApp) unregisterWindow(w *windowsWebviewWindow) {
|
|
m.windowMapLock.Lock()
|
|
delete(m.windowMap, w.hwnd)
|
|
m.windowMapLock.Unlock()
|
|
|
|
// If this was the last window...
|
|
if len(m.windowMap) == 0 && !m.parent.options.Windows.DisableQuitOnLastWindowClosed {
|
|
w32.PostQuitMessage(0)
|
|
}
|
|
}
|
|
|
|
func setupDPIAwareness() error {
|
|
// https://learn.microsoft.com/en-us/windows/win32/hidpi/setting-the-default-dpi-awareness-for-a-process
|
|
// https://learn.microsoft.com/en-us/windows/win32/hidpi/high-dpi-desktop-application-development-on-windows
|
|
|
|
if w32.HasSetProcessDpiAwarenessContextFunc() {
|
|
// This is most recent version with the best results
|
|
// supported beginning with Windows 10, version 1703
|
|
return w32.SetProcessDpiAwarenessContext(w32.DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2)
|
|
}
|
|
|
|
if w32.HasSetProcessDpiAwarenessFunc() {
|
|
// Supported beginning with Windows 8.1
|
|
return w32.SetProcessDpiAwareness(w32.PROCESS_PER_MONITOR_DPI_AWARE)
|
|
}
|
|
|
|
if w32.HasSetProcessDPIAwareFunc() {
|
|
// If none of the above is supported, fallback to SetProcessDPIAware
|
|
// which is supported beginning with Windows Vista
|
|
return w32.SetProcessDPIAware()
|
|
}
|
|
|
|
return fmt.Errorf("no DPI awareness method supported")
|
|
}
|
|
|
|
func newPlatformApp(app *App) *windowsApp {
|
|
|
|
err := setupDPIAwareness()
|
|
if err != nil {
|
|
app.error(err.Error())
|
|
}
|
|
|
|
result := &windowsApp{
|
|
parent: app,
|
|
instance: w32.GetModuleHandle(""),
|
|
windowMap: make(map[w32.HWND]*windowsWebviewWindow),
|
|
systrayMap: make(map[w32.HWND]*windowsSystemTray),
|
|
}
|
|
|
|
err = result.processAndCacheScreens()
|
|
if err != nil {
|
|
app.fatal(err.Error())
|
|
}
|
|
|
|
result.init()
|
|
result.initMainLoop()
|
|
|
|
return result
|
|
}
|
|
|
|
func (a *App) logPlatformInfo() {
|
|
var args []any
|
|
args = append(args, "Go-WebView2Loader", webviewloader.UsingGoWebview2Loader)
|
|
webviewVersion, err := webviewloader.GetAvailableCoreWebView2BrowserVersionString(a.options.Windows.WebviewBrowserPath)
|
|
if err != nil {
|
|
args = append(args, "WebView2", "Error: "+err.Error())
|
|
} else {
|
|
args = append(args, "WebView2", webviewVersion)
|
|
}
|
|
|
|
osInfo, _ := operatingsystem.Info()
|
|
args = append(args, osInfo.AsLogSlice()...)
|
|
|
|
a.info("Platform Info:", args...)
|
|
}
|
|
|
|
func (a *App) platformEnvironment() map[string]any {
|
|
result := map[string]any{}
|
|
webviewVersion, _ := webviewloader.GetAvailableCoreWebView2BrowserVersionString(a.options.Windows.WebviewBrowserPath)
|
|
result["Go-WebView2Loader"] = webviewloader.UsingGoWebview2Loader
|
|
result["WebView2"] = webviewVersion
|
|
return result
|
|
}
|
|
|
|
func fatalHandler(errFunc func(error)) {
|
|
w32.Fatal = errFunc
|
|
return
|
|
}
|