C++中的动态图形与音频同步:实现罗盘时钟与音乐播放器

目录

前言

一、项目概述

二、图形界面和时间圈的实现

2.1图形库的选择与图形系统初始化

2.2时间圈的动态更新

2.3多线程音乐播放

三、主函数实现

四、剩余代码解释


前言

时代滚滚向前,技术日新月异。作为身处信息时代的一份子,就需要不断加强学习,展现对技术的渴望,从基础学习逐步建立起属于自己的“知识大厦”,这样才能不被时代淘汰。

C++是一门强大的编程语言,以我的浅见看来,其高效和灵活的特点,能够更好的帮助开发人员的任务,满足开发需求,成为我们首选编程语言之一。因此,本文将探讨如何利用C++实现罗盘时钟,并且在时钟的滚动时循环播放设定好的音乐。

作为一名初学者,垦请各位大佬指出文中错误和提出建议,我将不胜感激!

一、项目概述

我的项目旨在创建一个时钟界面,既显示时间,又能够显示日期,在时钟显示的过程中,循环播放音乐,而且在这个界面中添加了背景图片,让结果呈现不至于枯燥乏味。

二、图形界面和时间圈的实现

2.1图形库的选择与图形系统初始化

我选择的是easyx.h图形库,然后利用initgraph函数初始化,设置屏幕高度和宽度。我设置的屏幕宽度为1080像素,高度为800像素,可以进行更改,你应当根据绘制时间圈的相关函数确定,否则呈现的效果不是很好。

#include<easyx.h>       //用于Windows平台的简易图形库,提供更高级的图形操作功能
//使用宏定义设置一个屏幕,宽度为1080像素,高度为800像素
#define Width 1080
#define Height 800
initgraph(Width, Height);               //初始化图像系统,根据定义的宽度和高度进行设置

2.2时间圈的动态更新

设置的时间圈有年、月、日、星期、时、分、秒七个,为了实现时间的平滑过渡,设计了一个TimeCircle结构体,包含了动画控制的参数。

//更新时间圈的函数,使用基于时间差的平滑过渡
void UpdateTimeCircle(TimeCircle& circle, int currentTime, double totalSegments)
{
	//检查当前时间是否到达了预定的更新时间
	if (circle.NextTime == currentTime)
	{
		//计算时间差:当前时间与上一次更新的时间之差,通过totalSegments标准化
		//确保了时间差在0到1之间,便于后续计算角度增量
		double timeDelta = static_cast<double>(currentTime - circle.LastUpdateTime) / totalSegments;

		//更新LastUpdateTime,记录当前时间点,以便于下次计算时间差
		circle.LastUpdateTime = currentTime;

		//根据时间差计算角度增量:将时间差映射到角度变化,实现平滑过渡
		double angleIncrement = (2 * PI) * timeDelta;

		//更新时间圈的角度(Radian):累积角度增量
		circle.Radian += angleIncrement;

		//角度归一化:使用fmod函数确保角度在0到2π之间,防止超出一个圆周
		circle.Radian = fmod(circle.Radian, 2 * PI);
	}
	else
	{
		//当当前时间与NextTime不匹配时,重置时间圈的状态
		//这通常发生在时间跳变或初始化时
		circle.NextTime = currentTime;

		//将角度设置为0,准备从新时间点开始新的周期
		circle.Radian = 0;

		//更新LastUpdateTime到当前时间点,开始新的时间差计算周期
		circle.LastUpdateTime = currentTime;
	}
}

时间的绘制,采用了DrawCircle函数负责在指定坐标和参数下生成一个带有文本的圆,通过计算角度和调整文本位置确保正确显示时间。

void DrawCircle(TCHAR str[25], int variable, int fors, int R, double Radian, int maxRadius, int centerX, int centerY)
{
	//计算文本颜色
	//如果variable不为0,则根据当前时间变量计算HSL颜色,否则使用白色
	settextcolor(variable ? HSLtoRGB((360.f / fors) * variable, 1, 0.5f) : WHITE);
	//从左往右,每个变量是字体的设置分别为:大小、风格、名称、旋转角度、倾斜角度、宽度比例、是否使用粗体、是否使用斜体、是否使用下划线、字符集、输出精度、裁剪精度、抗锯齿质量、字符间距
	settextstyle(22, 0, L"微软雅黑", variable * 3600 / fors, variable * 3600 / fors, 0, false, false, false, DEFAULT_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, ANTIALIASED_QUALITY, DEFAULT_PITCH);

	//计算角度,对于秒和分时间圈,使用更精确的更新
	double a = (fors == 60) ? ((variable + 0) * PI * 2 / fors - Radian) : (variable * PI * 2 / fors);
	//获取文本宽度和高度
	int w = textwidth(str);
	int h = textheight(str);

	//根据圆心、半径、角度,以及文本尺寸调整位置,确保文本居中
	int x = centerX + (R * cos(a)) - w / 2;
	int y = centerY - (R * sin(a)) - h / 2;

	//输出文本到计算的位置
	outtextxy(x, y, str);
}

