Win32 GDI绘图函数全解析:核心API与实战避坑指南
1. 从一条线开始Win32绘图到底在画什么很多人觉得Win32绘图是个老掉牙的话题一听到GDI就联想到MFC时代或者某个毕业设计里的简陋界面。但说实话直到今天任何需要自绘控件的桌面程序、任何做工业上位机软件的团队都绕不开这一套绘图API。它确实是老但老并不等于过时——恰恰因为它足够底层、足够直接你才能真正控制每一个像素落在哪里。这篇总结不是什么入门教程而是我在实际项目中把Win32里所有基础绘图函数过了一遍之后整理的完整笔记。覆盖的内容从SetPixel这种画一个点的函数到LineTo、Polyline、PolyBezier画线和曲线再到Rectangle、Ellipse、Arc、Chord、Pie、Polygon这些画封闭图形的函数。每一类我都会讲清楚三件事函数怎么用、参数到底是什么意思、以及你在实际项目中会踩到什么坑。先建立一个基本认知Win32绘图的一切操作都围绕设备上下文Device Context简称DC展开。DC就像一个画板你所有的绘图函数都是在某个DC上执行。拿到DC的方式主要有两种——处理WM_PAINT消息时用BeginPaint获取其他时候用GetDC获取。以前我刚开始写的时候有个误区总觉得绘图就是随便调个函数画上去其实不是绘图前的准备工作——决定在哪儿画、用什么笔画、用什么刷子填——往往比绘制本身更影响最终效果。这里建议你先记住整个绘图生命周期的四步曲获取DCBeginPaint或GetDC设置绘图对象画笔、画刷、字体等调用绘图函数画点、画线、画图形释放DCEndPaint或ReleaseDC步骤3只是一瞬间步骤2才是决定图形样貌的关键。比如你用默认的画笔画出来是1像素宽的黑色实线想画红色虚线必须先创建一个新的画笔对象把它选入DC画完之后再恢复原来的画笔。这篇文章适合谁看如果你是正在学Win32编程、准备应付期末项目或者面试题的学生这里面的每个函数都能直接用如果你是工作里被分配了自绘控件任务的开发者这篇总结能帮你少走很多弯路——我自己当年就因为在圆弧的参数上理解错了调了整整一晚。2. DC和画笔画刷的准备工作比想象中更容易出错2.1 四种获取DC的方式以及它们之间的区别在Win32里DC不是凭空出现的你得先拿到它才能画图。常见的获取方式有四种适用场景完全不同我按使用频率排序获取方式使用场景释放方式注意事项BeginPaint处理WM_PAINT消息时EndPaint只能用于WM_PAINT会自动裁剪无效区域GetDC任意时刻主动绘图ReleaseDC最常用但不会自动裁剪区域GetWindowDC连窗口边框、标题栏一起画ReleaseDC别乱用画到非客户区会很难看CreateCompatibleDC内存绘图双缓冲DeleteDC配合位图对象使用后面细说这里最容易犯的错是在WM_PAINT里用GetDC而不是BeginPaint。如果你这么做程序很可能出现画面闪烁、绘制残留的问题。原因在于BeginPaint返回的DC自带裁剪区域只有无效区域才会被重绘这是Windows帮你做的优化而GetDC没有这个机制它默认整个客户区都能画重绘效率低不说还容易把不该画的地方也抹掉。反过来在窗口消息处理函数的其他分支里你又不能用BeginPaint因为BeginPaint要求你必须处于WM_PAINT的处理过程中而且BeginPaint和EndPaint必须成对出现中间画的东西才会被正式提交到屏幕上。这种成对出现的模式是Win32里最常见的资源管理模式后面讲GDI对象时还会反复遇到。2.2 画笔画线画框全靠它默认的DC自带一个默认画笔画出来是黑色实线宽度1像素。如果你的需求就是黑线1像素那你什么都不用做。但现实中几乎不可能这么省心——你要画网格线、画红色瞄准线、画加粗的边框这就得自己创建画笔了。创建一个画笔最简单的方式是CreatePenHPEN hPen CreatePen(PS_SOLID, 3, RGB(255, 0, 0));三个参数分别是线型、线宽、颜色。线型常见的有PS_SOLID实线最常用PS_DASH虚线注意宽度为1时才有效PS_DOT点线同样要求宽度为1PS_DASHDOT点划线PS_NULL不画线有时候画矩形只想要填充效果时会用到画笔创建之后不能直接用要通过SelectObject把它选入DC然后原来的画笔会被替换掉SelectObject的返回值就是之前的画笔HPEN hOldPen (HPEN)SelectObject(hdc, hPen); // 这里开始画图 SelectObject(hdc, hOldPen); // 画完恢复 DeleteObject(hPen); // 删除创建的画笔这段代码里的模式值得背下来创建对象、选入DC、用完恢复、删除对象。很多人只做到前两步忘了恢复和删除短期运行没事程序长时间跑下来内存慢慢涨最后莫名其妙卡顿甚至崩溃——这就是GDI对象泄漏的典型症状。在任务管理器里加一列GDI对象如果数值稳定上涨而不是维持在一个平台那基本就是这种泄漏。2.3 画刷封闭图形的填充颜色画刷Brush负责填充封闭区域。默认画刷是白色所以画一个Rectangle出来中间是白的边框是黑的这是刚接触Win32绘图时最常见的画面。创建画刷有几种方式我常用CreateSolidBrushHBRUSH hBrush CreateSolidBrush(RGB(0, 0, 255));画刷一样需要选入DC、用完恢复、删除释放。如果你想要半透明效果或者渐变效果基础画刷就不够用了后面有机会展开光影和特效的部分再细说。这里先记住一个关键点画刷只影响填充部分边框还是由画笔决定的。矩形、椭圆、多边形这些函数都遵循这个规则——边框用画笔内部用画刷。3. 逐函数拆解点、线、折线和贝塞尔曲线3.1 SetPixel与SetPixelV画一个点的代价画一个点最简单的方式是SetPixelSetPixel(hdc, x, y, RGB(255, 0, 0));它的作用就是在指定坐标画一个指定颜色的点。看起来很简单但实际项目中要谨慎使用。SetPixel有返回值如果调用成功返回像素的颜色值失败返回-1SetPixelV则没有返回值。两者的区别只有在追求极限性能时才会体现——少一个返回值处理速度能快一点点。但问题在于SetPixel的本质是逐像素操作画100个点就要调用100次函数每次都有参数校验和DC状态检查的开销。如果你要画密密麻麻的散点图用SetPixel十有八九会卡。我自己试过在普通分辨率窗口上画几万个散点SetPixel明显感觉到延迟。这种情况下正确做法是把数据先组织好用SetPixel跑一遍然后换用内存DC配合StretchBlt一次性输出或者直接用Polyline把点连成线——视觉上差别不大但速度能有一个量级的提升。3.2 LineTo与MoveToEx画直线的黄金搭档初学的时候有个疑问为什么Win32不提供一个画一条从A到B的直线的完整函数非得先MoveToEx再LineTo分开调用原因在于GDI的绘图模型设计成当前位置模式画线不是指定两个端点而是从当前位置画到目标位置。标准画法MoveToEx(hdc, x1, y1, NULL); LineTo(hdc, x2, y2);MoveToEx把当前点移动到(x1, y1)LineTo从当前点画一条线到(x2, y2)。画完之后当前点自动变成(x2, y2)这意味着你可以连续调用多个LineTo画出一条折线MoveToEx(hdc, 10, 10, NULL); LineTo(hdc, 50, 50); LineTo(hdc, 100, 30);这样就能画出两段首尾相接的线。这种设计初看别扭习惯之后反而觉得灵活。你只要记住一个原则画任何直线之前先问一句我当前位置在哪儿。有个隐蔽的坑是在调用MoveToEx之前直接调用LineTo会从上次操作结束的位置画到目标点画出一条你不想要的飞线。这种情况在连续绘制多组曲线时特别常见比如画一个坐标系里的多条数据曲线忘记在每组数据前重置起点最后所有曲线都有一条斜线相连图表变得莫名其妙。3.3 Polyline一口气画完的折线如果折线的顶点是固定的逐个MoveToEx加LineTo还是略显啰嗦。这时用PolylinePOINT pts[] { {10, 10}, {50, 50}, {100, 30}, {150, 80} }; Polyline(hdc, pts, 4);一次调用就画出连接这四个点的折线同时当前点会移动到最后一个点的位置。Polyline的效率比循环LineTo高很多因为是单个GDI调用直接交给系统底层处理。与之相近的还有个PolylineTo它和Polyline的区别在于PolylineTo会从当前点开始画起不需要你把起点包含在数组里而Polyline总是从数组的第一个点开始忽略当前点。使用的时候别搞混。3.4 PolyBezier平滑曲线的真正实现贝塞尔曲线这个词听起来高深实际用起来却没那么玄。PolyBezier一次可以画一条或多条贝塞尔曲线每条曲线由4个控制点确定POINT pts[] { {10, 100}, {50, 10}, {100, 190}, {150, 100} }; PolyBezier(hdc, pts, 4);这4个点中第一个是起点第四个是终点中间两个是控制点。曲线会从起点出发被拉向两个控制点的方向最后到达终点。控制点不一定要在曲线上它们像磁铁一样改变曲线的形状。如果要画多条连续的贝塞尔曲线数组长度就是4的倍数每条曲线之间会自动保持连接。我在一个数据可视化项目里用它画过趋势线效果比直接用折线平滑很多视觉上立刻专业一截。这里有个细节必须提醒贝塞尔曲线是逼近曲线它不一定完全穿过所有控制点只保证经过第一个和第四个点。很多初学者把4个点当成曲线必须经过这4个点来用结果画出来和自己预期不符。如果要求曲线严格经过每个数据点那要用的是样条插值而不是贝塞尔逼近这是两类不同的问题。4. 封闭图形的家族矩形、椭圆与它们的分支4.1 Rectangle和RoundRect只需要对角坐标画矩形是Win32绘图最基础也最常用的操作函数签名极其简洁Rectangle(hdc, xLeft, yTop, xRight, yBottom);只用对角两点的坐标就确定了一个矩形。要注意的是xRight和yBottom这个边界是不包含在矩形区域内的也就是说画出来的矩形覆盖的坐标范围是[xLeft, xRight-1]和[yTop, yBottom-1]。这个细节在精确计算图形大小时很关键——你想画一个宽100像素的矩形右边界必须设成左边界100而不是左边界99。RoundRect是圆角矩形RoundRect(hdc, xLeft, yTop, xRight, yBottom, nWidth, nHeight);最后两个参数是圆角的椭圆宽度和高度控制圆角的大小。值越小越接近直角为0时等同矩形值越大越圆。实际做自定义按钮、标签控件时RoundRect出镜率非常高。4.2 Ellipse圆只是它的特例Ellipse函数的参数和Rectangle一模一样只是画出来的是内切于这个矩形的椭圆Ellipse(hdc, xLeft, yTop, xRight, yBottom);换句话说你不必指定椭圆的圆心和半径只需要指定一个能框住它的矩形。想画正圆让这个矩形变成正方形即可Ellipse(hdc, 10, 10, 110, 110); // 圆心在(60,60)半径50的圆这种用边界矩形定义图形的方式是GDI的一大特色圆弧家族的函数也遵循同样的哲学。刚接触时可能不太习惯但用多了会发现这其实非常方便——你不需要做任何圆心坐标和半径的换算只要框一个矩形就行。4.3 深入Arc圆弧的起点终点原来如此Arc是Win32绘图里最容易被误解的函数没有之一。它和Ellipse类似也基于边界矩形但多了一组起点和终点的定义Arc(hdc, xLeft, yTop, xRight, yBottom, xStart, yStart, xEnd, yEnd);前4个参数确定一个椭圆后4个参数不是直接指定弧的起点和终点坐标而是从椭圆中心引两条射线分别穿过(xStart, yStart)和(xEnd, yEnd)这两个点。这两条射线与椭圆的交点才是圆弧真正的起点和终点。同时还有一个不成文的规定圆弧从起点到终点是逆时针方向绘制的。我当年在这个函数上栽过跟头。我想画一个90度的圆弧天真地以为在边界矩形为正方形的情况下参数应该是圆心坐标加减半径。结果画出来的弧完全不是我想要的调了半天才反应过来Arc的起止参数只是方向参考点真正决定弧线位置的是圆心到参考点的连线与椭圆的交叉位置。正确的理解方式是这样的边界矩形确定椭圆总体形状参考点(xStart, yStart)决定从哪儿开始参考点(xEnd, yEnd)决定到哪儿结束弧线按照逆时针从起点走到终点给一个具体的例子。在一个200x200的正方形里画一个内切圆想要右上方1/4圆弧——从圆的正上方开始、正右方结束逆时针方向。圆心是(100, 100)半径是100。那么参考点可以设成(100, 0)正上方和(200, 100)正右方Arc(hdc, 0, 0, 200, 200, 100, 0, 200, 100);这样画出来的就是从正上方逆时针到正右方的那段1/4圆弧。4.4 Chord与Pie弓形和扇形只差一线Arc只画弧线不画两端连起来的弦。而Chord弓形会把圆弧的起点和终点用直线连起来形成一个封闭的弓形区域Chord(hdc, xLeft, yTop, xRight, yBottom, xStart, yStart, xEnd, yEnd);Pie扇形则更加常见它会把圆弧的起点、终点分别和圆心相连形成一个扇形Pie(hdc, xLeft, yTop, xRight, yBottom, xStart, yStart, xEnd, yEnd);三个函数的参数完全一致只是封闭的方式不同。Arc是一条开放弧线Chord是弧线加直线封口Pie是弧线加两条半径。画饼状图、仪表盘指针、雷达图都绕不开Pie。我做一个仪表盘控件时用Pie画背景色带和指针区域概念一下就通了——把整个圆按角度切分成若干扇形每个扇形填充不同的颜色即可。这里补充一个实用小技巧Pie这个封闭图形内的填充用的是当前画刷边框用的是当前画笔。想画一个只有填充没有边框的扇形把画笔设成PS_NULL即可HPEN hNullPen CreatePen(PS_NULL, 0, 0); SelectObject(hdc, hNullPen); Pie(hdc, ...); // 然后恢复画笔、删除hNullPen4.5 Polygon与PolyPolygon任意多边形的绘制多边形的参数和Polyline很像也是一组点但它是封闭图形会自动把最后一个点和第一个点连起来POINT pts[] { {50, 10}, {90, 60}, {70, 90}, {30, 60} }; Polygon(hdc, pts, 4);Polygon的填充规则和上面所有封闭图形一致——用当前画刷填充内部当前画笔绘制边缘。但多边形有个独特的属性自相交时如何填充。默认情况下Polygon使用交替填充规则Alternate图形内部交叉的区域按奇偶原则决定是否填充相交的部分会留空。如果你希望自相交多边形也能整个被填充需要使用SetPolyFillMode改成WINDING模式。这是我实际画五角星时遇到的问题——五角星的边会相交ALTERNATE模式下中心部分可能填充也可能不填充肉眼看起来星形中间出现空洞。把填充模式切到WINDING后一切正常。PolyPolygon则更进一步一次调用可以画多个多边形。它需要两个数组第一个是所有多边形的顶点坐标第二个是每个多边形的顶点数量POINT pts[] { {10, 10}, {30, 10}, {20, 30}, {50, 50}, {70, 50}, {60, 70} }; int counts[] { 3, 3 }; PolyPolygon(hdc, pts, counts, 2);这在画多个相同形状、需要批量渲染的场景下很高效避免了一次次调用Polygon带来的开销。5. 实操复盘用API组合一张可交互数据图表理论讲完得落到实际。我整理了一个参考案例画一个有网格背景、折线数据曲线和扇形统计区域的综合窗口。这个例子会串起上面所有函数也最能暴露各种配合使用时容易出现的问题。第一步在WM_PAINT里拿到DC后先画网格。网格用虚线画笔HPEN hDashPen CreatePen(PS_DASH, 1, RGB(200, 200, 200)); HPEN hOldPen (HPEN)SelectObject(hdc, hDashPen); for (int x 0; x 600; x 30) { MoveToEx(hdc, x, 0, NULL); LineTo(hdc, x, 400); } for (int y 0; y 400; y 30) { MoveToEx(hdc, 0, y, NULL); LineTo(hdc, 600, y); } SelectObject(hdc, hOldPen); DeleteObject(hDashPen);注意PS_DASH线型只有画笔宽度为1时才有效如果你把宽度改成2Windows会强制用实线代替。这个坑我踩过不止一次后来干脆记住虚线就老老实实用1像素。第二步画数据折线。数据点存在一个POINT数组里用Polyline画连线和PolyBezier画平滑曲线各一份方便对比效果POINT data[] { {30, 300}, {90, 220}, {150, 250}, {210, 140}, {270, 160}, {330, 80}, {390, 120}, {450, 50} }; // 折线 Polyline(hdc, data, 8); // 贝塞尔平滑曲线4个点一组这里简单演示用前4点 PolyBezier(hdc, data, 4);第三步用一个饼图表示数据占比。把数据百分比换算成角度这里以三个项目为例30%、50%、20%。圆的边界矩形设为(400, 150)到(550, 300)。先按百分比计算角度再用Pie成块绘制// 项目A0度到108度30% // 项目B108度到288度50% // 项目C288度到360度20%画扇形时参考点用极坐标转直角坐标的方式计算。圆心(475, 225)半径75比如0度方向对应参考点是(475 75, 225)也就是(550, 225)。代码示意#define PI 3.1415926 POINT center { 475, 225 }; int radius 75; auto angleToPoint [](double angleDeg) - POINT { double rad angleDeg * PI / 180.0; POINT pt; pt.x center.x (int)(radius * cos(rad)); pt.y center.y - (int)(radius * sin(rad)); // 屏幕坐标系y向下所以要取负 return pt; }; POINT aStart angleToPoint(0); POINT aEnd angleToPoint(108); POINT bStart angleToPoint(108); POINT bEnd angleToPoint(288); POINT cStart angleToPoint(288); POINT cEnd angleToPoint(360); HPEN hSolidPen CreatePen(PS_SOLID, 1, RGB(0, 0, 0)); SelectObject(hdc, hSolidPen); HBRUSH hBrushA CreateSolidBrush(RGB(255, 99, 71)); HBRUSH hBrushB CreateSolidBrush(RGB(100, 149, 237)); HBRUSH hBrushC CreateSolidBrush(RGB(60, 179, 113)); HBRUSH hOldBrush (HBRUSH)SelectObject(hdc, hBrushA); Pie(hdc, 400, 150, 550, 300, aStart.x, aStart.y, aEnd.x, aEnd.y); SelectObject(hdc, hBrushB); Pie(hdc, 400, 150, 550, 300, bStart.x, bStart.y, bEnd.x, bEnd.y); SelectObject(hdc, hBrushC); Pie(hdc, 400, 150, 550, 300, cStart.x, cStart.y, cEnd.x, cEnd.y);这里有一个源码角度值得注意Win32屏幕坐标系的y轴是向下增长的而数学中极坐标的y轴向上增长。所以在坐标转换时sin要取负号否则你画的扇形会上下颠倒。这个问题几乎每个人第一次画饼图都会碰到。绘制完成后把所有创建的GDI对象按顺序恢复并删除最后EndPaint结束绘制。整个过程看起来机械但每一步都有自己的意义不恢复而直接Delete可能因为对象还在DC中而删除失败不删除创建的画笔画刷长时间运行必然泄漏。6. 高频踩坑清单那些让你挠头的怪现象6.1 GDI对象泄漏程序越跑越卡的真凶GDI对象包括画笔、画刷、字体、位图、区域等系统对每个进程的GDI对象数量是有上限的默认一般是1万个左右。听起来很多但如果你的窗口每秒钟刷新一次每次刷新创建一个画笔而不删除几百秒之后程序就开始各种怪异——绘制内容缺失、窗口白屏、甚至崩溃。我的排查经验是任务管理器 → 详细信息 → 右键列头 → 选择GDI对象列盯着数字看。如果在你操作界面时数值只增不减恭喜你找到问题了。而解决办法其实在上面的示例代码里早已体现注意创建的GDI对象用完后必须选回旧对象并调用DeleteObject。这几乎和释放malloc出来的内存一样重要。6.2 坐标与边框为什么画的线粗了一圈或少了一条边GDI绘图时线条是沿着坐标向右和下方向扩展的。比如你在x100的位置画一条1像素宽的垂直线它实际覆盖的是x99到x100这个区间而不是x100到x101。多个图形拼接时如果不了解这个规律相邻区域之间会出现不和谐的缝隙或者重叠。处理方法是矩形范围统一按照左闭右开的思想设置坐标也就是右边界和底边界比实际需要大一像素。我曾经画一个棋盘格每格100x100用了0,100的边界结果格子之间总是出现白线或黑线改了一整天才弄明白是这个原因。6.3 消息循环中的画了就没了现象很多人写完绘图代码发现窗口被遮挡再恢复后图形就消失了只剩白底。这不是你的绘制代码写错了而是Windows的绘制即擦除机制在起作用窗口被遮挡时客户区内容被系统当作失效区域处理恢复显示时系统发送WM_PAINT消息让你重新画。如果你只在初始化时画了一幅图而没有在WM_PAINT中重画内容自然就丢了。正确架构是所有图形都在WM_PAINT中绘制其他任何位置想触发重绘时用InvalidateRect让系统产生WM_PAINT消息而不是直接调用绘图代码。当你需要在拖动鼠标时动态画线可以先用内存DC画好当前的图形再在WM_PAINT里整体输出这样既能保证刷新又不会过度闪烁。6.4 画面闪烁双缓冲是唯一的解药如果在窗口上频繁绘图例如拖动图形、动画、实时数据刷新你会看到明显的闪烁。原因在于WM_PAINT里直接绘图每次都要先擦掉旧内容再画新内容这个先擦后画的过程被肉眼捕捉到了。双缓冲的思路是先在内存DC里画好完整的画面再一次性地复制到窗口DC上整个过程只有一次刷新用户看起来就是完整的、连续的。实现核心如下HDC hdc BeginPaint(hwnd, ps); HDC memDC CreateCompatibleDC(hdc); HBITMAP memBmp CreateCompatibleBitmap(hdc, width, height); HGDIOBJ oldBmp SelectObject(memDC, memBmp); // 所有绘图操作在memDC上进行 Rectangle(memDC, ...); Ellipse(memDC, ...); Polyline(memDC, ...); // 一次性拷贝到窗口 BitBlt(hdc, 0, 0, width, height, memDC, 0, 0, SRCCOPY); SelectObject(memDC, oldBmp); DeleteObject(memBmp); DeleteDC(memDC); EndPaint(hwnd, ps);当画面内容很多时双缓冲不只是减少闪烁绘图速度也会明显提升——所有复杂操作都在内存里完成屏幕复制只发生在最后一步。我做实时数据曲线图的时候上千个点的重绘用双缓冲后流畅度完全是两个级别。7. 踩坑实录圆弧方向与填充规则的夜间调试前文提到Arc的方向问题和Polygon的填充问题这里把当时调试的完整过程还原一遍希望遇到类似图形看起来不对的人能顺着同样的思路快速定位。某个深夜我写一个模拟仪表盘的Demo需要画一个270度的弧留一个90度的缺口当作表盘的下开口。当时的代码是这样的Arc(hdc, 0, 0, 200, 200, 100, 100, 200, 100);我想当然地以为起点参考点(100,100)是圆心终点参考点(200,100)是右边所以弧应该从圆心右侧附近开始画。实际运行出来的图形完全不对弧线几乎从圆形底部冒出来。我盯着屏幕挠头最后把四个参考点分别用SetPixel标出来再对照弧线的实际位置才真正理解射线与椭圆的交点这个定义的含义。后来我在调试这类图形问题时养成一个习惯先用SetPixel把参考点画出来配合Arc本身的输出在视觉上验证自己的理解对不对。这个方法看起来笨但非常有效——你的直觉会骗你但屏幕上的像素不会。同一天晚上还遇上了Polygon填充的坑。画五角星时用的点坐标按常规五角星顶点顺序传入计算本身没问题但因为多边形自相交ALTERNATE填充模式下内部出现了空洞。我第一反应是点坐标算错了反复检查几遍没发现错误直到查SetPolyFillMode才明白问题出在填充规则。实验很简单同一个五角星点集在ALTERNATE和WINDING两种填充模式下分别输出对比。ALTERNATE出现中心空洞WINDING全部填满。结论很清晰——自相交多边形必须用WINDING模式才能得到视觉上完整的填充。提示SetPolyFillMode(hdc, WINDING)会影响当前DC上所有多边形的填充方式画完记得不需要时改回ALTERNATE以免影响其他图形的绘制。这次调试的最大收获其实是沟通层面的当你不知道某个图形函数的确切行为时不要猜不要用感觉去调而是用可视化工具把中间量画出来。改一个参数就重新编译运行看效果反复几次函数的脾气就摸清了。8. 实战演示Demo把所有图形组合成一个完整窗口为了验证上面所有函数配合使用的效果我写了一个综合Demo。窗口左边绘制各种基础图形右边放置一个数据图表区将所有元素集中展示方便复用和调参。Demo里两个核心区域左边图形展示区包含// 点阵随机100个散点 for (int i 0; i 100; i) { int x rand() % 380 10; int y rand() % 280 40; SetPixelV(hdc, x, y, RGB(0, 0, 255)); } // 折线三组不同颜色的数据曲线 POINT line1[] { {10, 300}, {60, 240}, {110, 260}, {160, 190}, {210, 220}, {260, 130} }; POINT line2[] { {10, 280}, {60, 260}, {110, 200}, {160, 210}, {210, 150}, {260, 160} }; HPEN hBluePen CreatePen(PS_SOLID, 2, RGB(30, 144, 255)); HPEN hGreenPen CreatePen(PS_SOLID, 2, RGB(60, 179, 113)); SelectObject(hdc, hBluePen); Polyline(hdc, line1, 6); SelectObject(hdc, hGreenPen); Polyline(hdc, line2, 6); // 封闭图形组矩形、圆角矩形、椭圆、弓形、扇形 Rectangle(hdc, 300, 40, 420, 140); RoundRect(hdc, 300, 160, 420, 260, 30, 30); Ellipse(hdc, 300, 280, 420, 380); Chord(hdc, 440, 40, 560, 140, 440, 90, 560, 90); Pie(hdc, 440, 160, 560, 260, 440, 210, 560, 210);右边数据图表区放一个双缓冲绘制的面积折线图用Polygon把折线与底部闭合填充半透明效果难以直接实现时暂时用纯色填充从视觉上能看出渐变模拟的潜力。这个Demo跑起来之后所有基础图形在同一个窗口里呈现对照代码逐个观察会比干看文档理解深刻得多。你把它当作一个活页测试页想调试哪个函数就把对应的那段代码单独提出来改参数、看效果。9. 进阶路线从GDI走向GDI基础GDI函数能覆盖大部分二维绘图需求但它有明显的天花板没有抗锯齿画圆和弧线时边缘有明显的锯齿、没有渐变填充、没有半透明合成、没有路径编辑能力。如果项目对视觉品质要求高下一步自然要走向GDI。GDI和GDI不是替代关系而是互补关系。GDI封装了更多高级功能比如用Graphics对象替代DC、支持AlphaBlend、支持LinearGradientBrush、支持GraphicsPath做复杂路径。但GDI在某些批量绘制的性能上并不比GDI快它的优势是开发效率和视觉效果。我的建议是如果只是画网格、画边框、画简单的图形继续用GDI如果要做现代化的仪表盘、图表、自定义控件皮肤GDI更合适。两者的函数风格差异很大但核心概念——先设置绘图状态再执行绘制操作——是一脉相承的。把GDI的基础打牢学GDI会轻松非常多至少你不会再问为什么要在绘图前先创建画笔和画刷这种问题了。这篇文章的每个函数背后都可以再展开一篇实战讲解。就我个人的体会Win32绘图最考验人的不是函数本身而是对绘图状态的管理能力——这个状态包括你选入DC的画笔、画刷以及你对坐标范围的精确把控。把状态管理好了GDI这套东西真的不难而且相当可靠。

