本文作者:Tianheng Ni
本文分类:编程学习笔记 软件评测 浏览:1630
阅读时间:9290字, 约10.5-15.5分钟
各位同学们大家好,今天我来和大家讲讲HYRing。
Github地址:eric-nth/HYRing: Huayu Ringtone Software 2022 (github.com)
README.md
Huayu Ringtone Software 2022
目的
使用 Visual Studio Community 2022 编写,目的是解决2022年上海市民办华育中学网课期间电脑铃声的问题。
若下载缓慢,请访问:HYRing铃声系统2022发布 - Ericnth的小站
版权声明
本软件遵循GPLv3协议开源。x64/Release/rings文件夹下的*.mp3音乐文件除外,归版权方所有,此处仅供测试,如有侵权请联系删除。
功能特性
可根据配置文件所指定的时间表与铃声进行打铃,使用winmm库,支持mp3格式的音频。
支持通过窗口置顶、半透明化来实现上课期间的使用。
使用传统Win32编写,因此应该有较高的兼容性。
不足之处是只能指定星期几来打铃,无法解决放假、调休的情况。
工作区合影

依赖:kernel32.lib;user32.lib;gdi32.lib;winspool.lib;comdlg32.lib;advapi32.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;odbc32.lib;odbccp32.lib;%(AdditionalDependencies)
资源文件(Resource.rc)
图标:(由 23786 制作)

项目属性:(会体现在生成的可执行文件的属性里)

字符串:(写在这里是为了规避可恶的char和WCHAR的问题,同时避免因为字符编码而乱码)

菜单:

帮助对话框:

HYRing.h(头文件与宏定义)

HYRing.cpp(程序主体)
// HYRing.cpp : 定义应用程序的入口点。
//
#include "framework.h"
#include "HYRing.h"
#define MAX_LOADSTRING 100
// 全局变量:
HINSTANCE hInst; // 当前实例
WCHAR szTitle[MAX_LOADSTRING]; // 标题栏文本
WCHAR szWindowClass[MAX_LOADSTRING]; // 主窗口类名
WCHAR szWelcomeText[MAX_LOADSTRING]; // 欢迎使用
WCHAR szMainText[MAX_LOADSTRING]; // 主字符串
HFONT winFont; // 窗口字符字体
BOOL winTopped; // 窗口置顶选择
BOOL winTransparent; // 窗口半透明选择
HANDLE hMonitorThread;
HANDLE hWinTopThread;
WCHAR szReadBlock[2048] = { 0 } ;
HWND hMainText;
WORD ringLoopEnd = 0;
BOOL stopRingFlag = 0;
OPENFILENAME ofn;
WCHAR szLoadCfgFileName[MAX_PATH];
// 此代码模块中包含的函数的前向声明:
ATOM MyRegisterClass(HINSTANCE hInstance);
BOOL InitInstance(HINSTANCE, int);
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
INT_PTR CALLBACK About(HWND, UINT, WPARAM, LPARAM);
VOID CDECL TimeMonitorProc(LPVOID);
VOID CDECL WinTopperProc(LPVOID);
int APIENTRY wWinMain(_In_ HINSTANCE hInstance,
_In_opt_ HINSTANCE hPrevInstance,
_In_ LPWSTR lpCmdLine,
_In_ int nCmdShow)
{
UNREFERENCED_PARAMETER(hPrevInstance);
UNREFERENCED_PARAMETER(lpCmdLine);
// TODO: 在此处放置代码。
// 初始化全局字符串
LoadStringW(hInstance, IDS_APP_TITLE, szTitle, MAX_LOADSTRING);
LoadStringW(hInstance, IDS_WELCOME, szWelcomeText, MAX_LOADSTRING);
LoadStringW(hInstance, IDS_MAINTEXT, szMainText, MAX_LOADSTRING);
LoadStringW(hInstance, IDC_HYRING, szWindowClass, MAX_LOADSTRING);
MyRegisterClass(hInstance);
// 执行应用程序初始化:
if (!InitInstance (hInstance, nCmdShow))
{
return FALSE;
}
HACCEL hAccelTable = LoadAccelerators(hInstance, MAKEINTRESOURCE(IDC_HYRING));
MSG msg;
// 主消息循环:
while (GetMessage(&msg, nullptr, 0, 0))
{
if (!TranslateAccelerator(msg.hwnd, hAccelTable, &msg))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
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_HYRING));
wcex.hCursor = LoadCursor(nullptr, IDC_ARROW);
wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW+1);
wcex.lpszMenuName = MAKEINTRESOURCEW(IDC_HYRING);
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);
return TRUE;
}
前面这段在Win32开发中可以说是套话了。基本没有什么值得讲的地方。开头为引用头文件,进行全局变量定义与函数声明。UNREFERENCED_PARAMETER宏用来避免触发参数未使用的警告。WinMain是程序入口点,传入参数,读入前面设置的字符串,设置窗口参数,初始化窗口实例,读入快捷键,开始主消息循环(获取并转译窗口发来的消息后交给WndProc处理)。
WndProc
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
LONG winStyle;
switch (message)
{
case WM_CREATE:
//创建控件
CreateWindow(_T("STATIC"), szWelcomeText, WS_CHILD | WS_VISIBLE, 10, 10, 350, 20, hWnd, (HMENU)IDM_WELCOME, GetModuleHandle(NULL), NULL);
hMainText = CreateWindow(_T("STATIC"), szMainText, WS_CHILD | WS_VISIBLE, 10, 35, 350, 40, hWnd, (HMENU)IDM_MAINTEXT, GetModuleHandle(NULL), NULL);
//设置字体
//winFont = CreateFont(20, 0, 0, 0, 0, FALSE, FALSE, 0, DEFAULT_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, DEFAULT_QUALITY, DEFAULT_PITCH | FF_SWISS, _T("Microsoft YaHei UI"));
winFont = (HFONT)GetStockObject(DEFAULT_GUI_FONT);
SendDlgItemMessage(hWnd, IDM_WELCOME, WM_SETFONT, (WPARAM)winFont, MAKELPARAM(TRUE, 0));
SendDlgItemMessage(hWnd, IDM_MAINTEXT, WM_SETFONT, (WPARAM)winFont, MAKELPARAM(TRUE, 0));
//启动线程
hMonitorThread = (HANDLE)_beginthread(TimeMonitorProc, 0, NULL);
//hWinTopThread = (HANDLE)_beginthread(WinTopperProc, 0, (LPVOID)hWnd);
winTopped = 1;
winTransparent = 1;
//窗口及风格设置
SetWindowPos((HWND)hWnd, HWND_TOPMOST, 0, 0, 400, 180, 0);
winStyle = GetWindowLong(hWnd, GWL_STYLE);
winStyle &= ~(WS_MAXIMIZEBOX) & ~WS_THICKFRAME;
SetWindowLong(hWnd, GWL_STYLE, winStyle);
winStyle = GetWindowLong(hWnd, GWL_EXSTYLE);
winStyle ^= WS_EX_LAYERED;
SetWindowLong(hWnd, GWL_EXSTYLE, winStyle);
SetLayeredWindowAttributes(hWnd, 0, 200, LWA_ALPHA);
break;
case WM_COMMAND:
{
int wmId = LOWORD(wParam);
// 分析菜单选择:
switch (wmId)
{
case IDM_ABOUT:
DialogBox(hInst, MAKEINTRESOURCE(IDD_ABOUTBOX), hWnd, About);
break;
case IDM_TOP:
CheckMenuItem(GetSubMenu(GetMenu(hWnd), 0), 0, MF_BYPOSITION | (winTopped ? MF_UNCHECKED : MF_CHECKED));
winTopped = !winTopped;
if (winTopped) {
SetWindowPos((HWND)hWnd, HWND_TOPMOST, 0, 0, 400, 180, 0);
}
else {
SetWindowPos((HWND)hWnd, HWND_NOTOPMOST, 0, 0, 400, 180, 0);
}
break;
case IDM_LOADCFG:
memset(&ofn, 0, sizeof(OPENFILENAME));
memset(szLoadCfgFileName, 0, sizeof(WCHAR) * MAX_PATH);
ofn.lStructSize = sizeof(OPENFILENAME);
ofn.lpstrFilter = L"HYRing Config (.cfg)\0*.cfg";
ofn.lpstrFile = szLoadCfgFileName;
ofn.nMaxFile = MAX_PATH;
ofn.Flags = OFN_FILEMUSTEXIST;
if (GetOpenFileName(&ofn)) {
//MessageBox(NULL, szLoadCfgFileName, L"即将打开配置文件...", NULL);
ringLoopEnd = 1;
}
else {
break;
}
Sleep(1000);
ringLoopEnd = 2;
Sleep(500);
hMonitorThread = (HANDLE)_beginthread(TimeMonitorProc, 0, NULL);
break;
case IDM_STOPRING:
stopRingFlag = 1;
break;
case IDM_TOGGLE_TRANSPARENCY:
CheckMenuItem(GetSubMenu(GetMenu(hWnd), 1), 2, MF_BYPOSITION | (winTransparent ? MF_UNCHECKED : MF_CHECKED));
winTransparent = !winTransparent;
if (winTransparent) {
SetLayeredWindowAttributes(hWnd, 0, 200, LWA_ALPHA);
}
else {
SetLayeredWindowAttributes(hWnd, 0, 255, LWA_ALPHA);
}
break;
case IDM_EXIT:
DestroyWindow(hWnd);
break;
default:
return DefWindowProc(hWnd, message, wParam, lParam);
}
}
break;
case WM_PAINT:
{
PAINTSTRUCT ps;
HDC hdc = BeginPaint(hWnd, &ps);
// TODO: 在此处添加使用 hdc 的任何绘图代码...
EndPaint(hWnd, &ps);
}
break;
case WM_CLOSE:
if (MessageBox(hWnd, _T("确认要退出HYRing?"), _T("HYRing - Close"), MB_OKCANCEL) == IDOK)
SendMessage(hWnd, WM_DESTROY, 0, 0);
break;
case WM_DESTROY:
//CloseHandle(hMonitorThread);
PostQuitMessage(0);
break;
default:
return DefWindowProc(hWnd, message, wParam, lParam);
}
return 0;
}
大名鼎鼎,有或者说是臭名昭著的WndProc,这是Win32程序设计中一个十分不优雅,又是非常主要的函数,它负责接受窗口发来的消息,通过一个switch,来根据消息编号进行用户指定的操作,如果用户没有指定(也就是到了default),就会调用DefWindowProc来进行系统默认的操作。我认为这比起Qt中信号-槽的机制与其它图形界面开发中常见的事件机制要傻上不少。况且,在WndProc中进行的操作不能是阻塞的,否则会阻止下一个消息处理的开始,导致应用未响应。wParam和lParam是为事件传入的参数。
首先是WM_CREATE,代表窗口被创建,我在此处进行了绘制应用界面的操作(因为我不知道如何在主窗口中使用可视化的控件编辑器),通过CreateWindow创建控件,通过SendDlgItemMessage发送字体设置,设置窗口置顶与半透明度,以及启动本程序中最重要的线程TimeMonitorProc(这就是我写的打铃程序,为了避免播放时阻塞程序运行,我把它放进了一个单独的线程里运行,虽然之后这也带来了许多麻烦)。
WM_COMMAND则一般用来处理菜单栏点击的操作。
WM_CLOSE与WM_DESTROY用于处理窗口关闭时的情况。
TimeMonitorProc
VOID CDECL TimeMonitorProc(LPVOID lpParam) {
time_t rawtime;
struct tm timeinfo;
time(&rawtime);
localtime_s(&timeinfo, &rawtime);
WCHAR szCfgFileName[260] = {0};
if (ringLoopEnd == 2) {
wsprintf(szCfgFileName, L"%s", szLoadCfgFileName);
}
else {
wsprintf(szCfgFileName, L"./rings/%d.cfg", timeinfo.tm_wday);
}
WCHAR szMp3PlayCommand[330] = { 0 };
FILE* pCfgFile = NULL;
_wfopen_s(&pCfgFile, szCfgFileName, L"r");
if (pCfgFile == 0) {
MessageBox(NULL, L"File Open Error", L"HYRing", 0);
PostQuitMessage(0);
_endthread();
}
int iHrs[50] = {0}, iMin[50] = {0}, iPtr = 0, iSFlag/*是否指定显示内容*/;
WCHAR szRingName[260][50] = {0};
int iRingCount = 0;
WCHAR szMainTextCopy[300] = {0};
for (int i = 0; i < 50; i++) {
fwscanf_s(pCfgFile, L"%d:%d %s\n", &(iHrs[i]), &(iMin[i]), szRingName[i], 260);
if (iHrs[i] == -1 || iMin[i] == -1) {
iRingCount = i;
break;
}
}
while (1) {
if (ringLoopEnd == 1) {
_fcloseall();
_endthread();
return;
}
if (stopRingFlag) {
stopRingFlag = 0;
wsprintf(szMp3PlayCommand, L"stop ring%d", iPtr - 1);
mciSendString(szMp3PlayCommand, NULL, 0, NULL);
MessageBox(NULL, L"铃声已停止!", L"HYRing", 0);
}
iSFlag = 0;
time(&rawtime);
localtime_s(&timeinfo, &rawtime);
for (; iPtr < iRingCount; iPtr++) {
if (iHrs[iPtr] > timeinfo.tm_hour) {
break;
}
if (iHrs[iPtr] == timeinfo.tm_hour) {
if (iMin[iPtr] >= timeinfo.tm_min) {
break;
}
}
if (iPtr >= iRingCount - 1) {
iSFlag = 1;
wsprintf(szMainTextCopy, L"今日无更多闹铃");
break;
}
}
if (iPtr > iRingCount - 1) {
iSFlag = 1;
wsprintf(szMainTextCopy, L"今日无更多闹铃");
}
if (iHrs[iPtr] == timeinfo.tm_hour && iMin[iPtr] == timeinfo.tm_min) {
//Ring!
iSFlag = 1;
wsprintf(szMainTextCopy, L"正在闹铃: %s", szRingName[iPtr]);
//PlaySound(szRingName[iPtr], NULL, SND_FILENAME | SND_ASYNC);
wsprintf(szMp3PlayCommand, L"open \"./rings/%s.mp3\" alias ring%d", szRingName[iPtr], iPtr);
mciSendString(szMp3PlayCommand, NULL, 0, NULL);
wsprintf(szMp3PlayCommand, L"play ring%d", iPtr);
mciSendString(szMp3PlayCommand, NULL, 0, NULL);
iPtr++;
}
if (!iSFlag) {
wsprintf(szMainTextCopy, szMainText, timeinfo.tm_wday, iRingCount, iHrs[iPtr], iMin[iPtr], iPtr + 1, szRingName[iPtr]);
}
SetWindowText(hMainText, szMainTextCopy);
Sleep(500);
}
_fcloseall();
_endthread();
}
2-5行:获取时间。
7-18行:创建配置文件输入流。
24-35行:根据格式读入当日打铃时间与铃声文件名。
43-48行:当点击UI中“停止铃声”,停止正在播放的铃声。
54-71行:通过对比得到当前即将需要打的铃声。
75-76行:声音播放代码。76行是winmm方式(支持mp3),75行预留了windows的原生方式(仅支持wav,但wav是不压缩的导致文件非常大)。
83-86行:将当前进度同步到UI的对话框上。
后记
主要的代码就这些,能看到这里真是辛苦大家了!
Win32开发真的是槽点多多,但又好处多多。槽点是真的很复杂,真的很麻烦,真的很难debug,很难找到资料,不能跨系统;好处是文件小(本次项目的exe文件仅60KB),运行无压力(占用系统资源极少,本次项目仅使用2MB不到的RAM,远不到1%的CPU),兼容性不错(本软件暂时还没有碰到过此类问题,除了祖传的高DPI兼容性),没有做不到的。以往一直坚持Win32的我可能之后也要转向Qt了。
之后再见,先去填WWDC的坑了。
关于作者Tianheng Ni
- 卑微站长23564~ 苣蒻OIer,电脑爱好者,喜欢C++编程/折腾网站
- Email: [email protected]
- 注册于: 2020-04-05 07:11:36