2.3多线程音乐播放

为了实现音乐的循环播放,采用了多线程,一个线程独立负责音乐的播放,使用PlaySound函数,并通过std::this_thread::sleep_for精确控制播放的时间间隔,确保音乐无缝链接,但是我选择的音乐时间长短不一,因此为了使音乐完全播放,所以选择了最长音乐的时间长短上加一秒作为播放的间隔时间。

//音乐播放函数
void playMusic(const TCHAR* songs[], int totalSongs) {
	int songIndex = 0;

	while (true) {
		PlaySound(songs[songIndex], NULL, SND_FILENAME | SND_ASYNC);

		//精确控制播放间隔
		std::this_thread::sleep_for(std::chrono::seconds(165)); //假设每首歌的平均长度为2分钟45秒

		//更新 songIndex,并确保它在数组范围内循环
		songIndex = (songIndex + 1) % totalSongs;
	}
}

三、主函数实现

我就不再过多的解释了,我在代码中都有详细的注释。

//主函数
int main()
{
	
	//歌曲文件名数组
	const TCHAR* songs[4] = {
		TEXT(""),
		TEXT(""),
		TEXT(""),
		TEXT("")
	};
	
	const int totalSongs = 4; //歌曲总数
	//创建并启动音乐播放线程
	std::thread musicThread(playMusic, songs, totalSongs);

	initgraph(Width, Height);               //初始化图像系统,根据定义的宽度和高度进行设置
	cout << "Graphics initialized." << endl;   //图形初始化完成

	IMAGE back;       //加载背景图片
	loadimage(&back, _T("IMAGE"), _T("back"), 1080, 800);       //按照设定的宽度和高度铺满设定的屏幕
	putimage(0, 0, &back);           //设定绘制图像开始的坐标,表示从(0,0)开始绘制
	cout << "Background image loaded." << endl;    //背景图片加载完成


		SYSTEMTIME ti;
		TimeCircle TC[7];//7表示对应年、月、日、星期、时、分、秒
		TCHAR str[25];   //能够显示的最长文本,比如2023年10月29号周日20时45分29秒+一个休止符


		//设置时钟的属性
		for (int i = 0; i < 7; i++)
		{
			TC[i].R = (i + 1) * 50;//按照每次增加50像素,来确定每个时间圈的半径
			TC[i].Radian = 0;  //设置初始弧度为0
			TC[i].NextTime = 0;
			//时间圈设置其分割数
			switch (i)
			{
			case 0:TC[i].fors = 1; break;							
			case 1:TC[i].fors = 12; break;					
			case 2:TC[i].fors = 30; break;							
			case 3:TC[i].fors = 7; break;							
			case 4:TC[i].fors = 24; break;							
			case 5:TC[i].fors = 60; break;							
			case 6:TC[i].fors = 60; break;							
			}
		}
		
		

		BeginBatchDraw();  //批量绘制
		while (true)
		{

			


			GetLocalTime(&ti);   //获取本地系统时间
			TC[2].fors = monthdasy(ti.wYear, ti.wMonth);  //基于当前的年和月,计算日时间圈的天数

			

			//更新时间圈
			for (int j = 0; j < 7; j++) {
				int currentTime = 0; //默认值为0,用于未明确指定的情况

				if (j == 0) {
					//年时间圈
					currentTime = 0; //通常年时间圈不会像其他圈一样根据实时更新,这里保持默认值0
				}
				else if (j == 1) {
					//月时间圈
					currentTime = ti.wMonth;
				}
				else if (j == 2) {
					//日时间圈
					currentTime = ti.wDay;
				}
				else if (j == 3) {
					//周时间圈
					currentTime = ti.wDayOfWeek;
				}
				else if (j == 4) {
					//时时间圈
					currentTime = ti.wHour;
				}
				else if (j == 5) {
					//分时间圈
					currentTime = ti.wMinute;
				}
				else if (j == 6) {
					//秒时间圈
					currentTime = ti.wSecond;
				}

				UpdateTimeCircle(TC[j], currentTime, TC[j].fors);
			}

			

			//绘制时间圈
			for (int j = 0; j < 7; j++) {    //遍历所有7个时间圈(年、月、日、周、时、分、秒)


				//确保str是一个wchar_t*类型的指针
				wchar_t str[25]; //确保了时间圈文本的完整性和正确性

				for (int i = 0; i < TC[j].fors; i++) {
					//声明并初始化所有可能在 switch 语句中使用的局部变量
					int month = 0;
					int day = 0;

					switch (j) {
					case 0: //年时间圈
						_stprintf_s(str, _T("%d年"), ti.wYear);
						break;

					case 1: //月时间圈
						month = (i + ti.wMonth - 1) % 12 + 1;
						_stprintf_s(str, _T("%02d月"), month);
						break;

					case 2: //日时间圈
						day = (i + ti.wDay - 1) % TC[2].fors + 1;
						_stprintf_s(str, _T("%02d号"), day);
						break;

					case 3: //周时间圈
						//初始化str为"周",然后追加星期字符
						swprintf_s(str, L"周%c", L"日一二三四五六"[(i + ti.wDayOfWeek) % 7]);
						break;

					case 4: //时时间圈
						_stprintf_s(str, _T("%02d时"), (i + ti.wHour) % 24);
						break;

					case 5: //分时间圈
						_stprintf_s(str, _T("%02d分"), (i + ti.wMinute) % 60);
						break;

					case 6: //秒时间圈
						_stprintf_s(str, _T("%02d秒"), (i + ti.wSecond) % 60);
						break;
					}

					DrawCircle(str, i, TC[j].fors, TC[j].R, TC[j].Radian, TC[6].R, Width / 2, Height / 2);//调用DrawCircle函数绘制当前时间圈的文本
				}
			}
			FlushBatchDraw();  //批处理绘制,确保所有绘制操作一次性完成,提高绘制效率

		}
		
	return 0;
}

