效果就是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;
}