相关新闻

BERT知识库问答系统实战:从环境配置到Web部署

BERT知识库问答系统实战:从环境配置到Web部署

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/9 4:45:00 阅读更多 →
SpringBoot租房管理系统毕设实战:核心模块设计与避坑经验

SpringBoot租房管理系统毕设实战:核心模块设计与避坑经验

最近刚带人做完一个 springboot 租房管理系统的毕业设计,前后折腾了大概三周,从选题、搭框架到跑通核心功能,踩了不少坑,也总结了不少能直接抄作业的经验。这个题目在计算机毕设里属于典型的中等难度 Java Web 项目,既…

2026/10/9 4:45:00 阅读更多 →
AnyPS5 跨平台兼容层实战:SPIR-V 转换与 relinker 重链接

AnyPS5 跨平台兼容层实战:SPIR-V 转换与 relinker 重链接

1. 从"AnyPS5"这个名字说起:它到底想解决什么问题第一次看到"AnyPS5"这个项目名,很多人会下意识往游戏主机方向联想,但结合关键词里的relinker、SPIR-V、Linux、Windows来看,这其实是一个典型的跨平台图形/计…

2026/10/9 4:44:00 阅读更多 →

最新新闻

Swift常量let深度解析:不可变绑定、编译优化与并发安全

