【SFML】窗口管理

SFML 是一个使用 C++ 编写的多媒体库,包括系统、窗口、图形、音频、网络五个模块,适用于 Windows, Linux, MacOS 等主流 PC 平台,未来还可能扩展到手机平台。SFML 用于快速构建 PC 窗口程序,很适合于开发游戏及多媒体应用,同时它本身也是一个 OpenGL 工具库,可以直接应用于 OpenGL 程序的窗口管理。

简介

SFML 的全称是 Simple and Fast Multimedia Library,即简单快速的多媒体库,顾名思义就是一个用于开发多媒体应用的库,包含 系统、窗口、图形、音频、网络 五个模块。SFML 为 PC 各个组件提供简单的接口,简化游戏及多媒体应用的开发,目前适用于 PC 的多个平台,包括 Windows,Linux,MacOS,未来可能会拓展到 Android 和 IOS,SFML 是使用 C++ 编写的库,同时官方也绑定了 C,Java,Pyhton,.Net,Go 等多种语言。另外,SFML 内嵌了 OpenGL 模块,很容易用来开发 OpenGL 程序,OpenGL 只是一个底层的图形库,本身没有窗口管理模块,所以需要借助第三方库,常用的库有 glut(freeglut), glfw, qt, sfml,而 SFML 除了窗口管理,还有系统图形、音频、网络模块,具备了一个完整多媒体应用所需的各个模块,无疑是一个比较好的选择,当然 QT 也是。

SFML 官网地址:https://www.sfml-dev.org, 官方有各个操作系统和各个版本的下载地址,以及 github 源码,以及清晰明了的 API 文档和教程。

环境配置

本文基于 Windows 系统,C++ 语言,Visual Studio 2019 IDE。

SFML 包括 system, window, graphics, audio, network 5 个模块,对应的 Windows 编译库就有下面 5 个库:

  • sfml-system.lib:系统库,最基础的库,是其它库的基础依赖;
  • sfml-window.lib:窗口管理库,依赖于 sfml-system.lib
  • sfml-graphics.lib:图形库,依赖于 sfml-system.libsfml-window.lib
  • sfml-audio.lib:音频库,依赖于 sfml-system.lib
  • sfml-network.lib:网络库,依赖于 sfml-system.lib

和 Windows 上的其它库一样,这五个库都区分 32 位和 64 位,debug 版本和 release 版本,动态库和静态库,以系统库为例,32 位版本和 64 位版本各有 sfml-system.lib, sfml-system-d.lib, sfml-system-s.lib, sfml-system-s-d.lib 这 4 个 lib 文件,另外还有 sfml-system.dll, sfml-system-d.dll 这 2 个 dll 文件,所以一个库就有 (4 + 2) * 2 = 12 个文件,5 个库就是 60 个文件,emm,是有点多。

可以看到所有库都依赖于系统库,图形库额外还要依赖于窗口库;如果只是想利用 SFML 来创建和管理窗口,则引用 sfml-system, sfml-window 这两个库,如果要使用其内置接口来绘制图形,则再加个 sfml-graphics 库;如果只是单纯的播放音频,则引入 sfml-system, sfml-audio 这两个库,只是单纯是使用网络功能,则引入 sfml-system, sfml-network 这两个库,当然如果使用音频或网络的时候是个 GUI 应用(非控制台应用),那 sfml-window 也是必不可少的。当然除了这五个库,还有其它基础库需要引入,gdi32.lib, opengl32.lib, ogg.lib,不过这些库 Visual Studio 都会自动帮我们引入,所以不用管,详细的可看官方文档 SFML and Visual Studio,关于 Visual Studio 配置的详细内容,也可看我这篇文章

创建窗口

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <SFML/Window.hpp>

int main()
{
// sf::Window window(sf::VideoMode(800, 600), "My window");

sf:Window window;
window.create(sf::VideoMode(800, 600), "My window");

//...

return 0;
}

