5
0
mirror of https://github.com/wailsapp/wails.git synced 2025-05-16 08:59:29 +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 |
|------------|---------|-------|-----|-------|
| GetAll | | | Y | |
| GetAll | Y | | Y | |
| GetPrimary | | | Y | |
| GetCurrent | | | Y | |
@ -148,12 +148,10 @@ Webview Window Interface Methods
| SetZoom | | | Y | Set view scale |
| Screen | | | Y | Get screen for window |
### Window Options
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.
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.
| Feature | Windows | Linux | Mac | Notes |
|---------------------------------|---------|-------|-----|--------------------------------------------|
@ -288,4 +286,31 @@ Built-in plugin support:
- [x] Translucency
- [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

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.
// "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
}
type Theme int

View File

@ -5,6 +5,7 @@ package application
import (
"errors"
"fmt"
"github.com/samber/lo"
"strconv"
"unicode/utf16"
"unsafe"
@ -42,11 +43,13 @@ func (w *windowsWebviewWindow) setSize(width, height int) {
}
func (w *windowsWebviewWindow) setAlwaysOnTop(alwaysOnTop bool) {
position := w32.HWND_NOTOPMOST
if alwaysOnTop {
position = w32.HWND_TOPMOST
}
w32.SetWindowPos(w.hwnd, position, 0, 0, 0, 0, uint(w32.SWP_NOMOVE|w32.SWP_NOSIZE))
w32.SetWindowPos(w.hwnd,
lo.Ternary(alwaysOnTop, w32.HWND_TOPMOST, w32.HWND_NOTOPMOST),
0,
0,
0,
0,
uint(w32.SWP_NOMOVE|w32.SWP_NOSIZE))
}
func (w *windowsWebviewWindow) setURL(url string) {
@ -144,14 +147,6 @@ func (w *windowsWebviewWindow) run() {
w.disableIcon()
}
switch options.BackgroundType {
case BackgroundTypeSolid:
w.setBackgroundColour(options.BackgroundColour)
case BackgroundTypeTransparent:
case BackgroundTypeTranslucent:
w.setBackdropType(options.Windows.BackdropType)
}
// Process the theme
switch options.Windows.Theme {
case SystemDefault:
@ -165,6 +160,14 @@ func (w *windowsWebviewWindow) run() {
w.updateTheme(true)
}
switch options.BackgroundType {
case BackgroundTypeSolid:
w.setBackgroundColour(options.BackgroundColour)
case BackgroundTypeTransparent:
case BackgroundTypeTranslucent:
w.setBackdropType(options.Windows.BackdropType)
}
// Process StartState
switch options.StartState {
case WindowStateMaximised:
@ -177,6 +180,11 @@ func (w *windowsWebviewWindow) run() {
w.fullscreen()
}
// Process window mask
if options.Windows.WindowMask != nil {
w.setWindowMask(options.Windows.WindowMask)
}
w.setForeground()
if !options.Hidden {
@ -497,22 +505,14 @@ func (w *windowsWebviewWindow) openContextMenu(menu *Menu, data *ContextMenuData
func (w *windowsWebviewWindow) setStyle(b bool, style int) {
currentStyle := int(w32.GetWindowLongPtr(w.hwnd, w32.GWL_STYLE))
if currentStyle != 0 {
if b {
currentStyle |= style
} else {
currentStyle &^= style
}
currentStyle = lo.Ternary(b, currentStyle|style, currentStyle&^style)
w32.SetWindowLongPtr(w.hwnd, w32.GWL_STYLE, uintptr(currentStyle))
}
}
func (w *windowsWebviewWindow) setExStyle(b bool, style int) {
currentStyle := int(w32.GetWindowLongPtr(w.hwnd, w32.GWL_EXSTYLE))
if currentStyle != 0 {
if b {
currentStyle |= style
} else {
currentStyle &^= style
}
currentStyle = lo.Ternary(b, currentStyle|style, currentStyle&^style)
w32.SetWindowLongPtr(w.hwnd, w32.GWL_EXSTYLE, uintptr(currentStyle))
}
}
@ -529,13 +529,7 @@ func (w *windowsWebviewWindow) setBackdropType(backdropType BackdropType) {
w32.SetWindowCompositionAttribute(w.hwnd, &data)
} else {
backdropValue := 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)))
w32.DwmSetWindowAttribute(w.hwnd, w32.DwmwaSystemBackdropType, w32.PVOID(&backdropType), unsafe.Sizeof(backdropType))
}
}
@ -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 {
switch msg {
case w32.WM_ACTIVATE:
@ -813,6 +814,50 @@ func (w *windowsWebviewWindow) scaleToDefaultDPI(width, height int) (int, int) {
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 {
return (pixels * int(dpi)) / 96
}

View File

@ -3549,3 +3549,11 @@ const (
SIF_TRACKPOS = 16
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")
)
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(
hwnd,
uintptr(dwAttribute),
uintptr(pvAttribute),
uintptr(cbAttribute))
cbAttribute)
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
}
type BLENDFUNCTION struct {
BlendOp byte
BlendFlags byte
SourceConstantAlpha byte
AlphaFormat byte
}
// http://msdn.microsoft.com/en-us/library/windows/desktop/dd183567.aspx
type DIBSECTION struct {
DsBm BITMAP

View File

@ -139,6 +139,7 @@ var (
procUnhookWindowsHookEx = moduser32.NewProc("UnhookWindowsHookEx")
procCallNextHookEx = moduser32.NewProc("CallNextHookEx")
procGetForegroundWindow = moduser32.NewProc("GetForegroundWindow")
procUpdateLayeredWindow = moduser32.NewProc("UpdateLayeredWindow")
procSystemParametersInfo = moduser32.NewProc("SystemParametersInfoW")
procSetClassLong = moduser32.NewProc("SetClassLongW")
@ -234,6 +235,21 @@ func UpdateWindow(hwnd HWND) bool {
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) {
procPostThreadMessageW.Call(threadID, uintptr(msg), wp, lp)
}