Swift常量let深度解析:不可变绑定、编译优化与并发安全

学习Swift的人越来越多,但真正能把let用明白的,说实话不多。很多同行写了几年Swift,提到"常量"仍然只会说"let就是不可变的var",可一旦深问下去——常量什么时候能延迟初始化、常量和并发安全有什么关系、为什…

2026/10/9 6:36:28 阅读更多 →
Swift常量正确打开方式:从let原理到编译期优化与并发安全实践

Swift常量正确打开方式:从let原理到编译期优化与并发安全实践

接手一个维护了三年的iOS项目,你最想吐槽的往往不是架构本身,而是散落在代码各个角落的魔法字符串和魔法数字。同一个key四处复制,隔三差五手滑拼错,改一处漏三处。这时候大家才会想起来,Swift里有个再基础不过的东西—…

2026/10/9 6:36:28 阅读更多 →
MiMo-V2.6自我改进强化学习规模化:MoE与因果推断实战解析

MiMo-V2.6自我改进强化学习规模化:MoE与因果推断实战解析

1. 从"自我改进"这个词说起:MiMo-V2.6到底想解决什么问题第一次看到"自我改进的强化学习规模化"这个说法,我的反应是:又是一个造词运动。但把技术报告翻了两遍之后,我改主意了——这次他们想解决的是一个真实…

