程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> C++ >> C++入門知識 >> 讓任務管理器播放動畫,任務管理器播放動畫

讓任務管理器播放動畫,任務管理器播放動畫

編輯:C++入門知識

讓任務管理器播放動畫,任務管理器播放動畫


一、源起

原先在B站上看到各式各樣拿Windows任務管理器播放動畫的視頻,感覺很新奇,也有人無私分享代碼。有些視頻中的動畫是後期加上的,也有些是實時渲染的。不管怎樣,像實時渲染這類程序就非常“奇特”,它是怎麼讓任務管理器播放視頻的呢?

自制高仿山寨視頻

工程源碼

二、探秘

“拿來主義”

如要完成一個程序,抑或是一個功能,不會寫,怎麼辦?拿來!

得到源碼,解讀ing,然後重寫,消化吸收,這是“高效”的學習方法。

剖析源碼

剖析源碼,理解思路。

這個程序大致的工作流程如下:

啟動子進程

常用的啟動進程無非是雙擊一下程序,但在C++中如何調用呢,用ShellExecute。

運行命令行:

ShellExecute(NULL, "open", "taskmgr", NULL, NULL, SW_SHOWNORMAL);

第三個參數相當於“開始-運行”中的命令。

由於taskmgr是子進程,那麼我們的父進程對它進行控制就有很高的權限。

網上常見的一段提權代碼:

HANDLE hToken;
if (!OpenProcessToken(::GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES, &hToken))
{
    return;//ERROR
}
TOKEN_PRIVILEGES tkp;
tkp.PrivilegeCount = 1;
if (!LookupPrivilegeValue(nullptr, SE_DEBUG_NAME, &tkp.Privileges[0].Luid))
{
    return;//ERROR
}
if (!AdjustTokenPrivileges(hToken, false, &tkp, sizeof(tkp), nullptr, nullptr))
{
    return;//ERROR
}

注入子進程

啟動子進程後,等待一段時間,開始注入。

“注入”的意思,比方說“注水豬肉”,將自己刻意編寫的代碼注入到目標進程中。

既然我們對taskmgr擁有最高權限,那麼注入也就不成問題。

找到進程

首先,需要找到子進程(taskmgr,下面略),在茫茫進程樹中,如何找到它呢?

我們就用一個比較便捷的方法。

HWND hWnd = FindWindow("TaskManagerWindow", "任務管理器");

根據類名和窗口名稱找到它,做到這些,只需下一個Spy++,自制Spy。

有人問,如果有開了多個任務管理器怎麼辦?那只能全部關掉重新試了,因為這裡有權限的問題。

接下來獲取它的句柄:

DWORD dwPId;
GetWindowThreadProcessId(hWnd, &dwPId);

進程注入

以最高權限打開進程:

HANDLE hRemoteProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwPId);

現在需要將縮寫的dll的路徑寫入到子進程:

LPCTSTR szLibPath = "DLL的絕對路徑";
LPVOID pLibRemoteSrc = VirtualAllocEx(hRemoteProcess, nullptr, nLibPathLength, MEM_COMMIT, PAGE_READWRITE);
WriteProcessMemory(hRemoteProcess, pLibRemoteSrc, LPVOID(*szLibPath), nLibPathLength, &dwPathLength);

現在,子進程的pLibRemoteSrc中就存放了DLL的路徑。

下面需要讓子進程根據路徑加載DLL。

進程加載DLL的函數是LoadLibraryA,我們需要讓子進程執行這個函數,首先需要獲取這個函數的地址。

LoadLibraryA位於kernel32.dll中,由於其特殊性,不同進程會將kernel32.dll加載到同一個地址,此外還有user32.dll等等,這給我們帶來了方便。

獲取LoadLibraryA的地址:

HMODULE hKernel32 = GetModuleHandleA("Kernel32");
FARPROC fpcLoadLibrary = GetProcAddress(hKernel32, "LoadLibraryA");

在子進程中執行LoadLibraryA:

CreateRemoteThread(hRemoteProcess, NULL, NULL, LPTHREAD_START_ROUTINE(fpcLoadLibrary), pLibRemoteSrc, NULL, NULL);

收尾工作

調用VirtualFreeEx釋放內存,CloseHandle關閉句柄,不用多說。

教會子進程

目前是程序最核心的階段——“教會”taskmgr去播放動畫。

動畫都是由一幀幀組成,現在我們已經擁有幾千幀的圖片,放置在某個文件夾中,編號如0000.jpg-9999.jpg

找到重繪窗口

我們需要找到播放動畫的那個子窗口,怎麼找?用Spy?我們來個高大上的Hook。

Hook在這裡的用處就是過濾或是截獲消息。

首先,我們要替換的CPU圖表為什麼會不停地動?就是因為定時器對它發送重繪消息,我們可以用Hook截獲它,或是SetWindowLong過濾掉它。在嘗試過程中,發現Hook有點問題,因此采用SetWindowLong。

其次,我們要高仿個Spy的功能——實時定位鼠標所在窗口。怎麼做呢?鼠標在移動過程中會發送移動消息WM_MOUSEMOVE,那麼我們規定按下鼠標中鍵WM_MBUTTONDOWN,鼠標此時所在的窗口就播放動畫。

那麼啟用Hook:

SetWindowsHookEx(WH_GETMESSAGE, HOOKPROC(MsgHookProc), NULL, GetWindowThreadProcessId(g_hWnd, NULL));

監聽消息:

LRESULT CALLBACK MsgHookProc(int nCode, WPARAM wParam, LPARAM lParam)
{
    if (nCode < 0) {
        return CallNextHookEx(g_hHook, nCode, wParam, lParam);
    }

    if (nCode == HC_ACTION)
    {
        auto lpMsg = LPMSG(lParam);
        POINT pt;

        switch (lpMsg->message) {
        case WM_MOUSEMOVE:
            if (g_bHooking)
            {
                pt = lpMsg->pt;
                ScreenToClient(g_hWnd, &pt);
                SpyExecScanning(pt);
            }
            break;
        case WM_MBUTTONDOWN:
            if (g_bHooking)
            {
                pt = lpMsg->pt;
                g_prevHwnd = SpyFindSmallestWindow(pt); //找到當前鼠標所在窗口
                g_bHooking = FALSE;
                uHook = SetTimer(g_hWnd, WM_USER + 401, 1000, TIMERPROC(HookProc)); //利用定時器創建渲染線程
            }
            break;
        default:
            break;
        }
    }

    return CallNextHookEx(g_hHook, nCode, wParam, lParam);
}

在這裡面有SpyFindSmallestWindow(找到鼠標所在窗口)和SpyExecScanning(對鼠標所在窗口邊框進行加粗)。

找到鼠標所在窗口:

HWND SpyFindSmallestWindow(const POINT &pt)
{
    auto hWnd = WindowFromPoint(pt); // 鼠標所在窗口

    if (hWnd)
    {
        // 得到本窗口大小和父窗口句柄,以便比較
        RECT rect;
        ::GetWindowRect(hWnd, &rect);
        auto parent = ::GetParent(hWnd); // 父窗口

        // 只有該窗口有父窗口才繼續比較
        if (parent)
        {
            // 按Z方向搜索
            auto find = hWnd; // 遞歸調用句柄
            RECT rect_find;

            while (1) // 循環
            {
                find = ::GetWindow(find, GW_HWNDNEXT); // 得到下一個窗口的句柄
                ::GetWindowRect(find, &rect_find); // 得到下一個窗口的大小

                if (::PtInRect(&rect_find, pt) // 鼠標所在位置是否在新窗口裡
                    && ::GetParent(find) == parent // 新窗口的父窗口是否是鼠標所在主窗口
                    && ::IsWindowVisible(find)) // 窗口是否可視
                {
                    // 比較窗口,看哪個更小
                    if (RECT_SIZE(rect_find) < RECT_SIZE(rect))
                    {
                        // 找到更小窗口
                        hWnd = find;

                        // 計算新窗口的大小
                        ::GetWindowRect(hWnd, &rect);
                    }
                }

                // hWnd的子窗口find為NULL,則hWnd為最小窗口
                if (!find)
                {
                    break; // 退出循環
                }
            }
        }
    }

    return hWnd;
}

