PAYATHON 2026

vb.net screensaver 安装为 .scr 后透明圆变灰怎么办

排障赵师傅

结论

不要依赖 WinForms 的 TransparencyKey 让屏幕保护程序“穿透”窗口并显示桌面。Windows 以屏保模式启动 .scr 时,程序可能位于屏保宿主窗口、预览子窗口或不同的桌面上下文中。透明像素显示的是当前窗口后面的合成表面,不一定是用户原来的桌面,所以透明圆后面可能出现深灰色的宿主背景。

更稳妥的做法是:

  1. 屏保启动时截取屏幕画面,或直接加载壁纸。
  2. 在窗体内绘制背景图。
  3. 绘制深色遮罩,并在遮罩中留出一个移动的圆形区域。

这种方式可以做出“透明洞”的视觉效果。窗口本身无须透明,显示结果也不会受屏保宿主窗口影响。

为什么直接运行正常,安装为 .scr 后却变灰

把 .exe 改名为 .scr 不会改变程序格式,却会改变 Windows 启动程序的方式。屏保通常会收到这些参数:

  • /s:全屏运行屏幕保护程序。
  • /p <HWND>:嵌入系统提供的预览窗口。
  • /c:打开配置窗口。

直接运行 .exe 时,窗体后面通常是普通桌面。作为屏保运行时,窗体后面可能是屏保宿主或其他背景表面。即使 TransparencyKey 正常生效,透明区域露出的也是这个表面,因此可能显示深灰色,而非原来的桌面内容。

TransparencyKey 还有两个限制。

它只支持颜色键透明

TransparencyKey 会把与指定颜色完全相同的像素设为透明:

Private ReadOnly TransparencyMagicColor As Color =
    Color.FromArgb(1, 2, 3)

它不支持真正的逐像素 Alpha 混合,每个像素只能是“透明”或“不透明”。

抗锯齿会生成其他边缘颜色

代码启用了抗锯齿:

e.Graphics.SmoothingMode = SmoothingMode.AntiAlias

圆形边缘会混合 Color.FromArgb(1, 2, 3) 和背景色,生成一圈新的颜色。这些像素与 TransparencyMagicColor 不完全相同,不会被透明色键移除,因而容易出现灰边或暗色边缘。

将 SmoothingMode 改为 None 可以减轻颜色键造成的边缘问题,但圆形会出现锯齿,屏保宿主背景无法穿透的问题也依然存在。

推荐方案:在窗体内合成画面

先保存屏幕图像,再在每一帧中绘制这张图,并用深灰色遮罩覆盖整个窗体,只排除圆形区域。

增加一个字段:

Private capturedScreen As Bitmap

启动全屏模式时,在窗体覆盖屏幕前抓取对应的显示器:

Private Sub LaunchFullScreen()
    Dim targetScreen As Screen = Screen.FromControl(Me)

    capturedScreen = New Bitmap(
        targetScreen.Bounds.Width,
        targetScreen.Bounds.Height,
        Imaging.PixelFormat.Format32bppArgb)

    Using g As Graphics = Graphics.FromImage(capturedScreen)
        g.CopyFromScreen(
            targetScreen.Bounds.Left,
            targetScreen.Bounds.Top,
            0,
            0,
            targetScreen.Bounds.Size,
            CopyPixelOperation.SourceCopy)
    End Using

    FormBorderStyle = FormBorderStyle.None
    WindowState = FormWindowState.Normal
    StartPosition = FormStartPosition.Manual
    Bounds = targetScreen.Bounds
    TopMost = True
    DoubleBuffered = True

    ' 不再使用窗口透明色键
    TransparencyKey = Color.Empty

    Cursor.Hide()
    animationTimer.Start()
End Sub

然后替换原来的 OnPaint:

Protected Overrides Sub OnPaint(e As PaintEventArgs)
    MyBase.OnPaint(e)

    e.Graphics.SmoothingMode = SmoothingMode.AntiAlias
    e.Graphics.CompositingQuality = CompositingQuality.HighQuality

    If capturedScreen IsNot Nothing Then
        e.Graphics.DrawImage(capturedScreen, Me.ClientRectangle)
    Else
        e.Graphics.Clear(Color.Black)
    End If

    Dim circleRect As New Rectangle(
        circleX - circleRadius,
        circleY - circleRadius,
        circleRadius * 2,
        circleRadius * 2)

    Using maskPath As New GraphicsPath(FillMode.Alternate)
        maskPath.AddRectangle(Me.ClientRectangle)
        maskPath.AddEllipse(circleRect)

        Using maskBrush As New SolidBrush(Color.FromArgb(48, 48, 48))
            e.Graphics.FillPath(maskBrush, maskPath)
        End Using
    End Using
