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

news2026/4/19 8:46:43
通用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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2456318.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

SpringBoot-17-MyBatis动态SQL标签之常用标签

文章目录 1 代码1.1 实体User.java1.2 接口UserMapper.java1.3 映射UserMapper.xml1.3.1 标签if1.3.2 标签if和where1.3.3 标签choose和when和otherwise1.4 UserController.java2 常用动态SQL标签2.1 标签set2.1.1 UserMapper.java2.1.2 UserMapper.xml2.1.3 UserController.ja…

wordpress后台更新后 前端没变化的解决方法

使用siteground主机的wordpress网站,会出现更新了网站内容和修改了php模板文件、js文件、css文件、图片文件后,网站没有变化的情况。 不熟悉siteground主机的新手,遇到这个问题,就很抓狂,明明是哪都没操作错误&#x…

网络编程(Modbus进阶)

思维导图 Modbus RTU(先学一点理论) 概念 Modbus RTU 是工业自动化领域 最广泛应用的串行通信协议,由 Modicon 公司(现施耐德电气)于 1979 年推出。它以 高效率、强健性、易实现的特点成为工业控制系统的通信标准。 包…

UE5 学习系列(二)用户操作界面及介绍

这篇博客是 UE5 学习系列博客的第二篇,在第一篇的基础上展开这篇内容。博客参考的 B 站视频资料和第一篇的链接如下: 【Note】:如果你已经完成安装等操作,可以只执行第一篇博客中 2. 新建一个空白游戏项目 章节操作,重…

IDEA运行Tomcat出现乱码问题解决汇总

最近正值期末周,有很多同学在写期末Java web作业时,运行tomcat出现乱码问题,经过多次解决与研究,我做了如下整理: 原因: IDEA本身编码与tomcat的编码与Windows编码不同导致,Windows 系统控制台…

利用最小二乘法找圆心和半径

#include <iostream> #include <vector> #include <cmath> #include <Eigen/Dense> // 需安装Eigen库用于矩阵运算 // 定义点结构 struct Point { double x, y; Point(double x_, double y_) : x(x_), y(y_) {} }; // 最小二乘法求圆心和半径 …

使用docker在3台服务器上搭建基于redis 6.x的一主两从三台均是哨兵模式

一、环境及版本说明 如果服务器已经安装了docker,则忽略此步骤,如果没有安装,则可以按照一下方式安装: 1. 在线安装(有互联网环境): 请看我这篇文章 传送阵>> 点我查看 2. 离线安装(内网环境):请看我这篇文章 传送阵>> 点我查看 说明&#xff1a;假设每台服务器已…

XML Group端口详解

在XML数据映射过程中&#xff0c;经常需要对数据进行分组聚合操作。例如&#xff0c;当处理包含多个物料明细的XML文件时&#xff0c;可能需要将相同物料号的明细归为一组&#xff0c;或对相同物料号的数量进行求和计算。传统实现方式通常需要编写脚本代码&#xff0c;增加了开…

LBE-LEX系列工业语音播放器|预警播报器|喇叭蜂鸣器的上位机配置操作说明

LBE-LEX系列工业语音播放器|预警播报器|喇叭蜂鸣器专为工业环境精心打造&#xff0c;完美适配AGV和无人叉车。同时&#xff0c;集成以太网与语音合成技术&#xff0c;为各类高级系统&#xff08;如MES、调度系统、库位管理、立库等&#xff09;提供高效便捷的语音交互体验。 L…

(LeetCode 每日一题) 3442. 奇偶频次间的最大差值 I (哈希、字符串)

题目&#xff1a;3442. 奇偶频次间的最大差值 I 思路 &#xff1a;哈希&#xff0c;时间复杂度0(n)。 用哈希表来记录每个字符串中字符的分布情况&#xff0c;哈希表这里用数组即可实现。 C版本&#xff1a; class Solution { public:int maxDifference(string s) {int a[26]…

【大模型RAG】拍照搜题技术架构速览:三层管道、两级检索、兜底大模型

