我在给一个对话框上的控件查找翻看合适的图标时,无形中看到了一个LED显示屏的图标,这里所说的LED显示屏是指由很多LED灯密集排列组成的点阵式LED屏,比如在股市交易所,公交车上,银行门口,我们经常能看到这样的滚动式显示屏。我不禁忽然想到,如果把它放在手机上显示,那效果是不是很别致呢?而且我在很久以前用 C# 模拟了这种LED显示屏的效果,因此技术上没有什么问题。不过现在,我想把它在手机上实现,而且我的想法是做成今日插件,因为相比普通的应用程序,插件更方便用户启用,禁用,可以在桌面上展示。然后,现在我要考虑使用 C++ 实现,在技术上的实现会和上一次在C#实现上略有不同,但本质原理是一致的,并且这一次实现的效果将更符合现实中我们看到的 LED 屏。这里我将会讲述一点 LED 屏在滚动时的原理和细节技巧。
首先来看,这是插件在手机上滚动显示的效果:
关于windows mobile 上的今日插件开发,这里就不认真讲解,可以参考我前几天发的文章。这里再额外提的一些内容是关于 LED 屏的滚动原理和细节,这些我们要针对的平台,开发的应用程序类型是无关的。
(1)在本范例中 Led 点阵屏幕的实现原理;
a)首先在根据要显示的内容,测量出字符串在指定DC上绘制的宽度和高度。( GetTextExtentPoint32 )
b)在内存中创建相应大小的一个设备无关的临时位图(DIB),并得到该它的位图数据地址。(CreateDIBSection)
c)在该临时位图上绘制字符串,这时临时位图成为一个白底黑字的实际大小的位图。它也是我们生成 LED 图像的基础。
d)根据每个LED灯占据的尺寸(LEDSIZE),生成一个相应大小的 LED 位图。同时得到它的位图数据地址。这个位图相比临时位图在高度和宽度上都扩大了 LEDSIZE 倍数,这张图是用于最终显示到屏幕上的位图,它将长期维持在内存中,直到用户改变显示内容。
这里,LEDSIZE 是实际LED灯的尺寸(3*3像素)加上 LED灯的间距(1像素)。因此,本例中 LEDSIZE = 4 像素;
e)根据我们得到的位图数据地址,遍历临时位图的所有像素,并根据临时位图的像素,设置LED位图的LED灯;这里实际上是一种映射,也就是把临时位图中的黑或者白像素映射到 LED位图上的 (LEDSIZE * LEDSIZE)的矩形上。
这里,注意的细节是,像素定位必须注意下面几个因素:
(i) 位图的扫描行是逆序的。也就是说,假设该图像有10行像素,那么在起始地址出现的行序是:10,9,8,7,...
(ii) 单个像素在内存中的地址从低到高依次是:Blue,Green,Red,Alpha;(假如存在Alpha通道)
(iii) 单个像素占据的字节数是 bpp/8;通常普通RGB位图也就是占据3个字节/像素。
(iv) 在行间导航的关键信息是扫描行宽度(stride);因为存在位图数据的扫描行必须以 DWORD 对齐。
f)我们设置好LED位图以后,临时位图就不需要了,这时可以清除临时位图。在绘制时,把LED位图的相应区域复制到 指定DC 的相关位置即可。可以用全局变量记录LED屏的位置,用定时器更新此位置,即实现了滚动效果。
(2)滚屏时的细节;
考虑实际的LED屏,在滚动起来时,是同样的一段字符串首尾相接的方式,在屏上连续从右向左滚动。因此为了实现的方便,我们实际上建立的逻辑模型是如下图所示:两个相同LED字符串,以一定的像素间距(MEMODISTANCE)首尾连接,我们分别用两个变量记录两个LED字符串的位置:
g_LedPos1: 第一个字符串的起始位置(LED屏幕客户区中的坐标);
g_LedPos2: 第二个字符串的起始位置(LED屏幕客户区中的坐标);
g_LedWidth: 字符串的像素宽度;(LED屏幕坐标)
MEMODISTANCE:两个字符串之间的像素距离;(LED屏幕坐标);
MEMOCOUNT:逻辑模型中首尾相接的字符串数量;(在本例中 = 2);
SCREENWIDTH:LED屏的像素宽度(LED屏幕坐标),在本例中= 60;
LED 屏幕坐标:在LED屏幕中,每个像素都是在实际位图中占据的是(4*4)尺寸;例如显示屏在该坐标体系下,是60像素宽度,但是实际位图是240像素宽度。
(a)连续滚动原理;
======================================================================================
●. 连续滚动需要满足下列条件: MEMODISTANCE >= SCREENWIDTH / MEMOCOUNT;
=======================================================================
上面的不等式用语言来描述就是:“相邻字符串 的 “首尾间距” 大于等于 LED屏幕像素宽度 / 屏幕上可同时显示的字符串最大个数”;假如我们把“字符串”看做汽车,LED屏看作“单行道”,那么这里提到的也就是对“车距”的要求。
屏幕上可同时显示的字符串个数(MEMOCOUNT)实际上就是我们的代码中在维护的逻辑模型的字符串个数,也就是说我们的代码中一共有多少个 g_LedPos 变量,每一个 g_LedPos 变量可以维护一个字符串;在本例中,我们维护了两个 g_LedPos; 这两个字符串首尾相接后成为无限长序列的一个基本单位(basic unit of the memo queue);无限长队列是从这个基本单位扩展而成。因此,MEMOCOUNT 是 这个基本单位的容量(容纳字符串的数量);
为什么我们会对“车距”有需求呢,想象下如果汽车的宽度(字符串长度)非常小,如果不控制车距,就会导致单行道上可同时容纳的汽车数量增加,而如果这个数量超过了我们代码中的模型(组成无限长字符串队列的基本单位的容量)的字符串个数,那么我们的代码就无法同时控制这么多汽车的出现位置;这将会导致帧突变,体现在视觉上就是,滚动过程中,屏幕中间本来是黑色背景,但是在下一帧忽然在中间冒出“文本内容”;
在本例中,我们的逻辑模型是两个字符串相接,因此因此我将 MEMODISTANCE 定义为 30 像素;假如不满足这个条件并且输入字符串极短(例如“1”),那么在连续滚动的过程中就可能造成帧突变,在某些时刻的帧和上一帧无法衔接,即在屏幕中的黑暗部分凭空闪现字符串内容。
(b)定时器中的处理:
由于是从右向左滚动,所以我们在定时器中只需要把 g_LedPos1, g_LedPos2 递减即可,当发现第一个字符串离开屏幕时,我们立即两个坐标的指向向后移动到它后面的那个字符串,即把 g_LedPos2 赋值给 g_LedPos1; g_LedPos2 根据 g_LedPos1 重新赋值;这样就以两个字符串为一个基本单位,向后逐一进行无穷扩展,形成无数字符串首尾相连的虚拟场景;
定时器中的代码如下所示:
(c)在绘制时的处理:
在绘制时,主要使用 BitBlt 函数来完成。但是需要注意的是,字符串的起始坐标(g_LedPos)是否在 LED 屏内部,将决定了 BitBlt 中的参数有所区别(这里我不详细分析原因)。
我使用一个辅助函数:GetBltInfo 来帮助我判断 BitBlt 函数中应该使用的参数;它可以计算出 BitBlt 中应该把 LedBitmap 中的何处 拷贝到 DC 的何处,拷贝多少宽度;相关代码如下:
(3)最重要的函数:生成LED位图(CreateLedBitmap);
这个函数的作用是:根据指定的字符串,生成 LedBitmap;并返回 LedWidth (该图片的LED屏幕坐标的像素宽度);在这个函数里,临时位图实际上在使用完毕后清除,但我为了在PC上的范例的对话框上展示它,所以没有立即清除。
如下图所示,为 4 个 LED 的放大图形,上面两个为熄灭,下面两个为点亮状态;
【注意】我们绘制的原始图像特点是“白底黑字”,因此黑色映射到LED图像的“点亮”状态,白色映射到“熄灭”状态;在下面代码中可看到这一点;
下面是 CreateLedBitmap 的代码,在这段代码中,包含了前面所讲的对“像素定位”的非常重要的方法和信息,涉及了扫描行宽度,位图中的逆序扫描,在图像数据中的像素定位,在像素中的RGB通道定位,这些都是通过指针来完成的,需要务必小心以确保指针的指向合法和正确。
(4)PC上的演示程序;
最后,本范例的代码是运行在 PDA (WINDOWS MOBILE)上的今日插件,但这里提到的LED滚屏技术是不局限平台的,因此我也做了一个在PC可以运行的对话框程序,来演示上面的代码和技术:对话框的截图如下:
(5)下载连接;
代码下载连接(包含PC端演示程序,和 WindowMobile 插件源代码):
http://files.cnblogs.com/hoodlum1980/JRL_LedScreen.rar
同时,我开发的插件也发布到 pdafans 论坛供网友测试和使用,因此也可以在 pdafans 论坛的以下帖子下载到本文提到的今日插件的最新 CAB 包,在我的博客上仅保留插件的较早版本。
一个模拟LED显示屏的桌面今日插件
by hoodlum1980 @ 2009
首先来看,这是插件在手机上滚动显示的效果:
关于windows mobile 上的今日插件开发,这里就不认真讲解,可以参考我前几天发的文章。这里再额外提的一些内容是关于 LED 屏的滚动原理和细节,这些我们要针对的平台,开发的应用程序类型是无关的。
(1)在本范例中 Led 点阵屏幕的实现原理;
a)首先在根据要显示的内容,测量出字符串在指定DC上绘制的宽度和高度。( GetTextExtentPoint32 )
b)在内存中创建相应大小的一个设备无关的临时位图(DIB),并得到该它的位图数据地址。(CreateDIBSection)
c)在该临时位图上绘制字符串,这时临时位图成为一个白底黑字的实际大小的位图。它也是我们生成 LED 图像的基础。
d)根据每个LED灯占据的尺寸(LEDSIZE),生成一个相应大小的 LED 位图。同时得到它的位图数据地址。这个位图相比临时位图在高度和宽度上都扩大了 LEDSIZE 倍数,这张图是用于最终显示到屏幕上的位图,它将长期维持在内存中,直到用户改变显示内容。
这里,LEDSIZE 是实际LED灯的尺寸(3*3像素)加上 LED灯的间距(1像素)。因此,本例中 LEDSIZE = 4 像素;
e)根据我们得到的位图数据地址,遍历临时位图的所有像素,并根据临时位图的像素,设置LED位图的LED灯;这里实际上是一种映射,也就是把临时位图中的黑或者白像素映射到 LED位图上的 (LEDSIZE * LEDSIZE)的矩形上。
这里,注意的细节是,像素定位必须注意下面几个因素:
(i) 位图的扫描行是逆序的。也就是说,假设该图像有10行像素,那么在起始地址出现的行序是:10,9,8,7,...
(ii) 单个像素在内存中的地址从低到高依次是:Blue,Green,Red,Alpha;(假如存在Alpha通道)
(iii) 单个像素占据的字节数是 bpp/8;通常普通RGB位图也就是占据3个字节/像素。
(iv) 在行间导航的关键信息是扫描行宽度(stride);因为存在位图数据的扫描行必须以 DWORD 对齐。
f)我们设置好LED位图以后,临时位图就不需要了,这时可以清除临时位图。在绘制时,把LED位图的相应区域复制到 指定DC 的相关位置即可。可以用全局变量记录LED屏的位置,用定时器更新此位置,即实现了滚动效果。
(2)滚屏时的细节;
考虑实际的LED屏,在滚动起来时,是同样的一段字符串首尾相接的方式,在屏上连续从右向左滚动。因此为了实现的方便,我们实际上建立的逻辑模型是如下图所示:两个相同LED字符串,以一定的像素间距(MEMODISTANCE)首尾连接,我们分别用两个变量记录两个LED字符串的位置:
g_LedPos1: 第一个字符串的起始位置(LED屏幕客户区中的坐标);
g_LedPos2: 第二个字符串的起始位置(LED屏幕客户区中的坐标);
g_LedWidth: 字符串的像素宽度;(LED屏幕坐标)
MEMODISTANCE:两个字符串之间的像素距离;(LED屏幕坐标);
MEMOCOUNT:逻辑模型中首尾相接的字符串数量;(在本例中 = 2);
SCREENWIDTH:LED屏的像素宽度(LED屏幕坐标),在本例中= 60;
LED 屏幕坐标:在LED屏幕中,每个像素都是在实际位图中占据的是(4*4)尺寸;例如显示屏在该坐标体系下,是60像素宽度,但是实际位图是240像素宽度。
(a)连续滚动原理;
======================================================================================
●. 连续滚动需要满足下列条件: MEMODISTANCE >= SCREENWIDTH / MEMOCOUNT;
=======================================================================
上面的不等式用语言来描述就是:“相邻字符串 的 “首尾间距” 大于等于 LED屏幕像素宽度 / 屏幕上可同时显示的字符串最大个数”;假如我们把“字符串”看做汽车,LED屏看作“单行道”,那么这里提到的也就是对“车距”的要求。
屏幕上可同时显示的字符串个数(MEMOCOUNT)实际上就是我们的代码中在维护的逻辑模型的字符串个数,也就是说我们的代码中一共有多少个 g_LedPos 变量,每一个 g_LedPos 变量可以维护一个字符串;在本例中,我们维护了两个 g_LedPos; 这两个字符串首尾相接后成为无限长序列的一个基本单位(basic unit of the memo queue);无限长队列是从这个基本单位扩展而成。因此,MEMOCOUNT 是 这个基本单位的容量(容纳字符串的数量);
为什么我们会对“车距”有需求呢,想象下如果汽车的宽度(字符串长度)非常小,如果不控制车距,就会导致单行道上可同时容纳的汽车数量增加,而如果这个数量超过了我们代码中的模型(组成无限长字符串队列的基本单位的容量)的字符串个数,那么我们的代码就无法同时控制这么多汽车的出现位置;这将会导致帧突变,体现在视觉上就是,滚动过程中,屏幕中间本来是黑色背景,但是在下一帧忽然在中间冒出“文本内容”;
在本例中,我们的逻辑模型是两个字符串相接,因此因此我将 MEMODISTANCE 定义为 30 像素;假如不满足这个条件并且输入字符串极短(例如“1”),那么在连续滚动的过程中就可能造成帧突变,在某些时刻的帧和上一帧无法衔接,即在屏幕中的黑暗部分凭空闪现字符串内容。
(b)定时器中的处理:
由于是从右向左滚动,所以我们在定时器中只需要把 g_LedPos1, g_LedPos2 递减即可,当发现第一个字符串离开屏幕时,我们立即两个坐标的指向向后移动到它后面的那个字符串,即把 g_LedPos2 赋值给 g_LedPos1; g_LedPos2 根据 g_LedPos1 重新赋值;这样就以两个字符串为一个基本单位,向后逐一进行无穷扩展,形成无数字符串首尾相连的虚拟场景;
定时器中的代码如下所示:
case
WM_TIMER:
g_LedPos1 -- ;
g_LedPos2 -- ;
// 第一条已经完全离开屏幕左侧了吗?如果是,则向后更替!
if (g_LedPos1 + g_LedWidth < 0 )
{
g_LedPos1 = g_LedPos2;
g_LedPos2 = g_LedPos1 + g_LedWidth + MEMODISTANCE;
}
InvalidateRect(hwnd, & rcLed, FALSE);
break ;
g_LedPos1 -- ;
g_LedPos2 -- ;
// 第一条已经完全离开屏幕左侧了吗?如果是,则向后更替!
if (g_LedPos1 + g_LedWidth < 0 )
{
g_LedPos1 = g_LedPos2;
g_LedPos2 = g_LedPos1 + g_LedWidth + MEMODISTANCE;
}
InvalidateRect(hwnd, & rcLed, FALSE);
break ;
(c)在绘制时的处理:
在绘制时,主要使用 BitBlt 函数来完成。但是需要注意的是,字符串的起始坐标(g_LedPos)是否在 LED 屏内部,将决定了 BitBlt 中的参数有所区别(这里我不详细分析原因)。
我使用一个辅助函数:GetBltInfo 来帮助我判断 BitBlt 函数中应该使用的参数;它可以计算出 BitBlt 中应该把 LedBitmap 中的何处 拷贝到 DC 的何处,拷贝多少宽度;相关代码如下:
Code
(3)最重要的函数:生成LED位图(CreateLedBitmap);
这个函数的作用是:根据指定的字符串,生成 LedBitmap;并返回 LedWidth (该图片的LED屏幕坐标的像素宽度);在这个函数里,临时位图实际上在使用完毕后清除,但我为了在PC上的范例的对话框上展示它,所以没有立即清除。
如下图所示,为 4 个 LED 的放大图形,上面两个为熄灭,下面两个为点亮状态;
【注意】我们绘制的原始图像特点是“白底黑字”,因此黑色映射到LED图像的“点亮”状态,白色映射到“熄灭”状态;在下面代码中可看到这一点;
下面是 CreateLedBitmap 的代码,在这段代码中,包含了前面所讲的对“像素定位”的非常重要的方法和信息,涉及了扫描行宽度,位图中的逆序扫描,在图像数据中的像素定位,在像素中的RGB通道定位,这些都是通过指针来完成的,需要务必小心以确保指针的指向合法和正确。
CreateLedBitmap
(4)PC上的演示程序;
最后,本范例的代码是运行在 PDA (WINDOWS MOBILE)上的今日插件,但这里提到的LED滚屏技术是不局限平台的,因此我也做了一个在PC可以运行的对话框程序,来演示上面的代码和技术:对话框的截图如下:
(5)下载连接;
代码下载连接(包含PC端演示程序,和 WindowMobile 插件源代码):
http://files.cnblogs.com/hoodlum1980/JRL_LedScreen.rar
同时,我开发的插件也发布到 pdafans 论坛供网友测试和使用,因此也可以在 pdafans 论坛的以下帖子下载到本文提到的今日插件的最新 CAB 包,在我的博客上仅保留插件的较早版本。
一个模拟LED显示屏的桌面今日插件
by hoodlum1980 @ 2009