当前位置: 首页 > article >正文

通用GUI编程技术——Win32 原生编程实战(十八)——GDI 设备上下文(HDC)完全指南

通用GUI编程技术——Win32 原生编程实战十八——GDI 设备上下文HDC完全指南前面一系列文章我们聊了对话框、控件、资源这些内容我们的窗口已经能够显示各种控件了。但你可能已经发现了一个问题我们所有的绘图操作都是在控件内部进行的如果我想在窗口的客户区直接画一条线、画一个圆、或者显示一张图片该怎么处理这就涉及到 Win32 编程中一个核心但容易被初学者忽视的主题——GDI 设备上下文HDC。今天我们要深入的就是这个让无数新手踩坑的话题。也遇到前辈指点笔者可以开始考虑一些更深层次更加高级的GUI编程特性比如说布局引擎响应式更新动画图像ApiShaderGPU渲染管线等笔者最近正在出差这些还真要仔细坐下来好好想想怎么讲。要不然的确快成Win32 窗口API大全了目前仓库开源相关想法和代码会第一时间同步https://github.com/Charliechen114514/anatomy_gui预计很快就会出一个更加详细的路线图前言从 WM_PAINT 说起的绘图需求说实话在我刚开始学 Win32 的时候对 GDI 绘图这件事其实有点抗拒。那时候觉得现代框架随便拖个控件就能实现大部分需求为什么要去折腾这些底层的绘图 API但很快我就发现这种想法是 naive 的。当你需要实现一个自定义的图表控件、需要在窗口上实时显示数据曲线、或者想做点炫酷的动画效果时你会发现控件完全帮不上忙必须亲自下场画。更现实的是很多看似简单的需求背后都需要 GDI 知识。比如你想在窗口背景上画一个渐变色想在状态栏显示一个进度图标或者想把控件的外观改成非标准样式这些都需要直接操作设备上下文。而如果你不理解 HDC 的本质和正确的使用方式你的程序要么画不出东西要么画出来一闪一闪的更糟糕的是——悄无声息地泄漏资源直到系统提示你 GDI 对象不足。另一个让新手头疼的问题是获取 HDC 的方式有好几种BeginPaint、GetDC、GetWindowDC、CreateCompatibleDC它们各自适用什么场景什么时候该用哪个用错了会有什么后果官方文档虽然写了但说实话那些描述对于初学者来说有点抽象很多坑只能自己踩过才知道。这篇文章会带着你从 HDC 的本质开始把四种获取方式、状态管理、GDI 对象生命周期这些核心问题彻底搞清楚。我们不只是知道怎么用更重要的是理解为什么要这么用。环境说明在我们正式开始之前先明确一下我们这次动手的环境平台Windows 10/11理论上 Windows 2000 都支持 GDI开发工具Visual Studio 2019 或更高版本编程语言CC17 或更新项目类型桌面应用程序Win32 项目Windows SDK任何最新版本即可代码假设你已经熟悉前面文章的内容——至少知道怎么创建一个基本窗口、怎么处理消息、什么是窗口过程函数。如果这些概念对你来说还比较陌生建议先去看看前面的笔记。第一步——HDC 的本质什么是设备上下文HDC 是什么HDCHandle to Device Context是 Windows GDI 中最核心的概念之一。官方定义是设备上下文一个结构体定义了一组图形对象及其关联属性以及影响输出的图形模式。这个定义听起来有点抽象我们换个角度理解。你可以把 HDC 想象成一张画布的句柄。就像画家需要画布来作画一样在 Windows 里绘图你需要先拿到一个 HDC然后才能在上面画线、画圆、写字。但这个画布不只是屏幕也可以是打印机、内存中的位图甚至是元文件。HDC 就是 Windows 给你提供的一个抽象层让你用同一套 API 就能在不同设备上绘图。HDC 包含什么一个 HDC 内部包含了很多东西你可以把它理解成当前绘图状态的一个快照。里面有什么呢有当前选中的画笔决定线的颜色和粗细、画刷决定填充颜色、字体决定文字样式、位图用于图像操作、调色板定义可用颜色还有一堆绘图属性比如背景模式、文本对齐方式、当前坐标位置等等。这些东西在 Windows 内部是怎么组织的呢其实 HDC 背后对应的是一段内核管理的内存结构。当你调用 GetDC 或 BeginPaint 时Windows 会为你准备好这个结构填充好默认值。你可以修改这些值你的修改会立即影响后续的绘图操作。当你 ReleaseDC 或 EndPaint 时Windows 会把这个 HDC 标记为可用其他地方可以复用。为什么需要 HDC你可能会问为什么不直接在窗口上画非要通过 HDC 这个中间层核心原因是抽象和隔离。Windows 需要支持很多种输出设备——显示器、打印机、绘图仪、内存位图等等。每种设备的特性都不一样分辨率的差异、色彩深度的差异、支持绘图原语的差异。如果没有 HDC 这个抽象层你的程序就需要针对每种设备写不同的代码这是不可想象的。有了 HDC你可以用同一套代码在不同设备上绘图。你只需要说画一条红色直线Windows 会根据当前设备的特性把这条指令转换成对应的设备操作。显示器上是画像素打印机上是喷墨内存位图上是修改像素数据。你的代码不需要关心这些细节。另一个重要的原因是多任务环境下的资源管理。Windows 是多任务系统很多程序可能同时想绘图。如果没有 HDC 的概念程序之间就会互相干扰——你的程序可能把另一个程序的绘图给覆盖了。通过 HDCWindows 可以管理绘图上下文确保每个程序只能操作自己的区域。第二步——获取 HDC 的四种方式现在我们进入正题。Windows 提供了几种获取 HDC 的方式每种方式都有自己的适用场景和注意事项。用错了不仅可能导致绘图失败还可能造成资源泄漏。我们一个个来看。BeginPaint/EndPaintWM_PAINT 中专用这是最常见的获取方式也是最容易用错的方式。BeginPaint 和 EndPaint 是专门为处理 WM_PAINT 消息设计的它们必须成对使用而且只能在 WM_PAINT 消息处理中调用。caseWM_PAINT:{PAINTSTRUCT ps;HDC hdcBeginPaint(hwnd,ps);// 所有绘图操作在这里进行Rectangle(hdc,50,50,200,150);EndPaint(hwnd,ps);return0;}这里有个重要的细节BeginPaint 返回的 HDC 只能用于绘制窗口的无效区域。什么是无效区域就是 Windows 标记为需要重绘的部分。当你调用 InvalidateRect 时Windows 会把指定区域标记为无效然后在下一个消息循环时发送 WM_PAINT 消息。BeginPaint 会自动把无效区域的矩形信息填充到 PAINTSTRUCT 结构的 rcPaint 字段里你的绘图操作应该限制在这个区域内这样效率最高。更重要的是BeginPaint 会自动验证无效区域。什么叫验证就是告诉 Windows这个区域我已经画好了不需要再发 WM_PAINT 了。如果你在 WM_PAINT 里不用 BeginPaint/EndPaint而是用 GetDC/ReleaseDC无效区域就永远不会被验证Windows 会一直发送 WM_PAINT 消息导致你的程序陷入无限重绘循环CPU 飙升。⚠️ 注意千万别在 WM_PAINT 以外的地方调用 BeginPaint。BeginPaint 的实现依赖于 WM_PAINT 消息的内部机制在其他消息里调用会导致未定义行为。如果你在非 WM_PAINT 消息里需要绘图用 GetDC/ReleaseDC。还有一个常见误区BeginPaint 返回的 HDC 可以缓存起来后续使用。不行BeginPaint 返回的 HDC 只在 BeginPaint 和 EndPaint 之间有效一旦调用了 EndPaint这个 HDC 就失效了。你必须在每次处理 WM_PAINT 时重新获取。GetDC/ReleaseDC随时获取当你需要在非 WM_PAINT 消息里绘图时应该使用 GetDC/ReleaseDC。这对函数的适用范围更广可以随时调用不限于特定消息。// 在任何地方都可以调用HDC hdcGetDC(hwnd);// 绘图操作Ellipse(hdc,10,10,100,100);// 必须配对调用ReleaseDC(hwnd,hdc);GetDC 返回的 HDC 覆盖整个窗口客户区不像 BeginPaint 只覆盖无效区域。这意味着你可以用 GetDC 在窗口的任意位置绘图不受无效区域的限制。但这也意味着你需要注意坐标计算确保画在你想要的位置。与 BeginPaint 不同GetDC/ReleaseDC 不会影响无效区域的验证。你在 WM_PAINT 里用 GetDC 绘图后无效区域仍然存在Windows 会继续发送 WM_PAINT 消息。所以在 WM_PAINT 里必须用 BeginPaint/EndPaint这是铁律。GetDC 有个兄弟叫 GetWindowDC我们稍后会讲到。GetDC 还有个变体是 GetDCEx可以指定更多选项比如是否包含子窗口、是否拦截绘图操作等。但对于大多数场景普通的 GetDC 就够用了。⚠️ 注意GetDC 和 ReleaseDC 必须成对调用而且必须在同一线程内配对。你不能在一个线程里 GetDC然后在另一个线程里 ReleaseDC。这会导致引用计数混乱最终造成资源泄漏。频繁调用 GetDC/ReleaseDC 会影响性能因为每次调用都需要内核介入。如果你需要在短时间内多次绘图可以考虑缓存 HDC但缓存时要非常小心——窗口销毁前必须释放而且窗口大小改变后缓存的 HDC 可能失效。GetWindowDC包含非客户区GetWindowDC 与 GetDC 类似但它返回的 HDC 覆盖整个窗口包括非客户区标题栏、边框、菜单栏等。这意味着你可以在窗口的标题栏上画东西或者实现自定义的窗口边框。HDC hdcGetWindowDC(hwnd);// 可以在整个窗口区域绘图包括标题栏TextOut(hdc,10,5,LCustom Title,13);ReleaseDC(hwnd,hdc);这个功能在某些特殊场景下很有用比如你想实现一个自定义标题栏或者在窗口边框上画一些装饰。但大多数情况下你不需要操作非客户区GetDC 就足够了。⚠️ 注意在非客户区绘图时要非常小心。Windows 有自己的非客户区绘制逻辑你的自定义绘图可能会与系统的绘制冲突。而且非客户区的尺寸和样式在不同 Windows 版本上可能有差异你的程序需要做好兼容性测试。GetWindowDC 返回的 HDC 也用 ReleaseDC 释放而不是 DeleteDC 或 EndPaint。这点和 GetDC 一样它们获取的都是借来的DC用完要还。CreateCompatibleDC内存 DC这是最特殊的一个获取方式。CreateCompatibleDC 创建的是一个内存设备上下文它不对应任何真实设备而是在内存中创建一个虚拟画布。内存 DC 主要用于离屏绘图和双缓冲技术这是实现平滑动画的基础。// 获取窗口 DC 作为参考HDC hdcWindowGetDC(hwnd);// 创建兼容的内存 DCHDC hdcMemCreateCompatibleDC(hdcWindow);// 创建一个位图并选入内存 DCHBITMAP hbmMemCreateCompatibleBitmap(hdcWindow,400,300);HBITMAP hbmOld(HBITMAP)SelectObject(hdcMem,hbmMem);// 现在可以在内存 DC 上绘图了Rectangle(hdcMem,0,0,400,300);TextOut(hdcMem,10,10,LHello Memory DC,15);// 把内存 DC 的内容复制到窗口BitBlt(hdcWindow,0,0,400,300,hdcMem,0,0,SRCCOPY);// 清理SelectObject(hdcMem,hbmOld);DeleteObject(hbmMem);DeleteDC(hdcMem);ReleaseDC(hwnd,hdcWindow);这段代码展示了内存 DC 的典型用法。我们先创建一个内存 DC然后在内存里画好所有东西最后一次性复制到屏幕上。这样做的好处是避免闪烁——因为所有绘图操作都在内存中完成用户看到的是最终结果不会看到中间过程。⚠️ 注意新创建的内存 DC 默认只有 1×1 像素的单色位图你必须先选入一个合适尺寸的位图才能正常绘图。这个陷阱让无数新手踩过坑你创建内存 DC 后如果不选入位图画什么都看不见。CreateCompatibleDC 创建的 DC 必须用 DeleteDC 释放不是 ReleaseDC。这是因为它是你创建的不是借用的。而选入的位图在删除 DC 前必须先选出来否则位图会泄漏——这个我们后面会详细讲。第三步——设备上下文状态管理什么是 DC 状态我们在前面提到HDC 内部包含了很多属性当前画笔、画刷、字体、背景色、文本颜色、绘图模式等等。所有这些属性构成了 DC 的当前状态。当你用 SelectObject 选入一个新画笔时DC 的状态就改变了。后续的绘图操作都会使用新画笔。问题来了如果你临时改了一下 DC 状态画完东西后想恢复原来的状态该怎么办一种方法是记住原来的值然后手动恢复。但这很麻烦因为 DC 状态包含很多属性你要全部记住并恢复很不现实。Windows 提供了一个更好的解决方案SaveDC 和 RestoreDC。SaveDC/RestoreDC 的使用SaveDC 会把当前 DC 的状态保存到一个内部栈里RestoreDC 则从栈里恢复之前保存的状态。这样你就可以随时备份当前状态折腾完之后一键恢复。// 保存当前状态intsavedStateSaveDC(hdc);// 修改 DC 状态HPEN hPenCreatePen(PS_SOLID,3,RGB(255,0,0));HPEN hOldPen(HPEN)SelectObject(hdc,hPen);SetTextColor(hdc,RGB(255,255,255));SetBkColor(hdc,RGB(0,0,0));// 绘图操作Rectangle(hdc,10,10,100,100);// 恢复之前保存的状态RestoreDC(hdc,savedState);// 现在 DC 状态已经恢复不需要手动恢复原来的画笔、颜色等DeleteObject(hPen);// 但别忘了删除创建的对象SaveDC 返回一个整数标识这次保存的状态。你可以把它传给 RestoreDC 来恢复这个特定的状态。RestoreDC 有两种调用方式传入正数恢复到指定的保存点传入负数恢复相对位置的状态。传入 -1 表示恢复到最近一次保存的状态最常用。⚠️ 注意SaveDC 和 RestoreDC 必须在同一 DC 上配对调用。你不能在一个 DC 上 SaveDC然后在另一个 DC 上 RestoreDC。这会导致状态栈混乱可能引发未定义行为。状态栈的深度限制SaveDC 使用栈来保存状态那栈的深度有限制吗有的但很大。Windows 保证至少支持 16 层嵌套实际实现通常支持更多。对于大多数应用程序来说这个深度绰绰有余。但如果你写的是深度递归的绘图代码还是要注意不要过度嵌套。一个常见的模式是在函数入口 SaveDC出口 RestoreDC这样函数内部可以随意修改 DC 状态不用担心影响调用者。这种模式在复杂的绘图代码中非常有用可以避免手动管理状态的麻烦。第四步——GDI 对象生命周期SelectObject 的返回值SelectObject 是 GDI 编程中最常用的函数之一但很多新手忽略了它的返回值。SelectObject 选入一个新对象时会返回之前选入的同类型对象。这个返回值非常重要你必须保存它以便后续恢复。HPEN hPenRedCreatePen(PS_SOLID,2,RGB(255,0,0));HPEN hOldPen(HPEN)SelectObject(hdc,hPenRed);// 使用红色画笔绘图Ellipse(hdc,10,10,100,100);// 恢复原来的画笔SelectObject(hdc,hOldPen);// 现在可以安全删除红色画笔了DeleteObject(hPenRed);为什么要这样麻烦因为 GDI 对象在被选入 DC 时不能被删除。如果你选入了红色画笔然后在没恢复的情况下调用 DeleteObject删除操作会失败画笔会泄漏。这是因为 DC 内部还持有这个对象的引用。DeleteObject 的调用时机GDI 对象画笔、画刷、字体、位图等在创建后必须手动删除否则会泄漏。但删除的前提是对象没有被任何 DC 选入。正确的删除时机是先从所有 DC 中选出对象然后删除。// 创建对象HPEN hPenCreatePen(PS_DASH,1,RGB(128,128,128));HBRUSH hBrushCreateSolidBrush(RGB(0,128,255));// 选入 DCHPEN hOldPen(HPEN)SelectObject(hdc,hPen);HBRUSH hOldBrush(HBRUSH)SelectObject(hdc,hBrush);// 绘图Rectangle(hdc,10,10,200,100);// 恢复原始对象SelectObject(hdc,hOldBrush);SelectObject(hdc,hOldPen);// 现在可以安全删除DeleteObject(hBrush);DeleteObject(hPen);对于内存 DC还有一个额外步骤在删除 DC 前必须先选出你选入的位图。内存 DC 创建时自带一个 1×1 的单色位图你在删除 DC 前必须恢复这个原始位图。HDC hdcMemCreateCompatibleDC(hdc);HBITMAP hbmMemCreateCompatibleBitmap(hdc,400,300);HBITMAP hbmOld(HBITMAP)SelectObject(hdcMem,hbmMem);// ... 使用内存 DC ...// 恢复原始位图SelectObject(hdcMem,hbmOld);// 先删除位图DeleteObject(hbmMem);// 再删除 DCDeleteDC(hdcMem);常见资源泄漏陷阱GDI 资源泄漏是 Win32 程序中最隐蔽也最危险的 bug 之一。Windows 对每个进程有 GDI 对象数量限制大约 10,000 个泄漏的对象会累积直到达到上限然后你的程序再也无法创建任何 GDI 对象绘图功能全面崩溃。最常见的问题是忘记调用 DeleteObject。每次 CreatePen、CreateBrush、CreateFont 都要配对 DeleteObject没例外。另一个常见问题是 SelectObject 后忘记恢复就 DeleteObject导致删除失败。还有一种更隐蔽的泄漏在错误处理路径上忘记清理。如果你的函数有多个返回点每个返回点都要确保释放已创建的对象。使用 RAII 风格的包装类可以大大减少这类问题。// RAII 风格的 GDI 对象包装classGDIObjectGuard{public:GDIObjectGuard(HDC hdc,HGDIOBJ obj):m_hdc(hdc),m_obj(obj){}~GDIObjectGuard(){if(m_obj)DeleteObject(m_obj);}operatorHGDIOBJ(){returnm_obj;}private:HDC m_hdc;HGDIOBJ m_obj;};// 使用示例voidDrawSomething(HDC hdc){HPEN hPenCreatePen(PS_SOLID,2,RGB(255,0,0));GDIObjectGuardpenGuard(hdc,hPen);// 自动管理生命周期HPEN hOldPen(HPEN)SelectObject(hdc,hPen);// ... 绘图操作 ...SelectObject(hdc,hOldPen);// 函数退出时自动调用 DeleteObject}第五步——完整示例一个简单的绘图程序我们来看一个完整的示例把今天讲的知识都用上。这个程序会在窗口上画一些图形展示基本的 GDI 绘图操作。#ifndefUNICODE#defineUNICODE#endif#includewindows.hLRESULT CALLBACKWindowProc(HWND hwnd,UINT uMsg,WPARAM wParam,LPARAM lParam);intWINAPIwWinMain(HINSTANCE hInstance,HINSTANCE hPrevInstance,PWSTR pCmdLine,intnCmdShow){constwchar_tCLASS_NAME[]LGDI Drawing Window;WNDCLASS wc{};wc.lpfnWndProcWindowProc;wc.hInstancehInstance;wc.lpszClassNameCLASS_NAME;wc.hbrBackground(HBRUSH)GetStockObject(WHITE_BRUSH);wc.hCursorLoadCursor(NULL,IDC_ARROW);RegisterClass(wc);HWND hwndCreateWindowEx(0,CLASS_NAME,LGDI Drawing Demo,WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,CW_USEDEFAULT,800,600,NULL,NULL,hInstance,NULL);if(!hwnd)return0;ShowWindow(hwnd,nCmdShow);UpdateWindow(hwnd);MSG msg{};while(GetMessage(msg,NULL,0,0)){TranslateMessage(msg);DispatchMessage(msg);}return0;}LRESULT CALLBACKWindowProc(HWND hwnd,UINT uMsg,WPARAM wParam,LPARAM lParam){switch(uMsg){caseWM_PAINT:{PAINTSTRUCT ps;HDC hdcBeginPaint(hwnd,ps);// 保存 DC 状态intsavedDCSaveDC(hdc);// 创建并选入红色画笔HPEN hPenRedCreatePen(PS_SOLID,3,RGB(255,0,0));HPEN hOldPen(HPEN)SelectObject(hdc,hPenRed);// 创建并选入蓝色画刷HBRUSH hBrushBlueCreateSolidBrush(RGB(0,0,255));HBRUSH hOldBrush(HBRUSH)SelectObject(hdc,hBrushBlue);// 画矩形Rectangle(hdc,50,50,250,150);// 画椭圆HBRUSH hBrushGreenCreateSolidBrush(RGB(0,255,0));SelectObject(hdc,hBrushGreen);Ellipse(hdc,300,50,500,150);DeleteObject(hBrushGreen);// 画文字SetTextColor(hdc,RGB(128,0,128));SetBkMode(hdc,TRANSPARENT);TextOut(hdc,50,200,LHello GDI!,10);// 恢复 DC 状态RestoreDC(hdc,savedDC);// 删除创建的对象恢复后已安全DeleteObject(hBrushBlue);DeleteObject(hPenRed);EndPaint(hwnd,ps);return0;}caseWM_LBUTTONDOWN:{// 响应鼠标点击触发重绘InvalidateRect(hwnd,NULL,TRUE);return0;}caseWM_DESTROY:PostQuitMessage(0);return0;}returnDefWindowProc(hwnd,uMsg,wParam,lParam);}编译运行这个程序你会看到一个窗口里面画着一个蓝色填充的矩形、一个绿色填充的椭圆还有一行紫色的文字。每次你在窗口上点击鼠标左键窗口会重新绘制。这个示例展示了几个关键点在 WM_PAINT 中使用 BeginPaint/EndPaint、正确保存和恢复 DC 状态、创建和删除 GDI 对象、使用 InvalidateRect 触发重绘。这些都是 GDI 编程的基础模式。第六步——调试技巧如何检测 GDI 泄漏GDI 泄漏很难发现因为初期没有任何症状直到对象数量累积到临界值才会爆发。我们需要一些工具和技巧来及早发现问题。任务监控器最简单的检测工具是 Windows 任务监控器。打开任务监控器在详细信息选项卡右键列标题选择选择列勾选GDI 对象。现在你可以看到每个进程的 GDI 对象数量。如果你的程序的 GDI 对象数量持续增长且不随操作回落那很可能就是泄漏了。正常情况下GDI 对象数量应该在一定范围内波动不会无限增长。GDI 对象计数在代码里你可以用 GetGuiResources 函数查询当前进程的 GDI 对象数量DWORD gdiCountGetGuiResources(GetCurrentProcess(),GR_GDIOBJECTS);DWORD userCountGetGuiResources(GetCurrentProcess(),GR_USEROBJECTS);wchar_tmsg[256];swprintf_s(msg,LGDI Objects: %u\nUser Objects: %u,gdiCount,userCount);OutputDebugString(msg);在关键操作前后调用这个函数可以看到对象数量的变化。如果你创建了一个对象数量应该加 1删除后应该减 1。如果只增不减就是泄漏了。Application Verifier对于更复杂的场景可以使用 Windows Application Verifier。这是一个微软提供的调试工具可以检测各种资源泄漏包括 GDI 对象。启用 GDI 泄漏检测后Application Verifier 会在程序退出时报告所有未释放的 GDI 对象及其分配调用栈。代码审查除了工具代码审查也很重要。几个常见检查点每次 Create 都有对应的 Delete、每个 SelectObject 的返回值都保存并恢复、BeginPaint/EndPaint 和 GetDC/ReleaseDC 正确配对、内存 DC 的位图正确恢复和删除。后续可以做什么到这里GDI 设备上下文的基础知识就讲完了。你现在应该能够理解 HDC 的本质知道在什么场景下用什么方式获取 HDC掌握状态管理和 GDI 对象生命周期的基本规则。但 GDI 的世界远不止这些还有很多高级主题等着我们去探索。下一篇文章我们会继续深入GDI 绘图 API——学习各种绘图函数的具体用法包括画线、画矩形、画椭圆、画多边形以及文本输出和位图操作。我们还会介绍双缓冲技术这是实现平滑动画的关键。在此之前建议你先把今天的内容消化一下。写一些小练习巩固一下知识修改示例程序让它在不同的位置绘制不同的图形实现一个简单的绘图程序让用户用鼠标在窗口上画图使用双缓冲技术消除重绘时的闪烁用任务监控器观察你的程序的 GDI 对象数量确保没有泄漏这些练习看似简单但能帮你把今天学到的知识真正变成自己的东西。特别是最后一个养成监控 GDI 对象数量的习惯能帮你避免很多难以排查的问题。好了今天的文章就到这里我们下一篇再见相关资源Device Contexts - Win32 apps | Microsoft LearnSaving, Restoring, and Resetting a Device Context - Win32 apps | Microsoft LearnWindows GDI - Win32 apps | Microsoft LearnGDI Objects - Win32 apps | Microsoft LearnBeginPaint function (Winuser.h) - Win32 apps | Microsoft LearnGetDC function (Winuser.h) - Win32 apps | Microsoft LearnSelectObject function (wingdi.h) - Win32 apps | Microsoft Learn

