5
0
mirror of https://github.com/wailsapp/wails.git synced 2025-05-16 17:09:28 +08:00

[v3 windows] Support irregular shaped windows

This commit is contained in:
Lea Anthony 2023-05-06 15:04:38 +10:00 committed by Misite Bao
parent 4ad2475ed6
commit cb28de47f8
9 changed files with 214 additions and 37 deletions

View File

@ -112,7 +112,7 @@ Webview Window Interface Methods
| Feature | Windows | Linux | Mac | Notes | | Feature | Windows | Linux | Mac | Notes |
|------------|---------|-------|-----|-------| |------------|---------|-------|-----|-------|
| GetAll | | | Y | | | GetAll | Y | | Y | |
| GetPrimary | | | Y | | | GetPrimary | | | Y | |
| GetCurrent | | | Y | | | GetCurrent | | | Y | |
@ -148,13 +148,11 @@ Webview Window Interface Methods
| SetZoom | | | Y | Set view scale | | SetZoom | | | Y | Set view scale |
| Screen | | | Y | Get screen for window | | Screen | | | Y | Get screen for window |
### Window Options ### Window Options
A 'Y' in the table below indicates that the option has been tested and is applied when the window is created. A 'Y' in the table below indicates that the option has been tested and is applied when the window is created.
An 'X' indicates that the option is not supported by the platform. An 'X' indicates that the option is not supported by the platform.
| Feature | Windows | Linux | Mac | Notes | | Feature | Windows | Linux | Mac | Notes |
|---------------------------------|---------|-------|-----|--------------------------------------------| |---------------------------------|---------|-------|-----|--------------------------------------------|
| Name | | | | | | Name | | | | |
@ -288,4 +286,31 @@ Built-in plugin support:
- [x] Translucency - [x] Translucency
- [x] Custom Themes - [x] Custom Themes
### Windows Options
| Feature | Default | Notes |
|-----------------------------------|---------|---------------------------------------------|
| BackdropType | | |
| DisableIcon | | |
| Theme | | |
| CustomTheme | | |
| DisableFramelessWindowDecorations | | |
| WindowMask | nil | Makes the window the contents of the bitmap |
// Select the type of translucent backdrop. Requires Windows 11 22621 or later.
BackdropType BackdropType
// Disable the icon in the titlebar
DisableIcon bool
// Theme. Defaults to SystemDefault which will use whatever the system theme is. The application will follow system theme changes.
Theme Theme
// Custom colours for dark/light mode
CustomTheme *ThemeSettings
// Disable all window decorations in Frameless mode, which means no "Aero Shadow" and no "Rounded Corner" will be shown.
// "Rounded Corners" are only available on Windows 11.
DisableFramelessWindowDecorations bool
// WindowMask is used to set the window shape. Use a PNG with an alpha channel to create a custom shape.
WindowMask []byte
## Linux Specific ## Linux Specific

View File

@ -0,0 +1,20 @@
package application
import (
"bytes"
"image"
"image/draw"
"image/png"
)
func pngToImage(data []byte) (*image.RGBA, error) {
img, err := png.Decode(bytes.NewReader(data))
if err != nil {
return nil, err
}
bounds := img.Bounds()
rgba := image.NewRGBA(bounds)
draw.Draw(rgba, bounds, img, bounds.Min, draw.Src)
return rgba, nil
}

View File

@ -31,6 +31,9 @@ type WindowsWindow struct {
// Disable all window decorations in Frameless mode, which means no "Aero Shadow" and no "Rounded Corner" will be shown. // Disable all window decorations in Frameless mode, which means no "Aero Shadow" and no "Rounded Corner" will be shown.
// "Rounded Corners" are only available on Windows 11. // "Rounded Corners" are only available on Windows 11.
DisableFramelessWindowDecorations bool DisableFramelessWindowDecorations bool
// WindowMask is used to set the window shape. Use a PNG with an alpha channel to create a custom shape.
WindowMask []byte
} }
type Theme int type Theme int

View File