2026/10/9 6:36:28 阅读更多 →
观察者模式实战:直播间送礼系统的解耦设计与实现

观察者模式实战:直播间送礼系统的解耦设计与实现

打开任何一个直播App,走进一个正在热闹开播的直播间。观众点了一下礼物栏,送出一发火箭。接下来几秒钟里,弹幕频道滚出"XXX 送出了火箭",全屏特效炸开,主播的语音感谢响起,礼物榜排名跳动&#x…

2026/10/9 6:36:28 阅读更多 →
学嵌入式Day1:先啃Linux基础命令,别急着点亮屏幕

学嵌入式Day1:先啃Linux基础命令,别急着点亮屏幕

学嵌入式第一天,很多人恨不得马上点亮一块LCD屏幕、跑一个MQTT协议栈,结果折腾三天连板子都没识别出来。我的建议反着来:第一天什么都别做,先把Linux基础命令在终端里敲熟。说句不太好听的话,嵌入式开发的大部分时间其…

2026/10/9 6:36:28 阅读更多 →
生产级Coding Agent调优实战:Harness工程化决定落地下限

生产级Coding Agent调优实战:Harness工程化决定落地下限

1. 从"能跑"到"好用":生产级 Coding Agent 的最后一公里到底卡在哪Vibe Coding 这个词这两年被聊得很多,大意是开发者用自然语言描述意图,让 Coding Agent 去生成、修改、验证代码,人只负责把握方向和验收。听…

