1. 从控制台时钟到桌面窗口:为什么值得折腾
很多人第一次写 C/C++ 时钟,都是像下面这样:控制台里system("cls")清屏,Sleep(1000)每秒刷新,GetLocalTime取时间,跑起来也能看。问题是它只能待在黑框里,任务栏那个小钟又丑又小,想放到桌面角落当个摆件,控制台方案就露怯了——窗口标题栏、边框、闪烁的光标,全都在破坏观感。
所以这篇的目标很明确:用 Win32/GDI 写一个无边框、置顶、半透明背景的简约桌面时钟,再把编辑器里的 AI 插件 endpoint 统一改到 TaoToken,让「写窗口代码 → 让 AI 补全/解释/改样式」这条链路只维护一个 Key、一个 Base URL。适合两类人:一是刚学完 C++ 语法、想找个能看见成果的小项目练手的同学;二是已经在用 AI 插件写代码、但被多个厂商 Key 和不同 endpoint 搞烦的开发者。
核心检索词先摆出来:c/c++ 简约桌面时钟,本质是一个用 Win32 API 创建分层窗口(Layered Window)、用 GDI 绘制文字、用SetTimer驱动每秒重绘的小程序。它能做什么?常驻桌面、无边框、可拖动、可置顶、显示时分秒和日期。适合谁?想练 Windows 编程、想有个干净桌面摆件、想顺便把 AI 编码链路理顺的人。
我试过纯控制台版本,也试过 Qt 版本,最后发现 Win32 + GDI 的代码量最少、依赖最少,一个.cpp文件加系统库就能编译,特别适合当「AI 辅助编码」的练手素材——因为窗口样式、颜色、字体这些参数,正好是让 AI 帮你反复微调的最佳场景。
下面按「先跑通窗口 → 再接 TaoToken → 再验证走时」的顺序来,每一步都能复制粘贴直接跑。
2. TaoToken 前置:统一 Key 与 API 通道,让 AI 插件只认一个 endpoint
在写窗口代码之前,先把 AI 辅助这条链路铺好。原因很现实:你写这个时钟的过程中,一定会遇到「UpdateLayeredWindow参数看不懂」「CreateFont怎么设字体」「为什么窗口不刷新」这类问题,如果每次都要在多个厂商的 Key 之间切换、改不同插件的 Base URL,效率会被拖垮。
TaoToken 在这里扮演的角色是统一的 API 通道:你申请一个 Key,把编辑器里各个 AI 插件的 endpoint 都指向同一个 Base URL,模型 ID 按需选择。这样无论是补全、对话还是 Agent 类任务,都走同一条通道,Key 也只维护一份。
先明确三个必须写全的参数,后面所有配置都围绕它们:
| 参数 | 值 | 说明 |
|---|---|---|
| Base URL | https://taotoken.net/api | 所有插件统一填这个,注意不要带多余路径 |
| API Key | 在控制台创建 | 形如sk-...,只显示一次,及时保存 |
| Model ID | 按插件要求填 | 补全类、对话类模型 ID 不同,以文档为准 |
获取 Key 的入口在控制台,创建后复制保存。文档页有各编辑器的接入示例,遇到不确定的字段先去文档核对,比在群里问快得多。
注意:Base URL 填
https://taotoken.net/api,不要自己拼/v1/chat/completions之类的后缀,多数插件会自动补全路径,手动加反而会 404。
这一步的产出是一个 Key 和一个 Base URL。接下来在具体插件里配置时,记住「三件套」原则:Base URL + Key + Model ID,缺一个都连不上。如果你用的是 Claude Code 这类工具,配置项名称可能叫ANTHROPIC_BASE_URL或写在settings.json里;如果是 Cline、Continue 这类 VS Code 插件,通常在设置界面填 OpenAI Compatible 的 Base URL 和 Key。不管哪种,值都来自上面这张表。
把这一步做完,你后面写窗口代码时,遇到任何 API 用法疑问,直接在当前编辑器里问 AI,不用切窗口、不用换 Key。
3. 可复制配置:Win32 无边框时钟 + 编辑器 AI 插件接入片段
这一节给两份可直接复制的配置:一份是时钟窗口的核心代码,一份是编辑器 AI 插件的接入片段。先看时钟。
窗口部分的关键点有三个:用WS_EX_LAYERED实现半透明、用UpdateLayeredWindow绘制、用SetTimer每秒触发重绘。下面是一个精简但完整的main.cpp,编译命令在代码后面。
// main.cpp - Win32 无边框简约桌面时钟 #include <windows.h> #include <string> #define TIMER_ID 1 int g_width = 260; int g_height = 120; // 绘制当前时间到内存 DC,返回位图句柄 HBITMAP RenderClock(HDC hdcScreen, int w, int h) { HDC hdcMem = CreateCompatibleDC(hdcScreen); BITMAPINFO bmi = {}; bmi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER); bmi.bmiHeader.biWidth = w; bmi.bmiHeader.biHeight = -h; // 负值表示自上而下 bmi.bmiHeader.biPlanes = 1; bmi.bmiHeader.biBitCount = 32; bmi.bmiHeader.biCompression = BI_RGB; void* bits = nullptr; HBITMAP hBmp = CreateDIBSection(hdcScreen, &bmi, DIB_RGB_COLORS, &bits, nullptr, 0); HGDIOBJ oldBmp = SelectObject(hdcMem, hBmp); // 背景:半透明黑 RECT rc = { 0, 0, w, h }; HBRUSH bg = CreateSolidBrush(RGB(20, 20, 20)); FillRect(hdcMem, &rc, bg); DeleteObject(bg); // 取时间 SYSTEMTIME st; GetLocalTime(&st); wchar_t timeBuf[16]; wchar_t dateBuf[32]; swprintf_s(timeBuf, L"%02d:%02d:%02d", st.wHour, st.wMinute, st.wSecond); swprintf_s(dateBuf, L"%04d-%02d-%02d", st.wYear, st.wMonth, st.wDay); SetBkMode(hdcMem, TRANSPARENT); SetTextColor(hdcMem, RGB(0, 255, 180)); // 时间大字 HFONT hFontTime = CreateFontW(56, 0, 0, 0, FW_BOLD, FALSE, FALSE, FALSE, DEFAULT_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, CLEARTYPE_QUALITY, DEFAULT_PITCH, L"Consolas"); HGDIOBJ oldFont = SelectObject(hdcMem, hFontTime); TextOutW(hdcMem, 16, 20, timeBuf, (int)wcslen(timeBuf)); // 日期小字 HFONT hFontDate = CreateFontW(20, 0, 0, 0, FW_NORMAL, FALSE, FALSE, FALSE, DEFAULT_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, CLEARTYPE_QUALITY, DEFAULT_PITCH, L"Microsoft YaHei"); SelectObject(hdcMem, hFontDate); SetTextColor(hdcMem, RGB(180, 180, 180)); TextOutW(hdcMem, 18, 84, dateBuf, (int)wcslen(dateBuf)); SelectObject(hdcMem, oldFont); DeleteObject(hFontTime); DeleteObject(hFontDate); SelectObject(hdcMem, oldBmp); DeleteDC(hdcMem); return hBmp; } void UpdateWindow(HWND hwnd) { HDC hdcScreen = GetDC(nullptr); HBITMAP hBmp = RenderClock(hdcScreen, g_width, g_height); HDC hdcMem = CreateCompatibleDC(hdcScreen); HGDIOBJ oldBmp = SelectObject(hdcMem, hBmp); POINT ptSrc = { 0, 0 }; SIZE size = { g_width, g_height }; POINT ptDst = { 0, 0 }; BLENDFUNCTION blend = { AC_SRC_OVER, 0, 220, AC_SRC_ALPHA }; RECT rc; GetWindowRect(hwnd, &rc); ptDst.x = rc.left; ptDst.y = rc.top; UpdateLayeredWindow(hwnd, hdcScreen, &ptDst, &size, hdcMem, &ptSrc, 0, &blend, ULW_ALPHA); SelectObject(hdcMem, oldBmp); DeleteDC(hdcMem); DeleteObject(hBmp); ReleaseDC(nullptr, hdcScreen); } LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) { switch (msg) { case WM_TIMER: if (wp == TIMER_ID) UpdateWindow(hwnd); return 0; case WM_LBUTTONDOWN: // 拖动窗口 ReleaseCapture(); SendMessage(hwnd, WM_NCLBUTTONDOWN, HTCAPTION, 0); return 0; case WM_DESTROY: KillTimer(hwnd, TIMER_ID); PostQuitMessage(0); return 0; } return DefWindowProc(hwnd, msg, wp, lp); } int WINAPI wWinMain(HINSTANCE hInst, HINSTANCE, PWSTR, int nCmdShow) { const wchar_t CLASS_NAME[] = L"SimpleClockWnd"; WNDCLASSW wc = {}; wc.lpfnWndProc = WndProc; wc.hInstance = hInst; wc.lpszClassName = CLASS_NAME; RegisterClassW(&wc); int screenW = GetSystemMetrics(SM_CXSCREEN); int screenH = GetSystemMetrics(SM_CYSCREEN); HWND hwnd = CreateWindowExW( WS_EX_LAYERED | WS_EX_TOPMOST | WS_EX_TOOLWINDOW, CLASS_NAME, L"Clock", WS_POPUP, screenW - g_width - 40, 40, g_width, g_height, nullptr, nullptr, hInst, nullptr); ShowWindow(hwnd, nCmdShow); UpdateWindow(hwnd); SetTimer(hwnd, TIMER_ID, 1000, nullptr); MSG msg; while (GetMessage(&msg, nullptr, 0, 0)) { TranslateMessage(&msg); DispatchMessage(&msg); } return 0; }编译命令(MinGW-w64 或 MSVC 都行,MinGW 示例):
g++ main.cpp -o clock.exe -mwindows -lgdi32 -luser32 -std=c++17-mwindows去掉控制台窗口,-lgdi32链接 GDI。跑起来后,桌面右上角会出现一个半透明黑底、青色大字的时钟,鼠标按住可以拖动,WS_EX_TOPMOST保证它一直在最上层。
再看编辑器 AI 插件的接入片段。以 VS Code 里常见的 OpenAI Compatible 配置为例,settings.json里这样写:
{ "aiAssistant.baseUrl": "https://taotoken.net/api", "aiAssistant.apiKey": "sk-你的Key", "aiAssistant.model": "按文档填对应模型ID" }如果你用的是 Cline 这类插件,配置项名称不同,但三件套不变:Base URL 填https://taotoken.net/api,Key 填控制台创建的,Model ID 按文档选。Claude Code 用户则是在settings.json里配置ANTHROPIC_BASE_URL指向同一地址,Key 用同一个。
注意:不同插件的字段名差异很大,别凭记忆填。先去文档页找到对应编辑器的示例,复制字段名再改值,能省掉大量 401 排查时间。
4. 验证请求与走时:编译运行、AI 补全、时钟精度三件事
配置写完不算完,得验证三件事:时钟能编译运行、AI 插件能通、时钟走时准。
先验证时钟。编译命令跑完后,双击clock.exe,观察三个点:窗口是否无边框且半透明、时间是否每秒跳动、拖动是否跟手。如果窗口出来了但时间不动,八成是SetTimer没生效或WM_TIMER没处理;如果窗口是纯黑不透明,检查UpdateLayeredWindow的BLENDFUNCTION参数和WS_EX_LAYERED是否都到位。
再验证 AI 插件。在编辑器里新建一个test.cpp,输入注释// 用 GDI 画一个圆,看补全是否触发;或者打开对话面板问「UpdateLayeredWindow的BLENDFUNCTION各参数含义」,看是否正常返回。这一步通了,说明 Base URL、Key、Model ID 三件套都对。
最后验证走时精度。把时钟和系统任务栏时间并排看,跑十分钟,观察秒数是否同步。Win32 的SetTimer精度受消息循环影响,长时间跑可能有累积误差。如果要求高,可以把SetTimer的间隔改成 500ms,在WM_TIMER里判断秒数变化再重绘,减少无效刷新。
验证 AI 请求是否真的走通,最直接的方式是在对话面板里发一条消息,看返回内容是否正常。如果返回空或报错,对照下一节的排查表。
这一步的产出是:一个能跑的时钟 + 一个能用的 AI 插件。两者都通了,后面的微调(改颜色、改字体、加秒针动画)就可以直接让 AI 帮你改代码,你只负责编译和看效果。
5. 常见报错排查:401、local proxy failed、reading choices、OAuth
配置 AI 插件时,报错基本集中在四类。下面按真实报错对照排查。
401 Unauthorized:Key 错了或没带上。检查三处:Key 是否复制完整(有没有漏字符)、Base URL 是否写成https://taotoken.net/api(多了/v1或少了/api都会 401)、插件是否真的读取了你填的 Key(有些插件有多个配置层级,改错了地方)。如果 Key 是在控制台刚创建的,确认没有多余空格。
local proxy failed / connection refused:插件试图走本地代理但没起来。检查插件设置里是否有「使用本地代理」之类的开关,关掉它,让它直连 Base URL。另外确认系统没有设置全局代理指向一个不存在的端口。
reading choices 相关报错:通常是返回结构不符合插件预期,常见原因是 Model ID 填错,或者 Base URL 指向了一个不兼容 OpenAI 格式的端点。回到文档确认该模型对应的 ID,以及 Base URL 是否应该带版本路径。
OAuth 相关报错:多见于 Claude Code 这类工具。如果你用的是 API Key 模式,确认配置里没有残留的 OAuth 登录态;如果工具要求先登录再切 API,按文档顺序操作。settings.json里ANTHROPIC_BASE_URL和 Key 要同时存在,只配一个会报错。
排查顺序建议:先确认 Base URL 和 Key 这两个「地址类」参数,再看 Model ID 这个「内容类」参数,最后看插件自身的代理/登录开关。多数问题出在前两步。
注意:报错信息里如果出现
proxy、tunnel这类词,先检查是不是插件自带的网络设置,而不是去改 Base URL。Base URL 始终是https://taotoken.net/api。
时钟本身的报错相对少,常见的是链接失败(忘了-lgdi32)、wWinMain入口找不到(MinGW 需要-mwindows)、中文乱码(源文件存成 UTF-8 带 BOM,或用L""宽字符)。这些在编译阶段就能定位。
6. 把链路固定下来:一个 Key 走完编码、调试、微调
到这里,时钟能跑、AI 插件能通、走时也验证过了。回头看这条链路的价值:你写窗口代码时遇到 API 疑问,直接在编辑器里问 AI;AI 返回的代码片段,你复制进main.cpp编译;样式不满意,让 AI 改颜色和字体参数;甚至可以让 AI 帮你把SetTimer改成更高精度的方案。整个过程只用一个 Key、一个 Base URL,不用在多个厂商之间切换。
如果你后面要长期做 C/C++ 项目,或者想让 AI 参与更复杂的重构、Agent 类任务,可以考虑把通道固定到 Coding Plan,省去每次单独配 Key 的麻烦。模型对话入口适合快速验证某个模型是否满足需求,接入文档则是遇到字段不确定时的第一手参考。
最后留一个实用技巧:把main.cpp里的g_width、g_height、颜色值、字体名都提成宏或常量放在文件顶部,这样每次让 AI 改样式,只需要改这几个值,不用在函数里翻找。时钟这种小项目,参数集中管理比代码优雅更重要——毕竟你只是想让它好看点,不是想重构一个框架。