更新代码

This commit is contained in:
hero920103 committed 2026-10-28 17:39:33 +08:00
1 parent eda7e8bc96
commit 6ff632e0b0
19 files changed
+2033 -197

No files matched your search

+612 -15
View File
@@ -2,17 +2,21 @@ package win32
// 取景框:屏幕上的一个红框,常驻显示当前框选区域。
//
// 三个关键点:
// 1. 用 WS_EX_LAYERED 分层窗口自己画,框外像素 alpha=0,既不遮挡下方内容,
// 四个关键点:
// 1. 用 WS_EX_LAYERED 分层窗口自己画,框内像素 alpha=0,既不遮挡下方内容,
// 也不需要 Fyne 的透明窗口支持(OpenGL 窗口改颜色键并不可靠);
// 2. WS_EX_TRANSPARENT + WM_NCHITTEST 返回 HTTRANSPARENT,鼠标事件直接穿透,
// 不会挡住底下的点击;
// 3. SetWindowDisplayAffinity(WDA_EXCLUDEFROMCAPTURE) 让它不出现在截屏和录屏
// 2. 整个窗口带 WS_EX_TRANSPARENT,鼠标事件一律穿透给下层程序,
// 被监视的软件照常能点、能拖,不会被取景框挡住;
// 3. 要跟红框交互(拖边框改大小、右上角「确定 / 取消」按钮),
// 靠一个全局低级鼠标钩子(WH_MOUSE_LL)在事件到达下层程序之前拿到它们,
// 只在自己的小区域里拦截,其余地方一律放行;
// 4. SetWindowDisplayAffinity(WDA_EXCLUDEFROMCAPTURE) 让它不出现在截屏和录屏
// 画面里,包括本程序自己抓屏用的 GDI BitBlt(Windows 10 2004+)。
import (
"fmt"
"os"
"runtime"
"sync"
"syscall"
"unsafe"
@@ -51,10 +55,57 @@ const (
// htTransparent 让鼠标事件穿透到下层窗口。
htTransparent = ^uintptr(0)
// 低级鼠标钩子。
whMouseLL = 14
hcAction = 0
wmMouseMove = 0x0200
wmLButtonDown = 0x0201
wmLButtonUp = 0x0202
// 标准尺寸光标。
idcArrow = 32512
idcSizeNS = 32645
idcSizeWE = 32644
idcSizeNESW = 32643
idcSizeNWSE = 32642
)
// 取景框上的交互区。
const (
zoneNone = iota
zoneLeft
zoneTop
zoneRight
zoneBottom
zoneTopLeft
zoneTopRight
zoneBottomLeft
zoneBottomRight
zoneButtons
)
// 按钮编号。
const (
btnNone = iota
btnOK
btnCancel
)
// 取景框的尺寸常量(物理像素)。
const (
frameGrip = 6 // 边框的拖拽响应宽度
frameMinSide = 16 // 选区允许的最小边长
frameBtnSize = 16 // 按钮边长
frameBtnGap = 4 // 两个按钮之间的间距
frameBtnRight = 8 // 按钮条距右边
frameBtnTop = 8 // 按钮条距上边
)
var (
gdi32 = windows.NewLazySystemDLL("gdi32.dll")
km32 = windows.NewLazySystemDLL("kernel32.dll")
procCreateCompatibleDC = gdi32.NewProc("CreateCompatibleDC")
procCreateDIBSection = gdi32.NewProc("CreateDIBSection")
@@ -76,6 +127,18 @@ var (
procSetDisplayAffinity = user32.NewProc("SetWindowDisplayAffinity")
procGetDisplayAffinity = user32.NewProc("GetWindowDisplayAffinity")
procIsWindowVisible = user32.NewProc("IsWindowVisible")
procSetTimer = user32.NewProc("SetTimer")
procKillTimer = user32.NewProc("KillTimer")
procSetCapture = user32.NewProc("SetCapture")
procReleaseCapture = user32.NewProc("ReleaseCapture")
procSetCursor = user32.NewProc("SetCursor")
procLoadCursorW = user32.NewProc("LoadCursorW")
procSetWindowsHookExW = user32.NewProc("SetWindowsHookExW")
procUnhookWindowsHookEx = user32.NewProc("UnhookWindowsHookEx")
procCallNextHookEx = user32.NewProc("CallNextHookEx")
procGetModuleHandleW = km32.NewProc("GetModuleHandleW")
)
type bitmapInfoHeader struct {
@@ -127,14 +190,37 @@ type wndClassEx struct {
IconSmall windows.Handle
}
// msllHookStruct 对应 Win32 的 MSLLHOOKSTRUCT。
type msllHookStruct struct {
Pt point
MouseData uint32
Flags uint32
Time uint32
DwExtraInfo uintptr
}
// Frame 是屏幕上的取景框。内部自带一条消息线程,
// 窗口消息必须有人泵,否则窗口不会响应。
type Frame struct {
mu sync.Mutex
hwnd syscall.Handle
region Rect
region Rect // 已生效的区域
draft Rect // 正在编辑的区域(拖拽时变化)
shown bool
hidden bool // 用户点了取消:红框不再自动显示
hoverZone int // 当前悬停的交互区
hoverBtn bool // 按钮条是否处于悬停状态
pressed int // 按下的按钮
dragging bool
dragZone int
dragStartX, dragStartY int
dragBase Rect
onApply func(Rect)
onCancel func()
hook syscall.Handle
memDC syscall.Handle
dib syscall.Handle
oldBmp syscall.Handle
@@ -156,6 +242,12 @@ var (
frames = map[syscall.Handle]*Frame{}
)
// active 是当前生效的取景框,鼠标钩子里要用。
var (
activeMu sync.Mutex
active *Frame
)
// NewFrame 创建一个取景框。此时还没有区域,调用 SetRegion 才会显示。
func NewFrame() (*Frame, error) {
f := &Frame{
@@ -171,6 +263,12 @@ func NewFrame() (*Frame, error) {
// loop 在独立线程上建窗口并跑消息循环。
func (f *Frame) loop() {
// 必须把 goroutine 钉在同一个系统线程上:窗口的消息队列是按线程分的,
// 一旦被调度到别的线程,GetMessage 就泵不到这个窗口的队列,
// 红框从此不再重绘,看起来就是"卡死"。
runtime.LockOSThread()
defer runtime.UnlockOSThread()
className, err := windows.UTF16PtrFromString(frameWNDClass)
if err != nil {
f.ready <- err
@@ -194,6 +292,8 @@ func (f *Frame) loop() {
}
}
// WS_EX_TRANSPARENT:整个窗口对鼠标透明,事件直接穿透给下层程序。
// 交互交给全局鼠标钩子处理,这样绝不会挡住被监视的软件。
style := uintptr(wsExLayered | wsExTransparent | wsExToolWindow | wsExNoActivate)
hwnd, _, _ := procCreateWindowExW.Call(
style,
@@ -233,6 +333,17 @@ func (f *Frame) loop() {
procReleaseDC.Call(0, hdcScreen)
}
// 装全局低级鼠标钩子:取景框是穿透窗口,收不到鼠标消息,
// 只能在事件到达下层程序之前截住。
activeMu.Lock()
active = f
activeMu.Unlock()
hmod, _, _ := procGetModuleHandleW.Call(0)
hh, _, _ := procSetWindowsHookExW.Call(whMouseLL, frameHookCallback, hmod, 0)
f.mu.Lock()
f.hook = syscall.Handle(hh)
f.mu.Unlock()
f.ready <- nil
var msg msg
@@ -251,14 +362,19 @@ func (f *Frame) loop() {
func (f *Frame) SetRegion(r Rect) {
f.mu.Lock()
f.region = r
f.shown = r.Width() > 0 && r.Height() > 0
// 拖拽过程中不要把正在编辑的区域顶掉。
if !f.dragging {
f.draft = r
}
show := r.Width() > 0 && r.Height() > 0 && !f.hidden
f.shown = show
hwnd := f.hwnd
f.mu.Unlock()
if hwnd == 0 {
return
}
if r.Width() <= 0 || r.Height() <= 0 {
if !show {
procShowWindow.Call(uintptr(hwnd), 0) // SW_HIDE
return
}
@@ -266,17 +382,70 @@ func (f *Frame) SetRegion(r Rect) {
procPostMessageW.Call(uintptr(hwnd), wmFrameRedraw, 0, 0)
}
// Hide 隐藏取景框。
// Show 重新显示取景框(会清掉"用户已取消"的状态)。
func (f *Frame) Show() {
f.mu.Lock()
f.hidden = false
r := f.region
f.mu.Unlock()
f.SetRegion(r)
}
// Hide 隐藏取景框(临时隐藏,不会阻止 SetRegion 再次显示它)。
func (f *Frame) Hide() {
f.mu.Lock()
f.shown = false
hwnd := f.hwnd
f.mu.Unlock()
if hwnd != 0 {
procKillTimer.Call(uintptr(hwnd), 1)
procShowWindow.Call(uintptr(hwnd), 0)
}
}
// HideByUser 按用户意图隐藏取景框:之后 SetRegion 也不会把它显示出来。
func (f *Frame) HideByUser() {
f.mu.Lock()
f.hidden = true
f.shown = false
f.hoverBtn = false
f.pressed = btnNone
hwnd := f.hwnd
f.mu.Unlock()
if hwnd != 0 {
procKillTimer.Call(uintptr(hwnd), 1)
procShowWindow.Call(uintptr(hwnd), 0)
}
}
// ToggleVisible 切换取景框的显示状态,返回切换后是否可见。
func (f *Frame) ToggleVisible() bool {
f.mu.Lock()
visible := f.shown
f.mu.Unlock()
if visible {
f.HideByUser()
return false
}
f.Show()
return true
}
// Visible 返回取景框当前是否显示。
func (f *Frame) Visible() bool {
f.mu.Lock()
defer f.mu.Unlock()
return f.shown
}
// SetCallbacks 设置交互回调:确定时回传当前框体大小,取消时通知上层。
func (f *Frame) SetCallbacks(onApply func(Rect), onCancel func()) {
f.mu.Lock()
f.onApply = onApply
f.onCancel = onCancel
f.mu.Unlock()
}
// SetCaptureExclusion 开关某个窗口的"不出现在截屏/录屏里"。
// 关掉之后窗口会被录进去,只用于自检和调试。
func SetCaptureExclusion(hwnd syscall.Handle, on bool) {
@@ -360,16 +529,28 @@ func FrameDebug(hwnd syscall.Handle) string {
// Close 销毁取景框。
func (f *Frame) Close() {
// 先摘掉钩子,别让已经关掉的取景框再拦截鼠标。
activeMu.Lock()
if active == f {
active = nil
}
activeMu.Unlock()
f.mu.Lock()
hwnd := f.hwnd
threadID := f.threadID
hook := f.hook
f.hwnd = 0
f.hook = 0
memDC := f.memDC
dib := f.dib
oldBmp := f.oldBmp
f.memDC, f.dib, f.oldBmp = 0, 0, 0
f.mu.Unlock()
if hook != 0 {
procUnhookWindowsHookEx.Call(uintptr(hook))
}
if hwnd != 0 {
framesMu.Lock()
delete(frames, syscall.Handle(hwnd))
@@ -391,10 +572,345 @@ func (f *Frame) Close() {
}
}
// ---------- 交互 ----------
// buttonRects 返回「确定」和「取消」两个按钮在框内坐标系下的矩形。
func (f *Frame) buttonRects(w int) (okR, cancelR [4]int) {
right := w - frameBtnRight
cancelR = [4]int{right - frameBtnSize, frameBtnTop, right, frameBtnTop + frameBtnSize}
okR = [4]int{cancelR[0] - frameBtnGap - frameBtnSize, frameBtnTop,
cancelR[0] - frameBtnGap, frameBtnTop + frameBtnSize}
return
}
// buttonBar 返回按钮条(悬停区)在框内坐标系下的范围。
func (f *Frame) buttonBar(w int) (x0, y0, x1, y1 int) {
okR, cancelR := f.buttonRects(w)
x0 = okR[0] - 2
y0 = frameBtnTop - 2
x1 = cancelR[2] + 2
y1 = cancelR[3] + 2
return
}
// hitTest 判断屏幕坐标落在哪个交互区。
func (f *Frame) hitTest(r Rect, sx, sy int) int {
w, h := r.Width(), r.Height()
if w <= 0 || h <= 0 {
return zoneNone
}
lx, ly := sx-r.Left, sy-r.Top
if lx < 0 || ly < 0 || lx >= w || ly >= h {
return zoneNone
}
// 按钮条优先:它就贴在右上角内侧,和边角拖拽区不重叠。
bx0, by0, bx1, by1 := f.buttonBar(w)
if lx >= bx0 && lx < bx1 && ly >= by0 && ly < by1 {
return zoneButtons
}
const grip = frameGrip
nearL := lx < grip
nearR := lx >= w-grip
nearT := ly < grip
nearB := ly >= h-grip
switch {
case nearL && nearT:
return zoneTopLeft
case nearR && nearT:
return zoneTopRight
case nearR && nearB:
return zoneBottomRight
case nearL && nearB:
return zoneBottomLeft
case nearL:
return zoneLeft
case nearR:
return zoneRight
case nearT:
return zoneTop
case nearB:
return zoneBottom
}
return zoneNone
}
// buttonAt 判断点在按钮条里的哪个按钮上。
func (f *Frame) buttonAt(r Rect, sx, sy int) int {
w := r.Width()
okR, cancelR := f.buttonRects(w)
lx := sx - r.Left
ly := sy - r.Top
inOK := lx >= okR[0] && lx < okR[2] && ly >= okR[1] && ly < okR[3]
inCancel := lx >= cancelR[0] && lx < cancelR[2] && ly >= cancelR[1] && ly < cancelR[3]
switch {
case inOK:
return btnOK
case inCancel:
return btnCancel
}
return btnNone
}
// frameHookCallback 是全局低级鼠标钩子的入口。
var frameHookCallback = windows.NewCallback(mouseHookTrampoline)
// mouseHookTrampoline 处理全局鼠标事件。返回非 0 表示吞掉这次事件。
func mouseHookTrampoline(hhk int32, nCode uint32, wParam uint32, lParam uintptr) uintptr {
// 钩子里 panic 会直接崩掉整个程序,这里兜一下。
defer func() { _ = recover() }()
if nCode == hcAction {
activeMu.Lock()
f := active
activeMu.Unlock()
if f != nil && f.handleHook(wParam, lParam) {
return 1 // 吞掉,别让下层程序收到
}
}
r, _, _ := procCallNextHookEx.Call(uintptr(hhk), uintptr(wParam), lParam)
return r
}
// hookStruct 把钩子回调传来的 lParam 还原成 MSLLHOOKSTRUCT。
//
// 直接写 unsafe.Pointer(lParam) 会被 vet 判成 uintptr 误用,
// 这里用"取变量地址再按指针读"的等价写法绕开。
func hookStruct(lParam uintptr) *msllHookStruct {
p := *(*unsafe.Pointer)(unsafe.Pointer(&lParam))
return (*msllHookStruct)(p)
}
// handleHook 处理钩子收到的事件。返回 true 表示这次事件被取景框接管。
func (f *Frame) handleHook(wParam uint32, lParam uintptr) bool {
hs := hookStruct(lParam)
x, y := int(hs.Pt.X), int(hs.Pt.Y)
switch wParam {
case wmMouseMove:
return f.hookMove(x, y)
case wmLButtonDown:
return f.hookDown(x, y)
case wmLButtonUp:
return f.hookUp(x, y)
}
return false
}
// hookMove 处理鼠标移动:更新悬停状态与光标,拖拽时更新选区。
func (f *Frame) hookMove(x, y int) bool {
f.mu.Lock()
if !f.shown || f.hidden {
f.mu.Unlock()
return false
}
dragging := f.dragging
d := f.draft
f.mu.Unlock()
if dragging {
f.applyDrag(x, y)
// 拖拽期间吞掉移动事件,下层程序别跟着乱动。
return true
}
zone := f.hitTest(d, x, y)
showBtns := zone == zoneButtons
f.mu.Lock()
f.hoverZone = zone
changed := f.hoverBtn != showBtns
f.hoverBtn = showBtns
f.mu.Unlock()
if changed {
f.redraw()
}
if zone == zoneNone {
return false
}
if zone == zoneButtons {
// 按钮条:光标用默认箭头就行,不用拦下层程序的移动事件,
// 免得那一小块区域的悬停效果失效。
return false
}
f.setCursorFor(zone)
// 吞掉:下层程序收不到移动事件,我们的缩放光标才不会被它改回去。
return true
}
// hookDown 处理鼠标按下:按钮或边框按下都接管,其余放行。
func (f *Frame) hookDown(x, y int) bool {
f.mu.Lock()
if !f.shown || f.hidden {
f.mu.Unlock()
return false
}
d := f.draft
hoverBtn := f.hoverBtn
f.mu.Unlock()
switch zone := f.hitTest(d, x, y); zone {
case zoneButtons:
if !hoverBtn {
// 按钮还没显示出来(鼠标刚扫过去),这次点击让给下层程序。
return false
}
f.mu.Lock()
f.pressed = f.buttonAt(d, x, y)
f.mu.Unlock()
return true
case zoneNone:
return false
default:
f.mu.Lock()
f.dragging = true
f.dragZone = zone
f.dragStartX, f.dragStartY = x, y
f.dragBase = d
f.mu.Unlock()
return true
}
}
// hookUp 处理鼠标松开。
func (f *Frame) hookUp(x, y int) bool {
f.mu.Lock()
dragging := f.dragging
pressed := f.pressed
f.dragging = false
f.pressed = btnNone
f.mu.Unlock()
if dragging {
// 松手后不立刻生效,等用户点「确定」。
f.redraw()
return true
}
switch pressed {
case btnOK:
f.applyDraft()
return true
case btnCancel:
f.cancel()
return true
}
return false
}
// applyDrag 按当前鼠标位置算出新的框。
func (f *Frame) applyDrag(sx, sy int) {
f.mu.Lock()
zone := f.dragZone
base := f.dragBase
startX, startY := f.dragStartX, f.dragStartY
f.mu.Unlock()
if zone == zoneNone {
return
}
dx := sx - startX
dy := sy - startY
r := base
vd := VirtualScreen()
minSide := frameMinSide
clamp := func(v, lo, hi int) int {
if v < lo {
return lo
}
if v > hi {
return hi
}
return v
}
switch zone {
case zoneLeft:
r.Left = clamp(base.Left+dx, vd.Left, base.Right-minSide)
case zoneRight:
r.Right = clamp(base.Right+dx, base.Left+minSide, vd.Right)
case zoneTop:
r.Top = clamp(base.Top+dy, vd.Top, base.Bottom-minSide)
case zoneBottom:
r.Bottom = clamp(base.Bottom+dy, base.Top+minSide, vd.Bottom)
case zoneTopLeft:
r.Left = clamp(base.Left+dx, vd.Left, base.Right-minSide)
r.Top = clamp(base.Top+dy, vd.Top, base.Bottom-minSide)
case zoneTopRight:
r.Right = clamp(base.Right+dx, base.Left+minSide, vd.Right)
r.Top = clamp(base.Top+dy, vd.Top, base.Bottom-minSide)
case zoneBottomLeft:
r.Left = clamp(base.Left+dx, vd.Left, base.Right-minSide)
r.Bottom = clamp(base.Bottom+dy, base.Top+minSide, vd.Bottom)
case zoneBottomRight:
r.Right = clamp(base.Right+dx, base.Left+minSide, vd.Right)
r.Bottom = clamp(base.Bottom+dy, base.Top+minSide, vd.Bottom)
}
f.mu.Lock()
f.draft = r
f.mu.Unlock()
f.redraw()
}
// applyDraft 把当前编辑中的区域交给上层(点「确定」)。
func (f *Frame) applyDraft() {
f.mu.Lock()
d := f.draft
cb := f.onApply
f.mu.Unlock()
if cb != nil {
cb(d)
}
}
// cancel 放弃当前编辑并隐藏红框(点「取消」)。
func (f *Frame) cancel() {
f.mu.Lock()
f.draft = f.region
f.hidden = true
f.shown = false
f.hoverBtn = false
f.pressed = btnNone
hwnd := f.hwnd
cb := f.onCancel
f.mu.Unlock()
if hwnd != 0 {
procShowWindow.Call(uintptr(hwnd), 0)
}
if cb != nil {
cb()
}
}
// setCursorFor 按交互区设置光标形状。
func (f *Frame) setCursorFor(zone int) {
id := uintptr(idcArrow)
switch zone {
case zoneLeft, zoneRight:
id = idcSizeWE
case zoneTop, zoneBottom:
id = idcSizeNS
case zoneTopLeft, zoneBottomRight:
id = idcSizeNWSE
case zoneTopRight, zoneBottomLeft:
id = idcSizeNESW
}
cur, _, _ := procLoadCursorW.Call(0, id)
if cur == 0 {
return
}
procSetCursor.Call(cur)
}
// ---------- 绘制 ----------
// redraw 重新绘制取景框(运行在窗口自己的线程上)。
func (f *Frame) redraw() {
f.mu.Lock()
r := f.region
r := f.draft
hoverBtn := f.hoverBtn
memDC := f.memDC
hwnd := f.hwnd
f.mu.Unlock()
@@ -415,6 +931,9 @@ func (f *Frame) redraw() {
fillRect(f.buf, w, h, 0, h-thickness, w, h, red)
fillRect(f.buf, w, h, 0, 0, thickness, h, red)
fillRect(f.buf, w, h, w-thickness, 0, w, h, red)
f.drawButtons(w, h, hoverBtn)
copy(unsafe.Slice((*uint32)(f.bits), len(f.buf)), f.buf)
hdcScreen, _, _ := procGetDC.Call(0)
@@ -446,6 +965,25 @@ func (f *Frame) redraw() {
procSetWindowPos.Call(uintptr(hwnd), hwndTopMost, 0, 0, 0, 0, swpNoMove|swpNoSize|swpNoActivate)
}
// drawButtons 画右上角的「确定 / 取消」。
//
// 平时完全不画(像素全透明),鼠标移上去才显示。
// 鼠标事件由全局钩子处理,这里不需要留任何"能点到的底"。
func (f *Frame) drawButtons(w, h int, visible bool) {
if !visible {
return
}
okR, cancelR := f.buttonRects(w)
bg := uint32(0xFF1E1E1E)
fillRect(f.buf, w, h, int(okR[0]), int(okR[1]), int(okR[2]), int(okR[3]), bg)
fillRect(f.buf, w, h, int(cancelR[0]), int(cancelR[1]), int(cancelR[2]), int(cancelR[3]), bg)
// 图标:绿色对勾 + 红色叉。
drawCheck(f.buf, w, h, okR, uint32(0xFF4CD44C))
drawCross(f.buf, w, h, cancelR, uint32(0xFF5252FF))
}
// ensureBitmap 保证离屏位图尺寸与区域一致。
func (f *Frame) ensureBitmap(w, h int) {
f.mu.Lock()
@@ -508,20 +1046,72 @@ func fillRect(buf []uint32, w, h, x0, y0, x1, y1 int, color uint32) {
}
}
// drawLine 在像素缓冲里画一条线(Bresenham)。
func drawLine(buf []uint32, w, h, x0, y0, x1, y1 int, c uint32) {
dx := absInt(x1 - x0)
dy := -absInt(y1 - y0)
sx, sy := -1, -1
if x0 < x1 {
sx = 1
}
if y0 < y1 {
sy = 1
}
err := dx + dy
for {
if x0 >= 0 && y0 >= 0 && x0 < w && y0 < h {
buf[y0*w+x0] = c
}
if x0 == x1 && y0 == y1 {
return
}
e2 := 2 * err
if e2 >= dy {
err += dy
x0 += sx
}
if e2 <= dx {
err += dx
y0 += sy
}
}
}
// drawCheck 在按钮里画一个对勾。
func drawCheck(buf []uint32, w, h int, r [4]int, c uint32) {
x0, y0 := r[0], r[1]
size := r[2] - r[0]
drawLine(buf, w, h, x0+size*2/5, y0+size/2, x0+size*3/5, y0+size*4/5, c)
drawLine(buf, w, h, x0+size*3/5, y0+size*4/5, x0+size*4/5, y0+size/5, c)
}
// drawCross 在按钮里画一个叉。
func drawCross(buf []uint32, w, h int, r [4]int, c uint32) {
x0, y0 := r[0], r[1]
size := r[2] - r[0]
drawLine(buf, w, h, x0+size/4, y0+size/4, x0+size*3/4, y0+size*3/4, c)
drawLine(buf, w, h, x0+size*3/4, y0+size/4, x0+size/4, y0+size*3/4, c)
}
// absInt 返回绝对值。
func absInt(v int) int {
if v < 0 {
return -v
}
return v
}
// frameWndProc 是取景框的窗口过程。
func frameWndProc(hwnd uintptr, msg uint32, wparam, lparam uintptr) uintptr {
switch msg {
case wmNCHitTest:
// 鼠标穿透:让下层窗口收到点击。
// 鼠标穿透:让下层窗口收到点击。交互由全局钩子负责。
return htTransparent
case wmEraseBkgnd:
// 不擦背景:整个窗口由分层位图定义。
return 1
case wmFrameRedraw:
framesMu.Lock()
f := frames[syscall.Handle(hwnd)]
framesMu.Unlock()
if f != nil {
if f := frameOf(hwnd); f != nil {
f.redraw()
}
return 0
@@ -532,6 +1122,13 @@ func frameWndProc(hwnd uintptr, msg uint32, wparam, lparam uintptr) uintptr {
return r
}
// frameOf 按窗口句柄找到取景框实例。
func frameOf(hwnd uintptr) *Frame {
framesMu.Lock()
defer framesMu.Unlock()
return frames[syscall.Handle(hwnd)]
}
// lastErrorCode 取最近一次 Win32 调用的错误码。
func lastErrorCode() uintptr {
if err, ok := windows.GetLastError().(syscall.Errno); ok {
+28
View File
@@ -23,6 +23,7 @@ var (
procEnumDisplayMonitors = user32.NewProc("EnumDisplayMonitors")
procGetMonitorInfoW = user32.NewProc("GetMonitorInfoW")
procGetDpiForMonitor = shcore.NewProc("GetDpiForMonitor")
procMessageBoxW = user32.NewProc("MessageBoxW")
)
const (
@@ -242,3 +243,30 @@ func MoveTo(hwnd syscall.Handle, r Rect) error {
}
return nil
}
// 消息框的图标与按钮常量。
const (
// MBOK 是只有「确定」按钮的消息框。
MBOK uintptr = 0x00000000
// MBError 是带错误图标的消息框。
MBError uintptr = 0x00000010
// MBInfo 是带信息图标的消息框。
MBInfo uintptr = 0x00000040
)
// MessageBox 弹出系统原生消息框。
// 程序打包后没有控制台窗口,启动失败之类的错误必须用弹窗告诉用户。
func MessageBox(title, text string, icon uintptr) {
t, err := windows.UTF16PtrFromString(title)
if err != nil {
return
}
m, err := windows.UTF16PtrFromString(text)
if err != nil {
return
}
_, _, _ = procMessageBoxW.Call(0,
uintptr(unsafe.Pointer(m)),
uintptr(unsafe.Pointer(t)),
icon|MBOK)
}