狐表实现"加载中"效果的遮罩层/等待窗口方案,2行代码简单控制,支持圆角、无锯齿、透明窗体、自定义Logo、无内存泄露,样式简约现代,代码一抄即用,长时间执行的代码强烈推荐配合使用

发表日期: 2022-11-29

1.简介

1.1效果图

长时间执行的代码,加个等待窗口,让用户等待,增加友好度,一般这在web领域叫遮罩层。不过狐表Exe的UI框架里并没有集成这个,只能自己变通实现试下。

image.png

这个Loading不是挺优雅的吗?

loading.gif

1.2遮罩层特色

  1. 1代码简单,控制开关也简单。

  2. 支持圆角和透明窗体效果。

  3. 可以自定义加载中的gif动画,添加你的自己的logo,(需要你有PS功底)。

  4. 提供通用简约的GIF图,能用于任何项目场合,不搞怪异化。

  5. 弹出位置在屏幕中间+向上偏移150像素,这就不会挡住业务代码出错时的MessageBox窗口
  6. 无内存泄露【重要】。试过连续打开关闭500次,内存在10M内波动,不会无限增加,程序不崩。

image.png

 1.3前人帖子

http://www.foxtable.com/bbs/dispbbs.asp?BoardID=2&ID=152101&replyID=&skin=1

http://www.foxtable.com/bbs/dispbbs.asp?BoardID=2&ID=73365&replyID=&skin=1

http://www.foxtable.com/bbs/dispbbs.asp?BoardID=2&ID=174427&replyID=&skin=1

我选了第三个帖子去二次改造,感觉这个代码最少,好理解维护。不过它的方法存在一个严重缺陷

内存泄露严重,开循环200次打开直接程序崩了

image.png

image.png

其他是小问题:

2.教程

2.1放好图片素材

图片素材放到Images文件夹里,与待会全局代码的路径相匹配

image.png

2.2添加全局代码

image.png

'通过WaitShow()和WaitClose()来打开和关闭遮罩层,'默认30秒自动关闭
'如果非常耗时,建议自定义时间WaitShow(50000),表示50秒,并配合Try使用,Finally里放WaitClose()

'思路:
'1先配置好等待窗口和里面的GIF动态图
'2通过WaitShow()启动新线程,以模式窗口形式展示
'3过程中执行你的长耗时业务代码
'4最后WaitClose()释放窗口内存,线程也会被自动销毁

' =========================================================
' 不抢焦点的遮罩层窗体
' 作用:
' 1. 显示加载 GIF,但【永远不获取输入焦点】
' 2. 避免与 InputBox / MessageBox 等模态窗口产生焦点竞争
' 3. 防止 Windows 因“跨线程抢焦点”而把焦点丢给其他程序
'
' 技术点:
' - 通过 WS_EX_NOACTIVATE 扩展样式
' - 窗体显示时不会激活(不会成为前台窗口)
'
' ⚠️ 这是解决“焦点跳到资源管理器/其他程序”的关键
' =========================================================
Public Class NoActivateForm
    Inherits System.Windows.Forms.Form

    ' 重写 CreateParams,注入 WS_EX_NOACTIVATE
    ' 该样式会告诉 Windows:
    '   - 这个窗体可以显示
    '   - 但不参与焦点/输入激活
    Protected Overrides ReadOnly Property CreateParams As System.Windows.Forms.CreateParams
        Get
            Dim cp As System.Windows.Forms.CreateParams = MyBase.CreateParams
            cp.ExStyle = cp.ExStyle Or &H8000000 ' WS_EX_NOACTIVATE
            Return cp
        End Get
    End Property

End Class



' =========================================================
' 遮罩层核心状态字段
' =========================================================

' 遮罩层专用 UI 线程
' - 独立于主线程
' - 负责 GIF 动画和窗体消息泵
Private overlayThread As System.Threading.Thread

' 遮罩层窗体实例(不抢焦点版)
Private overlayForm As NoActivateForm

' 自动关闭定时器
' - 默认 30 秒
' - 不依赖主线程
Private autoCloseTimer As System.Threading.Timer

' 遮罩层“就绪信号”
' ---------------------------------------------------------
' 作用:
' - 解决 WaitShow() 后立刻 WaitClose() 时的竞态问题
' - 确保窗体已经真正 Shown / Handle 已创建
'
' 如果没有这个信号:
' - Close 可能发生在 Application.Run 之前
' - 会导致“概率性关不掉”
Private overlayReady As New System.Threading.ManualResetEvent(False)

' 同步锁,保护线程/窗体生命周期
Private ReadOnly syncObj As New Object()



' =========================================================
' 对外 API(保持接口稳定,业务代码无需改动)
' =========================================================

' 显示遮罩层
' ---------------------------------------------------------
' closeTimeMilliseconds:
'   - 默认 30000ms(30 秒)
'   - 到时间后自动关闭,防止异常情况下永远不消失
Public Sub WaitShow(Optional closeTimeMilliseconds As Integer = 30000)
    SyncLock syncObj

        ' 每次 Show 前重置就绪状态
        overlayReady.Reset()

        ' 如果遮罩层线程已经存在且存活,则不重复创建
        ' 防止多次 WaitShow 导致多个遮罩层
        If overlayThread IsNot Nothing AndAlso overlayThread.IsAlive Then Exit Sub

        ' 创建独立 UI 线程
        overlayThread = New System.Threading.Thread(
            Sub()
                RunOverlayForm(closeTimeMilliseconds)
            End Sub
        )

        ' 后台线程:
        ' - 主程序退出时,CLR 会自动终止
        ' - 防止残留僵尸线程
        overlayThread.IsBackground = True

        ' 必须 STA:
        ' - WinForms / GDI / 图片加载都依赖 STA
        overlayThread.SetApartmentState(System.Threading.ApartmentState.STA)

        overlayThread.Start()

    End SyncLock
End Sub


' 关闭遮罩层
Public Sub WaitClose()

    ' 等待遮罩层真正就绪
    ' - 最多等 1 秒,防止极端情况下死等
    ' - 解决 Show → Close 紧挨着调用的问题
    overlayReady.WaitOne(1000)

    SyncLock syncObj
        CloseInternal()
    End SyncLock
End Sub



' =========================================================
' 内部实现(不直接给业务层调用)
' =========================================================

' 遮罩层线程入口
' ---------------------------------------------------------
' 在【独立 UI 线程】中:
' - 创建窗体
' - 启动 Application.Run 消息泵
Private Sub RunOverlayForm(closeTimeMilliseconds As Integer)

    ' 创建不抢焦点窗体
    overlayForm = New NoActivateForm()

    With overlayForm
        .FormBorderStyle = System.Windows.Forms.FormBorderStyle.None
        .ShowInTaskbar = False
        .StartPosition = System.Windows.Forms.FormStartPosition.Manual

        ' 使用绿色作为透明色(配合 GIF)
        .BackColor = System.Drawing.Color.Green
        .TransparencyKey = System.Drawing.Color.Green

        .Width = 120
        .Height = 130

        ' TopMost 只用于视觉遮挡
        ' 实际不会抢焦点(因为 WS_EX_NOACTIVATE)
        .TopMost = True
    End With

    ' 屏幕居中偏上,避免遮挡 MessageBox / InputBox
    Dim screen As System.Drawing.Rectangle = 
        System.Windows.Forms.Screen.PrimaryScreen.WorkingArea

    overlayForm.Location = New System.Drawing.Point(
        (screen.Width - overlayForm.Width) \ 2, 
        (screen.Height - overlayForm.Height) \ 2 - 140
    )

    ' 加载 GIF 动画
    Dim pic As New System.Windows.Forms.PictureBox()
    pic.Dock = System.Windows.Forms.DockStyle.Fill
    pic.Image = System.Drawing.Image.FromFile(ProjectPath & "Images\loading.gif")
    pic.SizeMode = System.Windows.Forms.PictureBoxSizeMode.Zoom
    overlayForm.Controls.Add(pic)

    ' 窗体真正显示后,标记为 ready
    ' ⚠️ 这是 WaitShow / WaitClose 稳定性的关键
    AddHandler overlayForm.Shown, 
        Sub()
            overlayReady.Set()
        End Sub

    ' 自动关闭定时器
    ' - 完全独立
    ' - 不依赖主线程
    autoCloseTimer = New System.Threading.Timer(
        Sub()
            CloseInternal()
        End Sub, 
        Nothing, 
        closeTimeMilliseconds, 
        System.Threading.Timeout.Infinite
    )

    ' 启动独立 UI 消息泵
    System.Windows.Forms.Application.Run(overlayForm)