摘要 拍照搜题系统采用“三层管道&#xff08;多模态 OCR → 语义检索 → 答案渲染&#xff09;、两级检索&#xff08;倒排 BM25 向量 HNSW&#xff09;并以大语言模型兜底”的整体框架&#xff1a; 多模态 OCR 层 将题目图片经过超分、去噪、倾斜校正后&#xff0c;分别用…

【Axure高保真原型】引导弹窗

今天和大家中分享引导弹窗的原型模板&#xff0c;载入页面后&#xff0c;会显示引导弹窗&#xff0c;适用于引导用户使用页面&#xff0c;点击完成后&#xff0c;会显示下一个引导弹窗&#xff0c;直至最后一个引导弹窗完成后进入首页。具体效果可以点击下方视频观看或打开下方…

接口测试中缓存处理策略

在接口测试中&#xff0c;缓存处理策略是一个关键环节&#xff0c;直接影响测试结果的准确性和可靠性。合理的缓存处理策略能够确保测试环境的一致性&#xff0c;避免因缓存数据导致的测试偏差。以下是接口测试中常见的缓存处理策略及其详细说明&#xff1a; 一、缓存处理的核…

龙虎榜——20250610

上证指数放量收阴线&#xff0c;个股多数下跌&#xff0c;盘中受消息影响大幅波动。 深证指数放量收阴线形成顶分型&#xff0c;指数短线有调整的需求&#xff0c;大概需要一两天。 2025年6月10日龙虎榜行业方向分析 1. 金融科技 代表标的&#xff1a;御银股份、雄帝科技 驱动…

观成科技:隐蔽隧道工具Ligolo-ng加密流量分析

1.工具介绍 Ligolo-ng是一款由go编写的高效隧道工具&#xff0c;该工具基于TUN接口实现其功能&#xff0c;利用反向TCP/TLS连接建立一条隐蔽的通信信道&#xff0c;支持使用Let’s Encrypt自动生成证书。Ligolo-ng的通信隐蔽性体现在其支持多种连接方式&#xff0c;适应复杂网…

铭豹扩展坞 USB转网口 突然无法识别解决方法

当 USB 转网口扩展坞在一台笔记本上无法识别,但在其他电脑上正常工作时,问题通常出在笔记本自身或其与扩展坞的兼容性上。以下是系统化的定位思路和排查步骤,帮助你快速找到故障原因: 背景: 一个M-pard(铭豹)扩展坞的网卡突然无法识别了,扩展出来的三个USB接口正常。…

未来机器人的大脑:如何用神经网络模拟器实现更智能的决策?

编辑&#xff1a;陈萍萍的公主一点人工一点智能 未来机器人的大脑&#xff1a;如何用神经网络模拟器实现更智能的决策&#xff1f;RWM通过双自回归机制有效解决了复合误差、部分可观测性和随机动力学等关键挑战&#xff0c;在不依赖领域特定归纳偏见的条件下实现了卓越的预测准…

Linux应用开发之网络套接字编程(实例篇)

服务端与客户端单连接 服务端代码 #include <sys/socket.h> #include <sys/types.h> #include <netinet/in.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <arpa/inet.h> #include <pthread.h> …

华为云AI开发平台ModelArts

华为云ModelArts&#xff1a;重塑AI开发流程的“智能引擎”与“创新加速器”&#xff01; 在人工智能浪潮席卷全球的2025年&#xff0c;企业拥抱AI的意愿空前高涨&#xff0c;但技术门槛高、流程复杂、资源投入巨大的现实&#xff0c;却让许多创新构想止步于实验室。数据科学家…

深度学习在微纳光子学中的应用

深度学习在微纳光子学中的主要应用方向 深度学习与微纳光子学的结合主要集中在以下几个方向&#xff1a; 逆向设计 通过神经网络快速预测微纳结构的光学响应&#xff0c;替代传统耗时的数值模拟方法。例如设计超表面、光子晶体等结构。 特征提取与优化 从复杂的光学数据中自…