目录
前言
时代滚滚向前,技术日新月异。作为身处信息时代的一份子,就需要不断加强学习,展现对技术的渴望,从基础学习逐步建立起属于自己的“知识大厦”,这样才能不被时代淘汰。
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天。
//取余运算,当被除数小于除数时,取余就是被除数本身