效果就是gif所展示的,我做这个是想模仿游戏里面拖动物品到方格上部署的这么一个效果,用纯windowsapi实现起来还真的有那么一丢丢困难。主要涉及以下这几个点:
1、双缓冲绘图:缓冲绘图十分重要,可以屏蔽掉InvalidateRect清除背景,如果直接先清除再重画,那么一定会看到屏闪。所以一般不对hdc进行直接操作,所有操作都是对内存DC操作,比如移动,我先让原位置的物体用背景替代,然而在新的位置绘制即可,最后再将内存DC拷贝给HDC,期间就没有擦除背景这个过程,而是对于原图的直接覆盖。这个也就在比较底层的设计中会看到吧,MFC中都有封装好的
2、Invalidate与wm_paint的延迟:我用双缓冲的时候移动物体线条居然会闪,那么考虑的就是刷新区域的问题,下面注释中写得有
3、渐变效果,动画的控制,alphablend混合
4、碰撞(距离)检测,:block什么时候变色,可以像物理引擎中碰撞检测一样,或者我这就很简单的检测中心的距离作为判断依据
5、重叠与复原:一般图形变化都是由一个基础图形经过一系列的转化形成的,那么重要的就是保存这个基础图形,然后再对其进行不一样的数值变化操作
还是先上代码,注释我写得很详细,上面主要的几点都在注释中写得有,我这就不再过多的讲解了
// Square.cpp: 定义应用程序的入口点。 // #include "stdafx.h" #include "Square.h" #include <condition_variable> #include <deque> #define MAX_LOADSTRING 100 #define WM_INVALIDATE 104 #pragma comment(lib, "Msimg32.lib") using namespace std; // 全局变量: HINSTANCE hInst; // 当前实例 WCHAR szTitle[MAX_LOADSTRING]; // 标题栏文本 WCHAR szWindowClass[MAX_LOADSTRING]; // 主窗口类名 // 此代码模块中包含的函数的前向声明: ATOM MyRegisterClass(HINSTANCE hInstance); BOOL InitInstance(HINSTANCE, int); LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); INT_PTR CALLBACK About(HWND, UINT, WPARAM, LPARAM); HDC hSrcDc; HBITMAP hSrc; RECT clientRect; HDC hMainDc; HBITMAP hMain; HWND mWnd; mutex m; condition_variable cv; bool animate = false; bool isAlive = true; int rectStatus = 0;//深色渐变/恢复 int ori = 0;//透明度 bool init = 0;//paint初始化状态标志 RECT mOriRect;//按下鼠标 物体位置 RECT mRect;//物体实时位置 bool isDown = false; POINT downPoint;//按下鼠标点 bool drawRect = false; HBRUSH bkBrush;//背景刷 int areaStatus = 0;//0初始状态 1重叠状态 2分离状态 3无效态 //线程控制动画效果,我这1S大概60FPS unsigned int _stdcall Animate(void *ch) { while(isAlive) { unique_lock<mutex> l(m); cv.wait(l); while(animate) { SendMessage(mWnd, WM_INVALIDATE, 0, 0); Sleep(16); } } return 0; } int APIENTRY wWinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance, _In_ LPWSTR lpCmdLine, _In_ int nCmdShow) { _beginthreadex(0, 0, Animate, 0, 0, 0); UNREFERENCED_PARAMETER(hPrevInstance); UNREFERENCED_PARAMETER(lpCmdLine); // TODO: 在此放置代码。 // 初始化全局字符串 LoadStringW(hInstance, IDS_APP_TITLE, szTitle, MAX_LOADSTRING); LoadStringW(hInstance, IDC_SQUARE, szWindowClass, MAX_LOADSTRING); MyRegisterClass(hInstance); // 执行应用程序初始化: if (!InitInstance (hInstance, nCmdShow)) { return FALSE; } HACCEL hAccelTable = LoadAccelerators(hInstance, MAKEINTRESOURCE(IDC_SQUARE)); MSG msg; // 主消息循环: while (GetMessage(&msg, nullptr, 0, 0)) { if (!TranslateAccelerator(msg.hwnd, hAccelTable, &msg)) { TranslateMessage(&msg); DispatchMessage(&msg); } } isAlive = false; cv.notify_all(); return (int) msg.wParam; } // // 函数: MyRegisterClass() // // 目的: 注册窗口类。 // ATOM MyRegisterClass(HINSTANCE hInstance) { WNDCLASSEXW wcex; wcex.cbSize = sizeof(WNDCLASSEX); wcex.style = CS_HREDRAW | CS_VREDRAW; wcex.lpfnWndProc = WndProc; wcex.cbClsExtra = 0; wcex.cbWndExtra = 0; wcex.hInstance = hInstance; wcex.hIcon = LoadIcon(hInstance, MAKEINTRESOURCE(IDI_SQUARE)); wcex.hCursor = LoadCursor(nullptr, IDC_ARROW); HBRUSH bkBrush = CreateSolidBrush(RGB(255, 255, 255)); wcex.hbrBackground = bkBrush; wcex.lpszMenuName = 0; wcex.lpszClassName = szWindowClass; wcex.hIconSm = LoadIcon(wcex.hInstance, MAKEINTRESOURCE(IDI_SMALL)); return RegisterClassExW(&wcex); } // // 函数: InitInstance(HINSTANCE, int) // // 目的: 保存实例句柄并创建主窗口 // // 注释: // // 在此函数中,我们在全局变量中保存实例句柄并 // 创建和显示主程序窗口。 // BOOL InitInstance(HINSTANCE hInstance, int nCmdShow) { hInst = hInstance; // 将实例句柄存储在全局变量中 HWND hWnd = CreateWindowW(szWindowClass, szTitle, WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, nullptr, nullptr, hInstance, nullptr); if (!hWnd) { return FALSE; } ShowWindow(hWnd, nCmdShow); UpdateWindow(hWnd); mWnd = hWnd; return TRUE; } // // 函数: WndProc(HWND, UINT, WPARAM, LPARAM) // // 目的: 处理主窗口的消息。 // // WM_COMMAND - 处理应用程序菜单 // WM_PAINT - 绘制主窗口 // WM_DESTROY - 发送退出消息并返回 // // void ControlAnimate() { if (drawRect) { if (!animate) { animate = true; cv.notify_all(); } return; } //动画开关条件 if (rectStatus == 0 && ori>0 || rectStatus == 1 && ori<125) { if (!animate) { animate = true; cv.notify_all(); } } else { animate = false; } } void DrawBlock(int drawType=0) { if(drawType==1) { BitBlt(hMainDc, 100, 100, 200, 200, hSrcDc, 0, 0, SRCCOPY); return; } else if(drawType==2) { BitBlt(hMainDc, 100, 100, 200, 200, hSrcDc, 0, 0, SRCCOPY); //将红色与block混合,使其表现为重叠状态 HDC hBitmapDc = CreateCompatibleDC(hMainDc); HBITMAP hBitmap = CreateCompatibleBitmap(hMainDc, 100, 100); SelectObject(hBitmapDc, hBitmap); RECT bitmapRect{ 0,0,100,100 }; HBRUSH b = CreateSolidBrush(RGB(255, 0, 0)); FillRect(hBitmapDc, &bitmapRect, b); BLENDFUNCTION bf; bf.BlendOp = AC_SRC_OVER; bf.BlendFlags = 0; bf.AlphaFormat = 0; bf.SourceConstantAlpha = 128; AlphaBlend(hMainDc, 100, 100, 100, 100, hBitmapDc, 0, 0, 100, 100, bf); DeleteObject(hBitmap); DeleteDC(hBitmapDc); DeleteObject(b); return; } else if(drawType==0) { if (rectStatus == 0 && ori <= 0 || rectStatus == 1 && ori>125) { return; } } //拷贝原型到 hMirrorDc HDC hMirrorDc = CreateCompatibleDC(hMainDc); HBITMAP hMirror = CreateCompatibleBitmap(hMainDc, 100, 100); SelectObject(hMirrorDc, hMirror); BitBlt(hMirrorDc, 0, 0, 100, 100, hSrcDc, 0, 0, SRCCOPY); //将镜像与纯黑混合,透明度动态变化,使其为动画效果 HDC hBitmapDc = CreateCompatibleDC(hMainDc); HBITMAP hBitmap = CreateCompatibleBitmap(hMainDc, 100, 100); SelectObject(hBitmapDc, hBitmap); SetBkColor(hBitmapDc, RGB(255, 255, 255)); BLENDFUNCTION bf; bf.BlendOp = AC_SRC_OVER; bf.BlendFlags = 0; bf.AlphaFormat = 0; if (rectStatus == 0) { bf.SourceConstantAlpha = ori--; } else { bf.SourceConstantAlpha = ori++; } AlphaBlend(hMirrorDc, 0, 0, 100, 100, hBitmapDc, 0, 0, 100, 100, bf); BitBlt(hMainDc, 100, 100, 200, 200, hMirrorDc, 0, 0, SRCCOPY); ControlAnimate(); DeleteObject(hBitmap); DeleteDC(hBitmapDc); DeleteObject(hMirror); DeleteDC(hMirrorDc); } void DrawRect(int drawType=0) { FillRect(hMainDc, &mRect, bkBrush);//如果背景是纯色,那直接brush就行,如果是图片,那么就需要将图片对应的区域拷贝回来 if (drawType == 0) { POINT point; GetCursorPos(&point); ScreenToClient(mWnd, &point); LONG difX = point.x - downPoint.x; LONG difY = point.y - downPoint.y; mRect.left = mOriRect.left + difX; mRect.right = mOriRect.right + difX; mRect.top = mOriRect.top + difY; mRect.bottom = mOriRect.bottom + difY; double x = mRect.left + 50 - 150; double y = mRect.top + 50 - 150; if (x*x + y*y<100 * 100 * 2)//碰撞检测这个地方我写的比较水,因为这地方可以根据实际情况来写的,我也就没必要写那么复杂了 { ori = 0; rectStatus = 0; DrawBlock(2);//重叠先画下面的,再画上面的 areaStatus = 1; } else if (areaStatus == 1)//离开后还需要一次额外绘制,确保block的完整性 { areaStatus = 2; } } else if(drawType==1) { mRect.left = 100; mRect.right = 200; mRect.top = 100; mRect.bottom = 200; } Rectangle(hMainDc, mRect.left, mRect.top, mRect.right, mRect.bottom); } LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) { POINT point; switch (message) { case WM_COMMAND: { int wmId = LOWORD(wParam); // 分析菜单选择: switch (wmId) { default: return DefWindowProc(hWnd, message, wParam, lParam); } } break; case WM_PAINT: { PAINTSTRUCT ps; HDC hdc = BeginPaint(hWnd, &ps); // TODO: 在此处添加使用 hdc 的任何绘图代码... if(!init) { //创建缓冲DC GetClientRect(hWnd, &clientRect); hMainDc = CreateCompatibleDC(hdc); hMain = CreateCompatibleBitmap(hdc, clientRect.right - clientRect.left, clientRect.bottom - clientRect.top); SelectObject(hMainDc, hMain); bkBrush = CreateSolidBrush(RGB(255, 255, 0)); FillRect(hMainDc, &clientRect, bkBrush); //绘制一个矩形,并且与对应位置背景做半透混合,使其作为原型存储在内存dc中 hSrcDc = CreateCompatibleDC(hdc); hSrc = CreateCompatibleBitmap(hdc, 100, 100); SelectObject(hSrcDc, hSrc); Rectangle(hSrcDc, 0, 0, 100, 100); BLENDFUNCTION bf; bf.BlendOp = AC_SRC_OVER; bf.BlendFlags = 0; bf.AlphaFormat = 0; bf.SourceConstantAlpha = 128; AlphaBlend(hSrcDc, 0, 0, 100, 100, hMainDc, 100, 100, 200, 200, bf); //将原型绘制到缓冲DC中 BitBlt(hMainDc, 100, 100, 200, 200, hSrcDc, 0, 0, SRCCOPY); //物体 mRect = { 400,100,500,200 }; Rectangle(hMainDc, 400, 100, 500, 200); init = true; } if(areaStatus ==2)//若为分离态,物体离开block后确保block的完整性,额外一次绘制block { DrawBlock(1); areaStatus++; } else//否则检测透明度,看是否需要重绘 { DrawBlock(); } if (drawRect) { if (!isDown) { if (areaStatus == 1) { DrawRect(1); } drawRect = false; animate = false; } else { DrawRect(); } } BitBlt(hdc, 0, 0, clientRect.right, clientRect.bottom, hMainDc, 0, 0, SRCCOPY); EndPaint(hWnd, &ps); } break; case WM_LBUTTONDOWN: areaStatus = 0;//重置状态 isDown = true; mOriRect = mRect; GetCursorPos(&downPoint);//记录按下点,推断移动位置 ScreenToClient(hWnd, &downPoint); break; case WM_LBUTTONUP: isDown = false; break; case WM_MOUSEMOVE: //大致思路就是,只有渐变和移动需要animate,那么我就在这两种状态下激活,剩下关闭的逻辑控制交给paint if (!isDown&&areaStatus != 1) {//如果没有按下鼠标和没有覆盖才去检测渐变区域 GetCursorPos(&point); ScreenToClient(hWnd, &point); if (point.x > 100 && point.x < 200 && point.y>100 && point.y < 200) { if (rectStatus == 0) { rectStatus = 1; ControlAnimate(); } } else { if (rectStatus == 1) { rectStatus = 0; ControlAnimate(); } } } if(isDown&&drawRect==false) { if(downPoint.x>mOriRect.left&&downPoint.x<mOriRect.right&&downPoint.y>mOriRect.top&&downPoint.y<mOriRect.bottom) { drawRect = true; ControlAnimate(); } } break; case WM_DESTROY: PostQuitMessage(0); break; case WM_INVALIDATE: RECT r; if(drawRect) { //这地方计算区域其实是有问题的,由于Invalidate与WM_PAINT之间存在延时,当Invalidate触发WM_PAINT将之放入消息队列时,可能此时队列中已经有了多个WM_PAINT消息却还未处理,但我内存DC中却在不断保持最新,那么区域自然就不一样了 // vRect.left = (vRect.left < mRect.left ? vRect.left : mRect.left)-10; // vRect.right = (vRect.right < mRect.right ? mRect.right : vRect.right)+10; // vRect.top = (vRect.top < mRect.top ? vRect.top : mRect.top)-10; // vRect.bottom = (vRect.bottom < mRect.bottom ? mRect.bottom : vRect.bottom)+10; InvalidateRect(hWnd, 0, false); } else { r.left = 100; r.right = 200; r.top = 100; r.bottom = 200; InvalidateRect(hWnd, &r, false); } break; default: return DefWindowProc(hWnd, message, wParam, lParam); } return 0; }