使用 Python 实现 Windows 对话框定时自动关闭

说明:以下函数所使用的对话框是基于Windows的,通过调用 Windows API 实现:

  • 思路一:调用MessageBox弹出对话框,另起一个定时器关闭它。
  • 思路二:调用MessageBoxTimeout,微软未公布的函数,来实现。

前言

在调用Windows的对话框时,它是阻塞的,直到用户点击对话框按钮,才会返回按下的按钮值,代码才会继续往下执行。

有时我们希望只是弹出提示对话框但不希望它阻塞主线程执行,那么我们可以新建一个线程来调用它,如果这个时候我们想获取它的返回值,或者希望在用户点击关闭对话框时做点别的事情,那么可以通过回调函数来实现了。

有时我们还希望,对话框会自动关闭,我们只是提示一下用户,省去用户自动点击的麻烦,这是多么美妙的事情。

以下代码,实现了以上的猜想。我们可以设置block参数决定对话框是否阻塞调用线程,默认值取决于interval <= 0interval参数代表对话框在弹出后interval秒自动关闭。如果用户在自动关闭前主动关闭了对话框,那么会取消该定时器,然后执行回调函数。

此外我们还可以设置对话框关闭时的回调函数,回调函数的唯一的参数是对话框关闭时的按键特殊值。回调函数建议在定时自动关闭模式下设置,当然在阻塞模式下也可以设置。

但是值得注意的是,对于多选一没有关闭功能的对话框,例如MB_YESNOMB_ABORTRETRYIGNORE等等,如果设置了自动关闭,那么自动关闭时默认返回值会是

  • win32con.IDCLOSE,使用MessageBoxEndDialog实现的自动关闭对话框,该值通过EndDialog函数第二个参数指定EndDialog(hwnd, win32con.IDCLOSE)
  • 32000,通过MessageBoxTimeout实现的自动关闭对话框。

除此之外返回用户按下的对话框按键特殊值

MessageBox 参数简述

MessageBox

MessageBox函数:显示一个模态对话框,其中包含一个系统图标、一组按钮和一条特定于应用程序的简短消息,例如状态或错误信息。 消息框返回一个整数值,指示用户单击了哪个按钮。

MessageBox C++函数原型:

int MessageBox(
  HWND    hWnd,
  LPCTSTR lpText,
  LPCTSTR lpCaption,
  UINT    uType
);

参数说明:

  • hWnd:要创建的消息框所有者窗口的句柄。如果此参数为NULL,则消息框没有所有者窗口
  • lpText:消息内容。对于多行消息可用回车或换行符分割
  • lpCaption:对话框标题。如果为NULL,默认为Error
  • uType:指定对话框的内容和行为的位标志集(图标、按钮类型…),通过组合标志位联合定义,以下会进行说明

返回值:

  • IDOK (1):确认
  • IDCANCEL (2):取消
  • IDABORT (3):中止
  • IDRETRY (4):重试
  • IDIGNORE (5):忽略
  • IDYES (6):是
  • IDNO (7):否

详情请看:MessageBox function (winuser.h) - Win32 apps | Microsoft Docs

MessageBoxTimeout

MessageBoxTimeout函数:微软未公开的Windows API函数。实现定时消息,功能类似于MessageBox。如果用户不回应,能定时关闭消息框。函数由user32.dll导出,Windows 2000及以下没有此函数。

MessageBoxTimeout C++函数原型:

int MessageBoxTimeoutA(IN HWND hWnd, IN LPCSTR lpText, 
    IN LPCSTR lpCaption, IN UINT uType, 
    IN WORD wLanguageId, IN DWORD dwMilliseconds);

int MessageBoxTimeoutW(IN HWND hWnd, IN LPCWSTR lpText, 
    IN LPCWSTR lpCaption, IN UINT uType, 
    IN WORD wLanguageId, IN DWORD dwMilliseconds);

#ifdef UNICODE
    #define MessageBoxTimeout MessageBoxTimeoutW
#else
    #define MessageBoxTimeout MessageBoxTimeoutA
#endif 

#define MB_TIMEDOUT 32000

值的注意的是,如果使用UNICODE我们就使用MessageBoxTimeoutW,否则使用MessageBoxTimeoutA,一般情况下我们使用MessageBoxTimeoutW

它对于MessageBox多出两个参数:

  • wLanguageId:函数扩展,一般取0。
  • dwMilliseconds:消息框延迟关闭时间,单位:毫秒

返回值:与MessageBox一致,但如果超时,即用户未操作,且对话框没有指定或默认关闭的按钮,当消息框自动关闭,返回32000

详情请看:MessageBoxTimeout API - CodeProject

uType组合参数

文档中指出了MessageBox参数uType可组合的五组参数。

  • 按钮,单选
  • MB_ABORTRETRYIGNORE (0x00000002L):中止、重试和忽略
  • MB_CANCELTRYCONTINUE (0x00000006L):取消、重试、继续
  • MB_HELP (0x00004000L):帮助
  • MB_OK (0x000000000L):确定
  • MB_RETRYCANCEL (0x00000005L):重试、取消
  • MB_YESNO (0x00000004L):是、否
  • MB_YESNOCANCEL (0x00000003L):是、否、取消
  • 图标,单选
  • MB_ICONEXCLAMATIONMB_ICONWARNING (0x00000030L):惊叹号图标
  • MB_ICONINFORMATIONMB_ICONASTERISK (0x00000040L):一个由小写字母i组成的
  • MB_ICONQUESTION (0x00000020L):问号图标
  • MB_ICONSTOPMB_ICONERRORMB_ICONHAND (0x00000010L):停止标志图标
  • 默认按钮,单选
  • MB_DEFBUTTON1 (0x000000000L):第一个按钮是默认按钮,默认值
  • MB_DEFBUTTON2 (0x00000100L):第二个按钮是默认按钮
  • MB_DEFBUTTON3 (0x00000200L):第二个按钮是默认按钮
  • MB_DEFBUTTON4 (0x00000300L):第二个按钮是默认按钮
  • 对话框模式,单选
  • MB_APPLMODAL (0x000000000L):在hwnd参数标识的窗口中继续工作以前,用户一定响应消息框。但是,用户可以移动到其他线程的窗口且在这些窗口中工作。根据应用程序中窗口的层次机构,用户则以移动到线程内的其他窗口。所有母消息框的子窗口自动地失效,但是弹出窗口不是这样。如果既没有指定MB_SYSTEMMODAL也没有指定MB_TASKMOOAL,则MB_APPLMODAL为缺省的。
  • MB_SYSTEMMODAL (0x00001000L):除了消息框有WB_EX_TOPMOST类型,MB_APPLMODAL和MB_SYSTEMMODAL一样。用系统模态消息框来改变各种各样的用户,主要的损坏错误需要立即注意(例如,内存溢出)。如果不是那些与hwnd联系的窗口,此标志对用户对窗口的相互联系没有影响。
  • MB_TASKMODAL (0x00002000L):如果参数hwnd为NULL的话,那么除了所有属于当前线程高层次的窗口失效外,MB_TASKMODALL和MB_APPLMODAL一样。当调用应用程序或库没有一个可以得到的窗口句柄时,使用此标志。但仍需要阻止输入到调用线程的其他窗口,而不是搁置其他线程。
  • 特殊声明,多选
  • MB_DEFAULT_DESKTOP_ONLY (0x00020000L):接收输入的当前桌面一定是一个缺省桌面。否则,函数调用失败。缺省桌面是一个在用户已经纪录且以后应用程序在此上面运行的桌面。
  • MB_RIGHT (0x00080000L):文本为右调整
  • MB_RTLREADING (0x00100000L):用在Hebrew和Arabic系统中从右到左的顺序显示消息和大写文本
  • MB_SETFOREGROUND (0x00010000L):消息框成为前台窗口。系统在内部调用消息框的SetForegroundWindow函数。
  • MB_TOPMOST (0x00040000L):消息框是用WS_EX_TOPMOST窗口样式创建的。
  • MB_SERVICE_NOTIFICATION (0x00200000L):呼叫者是通知事件用户的服务。该功能在当前活动桌面上显示一个消息框,即使没有用户登录到计算机。

详情请看:MessageBox function (winuser.h) - Win32 apps | Microsoft Docs

实现一:MessageBox+EndDialog+threading.Timer

如果你想直接使用它,那么你需要安装 pywin32pip install pywin32

实现方式:

  • 通过win32api.MessageBox调用对话框
  • 通过threading.Timerwin32gui.EndDialog实现定时自动关闭对话框。
  • 通过线程实现异步对话框;通过回调函数实现对话框关闭回调。

代码如下:

import threading

import win32api
import win32con
import win32gui


# 对每次创建对话框进行计数,方便查找同名对话框的句柄
__show_dialog_times = 0


def create_dialog(message: str, title: str, style: int = win32con.MB_OK,
                  block: bool = None, interval: float = 0, callback=None):
    """
    创建一个 Windows 对话框,支持同步异步和自动关闭

    值得注意的是,对于多选一没有关闭/取消功能的对话框,自动关闭时默认(回调)返回值为win32con.IDCLOSE,
    例如win32con.MB_YESNO、win32con.MB_ABORTRETRYIGNORE等等。

    :param message: 对话框消息内容
    :param title: 对话框标题
    :param style: 对话框类型,该值可以相加组合出不同的效果。
    :param block: 对话框是否阻塞调用线程,默认值取决于interval<=0,为Ture不会自动关闭,意味着阻塞调用线程
    :param interval: 对话框自动关闭秒数
    :param callback: 对话框关闭时的回调函数,含一参数为对话框关闭结果(按下的按钮值)
    :return: 当对话框为非阻塞时,无返回值(None),否则,对话框阻塞当前线程直到返回,值为按下的按钮值
    """

    global __show_dialog_times
    __show_dialog_times += 1

    title = '{} [{}]'.format(title, __show_dialog_times)

    def show(timer: threading.Timer):
        btn_val = win32api.MessageBox(0, message, title, style)
        if timer and timer.is_alive():
            timer.cancel()
        if callback and callable(callback):
            callback(btn_val)
        return btn_val

    def close():
        hwnd = win32gui.FindWindow(None, title)
        if hwnd:
            try:
                # 关闭对话框的一些方法:PostMessage 异步,SendMessage 同步
                # win32gui.PostMessage(hwnd, win32con.WM_CLOSE, 0, 0)
                # win32gui.SendMessage(hwnd, win32con.WM_CLOSE, 0, 0)
                # win32gui.EndDialog(hwnd, None)
                # 需要注意的是倒数第二个参数,指定如何发送消息
                # http://timgolden.me.uk/pywin32-docs/win32gui__SendMessageTimeout_meth.html
                # win32gui.SendMessageTimeout(hwnd, win32con.WM_CLOSE, 0, 0, win32con.SMTO_BLOCK, 1000)
                win32gui.EndDialog(hwnd, win32con.IDCLOSE)
            except Exception as e:
                log.error("对话框[{}]关闭错误:{}".format(title, e))

    block = block if (block is not None) else interval <= 0

    timer = None
    if interval > 0:
        timer = threading.Timer(interval, close)
        timer.start()

    if block:
        return show(timer)
    else:
        threading.Thread(target=lambda: show(timer)).start()

实现二 (推荐):MessageBoxTimeout

import threading
import win32con

user32 = ctypes.windll.user32

def create_dialog(message: str, title: str, style: int = win32con.MB_OK,
                    block: bool = None, interval: float = 0, callback=None):
    """
    使用微软未公布的Windows API: MessageBoxTimeout 实现自动关闭的对话框,通过user32.dll调用,
    相比于使用 MessageBox 来实现显得更加简洁,参数详情请参考以上函数 create_dialog

    值得注意的是 Windows 2000 没有导出该函数。并且对于多选一没有关闭/取消功能的对话框,
    自动关闭时默认(回调)返回值为 32000
    """

    block = block if (block is not None) else interval <= 0
    interval = int(interval * 1000) if interval > 0 else 0

    def show():
        # if UNICODE MessageBoxTimeoutW
        # else MessageBoxTimeoutA
        # MessageBoxTimeout(hwnd, lpText, lpCaption, uType, wLanguageId, dwMilliseconds)
        btn_val = user32.MessageBoxTimeoutW(0, message, title, style, 0, interval)
        if callback and callable(callback):
            callback(btn_val)
        return btn_val

    if block:
        return show()
    else:
        threading.Thread(target=show).start()

如何使用

基本调用:

# 基本使用
if __name__ == '__main__':
    def cb(res):
        print("[回调函数] 对话框返回值:{}".format(res))

    # 同步阻塞+手动关闭
    res = create_dialog('同步阻塞+手动关闭', '同步阻塞对话框')
    print(res)
    # 同步阻塞+自动关闭
    res = create_dialog('同步阻塞+自动关闭', '同步阻塞对话框', block=True, interval=3)
	print(res)
    
    # 当然你也可以指定对话框回调函数(多此一举?)
    res = create_dialog('同步阻塞对话框+回调', '同步阻塞对话框', style=win32con.MB_YESNO, callback=cb)
    print(res)
    
    # 异步非阻塞+手动关闭
    res = create_dialog('异步非阻塞+手动关闭', '异步非阻塞自动关闭对话框')
    print(res)

    # 异步非阻塞+自动关闭
    res = create_dialog('异步非阻塞+自动关闭', '异步非阻塞自动关闭对话框', interval=3)
    print(res)

    time.sleep(5)

    # 添加回调函数以获取结果
    res = create_dialog('异步非阻塞自动关闭对话框+回调', '异步非阻塞自动关闭对话框', interval=3, callback=lambda x: cb(x))
    print(res)
    
    
    # 组合style,带警告图标的确认对话框,并且对话框弹出处于前台
    res = create_dialog('message', 'title', interval=2, callback=cb, style=win32con.MB_OK | win32con.MB_ICONERROR | win32con.MB_SETFOREGROUND)
    print(res)