考虑实际的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;
(c)在绘制时的处理:
在绘制时,主要使用 BitBlt 函数来完成。但是需要注意的是,字符串的起始坐标(g_LedPos)是否在 LED 屏内部,将决定了 BitBlt 中的参数有所区别(这里我不详细分析原因)。
我使用一个辅助函数:GetBltInfo 来帮助我判断 BitBlt 函数中应该使用的参数;它可以计算出 BitBlt 中应该把 LedBitmap 中的何处 拷贝到 DC 的何处,拷贝多少宽度;相关代码如下:
//根据当前位置,决定贴图的相对位置
void GetBltInfo(int pos, int ledWidth, RECT* lpRcLed, int* lpDestX, int* lpSrcX, int* lpWidth)
{
//头部在显示区
if(pos>=0)
{
*lpDestX = pos * LEDSIZE;
*lpSrcX = 0;
*lpWidth = min(lpRcLed->right, (pos + ledWidth)*LEDSIZE) - *lpDestX;
}
else
{
*lpDestX = 0;
*lpSrcX = -pos*LEDSIZE;
*lpWidth = min(lpRcLed->right, (pos + ledWidth)*LEDSIZE);
}
}
//========================================
//这里是窗口函数中对 WM_PAINT 的处理:
//========================================
case WM_PAINT:
{
PAINTSTRUCT ps;
RECT rc;
//===============================================
// NOW WE START TO GET DC !
//===============================================
hDC = BeginPaint(hwnd, &ps);
HDC hMemDC = CreateCompatibleDC(hDC);
HBITMAP hOldBm = (HBITMAP)SelectObject(hMemDC, g_BitmapLed);
//贴第一个字符串
int destX, srcX, width;
GetBltInfo(g_LedPos1, g_LedWidth, &rcLed, &destX, &srcX, &width);
BitBlt(hDC,destX,rcLed.top, width, (rcLed.bottom - rcLed.top),
hMemDC,srcX, 0,
SRCCOPY);
//贴第二个字符串
if(g_LedPos2 * LEDSIZE <= rcLed.right)
{
GetBltInfo(g_LedPos2, g_LedWidth, &rcLed, &destX, &srcX, &width);
BitBlt(hDC,destX,rcLed.top, width, (rcLed.bottom - rcLed.top),
hMemDC,srcX, 0,
SRCCOPY);
}
SelectObject(hMemDC, hOldBm);
DeleteDC(hMemDC);
EndPaint(hwnd, &ps);
}
break;