程序A盖着程序B上面
关于鼠标的消息响应问题
- 为什么点击公共区域,响应的是上面那个
Windows
操作系统按照Z顺序处理窗口,这意味着最上层的窗口优先接收输入事件- 操作系统使用
Hit Testing
方法,根据点击坐标判断哪个窗口包含该坐标
关于未被遮盖部分的显示问题
概述
- 在
Windows
操作系统中,窗口的渲染由窗口管理器和图形设备接口(GDI
)处理 - 当一个窗口覆盖另一个窗口时,
Windows
会管理这些窗口的绘制区域,并确保每个窗口都能正确地显示其可见部分
窗口重叠的处理:
- 剪裁区域
- 每个窗口都有一个剪裁区域,表示该窗口当前可见的部分
- 当一个窗口部分被另一个窗口覆盖时,
Windows
会计算被覆盖窗口的可见区域,并只绘制这部分内容
- 无效区域
- 当窗口被移动、调整大小或被其他窗口覆盖时,系统会将需要重绘的区域标记为无效区域
- 无效区域会在下一次
WM_PAINT
消息处理中被重绘
- Z序(
Z-order
)Windows
窗口系统维护一个Z序列表,用于管理窗口的前后顺序- 最前面的窗口会首先被绘制,依次往后绘制
- 当窗口
A
覆盖窗口B
时,窗口A
的可见部分会覆盖B
的对应部分,但B
未被覆盖的部分仍然会显示
渲染过程
- 当一个窗口被部分覆盖时,
Windows
会根据窗口的Z序来决定哪些部分需要重绘,步骤如下: - 计算无效区域:
- 当窗口A覆盖窗口B时,系统会计算出窗口B的无效区域,即需要重绘的部分
- 发送
WM_PAINT
消息:- 对于无效区域,系统会发送
WM_PAINT
消息给对应的窗口,通知它们进行重绘 - 窗口B会接收到
WM_PAINT
消息并重绘其未被覆盖的部分
- 对于无效区域,系统会发送
- 绘制窗口内容:
- 每个窗口在处理
WM_PAINT
消息时,会根据其剪裁区域绘制内容 - 窗口
B
只会绘制其可见的部分,即未被窗口A
覆盖的部分
- 每个窗口在处理
无效区域和裁剪区域的理解
- 无效区域
- 无效区域由系统计算,解决“哪里需要更新”的问题
- 裁剪区域
- 裁剪区域由程序设定,解决“允许在哪里绘制”的问题
- 一句话总结
- 裁剪区域是程序定义的绘图允许范围(比如一个正方向窗口我通过代码让它显示成圆心窗口),无效区域是系统标记的重绘需求范围(比如这个圆形窗口或其他窗口被遮盖部分,或因为移动、改变大小,被系统计算出需要重绘的部分)
关于消息的传递
- 当你在桌面上点击程序A的界面时,输入设备(如鼠标或键盘)会生成硬件中断
- 这些中断被操作系统处理,然后转化为相应的输入消息(如鼠标点击消息
WM_LBUTTONDOWN
) - 这些输入消息首先会进入操作系统维护的系统消息队列
- 系统消息队列是全局的,用于暂时存储系统范围内的输入事件
- 操作系统会从系统消息队列中取出消息,并根据消息的目标窗口,将消息分发到相应的线程消息队列中
- 操作系统根据输入事件的坐标来确定目标窗口
会选择Z序列表里面第一个匹配的窗口
操作系统使用Hit Testing
方法,根据点击坐标判断哪个窗口包含该坐标 - 每个窗口与创建它的线程关联
操作系统根据目标窗口找到相应的线程 - 操作系统将消息从系统消息队列中移出,并放入目标线程的消息队列中
- 操作系统根据输入事件的坐标来确定目标窗口
- 每个
GUI
线程都有一个消息循环,它从线程的消息队列中提取消息- 并将消息分发到相应的窗口过程函数
关于线程的消息循环
- 只有当线程调用了用户界面相关的
API
(如CreateWindow
、PeekMessage
或GetMessage
)时,操作系统才会为该线程创建消息队列- 这是为了优化资源使用,避免为不需要消息处理的线程创建不必要的消息队列
- 至于怎么检测到我调用了这些函数:
- 是通过内部的函数调用和检测机制实现的
- 用户模式调用:
- 当应用程序调用
CreateWindow
或CreateWindowEx
函数时,这些函数属于User32.dll
库提供的用户模式API
CreateWindow
和CreateWindowEx
实际上是高级API
,它们内部会调用更多底层的API
和函数来完成窗口的创建
- 当应用程序调用
- 内部API调用:
CreateWindow
和CreateWindowEx
最终会调用CreateWindowStation
、CreateDesktop
和NtUserCreateWindow
等更低层次的API
- 这些低层次
API
负责实际的窗口创建工作,并进行与窗口管理相关的各种操作
- 在底层
API
(如NtUserCreateWindow
)调用之前,User32.dll
会首先检查当前线程是否已经有消息队列- 这是通过内部的线程数据结构和状态标志来实现的
- 每个线程都有一个线程本地存储区,用于存储线程特定的数据
Windows
使用TLS
来跟踪每个线程的消息队列状态
- 如果当前线程还没有消息队列,
User32.dll
会调用相应的内部函数来创建消息队列- 这些函数会与内核模式的窗口管理器组件(如
Win32k.sys
)交互,分配和初始化消息队列数据结构
- 这些函数会与内核模式的窗口管理器组件(如
对于窗口管理器
概述
- 窗口管理器是由
Windows
操作系统的内核和用户模式组件共同管理的 - 在
Windows
中,窗口管理器的主要职责是处理窗口的创建、销毁、移动、调整大小、层次管理以及输入事件的分发等
用户模式组件
- 用户模式下的窗口管理器包括
User32.dll
和GDI32.dll
User32.dll
负责窗口管理的核心功能,如窗口的创建、消息处理、输入事件的分发等GDI32.dll
则负责图形设备接口的实现,提供绘图功能
内核模式组件
- 内核模式下的窗口管理器包括
Win32k.sys
,负责低级别的图形和窗口管理操作,如窗口的合成和渲染
窗口在创建后是怎么被窗口管理器接管的?
- 在创建窗口之前,应用程序需要注册一个窗口类(使用
RegisterClass
或RegisterClassEx
)- 窗口类包含窗口过程的指针,用于处理窗口消息
- 应用程序调用
CreateWindowEx
(或CreateWindow
)函数来创建一个窗口- 这是一个
User32.dll
提供的函数
- 这是一个
CreateWindowEx
函数调用User32.dll
,User32.dll
负责创建窗口并分配资源,如窗口句柄HWND
User32.dll
会将窗口信息记录在系统的窗口数据结构中
User32.dll
与内核模式组件Win32k.sys
交互,将窗口信息传递给内核,确保窗口被正确地管理和渲染- 在窗口创建成功后,窗口管理器接管窗口的管理
- 它负责窗口的消息分发、
Z
序管理(前后顺序)、输入事件处理等
- 它负责窗口的消息分发、
是窗口管理器调用GDI进行绘制窗口的吗?
- 当窗口需要绘制时(例如,窗口的无效区域需要重绘),窗口管理器会发送
WM_PAINT
消息给窗口 - 在
BeginPaint
和EndPaint
之间,应用程序使用GDI
函数进行绘制GDI
(图形设备接口)是Windows
提供的用于绘制图形和文本的API
1 2 3 4 5 6 7 8 9 10 11 12 |
case WM_PAINT: { PAINTSTRUCT ps; HDC hdc = BeginPaint(hWnd, &ps); // 使用GDI函数进行绘制 RECT rect; GetClientRect(hWnd, &rect); DrawText(hdc, "This is a sample text", -1, &rect, DT_CENTER | DT_VCENTER | DT_SINGLELINE); EndPaint(hWnd, &ps); return 0; } |
- 内核模式处理:
GDI
函数调用会通过User32.dll
和GDI32.dll
传递到内核模式组件Win32k.sys
Win32k.sys
负责实际的绘制操作,将绘制指令发送到显卡驱动进行渲染
关于Z序列表
概述
Windows
操作系统的Z
序列表(Z-order list
)是由窗口管理器维护的,保存在内核空间- 具体来说,这些信息由操作系统的图形子系统(
Windows Graphics Subsystem
)管理,尤其是Win32k.sys
驱动程序 - 这些数据结构不直接暴露给用户模式代码,但它们的管理和操作由系统提供的
API
间接进行
- 具体来说,这些信息由操作系统的图形子系统(
内核空间
Z
序列表是由内核模式组件管理的,包括Win32k.sys
驱动程序- 这个驱动程序负责维护所有顶层窗口的
Z
序顺序
图形子系统
- 图形子系统(
Windows Graphics Subsystem
)是Windows
操作系统的一部分,负责处理所有与图形和窗口管理相关的操作 - 这个子系统在内核模式下运行,确保对窗口的管理和渲染是高效且安全的
关于图形设备接口
概述
- 图形设备接口(
Graphics Device Interface
,GDI
)是Windows
操作系统的核心组件之一,负责与图形输出设备(如显示器和打印机)进行交互 GDI
提供了一套API
,用于绘制图形、显示文本和管理图形对象
概念
- 图形设备上下文(
Device Context
,DC
)- 图形设备上下文是一个数据结构,包含了绘图属性和绘图状态
- 通过设备上下文,应用程序可以绘制图形、文本,并与图形设备进行交互
- 设备上下文通常用一个句柄(
HDC
)来表示
1 |
HDC hdc = GetDC(hWnd); // 获取设备上下文句柄 |
- 图形对象
GDI
使用一系列图形对象来描述绘图属性,包括笔(Pen
)、刷子(Brush
)、字体(Font
)、调色板(Palette
)等- 这些对象需要选择到设备上下文中,才能应用于绘图操作
1 2 |
HPEN hPen = CreatePen(PS_SOLID, 1, RGB(255, 0, 0)); // 创建红色实线笔 SelectObject(hdc, hPen); // 选择笔到设备上下文中 |
- 绘图操作
GDI
提供了丰富的绘图函数,用于绘制直线、矩形、椭圆、多边形、位图、文本等
1 2 |
MoveToEx(hdc, 10, 10, NULL); // 移动到起点 LineTo(hdc, 100, 100); // 绘制直线 |
主要功能
- 绘制图形
GDI
提供了绘制基本图形的函数,如直线、矩形、椭圆、多边形等
1 2 |
Rectangle(hdc, 10, 10, 200, 200); // 绘制矩形 Ellipse(hdc, 50, 50, 150, 150); // 绘制椭圆 |
- 显示文本
GDI
支持在窗口和设备上下文中绘制文本,支持不同的字体、颜色和对齐方式
1 |
TextOut(hdc, 10, 10, "Hello, GDI!", 10); // 绘制文本 |
- 管理图形对象
GDI
使用图形对象来定义绘图属性,如笔、刷子、字体、调色板等- 应用程序可以创建、选择和删除这些对象
1 2 |
HBRUSH hBrush = CreateSolidBrush(RGB(0, 255, 0)); // 创建绿色刷子 SelectObject(hdc, hBrush); // 选择刷子到设备上下文中 |
- 位图操作
GDI
支持位图操作,可以加载、显示、操作位图图像- 支持的格式包括
BMP
、JPEG
、PNG
等
1 2 |
HBITMAP hBitmap = LoadImage(NULL, "example.bmp", IMAGE_BITMAP, 0, 0, LR_LOADFROMFILE); SelectObject(hdc, hBitmap); // 选择位图到设备上下文中 |
- 打印支持
GDI
提供了与打印设备交互的功能,支持打印预览、分页打印和打印属性设置
每个应用程序会有一个图形设备上下文吗
- 在
Windows
操作系统中,每个应用程序可能有多个图形设备上下文(Device Context
,DC
),具体取决于应用程序的需要 - 窗口设备上下文
- 与窗口相关联的设备上下文,用于在窗口的客户区内绘图
1 |
HDC hdc = GetDC(hWnd); // 获取窗口设备上下文句柄 |
- 客户区设备上下文(
Client DC
)- 仅用于窗口的客户区
1 |
HDC hdc = GetDCEx(hWnd, NULL, DCX_WINDOW | DCX_CACHE); |
- 屏幕设备上下文(
Screen DC
)- 与整个屏幕相关联的设备上下文,用于在屏幕上绘图
1 |
HDC hdc = GetDC(NULL); // 获取屏幕设备上下文句柄 |
- 打印机设备上下文
- 与打印机相关联的设备上下文,用于打印操作
1 |
HDC hdcPrinter = CreateDC("WINSPOOL", printerName, NULL, NULL); // 创建打印机设备上下文 |
示例
- 窗口绘图
- 当应用程序需要在窗口内绘制内容时,会获取窗口设备上下文或客户区设备上下文
1 2 3 |
HDC hdc = GetDC(hWnd); // 获取窗口设备上下文 // 绘图操作 ReleaseDC(hWnd, hdc); // 释放设备上下文 |
- 离屏绘图
- 当应用程序需要在内存中绘制内容(如双缓冲技术),以减少屏幕闪烁时,会使用内存设备上下文
1 2 3 4 5 6 7 |
HDC hdcMem = CreateCompatibleDC(hdc); // 创建内存设备上下文 HBITMAP hBitmap = CreateCompatibleBitmap(hdc, width, height); // 创建兼容位图 SelectObject(hdcMem, hBitmap); // 选择位图到内存设备上下文 // 在内存设备上下文中绘图 BitBlt(hdc, 0, 0, width, height, hdcMem, 0, 0, SRCCOPY); // 将内存中的内容复制到屏幕上 DeleteDC(hdcMem); // 删除内存设备上下文 DeleteObject(hBitmap); // 删除位图对象 |
- 打印
- 当应用程序需要打印内容时,会创建打印机设备上下文
1 2 3 4 5 6 7 |
DOCINFO di = { sizeof(DOCINFO), "My Document" }; StartDoc(hdcPrinter, &di); // 开始打印文档 StartPage(hdcPrinter); // 开始打印页面 // 打印操作 EndPage(hdcPrinter); // 结束打印页面 EndDoc(hdcPrinter); // 结束打印文档 DeleteDC(hdcPrinter); // 删除打印机设备上下文 |
GDI和GDI+
概述
- 都是
Windows
操作系统中的图形处理API - 但它们在功能、性能和编程模型上有显著区别
GDI
GDI
是Windows
操作系统中最早的图形处理API
之一,自Windows 1.0
以来一直存在GDI
直接与图形设备(如显示器和打印机)交互,主要用于绘制基本的2D
图形和文本GDI
提供的图形功能相对有限,主要包括基本的形状绘制(如线条、矩形、椭圆)、位图操作和简单的文本绘制GDI
的绘图操作主要是基于像素的,缺乏高级的绘图功能(如反锯齿、透明度和复杂的变换)- 由于
GDI
直接与硬件交互,其性能在某些情况下可能优于GDI+
,特别是在处理简单的图形操作时
GDI+
GDI+
是GDI
的增强版,提供了更丰富的图形功能和更现代化的编程接口GDI+
采用面向对象的设计,提供了更高层次的抽象,使用起来比GDI
更简洁和直观GDI+
支持高级图形功能,如反锯齿、透明度、渐变、纹理填充、复杂变换和图形路径等GDI+
提供了更强大的文本处理功能,支持复杂的文本布局和格式设置GDI+
的绘图操作主要是基于矢量的,支持更高质量的图形输出- 由于
GDI+
提供了更多的高级功能,其性能在某些情况下可能不如GDI
,特别是在需要大量绘图操作时
CPU还是GPU渲染
GDI
GDI
主要使用CPU
进行渲染- 虽然在某些情况下,
GDI
会利用显卡的硬件加速功能(例如通过DirectDraw
),但大部分的绘图操作还是由CPU
完成的 GDI
的设计和实现主要是基于软件渲染
GDI+
GDI+
主要使用CPU
进行渲染
绘制异形窗口
概述
- 绘制异形窗口是一个涉及窗口区域(
Window Region
)和图形处理的任务 - 在
Windows
应用程序中,异形窗口是通过设置窗口的区域来实现的
步骤
- 定义窗口的形状:创建一个复杂的区域来定义窗口的形状
- 设置窗口区域:将该区域应用到窗口上,使其具有特定的形状
使用GDI
- 使用
GDI
的区域函数(如CreateRectRgn
、CreateEllipticRgn
、CreatePolygonRgn
等)创建一个复杂的区域 - 使用
SetWindowRgn
函数将该区域应用到窗口,使窗口具有特定的形状
使用GDI+
- 使用
GDI+
的GraphicsPath
对象创建复杂的形状 - 使用
Region
类将GraphicsPath
对象转换为区域 - 使用
SetWindowRgn
函数将该区域应用到窗口
关于Skia
概述
- 一个开源的
2D
图形库 - 支持多种操作系统,包括
Windows
、macOS
、Linux
、Android
和iOS
- 提供了丰富的图形功能,包括路径绘制、位图操作、反锯齿、渐变填充、图像滤镜、阴影等
- 利用硬件加速(如
GPU
)来提升绘图性能,同时也提供了软件渲染的支持 Skia
使用C++
进行开发,提供了面向对象的编程接口
硬件加速是怎么做的
-
Skia
可以通过使用GPU
(图形处理单元)来加速图形渲染 -
与传统的
CPU
渲染相比,GPU
在处理并行计算任务(如图形渲染)时具有更高的效率 -
Skia
支持多种GPU
图形API
,包括OpenGL
、Vulkan
和Direct3D
-
OpenGL
:Skia
通过Skia's Ganesh
(其GPU
图形子系统)来支持OpenGL
Ganesh
负责将Skia
的绘图命令转换为OpenGL
命令,从而在GPU
上执行渲染操作
-
Vulkan
:- 是一个现代的低开销、高性能的图形和计算
API
Skia
对Vulkan
的支持允许它在支持Vulkan
的设备上利用该API
进行硬件加速渲染
- 是一个现代的低开销、高性能的图形和计算
-
Direct3D
:Skia
也支持Direct3D
,这是Windows
平台上的一种广泛使用的图形API- 通过
Direct3D
,Skia
可以在Windows
设备上利用GPU
进行加速渲染
-
硬件加速的优点
-
并行处理
GPU
能够同时处理大量的图形计算任务,特别适合用于处理复杂的图形操作(如反锯齿、多边形填充、纹理映射等)
这使得绘图操作可以并行进行,而不是在CPU
上串行处理,从而大幅提升渲染性能 -
专用硬件单元
GPU
包含专门设计用于图形渲染的硬件单元,如着色器单元、纹理单元和光栅化单元
这些硬件单元能够高效地执行特定的图形操作,比通用处理器更快
-
-
示例:
- 一个使用
Skia
和OpenGL
进行硬件加速渲染的简单示例:
- 一个使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
#include "include/core/SkCanvas.h" #include "include/core/SkGraphics.h" #include "include/core/SkSurface.h" #include "include/gpu/gl/GrGLInterface.h" #include "include/gpu/GrDirectContext.h" void draw(SkCanvas* canvas) { SkPaint paint; paint.setColor(SK_ColorRED); paint.setAntiAlias(true); canvas->drawCircle(400, 300, 100, paint); } int main() { // 初始化Skia和OpenGL SkGraphics::Init(); auto interface = GrGLMakeNativeInterface(); auto context = GrDirectContext::MakeGL(interface); SkImageInfo info = SkImageInfo::MakeN32Premul(800, 600); auto surface = SkSurface::MakeRenderTarget(context.get(), SkBudgeted::kNo, info); auto canvas = surface->getCanvas(); // 绘制内容 draw(canvas); // 显示绘制结果 surface->flushAndSubmit(); // 清理资源 context->abandonContext(); SkGraphics::Term(); return 0; } |
对CPU渲染的理解
- 在
Windows
上,使用像GDI
或GDI+
这样的库,这些库提供了绘图的API
,让开发者可以在窗口或屏幕的某个位置进行绘图 - 绘图过程
- 你调用
GDI
或GDI+
的函数,例如绘制一个矩形或文本 - 这些
API
调用最终由CPU
来执行,CPU
负责处理这些绘图操作,并将结果渲染到内存或屏幕上 CPU
完成绘图操作后,结果显示在你指定的屏幕位置或窗口位置
- 你调用
对硬件渲染的理解
- 使用能够利用
GPU
的库,例如Skia
、Direct2D
、OpenGL
、Vulkan
等 - 绘图过程
- 你调用这些库的绘图函数,例如绘制一个圆或渲染一个
3D
对象 - 这些
API
调用被翻译成GPU
能理解的命令或指令,这些指令通过图形驱动程序发送到GPU
GPU
根据接收到的指令执行绘图操作,利用其强大的并行计算能力快速完成绘图任务GPU
完成绘图操作后,结果显示在你指定的屏幕位置或窗口位置
- 你调用这些库的绘图函数,例如绘制一个圆或渲染一个
是怎么显示在屏幕上的
- 绘图操作
- 无论是
CPU
还是GPU
进行渲染,首先会通过绘图API
(如GDI
、GDI+
、OpenGL
、Direct3D
、Skia
等)执行绘图操作,将图形数据绘制到一个帧缓冲区中 - 帧缓冲区是一个内存区域,存储了每个像素的颜色值
- 无论是
- 帧缓冲区
CPU
渲染:绘图操作由CPU
完成,结果存储在系统内存中的一个帧缓冲区GPU
渲染:绘图操作由GPU
完成,结果存储在显存(VRAM
)中的一个帧缓冲区
- 显示控制器
- 显示控制器负责管理从帧缓冲区到屏幕显示的过程
显示控制器通常由显卡(GPU
)或集成在主板上的图形芯片(如集成显卡)实现 - 显示控制器的主要任务包括:
- 扫描转换:将帧缓冲区的内容转换为显示器能够理解的信号(如
HDMI
、DisplayPort
信号) - 刷新率管理:控制帧缓冲区的内容何时被传输到显示器,通常以一定的刷新率(如
60Hz
、120Hz
)进行更新
- 显示控制器负责管理从帧缓冲区到屏幕显示的过程
- 垂直同步
- 为了避免屏幕撕裂,许多系统使用垂直同步(
V-Sync
)技术 - 垂直同步确保帧缓冲区的内容更新与显示器的刷新周期同步:
- 双缓冲:使用两个帧缓冲区,一个用于当前显示,一个用于绘制下一帧
当绘制完成后,两个缓冲区交换角色(交换缓冲,Swap Buffers
) - 三缓冲:增加一个额外的缓冲区,以提高性能和减少延迟
- 为了避免屏幕撕裂,许多系统使用垂直同步(
- 显示器
- 显示器接收来自显示控制器的信号,将信号转换为光信号,显示最终的图像
- 显示器的类型可以是
LCD
、LED
、OLED
等,不同类型的显示器有不同的显示技术和特性
本文为原创文章,版权归Aet所有,欢迎分享本文,转载请保留出处!
你可能也喜欢
- ♥ Soui二05/18
- ♥ Windows 核心编程 _ 作业07/01
- ♥ Cef:介绍06/29
- ♥ Windows 核心编程 _ 用户模式:线程同步二07/16
- ♥ Windows 核心编程 _ 用户模式:线程同步三07/23
- ♥ 关于异常的捕获和dump文件的生成07/05