news 2026/10/18 3:40:14

Win32 GDI绘制实战:从设备上下文到双缓冲,彻底告别界面闪烁

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Win32 GDI绘制实战:从设备上下文到双缓冲,彻底告别界面闪烁

Win32 GDI绘制这话题,放现在看好像有点老古董,但实际做桌面工具、工业上位机界面、自绘控件的时候,你绕不开它。很多时候产品经理甩过来一个需求:要一个实时曲线图、要一个不闪烁的自定义列表、要一个能局部刷新的仪表盘。你用框架自带控件做不出来,或者做出来卡成PPT,最后还得亲自下场调GDI。这套东西虽然API年迈,但逻辑清晰、上手快、可控性强,而且很多现代框架的渲染底层思路也从这里演化来的。这篇就把我这些年用Win32 GDI画界面、处理重绘、搞性能优化的经验拆开讲,代码都是实际项目里验证过的,照着改就能用。

1. GDI的核心设计思路:先搞懂这几个基础概念

1.1 设备上下文(DC):一切的起点

GDI里什么事都离不开设备上下文,也就是常说的DC。理解它最简单的方式就是把它当成一张"虚拟画布",所有绘制操作——画线、填色、写字、贴图——都必须在这张画布上进行。这张画布背后可能是屏幕窗口、也可能是内存中的一块位图,甚至是一台打印机。你不需要关心它具体怎么工作,只要拿到一个DC句柄就能在上面动手。

HDC hdc = GetDC(hwnd); // 拿到窗口DC // 这里执行各种绘制操作 ReleaseDC(hwnd, hdc); // 用完必须释放

这只是最基础的窗口DC。如果在WM_PAINT消息里绘制,正确姿势是用BeginPaint拿到DC,它跟GetDC有点区别:BeginPaint会正确设置裁剪区域,让绘制范围只落在无效区域内,而且会隐藏插入符,绘制完用EndPaint恢复。这两个函数的组合是Windows为消息驱动的重绘机制专门设计的,实际写代码的时候不要图省事绕开。