2026/10/9 6:35:27 阅读更多 →

日新闻

Java时间API实战:LocalDate、Date与ZonedDateTime的转换与避坑指南

Java时间API实战:LocalDate、Date与ZonedDateTime的转换与避坑指南

Java时间API这个话题,隔三差五就会在群里被翻出来讨论一次。上周还有个同事线上处理一个订单超时问题,排查到最后发现是ZonedDateTime序列化后时区丢了,用户在下单当天晚上看到的时间整整差了8个小时。这类问题几乎每个做Java开发的人都遇到过…

2026/10/9 0:00:49 阅读更多 →
EasyTier实践:从NAT穿透到子网代理的异地组网部署与排错

EasyTier实践:从NAT穿透到子网代理的异地组网部署与排错

前几个月我手头有好几台机器需要互相访问:办公室台式机、家里 NAS、还有一台云主机。如果只是偶尔传个文件倒还好,问题是工作场景经常要在几处环境之间来回切换,每次都先登录跳板机再层层代理,实在折腾。我先后试过端口映射、自建…

2026/10/9 0:00:49 阅读更多 →
AI Agent工程实战:从七要素到七个决策点的系统设计指南

AI Agent工程实战:从七要素到七个决策点的系统设计指南

AI Agent 这个词在过去一年里被反复提及,但真正动手搭过一套能跑起来的 Agent 系统的人都知道,从"知道它是什么"到"让它稳定干活"之间隔着一整套工程决策。我前后参与过几个 Agent 项目的落地,从最初用现成框架拼装&…

2026/10/9 0:01:50 阅读更多 →

周新闻

KT148A语音芯片外挂8002D功放的工程实践指南

KT148A语音芯片外挂8002D功放的工程实践指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/8 15:26:32 阅读更多 →
LLC谐振变换器增益公式推导:从FHA等效到完整归一化表达式

LLC谐振变换器增益公式推导:从FHA等效到完整归一化表达式

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/8 15:26:40 阅读更多 →
ARM架构深度解析:从RISC设计理念到交叉编译实战

ARM架构深度解析:从RISC设计理念到交叉编译实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/8 10:10:36 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/8 21:13:17 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/8 15:26:17 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/9 6:17:20 阅读更多 →