對鼠標所在窗口邊框進行加粗:

void SpyInvertBorder(const HWND &hWnd)
{
    // 若非窗口則返回
    if (!IsWindow(hWnd))
        return;

    RECT rect; // 窗口矩形

    // 得到窗口矩形
    ::GetWindowRect(hWnd, &rect);

    auto hDC = ::GetWindowDC(hWnd); // 窗口設備上下文

    // 設置窗口當前前景色的混合模式為R2_NOT
    // R2_NOT - 當前的像素值為屏幕像素值的取反,這樣可以覆蓋掉上次的繪圖
    SetROP2(hDC, R2_NOT);

    // 創建畫筆
    HPEN hPen;

    // PS_INSIDEFRAME - 產生封閉形狀的框架內直線,指定一個限定矩形
    // 3 * GetSystemMetrics(SM_CXBORDER) - 三倍邊界粗細
    // RGB(0,0,0) - 黑色
    hPen = ::CreatePen(PS_INSIDEFRAME, 3 * GetSystemMetrics(SM_CXBORDER), RGB(0, 0, 0));

    // 選擇畫筆
    auto old_pen = ::SelectObject(hDC, hPen);

    // 設定畫刷
    auto old_brush = ::SelectObject(hDC, GetStockObject(NULL_BRUSH));

    // 畫矩形
    Rectangle(hDC, 0, 0, RECT_WIDTH(rect), RECT_HEIGHT(rect));

    // 恢復原來的設備環境
    ::SelectObject(hDC, old_pen);
    ::SelectObject(hDC, old_brush);

    DeleteObject(hPen);
    ReleaseDC(hWnd, hDC);
}

void SpyExecScanning(POINT &pt)
{
    ClientToScreen(g_hWnd, &pt); // 轉換到屏幕坐標

    auto current_window = SpyFindSmallestWindow(pt); //找到當前位置的最小窗口

    if (current_window)
    {
        // 若是新窗口,就把舊窗口的邊界去掉,畫新窗口的邊界
        if (current_window != g_prevHwnd)
        {
            SpyInvertBorder(g_prevHwnd);
            g_prevHwnd = current_window;
            SpyInvertBorder(g_prevHwnd);
        }
    }

    g_savedHwnd = g_prevHwnd;
}

那麼我們就實現了跟隨鼠標找到目標窗口的功能。

大體思路:

屏蔽重繪消息

LRESULT CALLBACK PaintProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
    if (msg == WM_PAINT)
        return TRUE;

    if (msg == WM_LBUTTONUP)
        bUpdate = !bUpdate;

    return CallWindowProc(oldProc, hWnd, msg, wParam, lParam);
}

SetWindowLong(hWnd, GWL_WNDPROC, LONG(PaintProc));

替換窗口子過程,如是重繪消息,不予處理。

啟動渲染線程

原先的啟動Hook、替換子過程等任務是在OnAttach中做的,即DLL剛加載至子進程中,而在OnAttach這個初始化線程中,是無法創建新線程的,這是因為此時DLL尚未初始化完成。

然而有解決方法,即用SetTimer運行任務,當任務被Timer喚醒時,DLL已加載完畢。

此時開始渲染。

三、進軍

在播放動畫之前,做了許多繁瑣的工作。其實涉及渲染的代碼反而不是很多。

初始化SDL

我們這裡用SDL完成渲染任務,一方面為了方便,另一方面體驗一把SDL。

去官網上下載SDL後,還需下一個SDL_TTF插件,用來顯示文字。

下面就初始化:

Print("Create SDL Window...");
    if (SDL_Init(SDL_INIT_VIDEO) < 0)
    {
        Print("Create SDL Window... FAILED");
        Print(SDL_GetError());
        return;
    }

    sdlWindow = SDL_CreateWindowFrom(static_cast<void*>(hWnd));
    if (sdlWindow)
        Print("Create SDL Window... OK");
    else
    {
        Print("Create SDL Window... FAILED");
        return;
    }
    Print("Create SDL Surface... OK");

    sdlRenderer = SDL_CreateRenderer(sdlWindow, -1,
        SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC);
    if (!sdlRenderer)
    {
        Print("Create SDL Renderer... FAILED");
        return;
    }
    Print("Create SDL Renderer... OK");

    sdlSurface = SDL_GetWindowSurface(sdlWindow);
    if (!sdlSurface)
    {
        Print("Create SDL Surface... FAILED");
        return;
    }
    Print("Create SDL Surface... OK");

    if (TTF_Init() == -1)
    {
        Print("Create SDL TTF... FAILED");
        Print(TTF_GetError());
        return;
    }
    Print("Create SDL TTF... OK");

    SDL_SetRenderDrawColor(sdlRenderer, 255, 255, 255, 255);
    SDL_RenderClear(sdlRenderer);
    SDL_RenderPresent(sdlRenderer);

    auto font = TTF_OpenFont("C:\\windows\\fonts\\msyh.ttf", 32);//微軟雅黑
    assert(font);
    SDL_Color color = { 17, 152, 187 };

    auto surface = TTF_RenderUNICODE_Blended(font, PUINT16(L"准備播放動畫!"), color);
    auto texture = SDL_CreateTextureFromSurface(sdlRenderer, surface);

    SDL_Rect rt;
    rt.x = 0;
    rt.y = 0;
    SDL_QueryTexture(texture, nullptr, nullptr, &rt.w, &rt.h);

    SDL_RenderClear(sdlRenderer);
    SDL_RenderCopy(sdlRenderer, texture, &rt, &rt);
    SDL_RenderPresent(sdlRenderer);

    SDL_DestroyTexture(texture);
    SDL_FreeSurface(surface);
    TTF_CloseFont(font);

    SDL_Delay(2000);

    Prepare();

    GetWindowTextA(g_hWnd, oldCaption, sizeof(oldCaption));

    uSDL = SetTimer(g_hWnd, WM_USER + 402, REFRESH_RATE, SDLProc);//啟動計時器,開始播放逐幀動畫

我們將目標窗口的句柄直接傳給了SDL,這樣有個好處——窗口大小變化時,動畫的大小也會相應變化。

逐幀播放

在每一幀中,我們要加載一幀圖片,處理後再渲染。

VOID CALLBACK SDLProc(HWND, UINT, UINT_PTR, DWORD)
{
    static char filename[100];

    if (bUpdate)
        nSDLTime++;
    else
        return;

    sprintf_s(filename, g_strImagePathFormat, nSDLTime);
    int x, y, comp;
    auto data = stbi_load(filename, &x, &y, &comp, 0);
    if (!data)
        return;

    ProcessingImage(data, x, y, comp, x * comp);

    auto image = SDL_CreateRGBSurfaceFrom(data, x, y, comp << 3, x * comp, 0, 0, 0, 0);
    if (!image)
    {
        SetWindowTextA(g_hWnd, SDL_GetError());
        return;
    }

    auto texture = SDL_CreateTextureFromSurface(sdlRenderer, image);

    SDL_RenderClear(sdlRenderer);
    SDL_RenderCopy(sdlRenderer, texture, nullptr, nullptr);
    SDL_RenderPresent(sdlRenderer);

    SDL_DestroyTexture(texture);
    SDL_FreeSurface(image);
    stbi_image_free(data);

    if (nSDLTime > MAX_FRAME)
    {
        //對SDL的清掃工作

        if (uSDL)
            KillTimer(g_hWnd, uSDL);
        return;
    }
}

圖像處理

大家發現,顯示的動畫跟保存的jpg畫風相差太大,這是因為程序對圖片進行了處理。

簡單來說,做的工作有:

void ProcessingImage(stbi_uc* data, int width, int height, int comp, int pitch)
{
    int i, j;
    BYTE c, prev;
    //二值化
    for (j = 0; j < height; j++)
    {
        for (i = 0; i < width; i++)
        {
            //auto B = data[j * pitch + i * comp];
            //auto G = data[j * pitch + i * comp + 1];
            //auto R = data[j * pitch + i * comp + 2];
            auto Gray = 0.212671f * data[j * pitch + i * comp + 2] +
                0.715160f * data[j * pitch + i * comp + 1] +
                0.072169f * data[j * pitch + i * comp];
            if (Gray < 128.0f)
            {
                data[j * pitch + i * comp] = 0;
                data[j * pitch + i * comp + 1] = 0;
                data[j * pitch + i * comp + 2] = 0;
            }
            else
            {
                data[j * pitch + i * comp] = 255;
                data[j * pitch + i * comp + 1] = 255;
                data[j * pitch + i * comp + 2] = 255;
            }
        }
    }
    //邊緣檢測
    prev = 0;
    for (j = 0; j < height; j++)
    {
        for (i = 0; i < width; i++)
        {
            c = data[j * pitch + i * comp];
            if (c != prev)
            {
                data[j * pitch + i * comp] = DISPLAY_B;
                data[j * pitch + i * comp + 1] = DISPLAY_G;
                data[j * pitch + i * comp + 2] = DISPLAY_R;
            }
            prev = c;
        }
    }
    //邊緣檢測
    prev = 0;
    for (i = 0; i < width; i++)
    {
        for (j = 0; j < height; j++)
        {
            c = data[j * pitch + i * comp];
            if (c != prev)
            {
                data[j * pitch + i * comp] = DISPLAY_B;
                data[j * pitch + i * comp + 1] = DISPLAY_G;
                data[j * pitch + i * comp + 2] = DISPLAY_R;
            }
            prev = c;
        }
    }
    //背景
    for (i = 0; i < width; i++)
    {
        for (j = 0; j < height; j++)
        {
            c = data[j * pitch + i * comp];
            if (c == DISPLAY_B)
                continue;
            if ((j % (height / 10) == 0) && j != 0)//橫線
            {
                data[j * pitch + i * comp] = LINE_B;
                data[j * pitch + i * comp + 1] = LINE_G;
                data[j * pitch + i * comp + 2] = LINE_R;
            }
            else if ((i % (width / 5) == (((MAX_FRAME - nSDLTime) / 30 * (width / 20))) % (width / 5)) && i != 0)//豎線
            {
                data[j * pitch + i * comp] = LINE_B;
                data[j * pitch + i * comp + 1] = LINE_G;
                data[j * pitch + i * comp + 2] = LINE_R;
            }
            else if (c == 255)
            {
                data[j * pitch + i * comp] = BG_B;
                data[j * pitch + i * comp + 1] = BG_G;
                data[j * pitch + i * comp + 2] = BG_R;
            }
            else if (c == 0)
            {
                data[j * pitch + i * comp] = FILL_B;
                data[j * pitch + i * comp + 1] = FILL_G;
                data[j * pitch + i * comp + 2] = FILL_R;
            }
        }
    }
    //邊框
    for (i = 0; i < width; i++)
    {
        for (j = 0; j < height; j++)
        {
            if ((i == 0 || i == width - 1) || (j == 0 || j == height - 1))
            {
                data[j * pitch + i * comp] = EDGE_B;
                data[j * pitch + i * comp + 1] = EDGE_G;
                data[j * pitch + i * comp + 2] = EDGE_R;
            }
        }
    }
}

日志輸出

我們需要子進程把渲染的信息實時在控制台中顯示出來。

這就涉及到進程間通訊(IPC)了,方法有很多,這裡以管道(Pipe)為例。

主進程-控制台(管道讀):

子進程-應用程序(管道寫):

四、尾聲

一個非常有趣的程序就這麼完成了。

通過此次實驗,我們接觸了:

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved