本文作者:Tianheng Ni
本文分类:编程学习笔记 软件评测 浏览:1349
阅读时间: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: eric_ni2008@163.com
- 注册于: 2020-04-05 07:11:36