首页 > 系统相关 >Windows 上左键按住窗口标题栏的阻塞和等待500ms"退出"阻塞两个行为的小研究

Windows 上左键按住窗口标题栏的阻塞和等待500ms"退出"阻塞两个行为的小研究

时间:2024-04-21 16:55:32浏览次数:30  
标签:case Windows hwnd WM 标题栏 阻塞 windows CheckLastError id

之前就注意到一个现象就是 windows 的窗口被右键菜单栏弹出菜单后或者按住右键后整个程序似乎会被冻结, 而对于游戏更是直接像停掉了主循环一样. 除此之外左键按住窗口也会有同样的效果, 但是例外是部分游戏会被阻塞 500ms 后恢复, 而有些干脆没受到影响. 不过这个问题看上去重要性不大所以之前一段时间没去在意它.
这一段时间摸 win32 和游戏相关的内容比较多, 发现左键按住默认的行为是直接阻塞住, 那问题就来了, 该如何让程序的行为像某些程序一样只是阻塞 500ms? 目前这是一段简单并简陋的 opengl 程序:

#include <Windows.h>
#include <wingdi.h>
#include <stdio.h>
#include <iostream>
#include <gl/GL.h>

#pragma comment(lib, "opengl32")
#pragma comment(lib, "winmm")

#define CheckLastError() _CheckLastError(__LINE__)
void _CheckLastError(int line)
{
    DWORD le = GetLastError();
    if (!le) return;
    LPVOID buf = nullptr;
    FormatMessageW(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, le, 0, (LPTSTR)&buf, 0, NULL);
    std::wcout << L"ERROR(L" << line << L"): " << (wchar_t*)buf << std::flush;
    LocalFree(buf);
}

LRESULT CALLBACK WindowProc(_In_ HWND hwnd, _In_ UINT uMsg, _In_ WPARAM wParam, _In_ LPARAM lParam);

float c = 0.0f;
HDC winHdc;
HGLRC hglrc;

static void tick()
{
    glClear(GL_COLOR_BUFFER_BIT);
    c += 0.02f;
    glColor3f(abs(c), 0.0f, abs(c));
    if (c >= 1.0f)
        c = -1.0f;
    glRectf(0.0f, 0.0f, 0.5f, 0.5f);
    SwapBuffers(winHdc);
    Sleep(10);
}

int WINAPI WinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance, _In_ LPSTR lpCmdLine, _In_ int nCmdShow)
{
    timeBeginPeriod(1);
#if _DEBUG
    AllocConsole();
    FILE* conout, * conin;
    freopen_s(&conout, "CONOUT$", "w+t", stdout);
    freopen_s(&conin, "CONIN$", "r+t", stdin);
#endif

    WNDCLASSW wc{};
    wc.lpfnWndProc = WindowProc;
    wc.hInstance = hInstance;
    wc.hCursor = LoadCursorW(nullptr, IDC_ARROW);
    wc.lpszClassName = L"MyWindowClass";

    RegisterClassW(&wc);

    HWND hwnd = CreateWindowExW(WS_EX_APPWINDOW | WS_EX_CLIENTEDGE | WS_EX_ACCEPTFILES,
        L"MyWindowClass", L"一个简单的win32窗口",
        WS_OVERLAPPEDWINDOW | WS_CLIPCHILDREN | WS_CLIPSIBLINGS,
        CW_USEDEFAULT, CW_USEDEFAULT, 1200, 800, nullptr, nullptr, hInstance, nullptr);

    ShowWindow(hwnd, nCmdShow);
    UpdateWindow(hwnd);

    MSG msg{};
    while (true)
    {
        if (!IsWindow(hwnd))
            return 0;
        while (PeekMessageW(&msg, hwnd, 0, 0, PM_REMOVE))
        {
            TranslateMessage(&msg);
            DispatchMessageW(&msg);
        }

        tick();
    }
    return 0;
}

uint32_t move_timer_id = 0U;

