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:
parent
4ad2475ed6
commit
cb28de47f8
31
v3/STATUS.md
31
v3/STATUS.md
@ -112,7 +112,7 @@ Webview Window Interface Methods
|
||||
|
||||
| Feature | Windows | Linux | Mac | Notes |
|
||||
|------------|---------|-------|-----|-------|
|
||||
| GetAll | | | Y | |
|
||||
| GetAll | Y | | Y | |
|
||||
| GetPrimary | | | Y | |
|
||||
| GetCurrent | | | Y | |
|
||||
|
||||
@ -148,13 +148,11 @@ 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.
|
||||
|
||||
|
||||
| Feature | Windows | Linux | Mac | Notes |
|
||||
|---------------------------------|---------|-------|-----|--------------------------------------------|
|
||||
| Name | | | | |
|
||||
@ -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
|
||||
|
20
v3/pkg/application/image.go
Normal file
20
v3/pkg/application/image.go
Normal 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
|
||||
}
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
53
v3/pkg/w32/image.go
Normal 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
|
||||
}
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user