相关文章:

通用GUI编程技术——Win32 原生编程实战(十八)——GDI 设备上下文(HDC)完全指南

通用GUI编程技术——Win32 原生编程实战(十八)——GDI 设备上下文(HDC)完全指南 前面一系列文章我们聊了对话框、控件、资源这些内容,我们的窗口已经能够显示各种控件了。但你可能已经发现了一个问题:我们所…...

IDEA 2023.3 配置 JavaWeb 项目完整流程:从新建到打包 War 的保姆级避坑指南

IDEA 2023.3 配置 JavaWeb 项目完整流程:从新建到打包 War 的保姆级避坑指南 作为一名长期使用 IntelliJ IDEA 进行 JavaWeb 开发的工程师,我深知在配置项目时可能遇到的各种"坑"。特别是对于刚接触 IDEA 的新手来说,从项目创建到最…...

OpenSpec 生成文件说明

proposal.md —— 为什么做、做什么(产品/范围) Why:要解决什么问题、机会是什么。What Changes:会新增/改掉/删掉哪些能力,有没有 BREAKING。Capabilities:会动到哪些能力名(对应后面 specs/&l…...

电子小白之二极管

很多年前我第一次看到电路图上各种二极管符号时,心里只有一个想法:这玩意儿到底干嘛用的?硬件部门同事告诉我一句话,瞬间就通了: 正向导通,反向截止;整流防反,稳压发光。 今天就用最…...

云服务器购买怎么选?2026云服务器优惠与租赁指南

在AI创作、3D渲染、远程办公快速发展的今天,「云服务器购买」「云服务器租赁」已经成为越来越多个人和企业的刚需。但面对复杂的配置和价格体系,很多人都会问:👉 到底怎么选最划算? 👉 有没有长期稳定又有“…...

DBA_RECYCLEBIN purge指定日期前的表

SummaryHow to purge DBA_RECYCLBIN for objects older than x days/minutes? or do we have RECYCLEBIN RETENTION feature or truncate recyclebin ?--------------------------------------------------------------------------------------DBA_RECYCLEBIN has a column …...

AI 模型推理框架性能分析与对比

AI模型推理框架性能分析与对比 随着人工智能技术的快速发展,AI模型推理框架成为支撑各类应用落地的核心工具。无论是计算机视觉、自然语言处理还是推荐系统,高效的推理框架直接影响模型的响应速度、资源占用和部署成本。本文将从多个维度对比主流AI推理…...

Go语言的context.WithCancel取消信号传播与资源清理在分布式系统中的协调

Go语言的context.WithCancel取消信号传播与资源清理在分布式系统中的协调 在分布式系统中,任务的取消与资源清理是确保系统稳定性和高效性的关键挑战。Go语言通过context包提供了优雅的解决方案,尤其是context.WithCancel机制,能够实现跨组件…...

MxRadioRF2xx库:ARM Mbed平台RF2xx射频驱动开发指南

1. MxRadioRF2xx 库概述 MxRadioRF2xx 是一个专为 ARM Mbed OS 平台设计的 Atmel(现 Microchip)RF2xx 系列射频收发器驱动库。该库并非对底层寄存器操作的简单封装,而是面向嵌入式无线应用开发者的工程化抽象层,其核心目标是&…...

AIGC时代,程序员会被取代吗?我的看法与行动建议

AIGC时代,程序员会被取代吗?我的看法与行动建议 随着AI生成内容(AIGC)技术的迅猛发展,许多人开始担忧:程序员这一职业是否会被AI取代?从代码生成工具GitHub Copilot到对话式编程助手ChatGPT&am…...

深度学习中的优化器:原理与实践

深度学习中的优化器:原理与实践 一、背景与动机 在深度学习中,优化器是模型训练的核心组件,它决定了模型参数如何根据损失函数的梯度进行更新。选择合适的优化器对于模型的训练速度和最终性能至关重要。本文将深入探讨各种优化器的核心原理、…...

深度解析Internet Archive下载器:数字图书馆资源获取的完整方案

深度解析Internet Archive下载器:数字图书馆资源获取的完整方案 【免费下载链接】internet_archive_downloader A chrome/firefox extension that download books from Internet Archive(archive.org) and HathiTrust Digital Library (hathitrust.org) 项目地址:…...

feishu2md:飞书文档批量下载与Markdown转换解决方案

feishu2md:飞书文档批量下载与Markdown转换解决方案 【免费下载链接】feishu2md 一键命令下载飞书文档为 Markdown 项目地址: https://gitcode.com/gh_mirrors/fe/feishu2md 在团队协作和知识管理场景中,飞书文档已成为许多组织的核心工具。然而&…...

C++的std--ranges算法自定义比较器与等价关系在集合操作中的运用

C20引入的std::ranges库为算法操作带来了革命性改进,其中自定义比较器与等价关系的灵活运用,显著提升了集合操作的表达能力。通过精确控制元素间的比较逻辑,开发者能够实现更复杂的业务需求,例如处理自定义对象集合或实现非标准排…...

OpenClaw操作录制:ollama-QwQ-32B学习人工流程生成自动化脚本

OpenClaw操作录制:ollama-QwQ-32B学习人工流程生成自动化脚本 1. 为什么需要操作录制功能 上周我在整理月度运营报告时,突然意识到自己正在重复第7次执行完全相同的操作流程:打开三个数据源表格→复制特定列→粘贴到汇总表→生成折线图→导…...

LangChain4j vs Spring AI:Java AI 框架技术选型深度对比与生产落地指南

LangChain4j vs Spring AI:Java AI 框架技术选型深度对比与生产落地指南 摘要:当 Java 团队建设 AI 应用时,真正困难的通常不是“能否调通模型”,而是“如何把 Prompt、RAG、工具调用、可观测性、限流熔断、灰度发布、权限隔离与业务系统稳定地耦合起来”。本文不再停留在 …...

会用AI的人,早已拉开职场差距!全岗位工作范式重构进行时

AI深度融入职场,正在改写工作的底层逻辑,会用AI的从业者,已在工作效率与职业发展上形成明显优势。从开发人员的研发流程,到方案人员的工作模式,再到各行各业的基础岗位,AI不再只是简单的效率工具&#xff0…...

大模型私有化不是选型,是生存!Python工程师必须在Q3前掌握的5类国产化适配方案,否则明年项目全卡审批

第一章:大模型私有化是Python工程师的生存分水岭当企业开始将大语言模型从公有云API转向本地GPU集群部署,Python工程师的角色正经历一次静默但深刻的重构——不再只是调用requests.post()封装接口,而是要亲手构建模型加载、推理服务、权限控制…...

中国AI模型调用量领跑全球:成本与开源优势塑造竞争新范式

当前,全球人工智能(AI)领域的竞争正经历着深刻变革。据全球最大AI模型API聚合平台OpenRouter的最新监测数据,中国AI大模型的周调用量已连续数周实现对美国的稳定且显著的超越,并在特定时期内包揽了全球调用量排行榜的前…...

从‘偏差-方差’到一行代码:用NumPy/PyTorch五步实现GAE,附PPO实战避坑点

从‘偏差-方差’到一行代码:用NumPy/PyTorch五步实现GAE,附PPO实战避坑点 强化学习中的策略优化常常面临一个核心挑战:如何准确评估动作的价值?广义优势估计(GAE)通过巧妙平衡偏差与方差,成为PP…...

Mojo+Python混合部署案例深度拆解(从Jupyter到生产环境的无缝迁移全路径)

第一章:MojoPython混合部署案例深度拆解(从Jupyter到生产环境的无缝迁移全路径)Mojo 作为新兴的系统级编程语言,与 Python 生态天然兼容,为机器学习模型从探索性开发(Jupyter Notebook)迈向高吞…...

基于训练RBF神经网络的车速信息时序预测Matlab模型

✅作者简介:热爱科研的Matlab仿真开发者,擅长毕业设计辅导、数学建模、数据处理、建模仿真、程序设计、完整代码获取、论文复现及科研仿真。🍎 往期回顾关注个人主页:Matlab科研工作室👇 关注我领取海量matlab电子书和…...

WWW-万维网

万维网的概念与组成结构万维网(World Wide Web,WWW)是一个分布式的信息存储空间,在这个空间中:一个事物被称为一样 “资源”,并由一个全域 “统一资源定位符”(URL)标识。这些资源通…...

语音播报实时

目录 GPT-SoVITS(强烈推荐) Fish Speech-1.5 GPT-SoVITS(强烈推荐) RVC-Boss/GPT-SoVITS: 1 min voice data can also be used to train a good TTS model! (few shot voice cloning) Fish Speech-1.5 追求极致流畅的实时对话&a…...

从C语言到裸机运行:i.MX6ULL 的 GPIO 控制与编译链接过程分析

引言在嵌入式系统开发中,从高级语言到硬件控制的完整链路涉及编译、链接、寄存器配置等多个环节。本文基于 i.MX6ULL 平台,以 C 语言实现 LED 与蜂鸣器控制为例,系统分析 ARM 裸机开发中的编译工具链使用、链接脚本的作用,以及 GP…...

STM32实现智能酒驾监测系统设计

基于STM32的酒后驾车监测报警系统设计与实现1. 项目概述1.1 系统背景酒后驾车是全球交通事故的主要诱因之一,传统的人工检测方法存在效率低、覆盖范围有限等问题。随着嵌入式系统和物联网技术的发展,智能化的酒精监测系统成为解决这一问题的有效方案。1.…...

2026年3月27日NSSCTF之[SWPUCTF 2021 新生赛]ez_unserialize

[SWPUCTF 2021 新生赛]ez_unserialize 开启环境,进入并查看,可以看到一个动图,选择查看网页源码,得到 看到有隐藏信息,根据隐藏信息可以猜测,可以利用robots协议查看相关信息,访问得到 可以得…...

OpenClaw自动化测试:Qwen3.5-9B在API接口校验中的实战应用

OpenClaw自动化测试:Qwen3.5-9B在API接口校验中的实战应用 1. 为什么选择OpenClaw做接口自动化测试 去年接手一个个人项目时,我遇到了接口测试的痛点:每次后端更新都要手动验证几十个API,不仅耗时还容易遗漏边缘case。尝试过Pos…...

从拼图游戏到自动驾驶:点云配准技术的跨领域进化史

从拼图游戏到自动驾驶:点云配准技术的跨领域进化史 1. 三维世界的数字拼图师 1987年,当Paul Besl和Neil McKay在实验室里尝试将两组扫描数据对齐时,他们可能不会想到,这项被称为迭代最近点(ICP)的技术会成为…...

一本计算机专业,准大一,有什么忠告?

你现在大概处于一种很特别的状态。高考刚结束不久,录取通知书拿到了,专业是计算机。可能是你自己选的,也可能是家里建议的,也可能是分数刚好够就填了。不管哪种,你现在对”计算机专业到底学什么”的理解大概率是模糊的…...