@ -5,6 +5,7 @@ package application
import ( import (
"errors" "errors"
"fmt" "fmt"
"github.com/samber/lo"
"strconv" "strconv"
"unicode/utf16" "unicode/utf16"
"unsafe" "unsafe"
@ -42,11 +43,13 @@ func (w *windowsWebviewWindow) setSize(width, height int) {
} }
func (w *windowsWebviewWindow) setAlwaysOnTop(alwaysOnTop bool) { func (w *windowsWebviewWindow) setAlwaysOnTop(alwaysOnTop bool) {
position := w32.HWND_NOTOPMOST w32.SetWindowPos(w.hwnd,
if alwaysOnTop { lo.Ternary(alwaysOnTop, w32.HWND_TOPMOST, w32.HWND_NOTOPMOST),
position = w32.HWND_TOPMOST 0,
} 0,
w32.SetWindowPos(w.hwnd, position, 0, 0, 0, 0, uint(w32.SWP_NOMOVE|w32.SWP_NOSIZE)) 0,
0,
uint(w32.SWP_NOMOVE|w32.SWP_NOSIZE))
} }
func (w *windowsWebviewWindow) setURL(url string) { func (w *windowsWebviewWindow) setURL(url string) {
@ -144,14 +147,6 @@ func (w *windowsWebviewWindow) run() {
w.disableIcon() w.disableIcon()
} }
switch options.BackgroundType {
case BackgroundTypeSolid:
w.setBackgroundColour(options.BackgroundColour)
case BackgroundTypeTransparent:
case BackgroundTypeTranslucent:
w.setBackdropType(options.Windows.BackdropType)
}
// Process the theme // Process the theme
switch options.Windows.Theme { switch options.Windows.Theme {
case SystemDefault: case SystemDefault:
@ -165,6 +160,14 @@ func (w *windowsWebviewWindow) run() {
w.updateTheme(true) w.updateTheme(true)
} }
switch options.BackgroundType {
case BackgroundTypeSolid:
w.setBackgroundColour(options.BackgroundColour)
case BackgroundTypeTransparent:
case BackgroundTypeTranslucent:
w.setBackdropType(options.Windows.BackdropType)
}
// Process StartState // Process StartState
switch options.StartState { switch options.StartState {
case WindowStateMaximised: case WindowStateMaximised:
@ -177,6 +180,11 @@ func (w *windowsWebviewWindow) run() {
w.fullscreen() w.fullscreen()
} }
// Process window mask
if options.Windows.WindowMask != nil {
w.setWindowMask(options.Windows.WindowMask)
}
w.setForeground() w.setForeground()
if !options.Hidden { if !options.Hidden {
@ -497,22 +505,14 @@ func (w *windowsWebviewWindow) openContextMenu(menu *Menu, data *ContextMenuData
func (w *windowsWebviewWindow) setStyle(b bool, style int) { func (w *windowsWebviewWindow) setStyle(b bool, style int) {
currentStyle := int(w32.GetWindowLongPtr(w.hwnd, w32.GWL_STYLE)) currentStyle := int(w32.GetWindowLongPtr(w.hwnd, w32.GWL_STYLE))
if currentStyle != 0 { if currentStyle != 0 {
if b { currentStyle = lo.Ternary(b, currentStyle|style, currentStyle&^style)
currentStyle |= style
} else {
currentStyle &^= style
}
w32.SetWindowLongPtr(w.hwnd, w32.GWL_STYLE, uintptr(currentStyle)) w32.SetWindowLongPtr(w.hwnd, w32.GWL_STYLE, uintptr(currentStyle))
} }
} }
func (w *windowsWebviewWindow) setExStyle(b bool, style int) { func (w *windowsWebviewWindow) setExStyle(b bool, style int) {
currentStyle := int(w32.GetWindowLongPtr(w.hwnd, w32.GWL_EXSTYLE)) currentStyle := int(w32.GetWindowLongPtr(w.hwnd, w32.GWL_EXSTYLE))
if currentStyle != 0 { if currentStyle != 0 {
if b { currentStyle = lo.Ternary(b, currentStyle|style, currentStyle&^style)
currentStyle |= style
} else {
currentStyle &^= style
}
w32.SetWindowLongPtr(w.hwnd, w32.GWL_EXSTYLE, uintptr(currentStyle)) w32.SetWindowLongPtr(w.hwnd, w32.GWL_EXSTYLE, uintptr(currentStyle))
} }
} }
@ -529,13 +529,7 @@ func (w *windowsWebviewWindow) setBackdropType(backdropType BackdropType) {
w32.SetWindowCompositionAttribute(w.hwnd, &data) w32.SetWindowCompositionAttribute(w.hwnd, &data)
} else { } else {
backdropValue := backdropType w32.DwmSetWindowAttribute(w.hwnd, w32.DwmwaSystemBackdropType, w32.PVOID(&backdropType), unsafe.Sizeof(backdropType))
// We default to None, but in w32 None = 1 and Auto = 0
// So we check if the value given was Auto and set it to 0
if backdropType == Auto {
backdropValue = None
}
w32.DwmSetWindowAttribute(w.hwnd, w32.DwmwaSystemBackdropType, w32.LPCVOID(&backdropValue), uint32(unsafe.Sizeof(backdropValue)))
} }
} }
@ -652,6 +646,13 @@ func (w *windowsWebviewWindow) WndProc(msg uint32, wparam, lparam uintptr) uintp
} }
if w.parent.options.Windows.WindowMask != nil {
switch msg {
case w32.WM_NCHITTEST:
return w32.HTCAPTION
}
}
if options := w.parent.options; options.Frameless { if options := w.parent.options; options.Frameless {
switch msg { switch msg {
case w32.WM_ACTIVATE: case w32.WM_ACTIVATE:
@ -813,6 +814,50 @@ func (w *windowsWebviewWindow) scaleToDefaultDPI(width, height int) (int, int) {
return scaledWidth, scaledHeight return scaledWidth, scaledHeight
} }
func (w *windowsWebviewWindow) setWindowMask(imageData []byte) {
// Set the window to a WS_EX_LAYERED window
newStyle := w32.GetWindowLong(w.hwnd, w32.GWL_EXSTYLE) | w32.WS_EX_LAYERED
if w.isAlwaysOnTop() {
newStyle |= w32.WS_EX_TOPMOST
}
// Save the current window style
w.previousWindowExStyle = uint32(w32.GetWindowLong(w.hwnd, w32.GWL_EXSTYLE))
w32.SetWindowLong(w.hwnd, w32.GWL_EXSTYLE, uint32(newStyle))
data, err := pngToImage(imageData)
if err != nil {
panic(err)
}
bitmap, err := w32.CreateHBITMAPFromImage(data)
hdc := w32.CreateCompatibleDC(0)
defer w32.DeleteDC(hdc)
oldBitmap := w32.SelectObject(hdc, bitmap)
defer w32.SelectObject(hdc, oldBitmap)
screenDC := w32.GetDC(0)
defer w32.ReleaseDC(0, screenDC)
size := w32.SIZE{CX: int32(data.Bounds().Dx()), CY: int32(data.Bounds().Dy())}
ptSrc := w32.POINT{X: 0, Y: 0}
ptDst := w32.POINT{X: int32(w.width()), Y: int32(w.height())}
blend := w32.BLENDFUNCTION{
BlendOp: w32.AC_SRC_OVER,
BlendFlags: 0,
SourceConstantAlpha: 255,
AlphaFormat: w32.AC_SRC_ALPHA,
}
w32.UpdateLayeredWindow(w.hwnd, screenDC, &ptDst, &size, hdc, &ptSrc, 0, &blend, w32.ULW_ALPHA)
}
func (w *windowsWebviewWindow) isAlwaysOnTop() bool {
return w32.GetWindowLong(w.hwnd, w32.GWL_EXSTYLE)&w32.WS_EX_TOPMOST != 0
}
func ScaleWithDPI(pixels int, dpi uint) int { func ScaleWithDPI(pixels int, dpi uint) int {
return (pixels * int(dpi)) / 96 return (pixels * int(dpi)) / 96
} }

View File

@ -3549,3 +3549,11 @@ const (
SIF_TRACKPOS = 16 SIF_TRACKPOS = 16
SIF_ALL = SIF_RANGE + SIF_PAGE + SIF_POS + SIF_TRACKPOS SIF_ALL = SIF_RANGE + SIF_PAGE + SIF_POS + SIF_TRACKPOS
) )
const AC_SRC_OVER = 0
const AC_SRC_ALPHA = 1
const ULW_COLORKEY = 1
const ULW_ALPHA = 2
const ULW_OPAQUE = 4
const ULW_EX_NORESIZE = 8

View File

@ -14,12 +14,12 @@ var (
procDwmExtendFrameIntoClientArea = moddwmapi.NewProc("DwmExtendFrameIntoClientArea") procDwmExtendFrameIntoClientArea = moddwmapi.NewProc("DwmExtendFrameIntoClientArea")
) )
func DwmSetWindowAttribute(hwnd HWND, dwAttribute DWMWINDOWATTRIBUTE, pvAttribute LPCVOID, cbAttribute uint32) HRESULT { func DwmSetWindowAttribute(hwnd HWND, dwAttribute DWMWINDOWATTRIBUTE, pvAttribute unsafe.Pointer, cbAttribute uintptr) HRESULT {
ret, _, _ := procDwmSetWindowAttribute.Call( ret, _, _ := procDwmSetWindowAttribute.Call(
hwnd, hwnd,
uintptr(dwAttribute), uintptr(dwAttribute),
uintptr(pvAttribute), uintptr(pvAttribute),
uintptr(cbAttribute)) cbAttribute)
return HRESULT(ret) return HRESULT(ret)
} }

53
v3/pkg/w32/image.go Normal file
View File

@ -0,0 +1,53 @@
package w32
import (
"image"
"syscall"
"unsafe"
)
func CreateHBITMAPFromImage(img *image.RGBA) (HBITMAP, error) {
bounds := img.Bounds()
width, height := bounds.Dx(), bounds.Dy()
// Create a BITMAPINFO structure for the DIB
bmi := BITMAPINFO{
BmiHeader: BITMAPINFOHEADER{
BiSize: uint32(unsafe.Sizeof(BITMAPINFOHEADER{})),
BiWidth: int32(width),
BiHeight: int32(-height), // negative to indicate top-down bitmap
BiPlanes: 1,
BiBitCount: 32,
BiCompression: BI_RGB,
BiSizeImage: uint32(width * height * 4), // RGBA = 4 bytes
},
}
// Create the DIB section
var bits unsafe.Pointer
hbmp := CreateDIBSection(0, &bmi, DIB_RGB_COLORS, &bits, 0, 0)
if hbmp == 0 {
return 0, syscall.GetLastError()
}
// Copy the pixel data from the Go image to the DIB section
for y := 0; y < height; y++ {
for x := 0; x < width; x++ {
i := img.PixOffset(x, y)
r := img.Pix[i+0]
g := img.Pix[i+1]
b := img.Pix[i+2]
a := img.Pix[i+3]
// Write the RGBA pixel data to the DIB section (BGR order)
offset := y*width*4 + x*4
*((*uint8)(unsafe.Pointer(uintptr(bits) + uintptr(offset) + 0))) = b
*((*uint8)(unsafe.Pointer(uintptr(bits) + uintptr(offset) + 1))) = g
*((*uint8)(unsafe.Pointer(uintptr(bits) + uintptr(offset) + 2))) = r
*((*uint8)(unsafe.Pointer(uintptr(bits) + uintptr(offset) + 3))) = a
}
}
return hbmp, nil
}

View File

@ -459,6 +459,13 @@ type BITMAP struct {
BmBits unsafe.Pointer BmBits unsafe.Pointer
} }
type BLENDFUNCTION struct {
BlendOp byte
BlendFlags byte
SourceConstantAlpha byte
AlphaFormat byte
}
// http://msdn.microsoft.com/en-us/library/windows/desktop/dd183567.aspx // http://msdn.microsoft.com/en-us/library/windows/desktop/dd183567.aspx
type DIBSECTION struct { type DIBSECTION struct {
DsBm BITMAP DsBm BITMAP

View File

@ -139,6 +139,7 @@ var (
procUnhookWindowsHookEx = moduser32.NewProc("UnhookWindowsHookEx") procUnhookWindowsHookEx = moduser32.NewProc("UnhookWindowsHookEx")
procCallNextHookEx = moduser32.NewProc("CallNextHookEx") procCallNextHookEx = moduser32.NewProc("CallNextHookEx")
procGetForegroundWindow = moduser32.NewProc("GetForegroundWindow") procGetForegroundWindow = moduser32.NewProc("GetForegroundWindow")
procUpdateLayeredWindow = moduser32.NewProc("UpdateLayeredWindow")
procSystemParametersInfo = moduser32.NewProc("SystemParametersInfoW") procSystemParametersInfo = moduser32.NewProc("SystemParametersInfoW")
procSetClassLong = moduser32.NewProc("SetClassLongW") procSetClassLong = moduser32.NewProc("SetClassLongW")
@ -234,6 +235,21 @@ func UpdateWindow(hwnd HWND) bool {
return ret != 0 return ret != 0
} }
func UpdateLayeredWindow(hwnd HWND, hdcDst HDC, pptDst *POINT, psize *SIZE,
hdcSrc HDC, pptSrc *POINT, crKey COLORREF, pblend *BLENDFUNCTION, dwFlags DWORD) bool {
ret, _, _ := procUpdateLayeredWindow.Call(
hwnd,
hdcDst,
uintptr(unsafe.Pointer(pptDst)),
uintptr(unsafe.Pointer(psize)),
hdcSrc,
uintptr(unsafe.Pointer(pptSrc)),
uintptr(crKey),
uintptr(unsafe.Pointer(pblend)),
uintptr(dwFlags))
return ret != 0
}
func PostThreadMessage(threadID HANDLE, msg int, wp, lp uintptr) { func PostThreadMessage(threadID HANDLE, msg int, wp, lp uintptr) {
procPostThreadMessageW.Call(threadID, uintptr(msg), wp, lp) procPostThreadMessageW.Call(threadID, uintptr(msg), wp, lp)
} }