首先包含两个库 sfml-system.libsfml-window.lib,引入头文件 <sfml/window.hpp>。使用 sf::Window 类来创建一个窗口,有两种方式,一是在构造函数中直接传入参数,二是先创建一个无数据的对象,再调用 create 静态方法来初始化参数,二者的效果是一样的。窗口接收 4 个参数,

  • arg1 的类型为 sf::VideoMode,表示窗口的显示模式;
  • arg2 为窗口的标题;
  • arg3 为窗口样式,不传则为默认值 sf::Style::Default
  • arg4 为 OpenGL 的具体选项,不传则为空。

VideoMode

VideoMode 包括三个属性 width, height, bitsPerPixel,分别表示窗口的宽度、高度和深度(每个像素多少 bit),深度不传的话则默认为 32,宽高则必须传。

VideoMode 主要用途是“全屏模式”,它提供一个非常实用的静态方法 getFullscreenModes,用于获取当前操作系统下可用的全屏显示模式列表(显卡和显示器是否支持),只有使用这个列表里的显示模式,才能正常显示一个全屏窗口。我们玩主机游戏的时候会有一个选择分辨率的窗口,上面列出的分辨率不是随便列的,而是当前系统支持的显示模式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int main()
{
std::vector<sf::VideoMode> modes = sf::VideoMode::getFullscreenModes();
for (std::size_t i = 0; i < modes.size(); ++i)
{
sf::VideoMode mode = modes[i];
std::cout << "Mode #" << i << ": "
<< mode.width << "x" << mode.height << " - "
<< mode.bitsPerPixel << " bpp" << std::endl;
}
sf::Window window2(modes[0], "Window2", sf::Style::Fullscreen);
//...
return 0;
}

上面例子打印出所有可用的显示模式,并使用其中一个显示模式创建一个全屏窗口。

VideoMode 还提供另一个静态方法 getDesktopMode,用于获取当前桌面使用的显示模式,这样就能拿到桌面的分辨率和显示深度,从而创建一个和桌面一样分辨率和显示深度的窗口或其它操作。

窗口样式

SFML 提供五种窗口样式,有些样式可以组合,有些则与其它样式互斥。

  • sfml::Style::None:无任何装饰,不能与其它样式组合,在一些特殊场合很有用,比如启动界面;
  • sfml::Style:Fullscreen:全屏样式,全屏显示且无任何装饰,不能与其它样式组合,必须使用系统支持的显示模式;
  • sfml::Style::Titlebar:带一个标题栏,可与其它样式组合使用;
  • sfml::Style:Resize:可缩放且提供最大化按钮,可与其它样式组合使用;
  • sfml:Style:Close:可关闭,提供关闭按钮,可与其它样式组合使用。

默认为第六种样式 sfml:Style:Default,它是后面三种样式的组合,即 sfml::Style::Titlebar | sfml::Style::Resize | sfml::Style::Close,有标题栏、最大化按钮和关闭按钮,可调整大小和关闭,统称为窗口样式;因此,SFML 的窗口样式可大致分为三种,无样式、全屏样式和窗口样式。

OpenGL 上下文

创建一个窗口完整的代码如下,

1
2
sf::ContextSettings settings;
sf::Window window(sf::VideoMode(400, 400), "Hello SFML", sf::Style::Default, settings);

VideoModeStyle 分别表示显示模式和窗口样式,而最后一个参数 sf::ContextSettings 则是专门为 OpenGL 定制的,用于定义 OpenGL context 的配置,比如 OpenGL 主版本、次版本、深度缓冲区的位数、模块缓冲区的位数、抗锯齿等级,具体的这里先不说,后面讲到 OpenGL 的时候再详细讲,现在只需要知道这个参数可省略就行。

主循环

上面的代码只创建一个“死窗口”,首先在 main 函数创建完窗口之后马上就结束了,所以你会看到一个窗口闪一下就没了,然后程序就退出了,所以需要在创建窗口之后让程序停在某个地方而不是直接退出,即定义一个程序主循环。光让程序停住还不行,这时窗口虽然看到了,但还是个“死窗口”,无法移动、关闭、缩放,因为没有把窗口加入到程序主循环中去。把窗口加入到主循环,其实就是实时监听并处理窗口事件,对窗口的所有操作都是基于事件。

