#define UNICODE
#define _UNICODE
#include <windows.h>
#include <math.h>
#include <wchar.h> // 宽字符支持
#define WIN_WIDTH 600
#define WIN_HEIGHT 500
#define BALL_RADIUS 10
#define GRAVITY 0.0008
#define REBOUND_FACTOR 0.5
#define GROUND_Y (WIN_HEIGHT - 150) // 地面高度
// 控件 ID
#define IDC_HEIGHT_INPUT 201 // 高度输入框
#define IDC_BOUNCES_INPUT 202 // 次数输入框
#define IDC_SUBMIT_BUTTON 203 // 提交按钮
#define TIMER_ID 101 // 定时器 ID
// 小球结构体
typedef struct {
double x, y; // 小球位置 (像素)
double vy; // 垂直速度
double maxHeight; // 当前反弹的最高高度 (像素)
int bounces; // 当前反弹次数
int maxBounces; // 最大反弹次数
BOOL isActive; // 小球是否在运动状态
BOOL isFalling; // 小球是否处于下落状态、
double holdTime; // 停留时间(毫秒)
} Ball;
Ball ball; // 小球全局变量
double totalDistance = 0.0; // 小球累计总运动距离
double reboundHeights[100]; // 每次反弹高度记录
HWND heightInput; // 高度输入框句柄
HWND bouncesInput; // 次数输入框句柄
HWND submitButton; // 提交按钮句柄
HWND mainHwnd; // 主窗口句柄
HWND outputHwnd = NULL; // 结果窗口句柄
int scrollOffset = 0; // 当前滚动条的偏移量
int lineHeight = 20; // 每行内容的高度
int totalOutputHeight = 0; // 全部内容高度
SCROLLINFO scrollInfo; // 滚动条的状态
void InitializeBall(double initialHeightMeters, int maxBounces) {
if (initialHeightMeters <= 0 || maxBounces <= 0) {
return; // 防止异常参数导致错误初始化
}
double initialHeightPixels = initialHeightMeters * 2.0; // 假设 1 米 = 2 像素
ball.x = WIN_WIDTH / 2.0; // 小球水平居中
ball.y = GROUND_Y - initialHeightPixels; // 小球从用户设置的高度开始下落
ball.vy = 0; // 初始速度为 0
ball.maxHeight = initialHeightPixels; // 最大位置为初始输入高度
ball.bounces = 0; // 当前反弹次数设为 0
ball.maxBounces = maxBounces; // 设置最大反弹次数
ball.isActive = TRUE; // 激活运动
ball.isFalling = TRUE; // 初始状态设为下落(不运动)
ball.holdTime = 2000.0; // 在初始高度停留 2 秒
totalDistance = 0.0; // 总路程初始化为 0
memset(reboundHeights, 0, sizeof(reboundHeights)); // 清空反弹高度记录
}
// 小球的物理逻辑更新
void UpdateBall(DWORD deltaTime) {
if (!ball.isActive) return; // 如果小球处于非活动状态,直接返回
// 如果当前还有 "停留时间",则不更新下落状态
if (ball.holdTime > 0) {
ball.holdTime -= deltaTime; // 减小停留时间
if (ball.holdTime <= 0) {
ball.holdTime = 0; // 停留时间结束,开始正常运动
}
return; // 返回,不执行后续更新运动逻辑
}
if (ball.isFalling) { // 小球下落状态
ball.vy += GRAVITY * deltaTime; // 更新速度(v = v0 + gt)
ball.y += ball.vy * deltaTime; // 更新位置(y = y0 + vt)
if (ball.y >= GROUND_Y - BALL_RADIUS) { // 碰到底部地面
ball.y = GROUND_Y - BALL_RADIUS; // 确保小球停在地面
ball.isFalling = FALSE; // 切换为上升状态
totalDistance += ball.maxHeight; // 累加下落距离
ball.bounces++;
if (ball.bounces <= ball.maxBounces) { // 如果反弹次数未达到上限
reboundHeights[ball.bounces - 1] = ball.maxHeight * REBOUND_FACTOR; // 记录反弹高度
totalDistance += reboundHeights[ball.bounces - 1]; // 累加反弹的上升距离
ball.maxHeight *= REBOUND_FACTOR; // 新的反弹高度是之前高度的一半
ball.vy = -sqrt(2 * GRAVITY * ball.maxHeight); // 更新为反向(向上)的初速度
}
if (ball.bounces >= ball.maxBounces) { // 达到最大反弹次数,停止运动
ball.isActive = FALSE;
}
}
} else { // 小球处于上升状态
ball.y += ball.vy * deltaTime; // 更新上升时的位置
ball.vy += GRAVITY * deltaTime; // 受到重力减速(逐渐变慢)
if (ball.vy >= 0) { // 当速度大于等于零时(达到了最高点)
ball.isFalling = TRUE; // 切换为下落状态
}
}
}
// 绘制小球
void RenderBall(HDC hdc) {
HBRUSH hBrush = CreateSolidBrush(RGB(139, 69, 19)); // 棕色小球
HBRUSH oldBrush = SelectObject(hdc, hBrush);
Ellipse(hdc,
(int)(ball.x - BALL_RADIUS), (int)(ball.y - BALL_RADIUS),
(int)(ball.x + BALL_RADIUS), (int)(ball.y + BALL_RADIUS));
SelectObject(hdc, oldBrush);
DeleteObject(hBrush);
}
// 绘制地面
void RenderGround(HDC hdc) {
HPEN hPen = CreatePen(PS_SOLID, 2, RGB(0, 128, 0)); // 绿色
HPEN oldPen = SelectObject(hdc, hPen);
MoveToEx(hdc, 0, GROUND_Y, NULL);
LineTo(hdc, WIN_WIDTH, GROUND_Y);
SelectObject(hdc, oldPen);
DeleteObject(hPen);
}
// 渲染整体场景
void RenderScene(HDC hdc) {
RenderGround(hdc); // 绘制地面
RenderBall(hdc); // 绘制小球
}
LRESULT CALLBACK OutputWindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
switch (uMsg) {
case WM_SIZE: { // 在窗口大小调整时,设置滚动条范围
RECT clientRect;
GetClientRect(hwnd, &clientRect);
// 设置滚动条属性
totalOutputHeight = ball.bounces * lineHeight + 100; // 高度=反弹数量+顶部信息
int windowHeight = clientRect.bottom;
scrollInfo.cbSize = sizeof(SCROLLINFO);
scrollInfo.fMask = SIF_RANGE | SIF_PAGE;
scrollInfo.nMin = 0; // 滚动条最小位置 0
scrollInfo.nMax = totalOutputHeight; // 滚动条最大位置=总高度
scrollInfo.nPage = windowHeight; // 可视区域高度=窗口高度
SetScrollInfo(hwnd, SB_VERT, &scrollInfo, TRUE);
break;
}
case WM_PAINT: {
PAINTSTRUCT ps;
HDC hdc = BeginPaint(hwnd, &ps);
wchar_t buffer[256];
int offset = -scrollOffset; // 根据滚动偏移调整绘制位置
HFONT hFont = CreateFontW(18, 0, 0, 0, FW_NORMAL, FALSE, FALSE, FALSE,
DEFAULT_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS,
DEFAULT_QUALITY, DEFAULT_PITCH | FF_SWISS, L"SimSun"); // 使用宋体
HFONT oldFont = SelectObject(hdc, hFont);
// 显示运动结束提示
swprintf(buffer, 256, L"小球运动结束");
TextOutW(hdc, 10, offset + 10, buffer, wcslen(buffer));
offset += lineHeight;
// 显示总路程
swprintf(buffer, 256, L"小球总路程:%.2f 米", totalDistance / 2.0);
TextOutW(hdc, 10, offset + 10, buffer, wcslen(buffer));
offset += lineHeight;
// 显示每次反弹高度
for (int i = 0; i < ball.bounces; i++) {
swprintf(buffer, 256, L"第 %d 次反弹高度:%.2f 米", i + 1, reboundHeights[i] / 2.0);
TextOutW(hdc, 10, offset + 10, buffer, wcslen(buffer));
offset += lineHeight;
}
SelectObject(hdc, oldFont);
DeleteObject(hFont);
EndPaint(hwnd, &ps);
break;
}
case WM_VSCROLL: { // 滚动条消息
int scrollDelta = 0;
switch (LOWORD(wParam)) {
case SB_LINEDOWN: // 用户点击向下箭头
scrollDelta = lineHeight;
break;
case SB_LINEUP: // 用户点击向上箭头
scrollDelta = -lineHeight;
break;
case SB_THUMBPOSITION: // 用户点击滚动条滑块
case SB_THUMBTRACK: // 用户拖动滚动条滑块
scrollDelta = HIWORD(wParam) - scrollOffset;
break;
}
// 更新滚动偏移量
scrollOffset += scrollDelta;
if (scrollOffset < 0) scrollOffset = 0; // 限制最小偏移
if (scrollOffset > totalOutputHeight - scrollInfo.nPage) // 限制最大偏移
scrollOffset = totalOutputHeight - scrollInfo.nPage;
// 更新滚动条位置
scrollInfo.fMask = SIF_POS;
scrollInfo.nPos = scrollOffset;
SetScrollInfo(hwnd, SB_VERT, &scrollInfo, TRUE);
// 触发窗口重绘
InvalidateRect(hwnd, NULL, TRUE);
break;
}
case WM_DESTROY:
outputHwnd = NULL;
return 0;
default:
return DefWindowProc(hwnd, uMsg, wParam, lParam);
}
}
void ShowOutputWindow(HINSTANCE hInstance) {
if (outputHwnd != NULL) { // 如果结果窗口已经存在
InvalidateRect(outputHwnd, NULL, TRUE); // 强制刷新窗口内容
return;
}
// 定义结果窗口类名
const wchar_t OUTPUT_CLASS_NAME[] = L"OutputWindow";
WNDCLASSW wc = {0};
wc.lpfnWndProc = OutputWindowProc; // 设置结果窗口过程
wc.hInstance = hInstance;
wc.lpszClassName = OUTPUT_CLASS_NAME;
wc.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
// 注册窗口类
RegisterClassW(&wc);
// 创建新结果窗口
outputHwnd = CreateWindowW(
OUTPUT_CLASS_NAME, L"运动结果",
WS_OVERLAPPEDWINDOW & ~WS_THICKFRAME & ~WS_MAXIMIZEBOX,
CW_USEDEFAULT, CW_USEDEFAULT, 400, 300,
NULL, NULL, hInstance, NULL
);
ShowWindow(outputHwnd, SW_SHOW); // 显示结果窗口
}
// 主窗口过程函数
LRESULT CALLBACK MainWindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
static DWORD lastTime = 0;
switch (uMsg) {
case WM_CREATE:
// 添加输入控件
CreateWindow(L"STATIC", L"初始高度 (米):", WS_VISIBLE | WS_CHILD,
20, WIN_HEIGHT - 140, 110, 30, hwnd, NULL, NULL, NULL);
heightInput = CreateWindow(L"EDIT", L"", WS_VISIBLE | WS_CHILD | WS_BORDER | ES_NUMBER,
130, WIN_HEIGHT - 140, 100, 30, hwnd, (HMENU)IDC_HEIGHT_INPUT, NULL, NULL);
CreateWindow(L"STATIC", L"落地次数:", WS_VISIBLE | WS_CHILD,
20, WIN_HEIGHT - 100, 110, 30, hwnd, NULL, NULL, NULL);
bouncesInput = CreateWindow(L"EDIT", L"", WS_VISIBLE | WS_CHILD | WS_BORDER | ES_NUMBER,
130, WIN_HEIGHT - 100, 100, 30, hwnd, (HMENU)IDC_BOUNCES_INPUT, NULL, NULL);
submitButton = CreateWindow(L"BUTTON", L"提交", WS_VISIBLE | WS_CHILD,
250, WIN_HEIGHT - 120, 100, 40, hwnd, (HMENU)IDC_SUBMIT_BUTTON, NULL, NULL);
SetTimer(hwnd, TIMER_ID, 10, NULL); // 设置定时器,每 10ms 刷新
return 0;
case WM_COMMAND:
if (LOWORD(wParam) == IDC_SUBMIT_BUTTON) { // 提交按钮处理
wchar_t heightBuffer[256], bouncesBuffer[256];
GetWindowText(heightInput, heightBuffer, 256); // 获取输入高度
GetWindowText(bouncesInput, bouncesBuffer, 256); // 获取反弹次数
// 转换用户输入
double height = _wtof(heightBuffer); // 输入高度转换为浮点数
int bounces = _wtoi(bouncesBuffer); // 输入反弹次数转换为整数
// 验证输入合法性
if (height <= 0 || height > 150) { // 限制高度范围为 0 ~ 150
MessageBox(hwnd,
L"高度超出范围!请输入 0 到 150 之间的数值。",
L"输入无效", MB_ICONERROR);
return 0;
}
if (bounces < 1) { // 限制反弹次数为大于等于 1 的正整数
MessageBox(hwnd,
L"落地次数无效!请输入大于等于 1 的正整数。",
L"输入无效", MB_ICONERROR);
return 0;
}
// 如果小球当前正在运动,则重置运动
if (ball.isActive) {
KillTimer(hwnd, TIMER_ID); // 停止原来的定时器,避免重复更新
}
// 初始化小球参数,重新开始模拟
InitializeBall(height, bounces); // 初始化小球参数
SetTimer(hwnd, TIMER_ID, 10, NULL); // 启动定时器,进行物理更新
InvalidateRect(hwnd, NULL, TRUE); // 强制刷新主窗口
}
break;
case WM_TIMER:
if (wParam == TIMER_ID) { // 定时器触发响应
DWORD currentTime = GetTickCount(); // 当前时间戳
DWORD deltaTime = currentTime - lastTime; // 时间差计算
lastTime = currentTime;
if (ball.isActive) { // 如果小球处于活动状态
UpdateBall(deltaTime); // 更新物理状态
RECT groundAbove = { 0, 0, WIN_WIDTH, GROUND_Y };
InvalidateRect(hwnd, &groundAbove, TRUE); // 刷新窗口地面以上部分
} else {
KillTimer(hwnd, TIMER_ID); // 停止定时器
ShowOutputWindow((HINSTANCE)GetWindowLongPtr(hwnd, GWLP_HINSTANCE)); // 显示结果窗口
}
}
break;
case WM_PAINT: {
PAINTSTRUCT ps;
HDC hdc = BeginPaint(hwnd, &ps);
RenderScene(hdc); // 渲染当前场景(包括小球和地面)
EndPaint(hwnd, &ps);
break;
}
case WM_DESTROY:
KillTimer(hwnd, TIMER_ID);
PostQuitMessage(0);
return 0;
default:
return DefWindowProc(hwnd, uMsg, wParam, lParam);
}
return 0;
}
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) {
const wchar_t CLASS_NAME[] = L"BouncingBallWindow";
WNDCLASS wc = {0};
wc.lpfnWndProc = MainWindowProc;
wc.hInstance = hInstance;
wc.lpszClassName = CLASS_NAME;
wc.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
if (!RegisterClass(&wc)) return 0;
mainHwnd = CreateWindow(
CLASS_NAME, L"小球反弹模拟", WS_OVERLAPPEDWINDOW | WS_VISIBLE,
CW_USEDEFAULT, CW_USEDEFAULT, WIN_WIDTH, WIN_HEIGHT,
NULL, NULL, hInstance, NULL);
MSG msg;
while (GetMessage(&msg, NULL, 0, 0)) {
TranslateMessage(&msg);
DispatchMessage(&msg);
}
return 0;
}改一下代码规范
最新发布