四、剩余代码解释

#include <math.h>       //数学函数库,用于数学计算
#include <time.h>       //时间处理库,提供系统时间的获取、时间格式化等函数
#include<iostream>      //C++标准输入输出流库,提供高级的输入输出操作
#include <Windows.h>    //包含Windows API的函数,提供Windows系统下的各种功能
#include <chrono>       //包含C++标准库中的时间处理功能
#include <thread>       //提供多线程支持
using namespace std;              //引入标准命名空间std,以便于直接使用cout,cin等标准库函数,而不需要std::前缀
const double PI = 4 * atan(1.0);   //利用反正切函数atan计算Π(圆周率),以便后面时钟的展开
int monthdasy(int y, int m);
//monthdasy    计算给定年份和月份的天数
//y   确定某年是否为闰年,进而判断二月天数
//m   确定某月的天数
//定义TimeCircle,用于储存与时间有关的圆形元素信息
struct TimeCircle
{
	int fors;	       //圆形被分割的份数,用于图形分割										
	int R;			   //圆的半径									
	double NextTime;   //用于动画控制,确保图形在特定时间点更新或重绘									
	double Radian;	   //当前的弧度值,用于计算圆上的点的坐标	
	int LastUpdateTime;//上次更新的时间点

};
int monthdasy(int y, int m)
{
	if (m == 2)
		return ((y % 4 == 0 && y % 100 != 0) || y % 400 == 0) ? 29 : 28;
	else
		return 31 - (m - 3) % 5 % 2;
}
//计算给定月份的天数,如果年份是闰年,则2月是29天,反之28天,return ((y % 4 == 0 && y % 100 != 0) || y % 400 == 0) ? 29 : 28;
//如果给定年份是非整百的,则能够被4整除,不能被100整除则是闰年,反之被400整除则是闰年
//如果计算的不是2月的,那么则根据return 31 - (m - 3) % 5 % 2;
//除去2月,剩下的有1、3、4、5、6、7、8、9、10、11、12月,当m等于4时,4-3等于1,然后1%5,取余为1,1%2,取余为1,则4月的天数为30天。
//取余运算,当被除数小于除数时,取余就是被除数本身

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

夜作

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值