程序主循环最容易想到的就是在 main 方法中加入一个死循环 while(true){ ... },唯一要处理的就是什么时候跳出循环,这里我们关闭窗口的时候退出程序,自然是窗口关闭的时候退出主循环,所以主循环的写法为 while(window.isOpen()){ ... },而循环内则处理窗口事件。创建一个“活窗口”的完整代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <SFML/Window.hpp>

int main()
{
sf::Window window(sf::VideoMode(800, 600), "My window");

while (window.isOpen())
{
sf::Event event;
while (window.pollEvent(event))
{
if (event.type == sf::Event::Closed)
window.close();
}
}

return 0;
}

窗口操作

SFML 的窗口只是提供一个 OpenGL 上下文环境或者 SFML 内部绘制的环境,不具体其它专用的 GUI 库提供的高级功能,但也能做一些基础的操作,比如设置窗口位置、大小、标题、图标等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <SFML/Graphics.hpp>
int main()
{
sf::Window window(sf::VideoMode(400, 400), "Hello SFML");
sf::Image image;
if (image.loadFromFile("../Res/title.png"))
{
auto size = image.getSize();
window.setIcon(size.x, size.y, image.getPixelsPtr());
}
window.setTitle("SFML Window");
window.setPosition(sf::Vector2i(100, 100));
window.setSize(sf::Vector2u(960, 640));
//...
}

设置窗口图标需要使用 Image 对象来加载图片,所以需要引用图形库,即需要 sfml-system.lib, sfml-window.lib, sfml-graphics.lib 三个库,头文件则导入 SFML/Graphics.hpp> 即可。

如果要对窗口做更高级的操作,可以使用其它 GUI 库来创建窗口,然后将 SFML 的绘制环境嵌入进去,这种方式需要第三方创建的窗口返回一个特定操作系统的句柄,然后以这外句柄为参数来创建 SFML 窗口,SFML 窗口可以捕捉到第三方库创建的父窗口的事件,而且不影响父窗口的管理。

1
2
sf::WindowHandle handle = /* specific to what you're doing and the library you're using */;
sf::Window window(handle);

反过来,也可以使用 SFML 来创建窗口,然后返回一个特定操作系统的句柄,再由其它库操作这个句柄从而实现更高级的窗口操作。

1
2
3
sf::Window window(sf::VideoMode(800, 600), "SFML window");
sf::WindowHandle handle = window.getSystemHandle();
// you can now use the handle with OS specific functions

帧率

SFML 有两种设置帧率的方法,第一种是开启显示器垂直同步,第二种是手动设置最大帧率,两种方式选其一,不能同时使用,即不能开启显示器垂直同步的同时又手动设置最大帧率。

通过接口 setVerticalSyncEnabled 开启显示器垂直同步,可解决一些视觉问题,比如撕裂,当应用的刷新频率与显示器不同步时,显示器的上方可能会显示上一帧的内容,而下方显示下一帧的内容,这就出现的画面撕裂的情况;要注意的是这个可能会无效,原因是显示器把垂直同步关了,把显示器设置中的 vertical synchronizationoff 改为 controlled by application 即可。

1
window.setVerticalSyncEnabled(true); // call it once, after creating the window

通过接口 setFramerateLimit 手动设置最大帧率,这是 SFML 内部使用 sf::Clocksf:sleep 实现的,所以不是完全可靠的,特别是高帧率,其取决于特定操作系统和底层硬件,不要用此来实现精准计时。

1
window.setFramerateLimit(60); // call it once, after creating the window

注意事项

  • SFML 支持一个程序创建多个窗口,每个窗口可以运行在单独的线程上,也可以都运行在主线程上(在 MacOS 上,所以窗口必须运行在主线程上);
  • SFML 暂不支持多显示器管理,即多个显示器的时候,你无法决定窗口默认显示在哪个显示器上;
  • 窗口比桌面大可能会显示不正常,当然一般也不会创建比桌面还大的窗口,但要注意的是使用 VideoMode:getDesktopMode() 获取的显示模式,再加上边界和标题格栏等装饰物,创建出来的窗口是比桌面大的;
  • 窗口的事件处理必须和窗口在同一线程,如果实在想分离一些东西出来,可以把渲染、物理、逻辑等分离到其它线程,但窗口管理和该窗口的事件处理必须在同一线程。