个人博客:Sekyoro的博客小屋
个人网站:Proanimer的个人网站
当我们想要进行底层图形应用(GUI)开发时,往往需要用到窗口系统和图形库,这里简单介绍一下
视窗系统(window system)与通信协议
下面内容主要针对Unix-like操作系统
视窗系统是以使用视窗作为主要特征之一的图形用户接口的构成组件.更为明确地说,它是桌面环境的构成组件.视窗系统支撑著窗口管理器的实现(implementation);视窗系统为“图像硬件(graphics hardware)、指向设备(pointing devices)提供基本支持.绘制鼠标光标,一般也与视窗系统相关.
X Window System(X11)
X窗口系统是一种以位图方式显示的软件窗口系统,X窗口系统通过软件工具及架构协议来建立操作系统所用的图形用户界面,此后则逐渐扩展适用到各形各色的其他操作系统上.
在X11的设计中,应用程序和显示器不必在同一台计算机上,这一点并不明显.在开发X时,X server运行在工作站上,而用户在具有更强处理能力的远程计算机上运行应用程序是很常见的.
X Window核心协议
X Window 核心协议是X窗口系统的基础协议,它是一个以位图显示的网络化视窗系统,用来在Unix、类Unix和其它操作系统上建立用户图形界面.X Window 系统基于主从式模型:单一服务器控管硬件的输出入,如屏幕、键盘和鼠标;所有的应用程序都被视作客户端,用户之间透过服务器来交互.
交互部分由X Window核心协议来管理.还有其它与X窗口系统有关的协议,有的建立在X Window核心协议之上的,有的是独立的协议.
X server接受来自键盘,鼠标,显示器的输入,并将这些请求发送给client.
X server
与大多数早期的显示协议不同,X是专门设计用于网络连接,而不是用于集成或附加的显示设备.X具有网络透明性,这意味着在网络上某处的计算机(例如Internet)上运行的X程序可以在网络上其他计算机上运行的X服务器上显示其用户界面.
X服务器通常为X客户机提供图形资源和键盘/鼠标事件,这意味着X服务器通常在人类用户面前的计算机上运行,而X客户机应用程序在网络上的任何地方运行,并与用户的计算机通信,请求图形内容的呈现,并从包括键盘和鼠标在内的输入设备接收事件
Xlib与其他的客户端程序
大部分的客户端程序借由 Xlib 客户端程序库与服务器交流.特别是客户端大多使用 Xaw、Motif、GTK+、Qt 之类使用到 Xlib 的程序库,方便和服务器交互.
XLib是X Window System的核心库,它提供了与窗口系统交互的基本功能,如创建窗口、处理事件和绘制图形
Xlib是一种X Window System协议的客户端,以C语言撰写.其功能是与X server沟通.这样的功能可以让程序人员撰写程序时,
#include <X11/Xlib.h>
#include <unistd.h>
int main()
{
Display* MainDisplay = XOpenDisplay(0);
Window RootWindow = XDefaultRootWindow(MainDisplay);
Window MainWindow = XCreateSimpleWindow(MainDisplay, RootWindow, 0, 0, 800, 600, 0, 0, 0x00aade87);
XMapWindow(MainDisplay, MainWindow);
XFlush(MainDisplay);
for(;;) { sleep(1); }
}
#include <X11/Xlib.h>
#include <X11/Xutil.h>
#include <stdio.h>
typedef struct {
int X;
int Y;
int Width;
int Height;
} entity;
int main()
{
Display* MainDisplay = XOpenDisplay(0);
Window RootWindow = XDefaultRootWindow(MainDisplay);
int DefaultScreen = DefaultScreen(MainDisplay);
GC Context = XDefaultGC(MainDisplay, DefaultScreen);
int WindowX = 0;
int WindowY = 0;
int WindowWidth = 800;
int WindowHeight = 600;
int BorderWidth = 0;
int WindowDepth = CopyFromParent;
int WindowClass = CopyFromParent;
Visual* WindowVisual = CopyFromParent;
int AttributeValueMask = CWBackPixel | CWEventMask;
XSetWindowAttributes WindowAttributes = {};
WindowAttributes.background_pixel = 0xffffccaa;
WindowAttributes.event_mask = StructureNotifyMask | KeyPressMask | KeyReleaseMask | ExposureMask;
Window MainWindow = XCreateWindow(MainDisplay, RootWindow,
WindowX, WindowY, WindowWidth, WindowHeight,
BorderWidth, WindowDepth, WindowClass, WindowVisual,
AttributeValueMask, &WindowAttributes);
XMapWindow(MainDisplay, MainWindow);
XStoreName(MainDisplay, MainWindow, "Moving rectangle. Use arrow keys to move.");
Atom WM_DELETE_WINDOW = XInternAtom(MainDisplay, "WM_DELETE_WINDOW", False);
if(!XSetWMProtocols(MainDisplay, MainWindow, &WM_DELETE_WINDOW, 1)) {
printf("Couldn't register WM_DELETE_WINDOW property \n");
}
entity Box = {};
Box.Width = 50;
Box.Height = 80;
Box.X = WindowWidth/2 - Box.Width/2;
Box.Y = WindowHeight/2 - Box.Height/2;
int StepSize = 5;
int IsWindowOpen = 1;
while(IsWindowOpen) {
XEvent GeneralEvent = {};
XNextEvent(MainDisplay, &GeneralEvent);
switch(GeneralEvent.type) {
case KeyPress:
case KeyRelease:
{
XKeyPressedEvent *Event = (XKeyPressedEvent *)&GeneralEvent;
if(Event->keycode == XKeysymToKeycode(MainDisplay, XK_Escape))
{
IsWindowOpen = 0;
}
if(Event->keycode == XKeysymToKeycode(MainDisplay, XK_Down))
{
Box.Y += StepSize;
}
else if(Event->keycode == XKeysymToKeycode(MainDisplay, XK_Up))
{
Box.Y -= StepSize;
}
else if(Event->keycode == XKeysymToKeycode(MainDisplay, XK_Right))
{
Box.X += StepSize;
}
else if(Event->keycode == XKeysymToKeycode(MainDisplay, XK_Left))
{
Box.X -= StepSize;
}
} break;
case ClientMessage:
{
XClientMessageEvent *Event = (XClientMessageEvent *) &GeneralEvent;
if((Atom)Event->data.l[0] == WM_DELETE_WINDOW) {
XDestroyWindow(MainDisplay, MainWindow);
IsWindowOpen = 0;
}
} break;
}
XClearWindow(MainDisplay, MainWindow);
XFillRectangle(MainDisplay, MainWindow, Context, Box.X, Box.Y, Box.Width, Box.Height);
}
}
窗口管理器
窗口管理器(Window manager)是在图形用户界面中,控制窗口位置与外观的软件. 许多窗口管理器是为了桌面环境编写,与桌面环境一同发布的,例如被GNOME使用的Mutter.同时也存在不少独立的窗口管理器,如Openbox、Awesome等.
linux的窗口管理器(dwm,i3wm)以及桌面环境(比如Gnome,KDE等)往往不会直接使用xlib编写界面,而是使用其他库调用xlib,比如GTK,Qt以及cairographics.org等.
窗口管理器是一个常规的X客户机.它没有任何超级用户权限;它是X服务器允许调用一组特殊api的普通用户进程
X通过拒绝客户端对这些api的访问(如果另一个客户端当前具有访问权限),确保在任何给定点上运行的窗口管理器不超过一个.第一个尝试访问这些api的客户端总是成功的.
窗口管理器通过两个X机制与它所管理的窗口进行通信:属性和事件。通信是通过X服务器进行的,而不是直接在窗口管理器和其他应用程序之间进行的。
substructure redirection
在没有窗口管理器的情况下,当一个应用程序想要对一个窗口做一些事情——移动它、调整它的大小、显示/隐藏它等等——它的请求直接由X服务器处理。但是,窗口管理器需要拦截这些请求。例如,窗口管理器可能需要知道一个新的顶层窗口已经创建并显示,以便在其周围绘制窗口装饰(例如最小化/最大化/关闭按钮)。它可能还需要知道现有的顶层窗口已被调整大小,以便重新绘制窗口装饰以重新填充.允许窗口管理器拦截此类请求的机制称为substructure redirection
假设我们有一个窗口W。如果程序M在W上注册substructure redirection,则X服务器不会执行修改W的任何直接子窗口的匹配请求。相反,X服务器将此请求重定向到程序M,程序M可以对请求执行任何操作,包括直接拒绝请求或通过、修改请求。
窗口管理器能为一个root window(是应用的top-level window的父窗口)注册substructure redirection,使得这个顶层窗口的任何直接子窗口(也就是应用的top-level窗口)发送的请求经过x server不经过修改转发到window manager,windows manager可以进行处理
reparenting
如果在没有窗口管理器的情况下运行X应用程序,则应用程序的顶层窗口将是根窗口的直接子窗口.但是,在运行窗口管理器时,应用程序的顶层窗口可能被窗口管理器重新定义;它成为由窗口管理器创建的框架窗口的子窗口,并且它本身是根窗口的直接子窗口。窗口管理器可以将其他UI元素添加到这个框架窗口中,并与应用程序的顶层窗口一起使用
Reparenting允许不同的窗口管理器绘制不同的窗口装饰,从而实现跨窗口的一致外观。然而,也有一些窗口管理器根本不reparent:这些窗口管理器称为非reparent窗口管理器。窗口管理器不希望重命名有两个原因:
-
如果窗口管理器不在顶层窗口周围绘制窗口装饰,那么它显然不需要重新表示它们。例如:xmonad、dwm。
-
合成窗口管理器并不总是需要reparent窗口
简单来说,reparent赋予了窗口管理器对其他应用程序的top-level也就是顶层窗口绘制的能力
让window manager管理了应用程序的top-level window
compositing
从窗口管理器的角度来看,top-level窗口是黑盒;它们各自管理自己的后代窗口(UI元素),可能通过GTK+或Qt这样的框架,窗口管理器无权干涉那里。
但现在,随着图形硬件计算能力增强,window manager能做的更多了
绘制应用top-level窗口下的子元素往往如果直接由应用程序管理,那么其子元素的绘制和事件处理均通过应用发送请求到X server(中间通过window manager可能做出一些修改)
让我们花点时间思考一下如何实现像上面的Shift Switcher这样的接口。当用户触发这个接口时,我们需要:
-
将每个顶层窗口及其所有后代窗口(UI元素)呈现到off-screen的内存缓冲区中,而不是直接呈现给硬件。
-
根据设计变换(旋转,扭曲等)每个缓冲区。
-
将转换后的buffer与背景和我们需要显示的任何其他浮动UI元素一起合成为最终buffer
-
创建一个覆盖窗口,覆盖整个屏幕,并隐藏所有其他窗口
-
渲染最终的buffer到覆盖的窗口
Composite扩展提供了一种机制,请求X服务器不要将特定的窗口及其后代直接呈现给硬件,而是呈现给X服务器维护的一个特殊缓冲区,这样做不需要进行常规的裁剪和重叠计算。然后,发出请求的客户端(也就是composition window manager)可以读取和使用该缓冲区
由于合成窗口管理器已经知道所有顶层窗口的大小和位置,因此在使用图形操作(例如OpenGL)合成到覆盖窗口时,只需绘制窗口装饰就很容易了,而无需创建实际的X框架窗口和reparent
另一方面,窗口管理器可能需要同时支持合成和非合成模式,以兼容较旧或不受支持的图形硬件。在这种情况下,它需要为非合成模式实现修复和框架窗口,因此使用图形操作额外实现绘制窗口装饰变得多余。这就是为什么许多其他合成窗口管理器仍然选择重命名的原因
XCB
XCB - 维基百科,自由的百科全书 (wikipedia.org)
XCB是目标在于取代Xlib,XCB 主要目标是:
- 减轻函数库的大小与复杂度;
- 可直接访问 X Window核心协议.
Wayland
Wayland 是在同一个主机上的 GUI 通信协议:新、简单、快速,不需要 setuid root 二进制
Wayland是一个通信协议,规定了显示服务器与其客户机之间的通信方式,而使用这个协议的显示服务器称为Wayland Compositor.它由Kristian Høgsberg于2008年发起,目标是用更简单的现代化视窗系统取代X Window System.Wayland协议的参考实现称为Weston,由Wayland项目组使用C语言开发
Wayland与X Window System的最大不同在于它规定由客户机自身负责窗口边框和装饰的绘制(应用程序直接修改显存?),并且客户机能够通过EGL以及一些Wayland特定的EGL扩展组件直接在显示内存中算绘自己的缓冲器.
窗口管理器简化成显示管理服务,专门负责算绘那些屏幕上的程序.这比X Window System中的窗口管理器要更简单、高效
以“鼠标点击按钮引发按钮更新动作”为例来说明一下Wayland和X server的区别
在X中:
-
内核捕获鼠标点击事件并发送给X server.
-
X server会计算该把这一事件发送给哪个窗口(事实上,窗口位置是由Compositor控制的,X server并不能够正确的计算Compositor做过特效变化之后的按钮的正确位置).
-
应用程序对此事件进行处理(将引发按钮更新动作).但是,在此之前它得向X server发送绘制请求.
-
X server接收到这条绘制请求,然后把它发给视频驱动来渲染.X还计算了更新区域,并且这条“垃圾信息”发送给了Compositor.
-
这时,Compositor知道它必须要重新合成屏幕上的一块区域.当然,这还是要向X server发送绘制请求的.
-
开始绘制.但是X server还会去做一些不必要的本职工作(窗口重叠计算、窗口剪裁计算等)
在Wayland中:
- 内核捕获鼠标点击事件并发送给Wayland Compositor.
- 由于是直接发给Wayland Compositor的,所以Wayland Compositor会正确地计算出按钮的位置.同时它会把这一事件发送给按钮所在的应用程序来处理.
- 应用程序直接渲染,无需向Wayland Compositor请求.只需在绘制完成之后向Wayland Compositor发送一条信息表明这块区域被更新了.
- Wayland Compositor收到这条信息后,立即重新合成整个桌面
实现 Wayland 显示服务器协议的显示服务器也称为 Wayland 合成器,因为它们也执行合成视窗管理器的任务.
-
Hyprland – 一个用C++ 编写的基于wlroots 的平铺Wayland 合成器,Hyprland 值得注意的功能包括动态平铺、选项卡式视窗、干净且可读的C++ 代码库以及提供视窗动画、圆角和Dual-Kawase模糊的自定义渲染器.
-
Weston – Wayland 合成器的参考实现,实现客户端装饰.
-
Sway – 平铺 Wayland 合成器和 X11 i3 视窗管理器的直接替代品.
在Windows上
win32 api
我去,这接口名把我看吐了
win32api提供了用户界面,图形,音视频,设备以及网络等等Win32 API 的编程参考 - Win32 apps | Microsoft Learn
winui
WinUI 是适用于 Windows 桌面应用和 UWP 应用的本机用户体验 (UX) 框架
使用界面使用xaml
,处逻辑处理使用c++
图形绘制接口
图形绘制与窗口管理(窗口上下文)往往是一起的,比如使用OpenGL往往需要搭配类似GLFW的窗口绘制库.
OpenGL
主页 - LearnOpenGL CN (learnopengl-cn.github.io)
DirectX
DirectX(Direct eXtension,缩写:DX)是一系列专为多媒体以及游戏开发的api.旗下包含Direct3D、Direct2D、DirectCompute等等多个不同用途的子部分,因为这一系列API皆以Direct字样开头,所以DirectX(只要把X字母替换为任何一个特定API的名字)就成为这一巨大的API系列的统称.目前最新版本为DirectX 12,随附于Windows 10操作系统之上. 之前还有GDI/GDI++(已经过时)Graphics and gaming - Win32 apps | Microsoft Learn
Direct2D 快速入门 - Win32 apps | Microsoft Learn
ID2D1Factory* pD2DFactory = NULL;
HRESULT hr = D2D1CreateFactory(
D2D1_FACTORY_TYPE_SINGLE_THREADED,
&pD2DFactory
);
// Obtain the size of the drawing area.
RECT rc;
GetClientRect(hwnd, &rc);
// Create a Direct2D render target
ID2D1HwndRenderTarget* pRT = NULL;
HRESULT hr = pD2DFactory->CreateHwndRenderTarget(
D2D1::RenderTargetProperties(),
D2D1::HwndRenderTargetProperties(
hwnd,
D2D1::SizeU(
rc.right - rc.left,
rc.bottom - rc.top)
),
&pRT
);
ID2D1SolidColorBrush* pBlackBrush = NULL;
if (SUCCEEDED(hr))
{
pRT->CreateSolidColorBrush(
D2D1::ColorF(D2D1::ColorF::Black),
&pBlackBrush
);
}
pRT->BeginDraw();
pRT->DrawRectangle(
D2D1::RectF(
rc.left + 100.0f,
rc.top + 100.0f,
rc.right - 100.0f,
rc.bottom - 100.0f),
pBlackBrush);
HRESULT hr = pRT->EndDraw();
SafeRelease(pRT);
SafeRelease(pBlackBrush);
Vulkan
跨平台图形与计算应用程序接口,号称下一代OpenGL
Home | Vulkan | Cross platform 3D Graphics
https://vulkan-tutorial.com/
void initVulkan() {
createInstance();
setupDebugMessenger();
createSurface();
pickPhysicalDevice();
createLogicalDevice();
createSwapChain();
createImageViews();
createGraphicsPipeline();
}
...
void createGraphicsPipeline() {
}
Metal
苹果上的类似OpenGL,Direct3D,兼顾图形与计算,面向底层应用程序接口.
相关应用库
- SFML
- SDL
- FLTK
- Qt
- GLFW(针对OpenGL图形库)
- raylib
- imgui
- nanoGui
- easyx
- Oxygine - 2D C++ game framework
相关资料
- How X Window Managers Work, And How To Write One (Part I) (jichu4n.com)
- Xlib 01: Creating windows from scratch with Xlib on Linux | Hereket
- tutorial (xcb.freedesktop.org)
- cairographics.org
- Metal (API) - 维基百科,自由的百科全书 (wikipedia.org)
- Vulkan - 维基百科,自由的百科全书 (wikipedia.org)
如有疑问,欢迎各位交流!
服务器配置
宝塔:宝塔服务器面板,一键全能部署及管理
云服务器:阿里云服务器
Vultr服务器: Vultr服务器
GPU服务器:Vast.ai
代码练习平台
CodeCrafters CodeCrafters