3、Screen API基础:Screen库的初始化与销毁、连接QNX图形服务器、获取屏幕信息

好,我们正式开始接触Screen API。说实话,很多刚接触QNX图形开发的朋友,第一反应就是——这玩意儿怎么跟Linux那套完全不一样?嗯,确实不一样。Screen是QNX原生的图形框架,它不依赖X11,也不走Wayland。你想想看,在嵌入式场景下,我们需要的是轻量、实时、可控。Screen就是为这个而生的。

我个人习惯,学一个新API,先搞清楚三件事:怎么连上去、怎么拿信息、怎么断开。今天我们就围绕这三步展开。

3.1 Screen库的初始化

初始化Screen库,说白了就是告诉系统:“嘿,我要用图形功能了。” 这一步很简单,但也很关键。你如果忘了初始化,后面所有调用都会返回错误。

核心函数就一个:screen_init()。它的原型长这样:

#include <screen/screen.h>

int screen_init(screen_context_t *ctx, const char *name);

参数说明:

  • ctx:输出参数,初始化成功后,你会得到一个上下文句柄。这个句柄后面所有操作都要用到。
  • name:应用名称。可以传NULL,但我建议你传一个有意义的名字。为什么?因为QNX的图形服务器会用这个名字来标识你的应用,调试时一眼就能认出来。

返回值:成功返回0,失败返回-1。嗯,这里要注意,很多新手会忽略检查返回值。我曾经在项目里见过一个同事,初始化失败后直接往下走,结果后面所有screen调用都返回奇怪错误,排查了半天才发现是初始化没成功。

警告:screen_init() 必须在任何其他Screen API调用之前执行。而且,每个进程只能调用一次。重复调用会导致未定义行为。

来看一个完整的初始化示例:

screen_context_t screen_ctx;
int rc;

rc = screen_init(&screen_ctx, "my_app");
if (rc != 0) {
    fprintf(stderr, "screen_init failed: %s\n", strerror(errno));
    return -1;
}

printf("Screen initialized successfully.\n");

你看,就这么几行。但这里有个细节——screen_context_t 其实是一个不透明指针。你不需要关心它内部是什么结构,只需要知道它是你连接图形服务器的“钥匙”就行。

3.2 连接QNX图形服务器

初始化之后,下一步就是连接图形服务器。其实,screen_init() 内部已经帮你做了连接工作。它会在初始化时自动与QNX的图形服务器(Screen Server)建立通信通道。

但这里有个概念我想强调一下:Screen的架构是典型的客户端-服务器模型。你的应用是客户端,Screen Server是服务端。所有绘图操作、窗口管理、事件处理,都是通过这个通道来完成的。

我记得第一次接触这个模型时,总觉得有点绕。后来想通了——你想想看,如果每个应用都直接操作显存,那不乱套了?所以必须有个中央调度者,也就是Screen Server。

连接成功后,你可以通过 screen_get_context_property_iv() 来获取一些全局信息,比如当前连接的服务器版本:

int version;
screen_get_context_property_iv(screen_ctx, SCREEN_PROPERTY_VERSION, &version);
printf("Screen server version: %d\n", version);

这个版本号很有用。我在项目中遇到过,不同版本的Screen API在行为上有细微差异。比如早期版本不支持某些窗口特效,如果你没检查版本就直接用,程序可能会崩溃。

小技巧:建议在应用启动时打印一下Screen版本号,方便后续排查问题。特别是当你需要兼容多个QNX版本时,这个信息能帮你快速定位问题。

3.3 获取屏幕信息

连接上服务器后,我们最常做的事就是获取屏幕信息。比如分辨率、像素格式、刷新率等等。这些信息都存储在 screen_display_t 对象中。

获取屏幕信息的流程分三步:

  1. 获取当前可用的显示器列表
  2. 选择你要操作的显示器(通常是第一个)
  3. 读取该显示器的属性

来看代码:

int display_count;
screen_display_t *displays;

// 第一步:获取显示器数量
screen_get_context_property_iv(screen_ctx, SCREEN_PROPERTY_DISPLAY_COUNT, &display_count);
printf("Number of displays: %d\n", display_count);

