用Windows API使用Bresenham算法通过画直线的方式实现圆填充算法
作者 | 将狼才鲸 |
---|---|
创建日期 | 2023-01-31 |
- 备注:在Windows电脑上,使用VS软件,使用C语言风格,使用Windows API函数接口(以前叫Win32 API)实现画圆和圆的填充。
- 显示效果:
- 源码展示(工程文件可以在上方Gitee中下载,点开即用):
/******************************************************************************
* \brief 画一个圆,燃用用Bresenham算法对圆进行填充
* \details 在Windows下使用VS编译,直接使用Windows API(C/C++),不使用MFC、WPF等框架;
* 画圆使用Windows API,填充用画线的API自行实现
* \note UTF-8 BOM编码
* \author 将狼才鲸
* \date 2023-01-31
* \remarks
* 参考网址:
* [基于 Bresenham 算法画填充圆](http://www.javashuo.com/article/p-oiizeyjd-de.html)
******************************************************************************/
/*********************************** 头文件 ***********************************/
#include <windows.h> /* Windows API的头文件 */
#include <stdio.h> /* printf */
/*********************************** 宏定义 ***********************************/
#define FPS 60 /* 帧率,Frames per Second */
#define DEFAULT_RADIUS_RATIO 4 /* 默认的气泡半径是窗口宽度的多少分之1 */
/********************************** 类型定义 **********************************/
/********************************** 全局变量 **********************************/
static WCHAR titleName[] = L"Windows API Demo"; /* 窗口标题文字 */
static int g_window_x, g_window_y; /* 窗口宽度和高度,单位为像素 */
static int g_x = 100, g_y = 100; /* 圆心坐标,单位为像素 */
static int g_fno; /* 帧序号,对每一帧显示进行计数 */
static int default_radius = 10; /* 当前圆的默认半径,单位为像素 */
static HANDLE hThread; /* 创建的绘图线程 */
static HPEN hPenWhite; /* 白色画笔 */
static HPEN hPenRed; /* 红色画笔 */
static HPEN hPenGreen; /* 绿色画笔 */
static int ms_per_frame = 1000 / FPS; /* 每帧所占据的时间,单位为ms */
/********************************** 私有函数 **********************************/
/**
* \brief 基于 Bresenham 算法画填充圆
* \param x,y = 圆心坐标;r = 半径
*/
void FillCircle_Bresenham(HDC hdc, int x, int y, int r)
{
int tx = 0, ty = r, d = 3 - 2 * r;
while (tx < ty)
{
// 小于 45 度横线
MoveToEx(hdc, x - ty, y - tx, NULL); LineTo(hdc, x + ty, y - tx);
if (tx != 0) // 防止水平线重复绘制
{
MoveToEx(hdc, x - ty, y + tx, NULL); LineTo(hdc, x + ty, y + tx);
}
if (d < 0) // 取上面的点
{
d += 4 * tx + 6;
}
else // 取下面的点
{
// 大于 45 度横线
MoveToEx(hdc, x - tx, y - ty, NULL); LineTo(hdc, x + tx, y - ty);
MoveToEx(hdc, x - tx, y + ty, NULL); LineTo(hdc, x + tx, y + ty);
d += 4 * (tx - ty) + 10;
ty--;
}
tx++;
}
if(tx == ty) // 45 度横线
{
MoveToEx(hdc, x - ty, y - tx, NULL); LineTo(hdc, x + ty, y - tx);
MoveToEx(hdc, x - ty, y + tx, NULL); LineTo(hdc, x + ty, y + tx);
}
}
/**
* \brief 在窗口中刷新一帧内容
*/
static int window_update(HDC hdc)
{
static WCHAR text_info[64]; /* 屏幕上显示文字时使用 */
BOOL ret; /* 用于拷机时画面无显示时调试用 */
/* 1. 帧数文字信息显示 */
wsprintf((LPWSTR)text_info, (LPCWSTR)L"当前帧:%d", g_fno++);
ret = TextOut(hdc, 10, 10, (LPCWSTR)text_info, wcslen(text_info));
if (ret != 1)
printf("error");
/* 2. 画圆 */
HPEN hOldPen1 = (HPEN)::SelectObject(hdc, hPenRed);
/* Windows API画圆和椭圆函数,参数为圆占据的矩形左上角和右下角坐标;横轴向右是增加,纵轴向上是减少 */
Ellipse(hdc, g_x - default_radius, g_y - default_radius,
g_x + default_radius, g_y + default_radius);
/* 3. 填充圆:用一条条直线进行填充 */
HPEN hOldPen2 = (HPEN)::SelectObject(hdc, hPenGreen);
MoveToEx(hdc, 1, 1, NULL); LineTo(hdc, 100, 100); /* 画一条直线,测试用 */
FillCircle_Bresenham(hdc, g_x, g_y, default_radius);
/* 4. 显示BMP图片:LoadImage() */
/* 5. 拷贝图片(贴图):BitBlt() */
return 0;
}
/**
* \brief 执行窗口内容更新的线程
*/
DWORD WINAPI ThreadProcessFunc(LPVOID lpParamter)
{
HWND hWnd = (HWND)lpParamter;
HDC hdc; /* 窗口信息 */
/* 窗口合法性判断 */
hdc = GetDC(hWnd); /* 选中当前绘图区域,同样也是当前窗口 */
if (IsIconic(hWnd))
return 0;
/* 画笔设置 */
hPenWhite = (HPEN)::CreatePen(PS_SOLID, 2, RGB(255, 255, 255)); /* 白色线 */
hPenRed = (HPEN)::CreatePen(PS_SOLID, 2, RGB(176, 48, 96)); /* 红色线 */
hPenGreen = (HPEN)::CreatePen(PS_SOLID, 2, RGB(34, 139, 34)); /* 绿色线 */
/* 死循环,持续运行 */
while (TRUE)
{
window_update(hdc);
//GdiFlush(); /* 及时将绘图区绘制的内容写入到窗口显存中去 */
Sleep(ms_per_frame); //单位是毫秒
}
//TODO: 此处未释放hdc窗口绘图区域,自己临时绘图时,记得GetDC和ReleaseDC成对使用
return 0;
}
/**
* \brief 窗口消息处理回调函数
* \details 如响应窗口的大小变化
*/
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
RECT rect; /* 窗口大小 */
/* 1. 处理想要处理的窗口消息 */
switch (message)
{
case WM_CREATE: /* 窗口被创建时的消息 */
return 0;
case WM_SIZE: /* 适配窗口大小的改变 */
{
/* 程序刚运行,窗口刚打开的时候,会自动进入一次 */
GetClientRect(hWnd, &rect); /* 获取窗口的大小 */
g_window_x = rect.right - rect.left;
g_window_y = rect.bottom - rect.top;
default_radius = g_window_y / DEFAULT_RADIUS_RATIO;
g_x = default_radius * 2;
g_y = default_radius * 2;
}
return 0;
case WM_TIMER: /* 定时器消息(中断)处理 */
{
}
return 0;
case WM_PAINT: /* 最开始绘制窗口中默认显示的内容 */
break;
case WM_CLOSE: /* 程序退出,先close再destroy */
{
TerminateThread(hThread, 0); /* 强制退出绘图线程 */
DestroyWindow(hWnd);
}
return 0;
case WM_DESTROY: /* 程序退出 */
PostQuitMessage(0); /* 该函数向消息队列中插入一条WM_QUIT消息,由GetMessage函数捕获返回0而退出程序 */
break;
}
/* 为应用程序没有处理的任何窗口消息提供缺省的处理,该函数确保每一个消息都得到处理 */
return DefWindowProc(hWnd, message, wParam, lParam);
}
/********************************** 接口函数 **********************************/
/**
* \brief Windows图形界面程序固定的入口函数
* \details 如果是命令行函数,则入口函数不一样
* \remarks
* 1、注册窗口类 (RegisterClassEx)
* 2、创建窗口 (CreateWindowsEx)
* 3、在桌面显示窗口 (ShowWindows)
* 4、更新窗口客户区 (UpdataWindows)
* 5、进入无限循环的消息获取和处理的循环:
* GetMessage,获取消息
* TranslateMessage,转换键盘消息
* DispatchMessage,将消息发送到相应的窗口函数
*/
int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
/* 1. 设置窗口属性和注册窗口 */
WNDCLASSEX wcex; /* 定义窗口属性结构体 */
wcex.cbSize = sizeof(WNDCLASSEX);
wcex.style = CS_HREDRAW | CS_VREDRAW; /* 允许窗口缩放 */
wcex.lpfnWndProc = (WNDPROC)WndProc; /* 窗口消息处理回调函数 */
wcex.cbClsExtra = 0;
wcex.cbWndExtra = 0;
wcex.hInstance = hInstance;
wcex.hIcon = NULL; /* 窗口左上角图标的句柄 */
wcex.hCursor = LoadCursor(NULL, IDC_ARROW);
wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
wcex.lpszMenuName = NULL;
wcex.lpszClassName = (LPCWSTR)titleName; /* 类名称 */
wcex.hIconSm = NULL; /* 小图标句柄 */
RegisterClassEx(&wcex);
/* 2. 创建窗口 */
HWND hWnd;
#ifdef COLLISION_ALGORITHM_TEST
hWnd = CreateWindow((LPCWSTR)titleName, (LPCWSTR)titleName, WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, 0, 400, 400, NULL, NULL, hInstance, NULL); /* 窗口指定宽高 */
#else
/* 当前我的电脑上默认初始窗口大小 1424 * 720,测试碰撞时基于此设置初始位置和速度 */
hWnd = CreateWindow((LPCWSTR)titleName, (LPCWSTR)titleName, WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, NULL, NULL, hInstance, NULL); /* 窗口使用默认宽高 */
#endif
if (!hWnd)
return FALSE; /* 如果创建窗口失败则返回 */
/* 3. 显示窗口和刷新窗口 */
ShowWindow(hWnd, nCmdShow);
UpdateWindow(hWnd);
/* 4. 添加线程,通过hWnd持续绘制显示区域 */
/* CreateThread()函数参数介绍:
* LPSECURITY_ATTRIBUTESlpThreadAttributes, 表示线程内核对象的安全属性,一般传入NULL表示使用默认设置
* DWORDdwStackSize, 表示线程栈空间大小,传入0表示使用默认大小(1MB)
* LPTHREAD_START_ROUTINElpStartAddress, 表示新线程所执行的线程函数地址,多个线程可以使用同一个函数地址
* LPVOIDlpParameter, 是传给线程函数的参数
* DWORDdwCreationFlags,指定额外的标志来控制线程的创建,为0表示线程创建之后立即就可以进行调度,如果为CREATE_SUSPENDED则表示线程创建后暂停运行,这样它就无法调度,直到调用ResumeThread()
* LPDWORDlpThreadId 将返回线程的ID号,传入NULL表示不需要返回该线程ID号 */
hThread = CreateThread(NULL, 0, ThreadProcessFunc, hWnd, 0, NULL);
/* 5. 循环获取和处理窗口上的消息 */
MSG msg;
while (GetMessage(&msg, 0, 0, 0))
{
TranslateMessage(&msg); /* 转换键盘等产生的消息 */
DispatchMessage(&msg); /* 将消息发送到窗口函数 */
}
return (int)msg.wParam;
}
/*********************************** 文件尾 ***********************************/