LRESULT CALLBACK WindowProc(_In_ HWND hwnd, _In_ UINT uMsg, _In_ WPARAM wParam, _In_ LPARAM lParam)
{
    switch (uMsg)
    {
    case WM_CREATE:
    {
        winHdc = GetDC(hwnd);
        CheckLastError();
        PIXELFORMATDESCRIPTOR ps{
        sizeof(PIXELFORMATDESCRIPTOR),
        1,                     // version number  
        PFD_DRAW_TO_WINDOW |   // support window  
        PFD_SUPPORT_OPENGL |   // support OpenGL  
        PFD_DOUBLEBUFFER,      // double buffered  
        PFD_TYPE_RGBA,         // RGBA type  
        24,                    // 24-bit color depth  
        0, 0, 0, 0, 0, 0,      // color bits ignored  
        0,                     // no alpha buffer  
        0,                     // shift bit ignored  
        0,                     // no accumulation buffer  
        0, 0, 0, 0,            // accum bits ignored  
        24,                    // 24-bit z-buffer      
        0,                     // no stencil buffer  
        0,                     // no auxiliary buffer  
        PFD_MAIN_PLANE,        // main layer  
        0,                     // reserved  
        0, 0, 0                // layer masks ignored  
        };
        int fmt = ChoosePixelFormat(winHdc, &ps); 
        if (!fmt) CheckLastError();
        SetPixelFormat(winHdc, fmt, &ps); CheckLastError();
        hglrc = wglCreateContext(winHdc); CheckLastError();
        wglMakeCurrent(winHdc, hglrc);
        glViewport(0, 0, 1200, 800);
        glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
        CheckLastError();
        return 0;
    }
    case WM_DESTROY:
    {
        PostQuitMessage(0);
        CheckLastError();
        return 0;
    }
    case WM_SIZE:
    {
        CheckLastError();
        int width = LOWORD(lParam);
        int height = HIWORD(lParam);
        glViewport(0, 0, width, height);
        PostMessageW(hwnd, WM_PAINT, 0, 0);
        CheckLastError();
        return 0;
    }
    case WM_PAINT:
    {
        CheckLastError();
        PAINTSTRUCT ps{};
        BeginPaint(hwnd, &ps);
        EndPaint(hwnd, &ps);
        CheckLastError();
        return 0;
    }
    case WM_ERASEBKGND:
        CheckLastError();
        return TRUE;
    }
    return DefWindowProcW(hwnd, uMsg, wParam, lParam);
}

然后我们很容易发现这个程序的行为是按住直接阻塞住, 同时想到到 godot 游戏的行为是按住 500ms 后会停止阻塞, 我们不妨去翻阅 godot 的源码来查找这个问题.
翻阅和调试的过程还算顺利, 最终可以定位到如下几行 godot 源码上(display_server_windows.cpp(L4324): WndProc):

...
case WM_ENTERSIZEMOVE: {
            Input::get_singleton()->release_pressed_events();
            windows[window_id].move_timer_id = SetTimer(windows[window_id].hWnd, 1, USER_TIMER_MINIMUM, (TIMERPROC) nullptr);
        } break;
        case WM_EXITSIZEMOVE: {
            KillTimer(windows[window_id].hWnd, windows[window_id].move_timer_id);
        } break;
        case WM_TIMER: {
            if (wParam == windows[window_id].move_timer_id) {
                _process_key_events();
                if (!Main::is_iterating()) {
                    Main::iteration();
                }
            } else if (wParam == windows[window_id].focus_timer_id) {
                _process_activate_event(window_id, windows[window_id].saved_wparam, windows[window_id].saved_lparam);
                KillTimer(windows[window_id].hWnd, wParam);
                windows[window_id].focus_timer_id = 0U;
            }
        } break;
...

似乎有点复杂, 其中重要的是 move_timer_id 这个计时器, 我们可以看到在 WM_ENTERSIZEMOVE 消息发生时, 也就是左键按住窗口后, 该计时器被创建, 其中时长请求了 windows 能提供的最短时间, 不过这不要紧, 毕竟这会消息循环还在阻塞呢. 然后在 500ms(估计是 windows 内部的一个设置)后, 我们会发现此时 WM_TIMER 能够被触发了, 所以我们不妨在这会继续进行一次更新. 最后在左键被松开时, WM_EXITSIZEMOVE 发生后清除这个计时器避免多次触发更新.
那么知道具体如何了, 我们把这部分代码抄到刚才的简陋的 opengl 程序中:
首先把一次渲染的操作提到一个函数里, 比如直接叫 tick:

static void tick()
{
    glClear(GL_COLOR_BUFFER_BIT);
    c += 0.02f;
    glColor3f(abs(c), 0.0f, abs(c));
    if (c >= 1.0f)
        c = -1.0f;
    glRectf(0.0f, 0.0f, 0.5f, 0.5f);
    SwapBuffers(winHdc);
    Sleep(10);
}