// 第二步:分配空间并获取显示器列表
displays = calloc(display_count, sizeof(screen_display_t));
screen_get_context_property_pv(screen_ctx, SCREEN_PROPERTY_DISPLAYS, (void**)displays);

// 第三步:读取第一个显示器的信息
int width, height, format;
screen_get_display_property_iv(displays[0], SCREEN_PROPERTY_WIDTH, &width);
screen_get_display_property_iv(displays[0], SCREEN_PROPERTY_HEIGHT, &height);
screen_get_display_property_iv(displays[0], SCREEN_PROPERTY_FORMAT, &format);

printf("Display 0: %dx%d, format=%d\n", width, height, format);

这里有几个关键点:

  • SCREEN_PROPERTY_DISPLAY_COUNT 返回的是物理显示器的数量。在嵌入式设备上,通常只有一个。但有些设备(比如车载仪表盘)可能有多个屏幕。
  • SCREEN_PROPERTY_DISPLAYS 返回的是 screen_display_t 指针数组。注意,这里用的是 _pv 后缀,因为返回的是指针类型。
  • 像素格式 SCREEN_PROPERTY_FORMAT 返回的是一个枚举值,比如 SCREEN_FORMAT_RGBX8888SCREEN_FORMAT_RGBA8888。这个信息在后面创建窗口和缓冲区时非常重要。
核心要点:获取屏幕信息时,一定要检查返回值。我曾经在调试一个双屏系统时,发现第二个屏幕的宽高始终为0,排查了半天才发现是物理连接松动。代码里没有检查返回值,导致我一直以为是软件问题。

除了宽高和格式,你还可以获取很多其他属性。我整理了一个常用属性表:

属性常量 类型 说明
SCREEN_PROPERTY_WIDTH int 屏幕宽度(像素)
SCREEN_PROPERTY_HEIGHT int 屏幕高度(像素)
SCREEN_PROPERTY_FORMAT int 像素格式枚举值
SCREEN_PROPERTY_PHYSICAL_WIDTH int 物理宽度(毫米)
SCREEN_PROPERTY_PHYSICAL_HEIGHT int 物理高度(毫米)
SCREEN_PROPERTY_REFRESH_RATE int 刷新率(毫赫兹,除以1000得到Hz)
SCREEN_PROPERTY_ROTATION int 当前旋转角度(0/90/180/270)

嗯,这里要注意 SCREEN_PROPERTY_REFRESH_RATE 的单位是毫赫兹。也就是说,如果返回60000,实际刷新率是60Hz。我第一次用的时候没注意单位,直接当Hz用了,结果计算出来的帧间隔完全不对。

3.4 销毁与清理

有初始化就得有销毁。Screen库的清理工作很简单,就一个函数:

screen_destroy(screen_ctx);

调用 screen_destroy() 后,Screen库会释放所有内部资源,断开与图形服务器的连接。之后你就不能再使用任何Screen API了,除非重新调用 screen_init()

这里有个常见的坑:如果你在初始化后创建了窗口、缓冲区等资源,screen_destroy() 并不会自动销毁它们。你需要先手动销毁这些子资源,再调用 screen_destroy()。否则,可能会造成资源泄漏。

注意:正确的销毁顺序是:先销毁窗口和缓冲区,再销毁Screen上下文。我曾经见过一个项目,因为销毁顺序反了,导致图形服务器端的内存泄漏,系统运行几天后变得异常卡顿。

一个完整的生命周期示例:

screen_context_t screen_ctx;

// 初始化
if (screen_init(&screen_ctx, "my_app") != 0) {
    // 处理错误
}

// ... 创建窗口、缓冲区,进行绘图操作 ...

// 清理:先销毁子资源
// screen_destroy_window(...);
// screen_destroy_buffer(...);

// 最后销毁上下文
screen_destroy(screen_ctx);

好了,这一章的内容就到这里。我们走完了Screen API的“连接-获取信息-断开”全流程。下一章,我们会深入窗口的创建与管理。到时候你会看到,今天学的这些基础操作,是后面所有高级功能的地基。