End Sub


' 真正的关闭逻辑
' ---------------------------------------------------------
' 注意:
' - 可能被多次调用
' - 可能在不同线程调用
' - 必须足够健壮
Private Sub CloseInternal()
    Try
        ' 停止并释放定时器
        If autoCloseTimer IsNot Nothing Then
            autoCloseTimer.Dispose()
            autoCloseTimer = Nothing
        End If

        ' 安全关闭窗体
        If overlayForm IsNot Nothing AndAlso Not overlayForm.IsDisposed Then
            overlayForm.Invoke(
                New System.Windows.Forms.MethodInvoker(
                    Sub()
                        overlayForm.Close()
                    End Sub
                )
            )
        End If
    Catch
        ' 在以下情况下可能进入:
        ' - 主线程卡死
        ' - 消息泵已结束
        ' - 程序正在退出
        '
        ' 这里刻意忽略,交给进程退出回收
    End Try
End Sub

2.3使用方法

2.3.1自动关闭方法

如果你确定代码执行时间较短,且逻辑简单不容易报错

WaitShow '30秒内自动关闭   
 
'你的长时间运行代码,例如保存
DataTables("数据权限表").Save

WaitClose
2.3.1手动关闭方法

- 如果预计代码时间长,且逻辑复杂容易易出错,推荐配合Try使用,Finally里放WaitClose(),防止因为业务代码异常而导致遮罩层不关闭

- 当然如果你有把握,中间过程代码不会出错的,不用Try,直接使用也没问题。如果真的出现异常,你可以告诉客户在底部菜单栏右键强制关闭。它的关闭,并不会影响原来主线程在做的事情。

Try
    WaitShow(60000)'手动设置60秒后关闭
    '你的长时间运行代码,例如保存
    DataTables("数据权限表").Save
Catch ex As Exception 
    MessageBox.Show(ex.Message)
Finally
    WaitClose
End Try
3.自定义加载GIF动画

这个需要你有PS基础,我已经提供了我这次使用的gif图的原PSD文件。需要在PS里打开,并且启动PS的`时间轴`功能。你要去百度学习一下PS的时间轴玩法和GIF保存方法

3.1添加自定义logo和背景

例如我加一个迈宝伦科技官网的logo去中间

image.png

image.png


成品效果:

loading_带logo.gif

3.2透明背景和圆角

gif是支持透明背景的,圆角也支持,在PS做好即可。唯一要注意就是生成GIF图时,圆角要设置相应的杂边颜色,不然会有难看的白色锯齿

image.png


以下是正确保存方法

image.png

image.png

3.3注意不要用某个绿色

在刚才的全局代码里有一个

...
' 使用绿色作为透明色(配合 GIF)
.BackColor = System.Drawing.Color.Green
.TransparencyKey = System.Drawing.Color.Green
...

这个是狐表窗口实现边框透明的关键,设置绿色边框,并对绿色进行透明化。所以你如果GIF里刚好包含了RGB(0,128,0)的绿色,也会被抠图走,穿孔很丑了。

如果你的Gif图一定要用这种绿色,我推荐你可以换别的颜色组合,例如Color.Red。就跟好莱坞拍戏用绿幕,演员就别穿颜色相近的绿衣服一个道理。

4.案例源码下载

文件下载等待窗口演示.zip

版本说明:程序需要20220818或以上的商业版,或者用狐表官网最新的试用版。如果你没有这个版本也没关系,可以下载源码,只要里面的GIF素材和PS素材,配合上面分享的代码,复制就能用,不一定非得开源码看。


随便看看
狐表QQ交流群 : 123865097
商务联系QQ : 2385350359

Copyright 2016-2025 江门蓬江区华越科技公司 版权所有 | 承接软件定制开发,欢迎联系
粤ICP备19148806号-5