然后搞个保存计时器 id 的变量:

UINT_PTR move_timer_id = 0U;

最后在消息循环重要的部分加上那三个消息的处理:

case WM_ENTERSIZEMOVE:
{
    move_timer_id = SetTimer(hwnd, 1, USER_TIMER_MINIMUM, (TIMERPROC) nullptr);
    break;
}
case WM_EXITSIZEMOVE:
{
    KillTimer(hwnd, move_timer_id);
    break;
}
case WM_TIMER:
{
    if (wParam == move_timer_id)
    {
        tick();
    }
    break;
}

现在启动程序, 按住程序标题栏, 发现确实是期望的等待 500ms 后阻塞"停止".


最后我们也在 WinForms 框架中尝试这个操作. 已知的是 WinForms 的 Application.Idle 会在处理完一次消息后执行, 也就相当于之前的 tick(), 同时在按住 WinForms 的窗口时该事件不会被执行.
首先同样构造一些能帮我们识别程序是否阻塞了的标志, 比如这里订阅 Application.Idle 并持续更改一个 textbox 的内容:

namespace WinFormsApp1;

public partial class Form1 : Form
{
    private int counter;
    
    public Form1()
    {
        InitializeComponent();
    }

    private void Form1_Load(object sender, EventArgs e)
    {
        Application.Idle += Application_Idle;
    }

    private void Application_Idle(object? sender, EventArgs e)
    {
        counter += 1;
        textBox1.Text = $"counter: {counter}";
    }
}

不过有些小问题,我个人并不是很熟一些 win32 的东西封装进 WinForms 成什么了, 所以暂且暴力解决一下:

using System.Runtime.InteropServices;

namespace WinFormsApp1;

public partial class Form1 : Form
{
    private nint moveTimer;
    private int counter;

    public Form1()
    {
        InitializeComponent();
    }

    private void Form1_Load(object sender, EventArgs e)
    {
        Application.Idle += Application_Idle;
    }

    private void Application_Idle(object? sender, EventArgs e)
    {
        Tick();
    }

    private void Tick()
    {
        counter += 1;
        textBox1.Text = $"counter: {counter}";
    }

    protected override void WndProc(ref Message m)
    {
        switch (m.Msg)
        {
        case 0x0231: // WM_ENTERSIZEMOVE
            moveTimer = SetTimer(m.HWnd, 1, 0x0000000A, IntPtr.Zero);
            return;
        case 0x0232: // WM_EXITSIZEMOVE
            KillTimer(m.HWnd, moveTimer);
            moveTimer = 0;
            return;
        case 0x0113: // WM_TIMER
            Tick();
            return;
        }
        base.WndProc(ref m);
    }

    [DllImport("user32.dll")]
    private static extern nint SetTimer(IntPtr hWnd, nint nIDEvent, uint uElapse, IntPtr lpTimerFunc);

    [DllImport("user32.dll")]
    private static extern int KillTimer(IntPtr hWnd, nint uIDEvent);
}

一些题外话:
在研究这个问题的时候发现了一个额外有趣的用法. 在调试这个问题时发现阻塞状态下 DefWndProc 卡在 WM_SYSCOMMAND 这个消息上, 其中 wParam 值为 0xf012, 不过奇怪的是 查阅 msdn 时并没有找到值为 0xf012 的解释, 经过一番搜索发现这个值其实是 SC_MOVE + HTCAPTION, 而在 mfc 框架内部也是忽略了这个值的最后一位:

...
void CFrameWnd::OnSysCommand(UINT nID, LONG lParam)
{
    CFrameWnd* pFrameWnd = GetTopLevelFrame();
    ASSERT_VALID(pFrameWnd);

    // set status bar as appropriate
    UINT nItemID = (nID & 0xFFF0);  // 忽略最后一位

    // don't interfere with system commands if not in help mode
    if (pFrameWnd->m_bHelpMode)
...

有趣的用法就是, 我们可以手动发送这个消息, 比如通过 SendMessage(hwnd, WM_SYSCOMMAND, SC_MOVE + HTCAPTION, 0), 如果在处理这个消息的时候鼠标左键是按下的(任意窗口上的位置), 你会发现此时就像按住了标题栏一样在拖动窗口.

标签:case,Windows,hwnd,WM,标题栏,阻塞,windows,CheckLastError,id
From: https://www.cnblogs.com/saplonily/p/18149150

相关文章

  • Algorius Net Viewer 2024.2.1 (Windows) - 网络可视化、管理、监控和清点
    AlgoriusNetViewer2024.2.1(Windows)-网络可视化、管理、监控和清点Comprehensivesoftwareproductforvisualizing,administering,monitoring,andinventoryingcomputersnetworkofanylevel请访问原文链接:AlgoriusNetViewer2024.2.1(Windows)-网络可视化......
  • windows版redis使用bat文件启动闪退
    在redis中使用批处理命令。建立bat后缀的文件。里面内容为:titleredis-6379redis-server.exeredis.windows.conf但是双击执行bat文件,redis出现窗口闪退。解决方案:检查任务管理器中redis是否有启动。有则关闭,重新双击bat文件启动。否则1.Windows+r进入cmd。2.cd进入到re......
  • 【Redis】Windows下安装Redis
    我们已经能够在Linux下安装Redis,接下来我们可以学习在Windows下安装和使用Redis。下载源码并解压下载地址:https://github.com/tporadowski/redis/releases。Redis支持32位和64位。这个需要根据你系统平台的实际情况选择,这里我们下载Redis-x64-xxx.zip压缩包到C盘,解压......
  • 35-windows通过cmd查看端口占用,并停止该端口,杀死进程kill等命令
     1)cmd命令提示符窗口后,输入“netstat-ano”并按下回车执行,之后就会显示电脑上运行的所有端口号netstat-ano 2) 如果已知被占用的端口时,可以用命令netstat-aon|findstr8109直接找到端口号为7009的进程,PID为36304 netstat-aon|findstr8019 3) 根据PID进......
  • netdom 是一个 Windows 命令行工具,用于管理 Windows 计算机和域的成员身份。它提供了
    netdom/?此命令的语法是:NETDOMHELP命令   -或-NETDOM命令/帮助  可用的命令有:  NETDOMADD       NETDOMRESETPWD    NETDOMRESET  NETDOMCOMPUTERNAME  NETDOMQUERY      NETDOMTRUST  NETDOMHELP......
  • Ubuntu 22.04 解决和 Windows 共享蓝牙设备的问题
    我有一个Airpods,连接到WIndows可以正常工作,但连接到ubuntu后会无法连接,只能删除设备选择重联,但是这又会导致Windows不能连接到耳机,只能也删除重新连接,费神费力。要解决此问题,仍有两办法,让Windows将就Linux,或者Linux将就Windows,由于折腾注册表不太稳定,还是选择后者。......
  • Ubuntu 22.04 和 Windows 时间冲突解决方案
    默认情况下,Ubuntu(和大多数其他Linux发行版)假设硬件时钟设置为协调世界时间(UTC+0),而Windows则假设硬件时钟设置为当地时间,这导致Ubuntu快8小时。这种差异会导致你在双启动系统中切换操作系统时,经常遇到时间显示不正确的问题。要解决这个问题,有两种常用方法,要么让Linux......
  • 配置Linux【虚拟机】与 windows【宿主机】网络互通 (面向小白,简单操作)
    1.启动虚拟机,运行Linux系统这里我使用VMwareWorkstationPro来运行Linux系统(cent-os7)2.鼠标右键打开终端3.输入cd/etc/sysconfig/network-scripts,然后输入ls,查看当前目录下的网卡一般来说,虚拟机的网卡都是ifcfg-ens33的,当然也会有命名为ifcfg-ens32,注意辨......
  • windows下git客户端tortoise的使用
    一、软件安装这里不仅需要安装tortoise还需要安装git.他们是两个不同的应用哈。二、创建工程 一般我们的github上或者gitlab上先创建一个工程,然后在本地拉取该工程。在本地文件夹中点击右键选择“GitClone”填写正确的库地址等信息。三、提交工程先查看本地是否有更新,选中......
  • 【超详细】Windows申请iOS证书上架App Store详细教程
    ​转载:Windows申请iOS证书上架AppStore详细教程(有这一篇就够了)_windows提交ios审核-CSDN博客Windows申请iOS证书上架AppStore详细教程上架基本需求资料1、苹果开发者账号(如还没账号先申请-苹果开发者账号申请教程)2、开发好的APP通过本篇教程,可以学习到ios证书申请和打包i......