End Sub

FillMode.Alternate 会把矩形与椭圆重叠的部分挖空。圆内显示之前绘制的屏幕截图,圆外则显示深灰色遮罩。圆移动时继续调用 Invalidate() 即可。

如果希望圆外区域呈现半透明的变暗效果,可以调整画刷的 Alpha 值:

Using maskBrush As New SolidBrush(Color.FromArgb(210, 0, 0, 0))
    e.Graphics.FillPath(maskBrush, maskPath)
End Using

这个 Alpha 值只参与窗体内部的图像合成,不依赖窗口透明,通常比 TransparencyKey 更稳定。

关闭窗体时需要释放截图:

Protected Overrides Sub OnFormClosed(e As FormClosedEventArgs)
    animationTimer.Stop()

    If capturedScreen IsNot Nothing Then
        capturedScreen.Dispose()
        capturedScreen = Nothing
    End If

    MyBase.OnFormClosed(e)
End Sub

修正预览模式的窗口样式设置

原代码读取的是 GWL_STYLE:

Dim currentStyle As IntPtr =
    GetWindowLongPtr(Me.Handle, GWL_STYLE)

写回时却传入了 0:

SetWindowLongPtr(Me.Handle, 0, New IntPtr(newStyle))

写回时也应使用 GWL_STYLE:

SetWindowLongPtr(Me.Handle, GWL_STYLE, New IntPtr(newStyle))

完整写法如下:

Dim currentStyle As IntPtr =
    GetWindowLongPtr(Me.Handle, GWL_STYLE)

Dim newStyle As Long =
    currentStyle.ToInt64() Or
    WS_CHILD Or
    WS_VISIBLE Or
    WS_CLIPCHILDREN Or
    WS_CLIPSIBLINGS

SetWindowLongPtr(
    Me.Handle,
    GWL_STYLE,
    New IntPtr(newStyle))

这个错误会影响 /p 预览模式下的子窗口样式,但全屏模式的透明效果失效还有其他原因。

单独处理预览模式

在 /p 模式下,屏保窗体会通过 SetParent 成为控制面板预览窗口的子窗口,无法保证透过子窗口看到真实桌面。

预览模式可以使用固定背景图或程序启动时保存的缩略图,也可以只显示动画,不模拟真实桌面。还可以根据预览区域的尺寸单独生成背景位图。

如果预览模式仍使用 TransparencyKey,显示结果会同时受父窗口、系统合成和颜色键边缘影响,很难保持一致。

是否可以使用分层窗口

Win32 的 WS_EX_LAYERED、SetLayeredWindowAttributes 和 UpdateLayeredWindow 支持逐像素 Alpha,比 TransparencyKey 更灵活。不过,这些 API 改善的是窗口合成效果,无法保证透明区域后面就是用户原来的桌面。

屏保预览窗口仍是子窗口,其运行环境也可能不同于普通顶层窗口。要在圆形区域显示原桌面,使用截图或壁纸配合遮罩更容易控制。如果确实需要让整个窗口与当前桌面实时混合,并能接受不同 Windows 环境中的表现差异,再考虑使用分层窗口。

其他注意事项

  • 10 ms 的 Timer 间隔不代表能够稳定达到 100 FPS。WinForms Timer 会受到 UI 线程和系统计时精度影响,可以根据实际效果改为约 16 ms 或 33 ms。
  • 在多显示器环境中,应为每个 Screen 创建对应的窗体并分别截图,不要假设所有屏幕共用同一个坐标原点。
  • CopyFromScreen 捕获的是程序启动瞬间的静态画面。圆内不会继续显示桌面动画、视频或之后发生的窗口变化。
  • 在锁屏、安全桌面、远程会话或受保护内容场景中,屏幕捕获可能得到黑屏或不完整的画面。程序应准备纯色或图片背景作为回退。
  • 如果只需要固定壁纸,直接读取并绘制壁纸通常比截取屏幕更稳定。
  • 不要通过全屏透明窗口实现真正的屏幕保护。屏幕保护程序应自行绘制完整画面,以免意外露出仍在运行的桌面内容。

备注:内容仅供参考。