case WM_PAINT: { PAINTSTRUCT ps; HDC hdc = BeginPaint(hwnd, &ps); // 实际绘制代码 EndPaint(hwnd, &ps); return 0; }

对于内存DC,用CreateCompatibleDC创建,它一开始只有1x1像素大小,必须配合CreateCompatibleBitmap生成一块真正的可绘制内存区域。这块"画布"就是双缓冲的核心道具。很多人第一次接触GDI时被这些概念绕晕,其实记住了:DC代表"在哪里画",GDI对象代表"用什么画",两者分开理解就顺了。

1.2 逻辑坐标与映射模式

GDI绘制默认是MM_TEXT映射模式,坐标单位就是像素,原点在窗口客户区左上角,x向右增加、y向下增加。这个模式最直观,90%的界面绘制都用它。但某些场景下你会想换映射模式,比如画工程图纸、波形图,用MM_ISOTROPIC或MM_ANISOTROPIC可以让逻辑单位跟物理尺寸挂钩,窗口缩放时图形自动跟着缩放。

SetMapMode(hdc, MM_ISOTROPIC); SetWindowExtEx(hdc, 200, 100, NULL); SetViewportExtEx(hdc, 200, -100, NULL); SetViewportOrgEx(hdc, 0, 100, NULL);

这段代码的效果是:逻辑坐标原点到(200,100),y轴向上为正,画出来的图形不需要手动做坐标换算了。我在一个示波器模拟项目里就是这么做的,数据坐标直接扔给GDI,它自己处理缩放和方向。但要注意MM_ISOTROPIC模式下,GDI会强制调整viewport范围,让一个逻辑单位x和y对应的物理距离相同,如果窗口宽高比例不是1:1,图形四周会出现空白区域。这种模式的坑在于逻辑坐标和物理坐标混用容易引起混乱,非必要不建议新手直接上手,用默认模式加上自己的坐标变换函数反而更可控。

2. 一个最小可用的绘制框架:从零开始画点东西

2.1 窗口过程与WM_PAINT的协作关系

Windows绘制体系跟传统"画完就扔"的思路不同,它采用消息驱动加"随时可重绘"模型。窗口的内容不是永久保存在屏幕上的,当窗口被遮挡、拉伸、滚动时,系统会向窗口发送WM_PAINT消息,你的代码收到消息后重新绘制一遍内容。所以GDI编程的内核逻辑是:不只画一次,而是准备好"随时能画"的能力。

这意味着绘制代码必须设计成幂等操作——无论调用多少次,效果都一样。比如绘制一个自定义控件,你不能在WM_PAINT里做状态切换、申请资源、建立缓存这种有副作用的事,这些应该放到控件初始化和状态变更的地方。WM_PAINT里只做一件纯粹的事:根据当前状态,把界面画出来。

case WM_PAINT: { PAINTSTRUCT ps; HDC hdc = BeginPaint(hwnd, &ps); DrawDashboard(hdc, GetWidth(hwnd), GetHeight(hwnd)); // 纯绘制函数 EndPaint(hwnd, &ps); return 0; }

这种设计保证了系统随时能触发重绘,也方便你手动刷新。如果状态变了需要更新界面,调用InvalidateRect告诉系统某个区域已经失效,系统会在消息循环里生成WM_PAINT。不要直接调用UpdateWindow或SendMessage发WM_PAINT,除非你明确知道自己在做什么——比如在调试循环里想立刻看到绘制结果。实际项目中,频繁InvalidateRect小区域比一次InvalidateRect大区域更高效,这是经验之谈,后面性能部分细说。

2.2 双缓冲绘制:消除闪烁的必备套路

界面闪烁是GDI绘制新手遇到的第一个大难题。闪烁的根本原因:窗口在擦除背景和绘制前景之间存在"空窗期",背景被抹成白色(或系统默认色),而新内容还没画上去,眼睛看到的就是不停闪烁。更麻烦的是,如果每次重绘都做大量计算,窗口会像放幻灯片一样难受。

双缓冲方案很直接:先在内存里把一帧画好,再一次性地BitBlt到屏幕。这样屏幕上不会出现中间状态,闪烁自然消失。

// 通用双缓冲框架 HDC hdc = BeginPaint(hwnd, &ps); HDC memDC = CreateCompatibleDC(hdc); HBITMAP memBmp = CreateCompatibleBitmap(hdc, width, height); HBITMAP oldBmp = (HBITMAP)SelectObject(memDC, memBmp); // 在memDC上完成所有绘制 FillRect(memDC, &rect, (HBRUSH)GetStockObject(WHITE_BRUSH)); // ... 绘制曲线、文字、图形 ... // 一次性拷贝到窗口 BitBlt(hdc, 0, 0, width, height, memDC, 0, 0, SRCCOPY); SelectObject(memDC, oldBmp); DeleteObject(memBmp); DeleteDC(memDC); EndPaint(hwnd, &ps);

这里有两个细节值得注意。第一,CreateCompatibleBitmap的宽高必须跟客户区一致,如果不一致,BitBlt时要么拉伸要么裁切,容易出边界问题。第二,每次WM_PAINT都重新创建和释放内存DC、位图,在绘制频率低的情况下完全没问题,但如果每秒钟刷新几十帧,建议把位图缓存成成员变量,避免频繁的创建销毁开销。我在做实时曲线控件时就把memDC和memBmp做成缓存,窗口尺寸变了才重建,绘制帧率明显提升。

除了双缓冲,还应该处理WM_ERASEBKGND消息。默认情况下窗口在WM_PAINT之前会擦除背景,这个擦除动作是闪烁的一个独立来源。既然双缓冲已经把整个客户区重新画了一遍,背景擦除就多余了。

case WM_ERASEBKGND: return 1; // 阻止默认擦除,背景由双缓冲内容完全覆盖

返回1告诉系统"背景已经处理好了",系统就不会再刷白底。这个技巧配合双缓冲是消除闪烁的完整方案。如果控件只占窗口一部分,还有更精细的做法:只对控件区域执行BitBlt,避免覆盖整个窗口导致其他区域额外重绘。

3. 实际开发中最常用的绘制操作

3.1 画笔与画刷:线条和填充的正确打开方式

GDI里画笔(Pen)决定线条怎么画,画刷(Brush)决定区域怎么填充。很多人刚接触时会迷惑:为什么画一个矩形要有两个GDI对象?因为矩形边框和内部填充是独立控制的,这样设计带来很强的灵活性——边框用虚线,内部用斜纹填充,这种组合在数据可视化里很常见。

画笔创建常用CreatePen或ExtCreatePen:

HPEN hPen = CreatePen(PS_SOLID, 2, RGB(255, 0, 0)); HPEN oldPen = (HPEN)SelectObject(hdc, hPen); // 画边框 Rectangle(hdc, 10, 10, 200, 100); SelectObject(hdc, oldPen); DeleteObject(hPen);

PS_SOLID是实线,还有PS_DASH、PS_DOT等样式。注意PS_DASH和PS_DOT在画笔宽度大于1时会被系统自动转换成实线,因为Windows认为粗虚线的视觉意义不大。PS_SOLID配合不同的宽度和颜色已经覆盖90%需求。画刷创建常用CreateSolidBrush或CreateHatchBrush:

HBRUSH hBrush = CreateSolidBrush(RGB(0, 120, 215)); HBRUSH oldBrush = (HBRUSH)SelectObject(hdc, hBrush); FillRect(hdc, &rect, hBrush); SelectObject(hdc, oldBrush); DeleteObject(hBrush);

这里最容易踩的坑是资源管理。GDI对象不吃不喝不自动释放,每次CreatePen、CreateBrush都会消耗系统资源池,不DeleteObject就会泄漏。Windows对GDI对象数量有硬性限制(默认每进程约一万个),一旦超过,新的绘制调用会静默失败,或者单个对象变为唯一无效,界面上突然出现画不出来的空白区域。这种bug非常隐蔽,因为运行时不会抛异常。后来我把所有GDI对象封装成RAII类,构造函数创建、析构函数释放,再也没犯过这个错。建议你也这样处理,别指望靠记忆力管好这么多句柄。

还有一个值得说的对象是StockObject。系统内置了一批基础对象:白刷子、黑刷子、灰色刷子、系统字体等。GetStockObject直接拿现成的,不需要创建也绝不要DeleteObject。例如重绘背景时用(HBRUSH)GetStockObject(WHITE_BRUSH)非常方便。能系统给的就不用自己创建,这也是减少资源管理负担的实用技巧。

3.2 文本绘制:字体、抗锯齿与清晰度

GDI文本绘制涉及字体创建、背景模式设置、文本格式控制几个方面,看似简单,实际体验差距很大。我总结出来的经验是:默认的TextOut能跑通流程,但想要好看,得自己控制每个参数。

字体先用CreateFont或CreateFontIndirect创建,然后用SelectObject选入DC。CreateFont参数多,为了维护方便通常先填LOGFONT结构体:

LOGFONT lf = {0}; lf.lfHeight = -20; // 负数表示字符高度,正数表示字符宽度 lf.lfWeight = FW_NORMAL; lf.lfCharSet = DEFAULT_CHARSET; strcpy(lf.lfFaceName, "Microsoft YaHei UI"); HFONT hFont = CreateFontIndirect(&lf); SelectObject(hdc, hFont); SetTextColor(hdc, RGB(51, 51, 51)); SetBkMode(hdc, TRANSPARENT); // 关键:文字背景设为透明

BkMode是最容易被忽略的选项。默认OPAQUE模式会用当前背景色填充文字背后的矩形区域,这会在自绘控件里留下难看的色块。设成TRANSPARENT后文字直接叠加在已有内容上,视觉效果自然得多。代价是文字边缘可能不干净,因为直接叠加时边缘像素颜色混合可能产生毛边。解决办法是确保背景本身是统一颜色,或者干脆用带Alpha通道的绘图API,比如AlphaBlend配合32位位图。

TextOut和DrawText用哪个也有讲究。TextOut就是无脑地在某坐标开始输出字符串,不做格式处理。DrawText则可以做居中、对齐、自动换行、省略号控制:

RECT rc = {20, 20, 220, 60}; DrawText(hdc, "这是一段测试文本", -1, &rc, DT_CENTER | DT_VCENTER | DT_WORD_ELLIPSIS);

DT_WORD_ELLIPSIS在文本超宽时填充省略号,这种效果在列表控件、按钮文字中很实用。还有DT_CALCRECT,它可以只计算文本占用的矩形尺寸而不实际绘制,用来做控件尺寸自适应非常方便。

字体清晰度方面,GDI默认的字体光栅化不带ClearType抗锯齿,在液晶屏上就感觉发虚。对于窗口绘制,在CreateFont时指定lfQuality = CLEARTYPE_QUALITY可以有效改善文字边缘。如果做的是自定义控件,还可以在创建窗口时注册CS_HREDRAW | CS_VREDRAW | CS_DBLCLKS这些样式来管理重绘行为。字体这块要试,不同DPI环境下效果差异很大,不能只看设计器里的预览。

3.3 位图操作:从LoadImage到BitBlt

GDI位图操作是绘制体系里性能最好的部分,因为BitBlt本质上是内存块拷贝,速度极快。实际项目经常用位图做缓存的载体——把复杂的静态内容提前画到位图上,运行时直接贴出来。

加载位图文件用LoadImage最方便:

HBITMAP hBitmap = (HBITMAP)LoadImage(NULL, L"image.bmp", IMAGE_BITMAP, 0, 0, LR_LOADFROMFILE);

IMPORTANT:LoadImage支持bmp、jpg、gif、png(部分依赖系统解码器),但加载后拿到的是HBITMAP,透明度信息在BMP里通常不存在。PNG带Alpha通道,需要先用GDI+加载或者保持DIB格式才能正确处理透明。我用LoadImage加载PNG的教训是界面边角出现黑色方块,就是因为没有处理Alpha通道。后来要么转DIB,要么干脆用GDI+的Bitmap类统一管理。

把位图画到屏幕上用BitBlt或StretchBlt:

BitBlt(hdc, 0, 0, width, height, memDC, 0, 0, SRCCOPY); // 按目标大小缩放 StretchBlt(hdc, 0, 0, dstWidth, dstHeight, memDC, 0, 0, srcWidth, srcHeight, SRCCOPY);

BitBlt不做缩放,屏幕坐标和位图坐标一一对应,速度最快。StretchBlt可以缩放,但默认模式会产生锯齿。可以先SetStretchBltMode(hdc, HALFTONE)获得相对平滑的缩放效果,代价是速度下降。如果想在运行时动态画出带有渐变、圆角、阴影效果的图形,GDI原生能力有限,我通常用GDI+完成复杂绘制,然后渲染到内存DC,再用BitBlt贴出来。这种混合用法的性能比纯GDI+直接绘制到屏幕高不少,窗口的刷新速度和视觉效果都能兼顾。

透明贴图也是个高频需求。经典方案使用TransparentBlt:

TransparentBlt(hdc, x, y, width, height, memDC, srcX, srcY, width, height, RGB(255, 0, 255));

指定的颜色会被视为透明色不参与绘制。这种方案适合纯色背景的图标。如果需要真正的Alpha混合渐变透明效果,就得用AlphaBlend配合32位带Alpha通道的位图了。

4. 性能与资源管理的实战经验

4.1 资源泄漏:GDI对象数量的那条红线

GDI句柄泄漏是Win32绘制长期运行的桌面工具最容易遇到的问题。它的可怕之处在于前期无感,运行几小时后突然整个界面开始"失真"——按钮消失、文字变方块、控件背景变黑。原因很简单:进程的GDI对象句柄达到万级上限,新的分配失败。

排查方法并不复杂。在代码里定期加日志输出当前GDI对象数:

int gdiCount = GetGuiResources(GetCurrentProcess(), GR_GDIOBJECTS); OutputDebugString(("GDI objects: " + std::to_string(gdiCount)).c_str());

如果这个数字持续上升而不是趋于稳定,那一定有泄漏点。最常见的泄漏位置就在前面的CreatePen、CreateBrush、CreateFont调用周围——任何SelectObject加入DC的GDI对象,在不用之后都必须选回旧对象并DeleteObject自己的对象。

我在做仿真项目时就碰到过一次:界面每次刷新创建3个画笔、2个画刷、1个字体,但忘了DeleteObject。运行20分钟就突破了上限,所有的软件渲染全面崩坏。现在还心有余悸,所以强烈建议:在这些GDI对象外面包一层RAII类。

class CGdiObj { public: CGdiObj(HGDIOBJ obj) : m_obj(obj) {} ~CGdiObj() { if (m_obj) DeleteObject(m_obj); } operator HGDIOBJ() const { return m_obj; } private: HGDIOBJ m_obj; };

用完的临时对象放在局部作用域里,超出作用域自动释放,再也不会漏。这条经验怎么强调都不为过,分布式系统出了问题还能靠日志查,GDI句柄泄漏完全是"我只能靠日志加一轮轮代码审查,然后被它在生产环境背刺一次才长记性"。

还有个容易踩的细节:BeginPaint拿到的DC必须在EndPaint里释放;GetDC拿到的DC必须在ReleaseDC里释放;CreateCompatibleDC拿到的DC必须DeleteDC。这三种DC生命周期不一样,不能用同一种方式管理。特别是CreateCompatibleDC用完后必须DeleteDC,它不占窗口DC的引用,但占系统内存句柄。

4.2 局部刷新与裁剪:把重绘开销降下来

GDI性能优化的核心思想不是"画得越快",而是"尽量少画"。如果整个窗口只有一个小区域发生了变化,却每次都重绘全窗口,那再快的函数也顶不住。

用InvalidateRect指定无效区域:

InvalidateRect(hwnd, &smallRect, FALSE);

FALSE表示不擦除背景,TRUE会触发WM_ERASEBKGND擦除背景。在双缓冲场景里不需要系统擦背景,传FALSE,让WM_PAINT自己处理就行。系统在发送WM_PAINT时会根据无效区域自动设置裁剪区域,BeginPaint拿到的DC已经自动带上了裁剪矩形,你在这个DC上绘制超出区域的图形会被自动裁掉,不会影响外部显示。所以只要在WM_PAINT里正常画就行,系统帮你裁剪,不必自己再做区域判断。

实时刷新的曲线图是个典型的局部刷新场景。你不需要每帧重绘整个窗口,只需要把移动的曲线尾部和新增的数据点画出来。做法是:保存上一次曲线的位置,本次先BitBlt旧位置覆盖掉(或者直接把旧区域填充为背景色),再在当前位置绘制新线段。这样刷新区域固定在一小条范围内,CPU占用大幅下降,还能保持60帧稳定运行。我做的实时数据监控界面用了这个方案,CPU占用率从12%降到2%左右。

另一个加速策略是在绘制前判断可见性。如果控件完全不在窗口可见区域内,比如被滚动条滚出视野,就别费劲绘制了,直接返回。GDI虽然会自动裁剪,但裁剪发生在光栅化之后,CPU还是白算了一遍。在UpdateWindow或InvalidateRect之后检查一下客户区是否可见,节省的是光栅化前的计算开销。

4.3 GDI与GDI+、Direct2D的选型边界

很多人在做自绘界面时纠结到底该用GDI、GDI+还是Direct2D。我的经验是:能用GDI说清楚的问题,别上重武器;GDI画不动了,再考虑升级。这个"画不动"包括两种含义——一种是性能瓶颈,比如高频率全屏刷新,GDI确实撑不住;另一种是效果瓶颈,比如要画渐变、阴影、圆角、Alpha混合,光靠GDI原生功能做起来非常别扭。

GDI+引入了一些高级绘图能力:线性渐变画刷、路径、抗锯齿、Alpha混合、图像格式支持更广。但它也有明显缺点:性能比GDI慢,对象生命周期管理繁琐(笔刷和Pen都要手动释放),而且长时间运行在弱GPU环境下会有不稳定性。我的用法是把GDI+用在"静态效果"场景:画控件背景的渐变底、圆角边框、不规则的遮罩,画一次放到缓存位图里,后面的高频刷新仍用GDI的BitBlt显示缓存。这种"GDI+生成静态效果,GDI负责实时合成"的混合架构,既保住了视觉效果又保住了帧率。

Direct2D是硬件加速方案,适合复杂动画、大量文字重排、需要GPU参与的图形效果。但Direct2D开发门槛高出不少,要管理Device Context、SwapChain、渲染Target,还要处理GPU资源丢失的问题。对于一般桌面工具、上位机界面,GDI+混合GDI方案已经绰绰有余。只有遇到需要高质量实时渲染的游戏面板、高帧率的动态数据可视化时,我才会考虑Direct2D。

5. 常见问题与排查技巧实录

5.1 绘制内容不显示或显示错乱

这个问题的表现多样:绘制了但屏幕上什么也没有,或画面撕裂、交错、局部空白。遇到"画了没显示"时,首先检查DC是否正确。拿错DC或者在错误时机拿DC是最常见的原因。比如使用GetDC拿DC后不ReleaseDC,系统相关资源泄漏,后续绘制就出错;比如在WM_PAINT之外的地方直接画,画的内容马上会被下一次WM_PAINT覆盖掉。

其次是检查坐标是否越界。GDI对越界绘制不会报错,但结果不可预期。比如Rectangle(0,0,100,100)画到只有50x50像素的窗口上,你只能看到四分之一的内容,其余部分悄悄被裁掉了。用调试器查看坐标,或者临时填充一个极端颜色(品红色)来判断实际绘制区域是否跟预期一致,是有效的排查手段。

还有种常见情况是Bitmap尺寸和DC尺寸不匹配。双缓冲里创建的CompatibleBitmap宽度高度必须对应客户区大小,不一致时BitBlt可能只拷贝部分内容,看起来就是画面错位。解决方式是窗口尺寸变化时重建缓存位图:

case WM_SIZE: { // 释放旧的memBmp,重新创建与当前客户区等大的 ReleaseMemoryBuffer(); CreateMemoryBuffer(LOWORD(lParam), HIWORD(lParam)); return 0; }

这个处理必须做,否则窗口放大后右边和下边就是一片黑的"死区"。

5.2 中文乱码与字体回退问题

GDI显示中文乱码或者变成方框,多半是字体和字符集不匹配。老代码里用System Font或宋体可能在某些精简系统上缺失,系统就自动回退到默认字体,默认字体又对应不上中文字符编码,就显示出方块。

解决方法是统一使用微软雅黑(Microsoft YaHei UI)这类现代字体,并用CreateFontIndirect指定字符集为DEFAULT_CHARSET,让系统根据字体名称自动匹配正确的字符集。如果某个字体不存在,CreateFontIndirect不会立刻失败,而是在实际绘制时回退到默认字体,所以最好判断一下字体是否可用。另外手动指定lfCharSet = GB2312_CHARSET也可以明确告诉系统用中文字符集,但要注意管理好字体多样性,避免在不同语言系统上表现不一致。

除了字体本身,还有一个隐蔽问题:Unicode和ANSI字符串混用。GDI绘制函数TextOutA接受ANSI字符串,TextOutW接受宽字符字符串。如果你的工程设置了Unicode编译,但传进去的是char*,出来的就是乱码。这个问题现在比较少见,因为新工程默认都是宽字符。但维护老代码时一定要检查工程属性里的字符集设置,以及代码里有没有用#ifdef UNICODE做条件编译的地方。

5.3 高DPI缩放后界面模糊或错位

高DPI是GDI绘制最头疼的问题之一。默认情况下,Windows为了兼容老程序会进行DPI虚拟化——系统把窗口缩放后拉伸显示,结果就是界面模糊。解决方法有两种。第一种是声明程序支持高DPI,在程序启动时主动设置DPI感知:

SetProcessDPIAware(); // 或 SetProcessDpiAwareness(PROCESS_PER_MONITOR_DPI_AWARE)

声明后Windows不再做拉伸,你的窗口按照真实DPI坐标工作,你必须自己处理所有尺寸——字体高度、控件宽度、坐标位置、位图资源——都要按DPI比例缩放。

第二种是根据DPI动态调整绘制参数。最实用的是在WM_DPICHANGED消息中响应,根据新DPI重建字体和位图缓存:

case WM_DPICHANGED: { int dpi = HIWORD(wParam); // 按新DPI重建字体、重算布局 RecreateFontsForDpi(dpi); RebuildBitmapCache(dpi); // 建议使用lParam指向的建议矩形来调整窗口大小 SetWindowPos(hwnd, NULL, 0, 0, rect->right - rect->left, rect->bottom - rect->top, SWP_NOZORDER | SWP_NOACTIVATE); return 0; }

字体尺寸计算有个经验公式:lfHeight = -MulDiv(pointSize, dpi, 72),pointSize是设计时使用的磅数,dpi是当前系统的DPI值。这样字体在100%和200%缩放下都能保持一致的视觉比例。

DPI问题没有一劳永逸的解决方法,最好的策略是设计时就把所有尺寸集中管理,用相对单位而不是绝对像素,保证下次改DPI时只动一处代码。我习惯把控件尺寸、字体大小、间距全部收进一个布局结构体,DPI变化时重新计算一遍结构体即可,绘制代码完全不用改。

结尾

写了这么多,其实都是从一次次实际踩坑里总结出来的。最开始做GDI绘制时我也觉得这个技术太老,许多东西不知道该不该学,等真遇到问题才发现,越是基础的东西越考验内功。设备上下文、GDI对象管理、重绘机制、裁剪区域这些概念理解透了,不管是后来写自绘控件、做数据可视化,还是切换到GDI+和Direct2D,都能更快上手。我个人在实际操作中的体会是:GDI这块最好准备一个最小可复现的骨架工程,把双缓冲、字体管理、位图缓存这些套路固定下来,新项目直接在此基础上改,省去反复查资料的时间。如果你在折腾Win32绘制方面遇到了我上面提到的某个问题,可以照着排查思路试试,八成能解决。以后再看到界面闪烁、文字发虚、GDI泄露,心里就有底了。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/18 3:39:24

FPGA 复位回归实验:双时钟停钟、释放中重入与消费端首拍自检

复位链在单时钟、一次上电的波形里看起来正常,接入多个时钟域后却还要回答三个具体问题:一个时钟停了,另一个时钟域能否按自己的节拍退出复位;释放到一半又收到复位,是否重新等待完整链长;复位输出刚拉低的…

作者头像 李华
网站建设 2026/10/18 3:38:32

百考通AI实测:论文降重与AIGC优化的完整流程与避坑指南

前段时间,一位准备投核心期刊的朋友跟我抱怨:初稿查重率并不算高,但审稿人反馈语言风格平淡、结构松散,有些段落读起来像机器拼出来的。另一边,刚上研究生的A同学也来咨询:学校鼓励用AI辅助写作&#xff0c…

作者头像 李华
网站建设 2026/10/18 3:37:41

C++基础三件事:初始化、输入输出与const详解

C每天会收到无数句"凭什么"。凭什么局部变量不初始化就可能是垃圾值?凭什么输入一个字母程序就死循环?凭什么一个const变量碰都不能碰?这些问题的答案,其实都藏在C的"性格"里。我一直觉得,学C基础…

作者头像 李华
网站建设 2026/10/18 3:37:33

考试云全流程解析:在线考试系统组卷、防作弊与成绩分析实战

如果你组织过考试,大概率体会过那种被流程支配的恐惧:出题、排版、打印、封卷、排队领卷、监考、收卷、阅卷、登分、复核……一套流程下来,真正用来“考察能力”的时间反而没多少。我这些年参与过学校期末考、企业招聘笔试和技术认证考试的组…

作者头像 李华
网站建设 2026/10/18 3:37:30

单例模式五种实现详解:从线程安全到防反射,附Android避坑指南

简介:这是一份面向Android开发者的设计模式学习资料,以单例模式为专题展开实例详解。资源从单例模式的概念、定义与适用场景入手,重点剖析饿汉模式、懒汉模式、DCL双重检查锁定等常见实现方式,并结合Java内存模型说明线程安全、同…

作者头像 李华
网站建设 2026/10/18 3:36:52

网络理论面试题集:从协议原理到内核源码的思维诊断图

1. 这不是题库,而是一张网络理论的“能力诊断图”“网络理论面试题集”这六个字,乍看平平无奇,像极了简历筛选阶段被随手点开又迅速关闭的PDF。但在我带过十几届某高校通信工程方向实习生、参与过不下二十场某实验室系统架构岗技术面谈后&…

作者头像 李华